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/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..eb8cad33 100644 --- a/client/style/tree.scss +++ b/client/style/tree.scss @@ -1,111 +1,9 @@ @import "./variables.scss"; -.tree { +.tree-body { 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-600; - width: 24px; - height: 24px; - text-align: center; -} - -.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; +f-tree-node { + --tree-node-hover: $gray-100; } diff --git a/client/ui/index.js b/client/ui/index.js index e00074b8..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" @@ -64,6 +65,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 deleted file mode 100644 index 10f731b1..00000000 --- a/client/ui/tree.js +++ /dev/null @@ -1,171 +0,0 @@ -const frappe = require('frappejs'); -const octicons = require('octicons'); -const utils = require('frappejs/client/ui/utils'); - -class Tree { - 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 : this.iconSet.leaf; - } - 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 58262119..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,18 +56,62 @@ module.exports = class BaseTree extends BaseList { } renderTree(rootLabel) { - this.tree = new Tree({ + this.rootNode = { label: rootLabel, - parent: this.body, - method: async node => { - const children = await this.getData(node) || []; - return children.map(d => ({ + value: rootLabel, + isRoot: true, + isGroup: true, + children: null + } + + this.treeWrapper = frappe.ui.create(` + + ${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, '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, - expandable: d.isGroup + 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); + } } }); + + frappe.ui.on(this.treeWrapper, 'tree-node-action', 'f-tree-node', (e, treeNode) => { + if (treeNode.isRoot) return; + + 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) { @@ -89,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(); - }); - } }; 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: [