All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dependency.tree.assets.js.jquery.fancytree.js Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
/*!
 * jquery.fancytree.js
 * Dynamic tree view control, with support for lazy loading of branches.
 * https://github.com/mar10/fancytree/
 *
 * Copyright (c) 2006-2013, Martin Wendt (http://wwWendt.de)
 * Released under the MIT license
 * https://github.com/mar10/fancytree/wiki/LicenseInfo
 *
 * @version DEVELOPMENT
 * @date DEVELOPMENT
 */

// Start of local namespace
;(function($, window, document, undefined) {
// relax some jslint checks:
/*globals alert */

"use strict";

// prevent duplicate loading
if ( $.ui.fancytree && $.ui.fancytree.version ) {
	$.ui.fancytree.warn("Fancytree: ignored duplicate include");
	return;
}


/* *****************************************************************************
 * Private functions and variables
 */

function _raiseNotImplemented(msg){
	msg = msg || "";
	$.error("Not implemented: " + msg);
}

function _assert(cond, msg){
	// TODO: see qunit.js extractStacktrace()
	msg = ": " + msg || "";
	if(!cond){
		$.error("Assertion failed" + msg);
	}
}

function consoleApply(method, args){
	var i, s,
		fn = window.console ? window.console[method] : null;

	if(fn){
		if(fn.apply){
			fn.apply(window.console, args);
		}else{
			// IE?
			s = "";
			for( i=0; i t );
		}
	}
	return true;
}

/** Return a wrapper that calls sub.methodName() and exposes
 *  this        : tree
 *  this._local : tree.ext.EXTNAME
 *  this._super : base.methodName()
 */
function _makeVirtualFunction(methodName, tree, base, extension, extName){
	// $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName);
	// if(rexTestSuper && !rexTestSuper.test(func)){
	//     // extension.methodName() doesn't call _super(), so no wrapper required
	//     return func;
	// }
	// Use an immediate function as closure
	var proxy = (function(){
		var prevFunc = tree[methodName],      // org. tree method or prev. proxy
			baseFunc = extension[methodName], //
			_local = tree.ext[extName],
			_super = function(){
				return prevFunc.apply(tree, arguments);
			};

		// Return the wrapper function
		return function(){
			var prevLocal = tree._local,
				prevSuper = tree._super;
			try{
				tree._local = _local;
				tree._super = _super;
				return  baseFunc.apply(tree, arguments);
			}finally{
				tree._local = prevLocal;
				tree._super = prevSuper;
			}
		};
	})(); // end of Immediate Function
	return proxy;
}

/**
 * Subclass `base` by creating proxy functions
 */
function _subclassObject(tree, base, extension, extName){
	// $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName);
	for(var attrName in extension){
		if(typeof extension[attrName] === "function"){
			if(typeof tree[attrName] === "function"){
				// override existing method
				tree[attrName] = _makeVirtualFunction(attrName, tree, base, extension, extName);
			}else if(attrName.charAt(0) === "_"){
				// Create private methods in tree.ext.EXTENSION namespace
				tree.ext[extName][attrName] = _makeVirtualFunction(attrName, tree, base, extension, extName);
			}else{
				$.error("Could not override tree." + attrName + ". Use prefix '_' to create tree." + extName + "._" + attrName);
			}
		}else{
			// Create member variables in tree.ext.EXTENSION namespace
			if(attrName !== "options"){
				tree.ext[extName][attrName] = extension[attrName];
			}
		}
	}
}


function _getResolvedPromise(context, argArray){
	if(context === undefined){
		return $.Deferred(function(){this.resolve();}).promise();
	}else{
		return $.Deferred(function(){this.resolveWith(context, argArray);}).promise();
	}
}


function _getRejectedPromise(context, argArray){
	if(context === undefined){
		return $.Deferred(function(){this.reject();}).promise();
	}else{
		return $.Deferred(function(){this.rejectWith(context, argArray);}).promise();
	}
}


function _makeResolveFunc(deferred, context){
	return function(){
		deferred.resolveWith(context);
	};
}


// TODO: use currying
function _makeNodeTitleMatcher(s){
	s = s.toLowerCase();
	return function(node){
		return node.title.toLowerCase().indexOf(s) >= 0;
	};
}

var i,
	FT = null, // initialized below
	//Boolean attributes that can be set with equivalent class names in the LI tags
	CLASS_ATTRS = "active expanded focus folder lazy selected unselectable".split(" "),
	CLASS_ATTR_MAP = {},
	//	Top-level Fancytree node attributes, that can be set by dict
	NODE_ATTRS = "expanded extraClasses folder hideCheckbox key lazy selected title tooltip unselectable".split(" "),
	NODE_ATTR_MAP = {},
	// Attribute names that should NOT be added to node.data
	NONE_NODE_DATA_MAP = {"active": true, "children": true, "data": true, "focus": true};

for(i=0; i tag
	this.isStatusNode = false;
	this.data = {};

	// TODO: merge this code with node.toDict()
	// copy attributes from obj object
	for(i=0, l=NODE_ATTRS.length; i= 0, "insertBefore must be an existing child");
			// insert nodeList after children[pos]
			this.children.splice.apply(this.children, [pos, 0].concat(nodeList));
		}
		if(!this.parent || this.parent.ul){
			// render if the parent was rendered (or this is a root node)
			this.render();
		}
		if( this.tree.options.selectMode === 3 ){
			this.fixSelection3FromEndNodes();
		}
		return firstNode;
	},
	/**
	 * Append or prepend a node, or append a child node.
	 *
	 * @param {NodeData} node node definition
	 * @param {String} [mode] 'before', 'after', or 'child'
	 * @returns {FancytreeNode} new node
	 */
	addNode: function(node, mode){
		if(mode === undefined || mode === "over"){
			mode = "child";
		}
		switch(mode){
		case "after":
			return this.getParent().addChildren(node, this.getNextSibling());
		case "before":
			return this.getParent().addChildren(node, this);
		case "child":
		case "over":
			return this.addChildren(node);
		}
		_assert(false, "Invalid mode: " + mode);
	},
	/**
	 *
	 * @param {NodePatch} patch
	 * @returns {$.Promise}
	 * @see {@link applyPatch} to modify existing child nodes.
	 * @see FancytreeNode#addChildren
	 */
	applyPatch: function(patch) {
		// patch [key, null] means 'remove'
		if(patch === null){
			this.remove();
			return _getResolvedPromise(this);
		}
		// TODO: make sure that root node is not collapsed or modified
		// copy (most) attributes to node.ATTR or node.data.ATTR
		var name, promise, v,
			IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global

		for(name in patch){
			v = patch[name];
			if( !IGNORE_MAP[name] && !$.isFunction(v)){
				if(NODE_ATTR_MAP[name]){
					this[name] = v;
				}else{
					this.data[name] = v;
				}
			}
		}
		// Remove and/or create children
		if(patch.hasOwnProperty("children")){
			this.removeChildren();
			if(patch.children){ // only if not null and not empty list
				// TODO: addChildren instead?
				this._setChildren(patch.children);
			}
			// TODO: how can we APPEND or INSERT child nodes?
		}
		if(this.isVisible()){
			this.renderTitle();
			this.renderStatus();
		}
		// Expand collapse (final step, since this may be async)
		if(patch.hasOwnProperty("expanded")){
			promise = this.setExpanded(patch.expanded);
		}else{
			promise = _getResolvedPromise(this);
		}
		return promise;
	},
	/**
	 * @returns {$.Promise}
	 */
	collapseSiblings: function() {
		return this.tree._callHook("nodeCollapseSiblings", this);
	},
	/** Copy this node as sibling or child of `node`.
	 *
	 * @param {FancytreeNode} node source node
	 * @param {String} mode 'before' | 'after' | 'child'
	 * @param {Function} [map] callback function(NodeData) that could modify the new node
	 * @returns {FancytreeNode} new
	 */
	copyTo: function(node, mode, map) {
		return node.addNode(this.toDict(true, map), mode);
	},
	/** Count direct and indirect children.
	 *
	 * @param {Boolean} [deep=true] pass 'false' to only count direct children
	 * @returns {int} number of child nodes
	 */
	countChildren: function(deep) {
		var cl = this.children, i, l, n;
		if( !cl ){
			return 0;
		}
		n = cl.length;
		if(deep !== false){
			for(i=0, l=n; i= 2 (prepending node info)
	 *
	 * @param {*} msg string or object or array of such
	 */
	debug: function(msg){
		if( this.tree.options.debugLevel >= 2 ) {
			Array.prototype.unshift.call(arguments, this.toString());
			consoleApply("debug", arguments);
		}
	},
	/** Remove all children of a lazy node and collapse.*/
	discard: function(){
		if(this.lazy && $.isArray(this.children)){
			this.removeChildren();
			return this.setExpanded(false);
		}
	},
	// TODO: expand(flag)
	/**Find all nodes that contain `match` in the title.
	 *
	 * @param {String | function(node)} match string to search for, of a function that
	 * returns `true` if a node is matched.
	 * @returns {FancytreeNode[]} array of nodes (may be empty)
	 * @see FancytreeNode#findAll
	 */
	findAll: function(match) {
		match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match);
		var res = [];
		this.visit(function(n){
			if(match(n)){
				res.push(n);
			}
		});
		return res;
	},
	/**Find first node that contains `match` in the title (not including self).
	 *
	 * @param {String | function(node)} match string to search for, of a function that
	 * returns `true` if a node is matched.
	 * @returns {FancytreeNode} matching node or null
	 * @example
	 * fat text
	 */
	findFirst: function(match) {
		match = $.isFunction(match) ? match : _makeNodeTitleMatcher(match);
		var res = null;
		this.visit(function(n){
			if(match(n)){
				res = n;
				return false;
			}
		});
		return res;
	},
	/* Apply selection state (internal use only) */
	_changeSelectStatusAttrs: function (state) {
		var changed = false;

		switch(state){
		case false:
			changed = ( this.selected || this.partsel );
			this.selected = false;
			this.partsel = false;
			break;
		case true:
			changed = ( !this.selected || !this.partsel );
			this.selected = true;
			this.partsel = true;
			break;
		case undefined:
			changed = ( this.selected || !this.partsel );
			this.selected = false;
			this.partsel = true;
			break;
		default:
			_assert(false, "invalid state: " + state);
		}
		this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed);
		if( changed ){
			this.renderStatus();
		}
		return changed;
	},
	/**
	 * Fix selection status, after this node was (de)selected in multi-hier mode.
	 * This includes (de)selecting all children.
	 */
	fixSelection3AfterClick: function() {
		var flag = this.isSelected();

//		this.debug("fixSelection3AfterClick()");

		this.visit(function(node){
			node._changeSelectStatusAttrs(flag);
		});
		this.fixSelection3FromEndNodes();
	},
	/**
	 * Fix selection status for multi-hier mode.
	 * Only end-nodes are considered to update the descendants branch and parents.
	 * Should be called after this node has loaded new children or after
	 * children have been modified using the API.
	 */
	fixSelection3FromEndNodes: function() {
//		this.debug("fixSelection3FromEndNodes()");
		_assert(this.tree.options.selectMode === 3, "expected selectMode 3");

		// Visit all end nodes and adjust their parent's `selected` and `partsel`
		// attributes. Return selection state true, false, or undefined.
		function _walk(node){
			var i, l, child, s, state, allSelected,someSelected,
				children = node.children;

			if( children ){
				// check all children recursively
				allSelected = true;
				someSelected = false;

				for( i=0, l=children.length; i= 0);
			this.parent.children.splice(pos, 1);
		}
		// Remove from source DOM parent
//		if(this.parent.ul){
//			this.parent.ul.removeChild(this.li);
//		}

		// Insert this node to target parent's child list
		this.parent = targetParent;
		if( targetParent.hasChildren() ) {
			switch(mode) {
			case "child":
				// Append to existing target children
				targetParent.children.push(this);
				break;
			case "before":
				// Insert this node before target node
				pos = $.inArray(targetNode, targetParent.children);
				_assert(pos >= 0);
				targetParent.children.splice(pos, 0, this);
				break;
			case "after":
				// Insert this node after target node
				pos = $.inArray(targetNode, targetParent.children);
				_assert(pos >= 0);
				targetParent.children.splice(pos+1, 0, this);
				break;
			default:
				throw "Invalid mode " + mode;
			}
		} else {
			targetParent.children = [ this ];
		}
		// Parent has no 
    tag yet: // if( !targetParent.ul ) { // // This is the parent's first child: create UL tag // // (Hidden, because it will be // targetParent.ul = document.createElement("ul"); // targetParent.ul.style.display = "none"; // targetParent.li.appendChild(targetParent.ul); // } // // Issue 319: Add to target DOM parent (only if node was already rendered(expanded)) // if(this.li){ // targetParent.ul.appendChild(this.li); // }^ // Let caller modify the nodes if( map ){ targetNode.visit(map, true); } // Handle cross-tree moves if( this.tree !== targetNode.tree ) { // Fix node.tree for all source nodes // _assert(false, "Cross-tree move is not yet implemented."); this.warn("Cross-tree moveTo is experimantal!"); this.visit(function(n){ // TODO: fix selection state and activation, ... n.tree = targetNode.tree; }, true); } // A collaposed node won't re-render children, so we have to remove it manually if( !targetParent.expanded){ prevParent.ul.removeChild(this.li); } // Update HTML markup if( !prevParent.isDescendantOf(targetParent)) { prevParent.render(); } if( !targetParent.isDescendantOf(prevParent) && targetParent !== prevParent) { targetParent.render(); } // TODO: fix selection state // TODO: fix active state /* var tree = this.tree; var opts = tree.options; var pers = tree.persistence; // Always expand, if it's below minExpandLevel // tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel()); if ( opts.minExpandLevel >= ftnode.getLevel() ) { // tree.logDebug ("Force expand for %o", ftnode); this.bExpanded = true; } // In multi-hier mode, update the parents selection state // issue #82: only if not initializing, because the children may not exist yet // if( !ftnode.data.isStatusNode && opts.selectMode==3 && !isInitializing ) // ftnode._fixSelectionState(); // In multi-hier mode, update the parents selection state if( ftnode.bSelected && opts.selectMode==3 ) { var p = this; while( p ) { if( !p.hasSubSel ) p._setSubSel(true); p = p.parent; } } // render this node and the new child if ( tree.bEnableUpdate ) this.render(); return ftnode; */ }, /** * Discard and reload all children of a lazy node. * @param {Boolean} [discard=false] * @returns $.Promise */ lazyLoad: function(discard) { if(discard){ this.discard(); }else{ _assert(!$.isArray(this.children)); } var source = this.tree._triggerNodeEvent("lazyload", this); _assert(typeof source !== "boolean", "lazyload event must return source in data.result"); return this.tree._callHook("nodeLoadChildren", this, source); }, /** * @see Fancytree#nodeRender */ render: function(force, deep) { return this.tree._callHook("nodeRender", this, force, deep); }, /** * @see Fancytree#nodeRenderTitle */ renderTitle: function() { return this.tree._callHook("nodeRenderTitle", this); }, /** * @see Fancytree#nodeRenderStatus */ renderStatus: function() { return this.tree._callHook("nodeRenderStatus", this); }, /** Remove this node (not allowed for root).*/ remove: function() { return this.parent.removeChild(this); }, /**Remove childNode from list of direct children.*/ removeChild: function(childNode) { return this.tree._callHook("nodeRemoveChild", this, childNode); }, /**Remove all child nodes (and descendents).*/ removeChildren: function() { return this.tree._callHook("nodeRemoveChildren", this); }, // TODO: resetLazy() /** Schedule activity for delayed execution (cancel any pending request). * scheduleAction('cancel') will only cancel a pending request (if any). */ scheduleAction: function(mode, ms) { if( this.tree.timer ) { clearTimeout(this.tree.timer); // this.tree.debug("clearTimeout(%o)", this.tree.timer); } this.tree.timer = null; var self = this; // required for closures switch (mode) { case "cancel": // Simply made sure that timer was cleared break; case "expand": this.tree.timer = setTimeout(function(){ self.tree.debug("setTimeout: trigger expand"); self.setExpanded(true); }, ms); break; case "activate": this.tree.timer = setTimeout(function(){ self.tree.debug("setTimeout: trigger activate"); self.setActive(true); }, ms); break; default: throw "Invalid mode " + mode; } // this.tree.debug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer); }, /** * * @param {Boolean | PlainObject} [effects=false] animation options. * @param {FancytreeNode} [topNode=null] this node will remain visible in * any case, even if `this` is outside the scroll pane. * @returns $.Promise */ scrollIntoView: function(effects, topNode) { effects = (effects === true) ? {duration: 200, queue: false} : effects; var topNodeY, dfd = new $.Deferred(), nodeY = $(this.span).position().top, nodeHeight = $(this.span).height(), $container = this.tree.$container, scrollTop = $container[0].scrollTop, horzScrollHeight = Math.max(0, ($container.innerHeight() - $container[0].clientHeight)), // containerHeight = $container.height(), containerHeight = $container.height() - horzScrollHeight, newScrollTop = null; // console.log("horzScrollHeight: " + horzScrollHeight); // console.log("$container[0].scrollTop: " + $container[0].scrollTop); // console.log("$container[0].scrollHeight: " + $container[0].scrollHeight); // console.log("$container[0].clientHeight: " + $container[0].clientHeight); // console.log("$container.innerHeight(): " + $container.innerHeight()); // console.log("$container.height(): " + $container.height()); if(nodeY < 0){ newScrollTop = scrollTop + nodeY; }else if((nodeY + nodeHeight) > containerHeight){ newScrollTop = scrollTop + nodeY - containerHeight + nodeHeight; // If a topNode was passed, make sure that it is never scrolled // outside the upper border if(topNode){ topNodeY = topNode ? $(topNode.span).position().top : 0; if((nodeY - topNodeY) > containerHeight){ newScrollTop = scrollTop + nodeY; } } } if(newScrollTop !== null){ if(effects){ // TODO: resolve dfd after animation // var that = this; $container.animate({scrollTop: newScrollTop}, effects); }else{ $container[0].scrollTop = newScrollTop; dfd.resolveWith(this); } }else{ dfd.resolveWith(this); } return dfd.promise(); /* from jQuery.menu: var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; if ( this._hasScroll() ) { borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0; paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0; offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; scroll = this.activeMenu.scrollTop(); elementHeight = this.activeMenu.height(); itemHeight = item.height(); if ( offset < 0 ) { this.activeMenu.scrollTop( scroll + offset ); } else if ( offset + itemHeight > elementHeight ) { this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); } } */ }, /**Activate this node. * @param {Boolean} [flag=true] pass false to deactivate */ setActive: function(flag){ return this.tree._callHook("nodeSetActive", this, flag); }, /**Expand this node. * @param {Boolean} [flag=true] pass false to collapse */ setExpanded: function(flag){ return this.tree._callHook("nodeSetExpanded", this, flag); }, /**Set keyboard focus to this node. * @param {Boolean} [flag=true] pass false to blur * @see Fancytree#setFocus */ setFocus: function(flag){ return this.tree._callHook("nodeSetFocus", this, flag); }, // TODO: setLazyNodeStatus /**Select this node. * @param {Boolean} [flag=true] pass false to deselect */ setSelected: function(flag){ return this.tree._callHook("nodeSetSelected", this, flag); }, setTitle: function(title){ this.title = title; this.renderTitle(); }, /**Sort child list by title. * @param {function} [cmd] custom compare function. * @param {Boolean} [deep] pass true to sort all descendant nodes */ sortChildren: function(cmp, deep) { var i,l, cl = this.children; if( !cl ){ return; } cmp = cmp || function(a, b) { var x = a.title.toLowerCase(), y = b.title.toLowerCase(); return x === y ? 0 : x > y ? 1 : -1; }; cl.sort(cmp); if( deep ){ for(i=0, l=cl.length; i"; }, /** Call fn(node) for all child nodes. Stop iteration, if fn() returns false. * Skip current branch, if fn() returns 'skip'. * @param {function} fn the callback function. * Return false to stop iteration, return "skip" to skip this node and children only. * @param {Boolean} [includeSelf=false] * @returns {Boolean} false, if the iterator was stopped. */ visit: function(fn, includeSelf) { var i, l, res = true, children = this.children; if( includeSelf === true ) { res = fn(this); if( res === false || res === "skip" ){ return res; } } if(children){ for(i=0, l=children.length; iul.fancytree-container").remove(); // Create a node without parent. var fakeParent = { tree: this }, $ul; this.rootNode = new FancytreeNode(fakeParent, { title: "root", key: "root_" + this._id, children: null }); this.rootNode.parent = null; // Create root markup $ul = $("
      ", { "class": "ui-fancytree fancytree-container" }).appendTo(this.$div); this.$container = $ul; this.rootNode.ul = $ul[0]; if(this.options.debugLevel == null){ this.options.debugLevel = FT.debugLevel; } // Add container to the TAB chain // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant if(this.options.tabbable){ this.$container.attr("tabindex", "0"); } if(this.options.aria){ this.$container.attr("role", "tree") .attr("aria-multiselectable", true); } } Fancytree.prototype = /**@lends Fancytree*/{ /** Return a context object that can be re-used for _callHook(). * @param {Fancytree | FancytreeNode | EventData} obj * @param {Event} originalEvent * @returns {EventData} */ _makeHookContext: function(obj, originalEvent) { if(obj.node !== undefined){ // obj is already a context object if(originalEvent && obj.originalEvent !== originalEvent){ $.error("invalid args"); } return obj; }else if(obj.tree){ // obj is a FancytreeNode var tree = obj.tree; return { node: obj, tree: tree, widget: tree.widget, options: tree.widget.options, originalEvent: originalEvent }; }else if(obj.widget){ // obj is a Fancytree return { node: null, tree: obj, widget: obj.widget, options: obj.widget.options, originalEvent: originalEvent }; } $.error("invalid args"); }, /** Trigger a hook function: funcName(ctx, [...]). * * @param {String} funcName * @param {Fancytree|FancytreeNode|EventData} contextObject * @param {any, ...} [_extraArgs] optional additional arguments * @returns {any} */ _callHook: function(funcName, contextObject, _extraArgs) { var ctx = this._makeHookContext(contextObject), fn = this[funcName], args = Array.prototype.slice.call(arguments, 2); if(!$.isFunction(fn)){ $.error("_callHook('" + funcName + "') is not a function"); } args.unshift(ctx); // this.debug("_hook", funcName, ctx.node && ctx.node.toString() || ctx.tree.toString(), args); return fn.apply(this, args); }, /** Activate node with a given key. * * A prevously activated node will be deactivated. * Pass key = false, to deactivate the current node only. * @param {String} key * @returns {FancytreeNode} activated node (null, if not found) */ activateKey: function(key) { var node = this.getNodeByKey(key); if(node){ node.setActive(); }else if(this.activeNode){ this.activeNode.setActive(false); } return node; }, /** * * @param {Array} patchList array of [key, NodePatch] arrays * @returns {$.Promise} resolved, when all patches have been applied * @see TreePatch */ applyPatch: function(patchList) { var dfd, i, p2, key, patch, node, patchCount = patchList.length, deferredList = []; for(i=0; i= 2 (prepending tree info) * * @param {*} msg string or object or array of such */ debug: function(msg){ if( this.options.debugLevel >= 2 ) { Array.prototype.unshift.call(arguments, this.toString()); consoleApply("debug", arguments); } }, // TODO: disable() // TODO: enable() // TODO: enableUpdate() // TODO: fromDict /** * Generate INPUT elements that can be submitted with html forms. * * In selectMode 3 only the topmost selected nodes are considered. * * @param {Boolean | String} [selected=true] * @param {Boolean | String} [active=true] */ generateFormElements: function(selected, active) { // TODO: test case var nodeList, selectedName = (selected !== false) ? "ft_" + this._id : selected, activeName = (active !== false) ? "ft_" + this._id + "_active" : active, id = "fancytree_result_" + this._id, $result = this.$container.find("div#" + id); if($result.length){ $result.empty(); }else{ $result = $("
      ", { id: id }).hide().appendTo(this.$container); } if(selectedName){ nodeList = this.getSelectedNodes( this.options.selectMode === 3 ); $.each(nodeList, function(idx, node){ $result.append($("", { type: "checkbox", name: selectedName, value: node.key, checked: true })); }); } if(activeName && this.activeNode){ $result.append($("", { type: "radio", name: activeName, value: this.activeNode.key, checked: true })); } }, /** * Return node that is active. * @returns {FancytreeNode} */ getActiveNode: function() { return this.activeNode; }, /** @returns {FancytreeNode | null}*/ getFirstChild: function() { return this.rootNode.getFirstChild(); }, /** * Return node that has keyboard focus. * @param {Boolean} [ifTreeHasFocus=false] * @returns {FancytreeNode} */ getFocusNode: function(ifTreeHasFocus) { // TODO: implement ifTreeHasFocus return this.focusNode; }, /** * Return node with a given key. * @param {String} key * @param {FancytreeNode} [searchRoot] only search below this node * @returns {FancytreeNode | null} */ getNodeByKey: function(key, searchRoot) { // Search the DOM by element ID (assuming this is faster than traversing all nodes). // $("#...") has problems, if the key contains '.', so we use getElementById() var el, match; if(!searchRoot){ el = document.getElementById(this.options.idPrefix + key); if( el ){ return el.ftnode ? el.ftnode : null; } } // Not found in the DOM, but still may be in an unrendered part of tree // TODO: optimize with specialized loop // TODO: consider keyMap? searchRoot = searchRoot || this.rootNode; match = null; searchRoot.visit(function(node){ // window.console.log("getNodeByKey(" + key + "): ", node.key); if(node.key === key) { match = node; return false; } }, true); return match; }, // TODO: getRoot() /** * Return a list of selected nodes. * @param {Boolean} [stopOnParents=false] only return the topmost selected * node (useful with selectMode 3) * @returns {FancytreeNode[]} */ getSelectedNodes: function(stopOnParents) { var nodeList = []; this.rootNode.visit(function(node){ if( node.selected ) { nodeList.push(node); if( stopOnParents === true ){ return "skip"; // stop processing this branch } } }); return nodeList; }, /** * @returns {Boolean} true if the tree control has keyboard focus */ hasFocus: function(){ return FT.focusTree === this; }, /** Write to browser console if debugLevel >= 1 (prepending tree info) * * @param {*} msg string or object or array of such */ info: function(msg){ if( this.options.debugLevel >= 1 ) { Array.prototype.unshift.call(arguments, this.toString()); consoleApply("info", arguments); } }, /* TODO: isInitializing: function() { return ( this.phase=="init" || this.phase=="postInit" ); }, TODO: isReloading: function() { return ( this.phase=="init" || this.phase=="postInit" ) && this.options.persist && this.persistence.cookiesFound; }, TODO: isUserEvent: function() { return ( this.phase=="userEvent" ); }, */ /** * Expand all parents of one or more nodes. * Calls * @param {String | String[]} keyPath one or more key paths (e.g. '/3/2_1/7') * @param {function} callback callbeck(mode) is called for every visited node * @returns {$.Promise} */ /* _loadKeyPath: function(keyPath, callback) { var tree = this.tree; tree.logDebug("%s._loadKeyPath(%s)", this, keyPath); if(keyPath === ""){ throw "Key path must not be empty"; } var segList = keyPath.split(tree.options.keyPathSeparator); if(segList[0] === ""){ throw "Key path must be relative (don't start with '/')"; } var seg = segList.shift(); for(var i=0, l=this.childList.length; i < l; i++){ var child = this.childList[i]; if( child.data.key === seg ){ if(segList.length === 0) { // Found the end node callback.call(tree, child, "ok"); }else if(child.data.isLazy && (child.childList === null || child.childList === undefined)){ tree.logDebug("%s._loadKeyPath(%s) -> reloading %s...", this, keyPath, child); var self = this; child.reloadChildren(function(node, isOk){ // After loading, look for direct child with that key if(isOk){ tree.logDebug("%s._loadKeyPath(%s) -> reloaded %s.", node, keyPath, node); callback.call(tree, child, "loaded"); node._loadKeyPath(segList.join(tree.options.keyPathSeparator), callback); }else{ tree.logWarning("%s._loadKeyPath(%s) -> reloadChildren() failed.", self, keyPath); callback.call(tree, child, "error"); } }); // Note: this line gives a JSLint warning (Don't make functions within a loop) // we can ignore it, since it will only be exectuted once, the the loop is ended // See also http://stackoverflow.com/questions/3037598/how-to-get-around-the-jslint-error-dont-make-functions-within-a-loop } else { callback.call(tree, child, "loaded"); // Look for direct child with that key child._loadKeyPath(segList.join(tree.options.keyPathSeparator), callback); } return; } } // Could not find key tree.logWarning("Node not found: " + seg); return; }, */ /** * Expand all parents of one or more nodes. * Calls * @param {String | String[]} keyPathList one or more key paths (e.g. '/3/2_1/7') * @param {function} callback callbeck(mode) is called for every visited node ('loaded', 'ok', 'error') * @returns {$.Promise} */ loadKeyPath: function(keyPathList, callback, _rootNode) { var deferredList, dfd, i, path, key, loadMap, node, segList, root = _rootNode || this.rootNode, sep = this.options.keyPathSeparator, self = this; if(!$.isArray(keyPathList)){ keyPathList = [keyPathList]; } // Pass 1: handle all path segments for nodes that are already loaded // Collect distinct top-most lazy nodes in a map loadMap = {}; for(i=0; i doesn't steal focus (see table sample) if( targetType === "expander" ) { // Clicking the expander icon always expands/collapses this._callHook("nodeToggleExpanded", ctx); // this._callHook("nodeSetFocus", ctx, true); // issue 95 } else if( targetType === "checkbox" ) { // Clicking the checkbox always (de)selects this._callHook("nodeToggleSelected", ctx); this._callHook("nodeSetFocus", ctx, true); // issue 95 } else { // Honor `clickFolderMode` for expand = false; activate = true; if( node.folder ) { switch( ctx.options.clickFolderMode ) { case 2: // expand only expand = true; activate = false; break; case 3: // expand and activate activate = true; expand = true; //!node.isExpanded(); break; // else 1 or 4: just activate } } if( activate ) { this.nodeSetFocus(ctx); this._callHook("nodeSetActive", ctx, true); } if( expand ) { if(!activate){ // this._callHook("nodeSetFocus", ctx); } // this._callHook("nodeSetExpanded", ctx, true); this._callHook("nodeToggleExpanded", ctx); } } // Make sure that clicks stop, otherwise jumps to the top if(event.target.localName === "a" && event.target.className === "fancytree-title"){ event.preventDefault(); } // TODO: return promise? }, nodeCollapseSiblings: function(ctx) { // TODO: return promise? var ac, i, l, node = ctx.node; if( node.parent ){ ac = node.parent.children; for (i=0, l=ac.length; i=0; i--) { sib = parents[i].getNextSibling(); if( sib ){ break; } } } _goto(sib); break; default: handled = false; } if(handled){ event.preventDefault(); } }, // /** Default handling for mouse keypress events. */ // nodeKeypress: function(ctx) { // var event = ctx.originalEvent; // }, // /** Trigger lazyload event (async). */ // nodeLazyLoad: function(ctx) { // var node = ctx.node; // if(this._triggerNodeEvent()) // }, /** Load children (async). * source may be * - an array of children * - a node object * - an Ajax options object * - an Ajax.promise * * @param {object} ctx * @param {object[]|object|string|$.Promise|function} source * @returns {$.Promise} The deferred will be resolved as soon as the (ajax) * data was rendered. */ nodeLoadChildren: function(ctx, source) { var ajax, children, delay, dfd, tree = ctx.tree, node = ctx.node, self = this; if($.isFunction(source)){ source = source(); } if(source.url || $.isFunction(source.done)){ tree.nodeSetStatus(ctx, "loading"); if(source.url){ // `source` is an Ajax options object ajax = $.extend({}, ctx.options.ajax, source); if(ajax.debugDelay){ // simulate a slow server delay = ajax.debugDelay; if($.isArray(delay)){ // random delay range [min..max] delay = delay[0] + Math.random() * (delay[1] - delay[0]); } node.debug("nodeLoadChildren waiting debug delay " + Math.round(delay) + "ms"); dfd = $.Deferred(); setTimeout(function(){ ajax.debugDelay = false; self.nodeLoadChildren(ctx, ajax).complete(function(){ dfd.resolve.apply(this, arguments); }); }, delay); return dfd; }else{ dfd = $.ajax(ajax); } }else{ // `source` is a promise, as returned by a $.ajax call dfd = source; } dfd.done(function(data, textStatus, jqXHR){ var res; tree.nodeSetStatus(ctx, "ok"); if(typeof data === "string"){ $.error("Ajax request returned a string (did you get the JSON dataType wrong?)."); } // postProcess is similar to the standard dataFilter hook, // but it is also called for JSONP if( ctx.options.postProcess ){ res = tree._triggerNodeEvent("postProcess", ctx, ctx.originalEvent, {response: data, dataType: this.dataType}); data = $.isArray(res) ? res : data; } else if (data && data.hasOwnProperty("d") && ctx.options.enableAspx ) { // Process ASPX WebMethod JSON object inside "d" property data = (typeof data.d === "string") ? $.parseJSON(data.d) : data.d; } children = data; }).fail(function(jqXHR, textStatus, errorThrown){ tree.nodeSetStatus(ctx, "error", textStatus, jqXHR.status + ": " + errorThrown); alert("error: " + textStatus + " (" + jqXHR.status + ": " + (errorThrown.message || errorThrown) + ")"); }); }else{ // `source` is an array of child objects dfd = $.Deferred(); children = source; dfd.resolve(); } dfd.done(function(){ _assert($.isArray(children), "expected array of children"); node._setChildren(children); if(node.parent){ // if nodeLoadChildren was called for rootNode, the caller must // use tree.render() instead if(node.isVisible()){ tree.nodeRender(ctx); } // trigger fancytreeloadchildren (except for tree-reload) tree._triggerNodeEvent("loadChildren", node); } }).fail(function(){ tree.nodeRender(ctx); }); return dfd; }, // isVisible: function() { // // Return true, if all parents are expanded. // var parents = ctx.node.getParentList(false, false); // for(var i=0, l=parents.length; i= 0); // Unlink to support GC childNode.visit(function(n){ n.parent = null; }, true); if ( opts.removeNode ){ opts.removeNode.call(ctx.tree, {type: "removeNode"}, subCtx); } // remove from child list children.splice(idx, 1); }, /**Remove HTML markup for all descendents of ctx.node. * @param {EventData} ctx */ nodeRemoveChildMarkup: function(ctx) { var node = ctx.node; FT.debug("nodeRemoveChildMarkup()", node.toString()); // TODO: Unlink attr.ftnode to support GC if(node.ul){ $(node.ul).remove(); node.visit(function(n){ n.li = n.ul = null; }); node.ul = null; } }, /**Remove all descendants of ctx.node. * @param {EventData} ctx */ nodeRemoveChildren: function(ctx) { var subCtx, node = ctx.node, children = node.children, opts = ctx.options; FT.debug("nodeRemoveChildren()", node.toString()); if(!children){ return; } if( this.activeNode && this.activeNode.isDescendantOf(node)){ this.activeNode.setActive(false); // TODO: don't fire events } if( this.focusNode && this.focusNode.isDescendantOf(node)){ this.focusNode = null; } // TODO: persist must take care to clear select and expand cookies this.nodeRemoveChildMarkup(ctx); // Unlink children to support GC // TODO: also delete this.children (not possible using visit()) subCtx = $.extend({}, ctx); node.visit(function(n){ n.parent = null; if ( opts.removeNode ){ subCtx.node = n; opts.removeNode.call(ctx.tree, {type: "removeNode"}, subCtx); } }); // Set to 'undefined' which is interpreted as 'not yet loaded' for lazy nodes node.children = undefined; // TODO: ? this._isLoading = false; this.nodeRenderStatus(ctx); }, /**Remove HTML markup for ctx.node and all its descendents. * @param {EventData} ctx */ nodeRemoveMarkup: function(ctx) { var node = ctx.node; FT.debug("nodeRemoveMarkup()", node.toString()); // TODO: Unlink attr.ftnode to support GC if(node.li){ $(node.li).remove(); node.li = null; } this.nodeRemoveChildMarkup(ctx); }, /** * Create `
    • .. ..
    • ` tags for this node. * * This method takes care that all HTML markup is created that is required * to display this node in it's current state. * * Call this method to create new nodes, or after the strucuture * was changed (e.g. after moving this node or adding/removing children) * nodeRenderTitle() and nodeRenderStatus() are implied. * * Note: if a node was created/removed, nodeRender() must be called for the * parent! * *
    • * * * // only present in checkbox mode * * Node 1 * *
        // only present if node has children *
      • child1 ...
      • *
      • child2 ...
      • *
      *
    • *
      * * @param: {EventData} ctx * @param: {Boolean} [force=false] re-render, even if html markup was already created * @param: {Boolean} [deep=false] also render all descendants, even if parent is collapsed * @param: {Boolean} [collapsed=false] force root node to be collapsed, so we can apply animated expand later */ nodeRender: function(ctx, force, deep, collapsed, _recursive) { /* This method must take care of all cases where the current data mode * (i.e. node hierarchy) does not match the current markup. * * - node was not yet rendered: * create markup * - node was rendered: exit fast * - children have been added * - childern have been removed */ var childLI, childNode1, childNode2, i, l, subCtx, node = ctx.node, tree = ctx.tree, opts = ctx.options, aria = opts.aria, firstTime = false, parent = node.parent, isRootNode = !parent, children = node.children; // FT.debug("nodeRender(" + !!force + ", " + !!deep + ")", node.toString()); _assert(isRootNode || parent.ul, "parent UL must exist"); // Render the node if( !isRootNode ){ // Discard markup on force-mode, or if it is not linked to parent
        if(node.li && (force || (node.li.parentNode !== node.parent.ul) ) ){ if(node.li.parentNode !== node.parent.ul){ // alert("unlink " + node + " (must be child of " + node.parent + ")"); this.warn("unlink " + node + " (must be child of " + node.parent + ")"); } // this.debug("nodeRemoveMarkup..."); this.nodeRemoveMarkup(ctx); } // Create
      • // node.debug("render..."); if( !node.li ) { // node.debug("render... really"); firstTime = true; node.li = document.createElement("li"); node.li.ftnode = node; if(aria){ // TODO: why doesn't this work: // node.li.role = "treeitem"; // $(node.li).attr("role", "treeitem") // .attr("aria-labelledby", "ftal_" + node.key); } if( node.key && opts.generateIds ){ node.li.id = opts.idPrefix + node.key; } node.span = document.createElement("span"); node.span.className = "fancytree-node"; if(aria){ $(node.span).attr("aria-labelledby", "ftal_" + node.key); } node.li.appendChild(node.span); // Note: we don't add the LI to the DOM know, but only after we // added all sub elements (hoping that this performs better since // the browser only have to render once) // TODO: benchmarks to prove this // parent.ul.appendChild(node.li); // Create inner HTML for the (expander, checkbox, icon, and title) this.nodeRenderTitle(ctx); // Allow tweaking and binding, after node was created for the first time // tree._triggerNodeEvent("createNode", ctx); if ( opts.createNode ){ opts.createNode.call(tree, {type: "createNode"}, ctx); } }else{ // this.nodeRenderTitle(ctx); } // Allow tweaking after node state was rendered // tree._triggerNodeEvent("renderNode", ctx); if ( opts.renderNode ){ opts.renderNode.call(tree, {type: "renderNode"}, ctx); } } // Visit child nodes if( children ){ if( isRootNode || node.expanded || deep === true ) { // Create a UL to hold the children if( !node.ul ){ node.ul = document.createElement("ul"); if((collapsed === true && !_recursive) || !node.expanded){ // hide top UL, so we can use an animation to show it later node.ul.style.display = "none"; } if(aria){ $(node.ul).attr("role", "group"); } node.li.appendChild(node.ul); } // Add child markup for(i=0, l=children.length; i order matches node.children order. // this.nodeFixOrder(ctx); childLI = node.ul.firstChild; for(i=0, l=children.length-1; i (expander, checkbox, icon, and title). * @param {EventData} ctx */ nodeRenderTitle: function(ctx, title) { // set node connector images, links and text var id, imageSrc, nodeTitle, role, tooltip, node = ctx.node, tree = ctx.tree, opts = ctx.options, aria = opts.aria, level = node.getLevel(), ares = [], icon = node.data.icon; if(title !== undefined){ node.title = title; } if(!node.span){ // Silently bail out if node was not rendered yet, assuming // node.render() will be called as the node becomes visible return; } // connector (expanded, expandable or simple) // TODO: optiimize this if clause if( level < opts.minExpandLevel ) { if(level > 1){ if(aria){ ares.push(""); }else{ ares.push(""); } } // .. else (i.e. for root level) skip expander/connector alltogether } else { if(aria){ ares.push(""); }else{ ares.push(""); } } // Checkbox mode if( opts.checkbox && node.hideCheckbox !== true && !node.isStatusNode ) { if(aria){ ares.push(""); }else{ ares.push(""); } } // folder or doctype icon role = aria ? " role='img'" : ""; if ( icon && typeof icon === "string" ) { imageSrc = (icon.charAt(0) === "/") ? icon : (opts.imagePath + icon); ares.push(""); } else if ( node.data.iconclass ) { // TODO: review and test and document ares.push(""); } else if ( icon === true || (icon !== false && opts.icons !== false) ) { // opts.icons defines the default behavior. // node.icon == true/false can override this ares.push(""); } // node title nodeTitle = ""; // TODO: currently undocumented; may be removed? if ( opts.renderTitle ){ nodeTitle = opts.renderTitle.call(tree, {type: "renderTitle"}, ctx) || ""; } if(!nodeTitle){ // TODO: escape tooltip string tooltip = node.tooltip ? " title='" + node.tooltip.replace(/\"/g, """) + "'" : ""; id = aria ? " id='ftal_" + node.key + "'" : ""; role = aria ? " role='treeitem'" : ""; // href = node.data.href || "#"; // if( opts.nolink || node.nolink ) { // nodeTitle = "" + node.title + ""; nodeTitle = "" + node.title + ""; // } else { // nodeTitle = "" + node.title + ""; // } } ares.push(nodeTitle); // Note: this will trigger focusout, if node had the focus node.span.innerHTML = ares.join(""); }, /** Update element classes according to node state. * @param {EventData} ctx */ nodeRenderStatus: function(ctx) { // Set classes for current status var node = ctx.node, tree = ctx.tree, opts = ctx.options, // nodeContainer = node[tree.nodeContainerAttrName], hasChildren = node.hasChildren(), isLastSib = node.isLastSibling(), aria = opts.aria, // $ariaElem = aria ? $(node[tree.ariaPropName]) : null, $ariaElem = $(node.span).find(".fancytree-title"), cn = opts._classNames, cnList = [], statusElem = node[tree.statusClassPropName]; if( !statusElem ){ // if this function is called for an unrendered node, ignore it (will be updated on nect render anyway) return; } // Build a list of class names that we will add to the node cnList.push(cn.node); if( tree.activeNode === node ){ cnList.push(cn.active); // $(">span.fancytree-title", statusElem).attr("tabindex", "0"); // tree.$container.removeAttr("tabindex"); }else{ // $(">span.fancytree-title", statusElem).removeAttr("tabindex"); // tree.$container.attr("tabindex", "0"); } if( tree.focusNode === node ){ cnList.push(cn.focused); if(aria){ // $(">span.fancytree-title", statusElem).attr("tabindex", "0"); // $(">span.fancytree-title", statusElem).attr("tabindex", "-1"); // TODO: is this the right element for this attribute? $ariaElem .attr("aria-activedescendant", true); // .attr("tabindex", "-1"); } }else if(aria){ // $(">span.fancytree-title", statusElem).attr("tabindex", "-1"); $ariaElem .removeAttr("aria-activedescendant"); // .removeAttr("tabindex"); } if( node.expanded ){ cnList.push(cn.expanded); if(aria){ $ariaElem.attr("aria-expanded", true); } }else if(aria){ $ariaElem.removeAttr("aria-expanded"); } if( node.folder ){ cnList.push(cn.folder); } if( hasChildren !== false ){ cnList.push(cn.hasChildren); } // TODO: required? if( isLastSib ){ cnList.push(cn.lastsib); } if( node.lazy && node.children === null ){ cnList.push(cn.lazy); } if( node.partsel ){ cnList.push(cn.partsel); } if( node.selected ){ cnList.push(cn.selected); if(aria){ $ariaElem.attr("aria-selected", true); } }else if(aria){ $ariaElem.attr("aria-selected", false); } if( node.extraClasses ){ cnList.push(node.extraClasses); } // IE6 doesn't correctly evaluate multiple class names, // so we create combined class names that can be used in the CSS if( hasChildren === false ){ cnList.push(cn.combinedExpanderPrefix + "n" + (isLastSib ? "l" : "") ); }else{ cnList.push(cn.combinedExpanderPrefix + (node.expanded ? "e" : "c") + (node.lazy && node.children === null ? "d" : "") + (isLastSib ? "l" : "") ); } cnList.push(cn.combinedIconPrefix + (node.expanded ? "e" : "c") + (node.folder ? "f" : "") ); // node.span.className = cnList.join(" "); node[tree.statusClassPropName].className = cnList.join(" "); // TODO: we should not set this in the tag also, if we set it here: // Maybe most (all) of the classes should be set in LI instead of SPAN? if(node.li){ node.li.className = isLastSib ? cn.lastsib : ""; } }, /** Activate node. * flag defaults to true. * If flag is true, the node is activated (must be a synchronous operation) * If flag is false, the node is deactivated (must be a synchronous operation) * @param {EventData} ctx * @param {Boolean} [flag=true] */ nodeSetActive: function(ctx, flag) { // Handle user click / [space] / [enter], according to clickFolderMode. var subCtx, node = ctx.node, tree = ctx.tree, opts = ctx.options, // userEvent = !!ctx.originalEvent, isActive = (node === tree.activeNode); // flag defaults to true flag = (flag !== false); node.debug("nodeSetActive", flag); if(isActive === flag){ // Nothing to do return _getResolvedPromise(node); }else if(flag && this._triggerNodeEvent("beforeActivate", node, ctx.originalEvent) === false ){ // Callback returned false return _getRejectedPromise(node, ["rejected"]); } if(flag){ if(tree.activeNode){ _assert(tree.activeNode !== node, "node was active (inconsistency)"); subCtx = $.extend({}, ctx, {node: tree.activeNode}); tree.nodeSetActive(subCtx, false); _assert(tree.activeNode === null, "deactivate was out of sync?"); } if(opts.activeVisible){ tree.nodeMakeVisible(ctx); } tree.activeNode = node; tree.nodeRenderStatus(ctx); tree.nodeSetFocus(ctx); tree._triggerNodeEvent("activate", node); }else{ _assert(tree.activeNode === node, "node was not active (inconsistency)"); tree.activeNode = null; this.nodeRenderStatus(ctx); ctx.tree._triggerNodeEvent("deactivate", node); } }, /** Expand or collapse node, return Deferred.promise. * * @param {EventData} ctx * @param {Boolean} [flag=true] * @returns {$.Promise} The deferred will be resolved as soon as the (lazy) * data was retrieved, rendered, and the expand animation finshed. */ nodeSetExpanded: function(ctx, flag) { var _afterLoad, dfd, i, l, parents, prevAC, node = ctx.node, tree = ctx.tree, opts = ctx.options; // flag defaults to true flag = (flag !== false); node.debug("nodeSetExpanded(" + flag + ")"); // TODO: !!node.expanded is nicer, but doesn't pass jshint // https://github.com/jshint/jshint/issues/455 // if( !!node.expanded === !!flag){ if((node.expanded && flag) || (!node.expanded && !flag)){ // Nothing to do node.debug("nodeSetExpanded(" + flag + "): nothing to do"); return _getResolvedPromise(node); }else if(flag && !node.lazy && !node.hasChildren() ){ // Prevent expanding of empty nodes return _getRejectedPromise(node, ["empty"]); }else if( !flag && node.getLevel() < opts.minExpandLevel ) { // Prevent collapsing locked levels return _getRejectedPromise(node, ["locked"]); }else if ( this._triggerNodeEvent("beforeExpand", node, ctx.originalEvent) === false ){ // Callback returned false return _getRejectedPromise(node, ["rejected"]); } // dfd = new $.Deferred(); // Auto-collapse mode: collapse all siblings if( flag && !node.expanded && opts.autoCollapse ) { parents = node.getParentList(false, true); prevAC = opts.autoCollapse; try{ opts.autoCollapse = false; for(i=0, l=parents.length; iul.fancytree-container").empty(); // TODO: call destructors and remove reference loops tree.rootNode.children = null; }, /** Widget was created (called only once, even it re-initialized). * @param {EventData} ctx */ treeCreate: function(ctx) { }, /** Widget was destroyed. * @param {EventData} ctx */ treeDestroy: function(ctx) { }, /** Widget was (re-)initialized. * @param {EventData} ctx */ treeInit: function(ctx) { //this.debug("Fancytree.treeInit()"); this.treeLoad(ctx); }, /** Parse Fancytree from source, as configured in the options. * @param {EventData} ctx * @param {object} [source] new source */ treeLoad: function(ctx, source) { var type, $ul, tree = ctx.tree, $container = ctx.widget.element, dfd, // calling context for root node rootCtx = $.extend({}, ctx, {node: this.rootNode}); if(tree.rootNode.children){ this.treeClear(ctx); } source = source || this.options.source; if(!source){ type = $container.data("type") || "html"; switch(type){ case "html": $ul = $container.find(">ul:first"); $ul.addClass("ui-fancytree-source ui-helper-hidden"); source = $.ui.fancytree.parseHtml($ul); break; case "json": // $().addClass("ui-helper-hidden"); source = $.parseJSON($container.text()); if(source.children){ if(source.title){tree.title = source.title;} source = source.children; } break; default: $.error("Invalid data-type: " + type); } }else if(typeof source === "string"){ // TODO: source is an element ID _raiseNotImplemented(); } // $container.addClass("ui-widget ui-widget-content ui-corner-all"); // Trigger fancytreeinit after nodes have been loaded dfd = this.nodeLoadChildren(rootCtx, source).done(function(){ tree.render(); if( ctx.options.selectMode === 3 ){ tree.rootNode.fixSelection3FromEndNodes(); } tree._triggerTreeEvent("init", true); }).fail(function(){ tree.render(); tree._triggerTreeEvent("init", false); }); return dfd; }, /* Handle focus and blur events for the container (also fired for child elements). */ treeOnFocusInOut: function(event) { var flag = (event.type === "focusin"), node = $.ui.fancytree.getNode(event); try{ this.debug("treeOnFocusInOut(" + flag + "), node=", node); _assert(!this._inFocusHandler, "Focus handler recursion"); this.systemFocusElement = flag ? event.target : null; this._inFocusHandler = true; if(node){ // For example clicking into an that is part of a node this._callHook("nodeSetFocus", node, flag); }else{ this._callHook("treeSetFocus", this, flag); } }finally{ this._inFocusHandler = false; } }, /* */ treeSetFocus: function(ctx, flag, _calledByNodeSetFocus) { flag = (flag !== false); this.debug("treeSetFocus(" + flag + "), _calledByNodeSetFocus: " + _calledByNodeSetFocus); this.debug(" focusNode: " + this.focusNode); this.debug(" activeNode: " + this.activeNode); // Blur previous tree if any if(FT.focusTree){ if(this !== FT.focusTree || !flag ){ // prev. node looses focus, if prev. tree blurs if(FT.focusTree.focusNode){ FT.focusTree.focusNode.setFocus(false); } FT.focusTree.$container.removeClass("fancytree-focused"); this._triggerTreeEvent("blurTree"); FT.focusTree = null; } } // if( flag && FT.focusTree !== this ){ FT.focusTree = this; this.$container.addClass("fancytree-focused"); // Make sure container gets `:focus` when we clicked inside if( !this.systemFocusElement ){ this.debug("Set `:focus` to container"); this.$container.focus(); } // Set focus to a node if( ! this.focusNode && !_calledByNodeSetFocus){ if( this.activeNode ){ this.activeNode.setFocus(); }else if( this.rootNode.hasChildren()){ this.warn("NOT setting focus to first child"); // this.rootNode.getFirstChild().setFocus(); } } this._triggerTreeEvent("focusTree"); }else{ FT.focusTree = null; } }, /** Re-fire beforeActivate and activate events. */ reactivate: function(setFocus) { var node = this.activeNode; if( node ) { this.activeNode = null; // Force re-activating node.setActive(); if( setFocus ){ node.setFocus(); } } }, // TODO: redraw() /** Reload tree from source and return a promise. * @param source * @returns {$.Promise} */ reload: function(source) { this._callHook("treeClear", this); return this._callHook("treeLoad", this, source); }, /**Render tree (i.e. all top-level nodes). * @param {Boolean} [force=false] * @param {Boolean} [deep=false] */ render: function(force, deep) { return this.rootNode.render(force, deep); }, // TODO: selectKey: function(key, select) // TODO: serializeArray: function(stopOnParents) /** * @param {Boolean} [flag=true] */ setFocus: function(flag) { // _assert(false, "Not implemented"); return this._callHook("treeSetFocus", this, flag); }, /** * Return all nodes as nested list of {@link NodeData}. * * @param {Boolean} [includeRoot=false] Returns the hidden system root node (and it's children) * @param {function} [callback] Called for every node * @returns {Array | object} * @see FancytreeNode#toDict */ toDict: function(includeRoot, callback){ var res = this.rootNode.toDict(true, callback); return includeRoot ? res : res.children; }, /**Implicitly called for string conversions. * @returns {String} */ toString: function(){ return ""; }, /** _trigger a widget event with additional node ctx. * @see EventData */ _triggerNodeEvent: function(type, node, originalEvent, extra) { // this.debug("_trigger(" + type + "): '" + ctx.node.title + "'", ctx); var res, ctx = this._makeHookContext(node, originalEvent); if( extra ) { $.extend(ctx, extra); } res = this.widget._trigger(type, originalEvent, ctx); if(res !== false && ctx.result !== undefined){ return ctx.result; } return res; }, /** _trigger a widget event with additional tree data. */ _triggerTreeEvent: function(type, originalEvent) { // this.debug("_trigger(" + type + ")", ctx); var ctx = this._makeHookContext(this, originalEvent), res = this.widget._trigger(type, originalEvent, ctx); if(res !== false && ctx.result !== undefined){ return ctx.result; } return res; }, /** Call fn(node) for all nodes. * * @param {function} fn the callback function. * Return false to stop iteration, return "skip" to skip this node and children only. * @returns {Boolean} false, if the iterator was stopped. */ visit: function(fn) { return this.rootNode.visit(fn, false); }, /** Write warning to browser console (prepending tree info) * * @param {*} msg string or object or array of such */ warn: function(msg){ Array.prototype.unshift.call(arguments, this.toString()); consoleApply("warn", arguments); } }; /* ****************************************************************************** * jQuery UI widget boilerplate * @ name ui_fancytree * @ class The jQuery.ui.fancytree widget */ /* * @namespace ui */ /* * @namespace ui.fancytree */ /** @namespace $.ui.fancytree */ $.widget("ui.fancytree", /** @lends $.ui.fancytree.prototype */ { /**These options will be used as defaults * @type {FancytreeOptions} */ options: { /** @type {Boolean} Make sure, active nodes are visible (expanded). */ activeVisible: true, ajax: { type: "GET", cache: false, // false: Append random '_' argument to the request url to prevent caching. // timeout: 0, // >0: Make sure we get an ajax error if server is unreachable dataType: "json" // Expect json format and pass json object to callbacks. }, // aria: false, // TODO: default to true autoActivate: true, autoCollapse: false, // autoFocus: false, autoScroll: false, checkbox: false, /**defines click behavior*/ clickFolderMode: 4, debugLevel: null, // 0..2 (null: use global setting $.ui.fancytree.debugInfo) disabled: false, // TODO: required anymore? enableAspx: true, // TODO: document extensions: [], fx: { height: "toggle", duration: 200 }, generateIds: false, icons: true, idPrefix: "ft_", keyboard: true, keyPathSeparator: "/", minExpandLevel: 1, selectMode: 2, strings: { loading: "Loading…", loadError: "Load error!" }, tabbable: true, _classNames: { node: "fancytree-node", folder: "fancytree-folder", combinedExpanderPrefix: "fancytree-exp-", combinedIconPrefix: "fancytree-ico-", hasChildren: "fancytree-has-children", active: "fancytree-active", selected: "fancytree-selected", expanded: "fancytree-expanded", lazy: "fancytree-lazy", focused: "fancytree-focused", partsel: "fancytree-partsel", lastsib: "fancytree-lastsib" }, // events lazyload: null, postProcess: null }, /* Set up the widget, Called on first $().fancytree() */ _create: function() { this.tree = new Fancytree(this); this.$source = this.source || this.element.data("type") === "json" ? this.element : this.element.find(">ul:first"); // Subclass Fancytree instance with all enabled extensions var extension, extName, i, extensions = this.options.extensions, base = this.tree; for(i=0; i"); if(callDefault){ // In jQuery UI 1.8, you have to manually invoke the _setOption method from the base widget $.Widget.prototype._setOption.apply(this, arguments); // TODO: In jQuery UI 1.9 and above, you use the _super method instead // this._super( "_setOption", key, value ); } if(rerender){ this.tree.render(true, false); // force, not-deep } }, /** Use the destroy method to clean up any modifications your widget has made to the DOM */ destroy: function() { this._unbind(); this.tree._callHook("treeDestroy", this.tree); // this.element.removeClass("ui-widget ui-widget-content ui-corner-all"); this.tree.$div.find(">ul.fancytree-container").remove(); this.$source && this.$source.removeClass("ui-helper-hidden"); // In jQuery UI 1.8, you must invoke the destroy method from the base widget $.Widget.prototype.destroy.call(this); // TODO: delete tree and nodes to make garbage collect easier? // TODO: In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method }, // ------------------------------------------------------------------------- /* Remove all event handlers for our namespace */ _unbind: function() { var ns = this.tree._ns; this.element.unbind(ns); this.tree.$container.unbind(ns); $(document).unbind(ns); }, /* Add mouse and kyboard handlers to the container */ _bind: function() { var that = this, opts = this.options, tree = this.tree, ns = tree._ns, selstartEvent = ( $.support.selectstart ? "selectstart" : "mousedown" ); // Remove all previuous handlers for this tree this._unbind(); //alert("keydown" + ns + "foc=" + tree.hasFocus() + tree.$container); tree.debug("bind events; container: ", tree.$container); tree.$container.bind("focusin" + ns + " focusout" + ns, function(event){ tree.debug("Tree container got event " + event.type); tree.treeOnFocusInOut.call(tree, event); }).delegate("span.fancytree-title", selstartEvent + ns, function(event){ // prevent mouse-drags to select text ranges tree.debug(" got event " + event.type); event.preventDefault(); }); // keydown must be bound to document, because $container might not // receive these events $(document).bind("keydown" + ns, function(event){ // TODO: also bind keyup and keypress tree.debug("doc got event " + event.type + ", hasFocus:" + tree.hasFocus()); if(opts.disabled || opts.keyboard === false || !tree.hasFocus()){ return true; } var node = tree.focusNode, // node may be null ctx = tree._makeHookContext(node || tree, event), prevPhase = tree.phase; try { tree.phase = "userEvent"; if(node){ return ( tree._triggerNodeEvent("keydown", node, event) === false ) ? false : tree._callHook("nodeKeydown", ctx); }else{ return ( tree._triggerTreeEvent("keydown", event) === false ) ? false : tree._callHook("nodeKeydown", ctx); } } finally { tree.phase = prevPhase; } }); this.element.bind("click" + ns + " dblclick" + ns, function(event){ if(opts.disabled){ return true; } var ctx, et = FT.getEventTarget(event), node = et.node, tree = that.tree, prevPhase = tree.phase; if( !node ){ return true; // Allow bubbling of other events } ctx = tree._makeHookContext(node, event); // that.tree.debug("event(" + event.type + "): node: ", node); try { tree.phase = "userEvent"; switch(event.type) { case "click": ctx.targetType = et.type; return ( tree._triggerNodeEvent("click", ctx, event) === false ) ? false : tree._callHook("nodeClick", ctx); case "dblclick": ctx.targetType = et.type; return ( tree._triggerNodeEvent("dblclick", ctx, event) === false ) ? false : tree._callHook("nodeDblclick", ctx); } // } catch(e) { // // var _ = null; // issue 117 // TODO // $.error(e); } finally { tree.phase = prevPhase; } }); }, /** @returns {FancytreeNode} the active node or null */ getActiveNode: function() { return this.tree.activeNode; }, /** * @param {String} key * @returns {FancytreeNode} the matching node or null */ getNodeByKey: function(key) { return this.tree.getNodeByKey(key); }, /** @returns {FancytreeNode} the invisible system root node */ getRootNode: function() { return this.tree.rootNode; }, /** @returns {Fancytree} the current tree instance */ getTree: function() { return this.tree; } }); // $.ui.fancytree was created by the widget factory. Create a local shortcut: FT = $.ui.fancytree; /** * Static members in the `$.ui.fancytree` namespace. * @ name $.ui.fancytree * @example: * alert(""version: " + $.ui.fancytree.version); * var node = $.ui.fancytree.() */ $.extend($.ui.fancytree, /** @lends $.ui.fancytree */ { /** @type {String} */ version: "development", /** @type {String} */ buildType: "develop", /** @type {int} */ debugLevel: 2, // used by $.ui.fancytree.debug() and as default for tree.options.debugLevel _nextId: 1, _nextNodeKey: 1, _extensions: {}, focusTree: null, /** Expose class object as $.ui.fancytree._FancytreeClass */ _FancytreeClass: Fancytree, /** Expose class object as $.ui.fancytree._FancytreeNodeClass */ _FancytreeNodeClass: FancytreeNode, /* Feature checks to provide backwards compatibility */ jquerySupports: { // http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at positionMyOfs: isVersionAtLeast($.ui.version, 1, 9) }, assert: function(cond, msg){ return _assert(cond, msg); }, debug: function(msg){ /*jshint expr:true */ ($.ui.fancytree.debugLevel >= 2) && consoleApply("log", arguments); }, error: function(msg){ consoleApply("error", arguments); }, /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event. * * @static * @param {Event} event Mouse event, e.g. click, ... * @returns {String} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined */ getEventTargetType: function(event){ return this.getEventTarget(event).type; }, /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event. * * @param {Event} event Mouse event, e.g. click, ... * @returns {object} Return a {node: FancytreeNode, type: TYPE} object * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined */ getEventTarget: function(event){ var tcn = event && event.target ? event.target.className : "", res = {node: this.getNode(event.target), type: undefined}; // tcn may contains UI themeroller or Font Awesome classes, so we use // a fast version of $(res.node).hasClass() // See http://jsperf.com/test-for-classname/2 if( /\bfancytree-title\b/.test(tcn) ){ res.type = "title"; }else if( /\bfancytree-expander\b/.test(tcn) ){ res.type = (res.node.hasChildren() === false ? "prefix" : "expander"); }else if( /\bfancytree-checkbox\b/.test(tcn) ){ res.type = "checkbox"; }else if( /\bfancytree-icon\b/.test(tcn) ){ res.type = "icon"; }else if( /\bfancytree-node\b/.test(tcn) ){ // TODO: issue #93 (http://code.google.com/p/fancytree/issues/detail?id=93) // res.type = this._getTypeForOuterNodeEvent(event); res.type = "title"; } return res; }, /** Return a FancytreeNode instance from element. * * @param {Element | jQueryObject | Event} el * @returns {FancytreeNode} matching node or null */ getNode: function(el){ if(el instanceof FancytreeNode){ return el; // el already was a FancytreeNode }else if(el.selector !== undefined){ el = el[0]; // el was a jQuery object: use the DOM element }else if(el.originalEvent !== undefined){ el = el.target; // el was an Event } while( el ) { if(el.ftnode) { return el.ftnode; } el = el.parentNode; } return null; }, /* Return a Fancytree instance from element. * TODO: this function could help to get around the data('fancytree') / data('ui-fancytree') problem * @param {Element | jQueryObject | Event} el * @returns {Fancytree} matching tree or null * / getTree: function(el){ if(el instanceof Fancytree){ return el; // el already was a Fancytree }else if(el.selector !== undefined){ el = el[0]; // el was a jQuery object: use the DOM element }else if(el.originalEvent !== undefined){ el = el.target; // el was an Event } ... return null; }, */ info: function(msg){ /*jshint expr:true */ ($.ui.fancytree.debugLevel >= 1) && consoleApply("info", arguments); }, /** * Parse tree data from HTML




© 2015 - 2024 Weber Informatics LLC | Privacy Policy