bkjxxxw/WebContent/static/ace/assets/js/fuelux/fuelux.tree.js

605 lines
19 KiB
JavaScript

/*
* Fuel UX Tree
* https://github.com/ExactTarget/fuelux
*
* Copyright (c) 2014 ExactTarget
* Licensed under the BSD New license.
*/
// -- BEGIN UMD WRAPPER PREFACE --
// For more information on UMD visit:
// https://github.com/umdjs/umd/blob/master/jqueryPlugin.js
(function (factory) {
if (typeof define === 'function' && define.amd) {
// if AMD loader is available, register as an anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS
module.exports = factory(require('jquery'));
} else {
// OR use browser globals if AMD is not present
factory(jQuery);
}
}(function ($) {
// -- END UMD WRAPPER PREFACE --
// -- BEGIN MODULE CODE HERE --
var old = $.fn.tree;
// TREE CONSTRUCTOR AND PROTOTYPE
var Tree = function Tree(element, options) {
this.$element = $(element);
this.options = $.extend({}, $.fn.tree.defaults, options);
if (this.options.itemSelect) {
this.$element.on('click.fu.tree', '.tree-item', $.proxy(function (ev) {
this.selectItem(ev.currentTarget);
}, this));
}
//ACE
this.$element.on('click.fu.tree', '.tree-branch-header', $.proxy(function (ev) {
this.toggleFolder(ev.currentTarget);
}, this));
//ACE
// folderSelect default is true
if (this.options.folderSelect) {
this.$element.addClass('tree-folder-select');
this.$element.off('click.fu.tree', '.tree-branch-header');
this.$element.on('click.fu.tree', '.icon-caret', $.proxy(function (ev) {
this.toggleFolder($(ev.currentTarget).next());
}, this));
this.$element.on('click.fu.tree', '.tree-branch-header', $.proxy(function (ev) {
this.selectFolder($(ev.currentTarget));
}, this));
}
this.render();
};
Tree.prototype = {
constructor: Tree,
deselectAll: function deselectAll(nodes) {
// clear all child tree nodes and style as deselected
nodes = nodes || this.$element;
var $selectedElements = $(nodes).find('.tree-selected');
$selectedElements.each(function (index, element) {
//styleNodeDeselected( $(element), $(element).find( '.glyphicon' ) );
styleNodeDeselected( this, $(element), $(element).find('.'+$.trim(this.options['base-icon']).replace(/(\s+)/g, '.')) );//ACE
});
return $selectedElements;
},
destroy: function destroy() {
// any external bindings [none]
// empty elements to return to original markup
this.$element.find("li:not([data-template])").remove();
this.$element.remove();
// returns string of markup
return this.$element[0].outerHTML;
},
render: function render() {
this.populate(this.$element);
},
populate: function populate($el) {
var self = this;
var $parent = ($el.hasClass('tree')) ? $el : $el.parent();
var loader = $parent.find('.tree-loader:eq(0)');
var treeData = $parent.data();
loader.removeClass('hide hidden'); // hide is deprecated
this.options.dataSource(treeData ? treeData : {}, function (items) {
loader.addClass('hidden');
$.each(items.data, function (index, value) {
var $entity;
if (value.type === 'folder') {
$entity = self.$element.find('[data-template=treebranch]:eq(0)').clone().removeClass('hide hidden').removeData('template'); // hide is deprecated
$entity.data(value);
$entity.find('.tree-branch-name > .tree-label').html(value.text || value.name);
//ACE
var header = $entity.find('.tree-branch-header');
if('icon-class' in value)
header.find('i').addClass(value['icon-class']);
if('additionalParameters' in value
&& 'item-selected' in value.additionalParameters
&& value.additionalParameters['item-selected'] == true) {
setTimeout(function(){header.trigger('click')}, 0);
}
} else if (value.type === 'item') {
$entity = self.$element.find('[data-template=treeitem]:eq(0)').clone().removeClass('hide hidden').removeData('template'); // hide is deprecated
$entity.find('.tree-item-name > .tree-label').html(value.text || value.name);
$entity.data(value);
//ACE
if('additionalParameters' in value
&& 'item-selected' in value.additionalParameters
&& value.additionalParameters['item-selected'] == true) {
$entity.addClass ('tree-selected');
$entity.find('i').removeClass(self.options['unselected-icon']).addClass(self.options['selected-icon']);
//$entity.closest('.tree-folder-content').show();
}
}
// Decorate $entity with data or other attributes making the
// element easily accessable with libraries like jQuery.
//
// Values are contained within the object returned
// for folders and items as attr:
//
// {
// text: "An Item",
// type: 'item',
// attr = {
// 'classes': 'required-item red-text',
// 'data-parent': parentId,
// 'guid': guid,
// 'id': guid
// }
// };
//
// the "name" attribute is also supported but is deprecated for "text".
// add attributes to tree-branch or tree-item
var attr = value.attr || value.dataAttributes || [];
$.each(attr, function (key, value) {
switch (key) {
case 'cssClass':
case 'class':
case 'className':
$entity.addClass(value);
break;
// allow custom icons
case 'data-icon':
$entity.find('.icon-item').removeClass().addClass('icon-item ' + value);
$entity.attr(key, value);
break;
// ARIA support
case 'id':
$entity.attr(key, value);
$entity.attr('aria-labelledby', value + '-label');
$entity.find('.tree-branch-name > .tree-label').attr('id', value + '-label');
break;
// style, data-*
default:
$entity.attr(key, value);
break;
}
});
// add child nodes
if ($el.hasClass('tree-branch-header')) {
$parent.find('.tree-branch-children:eq(0)').append($entity);
} else {
$el.append($entity);
}
});
// return newly populated folder
self.$element.trigger('loaded.fu.tree', $parent);
});
},
selectTreeNode: function selectItem(clickedElement, nodeType) {
var clicked = {}; // object for clicked element
clicked.$element = $(clickedElement);
var selected = {}; // object for selected elements
selected.$elements = this.$element.find('.tree-selected');
selected.dataForEvent = [];
// determine clicked element and it's icon
if (nodeType === 'folder') {
// make the clicked.$element the container branch
clicked.$element = clicked.$element.closest('.tree-branch');
clicked.$icon = clicked.$element.find('.icon-folder');
}
else {
clicked.$icon = clicked.$element.find('.icon-item');
}
clicked.elementData = clicked.$element.data();
// the below functions pass objects by copy/reference and use modified object in this function
if ( this.options.multiSelect ) {
multiSelectSyncNodes(this, clicked, selected);
}
else {
singleSelectSyncNodes(this, clicked, selected);
}
// all done with the DOM, now fire events
this.$element.trigger(selected.eventType + '.fu.tree', {
target: clicked.elementData,
selected: selected.dataForEvent
});
clicked.$element.trigger('updated.fu.tree', {
selected: selected.dataForEvent,
item: clicked.$element,
eventType: selected.eventType
});
},
discloseFolder: function discloseFolder(el) {
var $el = $(el);
var $branch = $el.closest('.tree-branch');
var $treeFolderContent = $branch.find('.tree-branch-children');
var $treeFolderContentFirstChild = $treeFolderContent.eq(0);
//take care of the styles
$branch.addClass('tree-open');
$branch.attr('aria-expanded', 'true');
$treeFolderContentFirstChild.removeClass('hide hidden'); // hide is deprecated
$branch.find('> .tree-branch-header .icon-folder').eq(0)
//.removeClass('glyphicon-folder-close')
//.addClass('glyphicon-folder-open');
.removeClass(this.options['close-icon']).addClass(this.options['open-icon']);//ACE
$branch.find('> .icon-caret').eq(0)
.removeClass(this.options['folder-open-icon']).addClass(this.options['folder-close-icon']);//ACE
//add the children to the folder
if (!$treeFolderContent.children().length) {
this.populate($treeFolderContent);
}
this.$element.trigger('disclosedFolder.fu.tree', $branch.data());
},
closeFolder: function closeFolder(el) {
var $el = $(el);
var $branch = $el.closest('.tree-branch');
var $treeFolderContent = $branch.find('.tree-branch-children');
var $treeFolderContentFirstChild = $treeFolderContent.eq(0);
//take care of the styles
$branch.removeClass('tree-open');
$branch.attr('aria-expanded', 'false');
$treeFolderContentFirstChild.addClass('hidden');
$branch.find('> .tree-branch-header .icon-folder').eq(0)
//.removeClass('glyphicon-folder-open')
//.addClass('glyphicon-folder-close');
.removeClass(this.options['open-icon']).addClass(this.options['close-icon']);//ACE
$branch.find('> .icon-caret').eq(0)
.removeClass(this.options['folder-close-icon']).addClass(this.options['folder-open-icon']);//ACE
// remove chidren if no cache
if (!this.options.cacheItems) {
$treeFolderContentFirstChild.empty();
}
this.$element.trigger('closed.fu.tree', $branch.data());
},
toggleFolder: function toggleFolder(el) {
var $el = $(el);
/**
if ($el.find('.glyphicon-folder-close').length) {
this.discloseFolder(el);
} else if ($el.find('.glyphicon-folder-open').length) {
this.closeFolder(el);
}
*/
if ($el.find('.'+$.trim(this.options['close-icon']).replace(/(\s+)/g, '.')).length) {//ACE
this.discloseFolder(el);
} else if($el.find('.'+$.trim(this.options['open-icon']).replace(/(\s+)/g, '.')).length) {//ACE
this.closeFolder(el);
}
},
selectFolder: function selectFolder(el) {
if (this.options.folderSelect) {
this.selectTreeNode(el, 'folder');
}
},
selectItem: function selectItem(el) {
if (this.options.itemSelect) {
this.selectTreeNode(el, 'item');
}
},
selectedItems: function selectedItems() {
var $sel = this.$element.find('.tree-selected');
var data = [];
$.each($sel, function (index, value) {
data.push($(value).data());
});
return data;
},
// collapses open folders
collapse: function collapse() {
var self = this;
var reportedClosed = [];
var closedReported = function closedReported(event, closed) {
reportedClosed.push(closed);
// hide is deprecated
if (self.$element.find(".tree-branch.tree-open:not('.hidden, .hide')").length === 0) {
self.$element.trigger('closedAll.fu.tree', {
tree: self.$element,
reportedClosed: reportedClosed
});
self.$element.off('loaded.fu.tree', self.$element, closedReported);
}
};
//trigger callback when all folders have reported closed
self.$element.on('closed.fu.tree', closedReported);
self.$element.find(".tree-branch.tree-open:not('.hidden, .hide')").each(function () {
self.closeFolder(this);
});
},
//disclose visible will only disclose visible tree folders
discloseVisible: function discloseVisible() {
var self = this;
var $openableFolders = self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')");
var reportedOpened = [];
var openReported = function openReported(event, opened) {
reportedOpened.push(opened);
if (reportedOpened.length === $openableFolders.length) {
self.$element.trigger('disclosedVisible.fu.tree', {
tree: self.$element,
reportedOpened: reportedOpened
});
/*
* Unbind the `openReported` event. `discloseAll` may be running and we want to reset this
* method for the next iteration.
*/
self.$element.off('loaded.fu.tree', self.$element, openReported);
}
};
//trigger callback when all folders have reported opened
self.$element.on('loaded.fu.tree', openReported);
// open all visible folders
self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')").each(function triggerOpen() {
self.discloseFolder($(this).find('.tree-branch-header'));
});
},
/**
* Disclose all will keep listening for `loaded.fu.tree` and if `$(tree-el).data('ignore-disclosures-limit')`
* is `true` (defaults to `true`) it will attempt to disclose any new closed folders than were
* loaded in during the last disclosure.
*/
discloseAll: function discloseAll() {
var self = this;
//first time
if (typeof self.$element.data('disclosures') === 'undefined') {
self.$element.data('disclosures', 0);
}
var isExceededLimit = (self.options.disclosuresUpperLimit >= 1 && self.$element.data('disclosures') >= self.options.disclosuresUpperLimit);
var isAllDisclosed = self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')").length === 0;
if (!isAllDisclosed) {
if (isExceededLimit) {
self.$element.trigger('exceededDisclosuresLimit.fu.tree', {
tree: self.$element,
disclosures: self.$element.data('disclosures')
});
/*
* If you've exceeded the limit, the loop will be killed unless you
* explicitly ignore the limit and start the loop again:
*
* $tree.one('exceededDisclosuresLimit.fu.tree', function () {
* $tree.data('ignore-disclosures-limit', true);
* $tree.tree('discloseAll');
* });
*/
if (!self.$element.data('ignore-disclosures-limit')) {
return;
}
}
self.$element.data('disclosures', self.$element.data('disclosures') + 1);
/*
* A new branch that is closed might be loaded in, make sure those get handled too.
* This attachment needs to occur before calling `discloseVisible` to make sure that
* if the execution of `discloseVisible` happens _super fast_ (as it does in our QUnit tests
* this will still be called. However, make sure this only gets called _once_, because
* otherwise, every single time we go through this loop, _another_ event will be bound
* and then when the trigger happens, this will fire N times, where N equals the number
* of recursive `discloseAll` executions (instead of just one)
*/
self.$element.one('disclosedVisible.fu.tree', function () {
self.discloseAll();
});
/*
* If the page is very fast, calling this first will cause `disclosedVisible.fu.tree` to not
* be bound in time to be called, so, we need to call this last so that the things bound
* and triggered above can have time to take place before the next execution of the
* `discloseAll` method.
*/
self.discloseVisible();
} else {
self.$element.trigger('disclosedAll.fu.tree', {
tree: self.$element,
disclosures: self.$element.data('disclosures')
});
//if `cacheItems` is false, and they call closeAll, the data is trashed and therefore
//disclosures needs to accurately reflect that
if (!self.options.cacheItems) {
self.$element.one('closeAll.fu.tree', function () {
self.$element.data('disclosures', 0);
});
}
}
}
};
// ALIASES
//alias for collapse for consistency. "Collapse" is an ambiguous term (collapse what? All? One specific branch?)
Tree.prototype.closeAll = Tree.prototype.collapse;
//alias for backwards compatibility because there's no reason not to.
Tree.prototype.openFolder = Tree.prototype.discloseFolder;
//For library consistency
Tree.prototype.getValue = Tree.prototype.selectedItems;
// PRIVATE FUNCTIONS
function styleNodeSelected (self, $element, $icon) {
$element.addClass('tree-selected');
if ( $element.data('type') === 'item' && $icon.hasClass(self.options['unselected-icon']) ) {
//$icon.removeClass('fueluxicon-bullet').addClass('glyphicon-ok'); // make checkmark
$icon.removeClass(self.options['unselected-icon']).addClass(self.options['selected-icon']); //ACE
}
}
function styleNodeDeselected (self, $element, $icon) {
$element.removeClass('tree-selected');
//if ( $element.data('type') === 'item' && $icon.hasClass('glyphicon-ok') ) {
//$icon.removeClass('glyphicon-ok').addClass('fueluxicon-bullet'); // make bullet
//}
//ACE
if ( $element.data('type') === 'item' && $icon.hasClass(self.options['selected-icon']) ) {
$icon.removeClass(self.options['selected-icon']).addClass(self.options['unselected-icon']); // make bullet
}
}
function multiSelectSyncNodes (self, clicked, selected) {
// search for currently selected and add to selected data list if needed
$.each(selected.$elements, function (index, element) {
var $element = $(element);
if ($element[0] !== clicked.$element[0]) {
selected.dataForEvent.push( $($element).data() );
}
});
if (clicked.$element.hasClass('tree-selected')) {
styleNodeDeselected (self, clicked.$element, clicked.$icon);//ACE
// set event data
selected.eventType = 'deselected';
}
else {
styleNodeSelected(self, clicked.$element, clicked.$icon);//ACE
// set event data
selected.eventType = 'selected';
selected.dataForEvent.push(clicked.elementData);
}
}
function singleSelectSyncNodes(self, clicked, selected) {
// element is not currently selected
if (selected.$elements[0] !== clicked.$element[0]) {
var clearedElements = self.deselectAll(self.$element);
styleNodeSelected(self, clicked.$element, clicked.$icon);//ACE
// set event data
selected.eventType = 'selected';
selected.dataForEvent = [clicked.elementData];
}
else {
styleNodeDeselected(self, clicked.$element, clicked.$icon);//ACE
// set event data
selected.eventType = 'deselected';
selected.dataForEvent = [];
}
}
// TREE PLUGIN DEFINITION
$.fn.tree = function tree(option) {
var args = Array.prototype.slice.call(arguments, 1);
var methodReturn;
var $set = this.each(function () {
var $this = $(this);
var data = $this.data('fu.tree');
var options = typeof option === 'object' && option;
if (!data) {
$this.data('fu.tree', (data = new Tree(this, options)));
}
if (typeof option === 'string') {
methodReturn = data[option].apply(data, args);
}
});
return (methodReturn === undefined) ? $set : methodReturn;
};
$.fn.tree.defaults = {
dataSource: function dataSource(options, callback) {},
multiSelect: false,
cacheItems: true,
folderSelect: true,
itemSelect: true,
/*
* How many times `discloseAll` should be called before a stopping and firing
* an `exceededDisclosuresLimit` event. You can force it to continue by
* listening for this event, setting `ignore-disclosures-limit` to `true` and
* starting `discloseAll` back up again. This lets you make more decisions
* about if/when/how/why/how many times `discloseAll` will be started back
* up after it exceeds the limit.
*
* $tree.one('exceededDisclosuresLimit.fu.tree', function () {
* $tree.data('ignore-disclosures-limit', true);
* $tree.tree('discloseAll');
* });
*
* `disclusuresUpperLimit` defaults to `0`, so by default this trigger
* will never fire. The true hard the upper limit is the browser's
* ability to load new items (i.e. it will keep loading until the browser
* falls over and dies). On the Fuel UX `index.html` page, the point at
* which the page became super slow (enough to seem almost unresponsive)
* was `4`, meaning 256 folders had been opened, and 1024 were attempting to open.
*/
disclosuresUpperLimit: 0
};
$.fn.tree.Constructor = Tree;
$.fn.tree.noConflict = function () {
$.fn.tree = old;
return this;
};
// NO DATA-API DUE TO NEED OF DATA-SOURCE
// -- BEGIN UMD WRAPPER AFTERWORD --
}));
// -- END UMD WRAPPER AFTERWORD --