UNPKG

@scidian/osui

Version:

Lightweight JavaScript UI library.

427 lines (387 loc) 17.6 kB
import { Div } from '../core/Div.js'; import { Utils } from '../utils/Utils.js'; class TreeList extends Div { #shiftAdd = 0; // tracks up / down keys while shift is pressed #shiftTrack = []; // tracks selected values when shift key is starting to be held #dragImage = null; // drag ghost image constructor(multiSelect = false) { super(); const self = this; this.setClass('osui-tree-list'); this.allowFocus(); // Properties this.multiSelect = multiSelect; // multi-select allowed? this.options = []; // list item divs this.selectedValue = null; // for single select mode this.selectedValues = []; // for multi select mode // Key Events - arrow navigation, prevents native scroll behavior function onKeyDown(event) { // Single Select Keypress if (!self.multiSelect) { if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { event.preventDefault(); event.stopPropagation(); let index = self.getIndex(self.selectedValue); if (index === -1) return; if (event.key === 'ArrowUp') index--; if (event.key === 'ArrowDown') index++; if (index >= 0 && index < self.options.length) { self.setValue(self.options[index].value, true); self.dom.dispatchEvent(new Event('change')); } } // Multi-Select Keypress } else { // Reset shift tracking values on no shift key if (!event.shiftKey) { self.#shiftAdd = 0; self.#shiftTrack = []; } // Process Key Codes if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { event.preventDefault(); event.stopPropagation(); let values = [...self.selectedValues]; // Shift Key if (event.shiftKey) { // Initial Shift Key Values if (self.#shiftTrack.length === 0) self.#shiftTrack = [...values]; values = [...self.#shiftTrack]; // Find Values let lastValue = values[values.length - 1]; let index = self.getIndex(lastValue); if (index === -1) return; if (event.key === 'ArrowUp' && index + self.#shiftAdd > 0) self.#shiftAdd--; if (event.key === 'ArrowDown' && index + self.#shiftAdd < self.options.length - 1) self.#shiftAdd++; index += self.#shiftAdd; if (index < 0 || index > self.options.length - 1) return; // Find Indices const index1 = self.getIndex(values[values.length - 1]); const index2 = index; // Select all items between last selected and newly selected if (index1 < index2) { for (let i = index1; i <= index2; i++) { const value = self.options[i].value; if (!values.includes(value)) values.push(value); } } else { for (let i = index1; i >= index2; i--) { const value = self.options[i].value; if (!values.includes(value)) values.push(value); } } self.setValues(values, true); self.dom.dispatchEvent(new Event('change')); // No Shift Key } else if (values.length > 0) { let lastValue = values[values.length - 1]; let index = self.getIndex(lastValue); if (index === -1) return; if (event.key === 'ArrowUp') index--; if (event.key === 'ArrowDown') index++; if (index >= 0 && index < self.options.length) { self.setValues([ self.options[index].value ], true); self.dom.dispatchEvent(new Event('change')); } } } } } function onKeyUp(event) { switch (event.key) { case 'ArrowUp': case 'ArrowDown': event.preventDefault(); event.stopPropagation(); if (!event.shiftKey) { self.#shiftAdd = 0; self.#shiftTrack = []; } break; } } this.onKeyDown(onKeyDown); this.onKeyUp(onKeyUp); } /******************** LOOKUP ********************/ getIndex(value) { for (let i = 0; i < this.options.length; i++) { if (this.options[i].value == value) return i; } return -1; } getOption(value) { for (let i = 0; i < this.options.length; i++) { if (this.options[i].value == value) return this.options[i]; } return undefined; } /******************** SELECT - SINGLE ********************/ getValue() { return this.selectedValue; } setValue(value, scrollTo = false) { let lastElement = undefined; // Deselect for (let i = 0; i < this.options.length; i++) { this.options[i].classList.remove('osui-active'); } // Select for (let i = 0; i < this.options.length; i++) { const element = this.options[i]; if (element.value == value) { element.classList.add('osui-active'); lastElement = element; } } // Scroll Into View if (lastElement && scrollTo) setTimeout(() => Utils.scrollIntoView(lastElement), 0); // Set Value, Return this.selectedValue = value; return this; } /******************** SELECT - MULTI ********************/ getValues() { return this.selectedValues; } setValues(valueArray = [], scrollTo = false) { let lastElement = undefined; // Deselect for (const div of this.options) { div.classList.remove('osui-active'); div.classList.remove('osui-active-top'); div.classList.remove('osui-active-bottom'); } // Select for (const value of valueArray) { for (const div of this.options) { if (div.value == value) { div.classList.add('osui-active'); lastElement = div; } } } // Multi Line Coloring for (let i = 0; i < this.options.length - 1; i++) { const element = this.options[i]; const elementAfter = this.options[i + 1]; if (element.classList.contains('osui-active') && elementAfter.classList.contains('osui-active')) { element.classList.add('osui-active-top'); elementAfter.classList.add('osui-active-bottom'); } } // Scroll Into View if (lastElement && scrollTo) setTimeout(() => { Utils.scrollIntoView(lastElement); }, 0); // Set Values, Return this.selectedValues = [...valueArray]; return this; } /******************** BUILD FROM OPTION (div) LIST ********************/ setOptions(options) { const self = this; // Clear Existing Options this.clearContents(); // Click function onPointerDown(event) { // Reset shift tracking values when no shift key if (!event.shiftKey) { self.#shiftAdd = 0; self.#shiftTrack = []; } // Multi-Select if (self.multiSelect) { let multiAllowed = false; multiAllowed = multiAllowed || !this.singleSelect; multiAllowed = multiAllowed || self.selectedValues.length < 1; if (self.selectedValues.length === 1) { const option = self.getOption(self.selectedValues[0]); if (option && option.singleSelect === true) multiAllowed = false; } let values = [...self.selectedValues]; // Control / Command if (event.altKey || event.ctrlKey || event.metaKey) { if (values.includes(this.value)) { const index = values.indexOf(this.value); values.splice(index, 1); } else { if (multiAllowed) values.push(this.value); } self.setValues(values); // Shift Key } else if (event.shiftKey && values.length > 0) { if (multiAllowed) { // Initial Shift Key Values if (self.#shiftTrack.length === 0) self.#shiftTrack = [...self.selectedValues]; values = [...self.#shiftTrack]; // Find Indices const index1 = self.getIndex(values[values.length - 1]); const index2 = self.getIndex(this.value); // Select all items between last selected and newly selected if (index1 < index2) { for (let i = index1; i <= index2; i++) { if (self.options[i].singleSelect) continue; const value = self.options[i].value; if (!values.includes(value)) values.push(value); } } else { for (let i = index1; i >= index2; i--) { if (self.options[i].singleSelect) continue; const value = self.options[i].value; if (!values.includes(value)) values.push(value); } } self.#shiftAdd = index2 - index1; self.setValues(values); } // No Key } else { if (!values.includes(this.value)) { self.setValues([ this.value ]); } } // Single Select } else { self.setValue(this.value); } // Pointer Up Event this.addEventListener('pointerup', onPointerUp); // Dispatch 'change' Event self.dom.dispatchEvent(new Event('change')); } function onPointerUp(event) { // Multi-Select if (self.multiSelect) { if (! (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)) { self.setValues([ this.value ]); } } this.removeEventListener('pointerup', onPointerUp); } // Drag & Drop let currentDrag = undefined; function onDrag() { currentDrag = this; } function onDragStart(event) { // Multi-Select if (self.multiSelect) { const divRect = this.getBoundingClientRect(); const width = divRect.width; const height = divRect.height * self.selectedValues.length; // Div Container self.#dragImage = document.createElement('div'); self.#dragImage.classList.add('osui-tree-list'); self.#dragImage.classList.add('osui-drag-image'); self.#dragImage.style['width'] = `${width}px`; self.#dragImage.style['height'] = `${height}px`; self.#dragImage.style['top'] = `${height * -2}px`; // Clone Options for (let i = 0; i < self.selectedValues.length; i++) { const value = self.selectedValues[i]; const option = self.getOption(value); const optionClone = option.cloneNode(true /* include children */); optionClone.classList.add('osui-active-top'); optionClone.classList.add('osui-active-bottom'); self.#dragImage.appendChild(optionClone); } // Set Drag Image document.body.appendChild(self.#dragImage); event.dataTransfer.setDragImage(self.#dragImage, 0, 0); event.dataTransfer.setData('text/plain', self.selectedValues); // Single Select } else { event.dataTransfer.setData('text/plain', self.selectedValue); } } function onDragEnd(event) { if (self.#dragImage instanceof HTMLElement) { document.body.removeChild(self.#dragImage); self.#dragImage = null; } } function onDragOver(event) { if (!currentDrag || this === currentDrag) return; const area = event.offsetY / this.clientHeight; if (this.dropGroup !== currentDrag.dropGroup) { this.classList.remove('osui-drag'); this.classList.remove('osui-drag-top'); this.classList.remove('osui-drag-bottom'); } else if (this.noDirectDrop) { this.classList.remove('osui-drag'); if (area < 0.5) { this.classList.add('osui-drag-top'); this.classList.remove('osui-drag-bottom'); } else { this.classList.add('osui-drag-bottom'); this.classList.remove('osui-drag-top'); } } else { if (area < 0.25) { this.classList.add('osui-drag-top'); this.classList.remove('osui-drag'); this.classList.remove('osui-drag-bottom'); } else if (area < 0.75) { this.classList.add('osui-drag'); this.classList.remove('osui-drag-top'); this.classList.remove('osui-drag-bottom'); } else { this.classList.add('osui-drag-bottom'); this.classList.remove('osui-drag'); this.classList.remove('osui-drag-top'); } } } function onDragLeave() { if (!currentDrag || this === currentDrag) return; this.classList.remove('osui-drag'); this.classList.remove('osui-drag-top'); this.classList.remove('osui-drag-bottom'); } function onDrop(event) { event.preventDefault(); event.stopPropagation(); this.classList.remove('osui-drag'); this.classList.remove('osui-drag-top'); this.classList.remove('osui-drag-bottom'); if (currentDrag && this !== currentDrag && this.dropGroup === currentDrag.dropGroup) { // Dropped Data const data = event.dataTransfer.getData('text/plain'); const values = data.split(','); // Let derived class handle 'drop' event if (typeof self.onDrop === 'function') { self.onDrop(event, this, values); } } // Reset 'currentDrag' currentDrag = undefined; } // Events self.options = []; for (let i = 0; i < options.length; i++) { const div = options[i]; div.classList.add('osui-option'); self.dom.appendChild(div); self.options.push(div); div.addEventListener('pointerdown', onPointerDown); div.addEventListener('destroy', () => div.removeEventListener('pointerdown', onPointerDown), { once: true }); if (div.draggable) { div.addEventListener('drag', onDrag); div.addEventListener('dragstart', onDragStart); div.addEventListener('dragend', onDragEnd); div.addEventListener('dragover', onDragOver); div.addEventListener('dragleave', onDragLeave); div.addEventListener('drop', onDrop); div.addEventListener('destroy', () => { div.removeEventListener('drag', onDrag); div.removeEventListener('dragstart', onDragStart); div.removeEventListener('dragend', onDragEnd); div.removeEventListener('dragover', onDragOver); div.removeEventListener('dragleave', onDragLeave); div.removeEventListener('drop', onDrop); }, { once: true }); } } return this; } // end setOptions() } export { TreeList };