From 9878055ddfe25583675bf96b5e829e6eb45b1503 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 12 Apr 2018 02:04:45 +0530 Subject: [PATCH] More functionality to Tree Component - Make a basecomponent to encapsulate bootstrapping of WC - Remove old tree.js - Refactor Tree Components into JS and HTML --- client/components/baseComponent.js | 43 +++++ client/components/tree/index.html | 11 ++ client/components/tree/index.js | 19 ++ client/components/tree/treeNode.html | 58 ++++++ client/components/tree/treeNode.js | 94 ++++++++++ client/style/tree.scss | 118 +----------- client/ui/index.js | 3 +- client/ui/tree.js | 262 --------------------------- client/view/tree.js | 182 +++++++------------ 9 files changed, 296 insertions(+), 494 deletions(-) create mode 100644 client/components/baseComponent.js create mode 100644 client/components/tree/index.html create mode 100644 client/components/tree/index.js create mode 100644 client/components/tree/treeNode.html create mode 100644 client/components/tree/treeNode.js delete mode 100644 client/ui/tree.js diff --git a/client/components/baseComponent.js b/client/components/baseComponent.js new file mode 100644 index 00000000..0b90ea65 --- /dev/null +++ b/client/components/baseComponent.js @@ -0,0 +1,43 @@ +let templates = {}; + +class BaseComponent extends HTMLElement { + constructor(name) { + super(); + + this._name = name; + this._shadowRoot = this.attachShadow({ mode: 'open' }); + + if (!templates[name]) { + makeTemplate(name, this.templateHTML); + } + + let template = getTemplate(name); + this._shadowRoot.appendChild(template); + } + + triggerEvent(eventName, detail = {}) { + const event = new CustomEvent(eventName, { + bubbles: true, + composed: true, + detail + }); + this.dispatchEvent(event); + } +} + + +function makeTemplate(name, html) { + if (!templates[name]) { + let template = document.createElement('template'); + template.id = name; + template.innerHTML = html; + + templates[name] = template; + } +} + +function getTemplate(name) { + return templates[name].content.cloneNode(true); +} + +module.exports = BaseComponent; diff --git a/client/components/tree/index.html b/client/components/tree/index.html new file mode 100644 index 00000000..17957bc7 --- /dev/null +++ b/client/components/tree/index.html @@ -0,0 +1,11 @@ + + + + +
+ +
diff --git a/client/components/tree/index.js b/client/components/tree/index.js new file mode 100644 index 00000000..1f2d2e31 --- /dev/null +++ b/client/components/tree/index.js @@ -0,0 +1,19 @@ +const BaseComponent = require('../baseComponent'); +const TreeNode = require('./treeNode'); + +class Tree extends BaseComponent { + get templateHTML() { + return require('./index.html'); + } + + constructor() { + super('Tree'); + } +} + +window.customElements.define('f-tree', Tree); + +module.exports = { + Tree, + TreeNode +} diff --git a/client/components/tree/treeNode.html b/client/components/tree/treeNode.html new file mode 100644 index 00000000..c8435088 --- /dev/null +++ b/client/components/tree/treeNode.html @@ -0,0 +1,58 @@ + + + + +
+
+
+
+
+
+ +
+
+
+ +
\ No newline at end of file diff --git a/client/components/tree/treeNode.js b/client/components/tree/treeNode.js new file mode 100644 index 00000000..7e56903f --- /dev/null +++ b/client/components/tree/treeNode.js @@ -0,0 +1,94 @@ +const octicons = require('octicons'); +const BaseComponent = require('../baseComponent'); + +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" }) +}; + +class TreeNode extends BaseComponent { + static get observedAttributes() { + return ['label', 'expanded']; + } + + get templateHTML() { + return require('./treeNode.html'); + } + + constructor() { + super('TreeNode'); + + let shadowRoot = this._shadowRoot; + this.iconEl = shadowRoot.querySelector('.tree-node-icon'); + this.labelEl = shadowRoot.querySelector('.tree-node-label'); + this.actionsEl = shadowRoot.querySelector('.tree-node-actions'); + this.childrenEl = shadowRoot.querySelector('.tree-node-children'); + + this.addEventListener('click', e => { + e.stopImmediatePropagation(); + + if (e.target.matches('[slot="actions"]')) { + this.triggerEvent('tree-node-action', { + actionEl: e.target + }); + return; + } + + if (this.expanded) { + this.removeAttribute('expanded'); + } else { + this.setAttribute('expanded', ''); + } + }); + this.onExpand(); + } + + attributeChangedCallback(name, oldValue, newValue) { + switch(name) { + case 'label': { + this.labelEl.innerHTML = newValue || ''; + break; + } + + case 'expanded': { + const isExpanded = this.hasAttribute('expanded'); + this.onExpand(isExpanded); + break; + } + + default: break; + } + } + + onExpand(isExpanded = false) { + if (this.isLeaf) return; + + if (isExpanded) { + this.iconEl.innerHTML = iconSet.open; + this.childrenEl.style.display = ''; + } else { + this.iconEl.innerHTML = iconSet.close; + this.childrenEl.style.display = 'none'; + } + + this.triggerEvent('tree-node-expand', { + expanded: isExpanded + }); + } + + get isRoot() { + return this.hasAttribute('is-root'); + } + + get expanded() { + return this.hasAttribute('expanded'); + } + + get isLeaf() { + return this.hasAttribute('is-leaf'); + } +} + +window.customElements.define('f-tree-node', TreeNode); + +module.exports = TreeNode; \ No newline at end of file diff --git a/client/style/tree.scss b/client/style/tree.scss index c3f9a670..eb8cad33 100644 --- a/client/style/tree.scss +++ b/client/style/tree.scss @@ -4,120 +4,6 @@ padding: $spacer-3 $spacer-4; } -f-tree, f-tree-node { - display: block; +f-tree-node { + --tree-node-hover: $gray-100; } - -f-tree-node:hover { - background-color: $gray-100; - cursor: pointer; -} - -// .tree { -// padding: $spacer-3 $spacer-4; -// } - -// .tree li { -// list-style: none; -// } - -// ul.tree-children { -// padding-left: $spacer-4; -// } - -// .tree-link { -// cursor: pointer; -// display: flex; -// align-items: center; -// width: 100%; -// } - -// .tree-link:hover { -// background-color: $gray-100; -// } - -// .tree-link .node-parent { -// color: $gray-200; -// width: 24px; -// height: 24px; -// } - -// .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; -// } -// } - -// .tree-hover { -// background-color: $gray-200; -// min-height: 20px; -// border: 1px solid $gray-600; -// } - -// .tree-node-toolbar { -// display: inline-block; -// padding: 0px 5px; -// margin-left: 15px; -// margin-bottom: -4px; -// margin-top: -8px; -// } - -// // @media (max-width: @screen-xs) { -// // ul.tree-children { -// // padding-left: 10px; -// // } -// // } - -// // decoration -// // .tree, .tree-node { -// .tree.with-skeleton, .tree.with-skeleton .tree-node { -// position: relative; - -// &.opened::before, &:last-child::after { -// content: ''; -// position: absolute; -// top: 12px; -// left: 7px; -// height: calc(100% - 23px); -// width: 1px; -// background: $gray-400; -// z-index: -1; -// } - -// &: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 5200cac0..c37f805b 100644 --- a/client/ui/index.js +++ b/client/ui/index.js @@ -3,12 +3,13 @@ const Dropdown = require('./dropdown'); module.exports = { create(tag, obj) { - if(!obj) { + if(tag.includes('<')) { let div = document.createElement('div'); div.innerHTML = tag.trim(); return div.firstChild; } let element = document.createElement(tag); + obj = obj || {}; let $ = (expr, con) => { return typeof expr === "string" diff --git a/client/ui/tree.js b/client/ui/tree.js deleted file mode 100644 index 6a8f171a..00000000 --- a/client/ui/tree.js +++ /dev/null @@ -1,262 +0,0 @@ -const frappe = require('frappejs'); -const octicons = require('octicons'); -const utils = require('frappejs/client/ui/utils'); - -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) { - 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" }) - }; - } - this.make(); - } - - make() { - this.tree = frappe.ui.create('div', { - inside: this.parent, - className: 'tree ' + (this.withSkeleton ? 'with-skeleton' : '') - }); - - this.rootNode = this.makeNode(this.label, this.label, true, null, this.tree); - this.expandNode(this.rootNode); - } - - refresh() { - // this.selectedNode.parentNode && - // this.loadChildren(this.selectedNode.parentNode, true); - } - - async loadChildren(node, deep = false) { - let children = !deep ? await this.method(node) : await this.getAllNodes(node); - this.renderNodeChildren(node, children); - } - - renderChildrenDeep(dataList) { - dataList.map(d => { this.renderNodeChildren(this.nodes[d.parent], d.data); }); - } - - renderNodeChildren(node, dataSet = []) { - frappe.ui.empty(node.childrenList); - - dataSet.forEach(data => { - let parentNode = this.nodes[node.value]; - let childNode = this.makeNode(data.label || data.value, data.value, - data.expandable, parentNode); - childNode.treeLink.dataset.nodeData = data; - }); - node.expanded = false; - - // As children loaded - node.loaded = true; - this.onNodeClick(node, true); - } - - getAllNodes() { } - - makeNode(label, value, expandable, parentNode, parentEl) { - let node = { - label: label, - value: value, - loaded: 0, - expanded: 0, - expandable: expandable, - }; - - if (parentNode) { - node.parentNode = parentNode; - node.parent = parentNode.childrenList; - node.isRoot = 0; - } else { - node.isRoot = 1; - node.parent = parentEl; - } - - this.nodes[value] = node; - this.buildNodeElement(node); - this.onRender && this.onRender(node); - - return node; - } - - buildNodeElement(node) { - node.parentLi = frappe.ui.create('li', { - inside: node.parent, - className: 'tree-node' - }); - - let iconHtml = ''; - if (this.iconSet) { - iconHtml = node.expandable ? this.iconSet.closed : ''; - } - let labelEl = ` ${node.label}`; - - node.treeLink = frappe.ui.create('span', { - inside: node.parentLi, - className: 'tree-link', - 'data-label': node.label, - innerHTML: iconHtml + labelEl - }); - node.treeLink.dataset.node = node; - node.treeLink.addEventListener('click', () => { - this.onNodeClick(node); - }); - - node.childrenList = frappe.ui.create('ul', { - inside: node.parentLi, - className: 'tree-children hide' - }); - - // if(this.toolbar) { - // node.toolbar = this.getToolbar(node).insertAfter(node.treeLink); - // } - } - - async onNodeClick(node, click = true) { - this.setSelectedNode(node); - 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); - } - - async expandNode(node) { - if (node.expandable) { - await this.toggleNode(node); - } - - node.expanded = !node.expanded; - // node.parent.classList.toggle('opened', node.expanded); - node.parent.classList.add('opened'); - node.parentLi.classList.add('opened'); - } - - async toggleNode(node) { - if (!node.loaded) await this.loadChildren(node); - - // expand children - if (node.childrenList) { - if (node.childrenList.innerHTML.length) { - if (node.expanded) { - node.childrenList.classList.add('hide'); - } else { - node.childrenList.classList.remove('hide'); - } - } - - // open close icon - if (this.iconSet) { - const oldIcon = node.treeLink.querySelector('svg'); - const newIconKey = node.expanded ? 'closed' : 'open'; - const newIcon = frappe.ui.create(this.iconSet[newIconKey]); - node.treeLink.replaceChild(newIcon, oldIcon); - } - } - } - - getSelectedNode() { return this.selectedNode; } - - setSelectedNode(node) { this.selectedNode = node; } - - showToolbar() { } -} - -module.exports = Tree; diff --git a/client/view/tree.js b/client/view/tree.js index d61d6fed..97d117eb 100644 --- a/client/view/tree.js +++ b/client/view/tree.js @@ -1,6 +1,6 @@ const frappe = require('frappejs'); const BaseList = require('./list'); -const Tree = require('frappejs/client/ui/tree'); +const Tree = require('frappejs/client/components/tree'); // const keyboard = require('frappejs/client/ui/keyboard'); module.exports = class BaseTree extends BaseList { @@ -56,83 +56,62 @@ module.exports = class BaseTree extends BaseList { } renderTree(rootLabel) { - // 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, value: rootLabel, isRoot: true, - expanded: true, - children: [] + isGroup: true, + children: null } - 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.getTreeNodeHTML(this.rootNode)} `); + const rootNode = this.treeWrapper.querySelector('f-tree-node[is-root]'); + rootNode.props = 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', ''); + frappe.ui.on(this.treeWrapper, 'tree-node-expand', 'f-tree-node', async (e, treeNode) => { + if (!treeNode.expanded) return; + + if (!treeNode.props.children) { + const data = await this.getData(treeNode.props); + const children = data.map(d => ({ + label: d.name, + value: d.name, + isGroup: d.isGroup, + doc: d + })); + treeNode.props.children = children; + + for (let child of children) { + const childNode = frappe.ui.create(this.getTreeNodeHTML(child)); + childNode.props = child; + treeNode.appendChild(childNode); + } } - - let node = null; - // if (treeNode.hasAttribute('is-root')) { - // node = this.rootNode; - // } else { - - // } - - }); + frappe.ui.on(this.treeWrapper, 'tree-node-action', 'f-tree-node', (e, treeNode) => { + if (treeNode.isRoot) return; - // 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 - // })); - // } - // }); + const button = e.detail.actionEl; + const action = button.getAttribute('data-action'); + + if (action === 'edit') { + this.edit(treeNode.props.doc.name); + } + }); + + rootNode.click(); // open the root node + } + + edit(name) { + frappe.desk.showFormModal(this.doctype, name); } async getData(node) { @@ -154,63 +133,36 @@ module.exports = class BaseTree extends BaseList { }); } + getTreeNodeHTML(node) { + return ( + ` + ${this.getActionButtonsHTML()} + ` + ); + } + + getActionButtonsHTML() { + return [ + { id: 'edit', label: frappe._('Edit') } + // { id: 'addChild', label: frappe._('Add Child') }, + // { id: 'delete', label: frappe._('Delete') }, + ].map(button => { + return ``; + }) + .join(''); + } + getFields() { let fields = [this.treeSettings.parentField, 'isGroup'] this.updateStandardFields(fields); return fields; } - - makeToolbar() { - this.makeSearch(); - - this.btnNew = this.page.addButton(frappe._('New'), 'btn-primary', async () => { - await frappe.router.setRoute('new', this.doctype); - }); - - this.btnDelete = this.page.addButton(frappe._('Delete'), 'btn-secondary hide', async () => { - await frappe.db.deleteMany(this.doctype, this.getCheckedRowNames()); - await this.refresh(); - }); - - this.btnReport = this.page.addButton(frappe._('Report'), 'btn-outline-secondary hide', async () => { - await frappe.router.setRoute('table', this.doctype); - }); - - this.on('state-change', () => { - const checkedCount = this.getCheckedRowNames().length; - this.btnDelete.classList.toggle('hide', checkedCount ? false : true); - this.btnNew.classList.toggle('hide', checkedCount ? true : false); - this.btnReport.classList.toggle('hide', checkedCount ? true : false); - }); - - this.page.body.addEventListener('click', (event) => { - if (event.target.classList.contains('checkbox')) { - this.trigger('state-change'); - } - }) - } - - makeSearch() { - this.toolbar = frappe.ui.add('div', 'list-toolbar', this.parent); - this.toolbar.innerHTML = ` - - `; - - this.searchInput = this.toolbar.querySelector('input'); - this.searchInput.addEventListener('keypress', (event) => { - if (event.keyCode === 13) { - this.refresh(); - } - }); - - this.btnSearch = this.toolbar.querySelector('.btn-search'); - this.btnSearch.addEventListener('click', (event) => { - this.refresh(); - }); - } };