k5kit
Version:
Utilities for TypeScript and Svelte
374 lines (373 loc) • 13.1 kB
JavaScript
import { check_modifiers, check_shortcut } from './shortcuts.ts';
export class KSelection {
/** Currently selected items. Disallowing assignment
* prevents the Svelte store from getting out of sync */
items = new Set();
/** Full list of items that can be selected */
all;
/** The last added index */
last_added = null;
/** An anchor index for shift selection. */
shift_anchor = null;
/** Whether the user is current mouseup is a click or a selection update */
possible_row_click = false;
scroll_to;
on_contextmenu;
constructor(all_items, options) {
this.all = all_items;
this.scroll_to = options.scroll_to;
this.on_contextmenu = options.on_contextmenu;
}
clear() {
this.items.clear();
this.last_added = null;
this.shift_anchor = null;
}
/** Update the list of items that can be selected.
* Items that no longer exist are de-selected. */
update_all_items(all) {
this.all = all;
const keep = new Set(all.filter((item) => this.items.has(item)));
for (const item of this.items) {
if (!keep.has(item)) {
this.items.delete(item);
}
}
if (this.last_added !== null) {
if (this.items.has(this.last_added.item)) {
const index = this.all.indexOf(this.last_added.item);
this.last_added = { index, item: this.last_added.item };
}
else {
this.last_added = null;
}
}
if (this.shift_anchor !== null && !this.items.has(this.shift_anchor.item)) {
if (this.items.has(this.shift_anchor.item)) {
const index = this.all.indexOf(this.shift_anchor.item);
this.shift_anchor = { index, item: this.shift_anchor.item };
}
else {
this.shift_anchor = null;
}
}
}
/** Get first selected index, or `null` if selection is empty */
find_first_index() {
const item_i = this.all.findIndex((item) => this.items.has(item));
if (item_i === -1) {
return null;
}
return item_i;
}
/** Get first selected item, or `undefined` if selection is empty */
find_first() {
const item_i = this.all.find((item) => this.items.has(item));
return item_i;
}
items_as_array() {
return this.all.filter((item) => this.items.has(item));
}
get_selected_indexes() {
const indexes = [];
for (let i = 0; i < this.all.length; i++) {
if (this.items.has(this.all[i])) {
indexes.push(i);
}
}
return indexes;
}
#get_shift_anchor() {
if (this.shift_anchor !== null)
return this.shift_anchor;
else
return this.last_added;
}
add_index(index) {
if (index >= 0 && index < this.all.length) {
this.items.add(this.all[index]);
this.last_added = { index, item: this.all[index] };
this.shift_anchor = null;
}
}
add_index_unchecked(index) {
this.items.add(this.all[index]);
this.last_added = { index, item: this.all[index] };
this.shift_anchor = null;
}
#add_index_in_shift_mode(index) {
this.items.add(this.all[index]);
this.last_added = { index, item: this.all[index] };
}
#add_index_range_in_shift_mode(from_index, to_index) {
// Direction here determines this.last_added
if (from_index < to_index) {
for (let i = from_index; i <= to_index; i++) {
this.#add_index_in_shift_mode(i);
}
}
else {
for (let i = from_index; i >= to_index; i--) {
this.#add_index_in_shift_mode(i);
}
}
}
#remove_range_in_shift_mode(from_i, to_i) {
if (from_i < to_i) {
for (let i = from_i; i <= to_i; i++) {
this.items.delete(this.all[i]);
}
}
else {
for (let i = from_i; i >= to_i; i--) {
this.items.delete(this.all[i]);
}
}
}
/** Shift-select to index */
shift_select_to(to_index) {
const anchor = this.#get_shift_anchor();
const last_added = this.last_added;
if (last_added === null || anchor === null) {
return this.items;
}
if (anchor.index < to_index) {
if (to_index < last_added.index) {
// Retract selection closer to anchor
this.#remove_range_in_shift_mode(to_index + 1, last_added.index);
}
else if (last_added.index < anchor.index) {
// New shift selection is on the other side of anchor
this.#remove_range_in_shift_mode(anchor.index - 1, last_added.index);
this.#add_index_range_in_shift_mode(anchor.index, to_index);
}
else {
this.#add_index_range_in_shift_mode(last_added.index, to_index);
}
this.last_added = { index: to_index, item: this.all[to_index] };
}
else {
if (to_index > last_added.index) {
// Retract selection closer to anchor
this.#remove_range_in_shift_mode(to_index - 1, last_added.index);
}
else if (last_added.index > anchor.index) {
// New shift selection is on the other side of anchor
this.#remove_range_in_shift_mode(anchor.index + 1, last_added.index);
this.#add_index_range_in_shift_mode(anchor.index, to_index);
}
else {
this.#add_index_range_in_shift_mode(last_added.index, to_index);
}
this.last_added = { index: to_index, item: this.all[to_index] };
}
this.shift_anchor = anchor;
}
/** Replace selection with the previous index, like perssing `ArrowUp` in a list. */
go_backward() {
if (this.all.length === 0) {
return;
}
else if (this.items.size === 0) {
this.add_index_unchecked(this.all.length - 1);
}
else if (this.last_added !== null) {
const prev_index = this.last_added.index - 1;
this.clear();
this.add_index_unchecked(Math.max(prev_index, 0));
}
}
/** Replace selection with the previous index, like perssing `ArrowDown` in a list. */
go_forward() {
if (this.all.length === 0) {
return;
}
else if (this.items.size === 0) {
this.add_index_unchecked(0);
}
else if (this.last_added !== null) {
const next_index = this.last_added.index + 1;
this.clear();
this.add_index_unchecked(Math.min(next_index, this.all.length - 1));
}
}
/** Expand or shrink selection backwards (shift+up) */
shift_select_backward() {
const anchor = this.#get_shift_anchor();
this.shift_anchor = anchor;
if (anchor === null || this.last_added === null) {
return;
}
if (this.last_added.index <= anchor.index) {
// add prev to selection
for (let i = this.last_added.index; i >= 0; i--) {
if (!this.items.has(this.all[i])) {
this.#add_index_in_shift_mode(i);
return;
}
}
}
else {
// remove first from selection
this.items.delete(this.last_added.item);
this.last_added = {
index: this.last_added.index - 1,
item: this.all[this.last_added.index - 1],
};
}
}
/** Expand or shrink selection forwards (shift+down) */
shift_select_forward() {
const anchor = this.#get_shift_anchor();
this.shift_anchor = anchor;
if (anchor === null || this.last_added === null) {
return;
}
if (this.last_added.index >= anchor.index) {
// add next to selection
for (let i = this.last_added.index; i < this.all.length; i++) {
if (!this.items.has(this.all[i])) {
this.#add_index_in_shift_mode(i);
return;
}
}
}
else {
// remove last from selection
this.items.delete(this.last_added.item);
this.last_added = {
index: this.last_added.index + 1,
item: this.all[this.last_added.index + 1],
};
}
}
#toggle(index) {
if (this.items.has(this.all[index])) {
if (this.last_added && this.last_added.item === this.all[index]) {
this.last_added = null;
}
this.items.delete(this.all[index]);
}
else {
this.add_index_unchecked(index);
}
this.shift_anchor = null;
}
mouse_down_select(e, index) {
const is_selected = this.items.has(this.all[index]);
if (check_modifiers(e) && !is_selected) {
this.clear();
this.add_index_unchecked(index);
}
else if (check_modifiers(e, 'cmdOrCtrl') && !is_selected) {
this.add_index_unchecked(index);
}
else if (check_modifiers(e, 'shift')) {
this.shift_select_to(index);
}
}
/** This does also handle keydown events, which aren't row events */
handle_events(e, index) {
if (e instanceof MouseEvent) {
if (e.type === 'mousedown') {
this.handle_mouse_down(e, index);
}
else if (e.type === 'contextmenu') {
this.handle_contextmenu(e, index);
}
else if (e.type === 'click') {
this.handle_click(e, index);
}
}
else if (e instanceof KeyboardEvent) {
if (e.type === 'keydown') {
this.handle_keydown(e);
}
}
}
handle_mouse_down(e, index) {
if (e.button !== 0) {
return;
}
if (this.items.has(this.all[index])) {
this.possible_row_click = true;
}
this.mouse_down_select(e, index);
}
handle_contextmenu(e, index) {
this.mouse_down_select(e, index);
this.on_contextmenu?.(this.items);
}
handle_click(e, index) {
if (this.possible_row_click && e.button === 0) {
if (check_modifiers(e)) {
this.clear();
this.add_index_unchecked(index);
}
else if (check_modifiers(e, 'cmdOrCtrl')) {
this.#toggle(index);
}
}
this.possible_row_click = false;
}
handle_keydown(e) {
if (check_shortcut(e, 'Escape')) {
this.clear();
}
else if (check_shortcut(e, 'A', 'cmdOrCtrl')) {
if (this.all.length > 1) {
this.#add_index_range_in_shift_mode(0, this.all.length - 1);
}
}
else if (check_shortcut(e, 'ArrowUp')) {
this.go_backward();
if (this.last_added)
this.scroll_to({ ...this.last_added });
}
else if (check_shortcut(e, 'ArrowUp', 'shift')) {
this.shift_select_backward();
if (this.last_added)
this.scroll_to({ ...this.last_added });
}
else if (check_shortcut(e, 'ArrowUp', 'alt')) {
this.clear();
if (this.all.length > 1) {
this.add_index_unchecked(0);
if (this.last_added)
this.scroll_to({ ...this.last_added });
}
}
else if (check_shortcut(e, 'ArrowUp', 'shift', 'alt')) {
this.shift_select_to(0);
if (this.last_added)
this.scroll_to({ ...this.last_added });
}
else if (check_shortcut(e, 'ArrowDown')) {
this.go_forward();
if (this.last_added)
this.scroll_to({ ...this.last_added });
}
else if (check_shortcut(e, 'ArrowDown', 'shift')) {
this.shift_select_forward();
if (this.last_added)
this.scroll_to({ ...this.last_added });
}
else if (check_shortcut(e, 'ArrowDown', 'alt')) {
this.clear();
if (this.all.length > 1) {
this.add_index_unchecked(this.all.length - 1);
if (this.last_added)
this.scroll_to({ ...this.last_added });
}
}
else if (check_shortcut(e, 'ArrowDown', 'shift', 'alt')) {
this.shift_select_to(this.all.length - 1);
if (this.last_added)
this.scroll_to({ ...this.last_added });
}
else {
return;
}
e.preventDefault();
}
}