pragma-views2
Version:
533 lines (452 loc) • 16.2 kB
JavaScript
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);