| | |
| | | /** |
| | | * HandleSelection - Reusable selection management for items in grids |
| | | * |
| | | * Handles selection logic including: |
| | | * - Individual item selection/deselection |
| | | * - Select all / Clear all |
| | | * - Range selection (shift+click) |
| | | * - Selection count updates |
| | | * - Bulk action visibility |
| | | * |
| | | * @class HandleSelection |
| | | */ |
| | | class HandleSelection { |
| | | /** |
| | | * @param {Object} config - Configuration object |
| | | * @param {HTMLElement} config.container - Container element holding items |
| | | * @param {Object} config.ui - UI element references |
| | | * @param {HTMLElement} config.ui.selectAll - Select all checkbox |
| | | * @param {HTMLElement} config.ui.bulkControls - Bulk actions container |
| | | * @param {HTMLElement} config.ui.count - Selection count display |
| | | * @param {string} config.itemSelector - Selector for individual items |
| | | * @param {string} config.checkboxSelector - Selector for item checkboxes |
| | | * @param {Function} config.onSelectionChange - Optional callback when selection changes |
| | | */ |
| | | constructor(config) { |
| | | this.container = config.container; |
| | | this.ui = config.ui || {}; |
| | | this.itemSelector = config.itemSelector || '.item'; |
| | | this.checkboxSelector = config.checkboxSelector || '[name*="select-item"]'; |
| | | constructor(container, options = {}) { |
| | | this.container = container; |
| | | const defaults = { |
| | | selectAll: { |
| | | checkbox: '[data-select-all]', |
| | | label: '.selected label', |
| | | span: '.selected label span', |
| | | target: 'data-selects', |
| | | count: '.selected-count, .selected .info', |
| | | bulkControls: '.bulk-actions', |
| | | }, |
| | | items: '.item-grid', |
| | | wrapper: { |
| | | wrapper: ':has(.item-grid, [data-select-all])', |
| | | id: 'selection', |
| | | }, |
| | | item: { |
| | | item: '.item', |
| | | idAttribute: 'id', |
| | | checkbox: '[name="select-item"]', |
| | | }, |
| | | wrappers: {}, |
| | | }; |
| | | this.selectors = window.deepMerge(defaults, options); |
| | | |
| | | this.a11y = window.jvbA11y; |
| | | |
| | | this.selectedItems = new Set(); |
| | | this.lastSelected = null; |
| | | this.lastSelected = null; // For shift+click range selection |
| | | this.lastSelectedWrapper = null; //Tracks which wrapper we're in |
| | | this.lastClicked = null; |
| | | this.subscribers = new Set(); |
| | | this.items = new Map(); |
| | | |
| | | this.init(); |
| | | this.initElements(); |
| | | this.initListeners(); |
| | | //store items in memory if available |
| | | this.collectItems(); |
| | | } |
| | | removeDataReferences() { |
| | | let selectors = JSON.parse(JSON.stringify(this.selectors)); |
| | | delete selectors.item.idAttribute; |
| | | delete selectors.wrapper.id; |
| | | delete selectors.selectAll.target; |
| | | return selectors; |
| | | } |
| | | initElements() { |
| | | this.index = 0; |
| | | let selectors = this.removeDataReferences(); |
| | | this.ui = window.uiFromSelectors(selectors, this.container); |
| | | |
| | | init() { |
| | | // Bind event handlers |
| | | this.container.querySelectorAll(this.selectors.wrapper.wrapper).forEach(wrapper => { |
| | | this.addWrapper(wrapper); |
| | | }); |
| | | } |
| | | addWrapper(el) { |
| | | let id = this.selectors.wrapper.id; |
| | | if (!Object.hasOwn(el.dataset, id)) { |
| | | el.setAttribute(`data-${id}`, this.index); |
| | | this.index++; |
| | | } |
| | | let selectors = this.removeDataReferences().selectAll; |
| | | |
| | | //store the DOM of the grid and selectAll |
| | | this.ui.wrappers[el.dataset[id]] = { |
| | | element: el, |
| | | items: el.querySelector(this.selectors.items), |
| | | selectAll: window.uiFromSelectors(selectors, el) |
| | | }; |
| | | } |
| | | removeWrapper(el) { |
| | | delete this.ui.wrappers[el.dataset[this.selectors.wrapper.id]]; |
| | | } |
| | | |
| | | initListeners() { |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.changeHandler = this.handleChange.bind(this); |
| | | this.keyHandler = this.handleKeys.bind(this); |
| | | |
| | | // Attach listeners |
| | | this.container.addEventListener('click', this.clickHandler); |
| | | this.container.addEventListener('change', this.changeHandler); |
| | | this.container.addEventListener('keydown', this.keyHandler); |
| | | this.container.addEventListener('click', this.clickHandler); |
| | | document.addEventListener('keydown', this.keyHandler); |
| | | } |
| | | |
| | | handleKeys(e) { |
| | | // Ctrl/Cmd + A: Select all |
| | | if ((e.ctrlKey || e.metaKey) && e.key === 'a') { |
| | | e.preventDefault(); |
| | | handleChange(e) { |
| | | if (!this.container.contains(e.target)) return; |
| | | |
| | | if (this.ui.selectAll) { |
| | | this.ui.selectAll.checked = true; |
| | | this.selectAll(true); |
| | | if (window.jvbA11y) { |
| | | window.jvbA11y.announce('All items selected'); |
| | | } |
| | | } |
| | | // Select all checkbox |
| | | if (e.target.matches(this.selectors.selectAll.checkbox)) { |
| | | this.handleSelectAll(e.target); |
| | | return; |
| | | } |
| | | |
| | | // Escape: Deselect all |
| | | if (e.key === 'Escape' && this.selectedItems.size > 0) { |
| | | this.selectAll(false); |
| | | if (window.jvbA11y) { |
| | | window.jvbA11y.announce('Selection cleared'); |
| | | } |
| | | } |
| | | // Individual checkbox - only process if not already handled by shift-click |
| | | if (e.target.matches(this.selectors.item.checkbox)) { |
| | | const item = e.target.closest(this.selectors.item.item); |
| | | if (!item) return; |
| | | |
| | | // Delete/Backspace: Remove selected items |
| | | if ((e.key === 'Delete' || e.key === 'Backspace') && |
| | | !e.target.matches('input, textarea') |
| | | && this.selectedItems > 0) { |
| | | e.preventDefault(); |
| | | if (confirm(`Remove ${this.selectedItems.size} selected item${this.selectedItems.size !== 1 ? 's' : ''}?`)) { |
| | | this.deselect(this.selectedItems); |
| | | const wrapper = this.getItemWrapper(e.target); |
| | | if (!wrapper) return; |
| | | |
| | | this.lastClicked = wrapper.element; |
| | | const id = this.getItemId(item); |
| | | if (!id) return; |
| | | |
| | | if (e.target.checked) { |
| | | this.select(id, false); |
| | | } else { |
| | | this.deselect(id, false); |
| | | } |
| | | |
| | | this.lastSelected = id; |
| | | this.lastSelectedWrapper = wrapper.element; |
| | | this.updateSelectionUI(); |
| | | } |
| | | } |
| | | |
| | | handleClick(e) { |
| | | const checkbox = e.target.closest(`${this.checkboxSelector}, label[for]`); |
| | | if (!checkbox) return; |
| | | // Only care about shift-clicks on checkboxes/labels |
| | | if (!e.shiftKey) return; |
| | | |
| | | // Get the actual checkbox element |
| | | const input = checkbox.tagName === 'LABEL' |
| | | ? document.getElementById(checkbox.getAttribute('for')) |
| | | : checkbox; |
| | | const item = e.target.closest(this.selectors.item.item); |
| | | if (!item) return; |
| | | |
| | | if (!input) return; |
| | | // Check if clicking checkbox or its label |
| | | const clickedCheckbox = e.target.matches(this.selectors.item.checkbox); |
| | | const clickedLabel = e.target.closest('label[for]'); |
| | | |
| | | // Handle shift+click for range selection |
| | | if (e.shiftKey && this.lastSelected) { |
| | | e.preventDefault(); |
| | | this.handleRangeSelection(input); |
| | | } else { |
| | | // Store last clicked for range selection |
| | | const item = input.closest(this.itemSelector); |
| | | if (item) { |
| | | this.lastSelected = item; |
| | | } |
| | | } |
| | | } |
| | | if (!clickedCheckbox && !clickedLabel) return; |
| | | |
| | | handleChange(e) { |
| | | if (this.ui.selectAll && e.target === this.ui.selectAll) { |
| | | this.selectAll(e.target.checked); |
| | | } else { |
| | | const checkbox = e.target.closest(this.checkboxSelector); |
| | | if (!checkbox) return; |
| | | |
| | | this.toggleSelection(this.getItemId(checkbox)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Toggle selection state of an item |
| | | */ |
| | | toggleSelection(id) { |
| | | if (!id) return; |
| | | |
| | | let selected = true; |
| | | if (this.selectedItems.has(id)) { |
| | | selected = false; |
| | | this.selectedItems.delete(id); |
| | | } else { |
| | | this.selectedItems.add(id); |
| | | const wrapper = this.getItemWrapper(item); |
| | | if (!wrapper) return; |
| | | // Can't do range selection without a previous selection in same wrapper |
| | | if (!this.lastSelected || !this.lastSelectedWrapper || wrapper.element !== this.lastSelectedWrapper) { |
| | | return; // Let change handler deal with it normally |
| | | } |
| | | |
| | | if (selected) { |
| | | this.notify('item-selected', { |
| | | selectedItem: id, |
| | | selectedItems: this.selectedItems, |
| | | container: this.container |
| | | }); |
| | | } else { |
| | | this.notify('item-deselected', { |
| | | selectedItem: id, |
| | | selectedItems: this.selectedItems, |
| | | container: this.container |
| | | }); |
| | | } |
| | | e.preventDefault(); // Stop the checkbox from toggling |
| | | |
| | | |
| | | this.updateSelectionUI(); |
| | | } |
| | | |
| | | /** |
| | | * Select or deselect all items |
| | | */ |
| | | selectAll(checked) { |
| | | const items = this.container.querySelectorAll(this.itemSelector); |
| | | |
| | | if (!checked) { |
| | | this.selectedItems.clear(); |
| | | if (this.ui.selectAll) { |
| | | this.ui.selectAll.checked = false; |
| | | } |
| | | } |
| | | |
| | | items.forEach(item => { |
| | | const id = this.getItemId(item); |
| | | const checkbox = item.querySelector(this.checkboxSelector); |
| | | |
| | | if (checkbox) { |
| | | checkbox.checked = checked; |
| | | } |
| | | |
| | | if (checked && id) { |
| | | this.selectedItems.add(id); |
| | | } |
| | | }); |
| | | |
| | | this.notify('select-all', { |
| | | container: this.container, |
| | | selected: checked, |
| | | items: items |
| | | }) |
| | | |
| | | this.updateSelectionUI(); |
| | | } |
| | | |
| | | /** |
| | | * Clear all selections |
| | | */ |
| | | clearSelection() { |
| | | this.selectAll(false); |
| | | } |
| | | |
| | | /** |
| | | * Handle shift+click range selection |
| | | */ |
| | | handleRangeSelection(currentCheckbox) { |
| | | if (!this.lastSelected) { |
| | | this.lastSelected = currentCheckbox.closest(this.itemSelector); |
| | | return; |
| | | } |
| | | |
| | | const currentItem = currentCheckbox.closest(this.itemSelector); |
| | | if (!currentItem) return; |
| | | |
| | | // Get all items |
| | | const allItems = Array.from(this.container.querySelectorAll(this.itemSelector)); |
| | | |
| | | // Find indices |
| | | const lastIndex = allItems.indexOf(this.lastSelected); |
| | | const currentIndex = allItems.indexOf(currentItem); |
| | | // Do range selection |
| | | const currentId = this.getItemId(item); |
| | | const items = this.getWrapperChildren(wrapper); |
| | | const lastIndex = items.findIndex(itemId => itemId === this.lastSelected); |
| | | const currentIndex = items.findIndex(itemId => itemId === currentId); |
| | | |
| | | if (lastIndex === -1 || currentIndex === -1) return; |
| | | |
| | | // Determine range (handle both directions) |
| | | const startIndex = Math.min(lastIndex, currentIndex); |
| | | const endIndex = Math.max(lastIndex, currentIndex); |
| | | const [start, end] = [Math.min(lastIndex, currentIndex), Math.max(lastIndex, currentIndex)]; |
| | | const rangeItems = items.slice(start, end + 1); |
| | | |
| | | let isChecked = !currentCheckbox.checked; |
| | | // Select all items in range |
| | | for (let i = startIndex; i <= endIndex; i++) { |
| | | const item = allItems[i]; |
| | | const checkbox = item.querySelector(this.checkboxSelector); |
| | | const id = this.getItemId(item); |
| | | rangeItems.forEach(rangeItem => { |
| | | this.select(rangeItem,true,false); |
| | | }); |
| | | |
| | | if (checkbox && id) { |
| | | checkbox.checked = isChecked; |
| | | this.selectedItems.add(id); |
| | | } |
| | | } |
| | | |
| | | // Update last selected to current |
| | | this.lastSelected = currentItem; |
| | | |
| | | this.lastSelected = currentId; |
| | | this.updateSelectionUI(); |
| | | |
| | | this.notify('range-selected', { |
| | | selectedItems: this.selectedItems, |
| | | container: this.container |
| | | selectedItems: new Set(this.selectedItems), |
| | | wrapper: wrapper |
| | | }); |
| | | |
| | | // Announce for accessibility |
| | | const selectedCount = endIndex - startIndex + 1; |
| | | if (window.jvbA11y) { |
| | | window.jvbA11y.announce(`Selected ${selectedCount} items in range`); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update selection UI elements |
| | | */ |
| | | updateSelectionUI() { |
| | | const count = this.selectedItems.size; |
| | | const totalItems = this.container.querySelectorAll(this.itemSelector).length; |
| | | getWrapperChildren(wrapper) { |
| | | return Array.from(wrapper.items.children) |
| | | .map(item => this.getItemId(item)); |
| | | } |
| | | |
| | | // Update bulk controls visibility |
| | | if (this.ui.bulkControls) { |
| | | this.ui.bulkControls.hidden = count === 0; |
| | | } |
| | | getItemWrapper(item) { |
| | | if (!item) return null; |
| | | let wrapper = item.closest(this.selectors.wrapper.wrapper); |
| | | if (!wrapper)return null; |
| | | return this.getWrapper(wrapper); |
| | | } |
| | | |
| | | // Update count display |
| | | if (this.ui.count) { |
| | | const itemText = count === 1 ? 'item' : 'items'; |
| | | this.ui.count.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`; |
| | | this.ui.count.hidden = count === 0; |
| | | } |
| | | getWrapper(wrapper) { |
| | | return this.ui.wrappers[wrapper.dataset[this.selectors.wrapper.id]]??null; |
| | | } |
| | | |
| | | // Update select all checkbox state |
| | | if (this.ui.selectAll) { |
| | | this.ui.selectAll.checked = totalItems > 0 && count === totalItems; |
| | | this.ui.selectAll.indeterminate = count > 0 && count < totalItems; |
| | | |
| | | // Update label text if available |
| | | const label = this.ui.selectAll.nextElementSibling || |
| | | this.ui.selectAll.previousElementSibling; |
| | | if (label && label.tagName === 'LABEL') { |
| | | label.textContent = (totalItems > 0 && count === totalItems) |
| | | ? 'Clear Selection' |
| | | : 'Select All'; |
| | | handleKeys(e) { |
| | | // Escape: Deselect all |
| | | if (e.key === 'Escape' && this.selectedItems.size > 0) { |
| | | e.preventDefault(); |
| | | if (Object.keys(this.ui.wrappers).length > 1 && this.lastClicked) { |
| | | this.clearWrapperSelection(this.lastClicked); |
| | | } else { |
| | | this.clearSelection(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get item ID from element |
| | | */ |
| | | getItemId(element) { |
| | | const item = element.closest(this.itemSelector); |
| | | if (!item) return null; |
| | | handleSelectAll(trigger) { |
| | | const wrapper = this.getItemWrapper(trigger); |
| | | if (!wrapper) return; |
| | | |
| | | // Try common ID attributes in order |
| | | return item.dataset.id || |
| | | item.dataset.itemId || |
| | | item.dataset.uploadId || |
| | | item.id; |
| | | // Clear any existing selection from other wrappers first |
| | | //Add this back if it makes more sense to clear between select-alls |
| | | // if (this.lastSelectedWrapper && wrapper.element !== this.lastSelectedWrapper) { |
| | | // this.clearSelection(); |
| | | // } |
| | | |
| | | const ids = this.getWrapperChildren(wrapper); |
| | | |
| | | if (trigger.checked) { |
| | | ids.forEach(id => this.select(id, true, false)); |
| | | this.lastSelectedWrapper = wrapper.element; |
| | | } else { |
| | | ids.forEach(id => this.deselect(id, true, false)); |
| | | this.lastSelectedWrapper = null; |
| | | } |
| | | |
| | | // Update label text |
| | | if (wrapper.selectAll.span) { |
| | | wrapper.selectAll.span.textContent = (trigger.checked && ids.length > 0) ? 'Clear Selection' : 'Select All'; |
| | | } |
| | | |
| | | this.updateSelectionUI(); |
| | | |
| | | this.notify('select-all', { |
| | | wrapper: wrapper, |
| | | checked: trigger.checked, |
| | | ids: ids, |
| | | selectedItems: new Set(this.selectedItems) |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Check if an item is selected |
| | | */ |
| | | getItemId(item) { |
| | | if (!item instanceof Element) { |
| | | item = item.element??false; |
| | | if (!item) return; |
| | | } |
| | | return item.dataset[`${this.selectors.item.idAttribute}`]; |
| | | } |
| | | |
| | | /******************************************************************* |
| | | * PUBLIC API |
| | | *******************************************************************/ |
| | | select(id, updateCheckbox = true, updateUI = true) { |
| | | if (this.selectedItems.has(id)) return; |
| | | |
| | | this.selectedItems.add(id); |
| | | let item = this.getItem(id); |
| | | if (item) item.element.classList.add('selected'); |
| | | if (updateCheckbox) this.setCheckboxState(id, true); |
| | | if (updateUI) { |
| | | this.updateSelectionUI(); |
| | | } |
| | | this.notify('item-selected', { id, selectedItems: new Set(this.selectedItems) }); |
| | | } |
| | | |
| | | deselect(id, updateCheckbox = true, updateUI = true) { |
| | | if (!this.selectedItems.has(id)) return; |
| | | |
| | | this.selectedItems.delete(id); |
| | | let item = this.getItem(id); |
| | | if (item) item.element.classList.remove('selected'); |
| | | if (updateCheckbox) this.setCheckboxState(id, false); |
| | | if (updateUI) { |
| | | this.updateSelectionUI(); |
| | | } |
| | | this.notify('item-deselected', { id, selectedItems: new Set(this.selectedItems) }); |
| | | } |
| | | |
| | | toggle(id) { |
| | | this.selectedItems.has(id) ? this.deselect(id) : this.select(id); |
| | | this.updateSelectionUI(); |
| | | } |
| | | |
| | | clearSelection() { |
| | | this.selectedItems.forEach(id => this.deselect(id,true,false)); |
| | | this.selectedItems.clear(); |
| | | this.lastSelected = null; |
| | | this.lastSelectedWrapper = null; |
| | | |
| | | // Uncheck all select-all triggers |
| | | for (let wrapper of Object.values(this.ui.wrappers)) { |
| | | if (wrapper.selectAll.checkbox) wrapper.selectAll.checkbox.checked = false; |
| | | if (wrapper.selectAll.span) { |
| | | wrapper.selectAll.span.textContent = 'Select All'; |
| | | } |
| | | } |
| | | this.a11y.announce('Selection cleared'); |
| | | this.updateSelectionUI(); |
| | | this.notify('selection-cleared', { selectedItems: new Set() }); |
| | | } |
| | | clearWrapperSelection(wrapper) { |
| | | wrapper = this.getWrapper(wrapper); |
| | | if(!wrapper) return; |
| | | this.getWrapperChildren(wrapper).forEach(id => this.deselect(id, true, false)); |
| | | if (wrapper.selectAll.checkbox) wrapper.selectAll.checkbox.checked = false; |
| | | if (wrapper.selectAll.span) { |
| | | wrapper.selectAll.span.textContent = 'Select All'; |
| | | } |
| | | this.a11y.announce('Selection cleared in group'); |
| | | this.updateSelectionUI(); |
| | | this.notify('wrapper-selection-cleared', {selectedItems: this.selectedItems}); |
| | | } |
| | | |
| | | isSelected(id) { |
| | | return this.selectedItems.has(id); |
| | | } |
| | | |
| | | /** |
| | | * Programmatically select specific items |
| | | */ |
| | | select(ids) { |
| | | const idArray = Array.isArray(ids) ? ids : [ids]; |
| | | getSelection() { |
| | | return new Set(this.selectedItems); |
| | | } |
| | | |
| | | idArray.forEach(id => { |
| | | this.selectedItems.add(id); |
| | | /******************************************************************* |
| | | * DOM HELPERS |
| | | *******************************************************************/ |
| | | setCheckboxState(id, checked) { |
| | | const item = this.getItem(id); |
| | | if (!item || !item.checkbox) return; |
| | | |
| | | // Update checkbox if element exists |
| | | const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`); |
| | | if (item) { |
| | | const checkbox = item.querySelector(this.checkboxSelector); |
| | | if (checkbox) checkbox.checked = true; |
| | | } |
| | | }); |
| | | if (item.checkbox.checked !== checked) { |
| | | item.checkbox.checked = checked; |
| | | } |
| | | } |
| | | updateSelectionUI() { |
| | | if (!this.lastClicked) return; |
| | | |
| | | this.updateSelectionUI(); |
| | | const wrapper = this.getWrapper(this.lastClicked); |
| | | if (!wrapper || !wrapper.selectAll) return; |
| | | |
| | | this.notify('item-selected', { |
| | | selectedItem: id, |
| | | selectedItems: this.selectedItems, |
| | | container: this.container |
| | | const count = this.selectedItems.size; |
| | | |
| | | // Fix property paths: |
| | | let controls = wrapper.selectAll.bulkControls; |
| | | if (controls) { |
| | | controls.hidden = count === 0; |
| | | } |
| | | |
| | | let countEl = wrapper.selectAll.count; |
| | | if (countEl) { |
| | | const itemText = count === 1 ? 'item' : 'items'; |
| | | countEl.textContent = count === 0 ? '' : `${count} ${itemText} selected`; |
| | | countEl.hidden = count === 0; |
| | | } |
| | | } |
| | | /******************************************************************* |
| | | ITEM DOM CACHING |
| | | *******************************************************************/ |
| | | collectItems() { |
| | | this.container.querySelectorAll(this.selectors.item.item).forEach(item => { |
| | | this.setItem(item, true); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Programmatically deselect specific items |
| | | */ |
| | | deselect(ids) { |
| | | const idArray = Array.isArray(ids) ? ids : [ids]; |
| | | |
| | | idArray.forEach(id => { |
| | | this.selectedItems.delete(id); |
| | | |
| | | // Update checkbox if element exists |
| | | const item = this.container.querySelector(`${this.itemSelector}[data-id="${id}"]`); |
| | | if (item) { |
| | | const checkbox = item.querySelector(this.checkboxSelector); |
| | | if (checkbox) checkbox.checked = false; |
| | | } |
| | | }); |
| | | |
| | | this.updateSelectionUI(); |
| | | |
| | | this.notify('item-deselected', { |
| | | selectedItem: id, |
| | | selectedItems: this.selectedItems, |
| | | container: this.container |
| | | }); |
| | | getItem(id) { |
| | | if (this.items.has(id)) return this.items.get(id); |
| | | return this.setItem(id); |
| | | } |
| | | /** |
| | | * Event system |
| | | */ |
| | | setItem(id, isElement = false) { |
| | | let element = (!isElement) |
| | | ? this.container.querySelector(`[data-${this.camelToKebab(this.selectors.item.idAttribute)}="${id}"]`) |
| | | : id; |
| | | if (!element) return null; |
| | | |
| | | id = this.getItemId(element); |
| | | if (this.items.has(id)) return this.items.get(id); |
| | | |
| | | this.items.set(id, { |
| | | element: element, |
| | | checkbox: element.querySelector(this.selectors.item.checkbox) |
| | | }); |
| | | return this.items.get(id); |
| | | } |
| | | |
| | | camelToKebab(str) { |
| | | return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); |
| | | } |
| | | /******************************************************************* |
| | | * EVENT SYSTEM |
| | | *******************************************************************/ |
| | | subscribe(callback) { |
| | | this.subscribers.add(callback); |
| | | return () => this.subscribers.delete(callback); |
| | | } |
| | | |
| | | notify(event, data) { |
| | | this.subscribers.forEach(cb => cb(event, data)); |
| | | this.subscribers.forEach(cb => { |
| | | try { |
| | | cb(event, data); |
| | | } catch (e) { |
| | | console.error('HandleSelection subscriber error:', e); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Clean up event listeners |
| | | */ |
| | | destroy() { |
| | | // Remove event listeners |
| | | if (this.container) { |
| | | this.container.removeEventListener('click', this.clickHandler); |
| | | this.container.removeEventListener('change', this.changeHandler); |
| | | this.container.removeEventListener('keydown', this.keyHandler); |
| | | } |
| | | |
| | | // Clear selections |
| | | this.clearSelection(); |
| | | |
| | | // Clear subscribers |
| | | this.container.removeEventListener('change', this.changeHandler); |
| | | this.container.removeEventListener('click', this.clickHandler); |
| | | this.container.removeEventListener('keydown', this.keyHandler); |
| | | this.subscribers.clear(); |
| | | |
| | | // Clear references |
| | | this.container = null; |
| | | this.ui = null; |
| | | this.lastSelected = null; |
| | | this.selectedItems.clear(); |
| | | } |
| | | } |
| | | |
| | | // Export for use in other modules |
| | | window.jvbHandleSelection = HandleSelection; |