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: [