From 2909cd74b7d6970a74433e1b339335d7dac79d74 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 9 Apr 2018 17:58:51 +0530 Subject: [PATCH] Tree as Web Component --- client/style/style.scss | 1 - client/style/tree.scss | 186 ++++++++++++++++++---------------- client/ui/index.js | 29 ++++++ client/ui/tree.js | 127 +++++++++++++++++++---- client/view/tree.js | 89 +++++++++++++--- config/rollup.config.style.js | 1 - 6 files changed, 314 insertions(+), 119 deletions(-) diff --git a/client/style/style.scss b/client/style/style.scss index 562fe4d7..1b0159a8 100644 --- a/client/style/style.scss +++ b/client/style/style.scss @@ -4,7 +4,6 @@ @import "node_modules/flatpickr/dist/themes/airbnb"; @import "node_modules/codemirror/lib/codemirror"; @import "node_modules/frappe-datatable/dist/frappe-datatable"; -// @import "node_modules/octicons/build/build.css"; @import "./variables.scss"; @import "./indicators.scss"; diff --git a/client/style/tree.scss b/client/style/tree.scss index 1df585db..c3f9a670 100644 --- a/client/style/tree.scss +++ b/client/style/tree.scss @@ -1,111 +1,123 @@ @import "./variables.scss"; -.tree { +.tree-body { padding: $spacer-3 $spacer-4; } -.tree li { - list-style: none; +f-tree, f-tree-node { + display: block; } -ul.tree-children { - padding-left: $spacer-4; -} - -.tree-link { - cursor: pointer; - display: flex; - align-items: center; - width: 100%; -} - -.tree-link:hover { +f-tree-node:hover { background-color: $gray-100; + cursor: pointer; } -.tree-link .node-parent { - color: $gray-600; - width: 24px; - height: 24px; - text-align: center; -} +// .tree { +// padding: $spacer-3 $spacer-4; +// } -.tree-link .node-leaf { - color: $gray-400; -} +// .tree li { +// list-style: none; +// } -.tree-link .node-parent, .tree-link .node-leaf { - padding: $spacer-2; -} +// ul.tree-children { +// padding-left: $spacer-4; +// } -.tree-link.active { - a { - color: $gray-600; - } -} +// .tree-link { +// cursor: pointer; +// display: flex; +// align-items: center; +// width: 100%; +// } -.tree-hover { - background-color: $gray-200; - min-height: 20px; - border: 1px solid $gray-600; -} +// .tree-link:hover { +// background-color: $gray-100; +// } -.tree-node-toolbar { - display: inline-block; - padding: 0px 5px; - margin-left: 15px; - margin-bottom: -4px; - margin-top: -8px; -} +// .tree-link .node-parent { +// color: $gray-200; +// width: 24px; +// height: 24px; +// } -// @media (max-width: @screen-xs) { -// ul.tree-children { -// padding-left: 10px; +// .tree-link .node-leaf { +// color: $gray-400; +// } + +// .tree-link .node-parent, .tree-link .node-leaf { +// padding: $spacer-2; +// } + +// .tree-link.active { +// a { +// color: $gray-600; // } // } -// decoration -// .tree, .tree-node { -.tree.with-skeleton, .tree.with-skeleton .tree-node { - position: relative; +// .tree-hover { +// background-color: $gray-200; +// min-height: 20px; +// border: 1px solid $gray-600; +// } - &.opened::before, &:last-child::after { - content: ''; - position: absolute; - top: 12px; - left: 7px; - height: calc(100% - 23px); - width: 1px; - background: $gray-400; - z-index: -1; - } +// .tree-node-toolbar { +// display: inline-block; +// padding: 0px 5px; +// margin-left: 15px; +// margin-bottom: -4px; +// margin-top: -8px; +// } - &:last-child::after { - top: 11px; - left: -13px; - height: calc(100% - 15px); - width: 3px; - background: #fff; - } +// // @media (max-width: @screen-xs) { +// // ul.tree-children { +// // padding-left: 10px; +// // } +// // } - &.opened > .tree-children > .tree-node > .tree-link::before { - content: ''; - position: absolute; - width: 18px; - height: 1px; - top: 10px; - left: -12px; - z-index: -1; - background: $gray-400; - } -} +// // decoration +// // .tree, .tree-node { +// .tree.with-skeleton, .tree.with-skeleton .tree-node { +// position: relative; -.tree.with-skeleton.opened::before { - left: 22px; - top: 33px; - height: calc(100% - 67px); -} +// &.opened::before, &:last-child::after { +// content: ''; +// position: absolute; +// top: 12px; +// left: 7px; +// height: calc(100% - 23px); +// width: 1px; +// background: $gray-400; +// z-index: -1; +// } -.tree-link.active ~ .balance-area { - color: $gray-600 !important; -} +// &:last-child::after { +// top: 11px; +// left: -13px; +// height: calc(100% - 15px); +// width: 3px; +// background: #fff; +// } + +// &.opened > .tree-children > .tree-node > .tree-link::before { +// content: ''; +// position: absolute; +// width: 18px; +// height: 1px; +// top: 10px; +// left: -12px; +// z-index: -1; +// background: $gray-400; +// } +// } + +// .tree.with-skeleton.opened::before { +// left: 22px; +// top: 33px; +// height: calc(100% - 67px); +// } + +// .tree-link.active ~ .balance-area { +// color: $gray-600 !important; +// } diff --git a/client/ui/index.js b/client/ui/index.js index e00074b8..5200cac0 100644 --- a/client/ui/index.js +++ b/client/ui/index.js @@ -64,6 +64,35 @@ module.exports = { element.parentNode.removeChild(element); }, + on(element, event, selector, handler) { + if (!handler) { + handler = selector; + this.bind(element, event, handler); + } else { + this.delegate(element, event, selector, handler); + } + }, + + off(element, event, handler) { + element.removeEventListener(event, handler); + }, + + bind(element, event, callback) { + event.split(/\s+/).forEach(function (event) { + element.addEventListener(event, callback); + }); + }, + + delegate(element, event, selector, callback) { + element.addEventListener(event, function (e) { + const delegatedTarget = e.target.closest(selector); + if (delegatedTarget) { + e.delegatedTarget = delegatedTarget; + callback.call(this, e, delegatedTarget); + } + }); + }, + empty(element) { while (element.firstChild) { element.removeChild(element.firstChild); diff --git a/client/ui/tree.js b/client/ui/tree.js index 10f731b1..6a8f171a 100644 --- a/client/ui/tree.js +++ b/client/ui/tree.js @@ -2,15 +2,106 @@ const frappe = require('frappejs'); const octicons = require('octicons'); const utils = require('frappejs/client/ui/utils'); -class Tree { - constructor({parent, label, iconSet, withSkeleton, method}) { +const iconSet = { + open: octicons["triangle-down"].toSVG({ width: "12", height: "12", "class": "tree-icon-open" }), + close: octicons["triangle-right"].toSVG({ width: "12", height: "12", "class": "tree-icon-closed" }) +}; + +let TreeTemplate = document.createElement('template'); +TreeTemplate.innerHTML = ` +
+ +
+`; + +class Tree extends HTMLElement { + constructor() { + super(); + + this.attachShadow({ mode: 'open' }) + .appendChild(TreeTemplate.content.cloneNode(true)); + } +} + +window.customElements.define('f-tree', Tree); + +let TreeNodeTemplate = document.createElement('template'); +TreeNodeTemplate.innerHTML = ` + +
+
+
+
+
+ +`; + + +class TreeNode extends HTMLElement { + static get observedAttributes() { + return ['label', 'expanded'] + } + + constructor() { + super(); + + let shadowRoot = this.attachShadow({ mode: 'open' }); + shadowRoot.appendChild(TreeNodeTemplate.content.cloneNode(true)); + this.iconEl = shadowRoot.querySelector('.tree-node-icon'); + this.labelEl = shadowRoot.querySelector('.tree-node-label'); + this.actionsEl = shadowRoot.querySelector('.tree-node-actions'); + } + + attributeChangedCallback(name, oldValue, newValue) { + console.log(name, oldValue, newValue); + + switch(name) { + case 'label': { + this.labelEl.innerHTML = newValue || ''; + break; + } + + case 'expanded': { + this.expanded = this.hasAttribute('expanded'); + + if (this.expanded) { + this.iconEl.innerHTML = iconSet.open; + } else { + this.iconEl.innerHTML = iconSet.close; + } + break; + } + + default: break; + } + } +} + +window.customElements.define('f-tree-node', TreeNode); + +class TreeOld { + constructor({ parent, label, iconSet, withSkeleton, method }) { Object.assign(this, arguments[0]); this.nodes = {}; - if(!iconSet) { + if (!iconSet) { this.iconSet = { - open: octicons["triangle-down"].toSVG({ "width": 10, "class": "node-parent"}), - closed: octicons["triangle-right"].toSVG({ "width": 5, "class": "node-parent"}), - leaf: octicons["primitive-dot"].toSVG({ "width": 7, "class": "node-leaf"}) + open: octicons["triangle-down"].toSVG({ "width": 10, "class": "node-parent" }), + closed: octicons["triangle-right"].toSVG({ "width": 5, "class": "node-parent" }), + leaf: octicons["primitive-dot"].toSVG({ "width": 7, "class": "node-leaf" }) }; } this.make(); @@ -31,7 +122,7 @@ class Tree { // this.loadChildren(this.selectedNode.parentNode, true); } - async loadChildren(node, deep=false) { + async loadChildren(node, deep = false) { let children = !deep ? await this.method(node) : await this.getAllNodes(node); this.renderNodeChildren(node, children); } @@ -40,7 +131,7 @@ class Tree { dataList.map(d => { this.renderNodeChildren(this.nodes[d.parent], d.data); }); } - renderNodeChildren(node, dataSet=[]) { + renderNodeChildren(node, dataSet = []) { frappe.ui.empty(node.childrenList); dataSet.forEach(data => { @@ -67,7 +158,7 @@ class Tree { expandable: expandable, }; - if(parentNode){ + if (parentNode) { node.parentNode = parentNode; node.parent = parentNode.childrenList; node.isRoot = 0; @@ -90,8 +181,8 @@ class Tree { }); let iconHtml = ''; - if(this.iconSet) { - iconHtml = node.expandable ? this.iconSet.closed : this.iconSet.leaf; + if (this.iconSet) { + iconHtml = node.expandable ? this.iconSet.closed : ''; } let labelEl = ` ${node.label}`; @@ -118,17 +209,17 @@ class Tree { async onNodeClick(node, click = true) { this.setSelectedNode(node); - if(click) { + if (click) { this.onClick && this.onClick(node); } await this.expandNode(node); // select link utils.activate(this.tree, node.treeLink, 'tree-link', 'active'); - if(node.toolbar) this.showToolbar(node); + if (node.toolbar) this.showToolbar(node); } async expandNode(node) { - if(node.expandable) { + if (node.expandable) { await this.toggleNode(node); } @@ -139,11 +230,11 @@ class Tree { } async toggleNode(node) { - if(!node.loaded) await this.loadChildren(node); + if (!node.loaded) await this.loadChildren(node); // expand children - if(node.childrenList) { - if(node.childrenList.innerHTML.length) { + if (node.childrenList) { + if (node.childrenList.innerHTML.length) { if (node.expanded) { node.childrenList.classList.add('hide'); } else { @@ -152,7 +243,7 @@ class Tree { } // open close icon - if(this.iconSet) { + if (this.iconSet) { const oldIcon = node.treeLink.querySelector('svg'); const newIconKey = node.expanded ? 'closed' : 'open'; const newIcon = frappe.ui.create(this.iconSet[newIconKey]); diff --git a/client/view/tree.js b/client/view/tree.js index 58262119..d61d6fed 100644 --- a/client/view/tree.js +++ b/client/view/tree.js @@ -56,18 +56,83 @@ module.exports = class BaseTree extends BaseList { } renderTree(rootLabel) { - this.tree = new Tree({ + // const tree = new Tree(); + // tree.getChildNodes = async node => { + // const children = await this.getData(node) || []; + // return children.map(d => ({ + // label: d.name, + // value: d.name, + // expandable: d.isGroup + // })); + // } + // tree.rootNode = { + // label: rootLabel, + // value: rootLabel, + // isRoot: 1, + // expandable: 1 + // } + // this.body.appendChild(tree); + + this.rootNode = { label: rootLabel, - parent: this.body, - method: async node => { - const children = await this.getData(node) || []; - return children.map(d => ({ - label: d.name, - value: d.name, - expandable: d.isGroup - })); - } + value: rootLabel, + isRoot: true, + expanded: true, + children: [] + } + + const getNodeHTML = node => + ` + `; + + this.treeWrapper = frappe.ui.create('f-tree'); + + this.rootNode.el = frappe.ui.create(getNodeHTML(this.rootNode), { + inside: this.treeWrapper }); + + this.treeWrapper = frappe.ui.create(` + + ${getNodeHTML(this.rootNode)} + + `); + + this.body.appendChild(this.treeWrapper); + + frappe.ui.on(this.treeWrapper, 'click', 'f-tree-node', async (e, treeNode) => { + if (treeNode.expanded) { + treeNode.removeAttribute('expanded'); + } else { + treeNode.setAttribute('expanded', ''); + } + + let node = null; + // if (treeNode.hasAttribute('is-root')) { + // node = this.rootNode; + // } else { + + // } + + + }); + + + // this.tree = new Tree({ + // label: rootLabel, + // parent: this.body, + // method: async node => { + // const children = await this.getData(node) || []; + // return children.map(d => ({ + // label: d.name, + // value: d.name, + // expandable: d.isGroup + // })); + // } + // }); } async getData(node) { @@ -119,7 +184,7 @@ module.exports = class BaseTree extends BaseList { }); this.page.body.addEventListener('click', (event) => { - if(event.target.classList.contains('checkbox')) { + if (event.target.classList.contains('checkbox')) { this.trigger('state-change'); } }) @@ -138,7 +203,7 @@ module.exports = class BaseTree extends BaseList { this.searchInput = this.toolbar.querySelector('input'); this.searchInput.addEventListener('keypress', (event) => { - if (event.keyCode===13) { + if (event.keyCode === 13) { this.refresh(); } }); diff --git a/config/rollup.config.style.js b/config/rollup.config.style.js index 2606b955..b219aa95 100644 --- a/config/rollup.config.style.js +++ b/config/rollup.config.style.js @@ -5,7 +5,6 @@ module.exports = { format: 'cjs' }, plugins: [ - require('rollup-plugin-sass')(), require('rollup-plugin-postcss')({ extract: true, plugins: [