264 lines
9.3 KiB
JavaScript
264 lines
9.3 KiB
JavaScript
(function (define) {
|
|
'use strict';
|
|
|
|
(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
define(factory);
|
|
} else if (typeof exports === 'object') {
|
|
module.exports = factory();
|
|
} else {
|
|
root.TreeView = factory();
|
|
}
|
|
}(window, function () {
|
|
return (function () {
|
|
|
|
/** List of events supported by the tree view */
|
|
var events = [
|
|
'expand',
|
|
'expandAll',
|
|
'collapse',
|
|
'collapseAll',
|
|
'select'
|
|
];
|
|
|
|
/**
|
|
* A utilite function to check to see if something is a DOM object
|
|
* @param {object} Object to test
|
|
* @returns {boolean} If the object is a DOM object
|
|
*/
|
|
function isDOMElement(obj) {
|
|
try {
|
|
return obj instanceof HTMLElement;
|
|
} catch (e) {
|
|
// Some browsers don't support using the HTMLElement so some extra
|
|
// checks are needed.
|
|
return typeof obj === 'object' && obj.nodeType === 1 && typeof obj.style === 'object' && typeof obj.ownerDocument === 'object';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A forEach that will work with a NodeList and generic Arrays
|
|
* @param {array|NodeList} arr The array to iterate over
|
|
* @param {function} callback Function that executes for each element. First parameter is element, second is index
|
|
* @param {object} The context to execute callback with
|
|
*/
|
|
function forEach(arr, callback, scope) {
|
|
var i, len = arr.length;
|
|
for (i = 0; i < len; i += 1) {
|
|
callback.call(scope, arr[i], i);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit an event from the tree view
|
|
* @param {string} name The name of the event to emit
|
|
*/
|
|
function emit(instance, name) {
|
|
var args = [].slice.call(arguments, 2);
|
|
if (events.indexOf(name) > -1) {
|
|
if (instance.handlers[name] && instance.handlers[name] instanceof Array) {
|
|
forEach(instance.handlers[name], function (handle) {
|
|
window.setTimeout(function () {
|
|
handle.callback.apply(handle.context, args);
|
|
}, 0);
|
|
});
|
|
}
|
|
} else {
|
|
throw new Error(name + ' event cannot be found on TreeView.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the tree view in the DOM
|
|
*/
|
|
function render(self) {
|
|
var container = isDOMElement(self.node) ? self.node : document.getElementById(self.node);
|
|
var leaves = [], click;
|
|
var renderLeaf = function (item) {
|
|
var leaf = document.createElement('div');
|
|
var content = document.createElement('div');
|
|
var text = document.createElement('div');
|
|
var expando = document.createElement('div');
|
|
|
|
leaf.setAttribute('class', 'tree-leaf');
|
|
content.setAttribute('class', 'tree-leaf-content');
|
|
content.setAttribute('data-item', JSON.stringify(item));
|
|
text.setAttribute('class', 'tree-leaf-text');
|
|
text.textContent = item.name;
|
|
expando.setAttribute('class', 'tree-expando ' + (item.expanded ? 'expanded' : ''));
|
|
expando.textContent = item.expanded ? '-' : '+';
|
|
content.appendChild(expando);
|
|
content.appendChild(text);
|
|
leaf.appendChild(content);
|
|
if (item.children && item.children.length > 0) {
|
|
var children = document.createElement('div');
|
|
children.setAttribute('class', 'tree-child-leaves');
|
|
forEach(item.children, function (child) {
|
|
var childLeaf = renderLeaf(child);
|
|
children.appendChild(childLeaf);
|
|
});
|
|
if (!item.expanded) {
|
|
children.classList.add('hidden');
|
|
}
|
|
leaf.appendChild(children);
|
|
} else {
|
|
expando.classList.add('hidden');
|
|
}
|
|
return leaf;
|
|
};
|
|
|
|
forEach(self.data, function (item) {
|
|
leaves.push(renderLeaf.call(self, item));
|
|
});
|
|
container.innerHTML = leaves.map(function (leaf) {
|
|
return leaf.outerHTML;
|
|
}).join('');
|
|
|
|
click = function (e) {
|
|
var parent = (e.target || e.currentTarget).parentNode;
|
|
var data = JSON.parse(parent.getAttribute('data-item'));
|
|
var leaves = parent.parentNode.querySelector('.tree-child-leaves');
|
|
if (leaves) {
|
|
if (leaves.classList.contains('hidden')) {
|
|
self.expand(parent, leaves);
|
|
} else {
|
|
self.collapse(parent, leaves);
|
|
}
|
|
} else {
|
|
emit(self, 'select', {
|
|
target: e,
|
|
data: data
|
|
});
|
|
}
|
|
};
|
|
|
|
forEach(container.querySelectorAll('.tree-leaf-text'), function (node) {
|
|
node.onclick = click;
|
|
});
|
|
forEach(container.querySelectorAll('.tree-expando'), function (node) {
|
|
node.onclick = click;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @property {object} handlers The attached event handlers
|
|
* @property {object} data The JSON object that represents the tree structure
|
|
* @property {DOMElement} node The DOM element to render the tree in
|
|
*/
|
|
function TreeView(data, node) {
|
|
this.handlers = {};
|
|
this.node = node;
|
|
this.data = data;
|
|
render(this);
|
|
}
|
|
|
|
TreeView.prototype.render = function () {
|
|
render(this);
|
|
};
|
|
|
|
/**
|
|
* Expands a leaflet by the expando or the leaf text
|
|
* @param {DOMElement} node The parent node that contains the leaves
|
|
* @param {DOMElement} leaves The leaves wrapper element
|
|
*/
|
|
TreeView.prototype.expand = function (node, leaves, skipEmit) {
|
|
var expando = node.querySelector('.tree-expando');
|
|
expando.textContent = '-';
|
|
leaves.classList.remove('hidden');
|
|
if (skipEmit) { return; }
|
|
emit(this, 'expand', {
|
|
target: node,
|
|
leaves: leaves
|
|
});
|
|
};
|
|
|
|
TreeView.prototype.expandAll = function () {
|
|
var self = this;
|
|
var nodes = document.getElementById(self.node).querySelectorAll('.tree-expando');
|
|
forEach(nodes, function (node) {
|
|
var parent = node.parentNode;
|
|
var leaves = parent.parentNode.querySelector('.tree-child-leaves');
|
|
if (parent && leaves && parent.hasAttribute('data-item')) {
|
|
self.expand(parent, leaves, true);
|
|
}
|
|
});
|
|
emit(this, 'expandAll', {});
|
|
};
|
|
|
|
/**
|
|
* Collapses a leaflet by the expando or the leaf text
|
|
* @param {DOMElement} node The parent node that contains the leaves
|
|
* @param {DOMElement} leaves The leaves wrapper element
|
|
*/
|
|
TreeView.prototype.collapse = function (node, leaves, skipEmit) {
|
|
var expando = node.querySelector('.tree-expando');
|
|
expando.textContent = '+';
|
|
leaves.classList.add('hidden');
|
|
if (skipEmit) { return; }
|
|
emit(this, 'collapse', {
|
|
target: node,
|
|
leaves: leaves
|
|
});
|
|
};
|
|
|
|
/**
|
|
*/
|
|
TreeView.prototype.collapseAll = function () {
|
|
var self = this;
|
|
var nodes = document.getElementById(self.node).querySelectorAll('.tree-expando');
|
|
forEach(nodes, function (node) {
|
|
var parent = node.parentNode;
|
|
var leaves = parent.parentNode.querySelector('.tree-child-leaves');
|
|
if (parent && leaves && parent.hasAttribute('data-item')) {
|
|
self.collapse(parent, leaves, true);
|
|
}
|
|
});
|
|
emit(this, 'collapseAll', {});
|
|
};
|
|
|
|
/**
|
|
* Attach an event handler to the tree view
|
|
* @param {string} name Name of the event to attach
|
|
* @param {function} callback The callback to execute on the event
|
|
* @param {object} scope The context to call the callback with
|
|
*/
|
|
TreeView.prototype.on = function (name, callback, scope) {
|
|
if (events.indexOf(name) > -1) {
|
|
if (!this.handlers[name]) {
|
|
this.handlers[name] = [];
|
|
}
|
|
this.handlers[name].push({
|
|
callback: callback,
|
|
context: scope
|
|
});
|
|
} else {
|
|
throw new Error(name + ' is not supported by TreeView.');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Deattach an event handler from the tree view
|
|
* @param {string} name Name of the event to deattach
|
|
* @param {function} callback The function to deattach
|
|
*/
|
|
TreeView.prototype.off = function (name, callback) {
|
|
var index, found = false;
|
|
if (this.handlers[name] instanceof Array) {
|
|
this.handlers[name].forEach(function (handle, i) {
|
|
index = i;
|
|
if (handle.callback === callback && !found) {
|
|
found = true;
|
|
}
|
|
});
|
|
if (found) {
|
|
this.handlers[name].splice(index, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
return TreeView;
|
|
}());
|
|
}));
|
|
}(window.define));
|