Administrator
2023-04-21 195945efc5db921a4c9eb8cf9421c172273293f5
提交 | 用户 | 时间
58d006 1 /*
A 2  * Fuel UX Tree
3  * https://github.com/ExactTarget/fuelux
4  *
5  * Copyright (c) 2014 ExactTarget
6  * Licensed under the BSD New license.
7  */
8
9 // -- BEGIN UMD WRAPPER PREFACE --
10
11 // For more information on UMD visit:
12 // https://github.com/umdjs/umd/blob/master/jqueryPlugin.js
13
14 (function (factory) {
15     if (typeof define === 'function' && define.amd) {
16         // if AMD loader is available, register as an anonymous module.
17         define(['jquery'], factory);
18     } else if (typeof exports === 'object') {
19         // Node/CommonJS
20         module.exports = factory(require('jquery'));
21     } else {
22         // OR use browser globals if AMD is not present
23         factory(jQuery);
24     }
25 }(function ($) {
26     // -- END UMD WRAPPER PREFACE --
27
28     // -- BEGIN MODULE CODE HERE --
29
30     var old = $.fn.tree;
31
32     // TREE CONSTRUCTOR AND PROTOTYPE
33
34     var Tree = function Tree(element, options) {
35         this.$element = $(element);
36         this.options = $.extend({}, $.fn.tree.defaults, options);
37
38         if (this.options.itemSelect) {
39             this.$element.on('click.fu.tree', '.tree-item', $.proxy(function (ev) {
40                 this.selectItem(ev.currentTarget);
41             }, this));
42         }
43
44         //ACE
45         this.$element.on('click.fu.tree', '.tree-branch-header', $.proxy(function (ev) {
46             this.toggleFolder(ev.currentTarget);
47         }, this));
48
49         // folderSelect default is true
50         if (this.options.folderSelect) {
51             this.$element.addClass('tree-folder-select');
52             this.$element.off('click.fu.tree', '.tree-branch-header');
53             this.$element.on('click.fu.tree', '.icon-caret', $.proxy(function (ev) {
54                 this.toggleFolder($(ev.currentTarget).next());
55             }, this));
56             this.$element.on('click.fu.tree', '.tree-branch-header', $.proxy(function (ev) {
57                 this.selectFolder($(ev.currentTarget));
58             }, this));
59         }
60
61         this.render();
62     };
63
64     Tree.prototype = {
65         constructor: Tree,
66
67         deselectAll: function deselectAll(nodes) {
68             // clear all child tree nodes and style as deselected
69             nodes = nodes || this.$element;
70             var that = this;
71             var $selectedElements = $(nodes).find('.tree-selected');
72             $selectedElements.each(function (index, element) {
73                 //styleNodeDeselected( $(element), $(element).find( '.glyphicon' ) );
74                 styleNodeDeselected( that, $(element), $(element).find('.'+$.trim(that.options['base-icon']).replace(/(\s+)/g, '.')) );//ACE
75             });
76             return $selectedElements;
77         },
78
79         destroy: function destroy() {
80             // any external bindings [none]
81             // empty elements to return to original markup
82             this.$element.find("li:not([data-template])").remove();
83
84             this.$element.remove();
85             // returns string of markup
86             return this.$element[0].outerHTML;
87         },
88
89         render: function render() {
90             this.populate(this.$element);
91         },
92
93         populate: function populate($el, isBackgroundProcess) {
94             var self = this;
95             var $parent = ($el.hasClass('tree')) ? $el : $el.parent();
96             var loader = $parent.find('.tree-loader:eq(0)');
97             var treeData = $parent.data();
98             isBackgroundProcess = isBackgroundProcess || false;    // no user affordance needed (ex.- "loading")
99
100             if (isBackgroundProcess === false) {
101                 loader.removeClass('hide hidden'); // hide is deprecated
102             }
103             this.options.dataSource(treeData ? treeData : {}, function (items) {
104                 loader.addClass('hidden');
105
106                 $.each(items.data, function (index, value) {
107                     var $entity;
108
109                     if (value.type === 'folder') {
110                         $entity = self.$element.find('[data-template=treebranch]:eq(0)').clone().removeClass('hide hidden').removeData('template'); // hide is deprecated
111                         $entity.data(value);
112                         $entity.find('.tree-branch-name > .tree-label').html(value.text || value.name);
113                         
114                         //ACE
115                         var header = $entity.find('.tree-branch-header');
116
117                         if('icon-class' in value)
118                             header.find('i').addClass(value['icon-class']);
119                         
120                         if('additionalParameters' in value
121                             && 'item-selected' in value.additionalParameters 
122                                 && value.additionalParameters['item-selected'] == true) {
123                                 setTimeout(function(){header.trigger('click')}, 0);
124                             }
125                     } else if (value.type === 'item') {
126                         $entity = self.$element.find('[data-template=treeitem]:eq(0)').clone().removeClass('hide hidden').removeData('template'); // hide is deprecated
127                         $entity.find('.tree-item-name > .tree-label').html(value.text || value.name);
128                         $entity.data(value);
129                         
130                         //ACE
131                         if('additionalParameters' in value
132                             && 'item-selected' in value.additionalParameters 
133                                 && value.additionalParameters['item-selected'] == true) {
134                                 $entity.addClass ('tree-selected');
135                                 $entity.find('i').removeClass(self.options['unselected-icon']).addClass(self.options['selected-icon']);
136                                 //$entity.closest('.tree-folder-content').show();
137                         }
138                     }
139
140                     // Decorate $entity with data or other attributes making the
141                     // element easily accessable with libraries like jQuery.
142                     //
143                     // Values are contained within the object returned
144                     // for folders and items as attr:
145                     //
146                     // {
147                     //     text: "An Item",
148                     //     type: 'item',
149                     //     attr = {
150                     //         'classes': 'required-item red-text',
151                     //         'data-parent': parentId,
152                     //         'guid': guid,
153                     //         'id': guid
154                     //     }
155                     // };
156                     //
157                     // the "name" attribute is also supported but is deprecated for "text".
158
159                     // add attributes to tree-branch or tree-item
160                     var attr = value.attr || value.dataAttributes || [];
161                     $.each(attr, function (key, value) {
162                         switch (key) {
163                             case 'cssClass':
164                             case 'class':
165                             case 'className':
166                                 $entity.addClass(value);
167                                 break;
168
169                             // allow custom icons
170                             case 'data-icon':
171                                 $entity.find('.icon-item').removeClass().addClass('icon-item ' + value);
172                                 $entity.attr(key, value);
173                                 break;
174
175                             // ARIA support
176                             case 'id':
177                                 $entity.attr(key, value);
178                                 $entity.attr('aria-labelledby', value + '-label');
179                                 $entity.find('.tree-branch-name > .tree-label').attr('id', value + '-label');
180                                 break;
181
182                             // style, data-*
183                             default:
184                                 $entity.attr(key, value);
185                                 break;
186                         }
187                     });
188
189                     // add child nodes
190                     if ($el.hasClass('tree-branch-header')) {
191                         $parent.find('.tree-branch-children:eq(0)').append($entity);
192                     } else {
193                         $el.append($entity);
194                     }
195                 });
196
197                 // return newly populated folder
198                 self.$element.trigger('loaded.fu.tree', $parent);
199             });
200         },
201
202         selectTreeNode: function selectItem(clickedElement, nodeType) {
203             var clicked = {};    // object for clicked element
204             clicked.$element = $(clickedElement);
205
206             var selected = {}; // object for selected elements
207             selected.$elements = this.$element.find('.tree-selected');
208             selected.dataForEvent = [];
209
210             // determine clicked element and it's icon
211             if (nodeType === 'folder') {
212                 // make the clicked.$element the container branch
213                 clicked.$element = clicked.$element.closest('.tree-branch');
214                 clicked.$icon = clicked.$element.find('.icon-folder');
215             }
216             else {
217                 clicked.$icon = clicked.$element.find('.icon-item');
218             }
219             clicked.elementData = clicked.$element.data();
220
221             // the below functions pass objects by copy/reference and use modified object in this function
222             if ( this.options.multiSelect ) {
223                 multiSelectSyncNodes(this, clicked, selected);
224             }
225             else {
226                 singleSelectSyncNodes(this, clicked, selected);
227             }
228
229             // all done with the DOM, now fire events
230             this.$element.trigger(selected.eventType + '.fu.tree', {
231                 target: clicked.elementData,
232                 selected: selected.dataForEvent
233             });
234
235             clicked.$element.trigger('updated.fu.tree', {
236                 selected: selected.dataForEvent,
237                 item: clicked.$element,
238                 eventType: selected.eventType
239             });
240         },
241
242         discloseFolder: function discloseFolder(el) {
243             var $el = $(el);
244
245             var $branch = $el.closest('.tree-branch');
246             var $treeFolderContent = $branch.find('.tree-branch-children');
247             var $treeFolderContentFirstChild = $treeFolderContent.eq(0);
248
249             //take care of the styles
250             $branch.addClass('tree-open');
251             $branch.attr('aria-expanded', 'true');
252             $treeFolderContentFirstChild.removeClass('hide hidden'); // hide is deprecated
253             $branch.find('> .tree-branch-header .icon-folder').eq(0)
254                 //.removeClass('glyphicon-folder-close')
255                 //.addClass('glyphicon-folder-open');
256                 .removeClass(this.options['close-icon']).addClass(this.options['open-icon']);//ACE
257                 
258             $branch.find('> .icon-caret').eq(0)
259                 .removeClass(this.options['folder-open-icon']).addClass(this.options['folder-close-icon']);//ACE
260
261             //add the children to the folder
262             if (!$treeFolderContent.children().length) {
263                 this.populate($treeFolderContent);
264             }
265
266             this.$element.trigger('disclosedFolder.fu.tree', $branch.data());
267         },
268
269         closeFolder: function closeFolder(el) {
270             var $el = $(el);
271             var $branch = $el.closest('.tree-branch');
272             var $treeFolderContent = $branch.find('.tree-branch-children');
273             var $treeFolderContentFirstChild = $treeFolderContent.eq(0);
274
275             //take care of the styles
276             $branch.removeClass('tree-open');
277             $branch.attr('aria-expanded', 'false');
278             $treeFolderContentFirstChild.addClass('hidden');
279             $branch.find('> .tree-branch-header .icon-folder').eq(0)
280                 //.removeClass('glyphicon-folder-open')
281                 //.addClass('glyphicon-folder-close');
282                 .removeClass(this.options['open-icon']).addClass(this.options['close-icon']);//ACE
283                 
284             $branch.find('> .icon-caret').eq(0)
285                 .removeClass(this.options['folder-close-icon']).addClass(this.options['folder-open-icon']);//ACE
286             
287             // remove chidren if no cache
288             if (!this.options.cacheItems) {
289                 $treeFolderContentFirstChild.empty();
290             }
291
292             this.$element.trigger('closed.fu.tree', $branch.data());
293         },
294
295         toggleFolder: function toggleFolder(el) {
296             var $el = $(el);
297
298             /**
299             if ($el.find('.glyphicon-folder-close').length) {
300                 this.discloseFolder(el);
301             } else if ($el.find('.glyphicon-folder-open').length) {
302                 this.closeFolder(el);
303             }
304             */
305             if ($el.find('.'+$.trim(this.options['close-icon']).replace(/(\s+)/g, '.')).length) {//ACE
306                 this.discloseFolder(el);
307             } else if($el.find('.'+$.trim(this.options['open-icon']).replace(/(\s+)/g, '.')).length) {//ACE
308                 this.closeFolder(el);
309             }
310         },
311
312         selectFolder: function selectFolder(el) {
313             if (this.options.folderSelect) {
314                 this.selectTreeNode(el, 'folder');
315             }
316         },
317
318         selectItem: function selectItem(el) {
319             if (this.options.itemSelect) {
320                 this.selectTreeNode(el, 'item');
321             }
322         },
323
324         selectedItems: function selectedItems() {
325             var $sel = this.$element.find('.tree-selected');
326             var data = [];
327
328             $.each($sel, function (index, value) {
329                 data.push($(value).data());
330             });
331             return data;
332         },
333
334         // collapses open folders
335         collapse: function collapse() {
336             var self = this;
337             var reportedClosed = [];
338
339             var closedReported = function closedReported(event, closed) {
340                 reportedClosed.push(closed);
341
342                 // hide is deprecated
343                 if (self.$element.find(".tree-branch.tree-open:not('.hidden, .hide')").length === 0) {
344                     self.$element.trigger('closedAll.fu.tree', {
345                         tree: self.$element,
346                         reportedClosed: reportedClosed
347                     });
348                     self.$element.off('loaded.fu.tree', self.$element, closedReported);
349                 }
350             };
351
352             //trigger callback when all folders have reported closed
353             self.$element.on('closed.fu.tree', closedReported);
354
355             self.$element.find(".tree-branch.tree-open:not('.hidden, .hide')").each(function () {
356                 self.closeFolder(this);
357             });
358         },
359
360         //disclose visible will only disclose visible tree folders
361         discloseVisible: function discloseVisible() {
362             var self = this;
363
364             var $openableFolders = self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')");
365             var reportedOpened = [];
366
367             var openReported = function openReported(event, opened) {
368                 reportedOpened.push(opened);
369
370                 if (reportedOpened.length === $openableFolders.length) {
371                     self.$element.trigger('disclosedVisible.fu.tree', {
372                         tree: self.$element,
373                         reportedOpened: reportedOpened
374                     });
375                     /*
376                     * Unbind the `openReported` event. `discloseAll` may be running and we want to reset this
377                     * method for the next iteration.
378                     */
379                     self.$element.off('loaded.fu.tree', self.$element, openReported);
380                 }
381             };
382
383             //trigger callback when all folders have reported opened
384             self.$element.on('loaded.fu.tree', openReported);
385
386             // open all visible folders
387             self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')").each(function triggerOpen() {
388                 self.discloseFolder($(this).find('.tree-branch-header'));
389             });
390         },
391
392         /**
393         * Disclose all will keep listening for `loaded.fu.tree` and if `$(tree-el).data('ignore-disclosures-limit')`
394         * is `true` (defaults to `true`) it will attempt to disclose any new closed folders than were
395         * loaded in during the last disclosure.
396         */
397         discloseAll: function discloseAll() {
398             var self = this;
399
400             //first time
401             if (typeof self.$element.data('disclosures') === 'undefined') {
402                 self.$element.data('disclosures', 0);
403             }
404
405             var isExceededLimit = (self.options.disclosuresUpperLimit >= 1 && self.$element.data('disclosures') >= self.options.disclosuresUpperLimit);
406             var isAllDisclosed = self.$element.find(".tree-branch:not('.tree-open, .hidden, .hide')").length === 0;
407
408
409             if (!isAllDisclosed) {
410                 if (isExceededLimit) {
411                     self.$element.trigger('exceededDisclosuresLimit.fu.tree', {
412                         tree: self.$element,
413                         disclosures: self.$element.data('disclosures')
414                     });
415
416                     /*
417                     * If you've exceeded the limit, the loop will be killed unless you
418                     * explicitly ignore the limit and start the loop again:
419                     *
420                     *    $tree.one('exceededDisclosuresLimit.fu.tree', function () {
421                     *        $tree.data('ignore-disclosures-limit', true);
422                     *        $tree.tree('discloseAll');
423                     *    });
424                     */
425                     if (!self.$element.data('ignore-disclosures-limit')) {
426                         return;
427                     }
428
429                 }
430
431                 self.$element.data('disclosures', self.$element.data('disclosures') + 1);
432
433                 /*
434                 * A new branch that is closed might be loaded in, make sure those get handled too.
435                 * This attachment needs to occur before calling `discloseVisible` to make sure that
436                 * if the execution of `discloseVisible` happens _super fast_ (as it does in our QUnit tests
437                 * this will still be called. However, make sure this only gets called _once_, because
438                 * otherwise, every single time we go through this loop, _another_ event will be bound
439                 * and then when the trigger happens, this will fire N times, where N equals the number
440                 * of recursive `discloseAll` executions (instead of just one)
441                 */
442                 self.$element.one('disclosedVisible.fu.tree', function () {
443                     self.discloseAll();
444                 });
445
446                 /*
447                 * If the page is very fast, calling this first will cause `disclosedVisible.fu.tree` to not
448                 * be bound in time to be called, so, we need to call this last so that the things bound
449                 * and triggered above can have time to take place before the next execution of the
450                 * `discloseAll` method.
451                 */
452                 self.discloseVisible();
453             } else {
454                 self.$element.trigger('disclosedAll.fu.tree', {
455                     tree: self.$element,
456                     disclosures: self.$element.data('disclosures')
457                 });
458
459                 //if `cacheItems` is false, and they call closeAll, the data is trashed and therefore
460                 //disclosures needs to accurately reflect that
461                 if (!self.options.cacheItems) {
462                     self.$element.one('closeAll.fu.tree', function () {
463                         self.$element.data('disclosures', 0);
464                     });
465                 }
466
467             }
468         },
469
470         // This refreshes the children of a folder. Please destroy and re-initilize for "root level" refresh.
471         // The data of the refreshed folder is not updated. This control's architecture only allows updating of children.
472         // Folder renames should probably be handled directly on the node.
473         refreshFolder: function refreshFolder($el) {
474             var $treeFolder = $el.closest('.tree-branch');
475             var $treeFolderChildren = $treeFolder.find('.tree-branch-children');
476             $treeFolderChildren.eq(0).empty();
477
478             if ($treeFolder.hasClass('tree-open')) {
479                 this.populate($treeFolderChildren, false);
480             }
481             else {
482                 this.populate($treeFolderChildren, true);
483             }
484
485             this.$element.trigger('refreshedFolder.fu.tree', $treeFolder.data());
486         }
487
488     };
489
490     // ALIASES
491
492     //alias for collapse for consistency. "Collapse" is an ambiguous term (collapse what? All? One specific branch?)
493     Tree.prototype.closeAll = Tree.prototype.collapse;
494     //alias for backwards compatibility because there's no reason not to.
495     Tree.prototype.openFolder = Tree.prototype.discloseFolder;
496     //For library consistency
497     Tree.prototype.getValue = Tree.prototype.selectedItems;
498
499     // PRIVATE FUNCTIONS
500
501     function styleNodeSelected (self, $element, $icon) {
502         $element.addClass('tree-selected');
503         if ( $element.data('type') === 'item' && $icon.hasClass(self.options['unselected-icon']) ) {
504             //$icon.removeClass('fueluxicon-bullet').addClass('glyphicon-ok'); // make checkmark
505             $icon.removeClass(self.options['unselected-icon']).addClass(self.options['selected-icon']); //ACE
506         }
507     }
508
509     function styleNodeDeselected (self, $element, $icon) {
510         $element.removeClass('tree-selected');
511         //if ( $element.data('type') === 'item' && $icon.hasClass('glyphicon-ok') ) {
512             //$icon.removeClass('glyphicon-ok').addClass('fueluxicon-bullet'); // make bullet
513         //}
514         //ACE
515         if ( $element.data('type') === 'item' && $icon.hasClass(self.options['selected-icon']) ) {
516             $icon.removeClass(self.options['selected-icon']).addClass(self.options['unselected-icon']); // make bullet
517         }
518     }
519
520     function multiSelectSyncNodes (self, clicked, selected) {
521         // search for currently selected and add to selected data list if needed
522         $.each(selected.$elements, function (index, element) {
523             var $element = $(element);
524             if ($element[0] !== clicked.$element[0]) {
525                 selected.dataForEvent.push( $($element).data() );
526             }
527         });
528
529         if (clicked.$element.hasClass('tree-selected')) {
530             styleNodeDeselected (self, clicked.$element, clicked.$icon);//ACE
531             // set event data
532             selected.eventType = 'deselected';
533         }
534         else {
535             styleNodeSelected(self, clicked.$element, clicked.$icon);//ACE
536             // set event data
537             selected.eventType = 'selected';
538             selected.dataForEvent.push(clicked.elementData);
539         }
540     }
541
542     function singleSelectSyncNodes(self, clicked, selected) {
543         // element is not currently selected
544         if (selected.$elements[0] !== clicked.$element[0]) {
545             var clearedElements = self.deselectAll(self.$element);
546             styleNodeSelected(self, clicked.$element, clicked.$icon);//ACE
547             // set event data
548             selected.eventType = 'selected';
549             selected.dataForEvent = [clicked.elementData];
550         }
551         else {
552             styleNodeDeselected(self, clicked.$element, clicked.$icon);//ACE
553             // set event data
554             selected.eventType = 'deselected';
555             selected.dataForEvent = [];
556         }
557     }
558
559
560     // TREE PLUGIN DEFINITION
561
562     $.fn.tree = function tree(option) {
563         var args = Array.prototype.slice.call(arguments, 1);
564         var methodReturn;
565
566         var $set = this.each(function () {
567             var $this = $(this);
568             var data = $this.data('fu.tree');
569             var options = typeof option === 'object' && option;
570
571             if (!data) {
572                 $this.data('fu.tree', (data = new Tree(this, options)));
573             }
574
575             if (typeof option === 'string') {
576                 methodReturn = data[option].apply(data, args);
577             }
578         });
579
580         return (methodReturn === undefined) ? $set : methodReturn;
581     };
582
583     $.fn.tree.defaults = {
584         dataSource: function dataSource(options, callback) {},
585         multiSelect: false,
586         cacheItems: true,
587         folderSelect: true,
588         itemSelect: true,
589         /*
590         * How many times `discloseAll` should be called before a stopping and firing
591         * an `exceededDisclosuresLimit` event. You can force it to continue by
592         * listening for this event, setting `ignore-disclosures-limit` to `true` and
593         * starting `discloseAll` back up again. This lets you make more decisions
594         * about if/when/how/why/how many times `discloseAll` will be started back
595         * up after it exceeds the limit.
596         *
597         *    $tree.one('exceededDisclosuresLimit.fu.tree', function () {
598         *        $tree.data('ignore-disclosures-limit', true);
599         *        $tree.tree('discloseAll');
600         *    });
601         *
602         * `disclusuresUpperLimit` defaults to `0`, so by default this trigger
603         * will never fire. The true hard the upper limit is the browser's
604         * ability to load new items (i.e. it will keep loading until the browser
605         * falls over and dies). On the Fuel UX `index.html` page, the point at
606         * which the page became super slow (enough to seem almost unresponsive)
607         * was `4`, meaning 256 folders had been opened, and 1024 were attempting to open.
608         */
609         disclosuresUpperLimit: 0
610     };
611
612     $.fn.tree.Constructor = Tree;
613
614     $.fn.tree.noConflict = function () {
615         $.fn.tree = old;
616         return this;
617     };
618
619
620     // NO DATA-API DUE TO NEED OF DATA-SOURCE
621
622     // -- BEGIN UMD WRAPPER AFTERWORD --
623 }));
624 // -- END UMD WRAPPER AFTERWORD --