jquery.fancytree
Version:
Fancytree is a JavaScript tree view plugin for jQuery with support for persistence, keyboard, checkboxes, drag'n'drop, and lazy loading.
354 lines (323 loc) • 11 kB
JavaScript
/*!
* 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-2017, Martin Wendt (http://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version @VERSION
* @date @DATE
*/
;(function($, window, document, undefined) {
;
/*******************************************************************************
* Private functions and variables
*/
var KeyNoData = "__not_found__",
escapeHtml = $.ui.fancytree.escapeHtml;
function _escapeRegex(str){
/*jshint regexdash:true */
return (str + "").replace(/([.?*+\^\$\[\]\\(){}|-])/g, "\\$1");
}
function extractHtmlText(s){
if( s.indexOf(">") >= 0 ) {
return $("<div/>").html(s).text();
}
return s;
}
$.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function(filter, branchMode, _opts){
var match, statusNode, re, reHighlight,
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"){
// console.log("rex", filter.split('').join('\\w*').replace(/\W/, ""))
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("").reduce(function(a, b) {
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");
filter = function(node){
var display,
text = escapeTitles ? node.title : extractHtmlText(node.title),
res = !!re.test(text);
if( res && opts.highlight ) {
display = escapeTitles ? escapeHtml(node.title) : text;
node.titleWithHighlight = display.replace(reHighlight, function(s){
return "<mark>" + s + "</mark>";
});
// node.debug("filter", escapeTitles, text, node.titleWithHighlight);
}
return res;
};
}
this.enableFilter = true;
this.lastFilterArgs = arguments;
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.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){
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;
}
});
}
});
treeOpts.autoCollapse = prevAutoCollapse;
if( count === 0 && opts.nodata && hideMode ) {
statusNode = opts.nodata;
if( $.isFunction(statusNode) ) {
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.render();
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);
};
/**
* @deprecated
*/
$.ui.fancytree._FancytreeClass.prototype.applyFilter = function(filter){
this.warn("Fancytree.applyFilter() is deprecated since 2.1.0 / 2014-05-29. Use .filterNodes() instead.");
return this.filterNodes.apply(this, arguments);
};
/**
* [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] 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;
if( statusNode ) {
statusNode.remove();
}
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.render();
};
/**
* [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 <mark> 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) {
return this._superApply(arguments).done(function() {
if( ctx.tree.enableFilter && ctx.tree.lastFilterArgs && ctx.options.filter.autoApply ) {
ctx.tree._applyFilterImpl.apply(ctx.tree, ctx.tree.lastFilterArgs);
}
});
},
nodeSetExpanded: function(ctx, flag, callOpts) {
delete ctx.node._filterAutoExpanded;
// Make sure counter badge is displayed again, when node is beeing collapsed
if( !flag && ctx.options.filter.hideExpandedCounter && ctx.node.$subMatchBadge ) {
ctx.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 class='fancytree-childcounter'/>");
$("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 chek 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;
}
});
}(jQuery, window, document));