import { UiElement } from "./UiElement.js";
import "./Tree.css";
/**
* Generic Tree Class to display hierarchical structures
*
* item = {
* title: string,
* icon: [string],
* active: boolean,
* expanded: boolean
* }
*
* structure = {
* "#": [children_ids],
* "parent_id": [children_ids],
* ...
* }
*
* items = { "id": item, ... }
*
* @memberof vef.ui
*/
class Tree extends UiElement {
classes = ["vef-tree"];
itemTemplate = `
<div class="item-inner">
<span class="fa-stack item-checkbox">
<i class="far fa-square fa-stack-1x"></i>
<i class="fas fa-check fa-stack-1x"></i>
</span>
<i class="item-icon"></i>
<span class="item-title"></span>
</div>
`;
/**
* @param {HTMLElement | string} target
* @param {object} items
* @param {object} structure
*/
constructor(target, items, structure) {
super(target, {
"select": [],
"deselect": []
});
this.items_ = {};
this.setClass(this.classes);
this.getElement().dataset.id = "#";
if (items && structure) this.addStructuredItems(items, structure);
}
/**
* Internal method for deselecting items
* @param {HTMLElement} element
* @private
*/
deselect_(element) {
if (element.classList.contains("selected")) {
element.classList.remove("selected");
const item = this.items_[element.dataset.id];
item.item.active = false;
this.fire("deselect", item.item);
}
element.classList.remove("partly");
}
/**
* Internal method for selecting items
* @param {HTMLElement} element
* @param {boolean} partly
* @private
*/
select_(element, partly) {
if (!element.classList.contains("selected")) {
element.classList.add("selected");
const item = this.items_[element.dataset.id];
item.item.active = true;
this.fire("select", item.item);
}
(partly)
? element.classList.add("partly")
: element.classList.remove("partly");
}
/**
* Internal method for automatically selecting or deselecting
* the parent based on the children
*
* @param {HTMLElement} element
* @private
*/
toggleParent_(element) {
const parent = element.parentElement;
if (!parent || !parent.classList.contains("tree-item")) return;
const selectedChildren = parent.querySelectorAll(":scope > .tree-item.selected");
if (selectedChildren.length > 0) {
const allChildren = parent.querySelectorAll(":scope > .tree-item");
this.select_(parent, (selectedChildren.length < allChildren.length));
} else {
this.deselect_(parent);
}
this.toggleParent_(parent);
}
/**
* Select all items
*/
selectAll() {
const children = this.queryAll(".tree-item");
children.forEach(child => this.select_(child));
}
/**
* Deselect all items
*/
deselectAll() {
const children = this.queryAll(".tree-item");
children.forEach(child => this.deselect_(child));
}
/**
* select an item
*
* @param {string} id
*/
select(id) {
if (!(id in this.items_)) return;
const element = this.items_[id].element;
const children = element.querySelectorAll(".tree-item");
children.forEach(child => this.select_(child));
this.select_(element);
this.toggleParent_(element);
}
/**
* deselect an item
*
* @param {*} id
*/
deselect(id) {
if (!(id in this.items_)) return;
const element = this.items_[id].element;
const children = element.querySelectorAll(".tree-item");
children.forEach(child => this.deselect_(child));
this.deselect_(element);
this.toggleParent_(element);
}
/**
* swap the state of an item to the opposite.
* partly selected items will also be deselected.
*
* @param {string} id
*/
toggle(id) {
if (!(id in this.items_)) return;
(this.items_[id].element.classList.contains("selected"))
? this.deselect(id)
: this.select(id);
}
/**
* @param {string} parent
* @param {string} id
* @param {object} item
*/
addFolder(parent, id, item) {
this.addItem(parent, id, item);
}
/**
* @param {string} parent
* @param {string} id
* @param {object} item
*/
addItem(parent, id, item) {
const parentElement = (parent == "#")
? this.getElement()
: this.query(`.tree-item[data-id='${parent}']`);
if (!parentElement) return null;
const element = document.createElement("div");
element.classList.add("tree-item");
element.dataset.id = id;
element.innerHTML = this.itemTemplate;
element.querySelector(".item-title").innerText = item.title;
element.querySelector(".item-title").title = item.title;
element.querySelector(".item-icon").classList.add(...item.icon.split(" "));
element.querySelector(".item-inner").addEventListener("click", e => {
this.toggle(id);
})
parentElement.appendChild(element);
this.items_[id] = {
element: element,
item: item
};
if (item.active) this.select(id);
}
/**
* @param {object} items
* @param {object} structure
* @param {string} root element to append item tree to (default = "#")
*/
addStructuredItems(items, structure, root) {
// resolve folders recursively
const resolveFolder = parent => {
if ((parent in structure) && ((parent in this.items_) || (parent == "#"))) {
structure[parent].forEach(child => {
if (!(child in items)) return;
if (child in structure) {
this.addFolder(parent, child, items[child])
resolveFolder(child);
} else {
this.addItem(parent, child, items[child])
}
});
}
}
resolveFolder(root || "#");
}
/**
* @param {boolean} selected only get selected layers
* @returns {object} structure
*/
getStructure(selected = false) {
const structure = {};
const items = this.queryAll(".tree-item");
items.forEach(item => {
if (selected && !(item.classList.contains("selected"))) return;
const parent = item.parentElement.dataset.id;
if (!(parent in structure)) structure[parent] = [];
structure[parent].push(item.dataset.id);
});
return structure;
}
/**
* @param {boolean} selected only get selected layers
* @returns {object} items
*/
getItems(selected = false) {
const items = {}
const nodes = this.queryAll((selected) ? ".tree-item.selected" : ".tree-item");
nodes.forEach(node => {
const id = node.dataset.id;
items[id] = this.items_[id].item;
});
return items;
}
/**
* Clear all items from tree
*/
clear() {
this.getElement().innerHTML = "";
this.items_ = {};
}
}
export { Tree };