/** * Copyright JS Foundation and other contributors, http://js.foundation * * 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($) { /** * options: * - data : array - initial items to display in tree * - multi : boolean - if true, .selected will return an array of results * otherwise, returns the first selected item * - sortable: boolean/string - TODO: see editableList * - selectable: boolean - default true - whether individual items can be selected * - rootSortable: boolean - if 'sortable' is set, then setting this to * false, prevents items being sorted to the * top level of the tree * - autoSelect: boolean - default true - triggers item selection when navigating * list by keyboard. If the list has checkboxed items * you probably want to set this to false * * methods: * - data(items) - clears existing items and replaces with new data * - clearSelection - clears the selected items * - filter(filterFunc) - filters the tree using the provided function * events: * - treelistselect : function(event, item) {} * - treelistconfirm : function(event,item) {} * - treelistchangeparent: function(event,item, oldParent, newParent) {} * * data: * [ * { * label: 'Local', // label for the item * sublabel: 'Local', // a sub-label for the item * icon: 'fa fa-rocket', // (optional) icon for the item * checkbox: true/false, // (optional) if present, display checkbox accordingly * radio: 'group-name', // (optional) if present, display radio box - using group-name to set radio group * selected: true/false, // (optional) whether the item is selected or not * children: [] | function(done,item) // (optional) an array of child items, or a function * // that will call the `done` callback with an array * // of child items * expanded: true/false, // show the child items by default * deferBuild: true/false, // don't build any ui elements for the item's children * until it is expanded by the user. * element: // custom dom element to use for the item - ignored if `label` is set * collapsible: true/false, // prevent a parent item from being collapsed. default true. * } * ] * * var treeList = $("
").css({width: "100%", height: "100%"}).treeList({data:[...]}) * treeList.on('treelistselect', function(e,item) { console.log(item)}) * treeList.treeList('data',[ ... ] ) * * * After `data` has been added to the tree, each item is augmented the following * properties and functions: * * item.parent - set to the parent item * item.depth - the depth in the tree (0 == root) * item.treeList.container * item.treeList.label - the label element for the item * item.treeList.parentList - the editableList instance this item is in * item.treeList.remove(detach) - removes the item from the tree. Optionally detach to preserve any event handlers on the item's label * item.treeList.makeLeaf(detachChildElements) - turns an element with children into a leaf node, * removing the UI decoration etc. * detachChildElements - any children with custom * elements will be detached rather than removed * so jQuery event handlers are preserved in case * the child elements need to be reattached later * item.treeList.makeParent(children) - turns an element into a parent node, adding the necessary * UI decoration. * item.treeList.insertChildAt(newItem,position,select) - adds a child item an the specified position. * Optionally selects the item after adding. * item.treeList.addChild(newItem,select) - appends a child item. * Optionally selects the item after adding. * item.treeList.expand(done) - expands the parent item to show children. Optional 'done' callback. * item.treeList.collapse() - collapse the parent item to hide children. * item.treeList.sortChildren(sortFunction) - does a one-time sort of the children using sortFunction * item.treeList.replaceElement(element) - replace the custom element for the item * * */ $.widget( "nodered.treeList", { _create: function() { var that = this; var autoSelect = true; if (that.options.autoSelect === false) { autoSelect = false; } this.element.addClass('red-ui-treeList'); this.element.attr("tabIndex",0); var wrapper = $('
',{class:'red-ui-treeList-container'}).appendTo(this.element); this.element.on('keydown', function(evt) { var focussed = that._topList.find(".focus").parent().data('data'); if (!focussed && (evt.keyCode === 40 || evt.keyCode === 38)) { if (that._data[0]) { if (autoSelect) { that.select(that._data[0]); } else { that._topList.find(".focus").removeClass("focus") } that._data[0].treeList.label.addClass('focus') } return; } var target; switch(evt.keyCode) { case 32: // SPACE case 13: // ENTER if (!that.options.selectable) { return } if (evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) { return } evt.preventDefault(); evt.stopPropagation(); if (focussed.checkbox) { focussed.treeList.checkbox.trigger("click"); } else if (focussed.radio) { focussed.treeList.radio.trigger("click"); } else if (focussed.children) { if (focussed.treeList.container.hasClass("expanded")) { focussed.treeList.collapse() } else { focussed.treeList.expand() } } else { that._trigger("confirm",null,focussed) } break; case 37: // LEFT evt.preventDefault(); evt.stopPropagation(); if (focussed.children&& focussed.treeList.container.hasClass("expanded")) { focussed.treeList.collapse() } else if (focussed.parent) { target = focussed.parent; } break; case 38: // UP evt.preventDefault(); evt.stopPropagation(); target = that._getPreviousSibling(focussed); if (target) { target = that._getLastDescendant(target); } if (!target && focussed.parent) { target = focussed.parent; } break; case 39: // RIGHT evt.preventDefault(); evt.stopPropagation(); if (focussed.children) { if (!focussed.treeList.container.hasClass("expanded")) { focussed.treeList.expand() } } break case 40: //DOWN evt.preventDefault(); evt.stopPropagation(); if (focussed.children && Array.isArray(focussed.children) && focussed.children.length > 0 && focussed.treeList.container.hasClass("expanded")) { target = focussed.children[0]; } else { target = that._getNextSibling(focussed); while (!target && focussed.parent) { focussed = focussed.parent; target = that._getNextSibling(focussed); } } break } if (target) { if (autoSelect) { that.select(target); } else { that._topList.find(".focus").removeClass("focus") } target.treeList.label.addClass('focus') } }); this._data = []; this._items = {}; this._selected = new Set(); this._topList = $('
    ').css({ position:'absolute', top:0, left:0, right:0, bottom:0 }).appendTo(wrapper); var topListOptions = { addButton: false, scrollOnAdd: false, height: '100%', addItem: function(container,i,item) { that._addSubtree(that._topList,container,item,0); } }; if (this.options.header) { topListOptions.header = this.options.header; } if (this.options.rootSortable !== false && !!this.options.sortable) { topListOptions.sortable = this.options.sortable; topListOptions.connectWith = '.red-ui-treeList-sortable'; this._topList.addClass('red-ui-treeList-sortable'); } this._topList.editableList(topListOptions) if (this.options.data) { this.data(this.options.data); } }, _getLastDescendant: function(item) { // Gets the last visible descendant of the item if (!item.children || !item.treeList.container.hasClass("expanded") || item.children.length === 0) { return item; } return this._getLastDescendant(item.children[item.children.length-1]); }, _getPreviousSibling: function(item) { var candidates; if (!item.parent) { candidates = this._data; } else { candidates = item.parent.children; } var index = candidates.indexOf(item); if (index === 0) { return null; } else { return candidates[index-1]; } }, _getNextSibling: function(item) { var candidates; if (!item.parent) { candidates = this._data; } else { candidates = item.parent.children; } var index = candidates.indexOf(item); if (index === candidates.length - 1) { return null; } else { return candidates[index+1]; } }, _addChildren: function(container,parent,children,depth,onCompleteChildren) { var that = this; var subtree = $('
      ').appendTo(container).editableList({ connectWith: ".red-ui-treeList-sortable", sortable: that.options.sortable, addButton: false, scrollOnAdd: false, height: 'auto', addItem: function(container,i,item) { that._addSubtree(subtree,container,item,depth+1); }, sortItems: function(data) { var children = []; var reparented = []; data.each(function() { var child = $(this).data('data'); children.push(child); var evt = that._fixDepths(parent,child); if (evt) { reparented.push(evt); } }) if (Array.isArray(parent.children)) { parent.children = children; } reparented.forEach(function(evt) { that._trigger("changeparent",null,evt); }); that._trigger("sort",null,parent); }, filter: parent.treeList.childFilter }); if (!!that.options.sortable) { subtree.addClass('red-ui-treeList-sortable'); } var sliceSize = 30; var index = 0; var addSlice = function() { var start = index; for (var i=0;i').css({ "background-position": (35+depth*20)+'px 50%' }).appendTo(container); } } else { if (that._loadingData || item.children.length > 20) { item.treeList.childList.show(); } else { item.treeList.childList.slideDown('fast'); } item.expanded = true; if (done) { done(!that._loadingData) } } container.addClass("expanded"); } item.treeList.collapse = function() { if (item.collapsible === false) { return } if (!item.children) { return; } item.expanded = false; if (item.treeList.container) { if (item.children.length < 20) { item.treeList.childList.slideUp('fast'); } else { item.treeList.childList.hide(); } item.treeList.container.removeClass("expanded"); } } item.treeList.sortChildren = function(sortFunc) { if (!item.children) { return; } item.children.sort(sortFunc); if (item.treeList.childList) { // Do a one-off sort of the list, which means calling sort twice: // 1. first with the desired sort function item.treeList.childList.editableList('sort',sortFunc); // 2. and then with null to remove it item.treeList.childList.editableList('sort',null); } } item.treeList.replaceElement = function (element) { if (item.element) { if (item.treeList.container) { $(item.element).remove(); $(element).appendTo(item.treeList.label); // using the JQuery Object, the gutter width will // be wrong when the element is reattached the second time var labelPaddingWidth = (item.gutter ? item.gutter[0].offsetWidth + 2 : 0) + (item.depth * 20); $(element).css({ width: "calc(100% - "+(labelPaddingWidth+20+(item.icon?20:0))+"px)" }) } item.element = element; } } if (item.children && typeof item.children !== "function") { item.children.forEach(function(i) { that._initItem(i,depth+1); }) } }, _addSubtree: function(parentList, container, item, depth) { var that = this; this._initItem(item,depth); // item.treeList = {}; // item.treeList.depth = depth; item.treeList.container = container; item.treeList.parentList = parentList; var label = $("
      ",{class:"red-ui-treeList-label"}); label.appendTo(container); item.treeList.label = label; if (item.class) { label.addClass(item.class); } if (item.gutter) { item.gutter.css({ position: 'absolute' }).appendTo(label) } var labelPaddingWidth = ((item.gutter&&!item.gutter.hasClass("red-ui-treeList-gutter-float"))?item.gutter.width()+2:0)+(depth*20); item.treeList.labelPadding = $('').css({ display: "inline-block", "flex-shrink": 0, width: labelPaddingWidth+'px' }).appendTo(label); label.on('mouseover',function(e) { that._trigger('itemmouseover',e,item); }) label.on('mouseout',function(e) { that._trigger('itemmouseout',e,item); }) label.on('mouseenter',function(e) { that._trigger('itemmouseenter',e,item); }) label.on('mouseleave',function(e) { that._trigger('itemmouseleave',e,item); }) item.treeList.makeLeaf = function(detachChildElements) { if (!treeListIcon.children().length) { // Already a leaf return } if (detachChildElements && item.children) { var detachChildren = function(item) { if (item.children) { item.children.forEach(function(child) { if (child.element) { child.element.detach(); } if (child.gutter) { child.gutter.detach(); } detachChildren(child); }); } } detachChildren(item); } treeListIcon.empty(); if (!item.deferBuild) { item.treeList.childList.remove(); delete item.treeList.childList; } label.off("click.red-ui-treeList-expand"); treeListIcon.off("click.red-ui-treeList-expand"); delete item.children; container.removeClass("expanded"); delete item.expanded; } item.treeList.makeParent = function(children) { if (treeListIcon.children().length) { // Already a parent because we've got the angle-right icon return; } $('').toggleClass("hide",item.collapsible === false).appendTo(treeListIcon); treeListIcon.on("click.red-ui-treeList-expand", function(e) { e.stopPropagation(); e.preventDefault(); if (container.hasClass("expanded")) { item.treeList.collapse(); } else { item.treeList.expand(); } }); // $('').appendTo(label); label.on("click.red-ui-treeList-expand", function(e) { if (container.hasClass("expanded")) { if (item.hasOwnProperty('selected') || label.hasClass("selected")) { item.treeList.collapse(); } } else { item.treeList.expand(); } }) if (!item.children) { item.children = children||[]; item.treeList.childList = that._addChildren(container,item,item.children,depth); } } var treeListIcon = $('').appendTo(label); if (item.children) { item.treeList.makeParent(); } if (item.checkbox) { var selectWrapper = $(''); var cb = $('').prop('checked',item.selected).appendTo(selectWrapper); cb.on('click', function(e) { e.stopPropagation(); }); cb.on('change', function(e) { item.selected = this.checked; if (item.selected) { that._selected.add(item); } else { that._selected.delete(item); } label.toggleClass("selected",this.checked); that._trigger("select",e,item); }) if (!item.children) { label.on("click", function(e) { e.stopPropagation(); cb.trigger("click"); that._topList.find(".focus").removeClass("focus") label.addClass('focus') }) } item.treeList.select = function(v) { if (v !== item.selected) { cb.trigger("click"); } } item.treeList.checkbox = cb; selectWrapper.appendTo(label) } else if (item.radio) { var selectWrapper = $(''); var cb = $('').prop('name', item.radio).prop('checked',item.selected).appendTo(selectWrapper); cb.on('click', function(e) { e.stopPropagation(); }); cb.on('change', function(e) { item.selected = this.checked; that._selected.forEach(function(selectedItem) { if (selectedItem.radio === item.radio) { selectedItem.treeList.label.removeClass("selected"); selectedItem.selected = false; that._selected.delete(selectedItem); } }) if (item.selected) { that._selected.add(item); } else { that._selected.delete(item); } label.toggleClass("selected",this.checked); that._trigger("select",e,item); }) if (!item.children) { label.on("click", function(e) { e.stopPropagation(); cb.trigger("click"); that._topList.find(".focus").removeClass("focus") label.addClass('focus') }) } item.treeList.select = function(v) { if (v !== item.selected) { cb.trigger("click"); } } selectWrapper.appendTo(label) item.treeList.radio = cb; } else { label.on("click", function(e) { if (!that.options.multi) { that.clearSelection(); } label.addClass("selected"); that._selected.add(item); that._topList.find(".focus").removeClass("focus") label.addClass('focus') that._trigger("select",e,item) }) label.on("dblclick", function(e) { that._topList.find(".focus").removeClass("focus") label.addClass('focus') if (!item.children) { that._trigger("confirm",e,item); } }) item.treeList.select = function(v) { if (!that.options.multi) { that.clearSelection(); } label.toggleClass("selected",v); if (v) { that._selected.add(item); that._trigger("select",null,item) } else { that._selected.delete(item); } that.reveal(item); } } label.toggleClass("selected",!!item.selected); if (item.selected) { that._selected.add(item); } if (item.icon) { if (typeof item.icon === "string") { $('').appendTo(label); } else { $('').appendTo(label).append(item.icon); } } if (item.hasOwnProperty('label') || item.hasOwnProperty('sublabel')) { if (item.hasOwnProperty('label')) { $('').text(item.label).appendTo(label); } if (item.hasOwnProperty('sublabel')) { $('').text(item.sublabel).appendTo(label); } } else if (item.element) { $(item.element).appendTo(label); $(item.element).css({ width: "calc(100% - "+(labelPaddingWidth+20+(item.icon?20:0))+"px)" }) } if (item.children) { if (Array.isArray(item.children) && !item.deferBuild) { item.treeList.childList = that._addChildren(container,item,item.children,depth); } if (item.expanded) { item.treeList.expand(); } } // label.appendTo(container); }, empty: function() { this._topList.editableList('empty'); }, data: function(items) { var that = this; if (items !== undefined) { this._data = items; this._items = {}; this._topList.editableList('empty'); this._loadingData = true; for (var i=0; i treeHeight) { this._topList.parent().scrollTop(scrollTop+((itemOffset+2.5*itemHeight)-treeHeight)); } }, select: function(item, triggerEvent, deselectExisting) { var that = this; if (!this.options.multi && deselectExisting !== false) { this.clearSelection(); } if (Array.isArray(item)) { item.forEach(function(i) { that.select(i,triggerEvent,false); }) return; } if (typeof item === "string") { item = this._items[item] } if (!item) { return; } // this.show(item.id); item.selected = true; this._selected.add(item); if (item.treeList.label) { item.treeList.label.addClass("selected"); } that._topList.find(".focus").removeClass("focus"); if (triggerEvent !== false) { this._trigger("select",null,item) } }, clearSelection: function() { this._selected.forEach(function(item) { item.selected = false; if (item.treeList.checkbox) { item.treeList.checkbox.prop('checked',false) } if (item.treeList.label) { item.treeList.label.removeClass("selected") } }); this._selected.clear(); }, selected: function() { var selected = []; this._selected.forEach(function(item) { selected.push(item); }) if (this.options.multi) { return selected; } if (selected.length) { return selected[0] } else { // TODO: This may be a bug.. it causes the call to return itself // not undefined. return undefined; } }, filter: function(filterFunc) { this.activeFilter = filterFunc; var totalCount = 0; var filter = function(item) { var matchCount = 0; if (filterFunc && filterFunc(item)) { matchCount++; totalCount++; } var childCount = 0; if (item.children && typeof item.children !== "function") { if (item.treeList.childList) { childCount = item.treeList.childList.editableList('filter', filter); } else { item.treeList.childFilter = filter; if (filterFunc) { item.children.forEach(function(i) { if (filter(i)) { childCount++; } }) } } matchCount += childCount; if (filterFunc && childCount > 0) { setTimeout(function() { item.treeList.expand(); },10); } } if (!filterFunc) { totalCount++; return true } return matchCount > 0 } this._topList.editableList('filter', filter); return totalCount; }, get: function(id) { return this._items[id] || null; } }); })(jQuery);