/*! * jquery.fancytree.filter.js * * Remove or highlight tree nodes, based on a filter. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) * * Released under the MIT license * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @version @VERSION * @date @DATE */ (function (factory) { if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. define(["jquery", "./jquery.fancytree"], factory); } else if (typeof module === "object" && module.exports) { // Node/CommonJS require("./jquery.fancytree"); module.exports = factory(require("jquery")); } else { // Browser globals factory(jQuery); } })(function ($) { "use strict"; /******************************************************************************* * Private functions and variables */ var KeyNoData = "__not_found__", escapeHtml = $.ui.fancytree.escapeHtml, exoticStartChar = "\uFFF7", exoticEndChar = "\uFFF8"; function _escapeRegex(str) { return (str + "").replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); } function extractHtmlText(s) { if (s.indexOf(">") >= 0) { return $("
").html(s).text(); } return s; } /** * @description Marks the matching charecters of `text` either by `mark` or * by exotic*Chars (if `escapeTitles` is `true`) based on `regexMatchArray` * which is an array of matching groups. * @param {string} text * @param {RegExpMatchArray} regexMatchArray */ function _markFuzzyMatchedChars(text, regexMatchArray, escapeTitles) { // It is extremely infuriating that we can not use `let` or `const` or arrow functions. // Damn you IE!!! var matchingIndices = []; // get the indices of matched characters (Iterate through `RegExpMatchArray`) for ( var _matchingArrIdx = 1; _matchingArrIdx < regexMatchArray.length; _matchingArrIdx++ ) { var _mIdx = // get matching char index by cumulatively adding // the matched group length regexMatchArray[_matchingArrIdx].length + (_matchingArrIdx === 1 ? 0 : 1) + (matchingIndices[matchingIndices.length - 1] || 0); matchingIndices.push(_mIdx); } // Map each `text` char to its position and store in `textPoses`. var textPoses = text.split(""); if (escapeTitles) { // If escaping the title, then wrap the matchng char within exotic chars matchingIndices.forEach(function (v) { textPoses[v] = exoticStartChar + textPoses[v] + exoticEndChar; }); } else { // Otherwise, Wrap the matching chars within `mark`. matchingIndices.forEach(function (v) { textPoses[v] = "" + textPoses[v] + ""; }); } // Join back the modified `textPoses` to create final highlight markup. return textPoses.join(""); } $.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function ( filter, branchMode, _opts ) { var match, statusNode, re, reHighlight, reExoticStartChar, reExoticEndChar, temp, prevEnableUpdate, count = 0, treeOpts = this.options, escapeTitles = treeOpts.escapeTitles, prevAutoCollapse = treeOpts.autoCollapse, opts = $.extend({}, treeOpts.filter, _opts), hideMode = opts.mode === "hide", leavesOnly = !!opts.leavesOnly && !branchMode; // Default to 'match title substring (not case sensitive)' if (typeof filter === "string") { if (filter === "") { this.warn( "Fancytree passing an empty string as a filter is handled as clearFilter()." ); this.clearFilter(); return; } if (opts.fuzzy) { // See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905 // and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed // and http://www.dustindiaz.com/autocomplete-fuzzy-matching match = filter .split("") // Escaping the `filter` will not work because, // it gets further split into individual characters. So, // escape each character after splitting .map(_escapeRegex) .reduce(function (a, b) { // create capture groups for parts that comes before // the character return a + "([^" + b + "]*)" + b; }, ""); } else { match = _escapeRegex(filter); // make sure a '.' is treated literally } re = new RegExp(match, "i"); reHighlight = new RegExp(_escapeRegex(filter), "gi"); if (escapeTitles) { reExoticStartChar = new RegExp( _escapeRegex(exoticStartChar), "g" ); reExoticEndChar = new RegExp(_escapeRegex(exoticEndChar), "g"); } filter = function (node) { if (!node.title) { return false; } var text = escapeTitles ? node.title : extractHtmlText(node.title), // `.match` instead of `.test` to get the capture groups res = text.match(re); if (res && opts.highlight) { if (escapeTitles) { if (opts.fuzzy) { temp = _markFuzzyMatchedChars( text, res, escapeTitles ); } else { // #740: we must not apply the marks to escaped entity names, e.g. `"` // Use some exotic characters to mark matches: temp = text.replace(reHighlight, function (s) { return exoticStartChar + s + exoticEndChar; }); } // now we can escape the title... node.titleWithHighlight = escapeHtml(temp) // ... and finally insert the desired `` tags .replace(reExoticStartChar, "") .replace(reExoticEndChar, ""); } else { if (opts.fuzzy) { node.titleWithHighlight = _markFuzzyMatchedChars( text, res ); } else { node.titleWithHighlight = text.replace( reHighlight, function (s) { return "" + s + ""; } ); } } // node.debug("filter", escapeTitles, text, node.titleWithHighlight); } return !!res; }; } this.enableFilter = true; this.lastFilterArgs = arguments; prevEnableUpdate = this.enableUpdate(false); this.$div.addClass("fancytree-ext-filter"); if (hideMode) { this.$div.addClass("fancytree-ext-filter-hide"); } else { this.$div.addClass("fancytree-ext-filter-dimm"); } this.$div.toggleClass( "fancytree-ext-filter-hide-expanders", !!opts.hideExpanders ); // Reset current filter this.rootNode.subMatchCount = 0; this.visit(function (node) { delete node.match; delete node.titleWithHighlight; node.subMatchCount = 0; }); statusNode = this.getRootNode()._findDirectChild(KeyNoData); if (statusNode) { statusNode.remove(); } // Adjust node.hide, .match, and .subMatchCount properties treeOpts.autoCollapse = false; // #528 this.visit(function (node) { if (leavesOnly && node.children != null) { return; } var res = filter(node), matchedByBranch = false; if (res === "skip") { node.visit(function (c) { c.match = false; }, true); return "skip"; } if (!res && (branchMode || res === "branch") && node.parent.match) { res = true; matchedByBranch = true; } if (res) { count++; node.match = true; node.visitParents(function (p) { if (p !== node) { p.subMatchCount += 1; } // Expand match (unless this is no real match, but only a node in a matched branch) if (opts.autoExpand && !matchedByBranch && !p.expanded) { p.setExpanded(true, { noAnimation: true, noEvents: true, scrollIntoView: false, }); p._filterAutoExpanded = true; } }, true); } }); treeOpts.autoCollapse = prevAutoCollapse; if (count === 0 && opts.nodata && hideMode) { statusNode = opts.nodata; if (typeof statusNode === "function") { statusNode = statusNode(); } if (statusNode === true) { statusNode = {}; } else if (typeof statusNode === "string") { statusNode = { title: statusNode }; } statusNode = $.extend( { statusNodeType: "nodata", key: KeyNoData, title: this.options.strings.noData, }, statusNode ); this.getRootNode().addNode(statusNode).match = true; } // Redraw whole tree this._callHook("treeStructureChanged", this, "applyFilter"); // this.render(); this.enableUpdate(prevEnableUpdate); return count; }; /** * [ext-filter] Dimm or hide nodes. * * @param {function | string} filter * @param {boolean} [opts={autoExpand: false, leavesOnly: false}] * @returns {integer} count * @alias Fancytree#filterNodes * @requires jquery.fancytree.filter.js */ $.ui.fancytree._FancytreeClass.prototype.filterNodes = function ( filter, opts ) { if (typeof opts === "boolean") { opts = { leavesOnly: opts }; this.warn( "Fancytree.filterNodes() leavesOnly option is deprecated since 2.9.0 / 2015-04-19. Use opts.leavesOnly instead." ); } return this._applyFilterImpl(filter, false, opts); }; /** * [ext-filter] Dimm or hide whole branches. * * @param {function | string} filter * @param {boolean} [opts={autoExpand: false}] * @returns {integer} count * @alias Fancytree#filterBranches * @requires jquery.fancytree.filter.js */ $.ui.fancytree._FancytreeClass.prototype.filterBranches = function ( filter, opts ) { return this._applyFilterImpl(filter, true, opts); }; /** * [ext-filter] Re-apply current filter. * * @returns {integer} count * @alias Fancytree#updateFilter * @requires jquery.fancytree.filter.js * @since 2.38 */ $.ui.fancytree._FancytreeClass.prototype.updateFilter = function () { if ( this.enableFilter && this.lastFilterArgs && this.options.filter.autoApply ) { this._applyFilterImpl.apply(this, this.lastFilterArgs); } else { this.warn("updateFilter(): no filter active."); } }; /** * [ext-filter] Reset the filter. * * @alias Fancytree#clearFilter * @requires jquery.fancytree.filter.js */ $.ui.fancytree._FancytreeClass.prototype.clearFilter = function () { var $title, statusNode = this.getRootNode()._findDirectChild(KeyNoData), escapeTitles = this.options.escapeTitles, enhanceTitle = this.options.enhanceTitle, prevEnableUpdate = this.enableUpdate(false); if (statusNode) { statusNode.remove(); } // we also counted root node's subMatchCount delete this.rootNode.match; delete this.rootNode.subMatchCount; this.visit(function (node) { if (node.match && node.span) { // #491, #601 $title = $(node.span).find(">span.fancytree-title"); if (escapeTitles) { $title.text(node.title); } else { $title.html(node.title); } if (enhanceTitle) { enhanceTitle( { type: "enhanceTitle" }, { node: node, $title: $title } ); } } delete node.match; delete node.subMatchCount; delete node.titleWithHighlight; if (node.$subMatchBadge) { node.$subMatchBadge.remove(); delete node.$subMatchBadge; } if (node._filterAutoExpanded && node.expanded) { node.setExpanded(false, { noAnimation: true, noEvents: true, scrollIntoView: false, }); } delete node._filterAutoExpanded; }); this.enableFilter = false; this.lastFilterArgs = null; this.$div.removeClass( "fancytree-ext-filter fancytree-ext-filter-dimm fancytree-ext-filter-hide" ); this._callHook("treeStructureChanged", this, "clearFilter"); // this.render(); this.enableUpdate(prevEnableUpdate); }; /** * [ext-filter] Return true if a filter is currently applied. * * @returns {Boolean} * @alias Fancytree#isFilterActive * @requires jquery.fancytree.filter.js * @since 2.13 */ $.ui.fancytree._FancytreeClass.prototype.isFilterActive = function () { return !!this.enableFilter; }; /** * [ext-filter] Return true if this node is matched by current filter (or no filter is active). * * @returns {Boolean} * @alias FancytreeNode#isMatched * @requires jquery.fancytree.filter.js * @since 2.13 */ $.ui.fancytree._FancytreeNodeClass.prototype.isMatched = function () { return !(this.tree.enableFilter && !this.match); }; /******************************************************************************* * Extension code */ $.ui.fancytree.registerExtension({ name: "filter", version: "@VERSION", // Default options for this extension. options: { autoApply: true, // Re-apply last filter if lazy data is loaded autoExpand: false, // Expand all branches that contain matches while filtered counter: true, // Show a badge with number of matching child nodes near parent icons fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar' hideExpandedCounter: true, // Hide counter badge if parent is expanded hideExpanders: false, // Hide expanders if all child nodes are hidden by filter highlight: true, // Highlight matches by wrapping inside tags leavesOnly: false, // Match end nodes only nodata: true, // Display a 'no data' status node if result is empty mode: "dimm", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) }, nodeLoadChildren: function (ctx, source) { var tree = ctx.tree; return this._superApply(arguments).done(function () { if ( tree.enableFilter && tree.lastFilterArgs && ctx.options.filter.autoApply ) { tree._applyFilterImpl.apply(tree, tree.lastFilterArgs); } }); }, nodeSetExpanded: function (ctx, flag, callOpts) { var node = ctx.node; delete node._filterAutoExpanded; // Make sure counter badge is displayed again, when node is beeing collapsed if ( !flag && ctx.options.filter.hideExpandedCounter && node.$subMatchBadge ) { node.$subMatchBadge.show(); } return this._superApply(arguments); }, nodeRenderStatus: function (ctx) { // Set classes for current status var res, node = ctx.node, tree = ctx.tree, opts = ctx.options.filter, $title = $(node.span).find("span.fancytree-title"), $span = $(node[tree.statusClassPropName]), enhanceTitle = ctx.options.enhanceTitle, escapeTitles = ctx.options.escapeTitles; res = this._super(ctx); // nothing to do, if node was not yet rendered if (!$span.length || !tree.enableFilter) { return res; } $span .toggleClass("fancytree-match", !!node.match) .toggleClass("fancytree-submatch", !!node.subMatchCount) .toggleClass( "fancytree-hide", !(node.match || node.subMatchCount) ); // Add/update counter badge if ( opts.counter && node.subMatchCount && (!node.isExpanded() || !opts.hideExpandedCounter) ) { if (!node.$subMatchBadge) { node.$subMatchBadge = $( "" ); $( "span.fancytree-icon, span.fancytree-custom-icon", node.span ).append(node.$subMatchBadge); } node.$subMatchBadge.show().text(node.subMatchCount); } else if (node.$subMatchBadge) { node.$subMatchBadge.hide(); } // node.debug("nodeRenderStatus", node.titleWithHighlight, node.title) // #601: also check for $title.length, because we don't need to render // if node.span is null (i.e. not rendered) if (node.span && (!node.isEditing || !node.isEditing.call(node))) { if (node.titleWithHighlight) { $title.html(node.titleWithHighlight); } else if (escapeTitles) { $title.text(node.title); } else { $title.html(node.title); } if (enhanceTitle) { enhanceTitle( { type: "enhanceTitle" }, { node: node, $title: $title } ); } } return res; }, }); // Value returned by `require('jquery.fancytree..')` return $.ui.fancytree; }); // End of closure