/** * @namespace ludo.tree */ /** * Tree widget * @class ludo.tree.Tree * @param {Object} config * @param {Object} config.defaults: Default values for properties not present in data, example: * <code> * defaults: { * "icon": "images/icon.gif" * } * </code> * @param {Object} config.tpl Template strings for how to render nodes in the tree. * Example: * <code> * tpl: '{title} * </code> * which renders JSON like : [{"title": "First node" },{"title": "Second node"}] * as "First node". * You can also have more than one template. * Example: * <code> tpl:{ tplKey : 'type', city : 'City : {city}', country : 'Country : {country} } * </code> * for JSON like: * <code> * [ { type:"country", "country" : "United States" }, { "type": "city", "city": "Washington" }] * </code> * tplKey refers to the "type" attribute. When type is "country", it should use the "country" tpl. When type is "city", * it should use the "city" template. * */ ludo.tree.Tree = new Class({ Extends:ludo.CollectionView, type:'tree.Tree', nodeCache:{}, renderedRecords: {}, /* String template for nodes in the tree @config {String|Object} @memberof ludo.tree.Tree.prototype @example tpl : '{title} or as an object: @example tpl:{ tplKey : 'type', city : 'City : {city}', country : 'Country : {country} } When using an object, "tplKey" is a reference to a common property of all objects, example "type". When type for a node is "city", it will use the city : 'City : {city}' tpl. When it's "country", it will use the "country" tpl. @example [ { id:1, "country":"Japan", "type":"country", "capital":"Tokyo", "population":"13,185,502", children:[ { id:11, city:'Kobe', "type":"city" }, { id:12, city:'Kyoto', "type":"city" }, { id:13, city:'Sapporo', "type":"city"}, { id:14, city:'Sendai', "type":"city"}, { id:15, city:'Kawasaki', "type":"city"} ]}, {id:2, "country":"South Korea", "capital":"Seoul", "population":"10,464,051", "type":"country", children:[ { id:21, city:'Seoul', "type":"city" } ]}, {id:3, "country":"Russia", "capital":"Moscow", "population":"10,126,424", "type":"country"}, ] is an example of a data structure for this tpl. */ tpl : '<span class="ludo-tree-node-spacer"></span> {title}', tplKey:undefined, dataSource:{ }, defaultDS: 'dataSource.JSONTree', /* Default values when not present in node. @config {Object} defaults @memberof ludo.tree.Tree.prototype @default undefined @example defaults:{ "database" : { "icon" : "image.gif" } } where "database" refers to the record attribute with name defined in categoryKey property of tree(default "type" ). */ defaults:undefined, /* * Key used to defined nodes inside categories. This key is used for default values and node config * @config {String} categoryKey * @memberof ludo.tree.Tree.prototype * @default "type" */ categoryKey : 'type', /* Config of tree node categories @config {Object} categoryConfig @memberof ludo.tree.Tree.prototype @example categoryConfig:{ "database":{ "selectable" : false } } */ categoryConfig:undefined, __construct:function(config){ this.parent(config); this.setConfigParams(config, ['defaults','categoryConfig','categoryKey']); }, ludoEvents:function () { this.parent(); if (this.dataSource) { this.getDataSource().addEvents({ 'select' : this.selectRecord.bind(this), 'deselect' : this.deSelectRecord.bind(this), 'add' : this.addRecord.bind(this), 'addChild' : this.addChild.bind(this), 'remove' : this.removeChild.bind(this), 'removeChild' : this.removeChild.bind(this), 'show' : this.showRecord.bind(this), 'hide' : this.hideRecord.bind(this) }); } }, ludoDOM:function () { this.parent(); this.getBody().css('overflowY', 'auto'); this.getBody().on("click", this.onClick.bind(this)); this.getBody().on("dblclick", this.onDblClick.bind(this)); }, onClick:function (e) { var record = this.getRecordByDOM(e.target); if (record) { if(e.target.tagName.toLowerCase() === 'span' && this.isSelectable(record)) { this.getDataSource().selectRecord(record); } if($(e.target).hasClass('ludo-tree-node-expand')){ this.expandOrCollapse(record, e.target); }else{ this.expand(record, e.target); } } }, onDblClick:function(e){ var record = this.getRecordByDOM(e.target); if(record){ this.fireEvent('dblClick', record); } }, selectRecord:function (record) { if(!record.getPlainRecord)record = this.getDataSource().getRecord(record); if(!record)return; if(!this.isRecordRendered(record))this.showRecord(record); var el = this.getDomElement(record, '.ludo-tree-node-plain'); if (el)el.addClass('ludo-tree-selected-node'); }, deSelectRecord:function (record) { var el = this.getDomElement(record, '.ludo-tree-node-plain'); if (el)el.removeClass('ludo-tree-selected-node'); }, getDomElement:function (record, cls) { var el = this.getDomByRecord(record); if (el)return $(el).find(cls).first(); return undefined; }, expandOrCollapse:function (record, el) { el = this.getExpandEl(record); var method = el.hasClass('ludo-tree-node-collapse') ? 'collapse' : 'expand'; this[method](record,el); }, expand:function (record, el) { el = this.getExpandEl(record); if(!this.areChildrenRendered(record)){ this.renderChildrenOf(record); } el.addClass('ludo-tree-node-collapse'); this.getCachedNode(record, 'children', 'child-container-').css('display', ''); }, collapse:function (record, el) { el = this.getExpandEl(record); el.removeClass('ludo-tree-node-collapse'); this.getCachedNode(record, 'children', 'child-container-').css('display', 'none'); }, getExpandEl:function (record) { return this.getCachedNode(record, 'expand', 'expand-'); }, getChildContainer:function (record) { return this.getCachedNode(record, 'children', 'child-container-'); }, hideRecord:function (record) { if(this.isRecordRendered(record)){ this.getCachedNode(record, 'node', '').css('display', 'none'); } }, showRecord:function (record) { if(!this.isRecordRendered(record)){ this.showRecord(record.getParent()); this.expand(record.getParent()); this.showRecord(record.getParent()); } this.getCachedNode(record, 'node', '').css('display', ''); }, getDomByRecord:function (record) { return record ? this.getCachedNode(record, 'node', '') : undefined; }, isRecordRendered:function(record){ return this.renderedRecords[record.getUID ? record.getUID() : record.uid] ? true : false; }, areChildrenRendered:function(record){ record = record.getUID ? record.record : record; return record.children && record.children.length ? this.isRecordRendered(record.children[0]) : false; }, getCachedNode:function (record, cacheKey, idPrefix) { if (this.nodeCache[cacheKey] === undefined)this.nodeCache[cacheKey] = {}; var uid = record.getUID ? record.getUID() : record.uid; if (!this.nodeCache[cacheKey][uid]) { this.nodeCache[cacheKey][uid] = this.getBody().find("#" + idPrefix + uid); } return this.nodeCache[cacheKey][uid]; }, getRecordByDOM:function (el) { var b = this.getBody(); while (el !== b && (!el.className || el.className.indexOf('ludo-tree-a-node') === -1)) { el = el.parentNode; } if (el)return this.getDataSource().getRecord(el.id); return undefined; }, JSON:function () { this.nodeCache = {}; this.renderedRecords = {}; this.nodeContainer().html(''); this.render(); }, render:function () { this.parent(); var data = this.getDataSource().getData(); this.nodeContainer().html(this.getHtmlForBranch(data)); }, getHtmlForBranch:function (branch) { var html = []; for (var i = 0; i < branch.length; i++) { html.push(this.getHtmlFor(branch[i], (i === branch.length - 1), true)); } return html.join(''); }, renderChildrenOf:function(record){ var p = this.getChildContainer(record); if(p){ var c = record.getChildren(); if(c)p.html(this.getHtmlForBranch(c)); } }, getHtmlFor:function (record, isLast, includeContainer) { var ret = []; this.renderedRecords[record.uid] = true; if (includeContainer) { ret.push('<div class="ludo-tree-a-node ludo-tree-node'); if (isLast)ret.push(' ludo-tree-node-last-sibling'); ret.push('" id="' + record.uid + '">'); } ret.push('<div class="ludo-tree-node-plain">'); ret.push(this.isSelectable(record) ? '<span class="ludo-tree-node-selectable">' : '<span>'); ret.push(this.getNodeTextFor(record)); ret.push('</span>'); ret.push('<div class="ludo-tree-node-expand" id="expand-' + record.uid + '" style="position:absolute;display:' + (record.children && record.children.length ? '' : 'none') + '"></div>'); ret.push('</div>'); ret.push('<div class="ludo-tree-node-container" style="display:none" id="child-container-'); ret.push(record.uid); ret.push('">'); ret.push('</div>'); if (includeContainer)ret.push('</div>'); /* * Event fired when a node is created * @event createNode * @param {String} id of DOM node * @param {Object} record * @param {tree.Tree} tree */ this.fireEvent('createNode', [record.uid, record, this]); return ret.join(''); }, isSelectable:function (record) { if(this.categoryConfig){ record = record.getUID ? record.getData() : record; var config = this.categoryConfig[record[this.categoryKey]]; return config && config.selectable !== undefined ? config.selectable : true } return true; }, getNodeTextFor:function (record) { var tplFields = this.getTplFields(record); var ret = this.getTpl(record); for (var i = 0, count = tplFields.length; i < count; i++) { var field = tplFields[i]; ret = ret.replace('{' + field + '}', record[field] ? record[field] : this.getDefaultValue(record, field)); } ret = '<span class="ludo-tree-node-spacer"></span>' + ret; return ret; }, getDefaultValue:function(record, field){ if(this.defaults){ var key = this.categoryKey; return record[key] && this.defaults && this.defaults[record[key]] ? this.defaults[record[key]][field] : ''; } return ""; }, tpls:undefined, getTpl:function(record){ return this.tpl['tplKey'] ? this.tpl[record[this.tplKey]] : this.tpl; }, getTplFields:function (record) { if (!this.tplFields) { if(ludo.util.isString(this.tpl)){ this.tplFields = this.getTplMatches(this.tpl); }else{ this.tplFields = {}; var tpl = Object.clone(this.tpl); this.tplKey = tpl.tplKey; for(var key in tpl){ if(tpl.hasOwnProperty(key)){ if(key != 'tplKey')this.tplFields[key] = this.getTplMatches(tpl[key]); } } } } return this.tplKey ? this.tplFields[record[this.tplKey]] : this.tplFields; }, getTplMatches:function(tpl){ var matches = tpl.match(/{([^}]+)}/g); for (var i = 0; i < matches.length; i++) { matches[i] = matches[i].replace(/[{}]/g, ''); } return matches; }, addRecord:function(record){ this.addChild(record); }, addChild:function (record, parentRecord) { var childContainer = parentRecord ? this.getChildContainer(parentRecord) : this.getBody(); if (childContainer) { var node = this.getDomByRecord(record) || this.getNewNodeFor(record); childContainer.appendChild(node); this.cssBranch(parentRecord ? parentRecord.children : this.getDataSource().data); if(parentRecord)this.getExpandEl(parentRecord).style.display=''; } }, getNewNodeFor:function (record) { record = record.getUID ? record.record : record; if (!record.uid)this.getDataSource().indexRecord(record); return ludo.dom.create({ cls : 'ludo-tree-a-node ludo-tree-node', html : this.getHtmlFor(record, false, false), id : record.uid }); }, cssBranch:function (nodes) { var count = nodes.length; for (var i = Math.max(0,count-2); i < count; i++) { var node = this.getDomByRecord(nodes[i]); if (node) { if (i < count - 1) { node.removeClass('ludo-tree-node-last-sibling'); } else { node.addClass('ludo-tree-node-last-sibling'); } } } }, removeChild:function(record){ var el = this.getDomByRecord(record); if(el){ el.parentNode.removeChild(el); delete this.renderedRecords[this.getUID(record)]; if(record.parentUid){ var p = this.dataSource.findRecord(record.parentUid); if(p)this.cssBranch(p.children); } } }, getUID:function(record){ return record.getUID ? record.getUID() : record.uid; } });