UNPKG

joicomponents

Version:

Design patterns to build native web components

131 lines (113 loc) 4.11 kB
//todo add a pattern for processedClick: a stopPropagation that works for only one element type. //todo selectable let processedClick; const templ = document.createElement("template"); templ.innerHTML = `<style> span { display: inline-block; } span::before { content: var(--tree-node-prefix, "▼"); } :host(:not([open])) span { transform: var(--tree-node-prefix-transform, rotate(-45deg)); } :host([_empty]) slot[name="prefix"] { display: none; } </style> <slot name='prefix'><span></span></slot> <slot></slot>`; function toggleAttribute(el, attName) { el.hasAttribute(attName) ? el.removeAttribute(attName) : el.setAttribute(attName, ""); } import {SlotchangeMixin} from "https://unpkg.com/joicomponents@1.2.27/src/slot/SlotChildMixin.js"; class TreeNode extends SlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.appendChild(templ.content.cloneNode(true)); this.shadowRoot.children[1].addEventListener("click", this.onPrefixClick.bind(this)); this.shadowRoot.addEventListener("click", this.onClick.bind(this)); this.__isTriggered = false; this.__isSetup = false; } static get observedAttributes() { return ["open", "selected", "_empty"]; } slotCallback(slot) { this.__isSetup = true; this.getRootTreeNode().syncAtBranchChange(); (slot.name === "") && this.updateEmpty(); } attributeChangedCallback(name, oldValue, newValue) { if (name === "open") { if (this.__isSetup) newValue === null ? this.closeChildren() : this.openParent(); } else if (name === "selected") { if (this.__isSetup && newValue !== null) this.unSelectAllOthers(this); //[4] } else if (name === "_empty") { this.updateEmpty(); } } unSelectAllOthers(skip) { //[4] let selecteds = this.getRootTreeNode().querySelectorAll("tree-node[selected]"); skip = skip || selecteds[selecteds.length - 1]; this !== skip && this.removeAttribute("selected"); for (let selected of selecteds) selected !== skip && selected.removeAttribute("selected") } getRootTreeNode() { let parent = this; while (parent.parentNode && parent.parentNode.tagName && parent.parentNode.tagName === "TREE-NODE") parent = parent.parentNode; return parent; } closeChildren() { if (this.children) for (let child of this.children) { if (child.tagName === "TREE-NODE" && child.hasAttribute("open")) child.removeAttribute("open"); } } openParent() { const p = this.parentNode; if (p.tagName && p.tagName === "TREE-NODE" && !p.hasAttribute("open")) p.setAttribute("open", ""); } syncAtBranchChange() { //[2] if (this.__isTriggered) return; this.__isTriggered = true; Promise.resolve().then(() => { this.__isTriggered = false; this.unSelectAllOthers(); //[4] const opens = this.querySelectorAll("tree-node[open]"); for (let i = 0; i < opens.length; i++) { let open = opens[i]; open.openParent(); } }); } onPrefixClick(e) { if (processedClick === e) return; processedClick = e; toggleAttribute(this, "open") } onClick(e) { if (processedClick === e) return; processedClick = e; const openOrSelect = (this.hasAttribute("selectable") || this.hasAttribute("_empty")) ? "selected" : "open"; toggleAttribute(this, openOrSelect); } updateEmpty() { const isParent = this.isParent(); if (!isParent && !this.hasAttribute("_empty")) this.setAttribute("_empty", ""); if (isParent && this.hasAttribute("_empty")) this.removeAttribute("_empty"); } isParent() { for (let child of this.children) { if (child.tagName === "TREE-NODE") return true; } return false; } } customElements.define("tree-node", TreeNode);