UNPKG

@author.io/element-options

Version:

author-options custom element (web component).

836 lines (657 loc) 25.3 kB
// Copyright (c) 2019 Author.io. MIT licensed. // @author.io/element-options v1.1.3 available at github.com/author-elements/options // Last Build: 5/17/2019, 1:42:23 AM var AuthorOptionsElement = (function () { 'use strict'; if (!window.hasOwnProperty('AuthorBaseElement')) { console.error('[ERROR] <author-options> Required dependency "AuthorBaseElement" not found.'); console.info('AuthorBaseElement is available at https://github.com/author-elements/base'); } class AuthorOptionsElement extends AuthorBaseElement(HTMLElement) { constructor () { super(`<template><style>@charset "UTF-8"; :host{contain:content;display:block;width:100%}:host *,:host :after,:host :before{box-sizing:border-box}author-options{contain:content;display:block;width:100%}author-options *,author-options :after,author-options :before{box-sizing:border-box}</style><slot name="afterbegin"></slot><slot name="beforeoptions"></slot><slot></slot><slot name="afteroptions"></slot><slot name="beforeend"></slot></template>`); this.UTIL.defineProperties({ cherryPickedOptions: { private: true }, filteredOptions: { readonly: true, get: () => { let { options } = this; for (let filter in this.PRIVATE.filters) { options = this.PRIVATE.filters[filter](); } return Array.isArray(options) ? options : [] } }, filters: { private: true, default: {} }, form: { readonly: true, get: () => this.parentNode.form }, displayOptions: { readonly: true, get: () => { let AuthorHTMLOptionsCollection = this.PRIVATE.generateAuthorHTMLOptionsCollectionConstructor(); let array = this.options.map(option => option.displayElement); let addFunction = (element, before) => this.addOption(this.PRIVATE.generateOptionObject(element), before); let removeFunction = index => this.removeOptionByIndex(index); return new AuthorHTMLOptionsCollection(array, this.selectedIndex, addFunction, removeFunction) } }, hoveredIndex: { readonly: true, get: () => this.options.findIndex(option => option.displayElement.hover) }, isSlave: { private: true, readonly: true, get: () => this.parentNode.localName === 'author-select' || this.parentNode.localName === 'author-datalist' }, lastSelectedIndex: { private: true }, multiple: { readonly: true, get: () => this.parentNode.multiple }, options: { readonly: true, default: [] }, selectedIndices: { readonly: true, get: () => [...this.selectedOptions].map(option => option.index) }, selectedOptions: { readonly: true, get: () => { let nodes = this.querySelectorAll('[selected]'); let AuthorHTMLCollection = this.PRIVATE.generateAuthorHTMLCollectionConstructor(); return new AuthorHTMLCollection(nodes) } }, Selection: { readonly: true, private: true, default: class { constructor (options = []) { this.options = options; } get displayElements () { return this.options.map(option => option.displayElement) } get length () { return this.options.length } get first () { return this.options[0] } get last () { return this.options[this.options.length - 1] } append (option) { this.options.push(option); } clear () { this.options = []; } includes (option) { return this.options.includes(option) } prepend (option) { this.options.unshift(option); } selectAll () { this.options.forEach(option => option.selected = true); } } }, selectionStartIndex: { private: true }, visibleOptions: { readonly: true, get: () => this.options.filter(option => !option.hidden) } }); this.UTIL.definePrivateMethods({ arrowDownHandler: evt => { if (!this.multiple) { let { startIndex } = evt.detail; switch (startIndex) { case this.options.length - 1: return default: return this.PRIVATE.hoverNextOption(startIndex) } return } let { lastSelectedIndex } = this.PRIVATE; if (lastSelectedIndex === this.options.length - 1) { return } return this.emit('option.selected', { index: lastSelectedIndex === null ? 0 : lastSelectedIndex + 1, keyboard: true, shiftKey: evt.detail.shiftKey, ctrlKey: false, metaKey: false }) }, arrowUpHandler: evt => { if (!this.multiple) { let { startIndex } = evt.detail; switch (startIndex) { case -1: case 0: return default: return this.PRIVATE.hoverPreviousOption(startIndex) } return } let { lastSelectedIndex } = this.PRIVATE; if (lastSelectedIndex === 0) { return } return this.emit('option.selected', { index: lastSelectedIndex === null ? this.options.length - 1 : lastSelectedIndex - 1, keyboard: true, shiftKey: evt.detail.shiftKey, ctrlKey: false, metaKey: false }) }, diffSelections: (comparator, comparable) => { return comparator.filter(option => !comparable.includes(option)) }, generateAuthorHTMLOptionsCollectionConstructor: () => { let _p = new WeakMap(); let AuthorHTMLCollection = this.PRIVATE.generateAuthorHTMLCollectionConstructor(); let AuthorHTMLOptionsCollection = class AuthorHTMLOptionsCollection extends AuthorHTMLCollection { constructor (arr, selectedIndex = -1, add, remove) { super(arr); this.selectedIndex = selectedIndex; this.add = add; this.remove = remove; _p.set(this, {arr}); } [Symbol.toStringTag] () { return 'AuthorHTMLOptionsCollection' } }; return AuthorHTMLOptionsCollection }, generateOptgroup: optgroup => { if (!customElements.get('author-optgroup')) { return this.UTIL.throwError({ type: 'dependency', vars: { name: 'author-optgroup' } }) } let surrogate = document.createElement('author-optgroup'); surrogate.id = this.UTIL.generateGuid('optgroup'); let label = optgroup.getAttribute('label'); if (!label || label.trim() === '') { return this.UTIL.throwError({ message: '<optgroup> must have a label attribute!' }) } surrogate.setAttribute('label', label); let options = optgroup.querySelectorAll('option'); Array.from(options).forEach(option => { this.addOption(this.PRIVATE.generateOptionObject(option), null, surrogate); }); return surrogate }, generateOptionObject: sourceElement => { if (!customElements.get('author-option')) { return this.UTIL.throwError({ type: 'dependency', vars: { name: 'author-option' } }) } let OptionConstructor = this.PRIVATE.generateOptionConstructor(); return new OptionConstructor(this, this.UTIL.generateGuid(), sourceElement, document.createElement('author-option')) }, generateOptionConstructor: () => { let _p = new WeakMap(); return class AuthorOptionObject { constructor (parent, key, sourceElement, displayElement) { this.key = key; this.form = parent.form; this.defaultSelected = sourceElement.selected; this.sourceElement = sourceElement; this.displayElement = displayElement; this.displayElement.parent = parent; this.displayElement.selected = sourceElement.selected; this.displayElement.defaultSelected = sourceElement.selected; this.displayElement.innerHTML = sourceElement.innerHTML; // Add additional attributes Array.from(sourceElement.attributes).forEach(attr => { if (typeof attr.value === 'boolean') { return attr.value ? this.displayElement.setAttribute(attr.name, '') : this.displayElement.removeAttribute(attr.name) } this.displayElement.setAttribute(attr.name, attr.value); }); _p.set(this, { attributes: { disabled: sourceElement.disabled, id: sourceElement.getAttribute('id'), label: sourceElement.getAttribute('label') || sourceElement.textContent.trim(), selected: sourceElement.selected, value: sourceElement.hasAttribute('value') ? sourceElement.getAttribute('value').trim() : null, text: sourceElement.text.trim() }, setAttr: (name, value) => { this.sourceElement[name] = value; if (typeof value === 'boolean') { value ? this.displayElement.setAttribute(name, '') : this.displayElement.removeAttribute(name); } else { this.displayElement.setAttribute(name, value); } _p.get(this).attributes[name] = value; } }); } get disabled () { return _p.get(this).attributes.disabled } set disabled (bool) { _p.get(this).setAttr('disabled', bool); } get hidden () { return this.displayElement.hidden } set hidden (bool) { this.displayElement.hidden = bool; } get index () { return this.sourceElement.index } get id () { return _p.get(this).attributes.id } set id (id) { _p.get(this).setAttr('id', id); } get selected () { return _p.get(this).attributes.selected } set selected (bool) { _p.get(this).setAttr('selected', bool); } get label () { return _p.get(this).attributes.label } set label (label) { _p.get(this).setAttr('label', label); } get text () { return _p.get(this).attributes.text } set text (text) { _p.get(this).setAttr('text', text); } get value () { return _p.get(this).attributes.value } set value (value) { _p.get(this).setAttr('value', value); } remove () { this.sourceElement.remove(); this.displayElement.remove(); } } }, getCurrentSelection: () => this.options.filter(option => option.selected), getPreviousVisibleOption: startIndex => { let index = startIndex - 1; let option = this.options[index]; if (!option) { return null } if (option.hidden) { option = this.PRIVATE.getPreviousVisibleOption(index); } return option }, getNextVisibleOption: startIndex => { let index = startIndex + 1; let option = this.options[index]; if (!option) { return null } if (option.hidden) { option = this.PRIVATE.getNextVisibleOption(index); } return option }, handleClickSelection: (detail, cb) => { let { cherryPickedOptions, getCurrentSelection, lastSelectedIndex, Selection, selectionStartIndex } = this.PRIVATE; let { index, shiftKey, ctrlKey, metaKey } = detail; let selectedOption = this.options[index]; if (shiftKey && lastSelectedIndex !== null) { this.PRIVATE.lastSelectedIndex = index; this.PRIVATE.cherryPickedOptions.clear(); let bounds = [index, selectionStartIndex].sort((a, b) => a - b); return cb(new Selection(bounds[0] === bounds[1] ? [selectedOption] : this.options.slice(bounds[0], bounds[1] + 1))) } let currentSelection = getCurrentSelection(); if (ctrlKey || metaKey) { this.PRIVATE.lastSelectedIndex = index; this.PRIVATE.selectionStartIndex = index; this.PRIVATE.cherryPickedOptions.options = selectedOption.selected ? currentSelection.filter(option => option !== selectedOption) : this.options.filter(option => option === selectedOption || currentSelection.includes(option)); return cb(this.PRIVATE.cherryPickedOptions) } if (currentSelection.length === 1 && index === lastSelectedIndex) { return } this.PRIVATE.lastSelectedIndex = index; this.PRIVATE.selectionStartIndex = index; this.PRIVATE.cherryPickedOptions.clear(); return cb(new Selection([selectedOption])) }, handleKeyboardSelection: (detail, cb) => { let { cherryPickedOptions, getCurrentSelection, Selection, selectionStartIndex } = this.PRIVATE; let { index, shiftKey } = detail; let selectedOption = this.options[index]; let currentSelection = getCurrentSelection(); this.PRIVATE.lastSelectedIndex = index; if (!shiftKey || currentSelection.length === 0) { this.PRIVATE.selectionStartIndex = index; this.PRIVATE.cherryPickedOptions.clear(); return cb(new Selection([selectedOption])) } // 1 option or more selected if (currentSelection.length > 0) { let bounds = [index, selectionStartIndex].sort(); let selection = new Selection(bounds[0] === bounds[1] ? [selectedOption] : this.options.slice(bounds[0], bounds[1] + 1)); if (cherryPickedOptions.length > 0) { selection.options = this.options.filter(option => selection.includes(option) || cherryPickedOptions.includes(option)); } return cb(selection) } }, hoverPreviousOption: (startIndex) => { let option = this.PRIVATE.getPreviousVisibleOption(startIndex); if (!option || option.index === startIndex) { return } this.hoverOption(option.index); }, hoverNextOption: (startIndex) => { let option = this.PRIVATE.getNextVisibleOption(startIndex); if (!option || option.index === startIndex) { return } this.hoverOption(option.index); }, optionSelectionHandler: evt => { let { cherryPickedOptions, diffSelections, getCurrentSelection, generateAuthorHTMLCollectionConstructor, handleClickSelection, handleKeyboardSelection, Selection } = this.PRIVATE; if (cherryPickedOptions === null) { this.PRIVATE.cherryPickedOptions = new Selection([]); } let { index, keyboard } = evt.detail; let completeOperation = selection => { if (!selection) { return this.emit('cleared') } let currentSelection = getCurrentSelection(); let comparator = selection.length >= currentSelection.length ? selection.options : currentSelection; let diff = diffSelections(comparator, comparator === currentSelection ? selection.options : currentSelection); if (diff.length === 0 || !this.PRIVATE.isSlave) { return } this.deselectAll(); selection.selectAll(); this.emit('options.selected', { options: selection.options, previous: this.selectedOptions, next: new (generateAuthorHTMLCollectionConstructor())(selection.displayElements) }, this.parentNode); // let { beforeChange } = this.parentNode // // let detail = { // options: selection.options, // previous: this.selectedOptions, // next: new (generateAuthorHTMLCollectionConstructor())(selection.displayElements) // } // // let cb = () => { // this.deselectAll() // selection.selectAll() // return this.emit('options.selected', detail, this.parentNode) // } // // if (!(beforeChange && typeof beforeChange === 'function')) { // return cb() // } // // beforeChange(this.selectedOptions, detail.next, cb) }; if (!this.multiple) { return completeOperation(this.options[index] ? new Selection([this.options[index]]) : null) } if (keyboard) { return this.PRIVATE.handleKeyboardSelection(evt.detail, completeOperation) } return this.PRIVATE.handleClickSelection(evt.detail, completeOperation) }, parentStateChangeHandler: evt => { this.emit('state.change', evt.detail); let { name, value } = evt.detail; switch (name) { case 'multiple': if (!value && this.selectedOptions.length > 0) { let index = this.selectedIndex; this.deselectAll(); this.emit('option.selected', { index }); } break default: return } } }); this.UTIL.registerListeners(this, { connected: () => { this.PRIVATE.selectionStartIndex = this.selectedIndex >= 0 ? this.selectedIndex : 0; if (this.PRIVATE.isSlave) { this.parentNode.on('state.change', this.PRIVATE.parentStateChangeHandler); } }, disconnected: () => { if (this.PRIVATE.isSlave) { this.parentNode.off('state.change', this.PRIVATE.parentStateChangeHandler); } }, 'keydown.arrowUp': this.PRIVATE.arrowUpHandler, 'keydown.arrowDown': this.PRIVATE.arrowDownHandler, 'option.selected': this.PRIVATE.optionSelectionHandler }); } /** * @property {number} selectedIndex * Represents the index of the first currently-selected option. * If no option is selected, will return -1. */ get selectedIndex () { return this.selectedOptions.length > 0 ? this.selectedOptions.item(0).index : -1 } set selectedIndex (index) { this.emit('option.selected', { index }); } addFilter (key = this.UTIL.generateGuid('filter_'), func) { if (typeof func !== 'function') { this.UTIL.throwError({ type: 'type', message: `Filter must be a function` }); } if (this.PRIVATE.filters.hasOwnProperty(key)) { console.warn(`Filter "${key}" alredy exists! Overwriting...`); } this.PRIVATE.filters[key] = func; } hasFilter (filter) { return this.PRIVATE.filters.hasOwnProperty(filter) } removeFilter (key) { if (!this.PRIVATE.filters.hasOwnProperty(key)) { return console.warn(`Filter "${key}" not found.`) } delete this.PRIVATE.filters[key]; } removeAllFilters () { this.PRIVATE.filters = {}; } addOptgroup (optgroup) { let label = document.createElement('author-optgroup-label'); label.innerHTML = optgroup.getAttribute('label'); this.appendChild(label); this.appendChild(optgroup); } addOption (option, index = null, dest = this) { if (!customElements.get('author-option')) { return this.UTIL.throwError({ type: 'dependency', vars: { name: 'author-option' } }) } if (option instanceof Option) { option = this.PRIVATE.generateOptionObject(option); } if (this.PRIVATE.isSlave) { this.parentNode[`${option.index}`] = option.displayElement; } if (index) { dest.insertBefore(option.displayElement, dest.children.item(index)); this.options.splice(index, 0, option); if (this.PRIVATE.isSlave) { this.parentNode.sourceElement.add(option.sourceElement, index); } } else { dest.appendChild(option.displayElement); this.options.push(option); if (this.PRIVATE.isSlave && !this.parentNode.sourceElement[this.options.length - 1]) { this.parentNode.sourceElement.appendChild(option.sourceElement); } } if (option.selected) { if (!this.multiple) { this.selectedIndex = option.index; return } } } addOptions (children) { Array.from(children).forEach(child => { let isElement = child instanceof HTMLElement; switch (child.nodeName) { case 'OPTION': return this.addOption(isElement ? this.PRIVATE.generateOptionObject(child) : child) case 'OPTGROUP': return this.addOptgroup(isElement ? this.PRIVATE.generateOptgroup(child) : child) default: return this.UTIL.printToConsole(`${child.nodeName.toLowerCase()} is not a valid child element for <author-select>. Removing...`, 'warning') } }); } clear () { this.selectedIndex = -1; this.innerHTML = ''; while (this.options.length) { this.options.pop(); } } deselect (option, updateList = true) { if (typeof option === 'number') { option = this.options[option]; } option.selected = false; if (this.PRIVATE.isSlave && this.selectedOptionsElement !== undefined) { this.parentNode.selectedOptionsElement.remove(option, updateList); } } deselectAll (showPlaceholder = true) { this.options.filter(option => option.selected).forEach((option, index, options) => { this.deselect(option, index = options.length - 1 && showPlaceholder); }); } find (query, caseSensitive = false) { let results = Array.from(this.options).filter(option => { let value = caseSensitive ? option.value : option.value.toLowerCase(); let text = caseSensitive ? option.text : option.text.toLowerCase(); query = caseSensitive ? query : query.toLowerCase(); return value.indexOf(query) >= 0 || text.indexOf(query) >= 0 }); return Array.isArray(results) ? results : [] } hoverOption (index) { this.unHoverAllOptions(); this.options[index].displayElement.hover = true; } item (index) { return this.options[index].displayElement } namedItem (value) { let query = this.options.filter(option => { let id = option.sourceElement.attributes.getNamedItem('id'); return id && id.value === value }); if (!query.length) { return null } return query[query.length - 1].displayElement } /** * @method removeOptionByIndex * @param {Number} [index=null] * Index of option to remove * @param {Boolean} [destroy=true] */ removeOptionByIndex (index = null) { if (index === null || index >= this.options.length) { return } this.options[index].remove(); } unHoverAllOptions () { this.options.forEach((option, index) => this.unHoverOption(index)); } unHoverOption (index) { this.options[index].displayElement.hover = false; } } customElements.define('author-options', AuthorOptionsElement); return AuthorOptionsElement; }()); //# sourceMappingURL=author-options.js.map