UNPKG

pragma-views2

Version:

533 lines (452 loc) 16.2 kB
import {HierarchicalBase} from './../lib/hierarchical-base.js' import {inflateTemplate, setAttribute} from '../../baremetal/lib/template-inflator.js' import {TreeShortcuts} from "../lib/pragma-tree-shortcuts.js"; class PragmaTree extends HierarchicalBase { get labelField() { return this.getAttribute("label-field") || this._labelField; } set labelField(value) { this._labelField = value; } /** * Clears the selected items on the tree * @private */ _clearSelection() { const items = this.querySelectorAll("[aria-selected='true']"); for (let item of items) { const input = item.querySelector("input"); if (input != null) { input.checked = false; setAttribute(item, {"aria-selected": "false"}); } } this.clearSelectedId(); } /** * Collapses a node on the tree. * Sets the child container to hidden * @param node: HTMLElement * @private */ _collapseTreeNode(node) { const children = node.querySelector("ul"); children.setAttribute("aria-hidden", "true"); setAttribute(node, {"aria-expanded": "false"}); } /** * Creates a content template for a leaf node item * @param leaf * @param template * @returns {*} * @private */ _constructLeafNodeTemplate(leaf, template) { const li = leaf.cloneNode(true); const container = li.querySelector(".item-container"); container.appendChild(template.content); return li; } /** * Creates a list element for an item with no children * @param item: Data item * @param selected: Parent item selected state * @private */ _createLeafTreeNode(item, selected, field) { const resourceTemplate = this._getResourceTemplate(field); const li = resourceTemplate.cloneNode(true); const itemSelected = selected == null ? false : selected === "true"; const depth = this._getItemDepth(item); this._setTreeNodeAttributes(li, item.model[this.idField], item.model[this.labelField], depth, itemSelected, field); // Checking if the leaf item should be selected if (itemSelected === true || this.isSelected(item.model[this.idField]) === true) { this._selectTreeNode(li, true); } return inflateTemplate(li, item); } /** * Returns the item depth, either from the hierarchical item or the model * @param item * @returns {*} * @private */ _getItemDepth(item) { return item.model.level || item.depth; } /** * Creates a tree node for a data item that contains children * @param item: HierarchicalItem * @param selected: Parent node selection state * @private */ _createParentTreeNode(item, selected) { const groupTemplate = this._templates.get("group-template"); const template = groupTemplate.cloneNode(true); const li = template.querySelector("li"); const itemSelected = selected == null ? false : selected === "true"; const depth = this._getItemDepth(item); item.model.title = item.model.title || item.model[this.labelField]; //NOTE JN: If no title is defined use the label-field as per leaf node. this._setTreeNodeAttributes(li, item.model[this.idField], item.model.title, depth, itemSelected, item.model.field); const inflated = inflateTemplate(template, item); if (itemSelected === true) { this._selectTreeNode(li, itemSelected); } return inflated; } /** * Creates a tree node for a given data item * @param item * @param selected: Selected status * @private */ _createTreeNode(item, selected, field) { return item.hasChildren === true ? this._createParentTreeNode(item, selected) : this._createLeafTreeNode(item, selected, field); } /** * Expands a tree node with the child items * @param node: HTMLElement * @param data: HierarchicalItem * @private */ async _expandTreeNode(node, data) { const ul = node.querySelector("ul"); if (node.dataset.loaded === "true") { setAttribute(node, { "aria-expanded": "true"}); setAttribute(ul, { "aria-hidden": "false"}); } else { if (data.items == undefined) { await this._datasource.load(null, data); } const requestData = { id: data.model[this.idField], dataset: data.items, page: 0, size: this._batchSize }; this._setIndentation(node); this.requestBatch(requestData); } } /** * Finds a list element with a specific id value attribute * @param id * @private */ _getListElementById(id) { return this.querySelector(`[model-id="${id}"]`); } /** * Returns the name of the resource from the data item * @param data * @returns {*} * @private */ _getResourceNameFromModel(data) { return data.model.resource != null ? data.model.resource : data.model.title; } /** * Finds the associated template for a resource * If a template cannot be found, use the fallback * @param resource * @returns {Node | undefined} * @private */ _getResourceTemplate(resource) { const fallbackName = "default"; return this._templates.has(resource) ? this._templates.get(resource) : this._templates.get(fallbackName); } /** * Event handler for when the tree node expander is clicked * @param event * @private */ _iconButtonClicked(event) { const li = event.target.closest("li"); const id = li.getAttribute("model-id"); if (id != null) { const expanded = this._isExpanded(li); if (expanded === false) { const item = this.find(this.data, this.idField, id); this._expandTreeNode(li, item); setAttribute(li, {"aria-expanded": "true"}); } else { this._collapseTreeNode(li); setAttribute(li, {"aria-expanded": "false"}); } } } /** * Click event handler for the checkbox * @param event * @private */ _inputClicked(event) { const li = event.target.closest("li"); const selected = li.getAttribute("aria-selected") === "true"; this._selectTreeNode(li, !selected); } /** * Inserts the fragment on the clicked item when a node is expanded * or to the root list element * @private */ _insertTreeFragment(id, fragment) { if (id === "root") { this.ul.appendChild(fragment); } else { const li = this._getListElementById(id); if (li != null) { const list = li.querySelector("ul"); list.appendChild(fragment); li.dataset.loaded = "true"; } } } /** * Checks if an item is a branch node * @param item * @returns {*} * @private */ _isBranchNode(item) { return item.classList.contains("branch-node"); } /** * Checks if item is expanded * @param item * @returns {boolean} * @private */ _isExpanded(item) { return item.getAttribute("aria-expanded") === "true"; } /** * Loads the templates for the component and caches them * @private */ _loadTemplates() { const groupElement = document.importNode(window.templates.get("pragma-tree-group-node-template"), true); const leafElement = document.importNode(window.templates.get("pragma-tree-leaf-node-template"), true); const leafTemplate = leafElement.querySelector("li"); const templates = Array.from(this.querySelectorAll("template")); this._templates = new Map(); for (let template of templates) { this._templates.set(template.getAttribute("resource"), this._constructLeafNodeTemplate(leafTemplate, template)); } this._templates.set("group-template", groupElement); } /** * Sets all the child items to the selected * state of the parent element * @param li: HTMLElement * @param selected: Selection status * @private */ _selectChildNodes(li, selected) { const items = li.querySelectorAll("li"); const children = Array.from(items); for (let child of children) { if (this._isBranchNode(child)) { this._selectChildNodes(child, selected); } this._setNodeSelectionState(child, selected); } } /** * Sets the selection status of a tree node * @private */ _selectTreeNode(li, selected) { const loaded = li.dataset.loaded === "true"; this._setNodeSelectionState(li, selected); if (this._isBranchNode(li) === true && loaded === true) { this._selectChildNodes(li, selected); } } /** * Sets the indentation of the group role under the nodef * @param node * @private */ _setIndentation(node) { const group = node.querySelector("ul"); const currentLevel = node.getAttribute("aria-level"); const level = group.getAttribute("level"); if (level == null) { const level = Number(currentLevel) + 1; group.style.paddingLeft = `${level * this._levelSpacing}rem`; } } /** * Sets the selection information on the node * @param li * @param selected * @private */ _setNodeSelectionState(li, selected) { const input = li.querySelector("input"); const id = li.getAttribute("model-id"); input.checked = selected; setAttribute(input, { "aria-checked": selected }); setAttribute(li, { "aria-selected": selected }); this.setSelectedId(id, selected); } /** * Sets the attributes a treeitem * @param li * @param id * @param label * @param level * @param selected * @param field * @private */ _setTreeNodeAttributes(li, id, label, level, selected, field) { setAttribute(li, { "model-id": id, "aria-label": label, "aria-level": level, "aria-selected": selected, "field": field }); } /** * Invoked when an item on the tree is clicked * Clears the current selected items and sets the current * item as the selected one * @param event * @private */ _treeItemClicked(event) { const li = event.target.closest("li"); const selected = li.getAttribute("aria-selected") === "true"; this._clearSelection(); this._selectTreeNode(li, !selected); } /** * Updates the UI with the data received from the batch worker * @param data */ updateUI(data) { requestAnimationFrame(() => { const fragment = document.createDocumentFragment(); for (let item of data.items) { let selected = "false"; let field = null; const li = this._getListElementById(data.id); if (li != null) { selected = li.getAttribute("aria-selected"); field = li.getAttribute("field"); } //TODO JN: set hasChildren if not set - not being set on this.data if (item.hasChildren === false && item.model[this._expandRef] === true) { item.hasChildren = item.model[this._expandRef] === true; } fragment.appendChild(this._createTreeNode(item, selected, field)); } this._insertTreeFragment(data.id, fragment); this.requestBatch(data); if (this._shortcuts != null) { this._shortcuts.updateChildren(); } }); } /** * Validates that the component has the required fields * @private */ _validateComponent() { if (this.idField == null) { throw new Error("Id field is required"); } if (this.labelField == null) { throw new Error("Label field is required"); } } /** * Initialises the keyboard shortcuts for the component * @private */ _initialiseShortcuts() { if (this._shortcuts == null) { this._shortcuts = new TreeShortcuts(this); this._shortcuts.initialize(); } } /** * Click event handler * @param event */ click(event) { const tag = event.target.tagName.toLowerCase(); const action = this._actions.get(tag); if (action != null) { action.apply(this, [event]); this._initialiseShortcuts(); } } async connectedCallback() { await super.connectedCallback(); this._validateComponent(); this.initSelectionMode(); this._loadTemplates(); this._expandRef = this.getAttribute('expand-ref') || 'expandable'; this._inputClickedHandler = this._inputClicked.bind(this); this._focusHandler = this.focus.bind(this); this._iconButtonClickedHandler = this._iconButtonClicked.bind(this); this._treeItemClickedHandler = this._treeItemClicked.bind(this); this._levelSpacing = 1; this._batchSize = 1000; this._actions = new Map([ ["input", this._inputClickedHandler], ["icon-button", this._iconButtonClickedHandler], ["li", this._treeItemClickedHandler], ["div", this._treeItemClickedHandler] ]); this.setAttribute("tabindex", "0"); this.addEventListener("focus", this._focusHandler); } disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener("focus", this._focusHandler); this._templates = null; this._inputClickedHandler = null; this._focusHandler = null; this._iconButtonClickedHandler = null; this._treeItemClickedHandler = null; this._expandRef = null; this._clickedTreeNode = null; this._worker = null; this._batchSize = null; this._page = null; this.ul = null; this._isHierarchical = null; this._data = null; this._datasource = null; if (this._shortcuts != null) { this._shortcuts.dispose(); this._shortcuts = null; } } /** * On focus receive handler. Intialises the component shortcuts. */ focus(event) { this._initialiseShortcuts(); } /** * Intialises the component template */ initTemplate() { const instance = document.importNode(window.templates.get("pragma-tree-template"), true); this.appendChild(instance); this.ul = this.querySelector("ul"); } } customElements.define("pragma-tree", PragmaTree);