2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

Merge pull request #46 from frappe/tree-web-component

Tree - Web Component
This commit is contained in:
Faris Ansari 2018-04-12 02:09:31 +05:30 committed by GitHub
commit 700464f925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 337 additions and 340 deletions

View File

@ -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;

View File

@ -0,0 +1,11 @@
<!-- styles -->
<style>
:host {
display: block;
}
</style>
<!-- template -->
<div class="tree">
<slot></slot>
</div>

View File

@ -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
}

View File

@ -0,0 +1,58 @@
<!-- styles -->
<style>
:host {
display: block;
}
.tree-node {
display: flex;
align-items: center;
}
.tree-node:hover {
background-color: gray;
background-color: var(--tree-node-hover);
cursor: pointer;
}
.tree-node:hover .tree-node-actions {
display: block;
}
.tree-node-content {
display: flex;
align-items: center;
}
.tree-node-actions {
display: none;
margin-left: 2rem;
}
.tree-node-icon {
width: 2rem;
height: 2rem;
}
.tree-node-icon svg {
padding: 0.5rem;
}
.tree-node-children {
padding-left: 2rem;
}
</style>
<!-- template -->
<div class="tree-node">
<div class="tree-node-content">
<div class="tree-node-icon"></div>
<div class="tree-node-label"></div>
</div>
<div class="tree-node-actions">
<slot name="actions"></slot>
</div>
</div>
<div class="tree-node-children">
<slot></slot>
</div>

View File

@ -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;

View File

@ -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";

View File

@ -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;
}

View File

@ -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);

View File

@ -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 = `<span class="tree-label"> ${node.label}</span>`;
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;

View File

@ -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(`
<f-tree>
${this.getTreeNodeHTML(this.rootNode)}
</f-tree>
`);
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 (
`<f-tree-node
label="${node.label}"
value="${node.value}"
${node.expanded ? 'expanded' : ''}
${node.isRoot ? 'is-root' : ''}
${node.isGroup ? '' : 'is-leaf'}
>
${this.getActionButtonsHTML()}
</f-tree-node>`
);
}
getActionButtonsHTML() {
return [
{ id: 'edit', label: frappe._('Edit') }
// { id: 'addChild', label: frappe._('Add Child') },
// { id: 'delete', label: frappe._('Delete') },
].map(button => {
return `<button class="btn btn-link btn-sm m-0" slot="actions" data-action="${button.id}">
${button.label}
</button>`;
})
.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 = `
<div class="input-group list-search">
<input class="form-control" type="text" placeholder="Search...">
<div class="input-group-append">
<button class="btn btn-outline-secondary btn-search">Search</button>
</div>
</div>
`;
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();
});
}
};

View File

@ -5,7 +5,6 @@ module.exports = {
format: 'cjs'
},
plugins: [
require('rollup-plugin-sass')(),
require('rollup-plugin-postcss')({
extract: true,
plugins: [