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 @@
+
\ 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 @@
+
\ 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;
});