[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Replace old selection

Kasper (Sep 26, 2024, 3:20 AM +0200) e350e3a4 57572568

+411 -744
+57 -53
src/components/Queue.svelte
··· 2 2 import { 3 3 clear_user_queue, 4 4 get_by_queue_index, 5 - get_queue_length, 6 5 insert_ids, 7 6 move_indexes, 8 7 queue, ··· 10 9 } from '../lib/queue' 11 10 import { onDestroy } from 'svelte' 12 11 import QueueItemComponent from './QueueItem.svelte' 13 - import { new_selection } from '@/lib/selection' 14 12 import { dragged } from '@/lib/drag-drop' 15 13 import { get_track } from '@/lib/data' 16 14 import * as dragGhost from './DragGhost.svelte' ··· 20 18 import VirtualListBlock, { scroll_container_keydown } from './VirtualListBlock.svelte' 21 19 import type { SelectedTracksAction } from '@/electron/typed_ipc' 22 20 import { get_flattened_tracklists, handle_selected_tracks_action } from '@/lib/menus' 21 + import { SvelteSelection } from '@/lib/selection' 23 22 24 23 let object_urls: string[] = [] 25 24 ··· 38 37 let up_next_list: VirtualListBlock<QueueItem> 39 38 let autoplay_list: VirtualListBlock<QueueItem> 40 39 41 - const selection = new_selection({ 42 - get_item_count: () => get_queue_length(), 43 - scroll_to_item: (i) => { 44 - if (i < $queue.past.length) { 45 - return history_list.scroll_to_index(i, 40) 40 + let full_queue_list = [ 41 + ...(show_history ? $queue.past : []), 42 + ...(show_history && $queue.current ? [$queue.current.item] : []), 43 + ...$queue.user_queue, 44 + ...$queue.auto_queue, 45 + ].map((item) => item.qId) 46 + $: full_queue_list = [ 47 + ...(show_history ? $queue.past : []), 48 + ...($queue.current ? [$queue.current.item] : []), 49 + ...$queue.user_queue, 50 + ...$queue.auto_queue, 51 + ].map((item) => item.qId) 52 + const selection = new SvelteSelection(full_queue_list, { 53 + scroll_to: ({ index }) => { 54 + if (index < $queue.past.length) { 55 + return history_list.scroll_to_index(index, 40) 46 56 } 47 - i -= $queue.past.length 48 - if ($queue.current && i === 0) { 49 - return history_list.scroll_to_index(i, 40) 57 + index -= $queue.past.length 58 + if ($queue.current && index === 0) { 59 + return history_list.scroll_to_index(index, 40) 50 60 } 51 - i -= Number(!!$queue.current) 52 - if (i < $queue.user_queue.length) { 53 - return up_next_list.scroll_to_index(i, 40) 61 + index -= Number(!!$queue.current) 62 + if (index < $queue.user_queue.length) { 63 + return up_next_list.scroll_to_index(index, 40) 54 64 } 55 - i -= $queue.user_queue.length 56 - autoplay_list.scroll_to_index(i, 40) 65 + index -= $queue.user_queue.length 66 + autoplay_list.scroll_to_index(index, 40) 57 67 }, 58 - async on_context_menu() { 68 + async on_contextmenu() { 59 69 const action = await ipc_renderer.invoke('show_tracks_menu', { 60 70 is_editable_playlist: false, 61 71 queue: true, ··· 66 76 } 67 77 }, 68 78 }) 69 - $: selection.setMinimumIndex(show_history ? 0 : up_next_index) 70 - 79 + $: selection.update_all_items(full_queue_list) 71 80 $: $queue, selection.clear() 72 81 73 82 function remove_from_queue() { 74 - if ($selection.count >= 1) { 75 - queue.removeIndexes(selection.getSelectedIndexes()) 83 + if (selection.items.size >= 1) { 84 + queue.removeIndexes(selection.get_selected_indexes()) 76 85 } 77 86 } 78 87 onDestroy(ipc_listen('context.Remove from Queue', remove_from_queue)) ··· 80 89 let queue_element: HTMLElement 81 90 82 91 function handle_action(action: SelectedTracksAction) { 83 - const first_index = selection.findFirst() 84 - const indexes = selection.getSelectedIndexes() 92 + const first_index = selection.find_first_index() 93 + const indexes = selection.get_selected_indexes() 85 94 const track_ids = indexes.map((i) => queue.getByQueueIndex(i).id) 86 95 const all_items = [ 87 96 ...$queue.past, ··· 106 115 onDestroy(track_action_unlisten) 107 116 108 117 let drag_line: HTMLElement 109 - let dagged_indexes: number[] = [] 118 + let dragged_indexes: number[] = [] 110 119 function on_drag_start(e: DragEvent) { 111 120 if (e.dataTransfer) { 112 - dagged_indexes = [] 113 - for (let i = 0; i < $selection.list.length; i++) { 114 - if ($selection.list[i]) { 115 - dagged_indexes.push(i) 116 - } 117 - } 121 + dragged_indexes = selection.get_selected_indexes() 118 122 e.dataTransfer.effectAllowed = 'move' 119 - if (dagged_indexes.length === 1) { 120 - const track = get_track(get_by_queue_index(dagged_indexes[0]).id) 123 + if (dragged_indexes.length === 1) { 124 + const track = get_track(get_by_queue_index(dragged_indexes[0]).id) 121 125 dragGhost.set_inner_text(track.artist + ' - ' + track.name) 122 126 } else { 123 - dragGhost.set_inner_text(dagged_indexes.length + ' items') 127 + dragGhost.set_inner_text(dragged_indexes.length + ' items') 124 128 } 125 129 dragged.tracks = { 126 - ids: dagged_indexes.map((i) => get_by_queue_index(i).id), 127 - queue_indexes: dagged_indexes, 130 + ids: dragged_indexes.map((i) => get_by_queue_index(i).id), 131 + queue_indexes: dragged_indexes, 128 132 } 129 133 e.dataTransfer.setDragImage(dragGhost.drag_el, 0, 0) 130 134 e.dataTransfer.setData('ferrum.tracks', '') ··· 167 171 ? move_indexes(dragged.tracks.queue_indexes, drag_to_index, to_user_queue) 168 172 : insert_ids(dragged.tracks.ids, drag_to_index, to_user_queue) 169 173 for (let i = new_selection.from; i <= new_selection.to; i++) { 170 - selection.add(i) 174 + selection.add_index(i) 171 175 } 172 176 drag_to_index = null 173 177 } ··· 183 187 tabindex="-1" 184 188 on:keydown={scroll_container_keydown} 185 189 on:keydown={(e) => { 186 - if (check_shortcut(e, 'Backspace') && $selection.count >= 1) { 190 + if (check_shortcut(e, 'Backspace') && selection.items.size >= 1) { 187 191 e.preventDefault() 188 192 remove_from_queue() 189 193 } else { 190 - selection.handleKeyDown(e) 194 + selection.handle_keydown(e) 191 195 } 192 196 }} 193 197 on:mousedown|self={selection.clear} ··· 234 238 <div 235 239 class="row" 236 240 role="row" 237 - class:selected={$selection.list[qi] === true} 238 - on:mousedown={(e) => selection.handleMouseDown(e, qi)} 239 - on:contextmenu={(e) => selection.handleContextMenu(e, qi)} 240 - on:click={(e) => selection.handleClick(e, qi)} 241 + class:selected={$selection.has(item.qId)} 242 + on:mousedown={(e) => selection.handle_mousedown(e, qi)} 243 + on:contextmenu={(e) => selection.handle_contextmenu(e, qi)} 244 + on:click={(e) => selection.handle_click(e, qi)} 241 245 draggable="true" 242 246 on:dragstart={on_drag_start} 243 247 on:dragover={(e) => on_drag_over(e, qi)} ··· 255 259 <div 256 260 class="row" 257 261 role="row" 258 - class:selected={$selection.list[qi] === true} 259 - on:mousedown={(e) => selection.handleMouseDown(e, qi)} 260 - on:contextmenu={(e) => selection.handleContextMenu(e, qi)} 261 - on:click={(e) => selection.handleClick(e, qi)} 262 + class:selected={$selection.has($queue.current.item.qId)} 263 + on:mousedown={(e) => selection.handle_mousedown(e, qi)} 264 + on:contextmenu={(e) => selection.handle_contextmenu(e, qi)} 265 + on:click={(e) => selection.handle_click(e, qi)} 262 266 draggable="true" 263 267 on:dragstart={on_drag_start} 264 268 on:dragover={(e) => on_drag_over(e, qi)} ··· 318 322 <div 319 323 class="row" 320 324 role="row" 321 - class:selected={$selection.list[qi] === true} 322 - on:mousedown={(e) => selection.handleMouseDown(e, qi)} 323 - on:contextmenu={(e) => selection.handleContextMenu(e, qi)} 324 - on:click={(e) => selection.handleClick(e, qi)} 325 + class:selected={$selection.has(item.qId)} 326 + on:mousedown={(e) => selection.handle_mousedown(e, qi)} 327 + on:contextmenu={(e) => selection.handle_contextmenu(e, qi)} 328 + on:click={(e) => selection.handle_click(e, qi)} 325 329 draggable="true" 326 330 on:dragstart={on_drag_start} 327 331 on:dragover={(e) => on_drag_over(e, qi)} ··· 359 363 <div 360 364 class="row" 361 365 role="row" 362 - class:selected={$selection.list[qi] === true} 363 - on:mousedown={(e) => selection.handleMouseDown(e, qi)} 364 - on:contextmenu={(e) => selection.handleContextMenu(e, qi)} 365 - on:click={(e) => selection.handleClick(e, qi)} 366 + class:selected={$selection.has(item.qId)} 367 + on:mousedown={(e) => selection.handle_mousedown(e, qi)} 368 + on:contextmenu={(e) => selection.handle_contextmenu(e, qi)} 369 + on:click={(e) => selection.handle_click(e, qi)} 366 370 draggable="true" 367 371 on:dragstart={on_drag_start} 368 372 on:dragover={(e) => on_drag_over(e, qi)}
+2 -2
src/components/TrackList.svelte
··· 43 43 import Cover from './Cover.svelte' 44 44 import Header from './Header.svelte' 45 45 import { writable } from 'svelte/store' 46 - import { SvelteSelection } from '@/lib/selection-new' 46 + import { SvelteSelection } from '@/lib/selection' 47 47 import { get_flattened_tracklists, handle_selected_tracks_action } from '@/lib/menus' 48 48 import type { SelectedTracksAction } from '@/electron/typed_ipc' 49 49 ··· 487 487 class="row" 488 488 role="row" 489 489 on:dblclick={(e) => double_click(e, i)} 490 - on:mousedown={(e) => selection.handle_mouse_down(e, i)} 490 + on:mousedown={(e) => selection.handle_mousedown(e, i)} 491 491 on:contextmenu={(e) => selection.handle_contextmenu(e, i)} 492 492 on:click={(e) => selection.handle_click(e, i)} 493 493 draggable="true"
-363
src/lib/selection-new.ts
··· 1 - import { writable, type Writable } from 'svelte/store' 2 - import { check_mouse_shortcut, check_shortcut } from './helpers' 3 - 4 - type SelectionOptions<T> = { 5 - scroll_to: (target: { item: T; index: number }) => void 6 - on_contextmenu: (items: Set<T>) => void 7 - } 8 - 9 - class Selection<T> { 10 - /** Currently selected items. Disallowing assignment 11 - * prevents the Svelte store from getting out of sync */ 12 - readonly items = new Set<T>() 13 - /** Full list of items that can be selected */ 14 - all: T[] 15 - /** The last added index */ 16 - last_added: { index: number; item: T } | null = null 17 - /** An anchor index for shift selection. */ 18 - shift_anchor: { index: number; item: T } | null = null 19 - /** Whether the user is current mouseup is a click or a selection update */ 20 - possible_row_click = false 21 - 22 - scroll_to: SelectionOptions<T>['scroll_to'] 23 - on_contextmenu: SelectionOptions<T>['on_contextmenu'] 24 - 25 - constructor(all_items: T[], options: SelectionOptions<T>) { 26 - this.all = all_items 27 - this.scroll_to = options.scroll_to 28 - this.on_contextmenu = options.on_contextmenu 29 - } 30 - 31 - clear() { 32 - this.items.clear() 33 - this.last_added = null 34 - this.shift_anchor = null 35 - } 36 - 37 - /** Update the list of items that can be selected. 38 - * Items that no longer exist are de-selected. */ 39 - update_all_items(all: T[]) { 40 - this.all = all 41 - const keep = new Set(all.filter((item) => this.items.has(item))) 42 - for (const item of this.items) { 43 - if (!keep.has(item)) { 44 - this.items.delete(item) 45 - } 46 - } 47 - if (this.last_added !== null && !this.items.has(this.last_added.item)) { 48 - this.last_added = null 49 - } 50 - if (this.shift_anchor !== null && !this.items.has(this.shift_anchor.item)) { 51 - this.shift_anchor = null 52 - } 53 - } 54 - 55 - /** Get first selected index, or `null` if selection is empty */ 56 - find_first_index() { 57 - const item_i = this.all.findIndex((item) => this.items.has(item)) 58 - if (item_i === -1) { 59 - return null 60 - } 61 - return item_i 62 - } 63 - 64 - /** Get first selected item, or `undefined` if selection is empty */ 65 - find_first() { 66 - const item_i = this.all.find((item) => this.items.has(item)) 67 - return item_i 68 - } 69 - 70 - items_as_array() { 71 - return this.all.filter((item) => this.items.has(item)) 72 - } 73 - 74 - #get_shift_anchor() { 75 - if (this.shift_anchor !== null) return this.shift_anchor 76 - else return this.last_added 77 - } 78 - 79 - /** Make sure the index exists */ 80 - add_index(index: number) { 81 - this.items.add(this.all[index]) 82 - this.last_added = { index, item: this.all[index] } 83 - this.shift_anchor = null 84 - } 85 - 86 - #add_index_in_shift_mode(index: number) { 87 - this.items.add(this.all[index]) 88 - this.last_added = { index, item: this.all[index] } 89 - } 90 - 91 - #add_index_range_in_shift_mode(from_index: number, to_index: number) { 92 - // Direction here determines this.last_added 93 - if (from_index < to_index) { 94 - for (let i = from_index; i <= to_index; i++) { 95 - this.#add_index_in_shift_mode(i) 96 - } 97 - } else { 98 - for (let i = from_index; i >= to_index; i--) { 99 - this.#add_index_in_shift_mode(i) 100 - } 101 - } 102 - } 103 - 104 - #remove_range_in_shift_mode(from_i: number, to_i: number) { 105 - if (from_i < to_i) { 106 - for (let i = from_i; i <= to_i; i++) { 107 - this.items.delete(this.all[i]) 108 - } 109 - } else { 110 - for (let i = from_i; i >= to_i; i--) { 111 - this.items.delete(this.all[i]) 112 - } 113 - } 114 - } 115 - 116 - /** Shift-select to index */ 117 - shift_select_to(to_index: number) { 118 - const anchor = this.#get_shift_anchor() 119 - const last_added = this.last_added 120 - if (last_added === null || anchor === null) { 121 - return this.items 122 - } 123 - 124 - if (anchor.index < to_index) { 125 - if (to_index < last_added.index) { 126 - // Retract selection closer to anchor 127 - this.#remove_range_in_shift_mode(to_index + 1, last_added.index) 128 - } else if (last_added.index < anchor.index) { 129 - // New shift selection is on the other side of anchor 130 - this.#remove_range_in_shift_mode(anchor.index - 1, last_added.index) 131 - this.#add_index_range_in_shift_mode(anchor.index, to_index) 132 - } else { 133 - this.#add_index_range_in_shift_mode(last_added.index, to_index) 134 - } 135 - this.last_added = { index: to_index, item: this.all[to_index] } 136 - } else { 137 - if (to_index > last_added.index) { 138 - // Retract selection closer to anchor 139 - this.#remove_range_in_shift_mode(to_index - 1, last_added.index) 140 - } else if (last_added.index > anchor.index) { 141 - // New shift selection is on the other side of anchor 142 - this.#remove_range_in_shift_mode(anchor.index + 1, last_added.index) 143 - this.#add_index_range_in_shift_mode(anchor.index, to_index) 144 - } else { 145 - this.#add_index_range_in_shift_mode(last_added.index, to_index) 146 - } 147 - this.last_added = { index: to_index, item: this.all[to_index] } 148 - } 149 - this.shift_anchor = anchor 150 - } 151 - 152 - /** Replace selection with the previous index, like perssing `ArrowUp` in a list. */ 153 - go_backward() { 154 - if (this.all.length === 0) { 155 - return 156 - } else if (this.items.size === 0) { 157 - this.add_index(this.all.length - 1) 158 - } else if (this.last_added !== null) { 159 - const prev_index = this.last_added.index - 1 160 - this.clear() 161 - this.add_index(Math.max(prev_index, 0)) 162 - } 163 - } 164 - 165 - /** Replace selection with the previous index, like perssing `ArrowDown` in a list. */ 166 - go_forward() { 167 - if (this.all.length === 0) { 168 - return 169 - } else if (this.items.size === 0) { 170 - this.add_index(0) 171 - } else if (this.last_added !== null) { 172 - const next_index = this.last_added.index + 1 173 - this.clear() 174 - this.add_index(Math.min(next_index, this.all.length - 1)) 175 - } 176 - } 177 - /** Expand or shrink selection backwards (shift+up) */ 178 - shift_select_backward() { 179 - const anchor = this.#get_shift_anchor() 180 - this.shift_anchor = anchor 181 - if (anchor === null || this.last_added === null) { 182 - return 183 - } 184 - if (this.last_added.index <= anchor.index) { 185 - // add prev to selection 186 - for (let i = this.last_added.index; i >= 0; i--) { 187 - if (!this.items.has(this.all[i])) { 188 - this.#add_index_in_shift_mode(i) 189 - return 190 - } 191 - } 192 - } else { 193 - // remove first from selection 194 - this.items.delete(this.last_added.item) 195 - this.last_added = { 196 - index: this.last_added.index - 1, 197 - item: this.all[this.last_added.index - 1], 198 - } 199 - } 200 - } 201 - /** Expand or shrink selection forwards (shift+down) */ 202 - shift_select_forward() { 203 - const anchor = this.#get_shift_anchor() 204 - this.shift_anchor = anchor 205 - if (anchor === null || this.last_added === null) { 206 - return 207 - } 208 - if (this.last_added.index >= anchor.index) { 209 - // add next to selection 210 - for (let i = this.last_added.index; i < this.all.length; i++) { 211 - if (!this.items.has(this.all[i])) { 212 - this.#add_index_in_shift_mode(i) 213 - return 214 - } 215 - } 216 - } else { 217 - // remove last from selection 218 - this.items.delete(this.last_added.item) 219 - this.last_added = { 220 - index: this.last_added.index + 1, 221 - item: this.all[this.last_added.index + 1], 222 - } 223 - } 224 - } 225 - #toggle(index: number) { 226 - if (this.items.has(this.all[index])) { 227 - if (this.last_added && this.last_added.item === this.all[index]) { 228 - this.last_added = null 229 - } 230 - this.items.delete(this.all[index]) 231 - } else { 232 - this.add_index(index) 233 - } 234 - this.shift_anchor = null 235 - } 236 - 237 - mouse_down_select(e: MouseEvent, index: number) { 238 - const is_selected = this.items.has(this.all[index]) 239 - if (check_mouse_shortcut(e) && !is_selected) { 240 - this.clear() 241 - this.add_index(index) 242 - } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true }) && !is_selected) { 243 - this.add_index(index) 244 - } else if (check_mouse_shortcut(e, { shift: true })) { 245 - this.shift_select_to(index) 246 - } 247 - } 248 - 249 - handle_mouse_down(e: MouseEvent, index: number) { 250 - if (e.button !== 0) { 251 - return 252 - } 253 - if (this.items.has(this.all[index])) { 254 - this.possible_row_click = true 255 - } 256 - this.mouse_down_select(e, index) 257 - } 258 - handle_contextmenu(e: MouseEvent, index: number) { 259 - this.mouse_down_select(e, index) 260 - this.on_contextmenu(this.items) 261 - } 262 - handle_click(e: MouseEvent, index: number) { 263 - if (this.possible_row_click && e.button === 0) { 264 - if (check_mouse_shortcut(e)) { 265 - this.clear() 266 - this.add_index(index) 267 - } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true })) { 268 - this.#toggle(index) 269 - } 270 - } 271 - this.possible_row_click = false 272 - } 273 - handle_keydown(e: KeyboardEvent) { 274 - if (check_shortcut(e, 'Escape')) { 275 - this.clear() 276 - } else if (check_shortcut(e, 'A', { cmd_or_ctrl: true })) { 277 - if (this.all.length > 1) { 278 - this.#add_index_range_in_shift_mode(0, this.all.length - 1) 279 - } 280 - } else if (check_shortcut(e, 'ArrowUp')) { 281 - this.go_backward() 282 - if (this.last_added) this.scroll_to({ ...this.last_added }) 283 - } else if (check_shortcut(e, 'ArrowUp', { shift: true })) { 284 - this.shift_select_backward() 285 - if (this.last_added) this.scroll_to({ ...this.last_added }) 286 - } else if (check_shortcut(e, 'ArrowUp', { alt: true })) { 287 - this.clear() 288 - if (this.all.length > 1) { 289 - this.add_index(0) 290 - if (this.last_added) this.scroll_to({ ...this.last_added }) 291 - } 292 - } else if (check_shortcut(e, 'ArrowUp', { shift: true, alt: true })) { 293 - this.shift_select_to(0) 294 - if (this.last_added) this.scroll_to({ ...this.last_added }) 295 - } else if (check_shortcut(e, 'ArrowDown')) { 296 - this.go_forward() 297 - if (this.last_added) this.scroll_to({ ...this.last_added }) 298 - } else if (check_shortcut(e, 'ArrowDown', { shift: true })) { 299 - this.shift_select_forward() 300 - if (this.last_added) this.scroll_to({ ...this.last_added }) 301 - } else if (check_shortcut(e, 'ArrowDown', { alt: true })) { 302 - this.clear() 303 - if (this.all.length > 1) { 304 - this.add_index(this.all.length - 1) 305 - if (this.last_added) this.scroll_to({ ...this.last_added }) 306 - } 307 - } else if (check_shortcut(e, 'ArrowDown', { shift: true, alt: true })) { 308 - this.shift_select_to(this.all.length - 1) 309 - if (this.last_added) this.scroll_to({ ...this.last_added }) 310 - } else { 311 - return 312 - } 313 - e.preventDefault() 314 - } 315 - } 316 - 317 - export class SvelteSelection<T> { 318 - readonly #selection: Selection<T> 319 - readonly #store: Writable<Set<T>> 320 - readonly subscribe: Writable<Set<T>>['subscribe'] 321 - readonly items: Set<T> 322 - constructor(all_items: T[], options: SelectionOptions<T>) { 323 - this.#selection = new Selection(all_items, options) 324 - this.#store = writable(this.#selection.items) 325 - this.subscribe = this.#store.subscribe 326 - this.items = this.#selection.items 327 - } 328 - 329 - find_first_index() { 330 - return this.#selection.find_first_index() 331 - } 332 - find_first() { 333 - return this.#selection.find_first() 334 - } 335 - items_as_array() { 336 - return this.#selection.items_as_array() 337 - } 338 - 339 - clear() { 340 - this.#selection.clear() 341 - this.#store.set(this.#selection.items) 342 - } 343 - update_all_items(all: T[]) { 344 - this.#selection.update_all_items(all) 345 - this.#store.set(this.#selection.items) 346 - } 347 - handle_mouse_down(e: MouseEvent, index: number) { 348 - this.#selection.handle_mouse_down(e, index) 349 - this.#store.set(this.#selection.items) 350 - } 351 - handle_contextmenu(e: MouseEvent, index: number) { 352 - this.#selection.handle_contextmenu(e, index) 353 - this.#store.set(this.#selection.items) 354 - } 355 - handle_click(e: MouseEvent, index: number) { 356 - this.#selection.handle_click(e, index) 357 - this.#store.set(this.#selection.items) 358 - } 359 - handle_keydown(e: KeyboardEvent) { 360 - this.#selection.handle_keydown(e) 361 - this.#store.set(this.#selection.items) 362 - } 363 - }
+352 -326
src/lib/selection.ts
··· 1 - import { check_mouse_shortcut, check_shortcut, getter_writable } from './helpers' 1 + import { writable, type Writable } from 'svelte/store' 2 + import { check_mouse_shortcut, check_shortcut } from './helpers' 2 3 3 - /** 4 - * The selection object. 5 - * - `list`: The array of indexes, where an index is `true` if it's selected 6 - * - `count`: The amount of selected items 7 - * - `lastAdded`: The last added index 8 - * - `shiftAnchor`: An anchor index for shift selection. 9 - */ 10 - type Selection = { 11 - list: boolean[] 12 - count: number 13 - lastAdded: number | null 14 - shiftAnchor: number | null 15 - minimumIndex: number 4 + type SelectionOptions<T> = { 5 + scroll_to: (target: { item: T; index: number }) => void 6 + on_contextmenu: (items: Set<T>) => void 16 7 } 17 - function clear(selection: Selection) { 18 - selection.list = [] 19 - selection.count = 0 20 - selection.lastAdded = null 21 - selection.shiftAnchor = null 22 - } 23 - function add_index(selection: Selection, index: number) { 24 - if (index < selection.minimumIndex) { 25 - return 8 + 9 + class Selection<T> { 10 + /** Currently selected items. Disallowing assignment 11 + * prevents the Svelte store from getting out of sync */ 12 + readonly items = new Set<T>() 13 + /** Full list of items that can be selected */ 14 + all: T[] 15 + /** The last added index */ 16 + last_added: { index: number; item: T } | null = null 17 + /** An anchor index for shift selection. */ 18 + shift_anchor: { index: number; item: T } | null = null 19 + /** Whether the user is current mouseup is a click or a selection update */ 20 + possible_row_click = false 21 + 22 + scroll_to: SelectionOptions<T>['scroll_to'] 23 + on_contextmenu: SelectionOptions<T>['on_contextmenu'] 24 + 25 + constructor(all_items: T[], options: SelectionOptions<T>) { 26 + this.all = all_items 27 + this.scroll_to = options.scroll_to 28 + this.on_contextmenu = options.on_contextmenu 26 29 } 27 - if (selection.list[index] !== true) { 28 - selection.list[index] = true 29 - selection.count++ 30 + 31 + clear() { 32 + this.items.clear() 33 + this.last_added = null 34 + this.shift_anchor = null 30 35 } 31 - selection.lastAdded = index 32 - } 33 - function add_range(selection: Selection, from: number, to: number) { 34 - if (from < selection.minimumIndex) { 35 - if (to < selection.minimumIndex) { 36 - return 36 + 37 + /** Update the list of items that can be selected. 38 + * Items that no longer exist are de-selected. */ 39 + update_all_items(all: T[]) { 40 + this.all = all 41 + const keep = new Set(all.filter((item) => this.items.has(item))) 42 + for (const item of this.items) { 43 + if (!keep.has(item)) { 44 + this.items.delete(item) 45 + } 37 46 } 38 - from = selection.minimumIndex 39 - } 40 - if (to < selection.minimumIndex) { 41 - to = selection.minimumIndex 42 - } 43 - if (from < to) { 44 - for (let i = from; i <= to; i++) { 45 - add_index(selection, i) 47 + if (this.last_added !== null && !this.items.has(this.last_added.item)) { 48 + this.last_added = null 46 49 } 47 - } else { 48 - for (let i = from; i >= to; i--) { 49 - add_index(selection, i) 50 + if (this.shift_anchor !== null && !this.items.has(this.shift_anchor.item)) { 51 + this.shift_anchor = null 52 + } 53 + } 54 + 55 + /** Get first selected index, or `null` if selection is empty */ 56 + find_first_index() { 57 + const item_i = this.all.findIndex((item) => this.items.has(item)) 58 + if (item_i === -1) { 59 + return null 50 60 } 61 + return item_i 62 + } 63 + 64 + /** Get first selected item, or `undefined` if selection is empty */ 65 + find_first() { 66 + const item_i = this.all.find((item) => this.items.has(item)) 67 + return item_i 68 + } 69 + 70 + items_as_array() { 71 + return this.all.filter((item) => this.items.has(item)) 51 72 } 52 - } 53 73 54 - function remove(selection: Selection, index: number) { 55 - selection.list[index] = false 56 - selection.count-- 57 - } 58 - function remove_range(selection: Selection, from: number, to: number) { 59 - if (from < to) { 60 - for (let i = from; i <= to; i++) { 61 - remove(selection, i) 74 + get_selected_indexes() { 75 + const indexes = new Array() 76 + for (let i = 0; i < this.all.length; i++) { 77 + if (this.items.has(this.all[i])) { 78 + indexes.push(i) 79 + } 62 80 } 63 - } else { 64 - for (let i = from; i >= to; i--) { 65 - remove(selection, i) 81 + return indexes 82 + } 83 + 84 + #get_shift_anchor() { 85 + if (this.shift_anchor !== null) return this.shift_anchor 86 + else return this.last_added 87 + } 88 + 89 + add_index(index: number) { 90 + if (index >= 0 && index < this.all.length) { 91 + this.items.add(this.all[index]) 92 + this.last_added = { index, item: this.all[index] } 93 + this.shift_anchor = null 66 94 } 67 95 } 68 - } 69 96 70 - function get_shift_anchor(selection: Selection) { 71 - if (selection.shiftAnchor !== null) return selection.shiftAnchor 72 - else return selection.lastAdded 73 - } 97 + add_index_unchecked(index: number) { 98 + this.items.add(this.all[index]) 99 + this.last_added = { index, item: this.all[index] } 100 + this.shift_anchor = null 101 + } 74 102 75 - function select_to(selection: Selection, to_index: number) { 76 - const anchor = get_shift_anchor(selection) 77 - const last_added = selection.lastAdded 78 - if (last_added === null || anchor === null) { 79 - return selection 103 + #add_index_in_shift_mode(index: number) { 104 + this.items.add(this.all[index]) 105 + this.last_added = { index, item: this.all[index] } 80 106 } 81 - if (anchor < to_index) { 82 - if (to_index < last_added) { 83 - // new shift selection is closer to anchor 84 - remove_range(selection, to_index + 1, last_added) 85 - } else if (last_added < anchor) { 86 - // new shift selection is on the other side of anchor 87 - remove_range(selection, anchor - 1, last_added) 88 - add_range(selection, anchor, to_index) 107 + 108 + #add_index_range_in_shift_mode(from_index: number, to_index: number) { 109 + // Direction here determines this.last_added 110 + if (from_index < to_index) { 111 + for (let i = from_index; i <= to_index; i++) { 112 + this.#add_index_in_shift_mode(i) 113 + } 89 114 } else { 90 - add_range(selection, last_added, to_index) 115 + for (let i = from_index; i >= to_index; i--) { 116 + this.#add_index_in_shift_mode(i) 117 + } 91 118 } 92 - selection.lastAdded = to_index 93 - } else { 94 - if (to_index > last_added) { 95 - // new shift selection is closer to anchor 96 - remove_range(selection, to_index - 1, last_added) 97 - } else if (last_added > anchor) { 98 - // new shift selection is on the other side of anchor 99 - remove_range(selection, anchor + 1, last_added) 100 - add_range(selection, anchor, to_index) 119 + } 120 + 121 + #remove_range_in_shift_mode(from_i: number, to_i: number) { 122 + if (from_i < to_i) { 123 + for (let i = from_i; i <= to_i; i++) { 124 + this.items.delete(this.all[i]) 125 + } 101 126 } else { 102 - add_range(selection, last_added, to_index) 127 + for (let i = from_i; i >= to_i; i--) { 128 + this.items.delete(this.all[i]) 129 + } 103 130 } 104 - selection.lastAdded = to_index 105 131 } 106 - selection.shiftAnchor = anchor 107 - } 132 + 133 + /** Shift-select to index */ 134 + shift_select_to(to_index: number) { 135 + const anchor = this.#get_shift_anchor() 136 + const last_added = this.last_added 137 + if (last_added === null || anchor === null) { 138 + return this.items 139 + } 108 140 109 - type SelectOptions = { 110 - get_item_count: () => number 111 - scroll_to_item: (index: number) => void 112 - on_context_menu: () => void 113 - minimum_index?: number 114 - } 115 - export function new_selection(options: SelectOptions) { 116 - const store = getter_writable<Selection>({ 117 - list: [], 118 - count: 0, 119 - lastAdded: null, 120 - shiftAnchor: null, 121 - minimumIndex: options.minimum_index || 0, 122 - }) 123 - let possible_row_click = false 141 + if (anchor.index < to_index) { 142 + if (to_index < last_added.index) { 143 + // Retract selection closer to anchor 144 + this.#remove_range_in_shift_mode(to_index + 1, last_added.index) 145 + } else if (last_added.index < anchor.index) { 146 + // New shift selection is on the other side of anchor 147 + this.#remove_range_in_shift_mode(anchor.index - 1, last_added.index) 148 + this.#add_index_range_in_shift_mode(anchor.index, to_index) 149 + } else { 150 + this.#add_index_range_in_shift_mode(last_added.index, to_index) 151 + } 152 + this.last_added = { index: to_index, item: this.all[to_index] } 153 + } else { 154 + if (to_index > last_added.index) { 155 + // Retract selection closer to anchor 156 + this.#remove_range_in_shift_mode(to_index - 1, last_added.index) 157 + } else if (last_added.index > anchor.index) { 158 + // New shift selection is on the other side of anchor 159 + this.#remove_range_in_shift_mode(anchor.index + 1, last_added.index) 160 + this.#add_index_range_in_shift_mode(anchor.index, to_index) 161 + } else { 162 + this.#add_index_range_in_shift_mode(last_added.index, to_index) 163 + } 164 + this.last_added = { index: to_index, item: this.all[to_index] } 165 + } 166 + this.shift_anchor = anchor 167 + } 124 168 125 - function mouse_down_select(e: MouseEvent, index: number) { 126 - const is_selected = store.get().list[index] 127 - if (check_mouse_shortcut(e) && !is_selected) { 128 - selection.clear() 129 - selection.add(index) 130 - } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true }) && !is_selected) { 131 - selection.add(index) 132 - } else if (check_mouse_shortcut(e, { shift: true })) { 133 - selection.shiftSelectTo(index) 169 + /** Replace selection with the previous index, like perssing `ArrowUp` in a list. */ 170 + go_backward() { 171 + if (this.all.length === 0) { 172 + return 173 + } else if (this.items.size === 0) { 174 + this.add_index_unchecked(this.all.length - 1) 175 + } else if (this.last_added !== null) { 176 + const prev_index = this.last_added.index - 1 177 + this.clear() 178 + this.add_index_unchecked(Math.max(prev_index, 0)) 134 179 } 135 180 } 136 181 137 - const selection = { 138 - subscribe: store.subscribe, 139 - /** Get first selected index */ 140 - findFirst() { 141 - const selection = store.get() 142 - for (let i = selection.minimumIndex; i < selection.list.length; i++) { 143 - if (selection.list[i] === true) return i 144 - } 145 - return null 146 - }, 147 - /* Indexes lower than this cannot be selected */ 148 - setMinimumIndex(index: number) { 149 - store.update((selection) => { 150 - selection.minimumIndex = index 151 - if (index > 0) { 152 - remove_range(selection, 0, index - 1) 153 - } 154 - return selection 155 - }) 156 - }, 157 - getSelectedIndexes(): number[] { 158 - const selection = store.get() 159 - const indexes = [] 160 - for (let i = selection.minimumIndex; i < selection.list.length; i++) { 161 - if (selection.list[i]) { 162 - indexes.push(i) 182 + /** Replace selection with the previous index, like perssing `ArrowDown` in a list. */ 183 + go_forward() { 184 + if (this.all.length === 0) { 185 + return 186 + } else if (this.items.size === 0) { 187 + this.add_index_unchecked(0) 188 + } else if (this.last_added !== null) { 189 + const next_index = this.last_added.index + 1 190 + this.clear() 191 + this.add_index_unchecked(Math.min(next_index, this.all.length - 1)) 192 + } 193 + } 194 + /** Expand or shrink selection backwards (shift+up) */ 195 + shift_select_backward() { 196 + const anchor = this.#get_shift_anchor() 197 + this.shift_anchor = anchor 198 + if (anchor === null || this.last_added === null) { 199 + return 200 + } 201 + if (this.last_added.index <= anchor.index) { 202 + // add prev to selection 203 + for (let i = this.last_added.index; i >= 0; i--) { 204 + if (!this.items.has(this.all[i])) { 205 + this.#add_index_in_shift_mode(i) 206 + return 163 207 } 164 208 } 165 - return indexes 166 - }, 167 - /** Add an index to the selection */ 168 - add(from_index: number, to_index?: number) { 169 - store.update((selection) => { 170 - if (to_index === undefined) { 171 - add_index(selection, from_index) 172 - } else { 173 - add_range(selection, from_index, to_index) 174 - } 175 - selection.shiftAnchor = null 176 - return selection 177 - }) 178 - }, 179 - /** 180 - * Replace selection with the previous index. 181 - * - `maxIndex`: The max index that can be selected. 182 - */ 183 - goBackward(max_index: number) { 184 - store.update((selection) => { 185 - if (selection.count === 0) { 186 - add_index(selection, max_index) 187 - } else if (selection.lastAdded !== null) { 188 - const new_index = selection.lastAdded - 1 189 - clear(selection) 190 - add_index(selection, Math.max(selection.minimumIndex, new_index)) 191 - } 192 - return selection 193 - }) 194 - }, 195 - /** 196 - * Replace selection with the next index. 197 - * - `maxIndex`: The max index that can be selected. 198 - */ 199 - goForward(max_index: number) { 200 - store.update((selection) => { 201 - if (selection.count === 0) { 202 - add_index(selection, selection.minimumIndex) 203 - } else if (selection.lastAdded !== null) { 204 - const new_index = selection.lastAdded + 1 205 - clear(selection) 206 - add_index(selection, Math.min(new_index, max_index)) 207 - } 208 - return selection 209 - }) 210 - }, 211 - /** Expand or shrink selection backwards (shift+up) */ 212 - shiftSelectBackward() { 213 - store.update((selection) => { 214 - const anchor = get_shift_anchor(selection) 215 - selection.shiftAnchor = anchor 216 - if (anchor === null || selection.lastAdded === null) { 217 - return selection 218 - } 219 - if (selection.lastAdded <= anchor) { 220 - // add prev to selection 221 - for (let i = selection.lastAdded; i >= selection.minimumIndex; i--) { 222 - if (selection.list[i] !== true) { 223 - add_index(selection, i) 224 - return selection 225 - } 226 - } 227 - } else { 228 - // remove first from selection 229 - remove(selection, selection.lastAdded) 230 - selection.lastAdded -= 1 231 - } 232 - return selection 233 - }) 234 - }, 235 - /** 236 - * Expand or shrink selection forwards (shift+down). 237 - * - `maxIndex`: The maximum index to expand to 238 - */ 239 - shiftSelectForward(max_index: number) { 240 - store.update((selection) => { 241 - const anchor = get_shift_anchor(selection) 242 - selection.shiftAnchor = anchor 243 - if (anchor === null || selection.lastAdded === null) { 244 - return selection 245 - } 246 - if (selection.lastAdded >= anchor) { 247 - // add next to selection 248 - for (let i = selection.lastAdded; i <= max_index; i++) { 249 - if (selection.list[i] !== true) { 250 - add_index(selection, i) 251 - return selection 252 - } 253 - } 254 - } else { 255 - // remove last from selection 256 - remove(selection, selection.lastAdded) 257 - selection.lastAdded += 1 258 - } 259 - return selection 260 - }) 261 - }, 262 - /** 263 - * Expand selection to index (shift+click selection). 264 - * Selects in either upwards or downwards order. 265 - * Selects only `toIndex` if there's no existing selection. 266 - */ 267 - shiftSelectTo(to_index: number) { 268 - store.update((selection) => { 269 - select_to(selection, to_index) 270 - return selection 271 - }) 272 - }, 273 - toggle(index: number) { 274 - store.update((selection) => { 275 - if (selection.list[index]) { 276 - if (selection.lastAdded === index) { 277 - selection.lastAdded = null 278 - } 279 - remove(selection, index) 280 - } else { 281 - add_index(selection, index) 209 + } else { 210 + // remove first from selection 211 + this.items.delete(this.last_added.item) 212 + this.last_added = { 213 + index: this.last_added.index - 1, 214 + item: this.all[this.last_added.index - 1], 215 + } 216 + } 217 + } 218 + /** Expand or shrink selection forwards (shift+down) */ 219 + shift_select_forward() { 220 + const anchor = this.#get_shift_anchor() 221 + this.shift_anchor = anchor 222 + if (anchor === null || this.last_added === null) { 223 + return 224 + } 225 + if (this.last_added.index >= anchor.index) { 226 + // add next to selection 227 + for (let i = this.last_added.index; i < this.all.length; i++) { 228 + if (!this.items.has(this.all[i])) { 229 + this.#add_index_in_shift_mode(i) 230 + return 282 231 } 283 - selection.shiftAnchor = null 284 - return selection 285 - }) 286 - }, 287 - clear() { 288 - store.update((selection) => { 289 - clear(selection) 290 - return selection 291 - }) 292 - }, 232 + } 233 + } else { 234 + // remove last from selection 235 + this.items.delete(this.last_added.item) 236 + this.last_added = { 237 + index: this.last_added.index + 1, 238 + item: this.all[this.last_added.index + 1], 239 + } 240 + } 241 + } 242 + #toggle(index: number) { 243 + if (this.items.has(this.all[index])) { 244 + if (this.last_added && this.last_added.item === this.all[index]) { 245 + this.last_added = null 246 + } 247 + this.items.delete(this.all[index]) 248 + } else { 249 + this.add_index_unchecked(index) 250 + } 251 + this.shift_anchor = null 252 + } 293 253 294 - handleMouseDown(e: MouseEvent, index: number) { 295 - if (e.button !== 0) { 296 - return 254 + mouse_down_select(e: MouseEvent, index: number) { 255 + const is_selected = this.items.has(this.all[index]) 256 + if (check_mouse_shortcut(e) && !is_selected) { 257 + this.clear() 258 + this.add_index_unchecked(index) 259 + } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true }) && !is_selected) { 260 + this.add_index_unchecked(index) 261 + } else if (check_mouse_shortcut(e, { shift: true })) { 262 + this.shift_select_to(index) 263 + } 264 + } 265 + 266 + handle_mouse_down(e: MouseEvent, index: number) { 267 + if (e.button !== 0) { 268 + return 269 + } 270 + if (this.items.has(this.all[index])) { 271 + this.possible_row_click = true 272 + } 273 + this.mouse_down_select(e, index) 274 + } 275 + handle_contextmenu(e: MouseEvent, index: number) { 276 + this.mouse_down_select(e, index) 277 + this.on_contextmenu(this.items) 278 + } 279 + handle_click(e: MouseEvent, index: number) { 280 + if (this.possible_row_click && e.button === 0) { 281 + if (check_mouse_shortcut(e)) { 282 + this.clear() 283 + this.add_index_unchecked(index) 284 + } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true })) { 285 + this.#toggle(index) 297 286 } 298 - if (store.get().list[index]) { 299 - possible_row_click = true 287 + } 288 + this.possible_row_click = false 289 + } 290 + handle_keydown(e: KeyboardEvent) { 291 + if (check_shortcut(e, 'Escape')) { 292 + this.clear() 293 + } else if (check_shortcut(e, 'A', { cmd_or_ctrl: true })) { 294 + if (this.all.length > 1) { 295 + this.#add_index_range_in_shift_mode(0, this.all.length - 1) 300 296 } 301 - mouse_down_select(e, index) 302 - }, 303 - handleContextMenu(e: MouseEvent, index: number) { 304 - mouse_down_select(e, index) 305 - options.on_context_menu() 306 - }, 307 - handleClick(e: MouseEvent, index: number) { 308 - if (possible_row_click && e.button === 0) { 309 - if (check_mouse_shortcut(e)) { 310 - selection.clear() 311 - selection.add(index) 312 - } else if (check_mouse_shortcut(e, { cmd_or_ctrl: true })) { 313 - selection.toggle(index) 314 - } 297 + } else if (check_shortcut(e, 'ArrowUp')) { 298 + this.go_backward() 299 + if (this.last_added) this.scroll_to({ ...this.last_added }) 300 + } else if (check_shortcut(e, 'ArrowUp', { shift: true })) { 301 + this.shift_select_backward() 302 + if (this.last_added) this.scroll_to({ ...this.last_added }) 303 + } else if (check_shortcut(e, 'ArrowUp', { alt: true })) { 304 + this.clear() 305 + if (this.all.length > 1) { 306 + this.add_index_unchecked(0) 307 + if (this.last_added) this.scroll_to({ ...this.last_added }) 315 308 } 316 - possible_row_click = false 317 - }, 318 - handleKeyDown(e: KeyboardEvent) { 319 - const item_count = options.get_item_count() 320 - const { minimumIndex } = store.get() 321 - if (item_count === 0 || item_count < minimumIndex) { 322 - return 309 + } else if (check_shortcut(e, 'ArrowUp', { shift: true, alt: true })) { 310 + this.shift_select_to(0) 311 + if (this.last_added) this.scroll_to({ ...this.last_added }) 312 + } else if (check_shortcut(e, 'ArrowDown')) { 313 + this.go_forward() 314 + if (this.last_added) this.scroll_to({ ...this.last_added }) 315 + } else if (check_shortcut(e, 'ArrowDown', { shift: true })) { 316 + this.shift_select_forward() 317 + if (this.last_added) this.scroll_to({ ...this.last_added }) 318 + } else if (check_shortcut(e, 'ArrowDown', { alt: true })) { 319 + this.clear() 320 + if (this.all.length > 1) { 321 + this.add_index_unchecked(this.all.length - 1) 322 + if (this.last_added) this.scroll_to({ ...this.last_added }) 323 323 } 324 - if (check_shortcut(e, 'Escape')) { 325 - selection.clear() 326 - } else if (check_shortcut(e, 'A', { cmd_or_ctrl: true })) { 327 - selection.add(minimumIndex, item_count - 1) 328 - } else if (check_shortcut(e, 'ArrowUp')) { 329 - selection.goBackward(item_count - 1) 330 - options.scroll_to_item(store.get().lastAdded || minimumIndex) 331 - } else if (check_shortcut(e, 'ArrowUp', { shift: true })) { 332 - selection.shiftSelectBackward() 333 - options.scroll_to_item(store.get().lastAdded || minimumIndex) 334 - } else if (check_shortcut(e, 'ArrowUp', { alt: true })) { 335 - selection.clear() 336 - selection.add(minimumIndex) 337 - options.scroll_to_item(minimumIndex) 338 - } else if (check_shortcut(e, 'ArrowUp', { shift: true, alt: true })) { 339 - selection.shiftSelectTo(minimumIndex) 340 - options.scroll_to_item(store.get().lastAdded || minimumIndex) 341 - } else if (check_shortcut(e, 'ArrowDown')) { 342 - selection.goForward(item_count - 1) 343 - options.scroll_to_item(store.get().lastAdded || minimumIndex) 344 - } else if (check_shortcut(e, 'ArrowDown', { shift: true })) { 345 - selection.shiftSelectForward(item_count - 1) 346 - options.scroll_to_item(store.get().lastAdded || minimumIndex) 347 - } else if (check_shortcut(e, 'ArrowDown', { alt: true })) { 348 - selection.clear() 349 - selection.add(item_count - 1) 350 - options.scroll_to_item(store.get().lastAdded || minimumIndex) 351 - } else if (check_shortcut(e, 'ArrowDown', { shift: true, alt: true })) { 352 - selection.shiftSelectTo(item_count - 1) 353 - options.scroll_to_item(store.get().lastAdded || minimumIndex) 354 - } else { 355 - return 356 - } 357 - e.preventDefault() 358 - }, 324 + } else if (check_shortcut(e, 'ArrowDown', { shift: true, alt: true })) { 325 + this.shift_select_to(this.all.length - 1) 326 + if (this.last_added) this.scroll_to({ ...this.last_added }) 327 + } else { 328 + return 329 + } 330 + e.preventDefault() 331 + } 332 + } 333 + 334 + export class SvelteSelection<T> { 335 + readonly #selection: Selection<T> 336 + readonly #store: Writable<Set<T>> 337 + readonly subscribe: Writable<Set<T>>['subscribe'] 338 + readonly items: Set<T> 339 + constructor(all_items: T[], options: SelectionOptions<T>) { 340 + this.#selection = new Selection(all_items, options) 341 + this.#store = writable(this.#selection.items) 342 + this.subscribe = this.#store.subscribe 343 + this.items = this.#selection.items 359 344 } 360 - return selection 345 + 346 + find_first_index() { 347 + return this.#selection.find_first_index() 348 + } 349 + find_first() { 350 + return this.#selection.find_first() 351 + } 352 + items_as_array() { 353 + return this.#selection.items_as_array() 354 + } 355 + get_selected_indexes() { 356 + return this.#selection.get_selected_indexes() 357 + } 358 + 359 + add_index(index: number) { 360 + this.#selection.add_index(index) 361 + } 362 + 363 + clear() { 364 + this.#selection.clear() 365 + this.#store.set(this.#selection.items) 366 + } 367 + update_all_items(all: T[]) { 368 + this.#selection.update_all_items(all) 369 + this.#store.set(this.#selection.items) 370 + } 371 + handle_mousedown(e: MouseEvent, index: number) { 372 + this.#selection.handle_mouse_down(e, index) 373 + this.#store.set(this.#selection.items) 374 + } 375 + handle_contextmenu(e: MouseEvent, index: number) { 376 + this.#selection.handle_contextmenu(e, index) 377 + this.#store.set(this.#selection.items) 378 + } 379 + handle_click(e: MouseEvent, index: number) { 380 + this.#selection.handle_click(e, index) 381 + this.#store.set(this.#selection.items) 382 + } 383 + handle_keydown(e: KeyboardEvent) { 384 + this.#selection.handle_keydown(e) 385 + this.#store.set(this.#selection.items) 386 + } 361 387 }