From a8ff18a3409a7f338f8bbfd9ce82f67727fdf12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Aug 2022 03:40:24 +0200 Subject: [PATCH 1/9] Added an basic implementation of bootstrap-treeview in typescript --- .../controllers/elements/tree_controller.js | 20 +- .../src/css/bootstrap-treeview.css | 52 + .../src/js/bootstrap-treeview.js | 1948 +++++++++++++++++ assets/ts_src/BSTreeSearchOptions.ts | 7 + assets/ts_src/BSTreeView.ts | 1866 ++++++++++++++++ assets/ts_src/BSTreeViewDisableOptions.ts | 8 + assets/ts_src/BSTreeViewEventOptions.ts | 12 + assets/ts_src/BSTreeViewExpandOptions.ts | 7 + assets/ts_src/BSTreeViewNode.ts | 44 + assets/ts_src/BSTreeViewNodeState.ts | 8 + assets/ts_src/BSTreeViewOptions.ts | 78 + assets/ts_src/BSTreeViewSelectOptions.ts | 7 + assets/tsconfig.json | 4 +- webpack.config.js | 2 +- 14 files changed, 4050 insertions(+), 13 deletions(-) create mode 100644 assets/js/lib/bootstrap-treeview/src/css/bootstrap-treeview.css create mode 100644 assets/js/lib/bootstrap-treeview/src/js/bootstrap-treeview.js create mode 100644 assets/ts_src/BSTreeSearchOptions.ts create mode 100644 assets/ts_src/BSTreeView.ts create mode 100644 assets/ts_src/BSTreeViewDisableOptions.ts create mode 100644 assets/ts_src/BSTreeViewEventOptions.ts create mode 100644 assets/ts_src/BSTreeViewExpandOptions.ts create mode 100644 assets/ts_src/BSTreeViewNode.ts create mode 100644 assets/ts_src/BSTreeViewNodeState.ts create mode 100644 assets/ts_src/BSTreeViewOptions.ts create mode 100644 assets/ts_src/BSTreeViewSelectOptions.ts diff --git a/assets/controllers/elements/tree_controller.js b/assets/controllers/elements/tree_controller.js index 44a4ee34..6ed34d51 100644 --- a/assets/controllers/elements/tree_controller.js +++ b/assets/controllers/elements/tree_controller.js @@ -1,7 +1,9 @@ import {Controller} from "@hotwired/stimulus"; -import "patternfly-bootstrap-treeview/src/css/bootstrap-treeview.css" -import "patternfly-bootstrap-treeview"; +import "../../js/lib/bootstrap-treeview/src/css/bootstrap-treeview.css" +//import "../../js/lib/bootstrap-treeview/src/js/bootstrap-treeview.js" + +import BSTreeView from "../../ts_src/BSTreeView"; export default class extends Controller { static targets = [ "tree" ]; @@ -27,9 +29,9 @@ export default class extends Controller { //Fetch data and initialize tree this._getData() .then(this._fillTree.bind(this)) - .catch((err) => { + /*.catch((err) => { console.error("Could not load the tree data: " + err); - }); + });*/ } setData(data) { @@ -46,9 +48,7 @@ export default class extends Controller { //Get primary color from css variable const primary_color = getComputedStyle(document.documentElement).getPropertyValue('--bs-warning'); - const tree = this.treeTarget; - - $(tree).treeview({ + const tree = new BSTreeView(this.treeTarget, { data: data, enableLinks: true, showIcon: false, @@ -62,7 +62,7 @@ export default class extends Controller { let a = document.createElement('a'); a.setAttribute('href', data.href); a.innerHTML = ""; - $(tree).append(a); + document.body.appendChild(a); a.click(); a.remove(); } @@ -71,13 +71,13 @@ export default class extends Controller { expandIcon: "fas fa-plus fa-fw fa-treeview", collapseIcon: "fas fa-minus fa-fw fa-treeview" }) - .on('initialized', function () { + /*.on('initialized', function () { //Collapse all nodes after init $(this).treeview('collapseAll', {silent: true}); //Reveal the selected ones $(this).treeview('revealNode', [$(this).treeview('getSelected')]); - }); + });*/ } collapseAll() { diff --git a/assets/js/lib/bootstrap-treeview/src/css/bootstrap-treeview.css b/assets/js/lib/bootstrap-treeview/src/css/bootstrap-treeview.css new file mode 100644 index 00000000..35a92849 --- /dev/null +++ b/assets/js/lib/bootstrap-treeview/src/css/bootstrap-treeview.css @@ -0,0 +1,52 @@ +/* ========================================================= + * patternfly-bootstrap-treeview.css v2.1.0 + * ========================================================= + * Copyright 2013 Jonathan Miles + * Project URL : http://www.jondmiles.com/bootstrap-treeview + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + +.treeview .list-group-item { + cursor: pointer; +} + +.treeview span.indent { + margin-left: 10px; + margin-right: 10px; +} + +.treeview span.icon { + width: 12px; + margin-right: 5px; +} + +.treeview .node-disabled { + color: silver; + cursor: not-allowed; +} + +.treeview .node-hidden { + display: none; +} + +.treeview span.image { + display: inline-block; + width: 12px; + height: 1.19em; + vertical-align: middle; + background-size: contain; + background-repeat: no-repeat; + margin-right: 5px; + line-height: 1em; +} diff --git a/assets/js/lib/bootstrap-treeview/src/js/bootstrap-treeview.js b/assets/js/lib/bootstrap-treeview/src/js/bootstrap-treeview.js new file mode 100644 index 00000000..048a80b9 --- /dev/null +++ b/assets/js/lib/bootstrap-treeview/src/js/bootstrap-treeview.js @@ -0,0 +1,1948 @@ +/* ========================================================= + * patternfly-bootstrap-treeview.js v2.1.0 + * ========================================================= + * Copyright 2013 Jonathan Miles + * Project URL : http://www.jondmiles.com/bootstrap-treeview + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + +;(function ($, window, document, undefined) { + + /*global jQuery, console*/ + + 'use strict'; + + var pluginName = 'treeview'; + + var _default = {}; + + _default.settings = { + + injectStyle: true, + + levels: 2, + + expandIcon: 'glyphicon glyphicon-plus', + collapseIcon: 'glyphicon glyphicon-minus', + loadingIcon: 'glyphicon glyphicon-hourglass', + emptyIcon: 'glyphicon', + nodeIcon: '', + selectedIcon: '', + checkedIcon: 'glyphicon glyphicon-check', + partiallyCheckedIcon: 'glyphicon glyphicon-expand', + uncheckedIcon: 'glyphicon glyphicon-unchecked', + tagsClass: 'badge', + + color: undefined, + backColor: undefined, + borderColor: undefined, + changedNodeColor: '#39A5DC', + onhoverColor: '#F5F5F5', + selectedColor: '#FFFFFF', + selectedBackColor: '#428bca', + searchResultColor: '#D9534F', + searchResultBackColor: undefined, + + highlightSelected: true, + highlightSearchResults: true, + showBorder: true, + showIcon: true, + showImage: false, + showCheckbox: false, + checkboxFirst: false, + highlightChanges: false, + showTags: false, + multiSelect: false, + preventUnselect: false, + allowReselect: false, + hierarchicalCheck: false, + propagateCheckEvent: false, + wrapNodeText: false, + + // Event handlers + onLoading: undefined, + onLoadingFailed: undefined, + onInitialized: undefined, + onNodeRendered: undefined, + onRendered: undefined, + onDestroyed: undefined, + + onNodeChecked: undefined, + onNodeCollapsed: undefined, + onNodeDisabled: undefined, + onNodeEnabled: undefined, + onNodeExpanded: undefined, + onNodeSelected: undefined, + onNodeUnchecked: undefined, + onNodeUnselected: undefined, + + onSearchComplete: undefined, + onSearchCleared: undefined + }; + + _default.options = { + silent: false, + ignoreChildren: false + }; + + _default.searchOptions = { + ignoreCase: true, + exactMatch: false, + revealResults: true + }; + + _default.dataUrl = { + method: 'GET', + dataType: 'json', + cache: false + }; + + var Tree = function (element, options) { + this.$element = $(element); + this._elementId = element.id; + this._styleId = this._elementId + '-style'; + + this._init(options); + + return { + + // Options (public access) + options: this._options, + + // Initialize / destroy methods + init: $.proxy(this._init, this), + remove: $.proxy(this._remove, this), + + // Query methods + findNodes: $.proxy(this.findNodes, this), + getNodes: $.proxy(this.getNodes, this), // todo document + test + getParents: $.proxy(this.getParents, this), + getSiblings: $.proxy(this.getSiblings, this), + getSelected: $.proxy(this.getSelected, this), + getUnselected: $.proxy(this.getUnselected, this), + getExpanded: $.proxy(this.getExpanded, this), + getCollapsed: $.proxy(this.getCollapsed, this), + getChecked: $.proxy(this.getChecked, this), + getUnchecked: $.proxy(this.getUnchecked, this), + getDisabled: $.proxy(this.getDisabled, this), + getEnabled: $.proxy(this.getEnabled, this), + + // Tree manipulation methods + addNode: $.proxy(this.addNode, this), + addNodeAfter: $.proxy(this.addNodeAfter, this), + addNodeBefore: $.proxy(this.addNodeBefore, this), + removeNode: $.proxy(this.removeNode, this), + updateNode: $.proxy(this.updateNode, this), + + // Select methods + selectNode: $.proxy(this.selectNode, this), + unselectNode: $.proxy(this.unselectNode, this), + toggleNodeSelected: $.proxy(this.toggleNodeSelected, this), + + // Expand / collapse methods + collapseAll: $.proxy(this.collapseAll, this), + collapseNode: $.proxy(this.collapseNode, this), + expandAll: $.proxy(this.expandAll, this), + expandNode: $.proxy(this.expandNode, this), + toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), + revealNode: $.proxy(this.revealNode, this), + + // Check / uncheck methods + checkAll: $.proxy(this.checkAll, this), + checkNode: $.proxy(this.checkNode, this), + uncheckAll: $.proxy(this.uncheckAll, this), + uncheckNode: $.proxy(this.uncheckNode, this), + toggleNodeChecked: $.proxy(this.toggleNodeChecked, this), + unmarkCheckboxChanges: $.proxy(this.unmarkCheckboxChanges, this), + + // Disable / enable methods + disableAll: $.proxy(this.disableAll, this), + disableNode: $.proxy(this.disableNode, this), + enableAll: $.proxy(this.enableAll, this), + enableNode: $.proxy(this.enableNode, this), + toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this), + + // Search methods + search: $.proxy(this.search, this), + clearSearch: $.proxy(this.clearSearch, this) + }; + }; + + Tree.prototype._init = function (options) { + this._tree = []; + this._initialized = false; + + this._options = $.extend({}, _default.settings, options); + + // Cache empty icon DOM template + this._template.icon.empty.addClass(this._options.emptyIcon); + + this._destroy(); + this._subscribeEvents(); + + this._triggerEvent('loading', null, _default.options); + this._load(options) + .then($.proxy(function (data) { + // load done + return this._tree = $.extend(true, [], data); + }, this), $.proxy(function (error) { + // load fail + this._triggerEvent('loadingFailed', error, _default.options); + }, this)) + .then($.proxy(function (treeData) { + // initialize data + return this._setInitialStates({ nodes: treeData }, 0); + }, this)) + .then($.proxy(function () { + // render to DOM + this._render(); + }, this)); + }; + + Tree.prototype._load = function (options) { + var done = new $.Deferred(); + if (options.data) { + this._loadLocalData(options, done); + } else if (options.dataUrl) { + this._loadRemoteData(options, done); + } + return done.promise(); + }; + + Tree.prototype._loadRemoteData = function (options, done) { + $.ajax($.extend(true, {}, _default.dataUrl, options.dataUrl)) + .done(function (data) { + done.resolve(data); + }) + .fail(function (xhr, status, error) { + done.reject(error); + }); + }; + + Tree.prototype._loadLocalData = function (options, done) { + done.resolve((typeof options.data === 'string') ? + JSON.parse(options.data) : + $.extend(true, [], options.data)); + }; + + Tree.prototype._remove = function () { + this._destroy(); + $.removeData(this, pluginName); + $('#' + this._styleId).remove(); + }; + + Tree.prototype._destroy = function () { + if (!this._initialized) return; + this._initialized = false; + + this._triggerEvent('destroyed', null, _default.options); + + // Switch off events + this._unsubscribeEvents(); + + // Tear down + this.$wrapper.remove(); + this.$wrapper = null; + }; + + Tree.prototype._unsubscribeEvents = function () { + this.$element.off('loading'); + this.$element.off('loadingFailed'); + this.$element.off('initialized'); + this.$element.off('nodeRendered'); + this.$element.off('rendered'); + this.$element.off('destroyed'); + this.$element.off('click'); + this.$element.off('nodeChecked'); + this.$element.off('nodeCollapsed'); + this.$element.off('nodeDisabled'); + this.$element.off('nodeEnabled'); + this.$element.off('nodeExpanded'); + this.$element.off('nodeSelected'); + this.$element.off('nodeUnchecked'); + this.$element.off('nodeUnselected'); + this.$element.off('searchComplete'); + this.$element.off('searchCleared'); + }; + + Tree.prototype._subscribeEvents = function () { + this._unsubscribeEvents(); + + if (typeof (this._options.onLoading) === 'function') { + this.$element.on('loading', this._options.onLoading); + } + + if (typeof (this._options.onLoadingFailed) === 'function') { + this.$element.on('loadingFailed', this._options.onLoadingFailed); + } + + if (typeof (this._options.onInitialized) === 'function') { + this.$element.on('initialized', this._options.onInitialized); + } + + if (typeof (this._options.onNodeRendered) === 'function') { + this.$element.on('nodeRendered', this._options.onNodeRendered); + } + + if (typeof (this._options.onRendered) === 'function') { + this.$element.on('rendered', this._options.onRendered); + } + + if (typeof (this._options.onDestroyed) === 'function') { + this.$element.on('destroyed', this._options.onDestroyed); + } + + this.$element.on('click', $.proxy(this._clickHandler, this)); + + if (typeof (this._options.onNodeChecked) === 'function') { + this.$element.on('nodeChecked', this._options.onNodeChecked); + } + + if (typeof (this._options.onNodeCollapsed) === 'function') { + this.$element.on('nodeCollapsed', this._options.onNodeCollapsed); + } + + if (typeof (this._options.onNodeDisabled) === 'function') { + this.$element.on('nodeDisabled', this._options.onNodeDisabled); + } + + if (typeof (this._options.onNodeEnabled) === 'function') { + this.$element.on('nodeEnabled', this._options.onNodeEnabled); + } + + if (typeof (this._options.onNodeExpanded) === 'function') { + this.$element.on('nodeExpanded', this._options.onNodeExpanded); + } + + if (typeof (this._options.onNodeSelected) === 'function') { + this.$element.on('nodeSelected', this._options.onNodeSelected); + } + + if (typeof (this._options.onNodeUnchecked) === 'function') { + this.$element.on('nodeUnchecked', this._options.onNodeUnchecked); + } + + if (typeof (this._options.onNodeUnselected) === 'function') { + this.$element.on('nodeUnselected', this._options.onNodeUnselected); + } + + if (typeof (this._options.onSearchComplete) === 'function') { + this.$element.on('searchComplete', this._options.onSearchComplete); + } + + if (typeof (this._options.onSearchCleared) === 'function') { + this.$element.on('searchCleared', this._options.onSearchCleared); + } + }; + + Tree.prototype._triggerEvent = function (event, data, options) { + if (options && !options.silent) { + this.$element.trigger(event, $.extend(true, {}, data)); + } + } + + /* + Recurse the tree structure and ensure all nodes have + valid initial states. User defined states will be preserved. + For performance we also take this opportunity to + index nodes in a flattened ordered structure + */ + Tree.prototype._setInitialStates = function (node, level) { + this._nodes = {}; + return $.when.apply(this, this._setInitialState(node, level)) + .done($.proxy(function () { + this._orderedNodes = this._sortNodes(); + this._inheritCheckboxChanges(); + this._triggerEvent('initialized', this._orderedNodes, _default.options); + return; + }, this)); + }; + + Tree.prototype._setInitialState = function (node, level, done) { + if (!node.nodes) return; + level += 1; + done = done || []; + + var parent = node; + $.each(node.nodes, $.proxy(function (index, node) { + var deferred = new $.Deferred(); + done.push(deferred.promise()); + + // level : hierarchical tree level, starts at 1 + node.level = level; + + // index : relative to siblings + node.index = index; + + // nodeId : unique, hierarchical identifier + node.nodeId = (parent && parent.nodeId) ? + parent.nodeId + '.' + node.index : + (level - 1) + '.' + node.index; + + // parentId : transversing up the tree + node.parentId = parent.nodeId; + + // if not provided set selectable default value + if (!node.hasOwnProperty('selectable')) { + node.selectable = true; + } + + // if not provided set checkable default value + if (!node.hasOwnProperty('checkable')) { + node.checkable = true; + } + + // where provided we should preserve states + node.state = node.state || {}; + + // set checked state; unless set always false + if (!node.state.hasOwnProperty('checked')) { + node.state.checked = false; + } + + // convert the undefined string if hierarchical checks are enabled + if (this._options.hierarchicalCheck && node.state.checked === 'undefined') { + node.state.checked = undefined; + } + + // set enabled state; unless set always false + if (!node.state.hasOwnProperty('disabled')) { + node.state.disabled = false; + } + + // set expanded state; if not provided based on levels + if (!node.state.hasOwnProperty('expanded')) { + if (!node.state.disabled && + (level < this._options.levels) && + (node.nodes && node.nodes.length > 0)) { + node.state.expanded = true; + } + else { + node.state.expanded = false; + } + } + + // set selected state; unless set always false + if (!node.state.hasOwnProperty('selected')) { + node.state.selected = false; + } + + // set visible state; based parent state plus levels + if ((parent && parent.state && parent.state.expanded) || + (level <= this._options.levels)) { + node.state.visible = true; + } + else { + node.state.visible = false; + } + + // recurse child nodes and transverse the tree, depth-first + if (node.nodes) { + if (node.nodes.length > 0) { + this._setInitialState(node, level, done); + } + else { + delete node.nodes; + } + } + + // add / update indexed collection + this._nodes[node.nodeId] = node; + + // mark task as complete + deferred.resolve(); + }, this)); + + return done; + }; + + Tree.prototype._sortNodes = function () { + return $.map(Object.keys(this._nodes).sort(function (a, b) { + if (a === b) return 0; + var a = a.split('.').map(function (level) { return parseInt(level); }); + var b = b.split('.').map(function (level) { return parseInt(level); }); + + var c = Math.max(a.length, b.length); + for (var i=0; i 0) return +1; + if (a[i] - b[i] < 0) return -1; + }; + + }), $.proxy(function (value, index) { + return this._nodes[value]; + }, this)); + }; + + Tree.prototype._clickHandler = function (event) { + + var target = $(event.target); + var node = this.targetNode(target); + if (!node || node.state.disabled) return; + + var classList = target.attr('class') ? target.attr('class').split(' ') : []; + if ((classList.indexOf('expand-icon') !== -1)) { + this._toggleExpanded(node, $.extend({}, _default.options)); + } + else if ((classList.indexOf('check-icon') !== -1)) { + if (node.checkable) { + this._toggleChecked(node, $.extend({}, _default.options)); + } + } + else { + if (node.selectable) { + this._toggleSelected(node, $.extend({}, _default.options)); + } else { + this._toggleExpanded(node, $.extend({}, _default.options)); + } + } + }; + + // Looks up the DOM for the closest parent list item to retrieve the + // data attribute nodeid, which is used to lookup the node in the flattened structure. + Tree.prototype.targetNode = function (target) { + var nodeId = target.closest('li.list-group-item').attr('data-nodeId'); + var node = this._nodes[nodeId]; + if (!node) { + console.log('Error: node does not exist'); + } + return node; + }; + + Tree.prototype._toggleExpanded = function (node, options) { + if (!node) return; + + // Lazy-load the child nodes if possible + if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) { + this._lazyLoad(node); + } else { + this._setExpanded(node, !node.state.expanded, options); + } + }; + + Tree.prototype._lazyLoad = function (node) { + // Show a different icon while loading the child nodes + node.$el.children('span.expand-icon') + .removeClass(this._options.expandIcon) + .addClass(this._options.loadingIcon); + + var _this = this; + this._options.lazyLoad(node, function (nodes) { + // Adding the node will expand its parent automatically + _this.addNode(nodes, node); + }); + // Only the first expand should do a lazy-load + delete node.lazyLoad; + }; + + Tree.prototype._setExpanded = function (node, state, options) { + + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.expanded) return; + + if (state && node.nodes) { + + // Set node state + node.state.expanded = true; + + // Set element + if (node.$el) { + node.$el.children('span.expand-icon') + .removeClass(this._options.expandIcon) + .removeClass(this._options.loadingIcon) + .addClass(this._options.collapseIcon); + } + + // Expand children + if (node.nodes && options) { + $.each(node.nodes, $.proxy(function (index, node) { + this._setVisible(node, true, options); + }, this)); + } + + // Optionally trigger event + this._triggerEvent('nodeExpanded', node, options); + } + else if (!state) { + + // Set node state + node.state.expanded = false; + + // Set element + if (node.$el) { + node.$el.children('span.expand-icon') + .removeClass(this._options.collapseIcon) + .addClass(this._options.expandIcon); + } + + // Collapse children + if (node.nodes && options) { + $.each(node.nodes, $.proxy(function (index, node) { + this._setVisible(node, false, options); + this._setExpanded(node, false, options); + }, this)); + } + + // Optionally trigger event + this._triggerEvent('nodeCollapsed', node, options); + } + }; + + Tree.prototype._setVisible = function (node, state, options) { + + if (options && state === node.state.visible) return; + + if (state) { + + // Set node state + node.state.visible = true; + + // Set element + if (node.$el) { + node.$el.removeClass('node-hidden'); + } + } + else { + + // Set node state to unchecked + node.state.visible = false; + + // Set element + if (node.$el) { + node.$el.addClass('node-hidden'); + } + } + }; + + Tree.prototype._toggleSelected = function (node, options) { + if (!node) return; + this._setSelected(node, !node.state.selected, options); + return this; + }; + + Tree.prototype._setSelected = function (node, state, options) { + + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && (state === node.state.selected)) return; + + if (state) { + + // If multiSelect false, unselect previously selected + if (!this._options.multiSelect) { + $.each(this._findNodes('true', 'state.selected'), $.proxy(function (index, node) { + this._setSelected(node, false, $.extend(options, {unselecting: true})); + }, this)); + } + + // Set node state + node.state.selected = true; + + // Set element + if (node.$el) { + node.$el.addClass('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + node.$el.children('span.node-icon') + .removeClass(node.icon || this._options.nodeIcon) + .addClass(node.selectedIcon || this._options.selectedIcon); + } + } + + // Optionally trigger event + this._triggerEvent('nodeSelected', node, options); + } + else { + + // If preventUnselect true + only one remaining selection, disable unselect + if (this._options.preventUnselect && + (options && !options.unselecting) && + (this._findNodes('true', 'state.selected').length === 1)) { + // Fire the nodeSelected event if reselection is allowed + if (this._options.allowReselect) { + this._triggerEvent('nodeSelected', node, options); + } + return this; + } + + // Set node state + node.state.selected = false; + + // Set element + if (node.$el) { + node.$el.removeClass('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + node.$el.children('span.node-icon') + .removeClass(node.selectedIcon || this._options.selectedIcon) + .addClass(node.icon || this._options.nodeIcon); + } + } + + // Optionally trigger event + this._triggerEvent('nodeUnselected', node, options); + } + + return this; + }; + + Tree.prototype._inheritCheckboxChanges = function () { + if (this._options.showCheckbox && this._options.highlightChanges) { + this._checkedNodes = $.grep(this._orderedNodes, function (node) { + return node.state.checked; + }); + } + }; + + Tree.prototype._toggleChecked = function (node, options) { + if (!node) return; + + if (this._options.hierarchicalCheck) { + // Event propagation to the parent/child nodes + var childOptions = $.extend({}, options, {silent: options.silent || !this._options.propagateCheckEvent}); + + var state, currentNode = node; + // Temporarily swap the tree state + node.state.checked = !node.state.checked; + + // Iterate through each parent node + while (currentNode = this._nodes[currentNode.parentId]) { + + // Calculate the state + state = currentNode.nodes.reduce(function (acc, curr) { + return (acc === curr.state.checked) ? acc : undefined; + }, currentNode.nodes[0].state.checked); + + // Set the state + this._setChecked(currentNode, state, childOptions); + } + + if (node.nodes && node.nodes.length > 0) { + // Copy the content of the array + var child, children = node.nodes.slice(); + // Iterate through each child node + while (children && children.length > 0) { + child = children.pop(); + + // Set the state + this._setChecked(child, node.state.checked, childOptions); + + // Append children to the end of the list + if (child.nodes && child.nodes.length > 0) { + children = children.concat(child.nodes.slice()); + } + } + } + // Swap back the tree state + node.state.checked = !node.state.checked; + } + + this._setChecked(node, !node.state.checked, options); + }; + + Tree.prototype._setChecked = function (node, state, options) { + + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.checked) return; + + // Highlight the node if its checkbox has unsaved changes + if (this._options.highlightChanges) { + node.$el.toggleClass('node-check-changed', (this._checkedNodes.indexOf(node) == -1) == state); + } + + if (state) { + + // Set node state + node.state.checked = true; + + // Set element + if (node.$el) { + node.$el.addClass('node-checked').removeClass('node-checked-partial'); + node.$el.children('span.check-icon') + .removeClass(this._options.uncheckedIcon) + .removeClass(this._options.partiallyCheckedIcon) + .addClass(this._options.checkedIcon); + } + + // Optionally trigger event + this._triggerEvent('nodeChecked', node, options); + } + else if (state === undefined && this._options.hierarchicalCheck) { + + // Set node state to partially checked + node.state.checked = undefined; + + // Set element + if (node.$el) { + node.$el.addClass('node-checked-partial').removeClass('node-checked'); + node.$el.children('span.check-icon') + .removeClass(this._options.uncheckedIcon) + .removeClass(this._options.checkedIcon) + .addClass(this._options.partiallyCheckedIcon); + } + + // Optionally trigger event, partially checked is technically unchecked + this._triggerEvent('nodeUnchecked', node, options); + } else { + + // Set node state to unchecked + node.state.checked = false; + + // Set element + if (node.$el) { + node.$el.removeClass('node-checked node-checked-partial'); + node.$el.children('span.check-icon') + .removeClass(this._options.checkedIcon) + .removeClass(this._options.partiallyCheckedIcon) + .addClass(this._options.uncheckedIcon); + } + + // Optionally trigger event + this._triggerEvent('nodeUnchecked', node, options); + } + }; + + Tree.prototype._setDisabled = function (node, state, options) { + + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.disabled) return; + + if (state) { + + // Set node state to disabled + node.state.disabled = true; + + // Disable all other states + if (options && !options.keepState) { + this._setSelected(node, false, options); + this._setChecked(node, false, options); + this._setExpanded(node, false, options); + } + + // Set element + if (node.$el) { + node.$el.addClass('node-disabled'); + } + + // Optionally trigger event + this._triggerEvent('nodeDisabled', node, options); + } + else { + + // Set node state to enabled + node.state.disabled = false; + + // Set element + if (node.$el) { + node.$el.removeClass('node-disabled'); + } + + // Optionally trigger event + this._triggerEvent('nodeEnabled', node, options); + } + }; + + Tree.prototype._setSearchResult = function (node, state, options) { + if (options && state === node.searchResult) return; + + if (state) { + + node.searchResult = true; + + if (node.$el) { + node.$el.addClass('node-result'); + } + } + else { + + node.searchResult = false; + + if (node.$el) { + node.$el.removeClass('node-result'); + } + } + }; + + Tree.prototype._render = function () { + if (!this._initialized) { + + // Setup first time only components + this.$wrapper = this._template.tree.clone(); + this.$element.empty() + .addClass(pluginName) + .append(this.$wrapper); + + this._injectStyle(); + + this._initialized = true; + } + + var previousNode; + $.each(this._orderedNodes, $.proxy(function (id, node) { + this._renderNode(node, previousNode); + previousNode = node; + }, this)); + + this._triggerEvent('rendered', this._orderedNodes, _default.options); + }; + + Tree.prototype._renderNode = function (node, previousNode) { + if (!node) return; + + if (!node.$el) { + node.$el = this._newNodeEl(node, previousNode) + .addClass('node-' + this._elementId); + } + else { + node.$el.empty(); + } + + // Append .classes to the node + node.$el.addClass(node.class); + + // Set the #id of the node if specified + if (node.id) { + node.$el.attr('id', node.id); + } + + // Append custom data- attributes to the node + if (node.dataAttr) { + $.each(node.dataAttr, function (key, value) { + node.$el.attr('data-' + key, value); + }); + } + + // Set / update nodeid; it can change as a result of addNode etc. + node.$el.attr('data-nodeId', node.nodeId); + + // Set the tooltip attribute if present + if (node.tooltip) { + node.$el.attr('title', node.tooltip); + } + + // Add indent/spacer to mimic tree structure + for (var i = 0; i < (node.level - 1); i++) { + node.$el.append(this._template.indent.clone()); + } + + // Add expand / collapse or empty spacer icons + node.$el + .append( + node.nodes || node.lazyLoad ? this._template.icon.expand.clone() : this._template.icon.empty.clone() + ); + + // Add checkbox and node icons + if (this._options.checkboxFirst) { + this._addCheckbox(node); + this._addIcon(node); + this._addImage(node); + } else { + this._addIcon(node); + this._addImage(node); + this._addCheckbox(node); + } + + // Add text + if (this._options.wrapNodeText) { + var wrapper = this._template.text.clone(); + node.$el.append(wrapper); + wrapper.append(node.text); + } else { + node.$el.append(node.text); + } + + // Add tags as badges + if (this._options.showTags && node.tags) { + $.each(node.tags, $.proxy(function addTag(id, tag) { + node.$el + .append(this._template.badge.clone() + .addClass( + (typeof tag === 'object' ? tag.class : undefined) + || node.tagsClass + || this._options.tagsClass + ) + .append( + (typeof tag === 'object' ? tag.text : undefined) + || tag + ) + ); + }, this)); + } + + // Set various node states + this._setSelected(node, node.state.selected); + this._setChecked(node, node.state.checked); + this._setSearchResult(node, node.searchResult); + this._setExpanded(node, node.state.expanded); + this._setDisabled(node, node.state.disabled); + this._setVisible(node, node.state.visible); + + // Trigger nodeRendered event + this._triggerEvent('nodeRendered', node, _default.options); + }; + + // Add checkable icon + Tree.prototype._addCheckbox = function (node) { + if (this._options.showCheckbox && (node.hideCheckbox === undefined || node.hideCheckbox === false)) { + node.$el + .append(this._template.icon.check.clone()); + } + } + + // Add node icon + Tree.prototype._addIcon = function (node) { + if (this._options.showIcon && !(this._options.showImage && node.image)) { + node.$el + .append(this._template.icon.node.clone() + .addClass(node.icon || this._options.nodeIcon) + ); + } + } + + Tree.prototype._addImage = function (node) { + if (this._options.showImage && node.image) { + node.$el + .append(this._template.image.clone() + .addClass('node-image') + .css('background-image', "url('" + node.image + "')") + ); + } + } + + // Creates a new node element from template and + // ensures the template is inserted at the correct position + Tree.prototype._newNodeEl = function (node, previousNode) { + var $el = this._template.node.clone(); + + if (previousNode) { + // typical usage, as nodes are rendered in + // sort order we add after the previous element + previousNode.$el.after($el); + } else { + // we use prepend instead of append, + // to cater for root inserts i.e. nodeId 0.0 + this.$wrapper.prepend($el); + } + + return $el; + }; + + // Recursively remove node elements from DOM + Tree.prototype._removeNodeEl = function (node) { + if (!node) return; + + if (node.nodes) { + $.each(node.nodes, $.proxy(function (index, node) { + this._removeNodeEl(node); + }, this)); + } + node.$el.remove(); + }; + + // Expand node, rendering it's immediate children + Tree.prototype._expandNode = function (node) { + if (!node.nodes) return; + + $.each(node.nodes.slice(0).reverse(), $.proxy(function (index, childNode) { + childNode.level = node.level + 1; + this._renderNode(childNode, node.$el); + }, this)); + }; + + // Add inline style into head + Tree.prototype._injectStyle = function () { + if (this._options.injectStyle && !document.getElementById(this._styleId)) { + $('').appendTo('head'); + } + }; + + // Construct trees style based on user options + Tree.prototype._buildStyle = function () { + var style = '.node-' + this._elementId + '{'; + + // Basic bootstrap style overrides + if (this._options.color) { + style += 'color:' + this._options.color + ';'; + } + + if (this._options.backColor) { + style += 'background-color:' + this._options.backColor + ';'; + } + + if (!this._options.showBorder) { + style += 'border:none;'; + } + else if (this._options.borderColor) { + style += 'border:1px solid ' + this._options.borderColor + ';'; + } + style += '}'; + + if (this._options.onhoverColor) { + style += '.node-' + this._elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this._options.onhoverColor + ';' + + '}'; + } + + // Style search results + if (this._options.highlightSearchResults && (this._options.searchResultColor || this._options.searchResultBackColor)) { + + var innerStyle = '' + if (this._options.searchResultColor) { + innerStyle += 'color:' + this._options.searchResultColor + ';'; + } + if (this._options.searchResultBackColor) { + innerStyle += 'background-color:' + this._options.searchResultBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-result{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-result:hover{' + innerStyle + '}'; + } + + // Style selected nodes + if (this._options.highlightSelected && (this._options.selectedColor || this._options.selectedBackColor)) { + + var innerStyle = '' + if (this._options.selectedColor) { + innerStyle += 'color:' + this._options.selectedColor + ';'; + } + if (this._options.selectedBackColor) { + innerStyle += 'background-color:' + this._options.selectedBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-selected{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-selected:hover{' + innerStyle + '}'; + } + + // Style changed nodes + if (this._options.highlightChanges) { + var innerStyle = 'color: ' + this._options.changedNodeColor + ';'; + style += '.node-' + this._elementId + '.node-check-changed{' + innerStyle + '}'; + } + + // Node level style overrides + $.each(this._orderedNodes, $.proxy(function (index, node) { + if (node.color || node.backColor) { + var innerStyle = ''; + if (node.color) { + innerStyle += 'color:' + node.color + ';'; + } + if (node.backColor) { + innerStyle += 'background-color:' + node.backColor + ';'; + } + style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"]{' + innerStyle + '}'; + } + + if (node.iconColor) { + var innerStyle = 'color:' + node.iconColor + ';'; + style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"] .node-icon{' + innerStyle + '}'; + } + }, this)); + + return this._css + style; + }; + + Tree.prototype._template = { + tree: $(''), + node: $('
  • '), + indent: $(''), + icon: { + node: $(''), + expand: $(''), + check: $(''), + empty: $('') + }, + image: $(''), + badge: $(''), + text: $('') + }; + + Tree.prototype._css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + + + /** + Returns an array of matching node objects. + @param {String} pattern - A pattern to match against a given field + @return {String} field - Field to query pattern against + */ + Tree.prototype.findNodes = function (pattern, field) { + return this._findNodes(pattern, field); + }; + + + /** + Returns an ordered aarray of node objects. + @return {Array} nodes - An array of all nodes + */ + Tree.prototype.getNodes = function () { + return this._orderedNodes; + }; + + /** + Returns parent nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of parent nodes + */ + Tree.prototype.getParents = function (nodes) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + var parentNodes = []; + $.each(nodes, $.proxy(function (index, node) { + var parentNode = node.parentId ? this._nodes[node.parentId] : false; + if (parentNode) { + parentNodes.push(parentNode); + } + }, this)); + return parentNodes; + }; + + /** + Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of sibling nodes + */ + Tree.prototype.getSiblings = function (nodes) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + var siblingNodes = []; + $.each(nodes, $.proxy(function (index, node) { + var parent = this.getParents([node]); + var nodes = parent[0] ? parent[0].nodes : this._tree; + siblingNodes = nodes.filter(function (obj) { + return obj.nodeId !== node.nodeId; + }); + }, this)); + + // flatten possible nested array before returning + return $.map(siblingNodes, function (obj) { + return obj; + }); + }; + + /** + Returns an array of selected nodes. + @returns {Array} nodes - Selected nodes + */ + Tree.prototype.getSelected = function () { + return this._findNodes('true', 'state.selected'); + }; + + /** + Returns an array of unselected nodes. + @returns {Array} nodes - Unselected nodes + */ + Tree.prototype.getUnselected = function () { + return this._findNodes('false', 'state.selected'); + }; + + /** + Returns an array of expanded nodes. + @returns {Array} nodes - Expanded nodes + */ + Tree.prototype.getExpanded = function () { + return this._findNodes('true', 'state.expanded'); + }; + + /** + Returns an array of collapsed nodes. + @returns {Array} nodes - Collapsed nodes + */ + Tree.prototype.getCollapsed = function () { + return this._findNodes('false', 'state.expanded'); + }; + + /** + Returns an array of checked nodes. + @returns {Array} nodes - Checked nodes + */ + Tree.prototype.getChecked = function () { + return this._findNodes('true', 'state.checked'); + }; + + /** + Returns an array of unchecked nodes. + @returns {Array} nodes - Unchecked nodes + */ + Tree.prototype.getUnchecked = function () { + return this._findNodes('false', 'state.checked'); + }; + + /** + Returns an array of disabled nodes. + @returns {Array} nodes - Disabled nodes + */ + Tree.prototype.getDisabled = function () { + return this._findNodes('true', 'state.disabled'); + }; + + /** + Returns an array of enabled nodes. + @returns {Array} nodes - Enabled nodes + */ + Tree.prototype.getEnabled = function () { + return this._findNodes('false', 'state.disabled'); + }; + + + /** + Add nodes to the tree. + @param {Array} nodes - An array of nodes to add + @param {optional Object} parentNode - The node to which nodes will be added as children + @param {optional number} index - Zero based insert index + @param {optional Object} options + */ + Tree.prototype.addNode = function (nodes, parentNode, index, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (parentNode instanceof Array) { + parentNode = parentNode[0]; + } + + options = $.extend({}, _default.options, options); + + // identify target nodes; either the tree's root or a parent's child nodes + var targetNodes; + if (parentNode && parentNode.nodes) { + targetNodes = parentNode.nodes; + } else if (parentNode) { + targetNodes = parentNode.nodes = []; + } else { + targetNodes = this._tree; + } + + // inserting nodes at specified positions + $.each(nodes, $.proxy(function (i, node) { + var insertIndex = (typeof(index) === 'number') ? (index + i) : (targetNodes.length + 1); + targetNodes.splice(insertIndex, 0, node); + }, this)); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done($.proxy(function () { + if (parentNode && !parentNode.state.expanded) { + this._setExpanded(parentNode, true, options); + } + this._render(); + }, this)); + } + + /** + Add nodes to the tree after given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added after + @param {optional Object} options + */ + Tree.prototype.addNodeAfter = function (nodes, node, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + this.addNode(nodes, this.getParents(node)[0], (node.index + 1), options); + } + + /** + Add nodes to the tree before given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added before + @param {optional Object} options + */ + Tree.prototype.addNodeBefore = function (nodes, node, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + this.addNode(nodes, this.getParents(node)[0], node.index, options); + } + + /** + Removes given nodes from the tree. + @param {Array} nodes - An array of nodes to remove + @param {optional Object} options + */ + Tree.prototype.removeNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + var targetNodes, parentNode; + $.each(nodes, $.proxy(function (index, node) { + + // remove nodes from tree + parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1); + + // remove node from DOM + this._removeNodeEl(node); + }, this)); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done(this._render.bind(this)); + }; + + /** + Updates / replaces a given tree node + @param {Object} node - A single node to be replaced + @param {Object} newNode - THe replacement node + @param {optional Object} options + */ + Tree.prototype.updateNode = function (node, newNode, options) { + if (node instanceof Array) { + node = node[0]; + } + + options = $.extend({}, _default.options, options); + + // insert new node + var targetNodes; + var parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1, newNode); + + // remove old node from DOM + this._removeNodeEl(node); + + // initialize new state and render changes + this._setInitialStates({nodes: this._tree}, 0) + .done(this._render.bind(this)); + }; + + + /** + Selects given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.selectNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setSelected(node, true, options); + }, this)); + }; + + /** + Unselects given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.unselectNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setSelected(node, false, options); + }, this)); + }; + + /** + Toggles a node selected state; selecting if unselected, unselecting if selected. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.toggleNodeSelected = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleSelected(node, options); + }, this)); + }; + + + /** + Collapse all tree nodes + @param {optional Object} options + */ + Tree.prototype.collapseAll = function (options) { + options = $.extend({}, _default.options, options); + options.levels = options.levels || 999; + this.collapseNode(this._tree, options); + }; + + /** + Collapse a given tree node + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.collapseNode = function (nodes, options) { + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setExpanded(node, false, options); + }, this)); + }; + + /** + Expand all tree nodes + @param {optional Object} options + */ + Tree.prototype.expandAll = function (options) { + options = $.extend({}, _default.options, options); + options.levels = options.levels || 999; + this.expandNode(this._tree, options); + }; + + /** + Expand given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.expandNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + // Do not re-expand already expanded nodes + if (node.state.expanded) return; + + if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) { + this._lazyLoad(node); + } + + this._setExpanded(node, true, options); + if (node.nodes) { + this._expandLevels(node.nodes, options.levels-1, options); + } + }, this)); + }; + + Tree.prototype._expandLevels = function (nodes, level, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setExpanded(node, (level > 0) ? true : false, options); + if (node.nodes) { + this._expandLevels(node.nodes, level-1, options); + } + }, this)); + }; + + /** + Reveals given tree nodes, expanding the tree from node to root. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.revealNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + var parentNode = node; + var tmpNode; + while (tmpNode = this.getParents([parentNode])[0]) { + parentNode = tmpNode; + this._setExpanded(parentNode, true, options); + }; + }, this)); + }; + + /** + Toggles a node's expanded state; collapsing if expanded, expanding if collapsed. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.toggleNodeExpanded = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleExpanded(node, options); + }, this)); + }; + + + /** + Check all tree nodes + @param {optional Object} options + */ + Tree.prototype.checkAll = function (options) { + options = $.extend({}, _default.options, options); + + var nodes = $.grep(this._orderedNodes, function (node) { + return !node.state.checked; + }); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, true, options); + }, this)); + }; + + /** + Checks given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.checkNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, true, options); + }, this)); + }; + + /** + Uncheck all tree nodes + @param {optional Object} options + */ + Tree.prototype.uncheckAll = function (options) { + options = $.extend({}, _default.options, options); + + var nodes = $.grep(this._orderedNodes, function (node) { + return node.state.checked || node.state.checked === undefined; + }); + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, false, options); + }, this)); + }; + + /** + Uncheck given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.uncheckNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setChecked(node, false, options); + }, this)); + }; + + /** + Toggles a node's checked state; checking if unchecked, unchecking if checked. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.toggleNodeChecked = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._toggleChecked(node, options); + }, this)); + }; + + /** + Saves the current state of checkboxes as default, cleaning up any highlighted changes + */ + Tree.prototype.unmarkCheckboxChanges = function () { + this._inheritCheckboxChanges(); + + $.each(this._nodes, function (index, node) { + node.$el.removeClass('node-check-changed'); + }); + }; + + /** + Disable all tree nodes + @param {optional Object} options + */ + Tree.prototype.disableAll = function (options) { + options = $.extend({}, _default.options, options); + + var nodes = this._findNodes('false', 'state.disabled'); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, true, options); + }, this)); + }; + + /** + Disable given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.disableNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, true, options); + }, this)); + }; + + /** + Enable all tree nodes + @param {optional Object} options + */ + Tree.prototype.enableAll = function (options) { + options = $.extend({}, _default.options, options); + + var nodes = this._findNodes('true', 'state.disabled'); + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, false, options); + }, this)); + }; + + /** + Enable given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.enableNode = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, false, options); + }, this)); + }; + + /** + Toggles a node's disabled state; disabling is enabled, enabling if disabled. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + Tree.prototype.toggleNodeDisabled = function (nodes, options) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this._setDisabled(node, !node.state.disabled, options); + }, this)); + }; + + + /** + Searches the tree for nodes (text) that match given criteria + @param {String} pattern - A given string to match against + @param {optional Object} options - Search criteria options + @return {Array} nodes - Matching nodes + */ + Tree.prototype.search = function (pattern, options) { + options = $.extend({}, _default.searchOptions, options); + + var previous = this._getSearchResults(); + var results = []; + + if (pattern && pattern.length > 0) { + + if (options.exactMatch) { + pattern = '^' + pattern + '$'; + } + + var modifier = 'g'; + if (options.ignoreCase) { + modifier += 'i'; + } + + results = this._findNodes(pattern, 'text', modifier); + } + + // Clear previous results no longer matched + $.each(this._diffArray(results, previous), $.proxy(function (index, node) { + this._setSearchResult(node, false, options); + }, this)); + + // Set new results + $.each(this._diffArray(previous, results), $.proxy(function (index, node) { + this._setSearchResult(node, true, options); + }, this)); + + // Reveal hidden nodes + if (results && options.revealResults) { + this.revealNode(results); + } + + this._triggerEvent('searchComplete', results, options); + + return results; + }; + + /** + Clears previous search results + */ + Tree.prototype.clearSearch = function (options) { + options = $.extend({}, { render: true }, options); + + var results = $.each(this._getSearchResults(), $.proxy(function (index, node) { + this._setSearchResult(node, false, options); + }, this)); + + this._triggerEvent('searchCleared', results, options); + }; + + Tree.prototype._getSearchResults = function () { + return this._findNodes('true', 'searchResult'); + }; + + Tree.prototype._diffArray = function (a, b) { + var diff = []; + $.grep(b, function (n) { + if ($.inArray(n, a) === -1) { + diff.push(n); + } + }); + return diff; + }; + + /** + Find nodes that match a given criteria + @param {String} pattern - A given string to match against + @param {optional String} attribute - Attribute to compare pattern against + @param {optional String} modifier - Valid RegEx modifiers + @return {Array} nodes - Nodes that match your criteria + */ + Tree.prototype._findNodes = function (pattern, attribute, modifier) { + attribute = attribute || 'text'; + modifier = modifier || 'g'; + return $.grep(this._orderedNodes, $.proxy(function (node) { + var val = this._getNodeValue(node, attribute); + if (typeof val === 'string') { + return val.match(new RegExp(pattern, modifier)); + } + }, this)); + }; + + /** + Recursive find for retrieving nested attributes values + All values are return as strings, unless invalid + @param {Object} obj - Typically a node, could be any object + @param {String} attr - Identifies an object property using dot notation + @return {String} value - Matching attributes string representation + */ + Tree.prototype._getNodeValue = function (obj, attr) { + var index = attr.indexOf('.'); + if (index > 0) { + var _obj = obj[attr.substring(0, index)]; + var _attr = attr.substring(index + 1, attr.length); + return this._getNodeValue(_obj, _attr); + } + else { + if (obj.hasOwnProperty(attr) && obj[attr] !== undefined) { + return obj[attr].toString(); + } + else { + return undefined; + } + } + }; + + var logError = function (message) { + if (window.console) { + window.console.error(message); + } + }; + + // Prevent against multiple instantiations, + // handle updates and method calls + $.fn[pluginName] = function (options, args) { + + var result; + if (this.length == 0) { + throw "No element has been found!"; + } + + this.each(function () { + var _this = $.data(this, pluginName); + if (typeof options === 'string') { + if (!_this) { + logError('Not initialized, can not call method : ' + options); + } + else if (!$.isFunction(_this[options]) || options.charAt(0) === '_') { + logError('No such method : ' + options); + } + else { + if (!(args instanceof Array)) { + args = [ args ]; + } + result = _this[options].apply(_this, args); + } + } + else if (typeof options === 'boolean') { + result = _this; + } + else { + $.data(this, pluginName, new Tree(this, $.extend(true, {}, options))); + } + }); + + return result || this; + }; + +})(jQuery, window, document); diff --git a/assets/ts_src/BSTreeSearchOptions.ts b/assets/ts_src/BSTreeSearchOptions.ts new file mode 100644 index 00000000..b993d9b0 --- /dev/null +++ b/assets/ts_src/BSTreeSearchOptions.ts @@ -0,0 +1,7 @@ +import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; + +export default class BSTreeSearchOptions extends BSTreeViewEventOptions { + ignoreCase: boolean = true; + exactMatch: boolean = false; + revealResults: boolean = true; +} \ No newline at end of file diff --git a/assets/ts_src/BSTreeView.ts b/assets/ts_src/BSTreeView.ts new file mode 100644 index 00000000..863ac4c3 --- /dev/null +++ b/assets/ts_src/BSTreeView.ts @@ -0,0 +1,1866 @@ +import BSTreeViewOptions from "./BSTreeViewOptions"; +import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; +import {default as BSTreeViewNode} from "./BSTreeViewNode"; +import BSTreeViewNodeState from "./BSTreeViewNodeState"; +import BSTreeViewSelectOptions from "./BSTreeViewSelectOptions"; +import BSTreeViewDisableOptions from "./BSTreeViewDisableOptions"; +import BSTreeViewExpandOptions from "./BSTreeViewExpandOptions"; +import BSTreeSearchOptions from "./BSTreeSearchOptions"; + + +const pluginName = 'treeview'; + +const EVENT_LOADING = 'bs-tree:loading'; +const EVENT_LOADING_FAILED = 'bs-tree:loadingFailed'; +const EVENT_INITIALIZED = 'bs-tree:initialized'; +const EVENT_NODE_RENDERED = 'bs-tree:nodeRendered'; +const EVENT_RENDERED = 'bs-tree:rendered'; +const EVENT_DESTROYED = 'bs-tree:destroyed'; + +const EVENT_NODE_CHECKED = 'bs-tree:nodeChecked'; +const EVENT_NODE_COLLAPSED = 'bs-tree:nodeCollapsed'; +const EVENT_NODE_DISABLED = 'bs-tree:nodeDisabled'; +const EVENT_NODE_ENABLED = 'bs-tree:nodeEnabled'; +const EVENT_NODE_EXPANDED = 'bs-tree:nodeExpanded'; +const EVENT_NODE_SELECTED = 'bs-tree:nodeSelected'; +const EVENT_NODE_UNCHECKED = 'bs-tree:nodeUnchecked'; +const EVENT_NODE_UNSELECTED = 'bs-tree:nodeUnselected'; + +const EVENT_SEARCH_COMPLETED = 'bs-tree:searchCompleted'; +const EVENT_SEARCH_CLEARED = 'bs-tree:searchCleared'; + +function templateElement(tagType: string, classes: string): HTMLElement { + const el = document.createElement(tagType); + if(classes.length > 0) { + el.classList.add(...classes.split(" ")); + } + return el; +} + +export default class BSTreeView +{ + _template = { + tree: templateElement('ul', "list-group"), + node: templateElement("li", "list-group-item"), + indent: templateElement("span", "indent"), + icon: { + node: templateElement("span", "icon node-icon"), + expand: templateElement("span", "icon expand-icon"), + check: templateElement("span", "icon check-icon"), + empty: templateElement("span", "icon") + }, + image: templateElement("span", "image"), + badge: templateElement("span", ""), + text: templateElement("span", "text"), + }; + + _css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + + + /** + * @param {HTMLElement} The HTMLElement this tree applies to + */ + element: HTMLElement; + + wrapper: HTMLElement|null; + + /** + * {string} + * @private + */ + _elementId: string; + + _styleId: string; + + _tree: BSTreeViewNode[]; + + _nodes: Map; + _orderedNodes: Map; + + _checkedNodes: BSTreeViewNode[]; + + /** + * @private + * {boolean} + */ + _initialized: boolean; + + _options: BSTreeViewOptions; + + constructor(element: HTMLElement, options: BSTreeViewOptions|object) + { + this.element = element; + this._elementId = element.id; + this._styleId = this._elementId + '-style'; + + this._init(options); + } + + _init (options: BSTreeViewOptions|object) + { + this._tree = []; + this._initialized = false; + + //If options is a BSTreeViewOptions object, we can use it directly + if(options instanceof BSTreeViewOptions) { + this._options = options; + } else { + //Otherwise we have to apply our options object on it + this._options = new BSTreeViewOptions(options); + } + + // Cache empty icon DOM template + this._template.icon.empty.classList.add(...this._options.emptyIcon.split(" ")); + + this._destroy(); + this._subscribeEvents(); + + this._triggerEvent('loading', null, new BSTreeViewEventOptions({silent: true})); + this._load(this._options) + .then((data) => { + // load done + return this._tree = data; + }) + .catch((error) => { + // load fail + this._triggerEvent('loadingFailed', error, new BSTreeViewEventOptions()); + }) + .then((treeData) => { + // initialize data + if(treeData) { + return this._setInitialStates(new BSTreeViewNode({nodes: treeData}), 0); + } + }) + .then(() => { + // render to DOM + this._render(); + }) + ; + } + + _load (options: BSTreeViewOptions): Promise { + if (options.data) { + return this._loadLocalData(options); + } else if (options.ajaxURL) { + return this._loadRemoteData(options); + } + throw new Error("No data source defined."); + } + + _loadRemoteData (options: BSTreeViewOptions): Promise { + return new Promise((resolve, reject) => { + fetch(options.ajaxURL, options.ajaxConfig).then((response) => { + resolve(response.json()); + }) + .catch((error) => reject(error)); + }); + } + + _loadLocalData (options: BSTreeViewOptions): Promise { + return new Promise((resolve, reject) => { + //if options.data is a string we need to JSON decode it first + if (typeof options.data === 'string') { + try { + resolve(JSON.parse(options.data)); + } catch (error) { + reject(error); + } + } else { + resolve(options.data); + } + }); + }; + + _remove () { + this._destroy(); + //$.removeData(this, pluginName); + const styleElement = document.getElementById(this._styleId); + styleElement.remove(); + }; + + _destroy () { + if (!this._initialized) return; + this._initialized = false; + + this._triggerEvent('destroyed', null, new BSTreeViewEventOptions()); + + // Switch off events + this._unsubscribeEvents(); + + // Tear down + this.wrapper.remove(); + this.wrapper = null; + }; + + _unsubscribeEvents () { + if (typeof (this._options.onLoading) === 'function') { + this.element.removeEventListener(EVENT_LOADING, this._options.onLoading); + } + + if (typeof (this._options.onLoadingFailed) === 'function') { + this.element.removeEventListener(EVENT_LOADING_FAILED, this._options.onLoadingFailed); + } + + if (typeof (this._options.onInitialized) === 'function') { + this.element.removeEventListener(EVENT_INITIALIZED, this._options.onInitialized); + } + + if (typeof (this._options.onNodeRendered) === 'function') { + this.element.removeEventListener(EVENT_NODE_RENDERED, this._options.onNodeRendered); + } + + if (typeof (this._options.onRendered) === 'function') { + this.element.removeEventListener(EVENT_RENDERED, this._options.onRendered); + } + + if (typeof (this._options.onDestroyed) === 'function') { + this.element.removeEventListener(EVENT_DESTROYED, this._options.onDestroyed); + } + + this.element.removeEventListener('click', this._clickHandler.bind(this)); + + if (typeof (this._options.onNodeChecked) === 'function') { + this.element.removeEventListener(EVENT_NODE_CHECKED, this._options.onNodeChecked); + } + + if (typeof (this._options.onNodeCollapsed) === 'function') { + this.element.removeEventListener(EVENT_NODE_COLLAPSED, this._options.onNodeCollapsed); + } + + if (typeof (this._options.onNodeDisabled) === 'function') { + this.element.removeEventListener(EVENT_NODE_DISABLED, this._options.onNodeDisabled); + } + + if (typeof (this._options.onNodeEnabled) === 'function') { + this.element.removeEventListener(EVENT_NODE_ENABLED, this._options.onNodeEnabled); + } + + if (typeof (this._options.onNodeExpanded) === 'function') { + this.element.removeEventListener(EVENT_NODE_EXPANDED, this._options.onNodeExpanded); + } + + if (typeof (this._options.onNodeSelected) === 'function') { + this.element.removeEventListener(EVENT_NODE_SELECTED, this._options.onNodeSelected); + } + + if (typeof (this._options.onNodeUnchecked) === 'function') { + this.element.removeEventListener(EVENT_NODE_UNCHECKED, this._options.onNodeUnchecked); + } + + if (typeof (this._options.onNodeUnselected) === 'function') { + this.element.removeEventListener(EVENT_NODE_UNSELECTED, this._options.onNodeUnselected); + } + + if (typeof (this._options.onSearchComplete) === 'function') { + this.element.removeEventListener(EVENT_SEARCH_COMPLETED, this._options.onSearchComplete); + } + + if (typeof (this._options.onSearchCleared) === 'function') { + this.element.removeEventListener(EVENT_SEARCH_CLEARED, this._options.onSearchCleared); + } + }; + + _subscribeEvents () { + this._unsubscribeEvents(); + + if (typeof (this._options.onLoading) === 'function') { + this.element.addEventListener(EVENT_LOADING, this._options.onLoading); + } + + if (typeof (this._options.onLoadingFailed) === 'function') { + this.element.addEventListener(EVENT_LOADING_FAILED, this._options.onLoadingFailed); + } + + if (typeof (this._options.onInitialized) === 'function') { + this.element.addEventListener(EVENT_INITIALIZED, this._options.onInitialized); + } + + if (typeof (this._options.onNodeRendered) === 'function') { + this.element.addEventListener(EVENT_NODE_RENDERED, this._options.onNodeRendered); + } + + if (typeof (this._options.onRendered) === 'function') { + this.element.addEventListener(EVENT_RENDERED, this._options.onRendered); + } + + if (typeof (this._options.onDestroyed) === 'function') { + this.element.addEventListener(EVENT_DESTROYED, this._options.onDestroyed); + } + + this.element.addEventListener('click', this._clickHandler.bind(this)); + + if (typeof (this._options.onNodeChecked) === 'function') { + this.element.addEventListener(EVENT_NODE_CHECKED, this._options.onNodeChecked); + } + + if (typeof (this._options.onNodeCollapsed) === 'function') { + this.element.addEventListener(EVENT_NODE_COLLAPSED, this._options.onNodeCollapsed); + } + + if (typeof (this._options.onNodeDisabled) === 'function') { + this.element.addEventListener(EVENT_NODE_DISABLED, this._options.onNodeDisabled); + } + + if (typeof (this._options.onNodeEnabled) === 'function') { + this.element.addEventListener(EVENT_NODE_ENABLED, this._options.onNodeEnabled); + } + + if (typeof (this._options.onNodeExpanded) === 'function') { + this.element.addEventListener(EVENT_NODE_EXPANDED, this._options.onNodeExpanded); + } + + if (typeof (this._options.onNodeSelected) === 'function') { + this.element.addEventListener(EVENT_NODE_SELECTED, this._options.onNodeSelected); + } + + if (typeof (this._options.onNodeUnchecked) === 'function') { + this.element.addEventListener(EVENT_NODE_UNCHECKED, this._options.onNodeUnchecked); + } + + if (typeof (this._options.onNodeUnselected) === 'function') { + this.element.addEventListener(EVENT_NODE_UNSELECTED, this._options.onNodeUnselected); + } + + if (typeof (this._options.onSearchComplete) === 'function') { + this.element.addEventListener(EVENT_SEARCH_COMPLETED, this._options.onSearchComplete); + } + + if (typeof (this._options.onSearchCleared) === 'function') { + this.element.addEventListener(EVENT_SEARCH_CLEARED, this._options.onSearchCleared); + } + }; + + _triggerEvent (eventType: string, data: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = null) { + if (options && !options.silent) { + const event = new CustomEvent(eventType, { detail: {data: data, eventOptions: options, treeView: this} }); + + this.element.dispatchEvent(event); + } + } + + /* + Recurse the tree structure and ensure all nodes have + valid initial states. User defined states will be preserved. + For performance we also take this opportunity to + index nodes in a flattened ordered structure + */ + _setInitialStates (node: BSTreeViewNode, level: number): Promise { + this._nodes = new Map(); + + const promise = new Promise((resolve) => { + this._setInitialState(node, level) + resolve(false); + }); + + promise.then(() => { + this._orderedNodes = this._sortNodes(this._nodes); + this._inheritCheckboxChanges(); + this._triggerEvent('initialized', Array.from(this._orderedNodes.values())); + }); + + return promise; + }; + + _setInitialState (node: BSTreeViewNode, level: number): void { + if (!node.nodes) return; + level += 1; + + let parent = node; + node.nodes.forEach((node, index) => { + // level : hierarchical tree level, starts at 1 + node.level = level; + + // index : relative to siblings + node.index = index; + + // nodeId : unique, hierarchical identifier + node.nodeId = (parent && parent.nodeId) ? + parent.nodeId + '.' + node.index : + (level - 1) + '.' + node.index; + + // parentId : transversing up the tree + node.parentId = parent.nodeId; + + // if not provided set selectable default value + if (!node.hasOwnProperty('selectable')) { + node.selectable = true; + } + + // if not provided set checkable default value + if (!node.hasOwnProperty('checkable')) { + node.checkable = true; + } + + // where provided we should preserve states + node.state = node.state || new BSTreeViewNodeState(); + + // set checked state; unless set always false + if (!node.state.hasOwnProperty('checked')) { + node.state.checked = false; + } + + // convert the undefined string if hierarchical checks are enabled + if (this._options.hierarchicalCheck && node.state.checked === null) { + node.state.checked = null; + } + + // set enabled state; unless set always false + if (!node.state.hasOwnProperty('disabled')) { + node.state.disabled = false; + } + + // set expanded state; if not provided based on levels + if (!node.state.hasOwnProperty('expanded')) { + if (!node.state.disabled && + (level < this._options.levels) && + (node.nodes && node.nodes.length > 0)) { + node.state.expanded = true; + } + else { + node.state.expanded = false; + } + } + + // set selected state; unless set always false + if (!node.state.hasOwnProperty('selected')) { + node.state.selected = false; + } + + // set visible state; based parent state plus levels + if ((parent && parent.state && parent.state.expanded) || + (level <= this._options.levels)) { + node.state.visible = true; + } + else { + node.state.visible = false; + } + + // recurse child nodes and transverse the tree, depth-first + if (node.nodes) { + if (node.nodes.length > 0) { + this._setInitialState(node, level); + } + else { + delete node.nodes; + } + } + + // add / update indexed collection + this._nodes[node.nodeId] = node; + }) + }; + + _sortNodes (nodes: Map): Map { + return new Map([...nodes].sort((pairA, pairB) => { + //Index 0 of our pair variables contains the index of our map + + if (pairA[0] === pairB[0]) return 0; + const a = pairA[0].split('.').map(function (level) { return parseInt(level); }); + const b = pairB[0].split('.').map(function (level) { return parseInt(level); }); + + const c = Math.max(a.length, b.length); + for (let i=0; i 0) return +1; + if (a[i] - b[i] < 0) return -1; + } + })); + }; + + _clickHandler (event: Event) { + const target = event.target as HTMLElement; + const node = this.targetNode(target); + if (!node || node.state.disabled) return; + + const classList = target.classList; + if (classList.contains('expand-icon')) { + this._toggleExpanded(node); + } + else if (classList.contains('check-icon')) { + if (node.checkable) { + this._toggleChecked(node); + } + } + else { + if (node.selectable) { + this._toggleSelected(node); + } else { + this._toggleExpanded(node); + } + } + }; + + /* Looks up the DOM for the closest parent list item to retrieve the + * data attribute nodeid, which is used to lookup the node in the flattened structure. */ + targetNode (target: HTMLElement): BSTreeViewNode { + const nodeElement = target.closest('li.list-group-item') as HTMLElement; + const nodeId = nodeElement.dataset.nodeId; + const node = this._nodes[nodeId]; + if (!node) { + console.warn('Error: node does not exist'); + } + return node; + }; + + _toggleExpanded (node: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { + if (!node) return; + + // Lazy-load the child nodes if possible + if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) { + this._lazyLoad(node); + } else { + this._setExpanded(node, !node.state.expanded, options); + } + }; + + _lazyLoad (node: BSTreeViewNode) { + if(!node.lazyLoad) return; + + // Show a different icon while loading the child nodes + const span = node.el.querySelector('span.expand-icon'); + span.classList.remove(...this._options.expandIcon.split(' ')); + span.classList.add(...this._options.loadingIcon.split(' ')); + + this._options.lazyLoad(node, (nodes) => { + // Adding the node will expand its parent automatically + this.addNode(nodes, node); + }); + // Only the first expand should do a lazy-load + node.lazyLoad = false; + }; + + _setExpanded (node: BSTreeViewNode, state: boolean, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { + + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.expanded) return; + + if (state && node.nodes) { + + // Set node state + node.state.expanded = true; + + // Set element + if (node.el) { + const span = node.el.querySelector('span.expand-icon'); + span.classList.remove(...this._options.expandIcon.split(" ")) + span.classList.remove(...this._options.loadingIcon.split(" ")) + span.classList.add(...this._options.collapseIcon.split(" ")); + } + + // Expand children + if (node.nodes && options) { + node.nodes.forEach((node) => { + this._setVisible(node, true, options); + }); + } + + // Optionally trigger event + this._triggerEvent('nodeExpanded', node, options); + } + else if (!state) { + + // Set node state + node.state.expanded = false; + + // Set element + if (node.el) { + const span = node.el.querySelector('span.expand-icon'); + span.classList.remove(...this._options.collapseIcon.split(" ")); + span.classList.add(...this._options.expandIcon.split(" ")); + } + + // Collapse children + if (node.nodes && options) { + node.nodes.forEach ((node) => { + this._setVisible(node, false, options); + this._setExpanded(node, false, options); + }); + } + + // Optionally trigger event + this._triggerEvent('nodeCollapsed', node, options); + } + }; + + _setVisible (node: BSTreeViewNode, state: boolean, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { + + if (options && state === node.state.visible) return; + + if (state) { + + // Set node state + node.state.visible = true; + + // Set element + if (node.el) { + node.el.classList.remove('node-hidden'); + } + } + else { + + // Set node state to unchecked + node.state.visible = false; + + // Set element + if (node.el) { + node.el.classList.add('node-hidden'); + } + } + }; + + _toggleSelected (node: BSTreeViewNode, options: BSTreeViewSelectOptions = new BSTreeViewSelectOptions()): this { + if (!node) return; + this._setSelected(node, !node.state.selected, options); + return this; + }; + + _setSelected (node: BSTreeViewNode, state: boolean, options = new BSTreeViewSelectOptions()): this { + + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && (state === node.state.selected)) return; + + if (state) { + + // If multiSelect false, unselect previously selected + if (!this._options.multiSelect) { + const selectedNodes = this._findNodes('true', 'state.selected'); + selectedNodes.forEach((node) => { + options.unselecting = true; + + this._setSelected(node, false, options); + }); + } + + // Set node state + node.state.selected = true; + + // Set element + if (node.el) { + node.el.classList.add('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + const span = node.el.querySelector('span.node-icon'); + span.classList.remove(...(node.icon || this._options.nodeIcon).split(" ")); + span.classList.add(...(node.selectedIcon || this._options.selectedIcon).split(" ")); + } + } + + // Optionally trigger event + this._triggerEvent('nodeSelected', node, options); + } + else { + + // If preventUnselect true + only one remaining selection, disable unselect + if (this._options.preventUnselect && + (options && !options.unselecting) && + (this._findNodes('true', 'state.selected').length === 1)) { + // Fire the nodeSelected event if reselection is allowed + if (this._options.allowReselect) { + this._triggerEvent('nodeSelected', node, options); + } + return this; + } + + // Set node state + node.state.selected = false; + + // Set element + if (node.el) { + node.el.classList.remove('node-selected'); + + if (node.selectedIcon || this._options.selectedIcon) { + const span = node.el.querySelector('span.node-icon'); + span.classList.remove(...(node.selectedIcon || this._options.selectedIcon).split(" ")) + span.classList.add(...(node.icon || this._options.nodeIcon).split(" ")); + } + } + + // Optionally trigger event + this._triggerEvent('nodeUnselected', node, options); + } + + return this; + }; + + _inheritCheckboxChanges (): void { + if (this._options.showCheckbox && this._options.highlightChanges) { + this._checkedNodes = []; + this._orderedNodes.forEach((node) => { + if(node.state.checked) { + this._checkedNodes.push(node); + } + }); + } + }; + + _toggleChecked (node: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): this { + if (!node) return; + + if (this._options.hierarchicalCheck) { + // Event propagation to the parent/child nodes + const childOptions = new BSTreeViewEventOptions(options); + childOptions.silent = options.silent || !this._options.propagateCheckEvent; + + let state: boolean|null; + let currentNode = node; + // Temporarily swap the tree state + node.state.checked = !node.state.checked; + + currentNode = this._nodes[currentNode.parentId] + // Iterate through each parent node + while (currentNode) { + + // Calculate the state + state = currentNode.nodes.reduce((acc, curr) => { + return (acc === curr.state.checked) ? acc : null; + }, currentNode.nodes[0].state.checked); + + // Set the state + this._setChecked(currentNode, state, childOptions); + + currentNode = this._nodes[currentNode.parentId] + } + + if (node.nodes && node.nodes.length > 0) { + // Copy the content of the array + let child, children = node.nodes.slice(); + // Iterate through each child node + while (children && children.length > 0) { + child = children.pop(); + + // Set the state + this._setChecked(child, node.state.checked, childOptions); + + // Append children to the end of the list + if (child.nodes && child.nodes.length > 0) { + children = children.concat(child.nodes.slice()); + } + } + } + // Swap back the tree state + node.state.checked = !node.state.checked; + } + + this._setChecked(node, !node.state.checked, options); + }; + + _setChecked (node: BSTreeViewNode, state: boolean, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.checked) return; + + // Highlight the node if its checkbox has unsaved changes + if (this._options.highlightChanges) { + const nodeNotInCheckList = this._checkedNodes.indexOf(node) == -1; + if(nodeNotInCheckList == state) { + node.el.classList.add('node-check-changed'); + } else { + node.el.classList.remove('node-check-changed'); + } + } + + if (state) { + + // Set node state + node.state.checked = true; + + // Set element + if (node.el) { + node.el.classList.add('node-checked'); + node.el.classList.remove('node-checked-partial'); + const span = node.el.querySelector('span.check-icon'); + span.classList.remove(...this._options.uncheckedIcon.split(" ")) + span.classList.remove(...this._options.partiallyCheckedIcon.split(" ")) + span.classList.add(...this._options.checkedIcon.split(" ")); + } + + // Optionally trigger event + this._triggerEvent('nodeChecked', node, options); + } + else if (state === null && this._options.hierarchicalCheck) { + + // Set node state to partially checked + node.state.checked = null; + + // Set element + if (node.el) { + node.el.classList.add('node-checked-partial'); + node.el.classList.remove('node-checked'); + const span = node.el.querySelector('span.check-icon'); + span.classList.remove(...this._options.uncheckedIcon.split(" ")); + span.classList.remove(...this._options.checkedIcon.split(" ")); + span.classList.add(...this._options.partiallyCheckedIcon.split(" ")); + } + + // Optionally trigger event, partially checked is technically unchecked + this._triggerEvent('nodeUnchecked', node, options); + } else { + + // Set node state to unchecked + node.state.checked = false; + + // Set element + if (node.el) { + node.el.classList.remove('node-checked node-checked-partial'); + const span = node.el.querySelector('span.check-icon'); + span.classList.remove(...this._options.checkedIcon.split(" ")); + span.classList.remove(...this._options.partiallyCheckedIcon.split(" ")); + span.classList.add(...this._options.uncheckedIcon.split(" ")); + } + + // Optionally trigger event + this._triggerEvent('nodeUnchecked', node, options); + } + }; + + _setDisabled (node: BSTreeViewNode, state: boolean, options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()) { + + // We never pass options when rendering, so the only time + // we need to validate state is from user interaction + if (options && state === node.state.disabled) return; + + if (state) { + + // Set node state to disabled + node.state.disabled = true; + + // Disable all other states + if (options && !options.keepState) { + this._setSelected(node, false, options); + this._setChecked(node, false, options); + this._setExpanded(node, false, options); + } + + // Set element + if (node.el) { + node.el.classList.add('node-disabled'); + } + + // Optionally trigger event + this._triggerEvent('nodeDisabled', node, options); + } + else { + + // Set node state to enabled + node.state.disabled = false; + + // Set element + if (node.el) { + node.el.classList.remove('node-disabled'); + } + + // Optionally trigger event + this._triggerEvent('nodeEnabled', node, options); + } + }; + + _setSearchResult (node: BSTreeViewNode, state: boolean, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { + if (options && state === node.searchResult) return; + + if (state) { + + node.searchResult = true; + + if (node.el) { + node.el.classList.add('node-result'); + } + } + else { + + node.searchResult = false; + + if (node.el) { + node.el.classList.remove('node-result'); + } + } + }; + + _render(): void { + if (!this._initialized) { + + // Setup first time only components + this.wrapper = this._template.tree.cloneNode(true) as HTMLElement; + //Empty this element + while(this.element.firstChild) { + this.element.removeChild(this.element.firstChild); + } + + + this.element.classList.add(...pluginName.split(" ")) + this.element.appendChild(this.wrapper); + + this._injectStyle(); + + this._initialized = true; + } + + let previousNode: BSTreeViewNode|null = null; + this._orderedNodes.forEach((node) => { + this._renderNode(node, previousNode); + previousNode = node; + }); + + this._triggerEvent('rendered', Array.from(this._orderedNodes.values()), new BSTreeViewEventOptions()); + }; + + _renderNode(node: BSTreeViewNode, previousNode: BSTreeViewNode|null): void { + if (!node) return; + + if (!node.el) { + node.el = this._newNodeEl(node, previousNode); + node.el.classList.add('node-' + this._elementId); + } + else { + node.el.innerHTML = ""; + } + + // Append .classes to the node + node.el.classList.add(...node.class.split(" ")); + + // Set the #id of the node if specified + if (node.id) { + node.el.id = node.id; + } + + // Append custom data- attributes to the node + if (node.dataAttr) { + for (const key in node.dataAttr) { + if (node.dataAttr.hasOwnProperty(key)) { + node.el.setAttribute('data-' + key, node.dataAttr[key]); + } + } + } + + // Set / update nodeid; it can change as a result of addNode etc. + node.el.dataset.nodeId = node.nodeId; + + // Set the tooltip attribute if present + if (node.tooltip) { + node.el.title = node.tooltip; + } + + // Add indent/spacer to mimic tree structure + for (let i = 0; i < (node.level - 1); i++) { + node.el.append(this._template.indent.cloneNode(true) as HTMLElement); + } + + // Add expand / collapse or empty spacer icons + node.el + .append( + node.nodes || node.lazyLoad ? this._template.icon.expand.cloneNode(true) as HTMLElement : this._template.icon.empty.cloneNode(true) as HTMLElement + ); + + // Add checkbox and node icons + if (this._options.checkboxFirst) { + this._addCheckbox(node); + this._addIcon(node); + this._addImage(node); + } else { + this._addIcon(node); + this._addImage(node); + this._addCheckbox(node); + } + + // Add text + if (this._options.wrapNodeText) { + const wrapper = this._template.text.cloneNode(true) as HTMLElement; + node.el.append(wrapper); + wrapper.append(node.text); + } else { + node.el.append(node.text); + } + + // Add tags as badges + if (this._options.showTags && node.tags) { + node.tags.forEach(tag => { + const template = this._template.badge.cloneNode(true) as HTMLElement; + template.classList.add( + //@ts-ignore + ...((typeof tag === 'object' ? tag.class : undefined) + || node.tagsClass + || this._options.tagsClass).split(" ") + ); + template.append( + //@ts-ignore + (typeof tag === 'object' ? tag.text : undefined) + || tag + ); + + node.el.append(template); + }); + } + + // Set various node states + this._setSelected(node, node.state.selected); + this._setChecked(node, node.state.checked); + this._setSearchResult(node, node.searchResult); + this._setExpanded(node, node.state.expanded); + this._setDisabled(node, node.state.disabled); + this._setVisible(node, node.state.visible); + + // Trigger nodeRendered event + this._triggerEvent('nodeRendered', node, new BSTreeViewEventOptions()); + }; + +// Add checkable icon + _addCheckbox (node: BSTreeViewNode): void { + if (this._options.showCheckbox && (node.hideCheckbox === undefined || node.hideCheckbox === false)) { + node.el + .append(this._template.icon.check.cloneNode(true) as HTMLElement); + } + } + +// Add node icon + _addIcon (node: BSTreeViewNode): void { + if (this._options.showIcon && !(this._options.showImage && node.image)) { + const template = this._template.icon.node.cloneNode(true) as HTMLElement; + template.classList.add(...(node.icon || this._options.nodeIcon).split(" ")) + + node.el.append(template); + } + } + + _addImage (node: BSTreeViewNode): void { + if (this._options.showImage && node.image) { + const template = this._template.image.cloneNode(true) as HTMLElement; + template.classList.add('node-image'); + template.style.backgroundImage = "url('" + node.image + "')"; + + + node.el + .append( + + ); + } + } + +// Creates a new node element from template and +// ensures the template is inserted at the correct position + _newNodeEl (node: BSTreeViewNode, previousNode: BSTreeViewNode|null): HTMLElement { + let template = this._template.node.cloneNode(true) as HTMLElement; + + if (previousNode) { + // typical usage, as nodes are rendered in + // sort order we add after the previous element + previousNode.el.after(template); + } else { + // we use prepend instead of append, + // to cater for root inserts i.e. nodeId 0.0 + this.wrapper.prepend(template); + } + + return template; + }; + +// Recursively remove node elements from DOM + _removeNodeEl (node: BSTreeViewNode): void { + if (!node) return; + + if (node.nodes) { + node.nodes.forEach((node) => { + this._removeNodeEl(node); + }); + } + node.el.remove(); + }; + +// Expand node, rendering it's immediate children + _expandNode (node: BSTreeViewNode): void { + if (!node.nodes) return; + + node.nodes.slice(0).reverse().forEach((childNode) => { + childNode.level = node.level + 1; + this._renderNode(childNode, node); + }); + }; + +// Add inline style into head + _injectStyle (): void { + if (this._options.injectStyle && !document.getElementById(this._styleId)) { + const styleElement = document.createElement('style'); + styleElement.id = this._styleId; + styleElement.type='text/css'; + styleElement.innerHTML = this._buildStyle(); + document.head.appendChild(styleElement); + } + }; + +// Construct trees style based on user options + _buildStyle () { + let style = '.node-' + this._elementId + '{'; + + // Basic bootstrap style overrides + if (this._options.color) { + style += 'color:' + this._options.color + ';'; + } + + if (this._options.backColor) { + style += 'background-color:' + this._options.backColor + ';'; + } + + if (!this._options.showBorder) { + style += 'border:none;'; + } + else if (this._options.borderColor) { + style += 'border:1px solid ' + this._options.borderColor + ';'; + } + style += '}'; + + if (this._options.onhoverColor) { + style += '.node-' + this._elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this._options.onhoverColor + ';' + + '}'; + } + + // Style search results + if (this._options.highlightSearchResults && (this._options.searchResultColor || this._options.searchResultBackColor)) { + + let innerStyle = '' + if (this._options.searchResultColor) { + innerStyle += 'color:' + this._options.searchResultColor + ';'; + } + if (this._options.searchResultBackColor) { + innerStyle += 'background-color:' + this._options.searchResultBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-result{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-result:hover{' + innerStyle + '}'; + } + + // Style selected nodes + if (this._options.highlightSelected && (this._options.selectedColor || this._options.selectedBackColor)) { + + let innerStyle = '' + if (this._options.selectedColor) { + innerStyle += 'color:' + this._options.selectedColor + ';'; + } + if (this._options.selectedBackColor) { + innerStyle += 'background-color:' + this._options.selectedBackColor + ';'; + } + + style += '.node-' + this._elementId + '.node-selected{' + innerStyle + '}'; + style += '.node-' + this._elementId + '.node-selected:hover{' + innerStyle + '}'; + } + + // Style changed nodes + if (this._options.highlightChanges) { + let innerStyle = 'color: ' + this._options.changedNodeColor + ';'; + style += '.node-' + this._elementId + '.node-check-changed{' + innerStyle + '}'; + } + + // Node level style overrides + this._orderedNodes.forEach((node) => { + if (node.color || node.backColor) { + let innerStyle = ''; + if (node.color) { + innerStyle += 'color:' + node.color + ';'; + } + if (node.backColor) { + innerStyle += 'background-color:' + node.backColor + ';'; + } + style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"]{' + innerStyle + '}'; + } + + if (node.iconColor) { + let innerStyle = 'color:' + node.iconColor + ';'; + style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"] .node-icon{' + innerStyle + '}'; + } + }); + + return this._css + style; + }; + + /** + Returns an array of matching node objects. + @param {String} pattern - A pattern to match against a given field + @return {String} field - Field to query pattern against + */ + findNodes (pattern: string, field: string): BSTreeViewNode[] { + return this._findNodes(pattern, field); + }; + + + /** + Returns an ordered aarray of node objects. + @return {Array} nodes - An array of all nodes + */ + getNodes (): Map { + return this._orderedNodes; + }; + + /** + Returns parent nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of parent nodes + */ + getParents (nodes: BSTreeViewNode[]|BSTreeViewNode): BSTreeViewNode[] { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + let parentNodes = []; + nodes.forEach((node) => { + const parentNode = node.parentId ? this._nodes[node.parentId] : false; + if (parentNode) { + parentNodes.push(parentNode); + } + }); + return parentNodes; + }; + + /** + Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined. + @param {Array} nodes - An array of nodes + @returns {Array} nodes - An array of sibling nodes + */ + getSiblings (nodes: BSTreeViewNode[]|BSTreeViewNode): BSTreeViewNode[] { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + let siblingNodes = []; + nodes.forEach((node) => { + let parent = this.getParents([node]); + let nodes = parent[0] ? parent[0].nodes : this._tree; + siblingNodes = nodes.filter(function (obj) { + return obj.nodeId !== node.nodeId; + }); + }); + + // flatten possible nested array before returning + return siblingNodes.map((obj) => { + return obj; + }); + }; + + /** + Returns an array of selected nodes. + @returns {Array} nodes - Selected nodes + */ + getSelected (): BSTreeViewNode[] { + return this._findNodes('true', 'state.selected'); + }; + + /** + Returns an array of unselected nodes. + @returns {Array} nodes - Unselected nodes + */ + getUnselected (): BSTreeViewNode[] { + return this._findNodes('false', 'state.selected'); + }; + + /** + Returns an array of expanded nodes. + @returns {Array} nodes - Expanded nodes + */ + getExpanded (): BSTreeViewNode[] { + return this._findNodes('true', 'state.expanded'); + }; + + /** + Returns an array of collapsed nodes. + @returns {Array} nodes - Collapsed nodes + */ + getCollapsed (): BSTreeViewNode[] { + return this._findNodes('false', 'state.expanded'); + }; + + /** + Returns an array of checked nodes. + @returns {Array} nodes - Checked nodes + */ + getChecked (): BSTreeViewNode[] { + return this._findNodes('true', 'state.checked'); + }; + + /** + Returns an array of unchecked nodes. + @returns {Array} nodes - Unchecked nodes + */ + getUnchecked (): BSTreeViewNode[] { + return this._findNodes('false', 'state.checked'); + }; + + /** + Returns an array of disabled nodes. + @returns {Array} nodes - Disabled nodes + */ + getDisabled (): BSTreeViewNode[] { + return this._findNodes('true', 'state.disabled'); + }; + + /** + Returns an array of enabled nodes. + @returns {Array} nodes - Enabled nodes + */ + getEnabled (): BSTreeViewNode[] { + return this._findNodes('false', 'state.disabled'); + }; + + + /** + Add nodes to the tree. + @param {Array} nodes - An array of nodes to add + @param {optional Object} parentNode - The node to which nodes will be added as children + @param {optional number} index - Zero based insert index + @param {optional Object} options + */ + addNode (nodes: BSTreeViewNode[]|BSTreeViewNode, parentNode: BSTreeViewNode|null = null, index: number = null, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (parentNode instanceof Array) { + parentNode = parentNode[0]; + } + + // identify target nodes; either the tree's root or a parent's child nodes + let targetNodes; + if (parentNode && parentNode.nodes) { + targetNodes = parentNode.nodes; + } else if (parentNode) { + targetNodes = parentNode.nodes = []; + } else { + targetNodes = this._tree; + } + + // inserting nodes at specified positions + nodes.forEach((node, i) => { + let insertIndex = (typeof(index) === 'number') ? (index + i) : (targetNodes.length + 1); + targetNodes.splice(insertIndex, 0, node); + }); + + // initialize new state and render changes + this._setInitialStates(new BSTreeViewNode({nodes: this._tree}), 0) + .then(() =>{ + if (parentNode && !parentNode.state.expanded) { + this._setExpanded(parentNode, true, options); + } + this._render(); + }); + } + + /** + Add nodes to the tree after given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added after + @param {optional Object} options + */ + addNodeAfter (nodes: BSTreeViewNode[]|BSTreeViewNode, node: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + this.addNode(nodes, this.getParents(node)[0], (node.index + 1), options); + } + + /** + Add nodes to the tree before given node. + @param {Array} nodes - An array of nodes to add + @param {Object} node - The node to which nodes will be added before + @param {optional Object} options + */ + addNodeBefore (nodes: BSTreeViewNode[]|BSTreeViewNode, node: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + if (node instanceof Array) { + node = node[0]; + } + + this.addNode(nodes, this.getParents(node)[0], node.index, options); + } + + /** + Removes given nodes from the tree. + @param {Array} nodes - An array of nodes to remove + @param {optional Object} options + */ + removeNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + let targetNodes: BSTreeViewNode[]; + let parentNode: BSTreeViewNode; + nodes.forEach((node) => { + + // remove nodes from tree + parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1); + + // remove node from DOM + this._removeNodeEl(node); + }); + + // initialize new state and render changes + this._setInitialStates(new BSTreeViewNode({nodes: this._tree}), 0) + .then(this._render.bind(this)); + }; + + /** + Updates / replaces a given tree node + @param {Object} node - A single node to be replaced + @param {Object} newNode - THe replacement node + @param {optional Object} options + */ + updateNode (node: BSTreeViewNode, newNode: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { + if (node instanceof Array) { + node = node[0]; + } + + // insert new node + let targetNodes; + const parentNode = this._nodes[node.parentId]; + if (parentNode) { + targetNodes = parentNode.nodes; + } else { + targetNodes = this._tree; + } + targetNodes.splice(node.index, 1, newNode); + + // remove old node from DOM + this._removeNodeEl(node); + + // initialize new state and render changes + this._setInitialStates(new BSTreeViewNode({nodes: this._tree}), 0) + .then(this._render.bind(this)); + }; + + + /** + Selects given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + selectNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewSelectOptions = new BSTreeViewSelectOptions()) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._setSelected(node, true, options); + }); + }; + + /** + Unselects given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + unselectNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewSelectOptions) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + + nodes.forEach((node) => { + this._setSelected(node, false, options); + }); + }; + + /** + Toggles a node selected state; selecting if unselected, unselecting if selected. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + toggleNodeSelected (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewSelectOptions = new BSTreeViewSelectOptions()) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._toggleSelected(node, options); + }, this); + }; + + + /** + Collapse all tree nodes + @param {optional Object} options + */ + collapseAll (options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()) { + options.levels = options.levels || 999; + this.collapseNode(this._tree, options); + }; + + /** + Collapse a given tree node + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + collapseNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._setExpanded(node, false, options); + }); + }; + + /** + Expand all tree nodes + @param {optional Object} options + */ + expandAll (options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { + options.levels = options.levels || 999; + this.expandNode(this._tree, options); + }; + + /** + Expand given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + expandNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + // Do not re-expand already expanded nodes + if (node.state.expanded) return; + + if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) { + this._lazyLoad(node); + } + + this._setExpanded(node, true, options); + if (node.nodes) { + this._expandLevels(node.nodes, options.levels-1, options); + } + }); + }; + + _expandLevels (nodes: BSTreeViewNode[]|BSTreeViewNode, level: number, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._setExpanded(node, (level > 0), options); + if (node.nodes) { + this._expandLevels(node.nodes, level-1, options); + } + }); + }; + + /** + Reveals given tree nodes, expanding the tree from node to root. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + revealNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + let parentNode = node; + let tmpNode; + while (tmpNode = this.getParents([parentNode])[0]) { + parentNode = tmpNode; + this._setExpanded(parentNode, true, options); + } + }); + }; + + /** + Toggles a node's expanded state; collapsing if expanded, expanding if collapsed. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + toggleNodeExpanded (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._toggleExpanded(node, options); + }); + + }; + + + /** + Check all tree nodes + @param {optional Object} options + */ + checkAll (options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { + this._orderedNodes.forEach((node) => { + if(!node.state.checked) { + this._setChecked(node, true, options); + } + }); + }; + + /** + Checks given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + checkNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._setChecked(node, true, options); + }); + }; + + /** + Uncheck all tree nodes + @param {optional Object} options + */ + uncheckAll (options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { + this._orderedNodes.forEach((node) => { + if(node.state.checked || node.state.checked === undefined) { + this._setChecked(node, false, options); + } + }); + }; + + /** + Uncheck given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + uncheckNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._setChecked(node, false, options); + }); + }; + + /** + Toggles a node's checked state; checking if unchecked, unchecking if checked. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + toggleNodeChecked (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._toggleChecked(node, options); + }); + }; + + /** + Saves the current state of checkboxes as default, cleaning up any highlighted changes + */ + unmarkCheckboxChanges (): void { + this._inheritCheckboxChanges(); + + this._nodes.forEach((node) => { + node.el.classList.remove('node-check-changed'); + }); + }; + + /** + Disable all tree nodes + @param {optional Object} options + */ + disableAll (options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()): void { + const nodes = this._findNodes('false', 'state.disabled'); + nodes.forEach((node) => { + this._setDisabled(node,true, options); + }); + }; + + /** + Disable given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + disableNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._setDisabled(node, true, options); + }); + }; + + /** + Enable all tree nodes + @param {optional Object} options + */ + enableAll (options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()): void { + const nodes = this._findNodes('true', 'state.disabled'); + + nodes.forEach((node) => { + this._setDisabled(node, false, options); + }); + }; + + /** + Enable given tree nodes + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + enableNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()) { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._setDisabled(node, false, options); + }) + }; + + /** + Toggles a node's disabled state; disabling is enabled, enabling if disabled. + @param {Array} nodes - An array of nodes + @param {optional Object} options + */ + toggleNodeDisabled (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()): void { + if (!(nodes instanceof Array)) { + nodes = [nodes]; + } + + nodes.forEach((node) => { + this._setDisabled(node, !node.state.disabled, options); + }) + }; + + + /** + Searches the tree for nodes (text) that match given criteria + @param {String} pattern - A given string to match against + @param {optional Object} options - Search criteria options + @return {Array} nodes - Matching nodes + */ + search (pattern: string, options: BSTreeSearchOptions = new BSTreeSearchOptions()): BSTreeViewNode[] { + let previous = this._getSearchResults(); + let results = []; + + if (pattern && pattern.length > 0) { + + if (options.exactMatch) { + pattern = '^' + pattern + '$'; + } + + let modifier = 'g'; + if (options.ignoreCase) { + modifier += 'i'; + } + + results = this._findNodes(pattern, 'text', modifier); + } + + // Clear previous results no longer matched + this._diffArray(results, previous).forEach((node) => { + this._setSearchResult(node, false, options); + }); + + // Set new results + this._diffArray(previous, results).forEach((node) => { + this._setSearchResult(node, true, options); + }); + + // Reveal hidden nodes + if (results && options.revealResults) { + this.revealNode(results); + } + + this._triggerEvent(EVENT_SEARCH_COMPLETED, results, options); + + return results; + }; + + /** + Clears previous search results + */ + clearSearch (options: BSTreeSearchOptions = new BSTreeSearchOptions()): void { + const results = this._getSearchResults(); + results.forEach((node) => { + this._setSearchResult(node, false, options); + }); + + this._triggerEvent(EVENT_SEARCH_CLEARED, results, options); + }; + + _getSearchResults (): BSTreeViewNode[] { + return this._findNodes('true', 'searchResult'); + }; + + _diffArray (a: Array, b: Array) { + let diff: Array = []; + + b.forEach((n) => { + if (a.indexOf(n) === -1) { + diff.push(n); + } + }); + return diff; + }; + + /** + Find nodes that match a given criteria + @param {String} pattern - A given string to match against + @param {optional String} attribute - Attribute to compare pattern against + @param {optional String} modifier - Valid RegEx modifiers + @return {Array} nodes - Nodes that match your criteria + */ + _findNodes (pattern: string, attribute: string = 'text', modifier: string = 'g'): BSTreeViewNode[] { + + let tmp = []; + + this._orderedNodes.forEach((node) => { + const val = this._getNodeValue(node, attribute); + if (typeof val === 'string') { + if(val.match(new RegExp(pattern, modifier))) { + tmp.push(node); + } + } + }); + + return tmp; + }; + + /** + Recursive find for retrieving nested attributes values + All values are return as strings, unless invalid + @param {Object} obj - Typically a node, could be any object + @param {String} attr - Identifies an object property using dot notation + @return {String} value - Matching attributes string representation + */ + _getNodeValue (obj: object, attr: string): string { + const index = attr.indexOf('.'); + if (index > 0) { + const _obj = obj[attr.substring(0, index)]; + const _attr = attr.substring(index + 1, attr.length); + return this._getNodeValue(_obj, _attr); + } + else { + if (obj.hasOwnProperty(attr) && obj[attr] !== undefined) { + return obj[attr].toString(); + } + else { + return undefined; + } + } + }; + +} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewDisableOptions.ts b/assets/ts_src/BSTreeViewDisableOptions.ts new file mode 100644 index 00000000..f74773cb --- /dev/null +++ b/assets/ts_src/BSTreeViewDisableOptions.ts @@ -0,0 +1,8 @@ +import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; + + +export default class BSTreeViewDisableOptions extends BSTreeViewEventOptions +{ + unselecting: boolean; + keepState: boolean; +} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewEventOptions.ts b/assets/ts_src/BSTreeViewEventOptions.ts new file mode 100644 index 00000000..3c268257 --- /dev/null +++ b/assets/ts_src/BSTreeViewEventOptions.ts @@ -0,0 +1,12 @@ +export default class BSTreeViewEventOptions { + silent: boolean = false; + ignoreChildren: boolean = false; + + lazyLoad: boolean = false; + + constructor(options: BSTreeViewEventOptions|object = null) { + if(options) { + Object.assign(this, options); + } + } +} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewExpandOptions.ts b/assets/ts_src/BSTreeViewExpandOptions.ts new file mode 100644 index 00000000..e1f8bb6e --- /dev/null +++ b/assets/ts_src/BSTreeViewExpandOptions.ts @@ -0,0 +1,7 @@ +import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; + + +export default class BSTreeViewExpandOptions extends BSTreeViewEventOptions +{ + levels: number = 999; +} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewNode.ts b/assets/ts_src/BSTreeViewNode.ts new file mode 100644 index 00000000..adfc5d53 --- /dev/null +++ b/assets/ts_src/BSTreeViewNode.ts @@ -0,0 +1,44 @@ +import BSTreeViewNodeState from "./BSTreeViewNodeState"; +import BSTreeViewOptions from "./BSTreeViewOptions"; + +export default class BSTreeViewNode { + text: string; + icon: string; + image: string; + selectedIcon: string; + color: string; + backColor: string; + iconColor: string; + iconBackground: string; + selectable: boolean; + checkable: boolean; + state: BSTreeViewNodeState; + tags: string[]; + dataAttr: object; + id: string; + class: string; + hideCheckbox: boolean; + nodes: BSTreeViewNode[]; + tooltip: string; + + lazyLoad: boolean; + tagsClass: string; + + + el: HTMLElement; + + searchResult: boolean; + + + level: number; + index: number; + nodeId: string; + parentId: string + + constructor(options: BSTreeViewNode|object = null) { + if(options) { + Object.assign(this, options); + } + } +} + diff --git a/assets/ts_src/BSTreeViewNodeState.ts b/assets/ts_src/BSTreeViewNodeState.ts new file mode 100644 index 00000000..216fc126 --- /dev/null +++ b/assets/ts_src/BSTreeViewNodeState.ts @@ -0,0 +1,8 @@ +export default class BSTreeViewNodeState { + checked: boolean|null; + disabled: boolean = false; + expanded: boolean; + selected: boolean; + + visible: boolean; +} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewOptions.ts b/assets/ts_src/BSTreeViewOptions.ts new file mode 100644 index 00000000..99d72151 --- /dev/null +++ b/assets/ts_src/BSTreeViewOptions.ts @@ -0,0 +1,78 @@ +import BSTreeViewNode from "./BSTreeViewNode"; + +export default class BSTreeViewOptions { + injectStyle: boolean = true; + + levels: number = 2; + + data: BSTreeViewNode[]|string = null; + ajaxURL: string = null; + ajaxConfig: RequestInit = { + method: "GET", + }; + + expandIcon: string = 'glyphicon glyphicon-plus'; + collapseIcon: string = 'glyphicon glyphicon-minus'; + loadingIcon: string = 'glyphicon glyphicon-hourglass'; + emptyIcon: string = 'glyphicon'; + nodeIcon: string = ''; + selectedIcon: string = ''; + checkedIcon: string = 'glyphicon glyphicon-check'; + partiallyCheckedIcon: string = 'glyphicon glyphicon-expand'; + uncheckedIcon: string = 'glyphicon glyphicon-unchecked'; + tagsClass: string = 'badge'; + + color: string = undefined; + backColor: string = undefined; + borderColor: string = undefined; + changedNodeColor: string = '#39A5DC'; + onhoverColor: string = '#F5F5F5'; + selectedColor: string = '#FFFFFF'; + selectedBackColor: string = '#428bca'; + searchResultColor: string = '#D9534F'; + searchResultBackColor: string = undefined; + + highlightSelected: boolean = true; + highlightSearchResults: boolean = true; + showBorder: boolean = true; + showIcon: boolean = true; + showImage: boolean = false; + showCheckbox: boolean = false; + checkboxFirst: boolean = false; + highlightChanges: boolean = false; + showTags: boolean = false; + multiSelect: boolean = false; + preventUnselect: boolean = false; + allowReselect: boolean = false; + hierarchicalCheck: boolean = false; + propagateCheckEvent: boolean = false; + wrapNodeText: boolean = false; + + // Event handlers + onLoading: (event: Event) => void = undefined; + onLoadingFailed: (event: Event) => void = undefined; + onInitialized: (event: Event) => void = undefined; + onNodeRendered: (event: Event) => void = undefined; + onRendered: (event: Event) => void = undefined; + onDestroyed: (event: Event) => void = undefined; + + onNodeChecked: (event: Event) => void = undefined; + onNodeCollapsed: (event: Event) => void = undefined; + onNodeDisabled: (event: Event) => void = undefined; + onNodeEnabled: (event: Event) => void = undefined; + onNodeExpanded: (event: Event) => void = undefined; + onNodeSelected: (event: Event) => void = undefined; + onNodeUnchecked: (event: Event) => void = undefined; + onNodeUnselected: (event: Event) => void = undefined; + + onSearchComplete: (event: Event) => void = undefined; + onSearchCleared: (event: Event) => void = undefined; + + lazyLoad: (node: BSTreeViewNode, renderer: (nodes: BSTreeViewNode[]) => void) => void = undefined; + + constructor(options: BSTreeViewOptions|object = null) { + if(options) { + Object.assign(this, options); + } + } +} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewSelectOptions.ts b/assets/ts_src/BSTreeViewSelectOptions.ts new file mode 100644 index 00000000..f110c43e --- /dev/null +++ b/assets/ts_src/BSTreeViewSelectOptions.ts @@ -0,0 +1,7 @@ +import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; + + +export default class BSTreeViewSelectOptions extends BSTreeViewEventOptions +{ + unselecting: boolean; +} \ No newline at end of file diff --git a/assets/tsconfig.json b/assets/tsconfig.json index 9c05d57d..adc8b23b 100644 --- a/assets/tsconfig.json +++ b/assets/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es6", "sourceMap": true, "typeRoots": ["../node_modules"], - "types": ["jquery", "bootstrap", "jquery.form", "bootstrap-treeview", "bootbox", "typeahead", "marked"] + "types": [] }, "exclude": [ "node_modules" diff --git a/webpack.config.js b/webpack.config.js index 9bd1f8f4..484b4744 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -100,7 +100,7 @@ Encore //.enableSassLoader() // uncomment if you use TypeScript - //.enableTypeScriptLoader() + .enableTypeScriptLoader() // uncomment if you use React //.enableReactPreset() From c13245fc2d9b084e4b2f16bafc89588dca429943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 6 Aug 2022 04:04:06 +0200 Subject: [PATCH 2/9] BSTreeView now rudimentaly works. --- .../controllers/elements/tree_controller.js | 3 +- assets/ts_src/BSTreeView.ts | 52 ++++++++++--------- assets/ts_src/BSTreeViewNode.ts | 1 + 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/assets/controllers/elements/tree_controller.js b/assets/controllers/elements/tree_controller.js index 6ed34d51..f5f1bcb2 100644 --- a/assets/controllers/elements/tree_controller.js +++ b/assets/controllers/elements/tree_controller.js @@ -55,7 +55,8 @@ export default class extends Controller { showBorder: true, searchResultBackColor: primary_color, searchResultColor: '#000', - onNodeSelected: function (event, data) { + onNodeSelected: function (event) { + const data = event.detail.data; if (data.href) { //Simulate a click so we just change the inner frame diff --git a/assets/ts_src/BSTreeView.ts b/assets/ts_src/BSTreeView.ts index 863ac4c3..6d872c21 100644 --- a/assets/ts_src/BSTreeView.ts +++ b/assets/ts_src/BSTreeView.ts @@ -90,7 +90,7 @@ export default class BSTreeView constructor(element: HTMLElement, options: BSTreeViewOptions|object) { this.element = element; - this._elementId = element.id; + this._elementId = element.id ?? "bs-treeview-" + Math.floor(Math.random() * 1000000); this._styleId = this._elementId + '-style'; this._init(options); @@ -115,7 +115,7 @@ export default class BSTreeView this._destroy(); this._subscribeEvents(); - this._triggerEvent('loading', null, new BSTreeViewEventOptions({silent: true})); + this._triggerEvent(EVENT_LOADING, null, new BSTreeViewEventOptions({silent: true})); this._load(this._options) .then((data) => { // load done @@ -123,7 +123,7 @@ export default class BSTreeView }) .catch((error) => { // load fail - this._triggerEvent('loadingFailed', error, new BSTreeViewEventOptions()); + this._triggerEvent(EVENT_LOADING_FAILED, error, new BSTreeViewEventOptions()); }) .then((treeData) => { // initialize data @@ -182,7 +182,7 @@ export default class BSTreeView if (!this._initialized) return; this._initialized = false; - this._triggerEvent('destroyed', null, new BSTreeViewEventOptions()); + this._triggerEvent(EVENT_DESTROYED, null, new BSTreeViewEventOptions()); // Switch off events this._unsubscribeEvents(); @@ -355,7 +355,7 @@ export default class BSTreeView promise.then(() => { this._orderedNodes = this._sortNodes(this._nodes); this._inheritCheckboxChanges(); - this._triggerEvent('initialized', Array.from(this._orderedNodes.values())); + this._triggerEvent(EVENT_INITIALIZED, Array.from(this._orderedNodes.values())); }); return promise; @@ -446,7 +446,7 @@ export default class BSTreeView } // add / update indexed collection - this._nodes[node.nodeId] = node; + this._nodes.set(node.nodeId, node); }) }; @@ -496,7 +496,7 @@ export default class BSTreeView targetNode (target: HTMLElement): BSTreeViewNode { const nodeElement = target.closest('li.list-group-item') as HTMLElement; const nodeId = nodeElement.dataset.nodeId; - const node = this._nodes[nodeId]; + const node = this._nodes.get(nodeId); if (!node) { console.warn('Error: node does not exist'); } @@ -557,7 +557,7 @@ export default class BSTreeView } // Optionally trigger event - this._triggerEvent('nodeExpanded', node, options); + this._triggerEvent(EVENT_NODE_EXPANDED, node, options); } else if (!state) { @@ -580,7 +580,7 @@ export default class BSTreeView } // Optionally trigger event - this._triggerEvent('nodeCollapsed', node, options); + this._triggerEvent(EVENT_NODE_COLLAPSED, node, options); } }; @@ -649,7 +649,7 @@ export default class BSTreeView } // Optionally trigger event - this._triggerEvent('nodeSelected', node, options); + this._triggerEvent(EVENT_NODE_SELECTED, node, options); } else { @@ -659,7 +659,7 @@ export default class BSTreeView (this._findNodes('true', 'state.selected').length === 1)) { // Fire the nodeSelected event if reselection is allowed if (this._options.allowReselect) { - this._triggerEvent('nodeSelected', node, options); + this._triggerEvent(EVENT_NODE_SELECTED, node, options); } return this; } @@ -679,7 +679,7 @@ export default class BSTreeView } // Optionally trigger event - this._triggerEvent('nodeUnselected', node, options); + this._triggerEvent(EVENT_NODE_UNSELECTED, node, options); } return this; @@ -709,7 +709,7 @@ export default class BSTreeView // Temporarily swap the tree state node.state.checked = !node.state.checked; - currentNode = this._nodes[currentNode.parentId] + currentNode = this._nodes.get(currentNode.parentId); // Iterate through each parent node while (currentNode) { @@ -721,7 +721,7 @@ export default class BSTreeView // Set the state this._setChecked(currentNode, state, childOptions); - currentNode = this._nodes[currentNode.parentId] + currentNode = this._nodes.get(currentNode.parentId); } if (node.nodes && node.nodes.length > 0) { @@ -778,7 +778,7 @@ export default class BSTreeView } // Optionally trigger event - this._triggerEvent('nodeChecked', node, options); + this._triggerEvent(EVENT_NODE_CHECKED, node, options); } else if (state === null && this._options.hierarchicalCheck) { @@ -796,7 +796,7 @@ export default class BSTreeView } // Optionally trigger event, partially checked is technically unchecked - this._triggerEvent('nodeUnchecked', node, options); + this._triggerEvent(EVENT_NODE_UNCHECKED, node, options); } else { // Set node state to unchecked @@ -812,7 +812,7 @@ export default class BSTreeView } // Optionally trigger event - this._triggerEvent('nodeUnchecked', node, options); + this._triggerEvent(EVENT_NODE_UNCHECKED, node, options); } }; @@ -840,7 +840,7 @@ export default class BSTreeView } // Optionally trigger event - this._triggerEvent('nodeDisabled', node, options); + this._triggerEvent(EVENT_NODE_DISABLED, node, options); } else { @@ -853,7 +853,7 @@ export default class BSTreeView } // Optionally trigger event - this._triggerEvent('nodeEnabled', node, options); + this._triggerEvent(EVENT_NODE_DISABLED, node, options); } }; @@ -903,7 +903,7 @@ export default class BSTreeView previousNode = node; }); - this._triggerEvent('rendered', Array.from(this._orderedNodes.values()), new BSTreeViewEventOptions()); + this._triggerEvent(EVENT_RENDERED, Array.from(this._orderedNodes.values()), new BSTreeViewEventOptions()); }; _renderNode(node: BSTreeViewNode, previousNode: BSTreeViewNode|null): void { @@ -918,7 +918,9 @@ export default class BSTreeView } // Append .classes to the node - node.el.classList.add(...node.class.split(" ")); + if(node.class) { + node.el.classList.add(...node.class.split(" ")); + } // Set the #id of the node if specified if (node.id) { @@ -1002,7 +1004,7 @@ export default class BSTreeView this._setVisible(node, node.state.visible); // Trigger nodeRendered event - this._triggerEvent('nodeRendered', node, new BSTreeViewEventOptions()); + this._triggerEvent(EVENT_NODE_RENDERED, node, new BSTreeViewEventOptions()); }; // Add checkable icon @@ -1203,7 +1205,7 @@ export default class BSTreeView let parentNodes = []; nodes.forEach((node) => { - const parentNode = node.parentId ? this._nodes[node.parentId] : false; + const parentNode = node.parentId ? this._nodes.get(node.parentId) : false; if (parentNode) { parentNodes.push(parentNode); } @@ -1394,7 +1396,7 @@ export default class BSTreeView nodes.forEach((node) => { // remove nodes from tree - parentNode = this._nodes[node.parentId]; + parentNode = this._nodes.get(node.parentId); if (parentNode) { targetNodes = parentNode.nodes; } else { @@ -1424,7 +1426,7 @@ export default class BSTreeView // insert new node let targetNodes; - const parentNode = this._nodes[node.parentId]; + const parentNode = this._nodes.get(node.parentId); if (parentNode) { targetNodes = parentNode.nodes; } else { diff --git a/assets/ts_src/BSTreeViewNode.ts b/assets/ts_src/BSTreeViewNode.ts index adfc5d53..8ed3cd6e 100644 --- a/assets/ts_src/BSTreeViewNode.ts +++ b/assets/ts_src/BSTreeViewNode.ts @@ -20,6 +20,7 @@ export default class BSTreeViewNode { hideCheckbox: boolean; nodes: BSTreeViewNode[]; tooltip: string; + href: string; lazyLoad: boolean; tagsClass: string; From c5b60689119a59b0edefe86e8fc003ba9ca0e4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Aug 2022 01:54:34 +0200 Subject: [PATCH 3/9] Moved bs-tree to its own repo --- assets/ts_src/BSTreeSearchOptions.ts | 7 - assets/ts_src/BSTreeView.ts | 1868 --------------------- assets/ts_src/BSTreeViewDisableOptions.ts | 8 - assets/ts_src/BSTreeViewEventOptions.ts | 12 - assets/ts_src/BSTreeViewExpandOptions.ts | 7 - assets/ts_src/BSTreeViewNode.ts | 45 - assets/ts_src/BSTreeViewNodeState.ts | 8 - assets/ts_src/BSTreeViewOptions.ts | 78 - assets/ts_src/BSTreeViewSelectOptions.ts | 7 - 9 files changed, 2040 deletions(-) delete mode 100644 assets/ts_src/BSTreeSearchOptions.ts delete mode 100644 assets/ts_src/BSTreeView.ts delete mode 100644 assets/ts_src/BSTreeViewDisableOptions.ts delete mode 100644 assets/ts_src/BSTreeViewEventOptions.ts delete mode 100644 assets/ts_src/BSTreeViewExpandOptions.ts delete mode 100644 assets/ts_src/BSTreeViewNode.ts delete mode 100644 assets/ts_src/BSTreeViewNodeState.ts delete mode 100644 assets/ts_src/BSTreeViewOptions.ts delete mode 100644 assets/ts_src/BSTreeViewSelectOptions.ts diff --git a/assets/ts_src/BSTreeSearchOptions.ts b/assets/ts_src/BSTreeSearchOptions.ts deleted file mode 100644 index b993d9b0..00000000 --- a/assets/ts_src/BSTreeSearchOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; - -export default class BSTreeSearchOptions extends BSTreeViewEventOptions { - ignoreCase: boolean = true; - exactMatch: boolean = false; - revealResults: boolean = true; -} \ No newline at end of file diff --git a/assets/ts_src/BSTreeView.ts b/assets/ts_src/BSTreeView.ts deleted file mode 100644 index 6d872c21..00000000 --- a/assets/ts_src/BSTreeView.ts +++ /dev/null @@ -1,1868 +0,0 @@ -import BSTreeViewOptions from "./BSTreeViewOptions"; -import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; -import {default as BSTreeViewNode} from "./BSTreeViewNode"; -import BSTreeViewNodeState from "./BSTreeViewNodeState"; -import BSTreeViewSelectOptions from "./BSTreeViewSelectOptions"; -import BSTreeViewDisableOptions from "./BSTreeViewDisableOptions"; -import BSTreeViewExpandOptions from "./BSTreeViewExpandOptions"; -import BSTreeSearchOptions from "./BSTreeSearchOptions"; - - -const pluginName = 'treeview'; - -const EVENT_LOADING = 'bs-tree:loading'; -const EVENT_LOADING_FAILED = 'bs-tree:loadingFailed'; -const EVENT_INITIALIZED = 'bs-tree:initialized'; -const EVENT_NODE_RENDERED = 'bs-tree:nodeRendered'; -const EVENT_RENDERED = 'bs-tree:rendered'; -const EVENT_DESTROYED = 'bs-tree:destroyed'; - -const EVENT_NODE_CHECKED = 'bs-tree:nodeChecked'; -const EVENT_NODE_COLLAPSED = 'bs-tree:nodeCollapsed'; -const EVENT_NODE_DISABLED = 'bs-tree:nodeDisabled'; -const EVENT_NODE_ENABLED = 'bs-tree:nodeEnabled'; -const EVENT_NODE_EXPANDED = 'bs-tree:nodeExpanded'; -const EVENT_NODE_SELECTED = 'bs-tree:nodeSelected'; -const EVENT_NODE_UNCHECKED = 'bs-tree:nodeUnchecked'; -const EVENT_NODE_UNSELECTED = 'bs-tree:nodeUnselected'; - -const EVENT_SEARCH_COMPLETED = 'bs-tree:searchCompleted'; -const EVENT_SEARCH_CLEARED = 'bs-tree:searchCleared'; - -function templateElement(tagType: string, classes: string): HTMLElement { - const el = document.createElement(tagType); - if(classes.length > 0) { - el.classList.add(...classes.split(" ")); - } - return el; -} - -export default class BSTreeView -{ - _template = { - tree: templateElement('ul', "list-group"), - node: templateElement("li", "list-group-item"), - indent: templateElement("span", "indent"), - icon: { - node: templateElement("span", "icon node-icon"), - expand: templateElement("span", "icon expand-icon"), - check: templateElement("span", "icon check-icon"), - empty: templateElement("span", "icon") - }, - image: templateElement("span", "image"), - badge: templateElement("span", ""), - text: templateElement("span", "text"), - }; - - _css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' - - - /** - * @param {HTMLElement} The HTMLElement this tree applies to - */ - element: HTMLElement; - - wrapper: HTMLElement|null; - - /** - * {string} - * @private - */ - _elementId: string; - - _styleId: string; - - _tree: BSTreeViewNode[]; - - _nodes: Map; - _orderedNodes: Map; - - _checkedNodes: BSTreeViewNode[]; - - /** - * @private - * {boolean} - */ - _initialized: boolean; - - _options: BSTreeViewOptions; - - constructor(element: HTMLElement, options: BSTreeViewOptions|object) - { - this.element = element; - this._elementId = element.id ?? "bs-treeview-" + Math.floor(Math.random() * 1000000); - this._styleId = this._elementId + '-style'; - - this._init(options); - } - - _init (options: BSTreeViewOptions|object) - { - this._tree = []; - this._initialized = false; - - //If options is a BSTreeViewOptions object, we can use it directly - if(options instanceof BSTreeViewOptions) { - this._options = options; - } else { - //Otherwise we have to apply our options object on it - this._options = new BSTreeViewOptions(options); - } - - // Cache empty icon DOM template - this._template.icon.empty.classList.add(...this._options.emptyIcon.split(" ")); - - this._destroy(); - this._subscribeEvents(); - - this._triggerEvent(EVENT_LOADING, null, new BSTreeViewEventOptions({silent: true})); - this._load(this._options) - .then((data) => { - // load done - return this._tree = data; - }) - .catch((error) => { - // load fail - this._triggerEvent(EVENT_LOADING_FAILED, error, new BSTreeViewEventOptions()); - }) - .then((treeData) => { - // initialize data - if(treeData) { - return this._setInitialStates(new BSTreeViewNode({nodes: treeData}), 0); - } - }) - .then(() => { - // render to DOM - this._render(); - }) - ; - } - - _load (options: BSTreeViewOptions): Promise { - if (options.data) { - return this._loadLocalData(options); - } else if (options.ajaxURL) { - return this._loadRemoteData(options); - } - throw new Error("No data source defined."); - } - - _loadRemoteData (options: BSTreeViewOptions): Promise { - return new Promise((resolve, reject) => { - fetch(options.ajaxURL, options.ajaxConfig).then((response) => { - resolve(response.json()); - }) - .catch((error) => reject(error)); - }); - } - - _loadLocalData (options: BSTreeViewOptions): Promise { - return new Promise((resolve, reject) => { - //if options.data is a string we need to JSON decode it first - if (typeof options.data === 'string') { - try { - resolve(JSON.parse(options.data)); - } catch (error) { - reject(error); - } - } else { - resolve(options.data); - } - }); - }; - - _remove () { - this._destroy(); - //$.removeData(this, pluginName); - const styleElement = document.getElementById(this._styleId); - styleElement.remove(); - }; - - _destroy () { - if (!this._initialized) return; - this._initialized = false; - - this._triggerEvent(EVENT_DESTROYED, null, new BSTreeViewEventOptions()); - - // Switch off events - this._unsubscribeEvents(); - - // Tear down - this.wrapper.remove(); - this.wrapper = null; - }; - - _unsubscribeEvents () { - if (typeof (this._options.onLoading) === 'function') { - this.element.removeEventListener(EVENT_LOADING, this._options.onLoading); - } - - if (typeof (this._options.onLoadingFailed) === 'function') { - this.element.removeEventListener(EVENT_LOADING_FAILED, this._options.onLoadingFailed); - } - - if (typeof (this._options.onInitialized) === 'function') { - this.element.removeEventListener(EVENT_INITIALIZED, this._options.onInitialized); - } - - if (typeof (this._options.onNodeRendered) === 'function') { - this.element.removeEventListener(EVENT_NODE_RENDERED, this._options.onNodeRendered); - } - - if (typeof (this._options.onRendered) === 'function') { - this.element.removeEventListener(EVENT_RENDERED, this._options.onRendered); - } - - if (typeof (this._options.onDestroyed) === 'function') { - this.element.removeEventListener(EVENT_DESTROYED, this._options.onDestroyed); - } - - this.element.removeEventListener('click', this._clickHandler.bind(this)); - - if (typeof (this._options.onNodeChecked) === 'function') { - this.element.removeEventListener(EVENT_NODE_CHECKED, this._options.onNodeChecked); - } - - if (typeof (this._options.onNodeCollapsed) === 'function') { - this.element.removeEventListener(EVENT_NODE_COLLAPSED, this._options.onNodeCollapsed); - } - - if (typeof (this._options.onNodeDisabled) === 'function') { - this.element.removeEventListener(EVENT_NODE_DISABLED, this._options.onNodeDisabled); - } - - if (typeof (this._options.onNodeEnabled) === 'function') { - this.element.removeEventListener(EVENT_NODE_ENABLED, this._options.onNodeEnabled); - } - - if (typeof (this._options.onNodeExpanded) === 'function') { - this.element.removeEventListener(EVENT_NODE_EXPANDED, this._options.onNodeExpanded); - } - - if (typeof (this._options.onNodeSelected) === 'function') { - this.element.removeEventListener(EVENT_NODE_SELECTED, this._options.onNodeSelected); - } - - if (typeof (this._options.onNodeUnchecked) === 'function') { - this.element.removeEventListener(EVENT_NODE_UNCHECKED, this._options.onNodeUnchecked); - } - - if (typeof (this._options.onNodeUnselected) === 'function') { - this.element.removeEventListener(EVENT_NODE_UNSELECTED, this._options.onNodeUnselected); - } - - if (typeof (this._options.onSearchComplete) === 'function') { - this.element.removeEventListener(EVENT_SEARCH_COMPLETED, this._options.onSearchComplete); - } - - if (typeof (this._options.onSearchCleared) === 'function') { - this.element.removeEventListener(EVENT_SEARCH_CLEARED, this._options.onSearchCleared); - } - }; - - _subscribeEvents () { - this._unsubscribeEvents(); - - if (typeof (this._options.onLoading) === 'function') { - this.element.addEventListener(EVENT_LOADING, this._options.onLoading); - } - - if (typeof (this._options.onLoadingFailed) === 'function') { - this.element.addEventListener(EVENT_LOADING_FAILED, this._options.onLoadingFailed); - } - - if (typeof (this._options.onInitialized) === 'function') { - this.element.addEventListener(EVENT_INITIALIZED, this._options.onInitialized); - } - - if (typeof (this._options.onNodeRendered) === 'function') { - this.element.addEventListener(EVENT_NODE_RENDERED, this._options.onNodeRendered); - } - - if (typeof (this._options.onRendered) === 'function') { - this.element.addEventListener(EVENT_RENDERED, this._options.onRendered); - } - - if (typeof (this._options.onDestroyed) === 'function') { - this.element.addEventListener(EVENT_DESTROYED, this._options.onDestroyed); - } - - this.element.addEventListener('click', this._clickHandler.bind(this)); - - if (typeof (this._options.onNodeChecked) === 'function') { - this.element.addEventListener(EVENT_NODE_CHECKED, this._options.onNodeChecked); - } - - if (typeof (this._options.onNodeCollapsed) === 'function') { - this.element.addEventListener(EVENT_NODE_COLLAPSED, this._options.onNodeCollapsed); - } - - if (typeof (this._options.onNodeDisabled) === 'function') { - this.element.addEventListener(EVENT_NODE_DISABLED, this._options.onNodeDisabled); - } - - if (typeof (this._options.onNodeEnabled) === 'function') { - this.element.addEventListener(EVENT_NODE_ENABLED, this._options.onNodeEnabled); - } - - if (typeof (this._options.onNodeExpanded) === 'function') { - this.element.addEventListener(EVENT_NODE_EXPANDED, this._options.onNodeExpanded); - } - - if (typeof (this._options.onNodeSelected) === 'function') { - this.element.addEventListener(EVENT_NODE_SELECTED, this._options.onNodeSelected); - } - - if (typeof (this._options.onNodeUnchecked) === 'function') { - this.element.addEventListener(EVENT_NODE_UNCHECKED, this._options.onNodeUnchecked); - } - - if (typeof (this._options.onNodeUnselected) === 'function') { - this.element.addEventListener(EVENT_NODE_UNSELECTED, this._options.onNodeUnselected); - } - - if (typeof (this._options.onSearchComplete) === 'function') { - this.element.addEventListener(EVENT_SEARCH_COMPLETED, this._options.onSearchComplete); - } - - if (typeof (this._options.onSearchCleared) === 'function') { - this.element.addEventListener(EVENT_SEARCH_CLEARED, this._options.onSearchCleared); - } - }; - - _triggerEvent (eventType: string, data: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = null) { - if (options && !options.silent) { - const event = new CustomEvent(eventType, { detail: {data: data, eventOptions: options, treeView: this} }); - - this.element.dispatchEvent(event); - } - } - - /* - Recurse the tree structure and ensure all nodes have - valid initial states. User defined states will be preserved. - For performance we also take this opportunity to - index nodes in a flattened ordered structure - */ - _setInitialStates (node: BSTreeViewNode, level: number): Promise { - this._nodes = new Map(); - - const promise = new Promise((resolve) => { - this._setInitialState(node, level) - resolve(false); - }); - - promise.then(() => { - this._orderedNodes = this._sortNodes(this._nodes); - this._inheritCheckboxChanges(); - this._triggerEvent(EVENT_INITIALIZED, Array.from(this._orderedNodes.values())); - }); - - return promise; - }; - - _setInitialState (node: BSTreeViewNode, level: number): void { - if (!node.nodes) return; - level += 1; - - let parent = node; - node.nodes.forEach((node, index) => { - // level : hierarchical tree level, starts at 1 - node.level = level; - - // index : relative to siblings - node.index = index; - - // nodeId : unique, hierarchical identifier - node.nodeId = (parent && parent.nodeId) ? - parent.nodeId + '.' + node.index : - (level - 1) + '.' + node.index; - - // parentId : transversing up the tree - node.parentId = parent.nodeId; - - // if not provided set selectable default value - if (!node.hasOwnProperty('selectable')) { - node.selectable = true; - } - - // if not provided set checkable default value - if (!node.hasOwnProperty('checkable')) { - node.checkable = true; - } - - // where provided we should preserve states - node.state = node.state || new BSTreeViewNodeState(); - - // set checked state; unless set always false - if (!node.state.hasOwnProperty('checked')) { - node.state.checked = false; - } - - // convert the undefined string if hierarchical checks are enabled - if (this._options.hierarchicalCheck && node.state.checked === null) { - node.state.checked = null; - } - - // set enabled state; unless set always false - if (!node.state.hasOwnProperty('disabled')) { - node.state.disabled = false; - } - - // set expanded state; if not provided based on levels - if (!node.state.hasOwnProperty('expanded')) { - if (!node.state.disabled && - (level < this._options.levels) && - (node.nodes && node.nodes.length > 0)) { - node.state.expanded = true; - } - else { - node.state.expanded = false; - } - } - - // set selected state; unless set always false - if (!node.state.hasOwnProperty('selected')) { - node.state.selected = false; - } - - // set visible state; based parent state plus levels - if ((parent && parent.state && parent.state.expanded) || - (level <= this._options.levels)) { - node.state.visible = true; - } - else { - node.state.visible = false; - } - - // recurse child nodes and transverse the tree, depth-first - if (node.nodes) { - if (node.nodes.length > 0) { - this._setInitialState(node, level); - } - else { - delete node.nodes; - } - } - - // add / update indexed collection - this._nodes.set(node.nodeId, node); - }) - }; - - _sortNodes (nodes: Map): Map { - return new Map([...nodes].sort((pairA, pairB) => { - //Index 0 of our pair variables contains the index of our map - - if (pairA[0] === pairB[0]) return 0; - const a = pairA[0].split('.').map(function (level) { return parseInt(level); }); - const b = pairB[0].split('.').map(function (level) { return parseInt(level); }); - - const c = Math.max(a.length, b.length); - for (let i=0; i 0) return +1; - if (a[i] - b[i] < 0) return -1; - } - })); - }; - - _clickHandler (event: Event) { - const target = event.target as HTMLElement; - const node = this.targetNode(target); - if (!node || node.state.disabled) return; - - const classList = target.classList; - if (classList.contains('expand-icon')) { - this._toggleExpanded(node); - } - else if (classList.contains('check-icon')) { - if (node.checkable) { - this._toggleChecked(node); - } - } - else { - if (node.selectable) { - this._toggleSelected(node); - } else { - this._toggleExpanded(node); - } - } - }; - - /* Looks up the DOM for the closest parent list item to retrieve the - * data attribute nodeid, which is used to lookup the node in the flattened structure. */ - targetNode (target: HTMLElement): BSTreeViewNode { - const nodeElement = target.closest('li.list-group-item') as HTMLElement; - const nodeId = nodeElement.dataset.nodeId; - const node = this._nodes.get(nodeId); - if (!node) { - console.warn('Error: node does not exist'); - } - return node; - }; - - _toggleExpanded (node: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { - if (!node) return; - - // Lazy-load the child nodes if possible - if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) { - this._lazyLoad(node); - } else { - this._setExpanded(node, !node.state.expanded, options); - } - }; - - _lazyLoad (node: BSTreeViewNode) { - if(!node.lazyLoad) return; - - // Show a different icon while loading the child nodes - const span = node.el.querySelector('span.expand-icon'); - span.classList.remove(...this._options.expandIcon.split(' ')); - span.classList.add(...this._options.loadingIcon.split(' ')); - - this._options.lazyLoad(node, (nodes) => { - // Adding the node will expand its parent automatically - this.addNode(nodes, node); - }); - // Only the first expand should do a lazy-load - node.lazyLoad = false; - }; - - _setExpanded (node: BSTreeViewNode, state: boolean, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { - - // We never pass options when rendering, so the only time - // we need to validate state is from user interaction - if (options && state === node.state.expanded) return; - - if (state && node.nodes) { - - // Set node state - node.state.expanded = true; - - // Set element - if (node.el) { - const span = node.el.querySelector('span.expand-icon'); - span.classList.remove(...this._options.expandIcon.split(" ")) - span.classList.remove(...this._options.loadingIcon.split(" ")) - span.classList.add(...this._options.collapseIcon.split(" ")); - } - - // Expand children - if (node.nodes && options) { - node.nodes.forEach((node) => { - this._setVisible(node, true, options); - }); - } - - // Optionally trigger event - this._triggerEvent(EVENT_NODE_EXPANDED, node, options); - } - else if (!state) { - - // Set node state - node.state.expanded = false; - - // Set element - if (node.el) { - const span = node.el.querySelector('span.expand-icon'); - span.classList.remove(...this._options.collapseIcon.split(" ")); - span.classList.add(...this._options.expandIcon.split(" ")); - } - - // Collapse children - if (node.nodes && options) { - node.nodes.forEach ((node) => { - this._setVisible(node, false, options); - this._setExpanded(node, false, options); - }); - } - - // Optionally trigger event - this._triggerEvent(EVENT_NODE_COLLAPSED, node, options); - } - }; - - _setVisible (node: BSTreeViewNode, state: boolean, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { - - if (options && state === node.state.visible) return; - - if (state) { - - // Set node state - node.state.visible = true; - - // Set element - if (node.el) { - node.el.classList.remove('node-hidden'); - } - } - else { - - // Set node state to unchecked - node.state.visible = false; - - // Set element - if (node.el) { - node.el.classList.add('node-hidden'); - } - } - }; - - _toggleSelected (node: BSTreeViewNode, options: BSTreeViewSelectOptions = new BSTreeViewSelectOptions()): this { - if (!node) return; - this._setSelected(node, !node.state.selected, options); - return this; - }; - - _setSelected (node: BSTreeViewNode, state: boolean, options = new BSTreeViewSelectOptions()): this { - - // We never pass options when rendering, so the only time - // we need to validate state is from user interaction - if (options && (state === node.state.selected)) return; - - if (state) { - - // If multiSelect false, unselect previously selected - if (!this._options.multiSelect) { - const selectedNodes = this._findNodes('true', 'state.selected'); - selectedNodes.forEach((node) => { - options.unselecting = true; - - this._setSelected(node, false, options); - }); - } - - // Set node state - node.state.selected = true; - - // Set element - if (node.el) { - node.el.classList.add('node-selected'); - - if (node.selectedIcon || this._options.selectedIcon) { - const span = node.el.querySelector('span.node-icon'); - span.classList.remove(...(node.icon || this._options.nodeIcon).split(" ")); - span.classList.add(...(node.selectedIcon || this._options.selectedIcon).split(" ")); - } - } - - // Optionally trigger event - this._triggerEvent(EVENT_NODE_SELECTED, node, options); - } - else { - - // If preventUnselect true + only one remaining selection, disable unselect - if (this._options.preventUnselect && - (options && !options.unselecting) && - (this._findNodes('true', 'state.selected').length === 1)) { - // Fire the nodeSelected event if reselection is allowed - if (this._options.allowReselect) { - this._triggerEvent(EVENT_NODE_SELECTED, node, options); - } - return this; - } - - // Set node state - node.state.selected = false; - - // Set element - if (node.el) { - node.el.classList.remove('node-selected'); - - if (node.selectedIcon || this._options.selectedIcon) { - const span = node.el.querySelector('span.node-icon'); - span.classList.remove(...(node.selectedIcon || this._options.selectedIcon).split(" ")) - span.classList.add(...(node.icon || this._options.nodeIcon).split(" ")); - } - } - - // Optionally trigger event - this._triggerEvent(EVENT_NODE_UNSELECTED, node, options); - } - - return this; - }; - - _inheritCheckboxChanges (): void { - if (this._options.showCheckbox && this._options.highlightChanges) { - this._checkedNodes = []; - this._orderedNodes.forEach((node) => { - if(node.state.checked) { - this._checkedNodes.push(node); - } - }); - } - }; - - _toggleChecked (node: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): this { - if (!node) return; - - if (this._options.hierarchicalCheck) { - // Event propagation to the parent/child nodes - const childOptions = new BSTreeViewEventOptions(options); - childOptions.silent = options.silent || !this._options.propagateCheckEvent; - - let state: boolean|null; - let currentNode = node; - // Temporarily swap the tree state - node.state.checked = !node.state.checked; - - currentNode = this._nodes.get(currentNode.parentId); - // Iterate through each parent node - while (currentNode) { - - // Calculate the state - state = currentNode.nodes.reduce((acc, curr) => { - return (acc === curr.state.checked) ? acc : null; - }, currentNode.nodes[0].state.checked); - - // Set the state - this._setChecked(currentNode, state, childOptions); - - currentNode = this._nodes.get(currentNode.parentId); - } - - if (node.nodes && node.nodes.length > 0) { - // Copy the content of the array - let child, children = node.nodes.slice(); - // Iterate through each child node - while (children && children.length > 0) { - child = children.pop(); - - // Set the state - this._setChecked(child, node.state.checked, childOptions); - - // Append children to the end of the list - if (child.nodes && child.nodes.length > 0) { - children = children.concat(child.nodes.slice()); - } - } - } - // Swap back the tree state - node.state.checked = !node.state.checked; - } - - this._setChecked(node, !node.state.checked, options); - }; - - _setChecked (node: BSTreeViewNode, state: boolean, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { - // We never pass options when rendering, so the only time - // we need to validate state is from user interaction - if (options && state === node.state.checked) return; - - // Highlight the node if its checkbox has unsaved changes - if (this._options.highlightChanges) { - const nodeNotInCheckList = this._checkedNodes.indexOf(node) == -1; - if(nodeNotInCheckList == state) { - node.el.classList.add('node-check-changed'); - } else { - node.el.classList.remove('node-check-changed'); - } - } - - if (state) { - - // Set node state - node.state.checked = true; - - // Set element - if (node.el) { - node.el.classList.add('node-checked'); - node.el.classList.remove('node-checked-partial'); - const span = node.el.querySelector('span.check-icon'); - span.classList.remove(...this._options.uncheckedIcon.split(" ")) - span.classList.remove(...this._options.partiallyCheckedIcon.split(" ")) - span.classList.add(...this._options.checkedIcon.split(" ")); - } - - // Optionally trigger event - this._triggerEvent(EVENT_NODE_CHECKED, node, options); - } - else if (state === null && this._options.hierarchicalCheck) { - - // Set node state to partially checked - node.state.checked = null; - - // Set element - if (node.el) { - node.el.classList.add('node-checked-partial'); - node.el.classList.remove('node-checked'); - const span = node.el.querySelector('span.check-icon'); - span.classList.remove(...this._options.uncheckedIcon.split(" ")); - span.classList.remove(...this._options.checkedIcon.split(" ")); - span.classList.add(...this._options.partiallyCheckedIcon.split(" ")); - } - - // Optionally trigger event, partially checked is technically unchecked - this._triggerEvent(EVENT_NODE_UNCHECKED, node, options); - } else { - - // Set node state to unchecked - node.state.checked = false; - - // Set element - if (node.el) { - node.el.classList.remove('node-checked node-checked-partial'); - const span = node.el.querySelector('span.check-icon'); - span.classList.remove(...this._options.checkedIcon.split(" ")); - span.classList.remove(...this._options.partiallyCheckedIcon.split(" ")); - span.classList.add(...this._options.uncheckedIcon.split(" ")); - } - - // Optionally trigger event - this._triggerEvent(EVENT_NODE_UNCHECKED, node, options); - } - }; - - _setDisabled (node: BSTreeViewNode, state: boolean, options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()) { - - // We never pass options when rendering, so the only time - // we need to validate state is from user interaction - if (options && state === node.state.disabled) return; - - if (state) { - - // Set node state to disabled - node.state.disabled = true; - - // Disable all other states - if (options && !options.keepState) { - this._setSelected(node, false, options); - this._setChecked(node, false, options); - this._setExpanded(node, false, options); - } - - // Set element - if (node.el) { - node.el.classList.add('node-disabled'); - } - - // Optionally trigger event - this._triggerEvent(EVENT_NODE_DISABLED, node, options); - } - else { - - // Set node state to enabled - node.state.disabled = false; - - // Set element - if (node.el) { - node.el.classList.remove('node-disabled'); - } - - // Optionally trigger event - this._triggerEvent(EVENT_NODE_DISABLED, node, options); - } - }; - - _setSearchResult (node: BSTreeViewNode, state: boolean, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { - if (options && state === node.searchResult) return; - - if (state) { - - node.searchResult = true; - - if (node.el) { - node.el.classList.add('node-result'); - } - } - else { - - node.searchResult = false; - - if (node.el) { - node.el.classList.remove('node-result'); - } - } - }; - - _render(): void { - if (!this._initialized) { - - // Setup first time only components - this.wrapper = this._template.tree.cloneNode(true) as HTMLElement; - //Empty this element - while(this.element.firstChild) { - this.element.removeChild(this.element.firstChild); - } - - - this.element.classList.add(...pluginName.split(" ")) - this.element.appendChild(this.wrapper); - - this._injectStyle(); - - this._initialized = true; - } - - let previousNode: BSTreeViewNode|null = null; - this._orderedNodes.forEach((node) => { - this._renderNode(node, previousNode); - previousNode = node; - }); - - this._triggerEvent(EVENT_RENDERED, Array.from(this._orderedNodes.values()), new BSTreeViewEventOptions()); - }; - - _renderNode(node: BSTreeViewNode, previousNode: BSTreeViewNode|null): void { - if (!node) return; - - if (!node.el) { - node.el = this._newNodeEl(node, previousNode); - node.el.classList.add('node-' + this._elementId); - } - else { - node.el.innerHTML = ""; - } - - // Append .classes to the node - if(node.class) { - node.el.classList.add(...node.class.split(" ")); - } - - // Set the #id of the node if specified - if (node.id) { - node.el.id = node.id; - } - - // Append custom data- attributes to the node - if (node.dataAttr) { - for (const key in node.dataAttr) { - if (node.dataAttr.hasOwnProperty(key)) { - node.el.setAttribute('data-' + key, node.dataAttr[key]); - } - } - } - - // Set / update nodeid; it can change as a result of addNode etc. - node.el.dataset.nodeId = node.nodeId; - - // Set the tooltip attribute if present - if (node.tooltip) { - node.el.title = node.tooltip; - } - - // Add indent/spacer to mimic tree structure - for (let i = 0; i < (node.level - 1); i++) { - node.el.append(this._template.indent.cloneNode(true) as HTMLElement); - } - - // Add expand / collapse or empty spacer icons - node.el - .append( - node.nodes || node.lazyLoad ? this._template.icon.expand.cloneNode(true) as HTMLElement : this._template.icon.empty.cloneNode(true) as HTMLElement - ); - - // Add checkbox and node icons - if (this._options.checkboxFirst) { - this._addCheckbox(node); - this._addIcon(node); - this._addImage(node); - } else { - this._addIcon(node); - this._addImage(node); - this._addCheckbox(node); - } - - // Add text - if (this._options.wrapNodeText) { - const wrapper = this._template.text.cloneNode(true) as HTMLElement; - node.el.append(wrapper); - wrapper.append(node.text); - } else { - node.el.append(node.text); - } - - // Add tags as badges - if (this._options.showTags && node.tags) { - node.tags.forEach(tag => { - const template = this._template.badge.cloneNode(true) as HTMLElement; - template.classList.add( - //@ts-ignore - ...((typeof tag === 'object' ? tag.class : undefined) - || node.tagsClass - || this._options.tagsClass).split(" ") - ); - template.append( - //@ts-ignore - (typeof tag === 'object' ? tag.text : undefined) - || tag - ); - - node.el.append(template); - }); - } - - // Set various node states - this._setSelected(node, node.state.selected); - this._setChecked(node, node.state.checked); - this._setSearchResult(node, node.searchResult); - this._setExpanded(node, node.state.expanded); - this._setDisabled(node, node.state.disabled); - this._setVisible(node, node.state.visible); - - // Trigger nodeRendered event - this._triggerEvent(EVENT_NODE_RENDERED, node, new BSTreeViewEventOptions()); - }; - -// Add checkable icon - _addCheckbox (node: BSTreeViewNode): void { - if (this._options.showCheckbox && (node.hideCheckbox === undefined || node.hideCheckbox === false)) { - node.el - .append(this._template.icon.check.cloneNode(true) as HTMLElement); - } - } - -// Add node icon - _addIcon (node: BSTreeViewNode): void { - if (this._options.showIcon && !(this._options.showImage && node.image)) { - const template = this._template.icon.node.cloneNode(true) as HTMLElement; - template.classList.add(...(node.icon || this._options.nodeIcon).split(" ")) - - node.el.append(template); - } - } - - _addImage (node: BSTreeViewNode): void { - if (this._options.showImage && node.image) { - const template = this._template.image.cloneNode(true) as HTMLElement; - template.classList.add('node-image'); - template.style.backgroundImage = "url('" + node.image + "')"; - - - node.el - .append( - - ); - } - } - -// Creates a new node element from template and -// ensures the template is inserted at the correct position - _newNodeEl (node: BSTreeViewNode, previousNode: BSTreeViewNode|null): HTMLElement { - let template = this._template.node.cloneNode(true) as HTMLElement; - - if (previousNode) { - // typical usage, as nodes are rendered in - // sort order we add after the previous element - previousNode.el.after(template); - } else { - // we use prepend instead of append, - // to cater for root inserts i.e. nodeId 0.0 - this.wrapper.prepend(template); - } - - return template; - }; - -// Recursively remove node elements from DOM - _removeNodeEl (node: BSTreeViewNode): void { - if (!node) return; - - if (node.nodes) { - node.nodes.forEach((node) => { - this._removeNodeEl(node); - }); - } - node.el.remove(); - }; - -// Expand node, rendering it's immediate children - _expandNode (node: BSTreeViewNode): void { - if (!node.nodes) return; - - node.nodes.slice(0).reverse().forEach((childNode) => { - childNode.level = node.level + 1; - this._renderNode(childNode, node); - }); - }; - -// Add inline style into head - _injectStyle (): void { - if (this._options.injectStyle && !document.getElementById(this._styleId)) { - const styleElement = document.createElement('style'); - styleElement.id = this._styleId; - styleElement.type='text/css'; - styleElement.innerHTML = this._buildStyle(); - document.head.appendChild(styleElement); - } - }; - -// Construct trees style based on user options - _buildStyle () { - let style = '.node-' + this._elementId + '{'; - - // Basic bootstrap style overrides - if (this._options.color) { - style += 'color:' + this._options.color + ';'; - } - - if (this._options.backColor) { - style += 'background-color:' + this._options.backColor + ';'; - } - - if (!this._options.showBorder) { - style += 'border:none;'; - } - else if (this._options.borderColor) { - style += 'border:1px solid ' + this._options.borderColor + ';'; - } - style += '}'; - - if (this._options.onhoverColor) { - style += '.node-' + this._elementId + ':not(.node-disabled):hover{' + - 'background-color:' + this._options.onhoverColor + ';' + - '}'; - } - - // Style search results - if (this._options.highlightSearchResults && (this._options.searchResultColor || this._options.searchResultBackColor)) { - - let innerStyle = '' - if (this._options.searchResultColor) { - innerStyle += 'color:' + this._options.searchResultColor + ';'; - } - if (this._options.searchResultBackColor) { - innerStyle += 'background-color:' + this._options.searchResultBackColor + ';'; - } - - style += '.node-' + this._elementId + '.node-result{' + innerStyle + '}'; - style += '.node-' + this._elementId + '.node-result:hover{' + innerStyle + '}'; - } - - // Style selected nodes - if (this._options.highlightSelected && (this._options.selectedColor || this._options.selectedBackColor)) { - - let innerStyle = '' - if (this._options.selectedColor) { - innerStyle += 'color:' + this._options.selectedColor + ';'; - } - if (this._options.selectedBackColor) { - innerStyle += 'background-color:' + this._options.selectedBackColor + ';'; - } - - style += '.node-' + this._elementId + '.node-selected{' + innerStyle + '}'; - style += '.node-' + this._elementId + '.node-selected:hover{' + innerStyle + '}'; - } - - // Style changed nodes - if (this._options.highlightChanges) { - let innerStyle = 'color: ' + this._options.changedNodeColor + ';'; - style += '.node-' + this._elementId + '.node-check-changed{' + innerStyle + '}'; - } - - // Node level style overrides - this._orderedNodes.forEach((node) => { - if (node.color || node.backColor) { - let innerStyle = ''; - if (node.color) { - innerStyle += 'color:' + node.color + ';'; - } - if (node.backColor) { - innerStyle += 'background-color:' + node.backColor + ';'; - } - style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"]{' + innerStyle + '}'; - } - - if (node.iconColor) { - let innerStyle = 'color:' + node.iconColor + ';'; - style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"] .node-icon{' + innerStyle + '}'; - } - }); - - return this._css + style; - }; - - /** - Returns an array of matching node objects. - @param {String} pattern - A pattern to match against a given field - @return {String} field - Field to query pattern against - */ - findNodes (pattern: string, field: string): BSTreeViewNode[] { - return this._findNodes(pattern, field); - }; - - - /** - Returns an ordered aarray of node objects. - @return {Array} nodes - An array of all nodes - */ - getNodes (): Map { - return this._orderedNodes; - }; - - /** - Returns parent nodes for given nodes, if valid otherwise returns undefined. - @param {Array} nodes - An array of nodes - @returns {Array} nodes - An array of parent nodes - */ - getParents (nodes: BSTreeViewNode[]|BSTreeViewNode): BSTreeViewNode[] { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - let parentNodes = []; - nodes.forEach((node) => { - const parentNode = node.parentId ? this._nodes.get(node.parentId) : false; - if (parentNode) { - parentNodes.push(parentNode); - } - }); - return parentNodes; - }; - - /** - Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined. - @param {Array} nodes - An array of nodes - @returns {Array} nodes - An array of sibling nodes - */ - getSiblings (nodes: BSTreeViewNode[]|BSTreeViewNode): BSTreeViewNode[] { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - let siblingNodes = []; - nodes.forEach((node) => { - let parent = this.getParents([node]); - let nodes = parent[0] ? parent[0].nodes : this._tree; - siblingNodes = nodes.filter(function (obj) { - return obj.nodeId !== node.nodeId; - }); - }); - - // flatten possible nested array before returning - return siblingNodes.map((obj) => { - return obj; - }); - }; - - /** - Returns an array of selected nodes. - @returns {Array} nodes - Selected nodes - */ - getSelected (): BSTreeViewNode[] { - return this._findNodes('true', 'state.selected'); - }; - - /** - Returns an array of unselected nodes. - @returns {Array} nodes - Unselected nodes - */ - getUnselected (): BSTreeViewNode[] { - return this._findNodes('false', 'state.selected'); - }; - - /** - Returns an array of expanded nodes. - @returns {Array} nodes - Expanded nodes - */ - getExpanded (): BSTreeViewNode[] { - return this._findNodes('true', 'state.expanded'); - }; - - /** - Returns an array of collapsed nodes. - @returns {Array} nodes - Collapsed nodes - */ - getCollapsed (): BSTreeViewNode[] { - return this._findNodes('false', 'state.expanded'); - }; - - /** - Returns an array of checked nodes. - @returns {Array} nodes - Checked nodes - */ - getChecked (): BSTreeViewNode[] { - return this._findNodes('true', 'state.checked'); - }; - - /** - Returns an array of unchecked nodes. - @returns {Array} nodes - Unchecked nodes - */ - getUnchecked (): BSTreeViewNode[] { - return this._findNodes('false', 'state.checked'); - }; - - /** - Returns an array of disabled nodes. - @returns {Array} nodes - Disabled nodes - */ - getDisabled (): BSTreeViewNode[] { - return this._findNodes('true', 'state.disabled'); - }; - - /** - Returns an array of enabled nodes. - @returns {Array} nodes - Enabled nodes - */ - getEnabled (): BSTreeViewNode[] { - return this._findNodes('false', 'state.disabled'); - }; - - - /** - Add nodes to the tree. - @param {Array} nodes - An array of nodes to add - @param {optional Object} parentNode - The node to which nodes will be added as children - @param {optional number} index - Zero based insert index - @param {optional Object} options - */ - addNode (nodes: BSTreeViewNode[]|BSTreeViewNode, parentNode: BSTreeViewNode|null = null, index: number = null, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - if (parentNode instanceof Array) { - parentNode = parentNode[0]; - } - - // identify target nodes; either the tree's root or a parent's child nodes - let targetNodes; - if (parentNode && parentNode.nodes) { - targetNodes = parentNode.nodes; - } else if (parentNode) { - targetNodes = parentNode.nodes = []; - } else { - targetNodes = this._tree; - } - - // inserting nodes at specified positions - nodes.forEach((node, i) => { - let insertIndex = (typeof(index) === 'number') ? (index + i) : (targetNodes.length + 1); - targetNodes.splice(insertIndex, 0, node); - }); - - // initialize new state and render changes - this._setInitialStates(new BSTreeViewNode({nodes: this._tree}), 0) - .then(() =>{ - if (parentNode && !parentNode.state.expanded) { - this._setExpanded(parentNode, true, options); - } - this._render(); - }); - } - - /** - Add nodes to the tree after given node. - @param {Array} nodes - An array of nodes to add - @param {Object} node - The node to which nodes will be added after - @param {optional Object} options - */ - addNodeAfter (nodes: BSTreeViewNode[]|BSTreeViewNode, node: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - if (node instanceof Array) { - node = node[0]; - } - - this.addNode(nodes, this.getParents(node)[0], (node.index + 1), options); - } - - /** - Add nodes to the tree before given node. - @param {Array} nodes - An array of nodes to add - @param {Object} node - The node to which nodes will be added before - @param {optional Object} options - */ - addNodeBefore (nodes: BSTreeViewNode[]|BSTreeViewNode, node: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - if (node instanceof Array) { - node = node[0]; - } - - this.addNode(nodes, this.getParents(node)[0], node.index, options); - } - - /** - Removes given nodes from the tree. - @param {Array} nodes - An array of nodes to remove - @param {optional Object} options - */ - removeNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - let targetNodes: BSTreeViewNode[]; - let parentNode: BSTreeViewNode; - nodes.forEach((node) => { - - // remove nodes from tree - parentNode = this._nodes.get(node.parentId); - if (parentNode) { - targetNodes = parentNode.nodes; - } else { - targetNodes = this._tree; - } - targetNodes.splice(node.index, 1); - - // remove node from DOM - this._removeNodeEl(node); - }); - - // initialize new state and render changes - this._setInitialStates(new BSTreeViewNode({nodes: this._tree}), 0) - .then(this._render.bind(this)); - }; - - /** - Updates / replaces a given tree node - @param {Object} node - A single node to be replaced - @param {Object} newNode - THe replacement node - @param {optional Object} options - */ - updateNode (node: BSTreeViewNode, newNode: BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()) { - if (node instanceof Array) { - node = node[0]; - } - - // insert new node - let targetNodes; - const parentNode = this._nodes.get(node.parentId); - if (parentNode) { - targetNodes = parentNode.nodes; - } else { - targetNodes = this._tree; - } - targetNodes.splice(node.index, 1, newNode); - - // remove old node from DOM - this._removeNodeEl(node); - - // initialize new state and render changes - this._setInitialStates(new BSTreeViewNode({nodes: this._tree}), 0) - .then(this._render.bind(this)); - }; - - - /** - Selects given tree nodes - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - selectNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewSelectOptions = new BSTreeViewSelectOptions()) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._setSelected(node, true, options); - }); - }; - - /** - Unselects given tree nodes - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - unselectNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewSelectOptions) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - - nodes.forEach((node) => { - this._setSelected(node, false, options); - }); - }; - - /** - Toggles a node selected state; selecting if unselected, unselecting if selected. - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - toggleNodeSelected (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewSelectOptions = new BSTreeViewSelectOptions()) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._toggleSelected(node, options); - }, this); - }; - - - /** - Collapse all tree nodes - @param {optional Object} options - */ - collapseAll (options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()) { - options.levels = options.levels || 999; - this.collapseNode(this._tree, options); - }; - - /** - Collapse a given tree node - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - collapseNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._setExpanded(node, false, options); - }); - }; - - /** - Expand all tree nodes - @param {optional Object} options - */ - expandAll (options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { - options.levels = options.levels || 999; - this.expandNode(this._tree, options); - }; - - /** - Expand given tree nodes - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - expandNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - // Do not re-expand already expanded nodes - if (node.state.expanded) return; - - if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) { - this._lazyLoad(node); - } - - this._setExpanded(node, true, options); - if (node.nodes) { - this._expandLevels(node.nodes, options.levels-1, options); - } - }); - }; - - _expandLevels (nodes: BSTreeViewNode[]|BSTreeViewNode, level: number, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._setExpanded(node, (level > 0), options); - if (node.nodes) { - this._expandLevels(node.nodes, level-1, options); - } - }); - }; - - /** - Reveals given tree nodes, expanding the tree from node to root. - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - revealNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - let parentNode = node; - let tmpNode; - while (tmpNode = this.getParents([parentNode])[0]) { - parentNode = tmpNode; - this._setExpanded(parentNode, true, options); - } - }); - }; - - /** - Toggles a node's expanded state; collapsing if expanded, expanding if collapsed. - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - toggleNodeExpanded (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewExpandOptions = new BSTreeViewExpandOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._toggleExpanded(node, options); - }); - - }; - - - /** - Check all tree nodes - @param {optional Object} options - */ - checkAll (options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { - this._orderedNodes.forEach((node) => { - if(!node.state.checked) { - this._setChecked(node, true, options); - } - }); - }; - - /** - Checks given tree nodes - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - checkNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._setChecked(node, true, options); - }); - }; - - /** - Uncheck all tree nodes - @param {optional Object} options - */ - uncheckAll (options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { - this._orderedNodes.forEach((node) => { - if(node.state.checked || node.state.checked === undefined) { - this._setChecked(node, false, options); - } - }); - }; - - /** - Uncheck given tree nodes - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - uncheckNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._setChecked(node, false, options); - }); - }; - - /** - Toggles a node's checked state; checking if unchecked, unchecking if checked. - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - toggleNodeChecked (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewEventOptions = new BSTreeViewEventOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._toggleChecked(node, options); - }); - }; - - /** - Saves the current state of checkboxes as default, cleaning up any highlighted changes - */ - unmarkCheckboxChanges (): void { - this._inheritCheckboxChanges(); - - this._nodes.forEach((node) => { - node.el.classList.remove('node-check-changed'); - }); - }; - - /** - Disable all tree nodes - @param {optional Object} options - */ - disableAll (options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()): void { - const nodes = this._findNodes('false', 'state.disabled'); - nodes.forEach((node) => { - this._setDisabled(node,true, options); - }); - }; - - /** - Disable given tree nodes - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - disableNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._setDisabled(node, true, options); - }); - }; - - /** - Enable all tree nodes - @param {optional Object} options - */ - enableAll (options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()): void { - const nodes = this._findNodes('true', 'state.disabled'); - - nodes.forEach((node) => { - this._setDisabled(node, false, options); - }); - }; - - /** - Enable given tree nodes - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - enableNode (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()) { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._setDisabled(node, false, options); - }) - }; - - /** - Toggles a node's disabled state; disabling is enabled, enabling if disabled. - @param {Array} nodes - An array of nodes - @param {optional Object} options - */ - toggleNodeDisabled (nodes: BSTreeViewNode[]|BSTreeViewNode, options: BSTreeViewDisableOptions = new BSTreeViewDisableOptions()): void { - if (!(nodes instanceof Array)) { - nodes = [nodes]; - } - - nodes.forEach((node) => { - this._setDisabled(node, !node.state.disabled, options); - }) - }; - - - /** - Searches the tree for nodes (text) that match given criteria - @param {String} pattern - A given string to match against - @param {optional Object} options - Search criteria options - @return {Array} nodes - Matching nodes - */ - search (pattern: string, options: BSTreeSearchOptions = new BSTreeSearchOptions()): BSTreeViewNode[] { - let previous = this._getSearchResults(); - let results = []; - - if (pattern && pattern.length > 0) { - - if (options.exactMatch) { - pattern = '^' + pattern + '$'; - } - - let modifier = 'g'; - if (options.ignoreCase) { - modifier += 'i'; - } - - results = this._findNodes(pattern, 'text', modifier); - } - - // Clear previous results no longer matched - this._diffArray(results, previous).forEach((node) => { - this._setSearchResult(node, false, options); - }); - - // Set new results - this._diffArray(previous, results).forEach((node) => { - this._setSearchResult(node, true, options); - }); - - // Reveal hidden nodes - if (results && options.revealResults) { - this.revealNode(results); - } - - this._triggerEvent(EVENT_SEARCH_COMPLETED, results, options); - - return results; - }; - - /** - Clears previous search results - */ - clearSearch (options: BSTreeSearchOptions = new BSTreeSearchOptions()): void { - const results = this._getSearchResults(); - results.forEach((node) => { - this._setSearchResult(node, false, options); - }); - - this._triggerEvent(EVENT_SEARCH_CLEARED, results, options); - }; - - _getSearchResults (): BSTreeViewNode[] { - return this._findNodes('true', 'searchResult'); - }; - - _diffArray (a: Array, b: Array) { - let diff: Array = []; - - b.forEach((n) => { - if (a.indexOf(n) === -1) { - diff.push(n); - } - }); - return diff; - }; - - /** - Find nodes that match a given criteria - @param {String} pattern - A given string to match against - @param {optional String} attribute - Attribute to compare pattern against - @param {optional String} modifier - Valid RegEx modifiers - @return {Array} nodes - Nodes that match your criteria - */ - _findNodes (pattern: string, attribute: string = 'text', modifier: string = 'g'): BSTreeViewNode[] { - - let tmp = []; - - this._orderedNodes.forEach((node) => { - const val = this._getNodeValue(node, attribute); - if (typeof val === 'string') { - if(val.match(new RegExp(pattern, modifier))) { - tmp.push(node); - } - } - }); - - return tmp; - }; - - /** - Recursive find for retrieving nested attributes values - All values are return as strings, unless invalid - @param {Object} obj - Typically a node, could be any object - @param {String} attr - Identifies an object property using dot notation - @return {String} value - Matching attributes string representation - */ - _getNodeValue (obj: object, attr: string): string { - const index = attr.indexOf('.'); - if (index > 0) { - const _obj = obj[attr.substring(0, index)]; - const _attr = attr.substring(index + 1, attr.length); - return this._getNodeValue(_obj, _attr); - } - else { - if (obj.hasOwnProperty(attr) && obj[attr] !== undefined) { - return obj[attr].toString(); - } - else { - return undefined; - } - } - }; - -} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewDisableOptions.ts b/assets/ts_src/BSTreeViewDisableOptions.ts deleted file mode 100644 index f74773cb..00000000 --- a/assets/ts_src/BSTreeViewDisableOptions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; - - -export default class BSTreeViewDisableOptions extends BSTreeViewEventOptions -{ - unselecting: boolean; - keepState: boolean; -} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewEventOptions.ts b/assets/ts_src/BSTreeViewEventOptions.ts deleted file mode 100644 index 3c268257..00000000 --- a/assets/ts_src/BSTreeViewEventOptions.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default class BSTreeViewEventOptions { - silent: boolean = false; - ignoreChildren: boolean = false; - - lazyLoad: boolean = false; - - constructor(options: BSTreeViewEventOptions|object = null) { - if(options) { - Object.assign(this, options); - } - } -} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewExpandOptions.ts b/assets/ts_src/BSTreeViewExpandOptions.ts deleted file mode 100644 index e1f8bb6e..00000000 --- a/assets/ts_src/BSTreeViewExpandOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; - - -export default class BSTreeViewExpandOptions extends BSTreeViewEventOptions -{ - levels: number = 999; -} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewNode.ts b/assets/ts_src/BSTreeViewNode.ts deleted file mode 100644 index 8ed3cd6e..00000000 --- a/assets/ts_src/BSTreeViewNode.ts +++ /dev/null @@ -1,45 +0,0 @@ -import BSTreeViewNodeState from "./BSTreeViewNodeState"; -import BSTreeViewOptions from "./BSTreeViewOptions"; - -export default class BSTreeViewNode { - text: string; - icon: string; - image: string; - selectedIcon: string; - color: string; - backColor: string; - iconColor: string; - iconBackground: string; - selectable: boolean; - checkable: boolean; - state: BSTreeViewNodeState; - tags: string[]; - dataAttr: object; - id: string; - class: string; - hideCheckbox: boolean; - nodes: BSTreeViewNode[]; - tooltip: string; - href: string; - - lazyLoad: boolean; - tagsClass: string; - - - el: HTMLElement; - - searchResult: boolean; - - - level: number; - index: number; - nodeId: string; - parentId: string - - constructor(options: BSTreeViewNode|object = null) { - if(options) { - Object.assign(this, options); - } - } -} - diff --git a/assets/ts_src/BSTreeViewNodeState.ts b/assets/ts_src/BSTreeViewNodeState.ts deleted file mode 100644 index 216fc126..00000000 --- a/assets/ts_src/BSTreeViewNodeState.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default class BSTreeViewNodeState { - checked: boolean|null; - disabled: boolean = false; - expanded: boolean; - selected: boolean; - - visible: boolean; -} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewOptions.ts b/assets/ts_src/BSTreeViewOptions.ts deleted file mode 100644 index 99d72151..00000000 --- a/assets/ts_src/BSTreeViewOptions.ts +++ /dev/null @@ -1,78 +0,0 @@ -import BSTreeViewNode from "./BSTreeViewNode"; - -export default class BSTreeViewOptions { - injectStyle: boolean = true; - - levels: number = 2; - - data: BSTreeViewNode[]|string = null; - ajaxURL: string = null; - ajaxConfig: RequestInit = { - method: "GET", - }; - - expandIcon: string = 'glyphicon glyphicon-plus'; - collapseIcon: string = 'glyphicon glyphicon-minus'; - loadingIcon: string = 'glyphicon glyphicon-hourglass'; - emptyIcon: string = 'glyphicon'; - nodeIcon: string = ''; - selectedIcon: string = ''; - checkedIcon: string = 'glyphicon glyphicon-check'; - partiallyCheckedIcon: string = 'glyphicon glyphicon-expand'; - uncheckedIcon: string = 'glyphicon glyphicon-unchecked'; - tagsClass: string = 'badge'; - - color: string = undefined; - backColor: string = undefined; - borderColor: string = undefined; - changedNodeColor: string = '#39A5DC'; - onhoverColor: string = '#F5F5F5'; - selectedColor: string = '#FFFFFF'; - selectedBackColor: string = '#428bca'; - searchResultColor: string = '#D9534F'; - searchResultBackColor: string = undefined; - - highlightSelected: boolean = true; - highlightSearchResults: boolean = true; - showBorder: boolean = true; - showIcon: boolean = true; - showImage: boolean = false; - showCheckbox: boolean = false; - checkboxFirst: boolean = false; - highlightChanges: boolean = false; - showTags: boolean = false; - multiSelect: boolean = false; - preventUnselect: boolean = false; - allowReselect: boolean = false; - hierarchicalCheck: boolean = false; - propagateCheckEvent: boolean = false; - wrapNodeText: boolean = false; - - // Event handlers - onLoading: (event: Event) => void = undefined; - onLoadingFailed: (event: Event) => void = undefined; - onInitialized: (event: Event) => void = undefined; - onNodeRendered: (event: Event) => void = undefined; - onRendered: (event: Event) => void = undefined; - onDestroyed: (event: Event) => void = undefined; - - onNodeChecked: (event: Event) => void = undefined; - onNodeCollapsed: (event: Event) => void = undefined; - onNodeDisabled: (event: Event) => void = undefined; - onNodeEnabled: (event: Event) => void = undefined; - onNodeExpanded: (event: Event) => void = undefined; - onNodeSelected: (event: Event) => void = undefined; - onNodeUnchecked: (event: Event) => void = undefined; - onNodeUnselected: (event: Event) => void = undefined; - - onSearchComplete: (event: Event) => void = undefined; - onSearchCleared: (event: Event) => void = undefined; - - lazyLoad: (node: BSTreeViewNode, renderer: (nodes: BSTreeViewNode[]) => void) => void = undefined; - - constructor(options: BSTreeViewOptions|object = null) { - if(options) { - Object.assign(this, options); - } - } -} \ No newline at end of file diff --git a/assets/ts_src/BSTreeViewSelectOptions.ts b/assets/ts_src/BSTreeViewSelectOptions.ts deleted file mode 100644 index f110c43e..00000000 --- a/assets/ts_src/BSTreeViewSelectOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import BSTreeViewEventOptions from "./BSTreeViewEventOptions"; - - -export default class BSTreeViewSelectOptions extends BSTreeViewEventOptions -{ - unselecting: boolean; -} \ No newline at end of file From 461de131c6eef70a3a23e105204c872775ebf056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 7 Aug 2022 01:55:44 +0200 Subject: [PATCH 4/9] Migrated tree logic to new @jbtronics/bs-treeview package- --- .../controllers/elements/tree_controller.js | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/assets/controllers/elements/tree_controller.js b/assets/controllers/elements/tree_controller.js index f5f1bcb2..8e4455a3 100644 --- a/assets/controllers/elements/tree_controller.js +++ b/assets/controllers/elements/tree_controller.js @@ -3,7 +3,7 @@ import {Controller} from "@hotwired/stimulus"; import "../../js/lib/bootstrap-treeview/src/css/bootstrap-treeview.css" //import "../../js/lib/bootstrap-treeview/src/js/bootstrap-treeview.js" -import BSTreeView from "../../ts_src/BSTreeView"; +import {BSTreeView} from "@jbtronics/bs-treeview"; export default class extends Controller { static targets = [ "tree" ]; @@ -11,6 +11,8 @@ export default class extends Controller { _url = null; _data = null; + _tree = null; + connect() { const treeElement = this.treeTarget; if (!treeElement) { @@ -48,7 +50,7 @@ export default class extends Controller { //Get primary color from css variable const primary_color = getComputedStyle(document.documentElement).getPropertyValue('--bs-warning'); - const tree = new BSTreeView(this.treeTarget, { + this._tree = new BSTreeView(this.treeTarget, { data: data, enableLinks: true, showIcon: false, @@ -82,11 +84,11 @@ export default class extends Controller { } collapseAll() { - $(this.treeTarget).treeview('collapseAll', {silent: true}); + this._tree.collapseAll({silent: true}); } expandAll() { - $(this.treeTarget).treeview('expandAll', {silent: true}); + this._tree.expandAll({silent: true}); } searchInput(event) { @@ -94,8 +96,8 @@ export default class extends Controller { //Do nothing if no data was passed const tree = this.treeTarget; - $(tree).treeview('collapseAll', {silent: true}); - $(tree).treeview('search', [data]); + this._tree.collapseAll({silent: true}); + this._tree.search([data]); } /** @@ -103,15 +105,7 @@ export default class extends Controller { * @private */ _isInitialized() { - const $tree = $(this.treeTarget).treeview(true); - - //If the tree is not initialized yet, we just get an empty jquery object with the treeview functions missing - if(typeof $tree.findNodes === 'undefined' ) { - return false; - } - - return true; - + return this._tree !== null; } _getData() { From 007df80b1cfd9052e830a5b1338738592fb6b9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 13 Aug 2022 00:28:03 +0200 Subject: [PATCH 5/9] Use @jbtronics/bs-treeview instead of patternfy-bootstrap-treeview --- .../controllers/elements/tree_controller.js | 52 ++--- package.json | 2 +- yarn.lock | 196 ++++++++---------- 3 files changed, 115 insertions(+), 135 deletions(-) diff --git a/assets/controllers/elements/tree_controller.js b/assets/controllers/elements/tree_controller.js index 8e4455a3..535d450a 100644 --- a/assets/controllers/elements/tree_controller.js +++ b/assets/controllers/elements/tree_controller.js @@ -1,9 +1,7 @@ import {Controller} from "@hotwired/stimulus"; -import "../../js/lib/bootstrap-treeview/src/css/bootstrap-treeview.css" -//import "../../js/lib/bootstrap-treeview/src/js/bootstrap-treeview.js" - -import {BSTreeView} from "@jbtronics/bs-treeview"; +import {BSTreeView, BS5Theme, FAIconTheme, EVENT_INITIALIZED} from "@jbtronics/bs-treeview"; +import "@jbtronics/bs-treeview/styles/bs-treeview.css"; export default class extends Controller { static targets = [ "tree" ]; @@ -11,6 +9,10 @@ export default class extends Controller { _url = null; _data = null; + /** + * @type {BSTreeView} + * @private + */ _tree = null; connect() { @@ -31,9 +33,9 @@ export default class extends Controller { //Fetch data and initialize tree this._getData() .then(this._fillTree.bind(this)) - /*.catch((err) => { + .catch((err) => { console.error("Could not load the tree data: " + err); - });*/ + }); } setData(data) { @@ -47,23 +49,22 @@ export default class extends Controller { } _fillTree(data) { - //Get primary color from css variable - const primary_color = getComputedStyle(document.documentElement).getPropertyValue('--bs-warning'); + if(this._tree) { + this._tree.remove(); + } this._tree = new BSTreeView(this.treeTarget, { + levels: 1, + //showTags: true, data: data, - enableLinks: true, showIcon: false, - showBorder: true, - searchResultBackColor: primary_color, - searchResultColor: '#000', onNodeSelected: function (event) { - const data = event.detail.data; - if (data.href) { + const node = event.detail.node; + if (node.href) { //Simulate a click so we just change the inner frame let a = document.createElement('a'); - a.setAttribute('href', data.href); + a.setAttribute('href', node.href); a.innerHTML = ""; document.body.appendChild(a); a.click(); @@ -71,16 +72,14 @@ export default class extends Controller { } }, //onNodeContextmenu: contextmenu_handler, - expandIcon: "fas fa-plus fa-fw fa-treeview", - collapseIcon: "fas fa-minus fa-fw fa-treeview" - }) - /*.on('initialized', function () { - //Collapse all nodes after init - $(this).treeview('collapseAll', {silent: true}); + }, [BS5Theme, FAIconTheme]); + + this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => { + const treeView = event.detail.treeView; + treeView.revealNode(treeView.getSelected()); + }); + - //Reveal the selected ones - $(this).treeview('revealNode', [$(this).treeview('getSelected')]); - });*/ } collapseAll() { @@ -97,7 +96,10 @@ export default class extends Controller { const tree = this.treeTarget; this._tree.collapseAll({silent: true}); - this._tree.search([data]); + this._tree.search(data); + + //Rereveal the selected node again + this._tree.revealNode(this._tree.getSelected()); } /** diff --git a/package.json b/package.json index ca587e95..aea5ebc5 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@ckeditor/ckeditor5-upload": "^34.2.0", "@ckeditor/ckeditor5-watchdog": "^34.2.0", "@ckeditor/ckeditor5-word-count": "^34.2.0", + "@jbtronics/bs-treeview": "^1.0.1", "@zxing/library": "^0.19.1", "bootbox": "^5.4.0", "bootstrap-select": "v1.14.0-beta3", @@ -78,7 +79,6 @@ "jszip": "^3.2.0", "katex": "^0.16.0", "marked": "^4.0.3", - "patternfly-bootstrap-treeview": "^2.1.8", "pdfmake": "^0.2.2", "stimulus-use": "^0.50.0", "tom-select": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 7859bcdb..7ee8a041 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,9 +44,9 @@ semver "^6.3.0" "@babel/generator@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.10.tgz#794f328bfabdcbaf0ebf9bf91b5b57b61fa77a2a" - integrity sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA== + version "7.18.12" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.12.tgz#fa58daa303757bd6f5e4bbca91b342040463d9f4" + integrity sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg== dependencies: "@babel/types" "^7.18.10" "@jridgewell/gen-mapping" "^0.3.2" @@ -235,13 +235,13 @@ integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== "@babel/helper-wrap-function@^7.18.9": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.18.10.tgz#a7fcd3ab9b1be4c9b52cf7d7fdc1e88c2ce93396" - integrity sha512-95NLBP59VWdfK2lyLKe6eTMq9xg+yWKzxzxbJ1wcYNi1Auz200+83fMDADjRxBvc2QQor5zja2yTQzXGhk2GtQ== + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz#bff23ace436e3f6aefb61f85ffae2291c80ed1fb" + integrity sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w== dependencies: "@babel/helper-function-name" "^7.18.9" "@babel/template" "^7.18.10" - "@babel/traverse" "^7.18.10" + "@babel/traverse" "^7.18.11" "@babel/types" "^7.18.10" "@babel/helpers@^7.18.9": @@ -262,10 +262,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.18.10", "@babel/parser@^7.18.9": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.10.tgz#94b5f8522356e69e8277276adf67ed280c90ecc1" - integrity sha512-TYk3OA0HKL6qNryUayb5UUEhM/rkOQozIBEA5ITXh5DWrSp0TlUQXMyZmnWxG/DizSWBeeQ0Zbc5z8UGaaqoeg== +"@babel/parser@^7.18.10", "@babel/parser@^7.18.11", "@babel/parser@^7.18.9": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" + integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" @@ -877,10 +877,10 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" -"@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.10.tgz#37ad97d1cb00efa869b91dd5d1950f8a6cf0cb08" - integrity sha512-J7ycxg0/K9XCtLyHf0cz2DqDihonJeIo+z+HEdRe9YuT8TY4A66i+Ab2/xZCEW7Ro60bPCBBfqqboHSamoV3+g== +"@babel/traverse@^7.18.10", "@babel/traverse@^7.18.11", "@babel/traverse@^7.18.9": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.11.tgz#3d51f2afbd83ecf9912bcbb5c4d94e3d2ddaa16f" + integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== dependencies: "@babel/code-frame" "^7.18.6" "@babel/generator" "^7.18.10" @@ -888,7 +888,7 @@ "@babel/helper-function-name" "^7.18.9" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.18.10" + "@babel/parser" "^7.18.11" "@babel/types" "^7.18.10" debug "^4.1.0" globals "^11.1.0" @@ -958,14 +958,14 @@ "@ckeditor/ckeditor5-utils" "^34.2.0" lodash-es "^4.17.15" -"@ckeditor/ckeditor5-dev-utils@^30.3.1", "@ckeditor/ckeditor5-dev-utils@^30.3.5": - version "30.3.5" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-dev-utils/-/ckeditor5-dev-utils-30.3.5.tgz#7c4592ed37e1f7d87fb4187cd0a15266b4ab2db9" - integrity sha512-SQABOw+vda+IASTzAuZVMu8cZomvJ/Qk4QIdMJ6coyh29SR2pve/RiqcyaJucpI3W6ypInvwDp0QClrZqsO0LA== +"@ckeditor/ckeditor5-dev-utils@^30.3.1", "@ckeditor/ckeditor5-dev-utils@^30.4.0": + version "30.4.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-dev-utils/-/ckeditor5-dev-utils-30.4.0.tgz#8165169fbe9fba2533034663b28a2eb54a77f946" + integrity sha512-FCuRQ5MzrJeWWKUULil6iI+ylzI0kZ2PqXBME66KZS9KIE7UEKabSAAJGmtc7xFCuGmbik6g2uAk3J0t+PWHjQ== dependencies: "@babel/parser" "^7.18.9" "@babel/traverse" "^7.18.9" - "@ckeditor/ckeditor5-dev-webpack-plugin" "^30.3.5" + "@ckeditor/ckeditor5-dev-webpack-plugin" "^30.4.0" chalk "^3.0.0" cli-cursor "^3.1.0" cli-spinners "^2.6.1" @@ -988,12 +988,12 @@ through2 "^3.0.1" ts-loader "^9.3.0" -"@ckeditor/ckeditor5-dev-webpack-plugin@^30.3.1", "@ckeditor/ckeditor5-dev-webpack-plugin@^30.3.5": - version "30.3.5" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-dev-webpack-plugin/-/ckeditor5-dev-webpack-plugin-30.3.5.tgz#5c5de1b633d0471bd50c69c2c1a476037c198a38" - integrity sha512-CsH4fSRLYJ1YeraetX+JjazP7vcCLrRBH2+VxKuk+VoEZ09dAHCV2jg2SNdAucAK8OQQX6PWguY0gQWv8fsr6g== +"@ckeditor/ckeditor5-dev-webpack-plugin@^30.3.1", "@ckeditor/ckeditor5-dev-webpack-plugin@^30.4.0": + version "30.4.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-dev-webpack-plugin/-/ckeditor5-dev-webpack-plugin-30.4.0.tgz#d2b46a5be3655bae199ba2adcb79cc47adba952b" + integrity sha512-wXux5YwysXvBw7ynBdt7M5c/0l2PRIpj/wTMK3yHSWo2pQAWMp76O3YNDbh2TdSSyNtFNj6OjCHd7e06lloL9w== dependencies: - "@ckeditor/ckeditor5-dev-utils" "^30.3.5" + "@ckeditor/ckeditor5-dev-utils" "^30.4.0" chalk "^4.0.0" rimraf "^3.0.2" semver "^7.3.4" @@ -1344,6 +1344,11 @@ resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.1.0.tgz#27e44e0e3dc5bd1d4bda0766d579cf5a14091cd7" integrity sha512-Q8kGjqwPqER+CtpQudbH+3Zgs2X4zb6pBAlr6NsKTXadg45pAOvxI9i4QpuHbwSzR2+x87HUm+rot9F/Pe8rxA== +"@jbtronics/bs-treeview@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@jbtronics/bs-treeview/-/bs-treeview-1.0.2.tgz#139e43eb81e9a03099e577975721b9cac0a420de" + integrity sha512-xviDoNNkZbgg5kmluNibiAPF39sPBz4WYcPs7IX6U2C0jYbomyEtwzLAz0GXDjB/w1z1C2Gp3EkivfRxpRHSnQ== + "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" @@ -1384,10 +1389,10 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.14" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" - integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== +"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -1455,9 +1460,9 @@ integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== "@popperjs/core@^2.10.2": - version "2.11.5" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" - integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== + version "2.11.6" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" + integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== "@symfony/stimulus-bridge@^3.2.0": version "3.2.1" @@ -1609,9 +1614,9 @@ integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/mime@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.0.tgz#e9a9903894405c6a6551f1774df4e64d9804d69c" - integrity sha512-fccbsHKqFDXClBZTDLA43zl0+TbxyIwyzIzwwhvoJvhNjOErCdeX2xJbURimv2EbSVUGav001PaCJg4mZxMl4w== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== "@types/minimatch@*": version "3.0.5" @@ -1619,9 +1624,9 @@ integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== "@types/node@*": - version "18.6.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.3.tgz#4e4a95b6fe44014563ceb514b2598b3e623d1c98" - integrity sha512-6qKpDtoaYLM+5+AFChLhHermMQxc3TOEFIDzrZLPRGHPrLEwqFkkT5Kx3ju05g6X7uDPazz3jHbKPX0KzCjntg== + version "18.7.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.2.tgz#22306626110c459aedd2cdf131c749ec781e3b34" + integrity sha512-ce7MIiaYWCFv6A83oEultwhBXb22fxwNOQf5DIxWA4WXvDQ7K+L0fbWl/YOfCzlR5B/uFkSnVBhPcOfOECcWvA== "@types/parse-json@^4.0.0": version "4.0.0" @@ -2162,30 +2167,16 @@ bootbox@^5.4.0: resolved "https://registry.yarnpkg.com/bootbox/-/bootbox-5.5.3.tgz#6b32da9c401a22b089e1544797cc9a91f1c35b23" integrity sha512-B4mnm1DYgNHzoNtD7I0L/fixqvya4EEQy5bFF/yNmGI2Eq3WwVVwdfWf3hoF8KS+EaV4f0uIMqtxB1EAZwZPhQ== -bootstrap-fileinput@^5.0.1: - version "5.5.0" - resolved "https://registry.yarnpkg.com/bootstrap-fileinput/-/bootstrap-fileinput-5.5.0.tgz#6ec1dee4c9a4177a4cea76ca422b8bea7b4da624" - integrity sha512-ZwDoW4Ws8WCxAOpQqLqNaJuCnwKtATUPW1R/rl1O2LvF0UIoEsXHFy4P1G+25SIQX0k59iQ4sJm3XquiY9SXOA== - dependencies: - bootstrap ">= 3.4.1" - jquery ">= 1.9.0" - opencollective-postinstall "^2.0.2" - bootstrap-select@v1.14.0-beta3: version "1.14.0-beta3" resolved "https://registry.yarnpkg.com/bootstrap-select/-/bootstrap-select-1.14.0-beta3.tgz#dc15083fe51d0ac7b38a3b99dfd492dcc0a783b0" integrity sha512-wYUDY4NAYBcNydXybE7wh3+ucyf+AcUOhZ+e0TFIoZ38A+k/3BVT1RPl5f0CiPxAexP1IQuqALKMqI8wtZS71A== -"bootstrap@>= 3.4.1", bootstrap@^5.1.3: +bootstrap@^5.1.3: version "5.2.0" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.0.tgz#838727fb60f1630db370fe57c63cbcf2962bb3d3" integrity sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A== -bootstrap@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.4.1.tgz#c3a347d419e289ad11f4033e3c4132b87c081d72" - integrity sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA== - bootswatch@^5.1.3: version "5.2.0" resolved "https://registry.yarnpkg.com/bootswatch/-/bootswatch-5.2.0.tgz#c02a0d84e0382552f8a7b9bdd055f36b758ffed9" @@ -2337,9 +2328,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001370: - version "1.0.30001373" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001373.tgz#2dc3bc3bfcb5d5a929bec11300883040d7b4b4be" - integrity sha512-pJYArGHrPp3TUqQzFYRmP/lwJlj8RCbVe3Gd3eJQkAV8SAC6b19XS9BjMvRdvaS8RMkaTN8ZhoHP6S1y8zzwEQ== + version "1.0.30001375" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz#8e73bc3d1a4c800beb39f3163bf0190d7e5d7672" + integrity sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw== chalk@^2.0.0, chalk@^2.3.2: version "2.4.2" @@ -2481,9 +2472,9 @@ color-name@~1.1.4: integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colord@^2.9.1: - version "2.9.2" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" - integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== colorette@^2.0.10, colorette@^2.0.14: version "2.0.19" @@ -2762,9 +2753,9 @@ cssnano-utils@^3.1.0: integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== cssnano@^5.0.0, cssnano@^5.1.8: - version "5.1.12" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.12.tgz#bcd0b64d6be8692de79332c501daa7ece969816c" - integrity sha512-TgvArbEZu0lk/dvg2ja+B7kYoD7BBCmn3+k58xD0qjrGHsFzXY/wKTo9M5egcUCabPol05e/PVoIu79s2JN4WQ== + version "5.1.13" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.13.tgz#83d0926e72955332dc4802a7070296e6258efc0a" + integrity sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ== dependencies: cssnano-preset-default "^5.2.12" lilconfig "^2.0.3" @@ -2969,7 +2960,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3: +define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== @@ -3125,9 +3116,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.4.202: - version "1.4.210" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.210.tgz#12611fe874b833a3bf3671438b5893aba7858980" - integrity sha512-kSiX4tuyZijV7Cz0MWVmGT8K2siqaOA4Z66K5dCttPPRh0HicOcOAEj1KlC8O8J1aOS/1M8rGofOzksLKaHWcQ== + version "1.4.218" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.218.tgz#d6b817b5454499a92c85888b42dc2ad075e4493a" + integrity sha512-INDylKH//YIf2w67D+IjkfVnGVrZ/D94DAU/FPPm6T4jEPbEDQvo9r2wTj0ncFdtJH8+V8BggZTaN8Rzk5wkgw== emoji-regex@^8.0.0: version "8.0.0" @@ -3750,7 +3741,7 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" -has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: +has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== @@ -3998,9 +3989,9 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-core-module@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + version "2.10.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== dependencies: has "^1.0.3" @@ -4142,7 +4133,7 @@ jest-worker@^27.4.5, jest-worker@^27.5.1: merge-stream "^2.0.0" supports-color "^8.0.0" -"jquery@>= 1.9.0", jquery@>=1.11, jquery@>=1.7, jquery@^3.4.1, jquery@^3.5.1: +jquery@>=1.11, jquery@>=1.7, jquery@^3.5.1: version "3.6.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== @@ -4621,13 +4612,13 @@ object-keys@^1.1.1: integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + version "4.1.3" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.3.tgz#d36b7700ddf0019abb6b1df1bb13f6445f79051f" + integrity sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA== dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" object-keys "^1.1.1" obuf@^1.0.0, obuf@^1.1.2: @@ -4670,11 +4661,6 @@ open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" -opencollective-postinstall@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" - integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== - opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" @@ -4829,14 +4815,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -patternfly-bootstrap-treeview@^2.1.8: - version "2.1.10" - resolved "https://registry.yarnpkg.com/patternfly-bootstrap-treeview/-/patternfly-bootstrap-treeview-2.1.10.tgz#f96043734bb4ecac951783e2745e3f9a8873ffd4" - integrity sha512-P9+iFu34CwX+R5Fd7/EWbxTug0q9mDj53PnZIIh5ie54KX2kD0+54lCWtpD9SVylDwDtDv3n3A6gbFVkx7HsuA== - dependencies: - bootstrap "^3.4.1" - jquery "^3.4.1" - pdfmake@^0.2.2: version "0.2.5" resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.2.5.tgz#48b17670d69dae3860a5d8721ff12f7988140613" @@ -5197,9 +5175,9 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@^8.2.14, postcss@^8.4.12, postcss@^8.4.13, postcss@^8.4.7: - version "8.4.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" - integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== + version "8.4.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" + integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== dependencies: nanoid "^3.3.4" picocolors "^1.0.0" @@ -6033,17 +6011,17 @@ terser-webpack-plugin@^4.2.3: webpack-sources "^1.4.3" terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.3.0: - version "5.3.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz#8033db876dd5875487213e87c627bca323e5ed90" - integrity sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ== + version "5.3.4" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.4.tgz#f4d31e265883d20fda3ca9c0fc6a53f173ae62e3" + integrity sha512-SmnkUhBxLDcBfTIeaq+ZqJXLVEyXxSaNcCeSezECdKjfkMrTTnPvapBILylYwyEvHFZAn2cJ8dtiXel5XnfOfQ== dependencies: - "@jridgewell/trace-mapping" "^0.3.7" + "@jridgewell/trace-mapping" "^0.3.14" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.0" - terser "^5.7.2" + terser "^5.14.1" -terser@^5.3.4, terser@^5.7.2: +terser@^5.14.1, terser@^5.3.4: version "5.14.2" resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== @@ -6189,9 +6167,9 @@ type@^1.0.1: integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== type@^2.5.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.7.0.tgz#aaff4ac90514e0dc1095b54af70505ef16cf00a2" - integrity sha512-NybX0NBIssNEj1efLf1mqKAtO4Q/Np5mqpa57be81ud7/tNHIXn48FDVXiyGMBF90FfXc5o7RPsuRQrPzgMOMA== + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== typedarray@^0.0.6: version "0.0.6" @@ -6395,9 +6373,9 @@ webpack-dev-middleware@^5.3.1: schema-utils "^4.0.0" webpack-dev-server@^4.8.0: - version "4.9.3" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.3.tgz#2360a5d6d532acb5410a668417ad549ee3b8a3c9" - integrity sha512-3qp/eoboZG5/6QgiZ3llN8TUzkSpYg1Ko9khWX1h40MIEUNS2mDoIa8aXsPfskER+GbTvs/IJZ1QTBBhhuetSw== + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.10.0.tgz#de270d0009eba050546912be90116e7fd740a9ca" + integrity sha512-7dezwAs+k6yXVFZ+MaL8VnE+APobiO3zvpp3rBHe/HmWQ+avwh0Q3d0xxacOiBybZZ3syTZw9HXzpa3YNbAZDQ== dependencies: "@types/bonjour" "^3.5.9" "@types/connect-history-api-fallback" "^1.3.5" @@ -6596,9 +6574,9 @@ yaml@^1.10.0, yaml@^1.10.2: integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yargs-parser@^21.0.0: - version "21.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.0.tgz#a11d06a3bf57f064e951aa3ef55fcf3a5705f876" - integrity sha512-xzm2t63xTV/f7+bGMSRzLhUNk1ajv/tDoaD5OeGyC3cFo2fl7My9Z4hS3q2VdQ7JaLvTxErO8Jp5pRIFGMD/zg== + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yocto-queue@^0.1.0: version "0.1.0" From 365b85ac4c08c47594293016933e44e99c08ca38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 13 Aug 2022 00:29:39 +0200 Subject: [PATCH 6/9] Removed patternfly-bootstrap-treeview reference from sidebar_tree_controller --- assets/controllers/elements/sidebar_tree_controller.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/assets/controllers/elements/sidebar_tree_controller.js b/assets/controllers/elements/sidebar_tree_controller.js index ca9ac515..228bbe40 100644 --- a/assets/controllers/elements/sidebar_tree_controller.js +++ b/assets/controllers/elements/sidebar_tree_controller.js @@ -1,9 +1,6 @@ import {Controller} from "@hotwired/stimulus"; import {default as TreeController} from "./tree_controller"; -import "patternfly-bootstrap-treeview/src/css/bootstrap-treeview.css" -import "patternfly-bootstrap-treeview"; - export default class extends TreeController { static targets = [ "tree", 'sourceText' ]; From f21cd55b2e8c553a19c6057d06cdb61b77fd0562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 13 Aug 2022 01:05:32 +0200 Subject: [PATCH 7/9] Show badges with the number of child nodes in the admin page treeviews again. --- assets/controllers/elements/tree_controller.js | 13 +++++++++++-- templates/components/tree_macros.html.twig | 2 +- webpack.config.js | 2 +- yarn.lock | 6 +++--- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/assets/controllers/elements/tree_controller.js b/assets/controllers/elements/tree_controller.js index 535d450a..aacb207e 100644 --- a/assets/controllers/elements/tree_controller.js +++ b/assets/controllers/elements/tree_controller.js @@ -1,14 +1,19 @@ import {Controller} from "@hotwired/stimulus"; -import {BSTreeView, BS5Theme, FAIconTheme, EVENT_INITIALIZED} from "@jbtronics/bs-treeview"; +import {BSTreeView, BSTreeViewNode, BS5Theme, FAIconTheme, EVENT_INITIALIZED} from "@jbtronics/bs-treeview"; import "@jbtronics/bs-treeview/styles/bs-treeview.css"; export default class extends Controller { static targets = [ "tree" ]; + /** @type {string} */ _url = null; + /** @type {BSTreeViewNode[]} */ _data = null; + /** @type {boolean} */ + _showTags = false; + /** * @type {BSTreeView} * @private @@ -25,6 +30,10 @@ export default class extends Controller { this._url = this.element.dataset.treeUrl; this._data = this.element.dataset.treeData; + if(this.element.dataset.treeShowTags === "true") { + this._showTags = true; + } + this.reinitTree(); } @@ -55,7 +64,7 @@ export default class extends Controller { this._tree = new BSTreeView(this.treeTarget, { levels: 1, - //showTags: true, + showTags: this._showTags, data: data, showIcon: false, onNodeSelected: function (event) { diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index f6b45054..395769e2 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -40,7 +40,7 @@ {% endmacro %} {% macro treeview(entity) %} -
    +
    diff --git a/webpack.config.js b/webpack.config.js index 484b4744..3d161a7d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -93,7 +93,7 @@ Encore // enables @babel/preset-env polyfills .configureBabelPresetEnv((config) => { - config.useBuiltIns = 'usage'; + //config.useBuiltIns = 'usage'; config.corejs = 3; }) // enables Sass/SCSS support diff --git a/yarn.lock b/yarn.lock index 7ee8a041..ac239b39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1345,9 +1345,9 @@ integrity sha512-Q8kGjqwPqER+CtpQudbH+3Zgs2X4zb6pBAlr6NsKTXadg45pAOvxI9i4QpuHbwSzR2+x87HUm+rot9F/Pe8rxA== "@jbtronics/bs-treeview@^1.0.1": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@jbtronics/bs-treeview/-/bs-treeview-1.0.2.tgz#139e43eb81e9a03099e577975721b9cac0a420de" - integrity sha512-xviDoNNkZbgg5kmluNibiAPF39sPBz4WYcPs7IX6U2C0jYbomyEtwzLAz0GXDjB/w1z1C2Gp3EkivfRxpRHSnQ== + version "1.0.3" + resolved "https://registry.yarnpkg.com/@jbtronics/bs-treeview/-/bs-treeview-1.0.3.tgz#19a176fc84fbee5c7cc86c190ee351f480bdaf3d" + integrity sha512-9KU5bnrZr5saEfkTvUY5/sg1AS1tf5OvDiQmjHSnfygtVW77FI91jyuMOmn/7gGwPypPYI354wlMVvlgf9HroA== "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" From 145aca485cd9c2df266da78ffec9e152d6193688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 13 Aug 2022 01:15:54 +0200 Subject: [PATCH 8/9] Treeview OnSelect link calling now respects turbo-frames again --- assets/controllers/elements/tree_controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/controllers/elements/tree_controller.js b/assets/controllers/elements/tree_controller.js index aacb207e..770a6da7 100644 --- a/assets/controllers/elements/tree_controller.js +++ b/assets/controllers/elements/tree_controller.js @@ -67,7 +67,7 @@ export default class extends Controller { showTags: this._showTags, data: data, showIcon: false, - onNodeSelected: function (event) { + onNodeSelected: (event) => { const node = event.detail.node; if (node.href) { @@ -75,7 +75,7 @@ export default class extends Controller { let a = document.createElement('a'); a.setAttribute('href', node.href); a.innerHTML = ""; - document.body.appendChild(a); + this.element.appendChild(a); a.click(); a.remove(); } From c5a6bbc749bdaa5c8deb2840f77678e405577acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 13 Aug 2022 01:24:02 +0200 Subject: [PATCH 9/9] Open treeview links in a new tab with a right click on a node. --- assets/controllers/elements/tree_controller.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/assets/controllers/elements/tree_controller.js b/assets/controllers/elements/tree_controller.js index 770a6da7..6fbc3ed9 100644 --- a/assets/controllers/elements/tree_controller.js +++ b/assets/controllers/elements/tree_controller.js @@ -84,11 +84,24 @@ export default class extends Controller { }, [BS5Theme, FAIconTheme]); this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => { + /** @type {BSTreeView} */ const treeView = event.detail.treeView; treeView.revealNode(treeView.getSelected()); + + //Add contextmenu event listener to the tree, which allows us to open the links in a new tab with a right click + treeView.getTreeElement().addEventListener("contextmenu", this._onContextMenu.bind(this)); }); + } + _onContextMenu(event) + { + //Find the node that was clicked and open link in new tab + const node = this._tree._domToNode(event.target); + if(node && node.href) { + event.preventDefault(); + window.open(node.href, '_blank'); + } } collapseAll() {