/** * Elements in a tree structure * @module Ink.UI.TreeView_1 * @version 1 */ Ink.createModule('Ink.UI.TreeView', '1', ['Ink.UI.Common_1','Ink.Dom.Event_1','Ink.Dom.Css_1','Ink.Dom.Element_1','Ink.Dom.Selector_1','Ink.Util.Array_1'], function(Common, Event, Css, Element, Selector, InkArray ) { 'use strict'; /** * Shows elements in a tree structure which can be expanded and contracted. * A TreeView is built with "node"s and "children". "node"s are `li` tags, and "children" are `ul` tags. * You can build your TreeView out of a regular UL and LI element structure which you already use to display lists with several levels. * If you want a node to be open when the TreeView is built, just add the data-open="true" attribute to it. * * @class Ink.UI.TreeView * @constructor * @version 1 * @param {String|DOMElement} selector Element or selector. * @param {String} [options] Options object, containing: * @param {String} [options.node] Selector for the nodes. Defaults to 'li'. * @param {String} [options.children] Selector for the children. Defaults to 'ul'. * @param {String} [options.parentClass] CSS classes to be added to parent nodes. Defaults to 'parent'. * @param {String} [options.openClass] CSS classes to be added to the icon when a parent is open. Defaults to 'fa fa-minus-circle'. * @param {String} [options.closedClass] CSS classes to be added to the icon when a parent is closed. Defaults to 'fa fa-plus-circle'. * @param {String} [options.hideClass] CSS Class to toggle visibility of the children. Defaults to 'hide-all'. * @param {String} [options.iconTag] The name of icon tag. The component tries to find a tag with that name as a direct child of the node. If it doesn't find it, it creates it. Defaults to 'i'. * @param {Boolean} [options.stopDefault] Flag to stops the default behavior of the click handler. Defaults to true. * @example * * * * @sample Ink_UI_TreeView_1.html */ function TreeView() { Common.BaseUIComponent.apply(this, arguments); } TreeView._name = 'TreeView_1'; TreeView._optionDefinition = { 'node': ['String', 'li'], // [3.0.1] Deprecate this terrible, terrible name 'child': ['String',null], 'children': ['String','ul'], 'parentClass': ['String','parent'], 'openNodeClass': ['String', 'open'], 'openClass': ['String','fa fa-minus-circle'], 'closedClass': ['String','fa fa-plus-circle'], 'hideClass': ['String','hide-all'], 'iconTag': ['String', 'i'], 'stopDefault' : ['Boolean', true] }; TreeView.prototype = { /** * Init function called by the constructor. Sets the necessary event handlers. * * @method _init * @private */ _init: function(){ if (this._options.child) { Ink.warn('Ink.UI.TreeView: options.child is being renamed to options.children.'); this._options.children = this._options.child; } this._handlers = { click: Ink.bindEvent(this._onClick,this) }; Event.on(this._element, 'click', this._options.node, this._handlers.click); InkArray.each(Ink.ss(this._options.node, this._element), Ink.bind(function(item){ if( this.isParent(item) ) { Css.addClassName(item, this._options.parentClass); var isOpen = this.isOpen(item); if( !this._getIcon(item) ){ Element.create(this._options.iconTag, { insertTop: item }); } this._setNodeOpen(item, isOpen); } },this)); }, _getIcon: function (node) { return Ink.s('> ' + this._options.iconTag, node); }, /** * Checks if a node is open. * * @method isOpen * @param {DOMElement} node The tree node to check **/ isOpen: function (node) { if (!this._getChild(node)) { throw new Error('not a node!'); } return Element.data(node).open === 'true' || Css.hasClassName(node, this._options.openNodeClass); }, /** * Checks if a node is a parent. * * @method isParent * @param {DOMElement} node Node to check **/ isParent: function (node) { return Css.hasClassName(node, this._options.parentClass) || this._getChild(node) != null; }, _setNodeOpen: function (node, beOpen) { var child = this._getChild(node); if (child) { Css.setClassName(child, this._options.hideClass, !beOpen); var icon = this._getIcon(node); node.setAttribute('data-open', beOpen); /* * Don't refactor this to * * setClassName(el, className, status); setClassName(el, className, !status); * * because it won't work with multiple classes. * * Doing: * setClassName(el, 'fa fa-whatever', true);setClassName(el, 'fa fa-whatever-else', false); * * will remove 'fa' although it is a class we want. */ var toAdd = beOpen ? this._options.openClass : this._options.closedClass; var toRemove = beOpen ? this._options.closedClass : this._options.openClass; Css.removeClassName(icon, toRemove); Css.addClassName(icon, toAdd); Css.setClassName(node, this._options.openNodeClass, beOpen); } else { Ink.error('Ink.UI.TreeView: node', node, 'is not a node!'); } }, /** * Opens one of the tree nodes * * Make sure you pass the node's DOMElement * @method open * @param {DOMElement} node The node you wish to open. **/ open: function (node) { this._setNodeOpen(node, true); }, /** * Closes one of the tree nodes * * Make sure you pass the node's DOMElement * @method close * @param {DOMElement} node The node you wish to close. **/ close: function (node) { this._setNodeOpen(node, false); }, /** * Toggles a node state * * @method toggle * @param {DOMElement} node The node to toggle. **/ toggle: function (node) { if (this.isOpen(node)) { this.close(node); } else { this.open(node); } }, _getChild: function (node) { return Selector.select(this._options.children, node)[0] || null; }, /** * Handles the click event (as specified in the _init function). * * @method _onClick * @param {Event} event * @private */ _onClick: function(ev){ /** * Summary: * If the clicked element is a "node" as defined in the options, will check if it has any "child". * If so, will toggle its state and stop the event's default behavior if the stopDefault option is true. **/ if (!this.isParent(ev.currentTarget) || Selector.matchesSelector(ev.target, this._options.node) || Selector.matchesSelector(ev.target, this._options.children)) { return; } if (this._options.stopDefault){ ev.preventDefault(); } this.toggle(ev.currentTarget); } }; Common.createUIComponent(TreeView); return TreeView; });