diff --git a/dist/index.html b/dist/index.html index 45fcc8e2..25f14d03 100644 --- a/dist/index.html +++ b/dist/index.html @@ -28,10 +28,15 @@ } return data; } - var instance = new jsTree({}, document.getElementById('tree')); + var instance = new jsTree({ + plugins:{ + checkbox:jsTree.plugin.checkbox + } + // , renderer : 'scroller' + }, document.getElementById('tree')); instance .empty() - .create(dummy(2, 10)) + .create(dummy(3, 4)) .openAll(); console.log(c); diff --git a/dist/jstree.css b/dist/jstree.css index f9d8f983..3f1db355 100644 --- a/dist/jstree.css +++ b/dist/jstree.css @@ -27,4 +27,25 @@ .jstree-node-icon:before { content: " "; width:60%; height:45%; left:20%; top:32%; background:navy; position:absolute; border-radius:0 1px 1px 1px; } .jstree-node-icon:after { content: " "; width:30%; height:10%; left:20%; top:22%; background:navy; position:absolute; border-radius:1px 1px 0 0; } .jstree-selected .jstree-node-icon:after, -.jstree-selected .jstree-node-icon:before { background:white; } \ No newline at end of file +.jstree-selected .jstree-node-icon:before { background:white; } +.jstree-icon-checkbox { + text-align : center; + display : inline-block; + width : var(--jstree-size); + height : var(--jstree-size); + line-height : var(--jstree-size); + position : relative; + vertical-align : middle; +} + +.jstree-icon-checkbox-checked { + background-image: url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter id='svg_3_blur'%3E%3CfeGaussianBlur stdDeviation='0.5' in='SourceGraphic'/%3E%3C/filter%3E%3C/defs%3E%3Cg%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3Cpath stroke-width='3' id='svg_3' d='m8.9609,13.93296c4.35754,8.49162 5.13966,9.83241 5.11731,9.75419c0.02235,0.07822 8.9609,-15.56424 8.93855,-15.64246' opacity='NaN' stroke='%23000' fill='none'/%3E%3Cpath filter='url(%23svg_3_blur)' stroke-width='3' id='svg_3' d='m8.9609,13.93296c4.35754,8.49162 5.13966,9.83241 5.11731,9.75419c0.02235,0.07822 8.9609,-15.56424 8.93855,-15.64246' opacity='NaN' stroke='%23628C52' fill='none'/%3E%3C/g%3E%3C/svg%3E"); +} + +.jstree-icon-checkbox-unchecked { + background-image: url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cg%3E%3Ctitle%3ELayer 1%3C/title%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3C/g%3E%3C/svg%3E"); +} + +.jstree-icon-checkbox-indeterminate { + background-image: url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter height='200%25' width='200%25' y='-50%25' x='-50%25' id='svg_2_blur'%3E%3CfeGaussianBlur stdDeviation='1' in='SourceGraphic'/%3E%3C/filter%3E%3C/defs%3E%3Cg%3E%3Ctitle%3ELayer 1%3C/title%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3Crect rx='4' filter='url(%23svg_2_blur)' id='svg_2' height='16' width='16' y='8' x='8' stroke='%23000' fill='%23729C62'/%3E%3C/g%3E%3C/svg%3E"); +} \ No newline at end of file diff --git a/dist/jstree.js b/dist/jstree.js index 1c767a2a..bc1dc032 100644 --- a/dist/jstree.js +++ b/dist/jstree.js @@ -1245,3 +1245,204 @@ jsTree.defaults = { }; + + + +if (!jsTree.plugin) + jsTree.plugin = {} + +jsTree.plugin.checkbox = function (options) { + const defaults = { + visible : false, + cascade : { up : true, down : true, indeterminate : true, includeHidden : true, includeDisabled : true }, + selection : true, + wholeNode : false + }; + options = jsTree._extend({}, defaults, options); + this.on( + `check uncheck checkAll uncheckAll`, + () => this.redraw() + ); + this.addRenderHandler(function (dom, node) { + // text (node content including icon) + const a = document.evaluate("a[contains(@class,'jstree-node-text')]",dom,null,XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue + // checkbox should/will be in front... + let checkBoxDomElement = a.previousSibling; + if (!checkBoxDomElement.classList.contains("jstree-icon-checkbox")) { + // checkbox element needs to be created... + checkBoxDomElement = document.createElement("i") + + if (this.getState(node, "checked", false)) + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-checked" + else if (this.getState(node, "indeterminate", false)) + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-indeterminate" + else + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-unchecked" + + // ... and added first. + a.parentElement.insertBefore(checkBoxDomElement,a); + + // register event listner to trigger check change ... + checkBoxDomElement.addEventListener('click', (e) => { + if (this.getState(node,"checked",false)) + this.uncheck(node); + else + this.check(node); + }); + } else { + // checkbox state will be set/updated + if (this.getState(node, "checked", false)) + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-checked") + else if (this.getState(node, "indeterminate", false)) + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-indeterminate") + else + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-unchecked") + } + }); + // TODO: all other plugin stuff (cascade, etc) + + // checkboxes (just visual indication - redraw involved nodes or loop and apply minor changes) + this.check = function(node) { + if (Array.isArray(node)) { + node.forEach(x => this.check(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "checked", true); + this.setState(node, "indeterminate", false); + this.trigger("check", { node : node }); + if (options.cascade.down) { + if (!this.getState(node, 'loaded', true)) { + this.load(node, () => this.check(node)); + } else { + this.check(node.children); + } + } + if (options.cascade.up) { + const traverseUpIndeterminate = (node)=>{ + const parent = node.getParent(); + if (parent) { + const nodeAndSiblings = parent.getChildren(); + if (nodeAndSiblings.every(s=>this.getState(s,"checked"))) // if 'checked'===true we know that indetermiante must be false + { + if (!this.getState(parent,"checked")) + this.check(parent) // this will tirgger update at parent as well (if required) + } else if ( + options.cascade.indeterminate && + nodeAndSiblings.some(s=>this.getState(s,"checked") || this.getState(s,"indeterminate",false)) + ) { + if (!this.getState(parent,"indeterminate")) + { + this.setState(parent,"indeterminate",true); + // since we don't have a 'undeterminate' setter, we need to traverse this up 'manually' + traverseUpIndeterminate(parent); + } + } + } + } + traverseUpIndeterminate(node); + } + } + return this; + } + this.uncheck = function(node) { + if (Array.isArray(node)) { + node.forEach(x => this.uncheck(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "checked", false); + this.setState(node, "indeterminate", false); + this.trigger("uncheck", { node : node }); + if (options.cascade.down) { + if (!this.getState(node, 'loaded', true)) { + this.load(node, () => this.uncheck(node)); + } else { + this.uncheck(node.children); + } + } + if (options.cascade.up) { + const traverseUpIndeterminate = (node)=>{ + const parent = node.getParent(); + if (parent) { + const nodeAndSiblings = parent.getChildren(); + if (nodeAndSiblings.every(s=>!this.getState(s,"checked") && !this.getState(s,"indeterminate"))) + { + if (this.getState(parent,"checked") || this.getState(parent,"indeterminate")) + { + this.uncheck(parent) // this will tirgger update at parent as well (if required) + } + } else if ( + options.cascade.indeterminate && + nodeAndSiblings.some(s=>this.getState(s,"checked",false) || this.getState(s,"indeterminate",false)) + ) { + if (this.getState(parent,"checked",false)) + { + this.setState(parent,"checked",false); + this.setState(parent,"indeterminate",true); + // since we don't have a 'undeterminate' setter, we need to traverse this up 'manually' + // also to avoid infitive update loop... + traverseUpIndeterminate(parent); + } + } + } + } + traverseUpIndeterminate(node); + } + } + return this; + } + this.checkAll = function() { + for (let node of this.tree) { + this.setState(node, "checked", true); + this.setState(node, "indeterminate", false); + } + this.trigger("checkAll"); + return this; + } + this.uncheckAll = function() { + for (let node of this.tree) { + this.setState(node, "checked", false); + this.setState(node, "indeterminate", false); + } + this.trigger("uncheckAll"); + return this; + } + this.getChecked = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? + return this.tree.find(function (node) { + return node.data && node.data.state && node.data.state.checked; + }); + } + this.getTopChecked = function() { + let recurse = function * (node) { + for (let child of node.children) { + if (node.data.state && node.data.state.checked) { + yield child; + } + yield * recurse(child); + } + }; + return Array.from(recurse(this.root)); + } + this.getBottomChecked = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? + let recurse = function * (node) { + for (let child of node.children) { + if (node.data.state && node.data.state.checked && !child.children.length) { + yield child; + } + yield * recurse(child); + } + }; + return Array.from(recurse(this.root)); + } + this.getIndeterminate = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? + return this.tree.find(function (node) { + return node.data && node.data.state && node.data.state.indeterminate; + }); + } +}; diff --git a/dist/jstree.min.css b/dist/jstree.min.css index 33fd3c01..af514ae1 100644 --- a/dist/jstree.min.css +++ b/dist/jstree.min.css @@ -4,4 +4,4 @@ * @link https://www.jstree.com * @license MIT */ -.jstree{--jstree-size:32px;--jstree-size-half:calc(var(--jstree-size) / 2);--jstree-size-quater:calc(var(--jstree-size) / 4)}.jstree-node{height:var(--jstree-size);white-space:nowrap}.jstree-icon{text-align:center;display:inline-block;width:var(--jstree-size);height:var(--jstree-size);line-height:var(--jstree-size);position:relative}.jstree-icon-i{background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1px' height='2px'%3e%3ccircle cx='0.5' cy='1.5' r='0.5' /%3e%3c/svg%3e") center bottom repeat-y}.jstree-icon-t{background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1px' height='2px'%3e%3ccircle cx='0.5' cy='1.5' r='0.5' /%3e%3c/svg%3e") center bottom repeat-y}.jstree-icon-h:before{content:" ";background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='2px' height='1px'%3e%3ccircle cx='0.5' cy='0.5' r='0.5' /%3e%3c/svg%3e") left bottom repeat-x;top:0;left:var(--jstree-size-half);height:var(--jstree-size-half);width:var(--jstree-size-half);position:absolute}.jstree-icon-v2:after{content:" ";background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1px' height='2px'%3e%3ccircle cx='0.5' cy='1.5' r='0.5' /%3e%3c/svg%3e") left bottom repeat-y;top:0;left:var(--jstree-size-half);height:var(--jstree-size-half);width:var(--jstree-size-half);position:absolute}.jstree-icon-v:after{content:" ";background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1px' height='2px'%3e%3ccircle cx='0.5' cy='1.5' r='0.5' /%3e%3c/svg%3e") left bottom repeat-y;top:0;left:var(--jstree-size-half);height:var(--jstree-size);width:var(--jstree-size-half);position:absolute}.jstree-closed,.jstree-leaf,.jstree-opened{cursor:pointer;display:inline-block;width:var(--jstree-size);height:var(--jstree-size);line-height:var(--jstree-size)}.jstree-closed:after,.jstree-opened:after{content:" ";display:inline-block;border:5px solid transparent;border-left-color:#000;height:0;width:0;z-index:1;position:absolute;left:var(--jstree-size-half);top:var(--jstree-size-half);margin-top:-5px;margin-left:-2px}.jstree-opened:after{transform:rotate(45deg);margin-top:-3px}.jstree-node-text{border-radius:2px;text-decoration:none;display:inline-block;padding:0 var(--jstree-size-quater) 0 0;height:var(--jstree-size)}.jstree-node-text{color:navy}.jstree-node-text:hover{background:silver}.jstree-selected,.jstree-selected:hover{background:navy;color:#fff}.jstree-node-icon:before{content:" ";width:60%;height:45%;left:20%;top:32%;background:navy;position:absolute;border-radius:0 1px 1px 1px}.jstree-node-icon:after{content:" ";width:30%;height:10%;left:20%;top:22%;background:navy;position:absolute;border-radius:1px 1px 0 0}.jstree-selected .jstree-node-icon:after,.jstree-selected .jstree-node-icon:before{background:#fff} \ No newline at end of file +.jstree{--jstree-size:32px;--jstree-size-half:calc(var(--jstree-size) / 2);--jstree-size-quater:calc(var(--jstree-size) / 4)}.jstree-node{height:var(--jstree-size);white-space:nowrap}.jstree-icon{text-align:center;display:inline-block;width:var(--jstree-size);height:var(--jstree-size);line-height:var(--jstree-size);position:relative}.jstree-icon-i{background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1px' height='2px'%3e%3ccircle cx='0.5' cy='1.5' r='0.5' /%3e%3c/svg%3e") center bottom repeat-y}.jstree-icon-t{background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1px' height='2px'%3e%3ccircle cx='0.5' cy='1.5' r='0.5' /%3e%3c/svg%3e") center bottom repeat-y}.jstree-icon-h:before{content:" ";background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='2px' height='1px'%3e%3ccircle cx='0.5' cy='0.5' r='0.5' /%3e%3c/svg%3e") left bottom repeat-x;top:0;left:var(--jstree-size-half);height:var(--jstree-size-half);width:var(--jstree-size-half);position:absolute}.jstree-icon-v2:after{content:" ";background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1px' height='2px'%3e%3ccircle cx='0.5' cy='1.5' r='0.5' /%3e%3c/svg%3e") left bottom repeat-y;top:0;left:var(--jstree-size-half);height:var(--jstree-size-half);width:var(--jstree-size-half);position:absolute}.jstree-icon-v:after{content:" ";background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1px' height='2px'%3e%3ccircle cx='0.5' cy='1.5' r='0.5' /%3e%3c/svg%3e") left bottom repeat-y;top:0;left:var(--jstree-size-half);height:var(--jstree-size);width:var(--jstree-size-half);position:absolute}.jstree-closed,.jstree-leaf,.jstree-opened{cursor:pointer;display:inline-block;width:var(--jstree-size);height:var(--jstree-size);line-height:var(--jstree-size)}.jstree-closed:after,.jstree-opened:after{content:" ";display:inline-block;border:5px solid transparent;border-left-color:#000;height:0;width:0;z-index:1;position:absolute;left:var(--jstree-size-half);top:var(--jstree-size-half);margin-top:-5px;margin-left:-2px}.jstree-opened:after{transform:rotate(45deg);margin-top:-3px}.jstree-node-text{border-radius:2px;text-decoration:none;display:inline-block;padding:0 var(--jstree-size-quater) 0 0;height:var(--jstree-size)}.jstree-node-text{color:navy}.jstree-node-text:hover{background:silver}.jstree-selected,.jstree-selected:hover{background:navy;color:#fff}.jstree-node-icon:before{content:" ";width:60%;height:45%;left:20%;top:32%;background:navy;position:absolute;border-radius:0 1px 1px 1px}.jstree-node-icon:after{content:" ";width:30%;height:10%;left:20%;top:22%;background:navy;position:absolute;border-radius:1px 1px 0 0}.jstree-selected .jstree-node-icon:after,.jstree-selected .jstree-node-icon:before{background:#fff}.jstree-icon-checkbox{text-align:center;display:inline-block;width:var(--jstree-size);height:var(--jstree-size);line-height:var(--jstree-size);position:relative;vertical-align:middle}.jstree-icon-checkbox-checked{background-image:url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter id='svg_3_blur'%3E%3CfeGaussianBlur stdDeviation='0.5' in='SourceGraphic'/%3E%3C/filter%3E%3C/defs%3E%3Cg%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3Cpath stroke-width='3' id='svg_3' d='m8.9609,13.93296c4.35754,8.49162 5.13966,9.83241 5.11731,9.75419c0.02235,0.07822 8.9609,-15.56424 8.93855,-15.64246' opacity='NaN' stroke='%23000' fill='none'/%3E%3Cpath filter='url(%23svg_3_blur)' stroke-width='3' id='svg_3' d='m8.9609,13.93296c4.35754,8.49162 5.13966,9.83241 5.11731,9.75419c0.02235,0.07822 8.9609,-15.56424 8.93855,-15.64246' opacity='NaN' stroke='%23628C52' fill='none'/%3E%3C/g%3E%3C/svg%3E")}.jstree-icon-checkbox-unchecked{background-image:url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cg%3E%3Ctitle%3ELayer 1%3C/title%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3C/g%3E%3C/svg%3E")}.jstree-icon-checkbox-indeterminate{background-image:url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter height='200%25' width='200%25' y='-50%25' x='-50%25' id='svg_2_blur'%3E%3CfeGaussianBlur stdDeviation='1' in='SourceGraphic'/%3E%3C/filter%3E%3C/defs%3E%3Cg%3E%3Ctitle%3ELayer 1%3C/title%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3Crect rx='4' filter='url(%23svg_2_blur)' id='svg_2' height='16' width='16' y='8' x='8' stroke='%23000' fill='%23729C62'/%3E%3C/g%3E%3C/svg%3E")} \ No newline at end of file diff --git a/dist/jstree.min.js b/dist/jstree.min.js index aa3c50b2..97ece684 100644 --- a/dist/jstree.min.js +++ b/dist/jstree.min.js @@ -10,4 +10,1445 @@ * @link https://www.jstree.com * @license MIT */ -class Flat{_empty(t){for(;t.firstChild;)t.removeChild(t.firstChild);return t}constructor(t,e){this.dataSource=e,this.dataSource.count||(this.dataSource.count=()=>0),this.dataSource.create||(this.dataSource.create=()=>document.createElement("div")),this.dataSource.wrapper||(this.dataSource.wrapper=()=>document.createElement("div")),this.container=this._empty(t),this.wrapper=this.dataSource.wrapper(),this.wrapper.className+=" vakata-scroller-wrapper",this.container.appendChild(this.wrapper),this.count=0,this.render()}render(){this.updateAll(!0)}updateItem(t,e,s=!0){this.dataSource.update(t,e,s)}updateAll(t=!0){if(t){this.count=this.dataSource.count();let t=document.createDocumentFragment();for(let e=0;e{let i=arguments;clearTimeout(s),s=setTimeout(()=>t.apply(this,i),e)}}_processScroll(){let t=this.scrollParent.scrollTop-this._state.scroll-this._state.topPosition,e=this._state.render*this._state.height/4,s=Math.floor(Math.abs(t-e)/this._state.height),i=()=>{let t=this._state.topPosition;(t+=this._state.render*this._state.height){let t=this._state.bottomPosition;(t-=this._state.render*this._state.height)>=0&&(this._state.index--,this._state.bottom.pos=t,this._state.top=this._state.bottom,this._state.bottom=this._state.bottom.previousSibling||this.wrapper.lastChild,this._state.topPosition=this._state.index*this._state.height,this._state.bottomPosition=this._state.topPosition+(this._state.render-1)*this._state.height)};if(t>e)for(let t=0;te&&(e=s.width)}while(s=s.nextSibling);(t||i!==e)&&(this.wrapper.width=e,this.wrapper.style.minWidth=e+"px")}constructor(t,e){this.dataSource=e,this.dataSource.count||(this.dataSource.count=()=>0),this.dataSource.create||(this.dataSource.create=()=>document.createElement("div")),this.dataSource.wrapper||(this.dataSource.wrapper=()=>document.createElement("div")),this._state={height:0,count:this.dataSource.count(),scroll:0,render:0,visible:0,index:0,top:null,bottom:null},this.container=this._empty(t),this.wrapper=this.dataSource.wrapper(),this.wrapper.className+=" vakata-scroller-wrapper",this.wrapper.style.position="relative",this.container.appendChild(this.wrapper),this.scrollParent=this._scrollParent(this.wrapper);let s=this.scrollParent===document.documentElement?window:this.scrollParent;this.render(),window.addEventListener("resize",this._debounce(this.render,50)),s.addEventListener("scroll",()=>{this.scrollID&&window.cancelAnimationFrame(this.scrollID),this.scrollID=window.requestAnimationFrame(()=>this._processScroll())})}render(){let t=this.dataSource.create();t.className+=" vakata-scroller-item",t.innerHTML=" ",this.wrapper.appendChild(t),this._state.height=t.offsetHeight,this._state.count=this.dataSource.count(),this.wrapper.removeChild(t),this.wrapper.style.height=this._state.count*this._state.height+"px",this._state.scroll=0,this.scrollParent!==this.container&&(this._state.scroll=this.scrollParent===document.documentElement?this._offset(this.container):this._offset(this.container)-this._offset(this.scrollParent)),this._state.visible=Math.min(window.innerHeight,this.scrollParent.offsetHeight),this._state.render=2*Math.max(1,Math.ceil(this._state.visible/this._state.height));let e=document.createDocumentFragment();for(let t=0;t0}isEmpty(){return 0===this.children.length}getAncestors(){for(var t=[],e=this.getParent();e&&e.data!==TreeNode.getRootSymbol();)t.push(e),e=e.getParent();return t}getIndex(){return null===this.parent?0:this.parent.children.indexOf(this)}isLast(){if(null===this.parent)return!0;var t=this.parent.children;return t[t.length-1]===this}getChildren(){return this.children}getDescendants(){var t=[];let e=function(s){for(let i of s.children)t.push(i),e(i)};return e(this),t}addChild(t,e){return Array.isArray(t)?(t.forEach((t,s)=>this.addChild(t,void 0===e?e:e+s)),this):(t.parent&&t.parent.children.splice(t.parent.children.indexOf(t),1),t.parent=this,void 0===e?this.children.push(t):this.children.splice(e,0,t),this)}delete(){return this.parent.children.splice(this.parent.children.indexOf(this),1),this.parent=null,this}empty(){this.children=[]}move(t,e){t.addChild(this,e)}copy(t,e){t.addChild(TreeNode.fromJSON(this.toJSON("children","data"),"children","data"),e)}get(t,e=null){return this.data[t]||e}set(t,e){return this.data[t]=e,this}data(t){return void 0!==t&&(this.data=t),this.data}toJSON(t="children",e=""){if(e)return{[e]:this.data,[t]:this.children.map(s=>s.toJSON(t,e))};let s=this.data;return s[t]=this.children.map(s=>s.toJSON(t,e)),s}toFlatJSON(t="id",e="id",s="parent",i="position",r=""){let a=[],n=o=>{if(r)a.push({[e]:o.data[t],[s]:o.parent?o.parent.data[t]:null,[i]:o.getIndex(),[r]:o.data});else{let r=o.data;r[e]=o.data[t],r[s]=o.parent?o.parent.data[t]:null,r[i]=o.getIndex(),a.push(r)}o.children.forEach(n)};return n(this),a}static fromJSON(t,e="children",s=""){if(Array.isArray(t))return t.map(t=>TreeNode.fromJSON(t,e));if(!s){let i={},r=[];for(let s of Object.keys(t))s!==e?i[s]=t[s]:r=t[s];let a=new TreeNode(i);return r&&r.forEach(t=>a.addChild(TreeNode.fromJSON(t,e,s))),a}let i=new TreeNode(t[s]);return t[e]&&t[e].forEach(t=>i.addChild(TreeNode.fromJSON(t,e,s))),i}static fromFlatJSON(t,e="id",s="parent",i="position",r=""){var a=new Map;return t.forEach(t=>{a[t[e]]={data:r?t[r]:t,children:[]}}),a.forEach((t,e)=>{a.has(e[s])&&a.get(e[s]).children.push(e)}),i&&a.forEach((t,e)=>{e.children.sort((function(t,e){return(t=t[i])===(e=e[i])?0:t{a.has(e[s])&&a.delete(t)}),TreeNode.fromJSON(Array.from(a.values()),"children","data")}static getRootSymbol(){return TreeNode.root||(TreeNode.root=Symbol("Root symbol")),TreeNode.root}static getRoot(){return new TreeNode(TreeNode.getRootSymbol())}}class Tree{constructor(){this.root=TreeNode.getRoot()}addRoot(t,e){return this.root.addChild(t,e)}getRoots(){return this.root.getChildren()}isEmpty(){return this.root.isEmpty()}empty(){return this.root.empty()}toJSON(t="children",e=""){return this.root.toJSON(t,e).children}toFlatJSON(t="id",e="id",s="parent",i="position",r=""){let a=this.root.toFlatJSON(t,e,s,i,r);return delete a[0],a}_normalizeCriteria(t){return"function"==typeof t?t:("object"!=typeof t&&(t={id:t}),function(e){for(let s of t.keys())if(e.data[s]!==t[s])return!1;return!0})}find(t){t=this._normalizeCriteria(t);let e=[];for(let s of this)t(s)&&e.push(s);return e}findFirst(t){t=this._normalizeCriteria(t);for(let e of this)if(t(e))return e;return null}*filter(t){t=this._normalizeCriteria(t);for(let e of this)t(e)&&(yield e)}*[Symbol.iterator](){let t=function*(e){for(let s of e.children)yield s,yield*t(s)};yield*t(this.root)}}class jsTree{static _extend(t){t=t||{};for(let e=1;ethis.redraw()),this.on("open close openAll closeAll\n hide show hideAll showAll\n empty create delete move copy",()=>this.redraw(!0)),e&&this.render(e),Object.entries(this.options.plugins).forEach(t=>jsTree.plugin[t[0]].call(this,t[1]||{})),this.options.data instanceof Function){let t=this;this.options.data(null,(function(e){t.empty().create(e,null)}),(function(){}))}else this.empty().create(t.data,null)}addCreateHandler(t){this.handlers.create.push(t)}addRenderHandler(t){this.handlers.render.push(t)}node(t){if(t instanceof TreeNode)return t;if(t instanceof HTMLElement){for(;!t.classList.contains("jstree-node");){if(!t.parentNode||t.parentNode===window.document)return null;t=t.parentNode}return this._nodeList[parseInt(t.getAttribute("data-index"),10)]||null}return"object"!=typeof t&&"function"!=typeof t&&(t={[this.options.idProperty]:t}),this.tree.findFirst(t)}find(t){return"object"!=typeof t&&"function"!=typeof t&&(t={[this.options.idProperty]:t}),this.tree.find(t)}*filter(t){"object"!=typeof t&&"function"!=typeof t&&(t={[this.options.idProperty]:t}),yield*this.tree.filter(t)}*[Symbol.iterator](){yield*this.tree[Symbol.iterator]()}getState(t,e,s=null){return this.node(t).data.state&&void 0!==this.node(t).data.state[e]?this.node(t).data.state[e]:s}setState(t,e,s){return(t=this.node(t)).data.state||(t.data.state={}),t.data.state[e]!==s&&(t.data.state[e]=s,t._dirty=!0),this}trigger(t,e={}){return e.event=t,e.instance=this,this.events[t]&&this.events[t].forEach(t=>t(e)),this.events["*"]&&this.events["*"].forEach(t=>t(e)),this}on(t,e){let s=t.split(" ");return s.length>1?(s.forEach(t=>this.on(t,e)),this):(t=s[0],this.events[t]||(this.events[t]=[]),this.events[t].push(e),this)}off(t,e=null){let s=t.split(" ");if(s.length>1)return s.forEach(t=>this.off(t,e)),this;if(t=s[0],this.events[t]){null===e&&(this.events[t]=[]),-1!==this.events[t].indexOf(e)&&this.events[t].splice(this.events[t].indexOf(e),1)}return this}load(t,e,s){if(Array.isArray(t))return t.forEach(t=>this.load(t,e,s)),this;if(t=this.node(t),!tree.getState(t,"loading")&&!tree.getState(t,"loaded",!0)&&this.options.data instanceof Function){let i=this;i.setState(t,"loading",!0),this.options.data(t,(function(s){i.setState(t,"loading",!1),i.setState(t,"loaded",!0),i.create(s,t),e&&e.call(i)}),(function(){i.setState(t,"loading",!1),s&&s.call(i)}))}return this}open(t){return Array.isArray(t)?(t.forEach(t=>this.open(t)),this):(t=this.node(t),this.getState(t,"loaded",!0)?(t&&(this.setState(t,"opened",!0),this.trigger("open",{node:t})),this):(this.load(t,()=>this.open(t)),this))}close(t){return Array.isArray(t)?(t.forEach(t=>this.close(t)),this):((t=this.node(t))&&(this.setState(t,"opened",!1),this.trigger("close",{node:t})),this)}openAll(){for(let t of this.tree)this.setState(t,"opened",!0);return this.trigger("openAll"),this}closeAll(){for(let t of this.tree)this.setState(t,"opened",!1);return this.trigger("closeAll"),this}hide(t){return Array.isArray(t)?(t.forEach(t=>this.hide(t)),this):((t=this.node(t))&&(this.setState(t,"hidden",!0),this.trigger("hide",{node:t})),this)}show(t){return Array.isArray(t)?(t.forEach(t=>this.show(t)),this):((t=this.node(t))&&(this.setState(t,"hidden",!1),this.trigger("show",{node:t})),this)}hideAll(){for(let t of this.tree)this.setState(t,"hidden",!0);return this.trigger("hideAll"),this}showAll(){for(let t of this.tree)this.setState(t,"hidden",!1);return this.trigger("showAll"),this}select(t){return Array.isArray(t)?(t.forEach(t=>this.select(t)),this):((t=this.node(t))&&(this.setState(t,"selected",!0),this.trigger("select",{node:t})),this)}deselect(t){return Array.isArray(t)?(t.forEach(t=>this.deselect(t)),this):((t=this.node(t))&&(this.setState(t,"selected",!1),this.trigger("deselect",{node:t})),this)}selectAll(){for(let t of this.tree)this.setState(t,"selected",!0);return this.trigger("selectAll"),this}deselectAll(){for(let t of this.tree)this.setState(t,"selected",!1);return this.trigger("deselectAll"),this}getSelected(){return this.tree.find((function(t){return t.data&&t.data.state&&t.data.state.selected}))}enable(t){return Array.isArray(t)?(t.forEach(t=>this.enable(t)),this):((t=this.node(t))&&(this.setState(t,"disabled",!1),this.trigger("enable",{node:t})),this)}disable(t){return Array.isArray(t)?(t.forEach(t=>this.disable(t)),this):((t=this.node(t))&&(this.setState(t,"disabled",!0),this.trigger("disable",{node:t})),this)}enableAll(){for(let t of this.tree)this.setState(t,"disabled",!1);return this.trigger("enableAll"),this}disableAll(){for(let t of this.tree)this.setState(t,"disabled",!0);return this.trigger("disableAll"),this}edit(t){throw new Error("Not defined",t)}empty(){return this.tree.empty(),this.trigger("empty"),this}parseNode(t,e=null){if(t instanceof TreeNode)return t;if(e||(e=this.options.format),"string"==typeof t){let s={};e.data?s[e.data][e.title]=t:s[e.title]=t,t=s}return e.flat?TreeNode.fromFlatJSON(t,e.id,e.parent,e.position,e.data):TreeNode.fromJSON(t,e.children,e.data)}create(t,e=null,s=null,i=null){if(t=this.parseNode(t,i),null===e)this.tree.addRoot(t,s);else{if(!(e=this.node(e)))throw new Error("Invalid parent");e.addChild(t,s)}return t._dirty=!0,this.trigger("create",{node:t,parent:e,index:s}),this}move(t,e,s=null){throw new Error("Not defined",t,e,s)}copy(t,e,s=null){throw new Error("Not defined",t,e,s)}delete(t){return Array.isArray(t)?(t.forEach(t=>this.enable(t)),this):((t=this.node(t)).remove(),this.trigger("delete",{node:t}),this)}*visible(){let t=function*(e){for(let s of e.children)s.data.state&&s.data.state.hidden||(yield s,s.data.state&&s.data.state.opened&&(yield*t(s)))};yield*t(this.tree.root)}redraw(t=!1){this.redrawID&&window.cancelAnimationFrame(this.redrawID),this.redrawID=window.requestAnimationFrame(()=>{this._nodeList||(t=!0),t&&(this._nodeList=Array.from(this.visible())),this._renderer&&this._renderer.updateAll(t)})}render(t){let e=this;switch(t.jsTree=e,t.classList.add("jstree"),t.addEventListener("click",(function(t){let s=t.target;s&&s.classList.contains("jstree-closed")&&e.open(s),s&&s.classList.contains("jstree-opened")&&e.close(s),s&&s.classList.contains("jstree-node-icon")&&(s=s.parentNode),s&&s.classList.contains("jstree-node-text")&&(t.preventDefault(),e.deselectAll(),e.select(s))})),this._nodeList=Array.from(this.visible()),this.options.renderer){case"scroller":this._renderer=new InfiniteScroller(t,{create:function(){let t=document.createElement("div");return t.classList.add("jstree-node"),this.handlers.create.forEach(e=>e.call(this,t)),t}.bind(this),update:function(t,e,s=!0){let i=this._nodeList[t];if(!i)return void(e.style.display="none");if(s&&!i._dirty)return;e.style.display="block";let r="";i.getAncestors().reverse().forEach((function(t){t.isLast()?r+=' ':r+=' '}));let a="leaf";this.getState(i,"loaded",!0)?i.hasChildren()&&(a=this.getState(i,"opened")?"opened":"closed"):a="closed",i.isLast()?r+=` `:r+=` `,a=this.getState(i,"selected",!1)?"jstree-selected":"",r+=`  ${i.data.text}`,e.setAttribute("data-index",t),e.innerHTML=r,this.handlers.render.forEach(t=>t.call(this,e,i)),i._dirty=!1}.bind(this),count:()=>this._nodeList.length});break;default:this._renderer=new Flat(t,{create:function(){let t=document.createElement("div");return t.classList.add("jstree-node"),this.handlers.create.forEach(e=>e.call(this,t)),t}.bind(this),update:function(t,e,s=!0){let i=this._nodeList[t];if(!i)return void(e.style.display="none");if(s&&!i._dirty)return;e.style.display="block";let r="";i.getAncestors().reverse().forEach((function(t){t.isLast()?r+=' ':r+=' '}));let a="leaf";this.getState(i,"loaded",!0)?i.hasChildren()&&(a=this.getState(i,"opened")?"opened":"closed"):a="closed",i.isLast()?r+=` `:r+=` `,a=this.getState(i,"selected",!1)?"jstree-selected":"",r+=`  ${i.data.text}`,e.setAttribute("data-index",t),e.innerHTML=r,this.handlers.render.forEach(t=>t.call(this,e,i)),i._dirty=!1}.bind(this),count:()=>this._nodeList.length})}}static instance(t){if(t instanceof HTMLElement){for(;void 0===t.jsTree;){if(!t.parentNode||t.parentNode===window.document)return null;t=t.parentNode}return t.jsTree}return null}}jsTree.defaults={data:(t,e)=>e([]),renderer:"flat",plugins:{},format:{flat:!1,id:"id",children:"children",parent:null,position:null,data:null,title:"text",html:!1},strings:{loading:"Loading ...",newNode:"New node"},selection:{multiple:!0,reveal:!0},check:()=>!0,error:t=>t}; \ No newline at end of file +class Flat +{ + _empty(dom) { + while (dom.firstChild) { + dom.removeChild(dom.firstChild); + } + return dom; + } + constructor(container, dataSource) { + // store and normalize data object + this.dataSource = dataSource; + if (!this.dataSource.count) { + this.dataSource.count = () => 0; + } + if (!this.dataSource.create) { + this.dataSource.create = () => document.createElement("div"); + } + if (!this.dataSource.wrapper) { + this.dataSource.wrapper = () => document.createElement("div"); + } + + // store and empty container + this.container = this._empty(container); + + // add wrapper + this.wrapper = this.dataSource.wrapper(); + this.wrapper.className += " vakata-scroller-wrapper"; + this.container.appendChild(this.wrapper); + + this.count = 0; + + // render the visible items + this.render(); + } + render() { + this.updateAll(true); + } + + updateItem(index, dom, onlyDirty = true) { + this.dataSource.update(index, dom, onlyDirty); + } + updateAll(updateNodeList = true) { + if (updateNodeList) { + this.count = this.dataSource.count(); + let fragment = document.createDocumentFragment(); + for (let i = 0; i < this.count; i++) { + let node = this.dataSource.create(); + node.className += " vakata-scroller-item"; + fragment.appendChild(node); + } + this._empty(this.wrapper).appendChild(fragment); + } + + let node = this.wrapper.firstChild; + for (let i = 0; i < this.count; i++) { + this.updateItem(i, node, !updateNodeList); + node = node.nextSibling || this.wrapper.firstChild; + } + } +} + + +class InfiniteScroller +{ + _empty(dom) { + while (dom.firstChild) { + dom.removeChild(dom.firstChild); + } + return dom; + } + _offset(dom) { + return dom.getBoundingClientRect().top + dom.ownerDocument.defaultView.pageYOffset; + } + _scrollParent(dom) { + while (dom && (dom.tagName || "body").toLowerCase() !== "body") { + if (dom.className.indexOf("vakata-scroller-parent") !== -1) { + return dom; + } + let styles = getComputedStyle(dom, null); + if (styles.getPropertyValue("overflow") === "auto" || + styles.getPropertyValue("overflow") === "scroll" || + styles.getPropertyValue("overflow-y") === "auto" || + styles.getPropertyValue("overflow-y") === "scroll" + ) { + return dom; + } + dom = dom.parentNode; + } + return document.documentElement; + } + _debounce(callback, wait) { + let timeout; + return () => { + let args = arguments; + let func = () => callback.apply(this, args); + clearTimeout(timeout); + timeout = setTimeout(func, wait); + }; + } + _processScroll() { + // TODO: optimize for big shifts - move whole block? + let currScrollPosition = this.scrollParent.scrollTop - this._state.scroll; + let currTopNodePosition = this._state.topPosition; //parseInt(this._state.top.style.transform.split("translateY(")[1], 10); + let topDifference = currScrollPosition - currTopNodePosition; + let quater = (this._state.render * this._state.height) / 4; + let diff = Math.floor(Math.abs(topDifference - quater) / this._state.height); + let moveNodeDown = () => { + let cur = this._state.topPosition; // parseInt(this._state.top.style.transform.split("translateY(")[1], 10); + cur += this._state.render * this._state.height; + if (cur < this._state.count * this._state.height) { + this._state.index ++; + this._state.top.pos = cur; + //this._state.top.style.transform = "translateY(" + (cur) + "px)"; + this._state.bottom = this._state.top; + this._state.top = this._state.top.nextSibling || this.wrapper.firstChild; + this._state.topPosition = this._state.index * this._state.height; + this._state.bottomPosition = this._state.topPosition + (this._state.render - 1) * this._state.height; + } + }; + let moveNodeUp = () => { + let cur = this._state.bottomPosition; //parseInt(this._state.bottom.style.transform.split("translateY(")[1], 10); + cur -= this._state.render * this._state.height; + if (cur >= 0) { + this._state.index --; + this._state.bottom.pos = cur; + //this._state.bottom.style.transform = "translateY(" + (cur) + "px)"; + this._state.top = this._state.bottom; + this._state.bottom = this._state.bottom.previousSibling || this.wrapper.lastChild; + this._state.topPosition = this._state.index * this._state.height; + this._state.bottomPosition = this._state.topPosition + (this._state.render - 1) * this._state.height; + } + }; + + if (topDifference > quater) { + for (let i = 0; i < diff; i++) { + moveNodeDown(); + } + } + if (topDifference < quater) { + for (let i = 0; i < diff; i++) { + moveNodeUp(); + } + } + let node = this._state.top; + for (let i = 0; i < this._state.render; i++) { + node.style.transform = "translateY(" + (node.pos) + "px)"; + node = node.nextSibling || this.wrapper.firstChild; + } + + // TODO: optimize? maybe update only needed nodes when scroll distance is low? + this.updateAll(); + } + _wrapperWidth(reset = false) { + let max = 0; + let node = this.wrapper.firstChild; + let curr = this.wrapper.width || this.wrapper.offsetWidth; + if (reset) { + this.wrapper.style.minWidth = "none"; + } + do { + if (node.width && node.width > max) { + max = node.width; + } + } while ((node = node.nextSibling)); + if (reset || curr !== max) { + this.wrapper.width = max; + this.wrapper.style.minWidth = max + "px"; + } + } + + constructor(container, dataSource) { + // store and normalize data object + this.dataSource = dataSource; + if (!this.dataSource.count) { + this.dataSource.count = () => 0; + } + if (!this.dataSource.create) { + this.dataSource.create = () => document.createElement("div"); + } + if (!this.dataSource.wrapper) { + this.dataSource.wrapper = () => document.createElement("div"); + } + + this._state = { + height : 0, + count : this.dataSource.count(), + scroll : 0, + render : 0, + visible : 0, + index : 0, + top : null, + bottom : null + }; + + // store and empty container + this.container = this._empty(container); + + // add wrapper + this.wrapper = this.dataSource.wrapper(); + this.wrapper.className += " vakata-scroller-wrapper"; + this.wrapper.style.position = "relative"; + //this.wrapper.style.minHeight = "100%"; + //this.wrapper.style.overflowY = "hidden"; + this.container.appendChild(this.wrapper); + + // get the scroll parent + this.scrollParent = this._scrollParent(this.wrapper); + let scrollEventHost = this.scrollParent === document.documentElement ? window : this.scrollParent; + + // render the visible items + this.render(); + + // bind events + window.addEventListener("resize", this._debounce(this.render, 50)); + scrollEventHost.addEventListener("scroll", () => { + if (this.scrollID) { + window.cancelAnimationFrame(this.scrollID); + } + this.scrollID = window.requestAnimationFrame(() => this._processScroll()); + }); + } + render() { + // calculate node height and adjust wrapper height + let node = this.dataSource.create(); + node.className += " vakata-scroller-item"; + node.innerHTML = " "; + this.wrapper.appendChild(node); + this._state.height = node.offsetHeight; + this._state.count = this.dataSource.count(); + this.wrapper.removeChild(node); + this.wrapper.style.height = (this._state.count * this._state.height) + "px"; + + // get the scroll difference between the container and the scroll parent + this._state.scroll = 0; + if (this.scrollParent !== this.container) { + this._state.scroll = this.scrollParent === document.documentElement ? + this._offset(this.container) : + this._offset(this.container) - this._offset(this.scrollParent); + } + + // calculate visible and rendered counts + this._state.visible = Math.min(window.innerHeight, this.scrollParent.offsetHeight); + this._state.render = Math.max(1, Math.ceil(this._state.visible / this._state.height)) * 2; // even number! + + // fill wrapper + let fragment = document.createDocumentFragment(); + for (let i = 0; i < this._state.render; i++) { + let node = this.dataSource.create(); + node.className += " vakata-scroller-item"; + node.style.willChange = "transform"; + node.style.position = "absolute"; + node.style.minWidth = "100%"; + node.style.height = this._state.height + "px"; + node.style.transform = "translateY(" + (this._state.height * i) + "px)"; + fragment.appendChild(node); + } + this._empty(this.wrapper).appendChild(fragment); + + // save references to the top and bottom nodes + this._state.top = this.wrapper.firstChild; + this._state.topPosition = 0; + this._state.index = 0; + this._state.bottom = this.wrapper.lastChild; + this._state.bottomPosition = this._state.height * (this._state.render - 1); + + this._processScroll(); + this.updateAll(true); + } + + updateItem(index, dom, onlyDirty = true) { + this.dataSource.update(index, dom, onlyDirty); + // store the width for easy access + // dom.width = dom.offsetWidth; + } + updateAll(updateNodeList = true) { + let count = this.dataSource.count(); + if (count !== this._state.count) { + this._state.count = count; + this.wrapper.style.height = (this._state.count * this._state.height) + "px"; + } + let node = this._state.top; + for (let i = this._state.index; i < this._state.index + this._state.render; i++) { + this.updateItem(i, node, !updateNodeList); + node = node.nextSibling || this.wrapper.firstChild; + } + //this._wrapperWidth(true); + } +} + + +class TreeNode +{ + constructor(data = {}) { + this.data = data; + this.parent = null; + this.children = []; + } + getParent() { + return this.isRoot() ? null : this.parent; + } + hasParent() { + return !this.isRoot(); + } + isRoot() { + return this.parent === null || this.parent.data === TreeNode.getRootSymbol(); + } + hasChildren() { + return this.children.length > 0; + } + isEmpty() { + return this.children.length === 0; + } + getAncestors() { + var parents = []; + var parent = this.getParent(); + while (parent && parent.data !== TreeNode.getRootSymbol()) { + parents.push(parent); + parent = parent.getParent(); + } + return parents; + } + getIndex() { + return this.parent === null ? 0 : this.parent.children.indexOf(this); + } + isLast() { + if (this.parent === null) { + return true; + } + var siblings = this.parent.children; + return siblings[siblings.length - 1] === this; + } + getChildren() { + return this.children; + } + getDescendants() { + var children = []; + let recurse = function (node) { + for (let child of node.children) { + children.push(child); + recurse(child); + } + }; + recurse(this); + return children; + } + addChild(node, index) { + if (Array.isArray(node)) { + node.forEach((x, i) => this.addChild(x, index === undefined ? index : index + i)); + return this; + } + if (node.parent) { + node.parent.children.splice(node.parent.children.indexOf(node), 1); + } + node.parent = this; + if (index === undefined) { + this.children.push(node); + } + else { + this.children.splice(index, 0, node); + } + return this; + } + delete() { + this.parent.children.splice(this.parent.children.indexOf(this), 1); + this.parent = null; + return this; + } + empty() { + this.children = []; + } + move(parent, index) { + parent.addChild(this, index); + } + copy(parent, index) { + parent.addChild(TreeNode.fromJSON(this.toJSON("children", "data"), "children", "data"), index); + } + // only if data is an object + get(prop, defaultValue = null) { + return this.data[prop] || defaultValue; + } + // only if data is an object + set(prop, value) { + this.data[prop] = value; + return this; + } + data(data) { + if (data !== undefined) { + this.data = data; + } + return this.data; + } + toJSON(childrenProperty = "children", dataProperty = "") { + if (dataProperty) { + return { + [dataProperty] : this.data, + [childrenProperty] : this.children.map(x => x.toJSON(childrenProperty, dataProperty)) + }; + } + let nodeData = this.data; + nodeData[childrenProperty] = this.children.map(x => x.toJSON(childrenProperty, dataProperty)); + return nodeData; + } + toFlatJSON(id = "id", idProperty = "id", parentProperty = "parent", positionProperty = "position", dataProperty = "") { + let result = []; + let recurse = node => { + if (dataProperty) { + result.push({ + [idProperty] : node.data[id], + [parentProperty] : node.parent ? node.parent.data[id] : null, + [positionProperty] : node.getIndex(), + [dataProperty] : node.data, + }); + } else { + let nodeData = node.data; + nodeData[idProperty] = node.data[id]; + nodeData[parentProperty] = node.parent ? node.parent.data[id] : null; + nodeData[positionProperty] = node.getIndex(); + result.push(nodeData); + } + node.children.forEach(recurse); + }; + recurse(this); + return result; + } + static fromJSON(data, childrenProperty = "children", dataProperty = "") { + if (Array.isArray(data)) { + return data.map(x => TreeNode.fromJSON(x, childrenProperty)); + } + if (!dataProperty) { + let nodeData = {}; + let children = []; + for (let key of Object.keys(data)) { + if (key !== childrenProperty) { + nodeData[key] = data[key]; + } else { + children = data[key]; + } + } + //let { [childrenProperty] : children, ...nodeData } = data; + let node = new TreeNode(nodeData); + if (children) { + children.forEach(x => node.addChild(TreeNode.fromJSON(x, childrenProperty, dataProperty))); + } + return node; + } + let node = new TreeNode(data[dataProperty]); + if (data[childrenProperty]) { + data[childrenProperty].forEach(x => node.addChild(TreeNode.fromJSON(x, childrenProperty, dataProperty))); + } + return node; + } + static fromFlatJSON(data, idProperty = "id", parentProperty = "parent", positionProperty = "position", dataProperty = "") { + var nodes = new Map(); + // build the flat list + data.forEach(x => { + nodes[x[idProperty]] = { + data : dataProperty ? x[dataProperty] : x, + children : [] + }; + }); + // assign children + nodes.forEach((k, v) => { + if (nodes.has(v[parentProperty])) { + nodes.get(v[parentProperty]).children.push(v); + } + }); + // sort children + if (positionProperty) { + nodes.forEach((k, v) => { + v.children.sort(function (a, b) { + a = a[positionProperty]; + b = b[positionProperty]; + return a === b ? 0 : (a < b ? -1 : 1); + }); + }); + } + // leave only roots + nodes.forEach((k, v) => { + if (nodes.has(v[parentProperty])) { + nodes.delete(k); + } + }); + return TreeNode.fromJSON(Array.from(nodes.values()), "children", "data"); + } + static getRootSymbol() { + if (!TreeNode.root) { + TreeNode.root = Symbol("Root symbol"); + } + return TreeNode.root; + } + static getRoot() { + return new TreeNode(TreeNode.getRootSymbol()); + } +} + + + + +class Tree +{ + constructor() { + this.root = TreeNode.getRoot(); + } + addRoot(node, index) { + return this.root.addChild(node, index); + } + getRoots() { + return this.root.getChildren(); + } + isEmpty() { + return this.root.isEmpty(); + } + empty() { + return this.root.empty(); + } + toJSON(childrenProperty = "children", dataProperty = "") { + return this.root.toJSON(childrenProperty, dataProperty).children; + } + toFlatJSON(id = "id", idProperty = "id", parentProperty = "parent", positionProperty = "position", dataProperty = "") { + let nodes = this.root.toFlatJSON(id, idProperty, parentProperty, positionProperty, dataProperty); + delete nodes[0]; + return nodes; + } + _normalizeCriteria(criteria) { + if (typeof criteria === "function") { + return criteria; + } + if (typeof criteria !== "object") { + criteria = { id : criteria }; + } + return function (node) { + for (let key of criteria.keys()) { + if (node.data[key] !== criteria[key]) { + return false; + } + } + return true; + }; + } + find(criteria) { + criteria = this._normalizeCriteria(criteria); + let matches = []; + for (let node of this) { + if (criteria(node)) { + matches.push(node); + } + } + return matches; + } + findFirst(criteria) { + criteria = this._normalizeCriteria(criteria); + for (let node of this) { + if (criteria(node)) { + return node; + } + } + return null; + } + * filter(criteria) { + criteria = this._normalizeCriteria(criteria); + for (let node of this) { + if (criteria(node)) { + yield node; + } + } + } + * [Symbol.iterator]() { + let recurse = function * (node) { + for (let child of node.children) { + yield child; + yield * recurse(child); + } + }; + yield * recurse(this.root); + } +} + + + + + + + +class jsTree +{ + static _extend (out) { + out = out || {}; + for (let i = 1; i < arguments.length; i++) { + let obj = arguments[i]; + if (!obj) { + continue; + } + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + if (typeof obj[key] === "object" && obj[key] !== null) { + out[key] = jsTree._extend(out[key], obj[key]); + } else { + out[key] = obj[key]; + } + } + } + } + return out; + } + constructor(options = {}, dom = null) { + this.options = jsTree._extend({}, jsTree.defaults, options); + this.tree = new Tree(); + this.events = {}; + this.handlers = { + create : [], + render : [] + }; + + // TODO: aria + // TODO: incomplete ("more" link as last child if incomplete) + + this._nodeList = []; + this._renderer = null; + // redraw with no change to visible structure + this.on( + `select deselect selectAll deselectAll + enable disable enableAll disableAll + rename`, + () => this.redraw() + ); + // redraw WITH change to visible structure + this.on( + `open close openAll closeAll + hide show hideAll showAll + empty create delete move copy`, + () => this.redraw(true) + ); + + if (dom) { + this.render(dom); + } + Object.entries(this.options.plugins).forEach(v => jsTree.plugin[v[0]].call(this, v[1] || {})); + + // TODO: render (or indicate) loading + if (this.options.data instanceof Function) { + let tree = this; + this.options.data( + null, + function (data) { + tree + .empty() + .create(data, null); + }, + function () {} + ); + } else { + this + .empty() + .create(options.data, null); + } + } + addCreateHandler(func) { + this.handlers.create.push(func); + } + addRenderHandler(func) { + this.handlers.render.push(func); + } + + node(node) { + if (node instanceof TreeNode) { + return node; + } + if (node instanceof HTMLElement) { + while (!node.classList.contains('jstree-node')) { + if (!node.parentNode || node.parentNode === window.document) { + return null; + } + node = node.parentNode; + } + return this._nodeList[parseInt(node.getAttribute('data-index'), 10)] || null; + } + if (typeof node !== "object" && typeof node !== "function") { + node = { [this.options.idProperty] : node }; + } + return this.tree.findFirst(node); + } + find(criteria) { + if (typeof criteria !== "object" && typeof criteria !== "function") { + criteria = { [this.options.idProperty] : criteria }; + } + return this.tree.find(criteria); + } + * filter(criteria) { + if (typeof criteria !== "object" && typeof criteria !== "function") { + criteria = { [this.options.idProperty] : criteria }; + } + yield * this.tree.filter(criteria); + } + * [Symbol.iterator]() { + yield * this.tree[Symbol.iterator](); + } + + getState(node, key, defaultValue = null) { + return !this.node(node).data.state ? + defaultValue : + (this.node(node).data.state[key] !== undefined ? this.node(node).data.state[key] : defaultValue); + } + setState(node, key, value) { + node = this.node(node); + if (!node.data.state) { + node.data.state = {}; + } + if (node.data.state[key] !== value) { + node.data.state[key] = value; + node._dirty = true; + } + return this; + } + + trigger(event, data = {}) { + data.event = event; + data.instance = this; + if (this.events[event]) { + this.events[event].forEach(x => x(data)); + } + if (this.events["*"]) { + this.events["*"].forEach(x => x(data)); + } + return this; + } + on(event, callback) { + let events = event.split(" "); + if (events.length > 1) { + events.forEach(x => this.on(x, callback)); + return this; + } + event = events[0]; + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(callback); + return this; + } + off(event, callback = null) { + let events = event.split(" "); + if (events.length > 1) { + events.forEach(x => this.off(x, callback)); + return this; + } + event = events[0]; + if (this.events[event]) { + if (callback === null) { + this.events[event] = []; + } + let i = this.events[event].indexOf(callback); + if (i !== -1) { + this.events[event].splice(this.events[event].indexOf(callback), 1); + } + } + return this; + } + + // open / close (changes visible node count (container height)) + load(node, done, fail) { + if (Array.isArray(node)) { + node.forEach(x => this.load(x, done, fail)); + return this; + } + node = this.node(node); + if (!this.getState(node, 'loading') && + !this.getState(node, 'loaded', true) && + this.options.data instanceof Function + ) { + let tree = this; + tree.setState(node, 'loading', true); + this.options.data( + node, + function (data) { + tree.setState(node, 'loading', false); + tree.setState(node, 'loaded', true); + tree.create(data, node); + if (done) { + done.call(tree); + } + }, + function () { + tree.setState(node, 'loading', false); + if (fail) { + fail.call(tree); + } + } + ); + } + return this; + } + open(node) { + if (Array.isArray(node)) { + node.forEach(x => this.open(x)); + return this; + } + node = this.node(node); + if (!this.getState(node, 'loaded', true)) { + this.load(node, () => this.open(node)); + return this; + } + if (node) { + this.setState(node, "opened", true); + this.trigger('open', { node : node }); + } + return this; + } + close(node) { + if (Array.isArray(node)) { + node.forEach(x => this.close(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "opened", false); + this.trigger('close', { node : node }); + } + return this; + } + openAll() { + for (let node of this.tree) { + this.setState(node, "opened", true); + } + this.trigger('openAll'); + return this; + } + closeAll() { + for (let node of this.tree) { + this.setState(node, "opened", false); + } + this.trigger('closeAll'); + return this; + } + + // hide / show (changes visible node count (container height)) + hide(node) { + if (Array.isArray(node)) { + node.forEach(x => this.hide(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "hidden", true); + this.trigger('hide', { node : node }); + } + return this; + } + show(node) { + if (Array.isArray(node)) { + node.forEach(x => this.show(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "hidden", false); + this.trigger('show', { node : node }); + } + return this; + } + hideAll() { + for (let node of this.tree) { + this.setState(node, "hidden", true); + } + this.trigger('hideAll'); + return this; + } + showAll() { + for (let node of this.tree) { + this.setState(node, "hidden", false); + } + this.trigger('showAll'); + return this; + } + + // selection (just visual indication - redraw involved nodes or loop and apply minor changes) + select(node) { + if (Array.isArray(node)) { + node.forEach(x => this.select(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "selected", true); + this.trigger("select", { node : node }); + } + return this; + } + deselect(node) { + if (Array.isArray(node)) { + node.forEach(x => this.deselect(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "selected", false); + this.trigger("deselect", { node : node }); + } + return this; + } + selectAll() { + for (let node of this.tree) { + this.setState(node, "selected", true); + } + this.trigger("selectAll"); + return this; + } + deselectAll() { + for (let node of this.tree) { + this.setState(node, "selected", false); + } + this.trigger("deselectAll"); + return this; + } + getSelected() { + return this.tree.find(function (node) { + return node.data && node.data.state && node.data.state.selected; + }); + } + + // enable / disable + enable(node) { + if (Array.isArray(node)) { + node.forEach(x => this.enable(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "disabled", false); + this.trigger("enable", { node : node }); + } + return this; + } + disable(node) { + if (Array.isArray(node)) { + node.forEach(x => this.disable(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "disabled", true); + this.trigger("disable", { node : node }); + } + return this; + } + enableAll() { + for (let node of this.tree) { + this.setState(node, "disabled", false); + } + this.trigger("enableAll"); + return this; + } + disableAll() { + for (let node of this.tree) { + this.setState(node, "disabled", true); + } + this.trigger("disableAll"); + return this; + } + + // TODO: edit (just visual indication - redraw involved nodes or loop and apply minor changes) + edit(node) { + // also handle array! + // a config option? which is the text property of the node? maybe the same for ID? and for children? + throw new Error("Not defined", node); + } + empty() { + this.tree.empty(); + this.trigger("empty"); + return this; + } + parseNode(node, format = null) { + if (node instanceof TreeNode) { + return node; + } + if (!format) { + format = this.options.format; + } + if (typeof node === "string") { + let temp = {}; + if (format.data) { + temp[format.data][format.title] = node; + } else { + temp[format.title] = node; + } + node = temp; + } + if (format.flat) { + return TreeNode.fromFlatJSON( + node, + format.id, + format.parent, + format.position, + format.data + ); + } + return TreeNode.fromJSON( + node, + format.children, + format.data + ); + } + create(node, parent = null, index = null, format = null) { + node = this.parseNode(node, format); + if (parent === null) { + this.tree.addRoot(node, index); + } else { + parent = this.node(parent); + if (!parent) { + throw new Error("Invalid parent"); + } + parent.addChild(node, index); + } + node._dirty = true; + this.trigger('create', { node : node, parent : parent, index : index }); + return this; + } + move(node, parent, index = null) { + throw new Error("Not defined", node, parent, index); + } + copy(node, parent, index = null) { + throw new Error("Not defined", node, parent, index); + } + delete(node) { + if (Array.isArray(node)) { + node.forEach(x => this.enable(x)); + return this; + } + node = this.node(node); + node.remove(); + this.trigger('delete', { node : node }); + return this; + } + + * visible() { + let recurse = function * (node) { + for (let child of node.children) { + if (!child.data.state || !child.data.state.hidden) { + yield child; + if (child.data.state && child.data.state.opened) { + yield * recurse(child); + } + } + } + }; + yield * recurse(this.tree.root); + } + redraw(updateNodeList = false) { + if (this.redrawID) { + window.cancelAnimationFrame(this.redrawID); + } + this.redrawID = window.requestAnimationFrame(() => { + if (!this._nodeList) { + updateNodeList = true; + } + if (updateNodeList) { + this._nodeList = Array.from(this.visible()); + } + if (this._renderer) { + this._renderer.updateAll(updateNodeList); + } + }); + } + + render(target) { + // events + let tree = this; + target.jsTree = tree; + target.classList.add('jstree'); + target.addEventListener('click', function (e) { + let target = e.target; + if (target && target.classList.contains("jstree-closed")) { + tree.open(target); + } + if (target && target.classList.contains("jstree-opened")) { + tree.close(target); + } + if (target && target.classList.contains("jstree-node-icon")) { + target = target.parentNode; + } + if (target && target.classList.contains("jstree-node-text")) { + e.preventDefault(); + tree.deselectAll(); + tree.select(target); + } + }); + + this._nodeList = Array.from(this.visible()); + switch (this.options.renderer) { + case 'scroller': + this._renderer = new InfiniteScroller( + target, + { + create : (function () { + let node = document.createElement("div"); + node.classList.add("jstree-node"); + this.handlers.create.forEach(v => v.call(this, node)); + return node; + }).bind(this), + update : (function (index, dom, onlyDirty = true) { + let node = this._nodeList[index]; + if (!node) { + dom.style.display = 'none'; + return; + } + if (onlyDirty && !node._dirty) { + return; + } + dom.style.display = 'block'; + let html = ""; + node.getAncestors().reverse().forEach(function (parent) { + if (parent.isLast()) { + html += ` `; + } else { + html += ` `; + } + }); + let clss = 'leaf'; + if (!this.getState(node, "loaded", true)) { + clss = 'closed'; + } else { + if (node.hasChildren()) { + clss = this.getState(node, "opened") ? "opened" : "closed"; + } + } + if (node.isLast()) { + html += ` `; + } else { + html += ` `; + } + clss = this.getState(node, "selected", false) ? 'jstree-selected' : ''; + html += `  ${node.data.text}`; + + // TODO: do not redraw the whole DIV! update classes and texts + // TODO: add "dots" DIVs, prepare a style to render them "invisible" (opacity: 0) + dom.setAttribute('data-index', index); + dom.innerHTML = html; + this.handlers.render.forEach(v => v.call(this, dom, node)); + node._dirty = false; + }).bind(this), + count : () => this._nodeList.length + } + ); + break; + default: + this._renderer = new Flat( + target, + { + create : (function () { + let node = document.createElement("div"); + node.classList.add("jstree-node"); + this.handlers.create.forEach(v => v.call(this, node)); + return node; + }).bind(this), + update : (function (index, dom, onlyDirty = true) { + let node = this._nodeList[index]; + if (!node) { + dom.style.display = 'none'; + return; + } + if (onlyDirty && !node._dirty) { + return; + } + dom.style.display = 'block'; + let html = ""; + node.getAncestors().reverse().forEach(function (parent) { + if (parent.isLast()) { + html += ` `; + } else { + html += ` `; + } + }); + let clss = 'leaf'; + if (!this.getState(node, "loaded", true)) { + clss = 'closed'; + } else { + if (node.hasChildren()) { + clss = this.getState(node, "opened") ? "opened" : "closed"; + } + } + if (node.isLast()) { + html += ` `; + } else { + html += ` `; + } + clss = this.getState(node, "selected", false) ? 'jstree-selected' : ''; + html += `  ${node.data.text}`; + + // TODO: do not redraw the whole DIV! update classes and texts + // TODO: add "dots" DIVs, prepare a style to render them "invisible" (opacity: 0) + dom.setAttribute('data-index', index); + dom.innerHTML = html; + this.handlers.render.forEach(v => v.call(this, dom, node)); + node._dirty = false; + }).bind(this), + count : () => this._nodeList.length + } + ); + break; + } + } + + static instance (node) { + if (node instanceof HTMLElement) { + while (node.jsTree === undefined) { + if (!node.parentNode || node.parentNode === window.document) { + return null; + } + node = node.parentNode; + } + return node.jsTree; + } + return null; + } +} + +jsTree.defaults = { + data : (node, done) => done([]), // include fail option in docs + renderer : 'flat', // 'scroller' + plugins : {}, + format : { + flat : false, + id : "id", + children : "children", + parent : null, + position : null, + data : null, + title : "text", + html : false + }, + strings : { + loading : "Loading ...", + newNode : "New node" + }, + selection : { + multiple : true, + reveal : true, + }, + check : () => true, + error : err => err +}; + + + + + +if (!jsTree.plugin) + jsTree.plugin = {} + +jsTree.plugin.checkbox = function (options) { + const defaults = { + visible : false, + cascade : { up : true, down : true, indeterminate : true, includeHidden : true, includeDisabled : true }, + selection : true, + wholeNode : false + }; + options = jsTree._extend({}, defaults, options); + this.on( + `check uncheck checkAll uncheckAll`, + () => this.redraw() + ); + this.addRenderHandler(function (dom, node) { + // text (node content including icon) + const a = document.evaluate("a[contains(@class,'jstree-node-text')]",dom,null,XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue + // checkbox should/will be in front... + let checkBoxDomElement = a.previousSibling; + if (!checkBoxDomElement.classList.contains("jstree-icon-checkbox")) { + // checkbox element needs to be created... + checkBoxDomElement = document.createElement("i") + + if (this.getState(node, "checked", false)) + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-checked" + else if (this.getState(node, "indeterminate", false)) + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-indeterminate" + else + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-unchecked" + + // ... and added first. + a.parentElement.insertBefore(checkBoxDomElement,a); + + // register event listner to trigger check change ... + checkBoxDomElement.addEventListener('click', (e) => { + if (this.getState(node,"checked",false)) + this.uncheck(node); + else + this.check(node); + }); + } else { + // checkbox state will be set/updated + if (this.getState(node, "checked", false)) + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-checked") + else if (this.getState(node, "indeterminate", false)) + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-indeterminate") + else + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-unchecked") + } + }); + // TODO: all other plugin stuff (cascade, etc) + + // checkboxes (just visual indication - redraw involved nodes or loop and apply minor changes) + this.check = function(node) { + if (Array.isArray(node)) { + node.forEach(x => this.check(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "checked", true); + this.setState(node, "indeterminate", false); + this.trigger("check", { node : node }); + if (options.cascade.down) { + if (!this.getState(node, 'loaded', true)) { + this.load(node, () => this.check(node)); + } else { + this.check(node.children); + } + } + if (options.cascade.up) { + const traverseUpIndeterminate = (node)=>{ + const parent = node.getParent(); + if (parent) { + const nodeAndSiblings = parent.getChildren(); + if (nodeAndSiblings.every(s=>this.getState(s,"checked"))) // if 'checked'===true we know that indetermiante must be false + { + if (!this.getState(parent,"checked")) + this.check(parent) // this will tirgger update at parent as well (if required) + } else if ( + options.cascade.indeterminate && + nodeAndSiblings.some(s=>this.getState(s,"checked") || this.getState(s,"indeterminate",false)) + ) { + if (!this.getState(parent,"indeterminate")) + { + this.setState(parent,"indeterminate",true); + // since we don't have a 'undeterminate' setter, we need to traverse this up 'manually' + traverseUpIndeterminate(parent); + } + } + } + } + traverseUpIndeterminate(node); + } + } + return this; + } + this.uncheck = function(node) { + if (Array.isArray(node)) { + node.forEach(x => this.uncheck(x)); + return this; + } + node = this.node(node); + if (node) { + this.setState(node, "checked", false); + this.setState(node, "indeterminate", false); + this.trigger("uncheck", { node : node }); + if (options.cascade.down) { + if (!this.getState(node, 'loaded', true)) { + this.load(node, () => this.uncheck(node)); + } else { + this.uncheck(node.children); + } + } + if (options.cascade.up) { + const traverseUpIndeterminate = (node)=>{ + const parent = node.getParent(); + if (parent) { + const nodeAndSiblings = parent.getChildren(); + if (nodeAndSiblings.every(s=>!this.getState(s,"checked") && !this.getState(s,"indeterminate"))) + { + if (this.getState(parent,"checked") || this.getState(parent,"indeterminate")) + { + this.uncheck(parent) // this will tirgger update at parent as well (if required) + } + } else if ( + options.cascade.indeterminate && + nodeAndSiblings.some(s=>this.getState(s,"checked",false) || this.getState(s,"indeterminate",false)) + ) { + if (this.getState(parent,"checked",false)) + { + this.setState(parent,"checked",false); + this.setState(parent,"indeterminate",true); + // since we don't have a 'undeterminate' setter, we need to traverse this up 'manually' + // also to avoid infitive update loop... + traverseUpIndeterminate(parent); + } + } + } + } + traverseUpIndeterminate(node); + } + } + return this; + } + this.checkAll = function() { + for (let node of this.tree) { + this.setState(node, "checked", true); + this.setState(node, "indeterminate", false); + } + this.trigger("checkAll"); + return this; + } + this.uncheckAll = function() { + for (let node of this.tree) { + this.setState(node, "checked", false); + this.setState(node, "indeterminate", false); + } + this.trigger("uncheckAll"); + return this; + } + this.getChecked = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? + return this.tree.find(function (node) { + return node.data && node.data.state && node.data.state.checked; + }); + } + this.getTopChecked = function() { + let recurse = function * (node) { + for (let child of node.children) { + if (node.data.state && node.data.state.checked) { + yield child; + } + yield * recurse(child); + } + }; + return Array.from(recurse(this.root)); + } + this.getBottomChecked = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? + let recurse = function * (node) { + for (let child of node.children) { + if (node.data.state && node.data.state.checked && !child.children.length) { + yield child; + } + yield * recurse(child); + } + }; + return Array.from(recurse(this.root)); + } + this.getIndeterminate = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? + return this.tree.find(function (node) { + return node.data && node.data.state && node.data.state.indeterminate; + }); + } +}; diff --git a/gulpfile.js b/gulpfile.js index c6ae691a..3d299bf2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,7 +2,7 @@ var gulp = require("gulp"); var replace = require("gulp-replace"); var concat = require("gulp-concat"); -var uglify = require("gulp-uglify-es").default; +// var uglify = require("gulp-uglify-es").default; var minifycss = require("gulp-clean-css"); var header = require("gulp-header"); var pkg = require("./package.json"); @@ -17,7 +17,7 @@ var banner = [ ].join("\n"); gulp.task("js", function () { - return gulp.src(["src/render/**/*.js","src/model/TreeNode.js","src/model/Tree.js","src/jstree.js","src/jstree.*.js"]) + return gulp.src(["src/render/**/*.js","src/model/TreeNode.js","src/model/Tree.js","src/jstree.js","src/jstree.*.js","src/plugin/checkbox.js"]) .pipe(concat("jstree.js")) .pipe(replace(/(import|export) [^;]*;/g, "")) //.pipe(uglify()) @@ -25,14 +25,14 @@ gulp.task("js", function () { .pipe(gulp.dest("dist")); }); gulp.task("css", function () { - return gulp.src("src/*.css") + return gulp.src(["src/*.css","src/plugin/*.css"]) .pipe(concat("jstree.css")) .pipe(gulp.dest("dist")); }); gulp.task("minifyjs", gulp.series('js', function () { return gulp.src(["dist/jstree.js"]) .pipe(concat("jstree.min.js")) - .pipe(uglify()) + // .pipe(uglify()) .pipe(header(banner, { pkg : pkg } )) .pipe(gulp.dest("dist")); })); diff --git a/src/icon/checkbox-checked.svg b/src/icon/checkbox-checked.svg new file mode 100644 index 00000000..0777d771 --- /dev/null +++ b/src/icon/checkbox-checked.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/icon/checkbox-indeterminate.svg b/src/icon/checkbox-indeterminate.svg new file mode 100644 index 00000000..9edf2a8b --- /dev/null +++ b/src/icon/checkbox-indeterminate.svg @@ -0,0 +1,12 @@ + + + + + + + + Layer 1 + + + + \ No newline at end of file diff --git a/src/icon/checkbox-unchecked.svg b/src/icon/checkbox-unchecked.svg new file mode 100644 index 00000000..ae76b8a9 --- /dev/null +++ b/src/icon/checkbox-unchecked.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/src/plugin/checkbox.css b/src/plugin/checkbox.css new file mode 100644 index 00000000..81ac15bc --- /dev/null +++ b/src/plugin/checkbox.css @@ -0,0 +1,21 @@ +.jstree-icon-checkbox { + text-align : center; + display : inline-block; + width : var(--jstree-size); + height : var(--jstree-size); + line-height : var(--jstree-size); + position : relative; + vertical-align : middle; +} + +.jstree-icon-checkbox-checked { + background-image: url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter id='svg_3_blur'%3E%3CfeGaussianBlur stdDeviation='0.5' in='SourceGraphic'/%3E%3C/filter%3E%3C/defs%3E%3Cg%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3Cpath stroke-width='3' id='svg_3' d='m8.9609,13.93296c4.35754,8.49162 5.13966,9.83241 5.11731,9.75419c0.02235,0.07822 8.9609,-15.56424 8.93855,-15.64246' opacity='NaN' stroke='%23000' fill='none'/%3E%3Cpath filter='url(%23svg_3_blur)' stroke-width='3' id='svg_3' d='m8.9609,13.93296c4.35754,8.49162 5.13966,9.83241 5.11731,9.75419c0.02235,0.07822 8.9609,-15.56424 8.93855,-15.64246' opacity='NaN' stroke='%23628C52' fill='none'/%3E%3C/g%3E%3C/svg%3E"); +} + +.jstree-icon-checkbox-unchecked { + background-image: url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cg%3E%3Ctitle%3ELayer 1%3C/title%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3C/g%3E%3C/svg%3E"); +} + +.jstree-icon-checkbox-indeterminate { + background-image: url("data:image/svg+xml,%3Csvg width='32' height='32' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cfilter height='200%25' width='200%25' y='-50%25' x='-50%25' id='svg_2_blur'%3E%3CfeGaussianBlur stdDeviation='1' in='SourceGraphic'/%3E%3C/filter%3E%3C/defs%3E%3Cg%3E%3Ctitle%3ELayer 1%3C/title%3E%3Crect rx='4' id='svg_1' height='22' width='22' y='5' x='5' stroke='%23000' fill='none'/%3E%3Crect rx='4' filter='url(%23svg_2_blur)' id='svg_2' height='16' width='16' y='8' x='8' stroke='%23000' fill='%23729C62'/%3E%3C/g%3E%3C/svg%3E"); +} \ No newline at end of file diff --git a/src/plugin/checkbox.js b/src/plugin/checkbox.js index 937487bc..60d48e6b 100644 --- a/src/plugin/checkbox.js +++ b/src/plugin/checkbox.js @@ -1,5 +1,8 @@ import jsTree from "../jstree"; +if (!jsTree.plugin) + jsTree.plugin = {} + jsTree.plugin.checkbox = function (options) { const defaults = { visible : false, @@ -7,13 +10,46 @@ jsTree.plugin.checkbox = function (options) { selection : true, wholeNode : false }; - let options = jsTree._extend({}, defaults, options); + options = jsTree._extend({}, defaults, options); this.on( `check uncheck checkAll uncheckAll`, () => this.redraw() ); this.addRenderHandler(function (dom, node) { - // TODO: add icon + // text (node content including icon) + const a = document.evaluate("a[contains(@class,'jstree-node-text')]",dom,null,XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue + // checkbox should/will be in front... + let checkBoxDomElement = a.previousSibling; + if (!checkBoxDomElement.classList.contains("jstree-icon-checkbox")) { + // checkbox element needs to be created... + checkBoxDomElement = document.createElement("i") + + if (this.getState(node, "checked", false)) + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-checked" + else if (this.getState(node, "indeterminate", false)) + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-indeterminate" + else + checkBoxDomElement.className = "jstree-icon-checkbox jstree-icon-checkbox-unchecked" + + // ... and added first. + a.parentElement.insertBefore(checkBoxDomElement,a); + + // register event listner to trigger check change ... + checkBoxDomElement.addEventListener('click', (e) => { + if (this.getState(node,"checked",false)) + this.uncheck(node); + else + this.check(node); + }); + } else { + // checkbox state will be set/updated + if (this.getState(node, "checked", false)) + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-checked") + else if (this.getState(node, "indeterminate", false)) + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-indeterminate") + else + checkBoxDomElement.classList.replace(/jstree-icon-checkbox-.+/,"jstree-icon-checkbox-unchecked") + } }); // TODO: all other plugin stuff (cascade, etc) @@ -23,12 +59,42 @@ jsTree.plugin.checkbox = function (options) { node.forEach(x => this.check(x)); return this; } - node = this.node(node); + node = this.node(node); if (node) { this.setState(node, "checked", true); - this.setState(node, "indeterminate", false); - // TODO: three state! disabled! hidden! + this.setState(node, "indeterminate", false); this.trigger("check", { node : node }); + if (options.cascade.down) { + if (!this.getState(node, 'loaded', true)) { + this.load(node, () => this.check(node)); + } else { + this.check(node.children); + } + } + if (options.cascade.up) { + const traverseUpIndeterminate = (node)=>{ + const parent = node.getParent(); + if (parent) { + const nodeAndSiblings = parent.getChildren(); + if (nodeAndSiblings.every(s=>this.getState(s,"checked"))) // if 'checked'===true we know that indetermiante must be false + { + if (!this.getState(parent,"checked")) + this.check(parent) // this will tirgger update at parent as well (if required) + } else if ( + options.cascade.indeterminate && + nodeAndSiblings.some(s=>this.getState(s,"checked") || this.getState(s,"indeterminate",false)) + ) { + if (!this.getState(parent,"indeterminate")) + { + this.setState(parent,"indeterminate",true); + // since we don't have a 'undeterminate' setter, we need to traverse this up 'manually' + traverseUpIndeterminate(parent); + } + } + } + } + traverseUpIndeterminate(node); + } } return this; } @@ -42,6 +108,41 @@ jsTree.plugin.checkbox = function (options) { this.setState(node, "checked", false); this.setState(node, "indeterminate", false); this.trigger("uncheck", { node : node }); + if (options.cascade.down) { + if (!this.getState(node, 'loaded', true)) { + this.load(node, () => this.uncheck(node)); + } else { + this.uncheck(node.children); + } + } + if (options.cascade.up) { + const traverseUpIndeterminate = (node)=>{ + const parent = node.getParent(); + if (parent) { + const nodeAndSiblings = parent.getChildren(); + if (nodeAndSiblings.every(s=>!this.getState(s,"checked") && !this.getState(s,"indeterminate"))) + { + if (this.getState(parent,"checked") || this.getState(parent,"indeterminate")) + { + this.uncheck(parent) // this will tirgger update at parent as well (if required) + } + } else if ( + options.cascade.indeterminate && + nodeAndSiblings.some(s=>this.getState(s,"checked",false) || this.getState(s,"indeterminate",false)) + ) { + if (this.getState(parent,"checked",false)) + { + this.setState(parent,"checked",false); + this.setState(parent,"indeterminate",true); + // since we don't have a 'undeterminate' setter, we need to traverse this up 'manually' + // also to avoid infitive update loop... + traverseUpIndeterminate(parent); + } + } + } + } + traverseUpIndeterminate(node); + } } return this; } @@ -62,6 +163,7 @@ jsTree.plugin.checkbox = function (options) { return this; } this.getChecked = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? return this.tree.find(function (node) { return node.data && node.data.state && node.data.state.checked; }); @@ -78,6 +180,7 @@ jsTree.plugin.checkbox = function (options) { return Array.from(recurse(this.root)); } this.getBottomChecked = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? let recurse = function * (node) { for (let child of node.children) { if (node.data.state && node.data.state.checked && !child.children.length) { @@ -89,6 +192,7 @@ jsTree.plugin.checkbox = function (options) { return Array.from(recurse(this.root)); } this.getIndeterminate = function() { + // how about 'not yet loaded' nodes in case of 'cascade down'? return this.tree.find(function (node) { return node.data && node.data.state && node.data.state.indeterminate; });