/**
* Treebeard : hierarchical grid built with Mithril
* https://github.com/caneruguz/treebeard
* Built by Center for Open Science -> http://www.cos.io
*/
//;
//(function (global, factory) {
// "use strict";
// var m;
// if (typeof define === 'function' && define.amd) {
// // AMD. Register as an anonymous module.
// define(['jQuery', 'mithril'], factory);
// } else if (typeof exports === 'object') {
// // Node. Does not work with strict CommonJS, but
// // only CommonJS-like environments that support module.exports,
// // like Node.
// m = require('mithril');
// module.exports = factory(jQuery, m);
// } else {
// // Browser globals (root is window)
// m = global.m;
// global.Treebeard = factory(jQuery, m);
// }
//}(this, function (jQuery, m) {
// "use strict";
//Force cache busting in IE
var oldmrequest = m.request;
m.request = function() {
var buster;
var requestArgs = arguments[0];
if (requestArgs.url.indexOf('?') !== -1) {
buster = '&_=';
} else {
buster = '?_=';
}
requestArgs.url += (buster + (new Date().getTime()));
return oldmrequest.apply(this, arguments);
};
// Indexes by id, shortcuts to the tree objects. Use example: var item = Indexes[23];
var Indexes = {},
// Item constructor
Item,
// Notifications constructor
Notify,
// Modal for box-wide errors
Modal,
// Initialize and namespace Treebeard module
Treebeard = {};
// Create unique ids, we are now using our own ids. Data ids are availbe to user through tree.data
// we are using globals here because of mithril views with unique keys for rows in case we have multiple
// instances of treebeard on the same page.
if (!window.treebeardCounter) {
window.treebeardCounter = -1;
}
/**
* Gets the incremented idCounter as a unique id
* @returns {Number} idCounter The state of id counter after incementing
*/
function getUID() {
window.treebeardCounter = window.treebeardCounter + 1;
return window.treebeardCounter;
}
/**
* Checks whether the argument passed is a string or function, useful for allowing different types of options to be set
* @param {Mixed} x Argument passed, can be anything
* @returns {Mixed} x If x is a function returns the execution, otherwise returns x as it is, expecting it to be a string.
*/
function functionOrString(x) {
if (!x) {
return "";
}
if ($.isFunction(x)) {
return x();
}
return x;
}
/**
* Sorts ascending based on any attribute on data
* @param {String} data The property of the object to be checked for comparison
* @param {String} sortType Whether sort is pure numbers of alphanumerical,
* @returns {Number} result The result of the comparison function, 0 for equal, -1 or 1 for one is bigger than other.
*/
function ascByAttr(data, sortType) {
if (sortType === "number") {
return function _numcompare(a, b) {
return a - b;
};
}
return function _compare(a, b) {
var titleA = a.data[data].toLowerCase().replace(/\s+/g, " "),
titleB = b.data[data].toLowerCase().replace(/\s+/g, " ");
if (titleA < titleB) {
return -1;
}
if (titleA > titleB) {
return 1;
}
return 0;
};
}
/**
* Sorts descending based on any attribute on data
* @param {String} data The property of the object to be checked for comparison
* @param {String} sortType Whether sort is pure numbers of alphanumerical,
* @returns {Number} result The result of the comparison function, 0 for equal, -1 or 1 for one is bigger than other.
*/
function descByAttr(data, sortType) {
if (sortType === "number") {
return function _numcompare(a, b) {
return b - a;
};
}
return function _compare(a, b) {
var titleA = a.data[data].toLowerCase().replace(/\s/g, ''),
titleB = b.data[data].toLowerCase().replace(/\s/g, '');
if (titleA > titleB) {
return -1;
}
if (titleA < titleB) {
return 1;
}
return 0;
};
}
/**
* Helper function that removes an item from an array of items based on the value of an attribute of that item
* @param {Array} arr The array that item needs to be removed from
* @param {String} attr The property based on which the removal should happen
* @param {String} value The value that needs to match the property for removal to happen
* @returns {Boolean} done Whether the remove was successful.
*/
function removeByProperty(arr, attr, value) {
var i,
done = false;
for (i = 0; i < arr.length; i++) {
if (arr[i] && arr[i].hasOwnProperty(attr) && (arguments.length > 2 && arr[i][attr] === value)) {
arr.splice(i, 1);
done = true;
return done;
}
}
return done;
}
var DEFAULT_NOTIFY_TIMEOUT = 3000;
/**
* Implementation of a notification system, added to each row
* @param {String} [message] Notification message
* @param {String} [type] One of the bootstrap alert types (info, danger, warning, success, primary, default)
* @param {Number} [column] Which column the message should replace, if empty the entire row will be used
* @param {Number} [timeout] Milliseconds that takes for message to be removed.
* @constructor
*/
Notify = function _notify(message, type, column, timeout) {
this.column = column || null;
this.type = type || "info";
this.message = message || 'Hello';
this.on = false;
this.timeout = timeout === undefined ? DEFAULT_NOTIFY_TIMEOUT : timeout;
this.css = '';
this.toggle = function () {
this.on = !this.on;
};
this.show = function () {
this.on = true;
var self = this;
if (self.timeout && self.timeout > 1) { // set timeout to 1 to stay forever
setTimeout(function () { self.hide(); }, self.timeout);
}
m.redraw(true);
};
this.hide = function () {
this.on = false;
m.redraw(true);
};
this.update = function(message, type, column, timeout, css) {
this.type = type || this.type;
this.column = column || this.column;
this.timeout = timeout === undefined ? DEFAULT_NOTIFY_TIMEOUT : timeout;
this.message = message;
this.css = css || '';
this.show(true);
};
this.selfDestruct = function (treebeard, item, timeout) {
this.on = false;
this.on = true;
var self = this,
out = timeout || 3000;
setTimeout(function () { self.hide(); item.removeSelf(); treebeard.redraw(); }, out);
};
};
/**
* Implementation of a modal system, currently used once sitewide
* @constructor
*/
Modal = function _modal() {
var el = $('#tb-tbody'),
self = this;
this.on = false;
this.timeout = false;
this.css = '';
this.content = null;
this.actions = null;
this.height = el.height();
this.width = el.width();
this.dismiss = function () {
this.on = false;
m.redraw(true);
$('#tb-tbody').css('overflow', 'auto');
};
this.show = function () {
this.on = true;
if (self.timeout) {
setTimeout(function () { self.dismiss(); }, self.timeout);
}
m.redraw(true);
};
this.toggle = function () {
this.on = !this.on;
m.redraw(true);
};
this.update = function (contentMithril, actions) {
self.updateSize();
if (contentMithril) {
this.content = contentMithril;
}
if (actions) {
this.actions = actions;
}
this.on = true;
m.redraw(true);
};
this.updateSize = function () {
this.height = $('#tb-tbody').height();
this.width = $('#tb-tbody').width();
m.redraw(true);
};
this.onmodalshow = function () {
var margin = $('.tb-tbody-inner>div').css('margin-top');
$('.tb-modal-shade').css('margin-top', margin);
$('#tb-tbody').css('overflow', 'hidden');
};
$(window).resize(function () {
self.updateSize();
});
};
/**
* Builds an _item that uses item related api calls, what we mean when we say "constructed by _item"
* @param {Object} data Raw data to be converted into an item
* @returns {Object} this Returns itself with the new properties.
* @constructor
*/
Item = function _item(data) {
if (data === undefined) {
this.data = {};
this.kind = "folder";
this.open = true;
} else {
this.data = data;
this.kind = data.kind || "file";
this.open = data.open;
}
if (this.kind === 'folder') {
this.load = false;
}
this.id = getUID();
this.depth = 0;
this.children = [];
this.parentID = null;
this.notify = new Notify();
};
/**
* Add a child item into the item and correctly assign depth and other properties
* @param {Object} component Item created with cosntructor _item, missing depth information
* @param {Boolean} [toTop] Whether the item should be added to the top or bottom of children array
* @returns {Object} this The current item.
*/
Item.prototype.add = function _itemAdd(component, toTop) {
component.parentID = this.id;
component.depth = this.depth + 1;
component.open = false;
component.load = false;
if (component.depth > 1 && component.children.length === 0) {
component.open = false;
}
if (toTop) {
this.children.unshift(component);
} else {
this.children.push(component);
}
return this;
};
/**
* Move item from one place to another within the tree
* @param {Number} toID Unique id of the container item to move to
* @returns {Object} this The current item.
*/
Item.prototype.move = function _itemMove(toID) {
var toItem = Indexes[toID],
parentID = this.parentID,
parent = Indexes[parentID];
toItem.add(this);
toItem.redoDepth();
if (parentID > -1) {
parent.removeChild(parseInt(this.id, 10));
}
return this;
};
/**
* Reassigns depth information when tree manipulations happen so depth remains accurate within object descendants
*/
Item.prototype.redoDepth = function _itemRedoDepth() {
function recursive(items, depth) {
var i;
for (i = 0; i < items.length; i++) {
items[i].depth = depth;
if (items[i].children.length > 0) {
recursive(items[i].children, depth + 1);
}
}
}
recursive(this.children, this.depth + 1);
};
/**
* Deletes itself
* @param {Number} childID Id of the child inside this item
* @returns {Object} parent The parent of the removed item
*/
Item.prototype.removeSelf = function _itemRemoveSelf() {
var parent = this.parent();
parent.removeChild(this.id);
return parent;
};
/**
* Deletes child from item by unique id
* @param {Number} childID Id of the child inside this item
* @returns {Object} this The item containing the child
*/
Item.prototype.removeChild = function _itemRemoveChild(childID) {
removeByProperty(this.children, 'id', childID);
return this;
};
/**
* Returns next sibling based on position in the parent list
* @returns {Object} next The next object constructed by _item
*/
Item.prototype.next = function _itemNext() {
var next, parent, i;
parent = Indexes[this.parentID];
for (i = 0; i < parent.children.length; i++) {
if (parent.children[i].id === this.id) {
next = parent.children[i + 1];
return next;
}
}
if (!next) {
throw new Error("Treebeard Error: Item with ID '" + this.id + "' has no next sibling");
}
};
/**
* Returns previous sibling based on position in the parent list
* @returns {Object} prev The previous object constructed by _item
*/
Item.prototype.prev = function _itemPrev() {
var prev, parent, i;
parent = Indexes[this.parentID];
for (i = 0; i < parent.children.length; i++) {
if (parent.children[i].id === this.id) {
prev = parent.children[i - 1];
return prev;
}
}
if (!prev) {
throw new Error("Treebeard Error: Item with ID '" + this.id + "' has no previous sibling");
}
};
/**
* Returns single child based on id
* @param {Number} childID Id of the child inside this item
* @returns {Object} child The child object constructed by _item
*/
Item.prototype.child = function _itemChild(childID) {
var child, i;
for (i = 0; i < this.children.length; i++) {
if (this.children[i].id === childID) {
child = this.children[i];
return child;
}
}
if (!child) {
throw new Error("Treebeard Error: Parent with ID '" + this.id + "' has no child with ID: " + childID);
}
};
/**
* Returns parent of the item one level above
* @returns {Object} parent The parent object constructed by _item
*/
Item.prototype.parent = function _itemParent() {
if (Indexes[this.parentID]) {
return Indexes[this.parentID];
}
return undefined;
};
/**
* Sorts children of the item by direction and selected field.
* @param {Object} treebeard The instance of the treebeard controller being used
* @param {String} direction Sort direction, can be 'asc' or 'desc'
* @param {String} sortType Whether the sort type is number or alphanumeric
* @param {Number} index The index of the column, needed to find out which field to be sorted
*/
Item.prototype.sortChildren = function _itemSort(treebeard, direction, sortType, index) {
var columns = treebeard.options.resolveRows.call(treebeard, this);
var field = columns[index].data;
if (!direction || (direction !== 'asc' && direction !== 'desc')) {
throw new Error("Treebeard Error: To sort children you need to pass direction as asc or desc to Item.sortChildren");
}
if (this.children.length > 0) {
if (direction === "asc") {
this.children.sort(ascByAttr(field, sortType));
}
if (direction === "desc") {
this.children.sort(descByAttr(field, sortType));
}
}
};
/**
* Checks if item is ancestor (contains) of another item passed as argument
* @param {Object} item An item constructed by _item
* @returns {Boolean} result Whether the item is ancestor of item passed as argument
*/
Item.prototype.isAncestor = function _isAncestor(item) {
function _checkAncestor(a, b) {
if (a.id === b.id) {
return true;
}
if (a.parent()) {
return _checkAncestor(a.parent(), b);
}
return false;
}
if (item.parent()) {
return _checkAncestor(item.parent(), this);
}
return false;
};
/**
* Checks if item is descendant (a child) of another item passed as argument
* @param {Object} item An item constructed by _item
* @returns {Boolean} result Whether the item is descendant of item passed as argument
*/
Item.prototype.isDescendant = function (item) {
var i,
result = false;
function _checkDescendant(children, b) {
for (i = 0; i < children.length; i++) {
if (children[i].id === b.id) {
result = true;
break;
}
if (children[i].children.length > 0) {
return _checkDescendant(children[i].children, b);
}
}
return result;
}
return _checkDescendant(item.children, this);
};
/**
* The main Treebeard API with publicly available variables and methods
* @alias Treebeard
* @param {Object} opts Options passed in
* @constructor
*/
Treebeard.controller = function _treebeardController(opts) {
// private variables
var self = this, // Treebard.controller
_lastLocation = 0, // The last scrollTop location, updates on every scroll.
_lastNonFilterLocation = 0; // The last scrolltop location before filter was used.
this.isSorted = {}; // Temporary variables for sorting
m.redraw.strategy("all");
// public variables
this.modal = new Modal(); // Box wide modal
this.flatData = []; // Flat data, gets regenerated often
this.treeData = {}; // The data in hierarchical form
this.filterText = m.prop(""); // value of the filtertext input
this.showRange = []; // Array of indexes that the range shows
this.options = opts; // User defined options
this.selected = undefined; // The row selected on click.
this.rangeMargin = 0; // Top margin, required for proper scrolling
this.visibleIndexes = []; // List of items viewable as a result of an operation like filter.
this.visibleTop = undefined; // The first visible item.
this.currentPage = m.prop(1); // for pagination
this.dropzone = null; // Treebeard's own dropzone object
this.dropzoneItemCache = undefined; // Cache of the dropped item
this.filterOn = false; // Filter state for use across the app
this.multiselected = [];
this.pressedKey = undefined;
this.dragOngoing = false;
this.initialized = false; // Treebeard's own initialization check, turns to true after page loads.
this.colsizes = {}; // Storing column sizes across the app.
/**
* Helper function to redraw if user makes changes to the item (like deleting through a hook)
*/
this.redraw = function _redraw() {
self.flatten(self.treeData.children, self.visibleTop);
};
/**
* Helper function to reset unique id to a reset number or -1
* @param {Number} resetNum Number to reset counter to
*/
this.resetCounter = function _resetCounter(resetNum) {
if (resetNum !== 0) {
window.treebeardCounter = resetNum || -1;
} else {
window.treebeardCounter = 0;
}
};
/**
* Instantiates draggable and droppable on DOM elements with options passed from self.options
*/
this.initializeMove = function () {
var draggableOptions,
droppableOptions,
x,
y;
draggableOptions = {
helper: "clone",
cursor : 'move',
containment : '.tb-tbody-inner',
delay : 100,
drag : function (event, ui) {
if (self.pressedKey === 27) {
return false;
}
if (self.options.dragEvents.drag) {
self.options.dragEvents.drag.call(self, event, ui);
} else {
if (self.dragText === "") {
if (self.multiselected.length > 1) {
var newHTML = $(ui.helper).text() + ' <b> + ' + (self.multiselected.length - 1) + ' more </b>';
self.dragText = newHTML;
$('.tb-drag-ghost').html(newHTML);
}
}
$(ui.helper).css({ 'display' : 'none'});
}
// keep copy of the element and attach it to the mouse location
x = event.pageX > 50 ? event.pageX - 50 : 50;
y = event.pageY - 10;
$('.tb-drag-ghost').css({ 'position' : 'absolute', top : y, left : x, 'height' : '25px', 'width' : '400px', 'background' : 'white', 'padding' : '0px 10px', 'box-shadow' : '0 0 4px #ccc'});
},
create : function (event, ui) {
if (self.options.dragEvents.create) {
self.options.dragEvents.create.call(self, event, ui);
}
},
start : function (event, ui) {
var thisID,
item,
ghost;
// if the item being dragged is not in multiselect clear multiselect
thisID = parseInt($(ui.helper).closest('.tb-row').attr('data-id'), 10);
item = self.find(thisID);
if (!self.isMultiselected(thisID)) {
self.clearMultiselect();
self.multiselected.push(item);
}
self.dragText = "";
ghost = $(ui.helper).clone();
ghost.addClass('tb-drag-ghost');
$('body').append(ghost);
if (self.options.dragEvents.start) {
self.options.dragEvents.start.call(self, event, ui);
}
self.dragOngoing = true;
$('.tb-row').removeClass(self.options.hoverClass + ' tb-h-error tb-h-success');
},
stop : function (event, ui) {
$('.tb-drag-ghost').remove();
if (self.options.dragEvents.stop) {
self.options.dragEvents.stop.call(self, event, ui);
}
self.dragOngoing = false;
$('.tb-row').removeClass(self.options.hoverClass + ' tb-h-error tb-h-success');
}
};
droppableOptions = {
tolerance : 'pointer',
activate : function (event, ui) {
if (self.options.dropEvents.activate) {
self.options.dropEvents.activate.call(self, event, ui);
}
},
create : function (event, ui) {
if (self.options.dropEvents.create) {
self.options.dropEvents.create.call(self, event, ui);
}
},
deactivate : function (event, ui) {
if (self.options.dropEvents.deactivate) {
self.options.dropEvents.deactivate.call(self, event, ui);
}
},
drop : function (event, ui) {
if (self.options.dropEvents.drop) {
self.options.dropEvents.drop.call(self, event, ui);
}
},
out : function (event, ui) {
if (self.options.dropEvents.out) {
self.options.dropEvents.out.call(self, event, ui);
}
},
over : function (event, ui) {
var id = parseInt($(event.target).closest('.tb-row').attr('data-id'), 10),
last = self.flatData[self.showRange[self.showRange.length - 1]].id,
first = self.flatData[self.showRange[0]].id,
currentScroll;
if (id === last) {
currentScroll = $('#tb-tbody').scrollTop();
$('#tb-tbody').scrollTop(currentScroll + self.options.rowHeight);
}
if (id === first) {
currentScroll = $('#tb-tbody').scrollTop();
$('#tb-tbody').scrollTop(currentScroll - self.options.rowHeight);
}
if (self.options.dropEvents.over) {
self.options.dropEvents.over.call(self, event, ui);
}
}
};
self.options.finalDragOptions = $.extend(draggableOptions, self.options.dragOptions);
self.options.finalDropOptions = $.extend(droppableOptions, self.options.dropOptions);
self.options.dragSelector = self.options.moveClass || 'td-title';
self.moveOn();
};
/**
* Turns move on for all elements or elements within a parent container
* @param parent DOM element for parent
*/
this.moveOn = function _moveOn(parent) {
if (!parent) {
$('.' + self.options.dragSelector).draggable(self.options.finalDragOptions);
$('.tb-row').droppable(self.options.finalDropOptions);
} else {
$(parent).find('.' + self.options.dragSelector).draggable(self.options.finalDragOptions);
$(parent).droppable(self.options.finalDropOptions);
}
};
/**
* Removes move related instances by destroying draggable and droppable.
*/
this.moveOff = function _moveOff() {
$(".td-title").draggable("destroy");
$(".tb-row").droppable("destroy");
};
/**
* Deletes item from tree and refreshes view
* @param {Number} parentID Unique id of the parent
* @param {Number} itemID Unique id of the item
* @returns {Object} A shallow copy of the item that was just deleted.
*/
this.deleteNode = function _deleteNode(parentID, itemID) { // TODO : May be redundant to include parentID
var item = Indexes[itemID],
itemcopy = $.extend({}, item);
$.when(self.options.deletecheck.call(self, item)).done(function _resolveDeleteCheck(check) {
if (check) {
var parent = Indexes[parentID];
parent.removeChild(itemID);
self.flatten(self.treeData.children, self.visibleTop);
if (self.options.ondelete) {
self.options.ondelete.call(self, itemcopy);
}
}
});
return itemcopy;
};
/**
* Checks if a move between items can be done based on logic of which contains the other
* @param {Object} toItem Receiving item data constructed by _item
* @param {Object} parentID Item to be moved as constructed by _item
* @returns {Boolean} Whether the move can be done or not
*/
this.canMove = function _canMove(toItem, fromItem) {
// is toItem a folder?
if (toItem.kind !== "folder") {
return false;
}
// is toItem a descendant of fromItem?
if (toItem.isDescendant(fromItem)) {
return false;
}
return true;
};
/**
* Adds an item to the list with proper tree and flat data and view updates
* @param {Object} item the raw data of the item
* @param {Number} parentID the unique id of the parent object the item should be added to
* @returns {Object} newItem the created item as constructed by _item with correct parent information.
*/
this.createItem = function _createItem(item, parentID) {
var parent = Indexes[parentID],
newItem;
$.when(self.options.createcheck.call(self, item, parent)).done(function _resolveCreateCheck(check) {
if (check) {
newItem = new Item(item);
parent.add(newItem, true);
self.flatten(self.treeData.children, self.visibleTop);
if (self.options.oncreate) {
self.options.oncreate.call(self, newItem, parent);
}
} else {
throw new Error('Treebeard Error: createcheck function returned false, item not created.');
}
});
return newItem;
};
/**
* Finds the entire item object through the id only
* @param {Number} id Unique id of the item acted on
* @returns {Number} _item The full item object constructed by _item.
*/
this.find = function _find(id) {
if (Indexes[id]) {
return Indexes[id];
}
return undefined;
};
/**
* Returns the index of an item in the flat row list (self.flatData)
* @param {Number} id Unique id of the item acted on (usually item.id) .
* @returns {Number} i The index at which the item is found or undefined if nothing is found.
*/
this.returnIndex = function _returnIndex(id) {
var len = self.flatData.length, i, o;
for (i = 0; i < len; i++) {
o = self.flatData[i];
if (o.id === id) {
return i;
}
}
return undefined;
};
/**
* Returns the index of an item in the showRange list (self.showRange)
* @param {Number} id Unique id of the item acted on (usually item.id) .
* @returns {Number} i The index at which the item is found or undefined if nothing is found.
*/
this.returnRangeIndex = function _returnRangeIndex(id) {
var len = self.showRange.length, i, o;
for (i = 0; i < len; i++) {
o = self.flatData[self.showRange[i]];
if (o.id === id) {
return i;
}
}
return undefined;
};
/**
* Returns whether a single row contains the filtered items, checking if columns can be filtered
* @param {Object} item Item constructed with _item which the filtering is acting on.
* @returns {Boolean} titleResult Whether text is found within the item, default is false;
*/
this.rowFilterResult = function _rowFilterResult(item) {
$('#tb-tbody').scrollTop(0);
self.currentPage(1);
var cols = self.options.resolveRows.call(self, item),
filter = self.filterText().toLowerCase(),
titleResult = false,
i,
o;
for (i = 0; i < cols.length; i++) {
o = cols[i];
if (o.filter && item.data[o.data].toLowerCase().indexOf(filter) !== -1) {
titleResult = true;
}
}
return titleResult;
};
/**
* Runs filter functions and resets depending on whether there is a filter word
* @param {Event} e Event object fired by the browser
* @config {Object} currentTarget Event object needs to have a e.currentTarget element object for mithril.
*/
this.filter = function _filter(e) {
m.withAttr("value", self.filterText)(e);
var filter = self.filterText().toLowerCase(),
index = self.visibleTop;
if (filter.length === 0) {
self.filterOn = false;
self.calculateVisible(0);
self.calculateHeight();
m.redraw(true);
$('#tb-tbody').scrollTop(_lastNonFilterLocation); // restore location of scroll
if (self.options.onfilterreset) {
self.options.onfilterreset.call(self, filter);
}
} else {
if (!self.filterOn) {
self.filterOn = true;
_lastNonFilterLocation = _lastLocation;
}
if (!self.visibleTop) {
index = 0;
}
self.calculateVisible(index);
self.calculateHeight();
m.redraw(true);
if (self.options.onfilter) {
self.options.onfilter.call(self, filter);
}
}
};
/**
* Updates content of the folder with new data or refreshes from lazyload
* @param {Array} data New raw items, may be returned from ajax call
* @param {Object} parent Item built with the _item constructor
*/
this.updateFolder = function (data, parent) {
if (data) {
parent.children = [];
var child, i;
for (i = 0; i < data.length; i++) {
child = self.buildTree(data[i], parent);
parent.add(child);
}
parent.open = true;
//return;
}
var index = self.returnIndex(parent.id);
parent.open = false;
parent.load = false;
self.toggleFolder(index, null);
};
/**
* Toggles folder, refreshing the view or reloading in event of lazyload
* @param {Number} index The index of the item in the flatdata.
* @param {Event} [event] Toggle click event if this function is triggered by an event.
*/
this.toggleFolder = function _toggleFolder(index, event) {
if (index === undefined || index === null) {
self.redraw();
return;
}
var len = self.flatData.length,
tree = Indexes[self.flatData[index].id],
item = self.flatData[index],
child,
skip = false,
skipLevel = item.depth,
level = item.depth,
i,
j,
o,
t,
lazyLoad,
icon = $('.tb-row[data-id="' + item.id + '"]').find('.tb-toggle-icon');
if (icon.get(0)) {
m.render(icon.get(0), self.options.resolveRefreshIcon());
}
$.when(self.options.resolveLazyloadUrl.call(self, tree)).done(function _resolveLazyloadDone(url) {
lazyLoad = url;
if (lazyLoad && item.row.kind === "folder" && tree.open === false && tree.load === false) {
tree.children = [];
m.request({method: "GET", url: lazyLoad})
.then(function _getUrlBuildtree(value) {
if (!value) {
self.options.lazyLoadError.call(self, tree);
} else {
if (!$.isArray(value)) {
value = value.data;
}
//tree.children = [];
var isUploadItem = function (element) {
return element.data.tmpID;
};
tree.children = tree.children.filter(isUploadItem);
for (i = 0; i < value.length; i++) {
child = self.buildTree(value[i], tree);
tree.add(child);
}
tree.open = true;
tree.load = true;
var iconTemplate = self.options.resolveToggle.call(self, tree);
if (icon.get(0)) {
m.render(icon.get(0), iconTemplate);
}
}
}, function (info) {
self.options.lazyLoadError.call(self, tree);
})
.then(function _getUrlFlatten() {
self.flatten(self.treeData.children, self.visibleTop);
if (self.options.lazyLoadOnLoad) {
self.options.lazyLoadOnLoad.call(self, tree);
}
});
} else {
for (j = index + 1; j < len; j++) {
o = self.flatData[j];
t = Indexes[self.flatData[j].id];
if (o.depth <= level) {break; }
if (skip && o.depth > skipLevel) {continue; }
if (o.depth === skipLevel) { skip = false; }
if (tree.open) { // closing
o.show = false;
} else { // opening
o.show = true;
if (!t.open) {
skipLevel = o.depth;
skip = true;
}
}
}
tree.open = !tree.open;
self.calculateVisible(self.visibleTop);
self.calculateHeight();
m.redraw(true);
var iconTemplate = self.options.resolveToggle.call(self, tree);
if (icon.get(0)) {
m.render(icon.get(0), iconTemplate);
}
}
if (self.options.allowMove) {
self.moveOn();
}
if (self.options.ontogglefolder) {
self.options.ontogglefolder.call(self, tree, event);
}
});
};
/**
* Toggles the sorting when clicked on sort icons.
* @param {Event} [event] Toggle click event if this function is triggered by an event.
*/
this.sortToggle = function _isSortedToggle(ev) {
var element = $(ev.target),
type = element.attr('data-direction'),
index = this,
col = element.parent('.tb-th').attr('data-tb-th-col'),
sortType = element.attr('data-sortType'),
parent = element.parent(),
counter = 0,
redo;
$('.asc-btn, .desc-btn').addClass('tb-sort-inactive'); // turn all styles off
self.isSorted[col].asc = false;
self.isSorted[col].desc = false;
if (!self.isSorted[col][type]) {
redo = function _redo(data) {
data.map(function _mapToggle(item) {
item.sortChildren(self, type, sortType, index);
if (item.children.length > 0) { redo(item.children); }
counter = counter + 1;
});
};
self.treeData.sortChildren(self, type, sortType, index); // Then start recursive loop
redo(self.treeData.children);
parent.children('.' + type + '-btn').removeClass('tb-sort-inactive');
self.isSorted[col][type] = true;
self.flatten(self.treeData.children, 0);
}
};
/**
* Calculate how tall the wrapping div should be so that scrollbars appear properly
* @name Component#calculateHeight
* @returns {Number} itemsHeight Number of pixels calculated in the function for height
*/
this.calculateHeight = function _calculateHeight() {
var itemsHeight;
if (!self.options.paginate) {
itemsHeight = self.visibleIndexes.length * self.options.rowHeight;
} else {
itemsHeight = self.options.showTotal * self.options.rowHeight;
self.rangeMargin = 0;
}
$('.tb-tbody-inner').height(itemsHeight + self.remainder);
return itemsHeight;
};
/**
* Calculates total number of visible items to return a row height
* @param {Number} rangeIndex The index to start refreshing range
* @returns {Number} total Number of items visible (not in showrange but total).
*/
this.calculateVisible = function _calculateVisible(rangeIndex) {
rangeIndex = rangeIndex || 0;
var len = self.flatData.length,
total = 0,
i,
item;
self.visibleIndexes = [];
for (i = 0; i < len; i++) {
item = Indexes[self.flatData[i].id];
if (self.filterOn) {
if (self.rowFilterResult(item)) {
total++;
self.visibleIndexes.push(i);
}
} else {
if (self.flatData[i].show) {
self.visibleIndexes.push(i);
total = total + 1;
}
}
}
self.refreshRange(rangeIndex);
return total;
};
/**
* Refreshes the view to start the the location where begin is the starting index
* @param {Number} begin The index location of visible indexes to start at.
*/
this.refreshRange = function _refreshRange(begin) {
var len = self.visibleIndexes.length,
range = [],
counter = 0,
i,
index;
if (!begin || begin > self.flatData.length) {
begin = 0;
}
self.visibleTop = begin;
for (i = begin; i < len; i++) {
if (range.length === self.options.showTotal) {break; }
index = self.visibleIndexes[i];
range.push(index);
counter = counter + 1;
}
self.showRange = range;
m.redraw(true);
};
/**
* Changes view to continous scroll when clicked on the scroll button
*/
this.toggleScroll = function _toggleScroll() {
self.options.paginate = false;
$('.tb-paginate').removeClass('active');
$('.tb-scroll').addClass('active');
$("#tb-tbody").scrollTop((self.currentPage() - 1) * self.options.showTotal * self.options.rowHeight);
self.calculateHeight();
};
/**
* Changes view to pagination when clicked on the paginate button
*/
this.togglePaginate = function _togglePaginate() {
var firstIndex = self.showRange[0],
first = self.visibleIndexes.indexOf(firstIndex),
pagesBehind = Math.floor(first / self.options.showTotal),
firstItem = (pagesBehind * self.options.showTotal);
self.options.paginate = true;
$('.tb-scroll').removeClass('active');
$('.tb-paginate').addClass('active');
self.currentPage(pagesBehind + 1);
self.calculateHeight();
self.refreshRange(firstItem);
};
/**
* During pagination goes UP one page
*/
this.pageUp = function _pageUp() {
// get last shown item index and refresh view from that item onwards
var lastIndex = self.showRange[self.options.showTotal - 1],
last = self.visibleIndexes.indexOf(lastIndex);
if (last > -1 && last + 1 < self.visibleIndexes.length) {
self.refreshRange(last + 1);
self.currentPage(self.currentPage() + 1);
return true;
}
return false;
};
/**
* During pagination goes DOWN one page
*/
this.pageDown = function _pageDown() {
var firstIndex = self.showRange[0],
first = self.visibleIndexes.indexOf(firstIndex);
if (first && first > 0) {
self.refreshRange(first - self.options.showTotal);
self.currentPage(self.currentPage() - 1);
return true;
}
return false;
};
/**
* During pagination jumps to specific page
* @param {Number} value the page number to jump to
*/
this.goToPage = function _goToPage(value) {
if (value && value > 0 && value <= (Math.ceil(self.visibleIndexes.length / self.options.showTotal))) {
var index = (self.options.showTotal * (value - 1));
self.currentPage(value);
self.refreshRange(index);
return true;
}
return false;
};
/**
* Check if item is part of the multiselected array
* @param {Number} id The unique id of the item.
* @returns {Boolean} outcome Whether the item is part of multiselected
*/
this.isMultiselected = function (id) {
var outcome = false;
self.multiselected.map(function (item) {
if (item.id === id) {
outcome = true;
}
});
return outcome;
};
/**
* Removes single item from the multiselected array
* @param {Number} id The unique id of the item.
* @returns {Boolean} result Whether the item removal was successful
*/
this.removeMultiselected = function (id) {
self.multiselected.map(function (item, index, arr) {
if (item.id === id) {
arr.splice(index, 1);
// remove highlight
$('.tb-row[data-id="' + item.id + '"]').removeClass(self.options.hoverClassMultiselect);
}
});
return false;
};
/**
* Adds highlight to the multiselected items using jquery.
*/
this.highlightMultiselect = function () {
$('.' + self.options.hoverClassMultiselect).removeClass(self.options.hoverClassMultiselect);
this.multiselected.map(function (item) {
$('.tb-row[data-id="' + item.id + '"]').addClass(self.options.hoverClassMultiselect);
});
};
/**
* Handles multiselect by adding items through shift or control key presses
* @param {Number} id The unique id of the item.
* @param {Number} [index] The showRange index of the item
* @param {Event} [event] Click event on the item
*/
this.handleMultiselect = function (id, index, event) {
var tree = Indexes[id],
begin,
end,
i,
cmdkey,
direction;
// if key is shift
if (self.pressedKey === 16) {
// get the index of this and add all visible indexes between this one and last selected
// If there is no multiselect yet
if (self.multiselected.length === 0) {
self.multiselected.push(tree);
} else {
begin = self.returnRangeIndex(self.multiselected[0].id);
end = self.returnRangeIndex(id);
if (begin > end) {
direction = 'up';
} else {
direction = 'down';
}
if (begin !== end) {
self.multiselected = [];
if (direction === 'down') {
for (i = begin; i < end + 1; i++) {
self.multiselected.push(Indexes[self.flatData[self.showRange[i]].id]);
}
}
if (direction === 'up') {
for (i = begin; i > end - 1; i--) {
self.multiselected.push(Indexes[self.flatData[self.showRange[i]].id]);
}
}
}
}
}
// if key is cmd
cmdkey = 91; // works with mac
if (window.navigator.userAgent.indexOf('MSIE') > -1 || window.navigator.userAgent.indexOf('Windows') > -1) {
cmdkey = 17; // works with windows
}
if (window.navigator.userAgent.indexOf('Firefox') > -1) {
cmdkey = 224; // works with Firefox
}
if (self.pressedKey === cmdkey) {
if (!self.isMultiselected(tree.id)) {
self.multiselected.push(tree);
} else {
self.removeMultiselected(tree.id);
}
}
// if there is no key add the one.
if (!self.pressedKey) {
self.clearMultiselect();
self.multiselected.push(tree);
}
if (self.options.onmultiselect) {
self.options.onmultiselect.call(self, event, tree);
}
self.highlightMultiselect.call(this);
};
this.clearMultiselect = function () {
$('.' + self.options.hoverClassMultiselect).removeClass(self.options.hoverClassMultiselect);
self.multiselected = [];
};
/**
* Remove dropzone from grid
*/
function _destroyDropzone() {
self.dropzone.destroy();
}
/**
* Apply dropzone to grid with the optional hooks
*/
function _applyDropzone() {
if (self.dropzone) { _destroyDropzone(); } // Destroy existing dropzone setup
var options = $.extend({
clickable : false,
counter : 0,
accept : function _dropzoneAccept(file, done) {
var parent = file.treebeardParent;
if (self.options.addcheck.call(this, self, parent, file)) {
$.when(self.options.resolveUploadUrl.call(self, parent, file))
.then(function _resolveUploadUrlThen(newUrl) {
if (newUrl) {
self.dropzone.options.url = newUrl;
self.dropzone.options.counter++;
if(self.dropzone.options.counter < 2 ) {
var index = self.returnIndex(parent.id);
if (!parent.open) {
self.toggleFolder(index, null);
}
}
}
return newUrl;
})
.then(function _resolveUploadMethodThen() {
if ($.isFunction(self.options.resolveUploadMethod)) {
self.dropzone.options.method = self.options.resolveUploadMethod.call(self, parent);
}
})
.done(function _resolveUploadUrlDone() {
done();
});
}
},
drop : function _dropzoneDrop(event) {
var rowID = $(event.target).closest('.tb-row').attr('data-id');
var item = Indexes[rowID];
if (item.kind === 'file') {
item = item.parent();
}
self.dropzoneItemCache = item;
if (!item.open) {
var index = self.returnIndex(item.id);
self.toggleFolder(index, null);
}
if ($.isFunction(self.options.dropzoneEvents.drop)) {
self.options.dropzoneEvents.drop.call(this, self, event);
}
},
dragstart : function _dropzoneDragStart(event) {
if ($.isFunction(self.options.dropzoneEvents.dragstart)) {
self.options.dropzoneEvents.dragstart.call(this, self, event);
}
},
dragend : function _dropzoneDragEnd(event) {
if ($.isFunction(self.options.dropzoneEvents.dragend)) {
self.options.dropzoneEvents.dragend.call(this, self, event);
}
},
dragenter : function _dropzoneDragEnter(event) {
if ($.isFunction(self.options.dropzoneEvents.dragenter)) {
self.options.dropzoneEvents.dragenter.call(this, self, event);
}
},
dragover : function _dropzoneDragOver(event) {
if ($.isFunction(self.options.dropzoneEvents.dragover)) {
self.options.dropzoneEvents.dragover.call(this, self, event);
}
},
dragleave : function _dropzoneDragLeave(event) {
if ($.isFunction(self.options.dropzoneEvents.dragleave)) {
self.options.dropzoneEvents.dragleave.call(this, self, event);
}
},
success : function _dropzoneSuccess(file, response) {
if ($.isFunction(self.options.dropzoneEvents.success)) {
self.options.dropzoneEvents.success.call(this, self, file, response);
}
if ($.isFunction(self.options.onadd)) {
self.options.onadd.call(this, self, file.treebeardParent, file, response);
}
},
error : function _dropzoneError(file, message, xhr) {
if ($.isFunction(self.options.dropzoneEvents.error)) {
self.options.dropzoneEvents.error.call(this, self, file, message, xhr);
}
},
uploadprogress : function _dropzoneUploadProgress(file, progress, bytesSent) {
if ($.isFunction(self.options.dropzoneEvents.uploadprogress)) {
self.options.dropzoneEvents.uploadprogress.call(this, self, file, progress, bytesSent);
}
},
sending : function _dropzoneSending(file, xhr, formData) {
if ($.isFunction(self.options.dropzoneEvents.sending)) {
self.options.dropzoneEvents.sending.call(this, self, file, xhr, formData);
}
},
complete : function _dropzoneComplete(file) {
if ($.isFunction(self.options.dropzoneEvents.complete)) {
self.options.dropzoneEvents.complete.call(this, self, file);
}
},
addedfile : function _dropzoneAddedFile(file) {
file.treebeardParent = self.dropzoneItemCache;
if ($.isFunction(self.options.dropzoneEvents.addedfile)) {
self.options.dropzoneEvents.addedfile.call(this, self, file);
}
}
}, self.options.dropzone); // Extend default options
// Add Dropzone with different scenarios of library inclusion, should work for most installations
var Dropzone;
if (typeof module === 'object') {
Dropzone = require('dropzone');
} else {
Dropzone = window.Dropzone;
}
if (typeof Dropzone === 'undefined') {
throw new Error('To enable uploads Treebeard needs "Dropzone" to be installed.');
}
// apply dropzone to the Treebeard object
self.dropzone = new Dropzone('#' + self.options.divID, options); // Initialize dropzone
}
/**
* Loads the data pushed in to Treebeard and handles it to comply with treebeard data structure.
* @param {Array| String} data Data sent in as an array of objects or a url in string form
*/
function _loadData(data) {
// Order of operations: Gewt data -> build tree -> flatten for view -> calculations for view: visible, height
if ($.isArray(data)) {
$.when(self.buildTree(data)).then(function _buildTreeThen(value) {
self.treeData = value;
Indexes[0] = value;
self.flatten(self.treeData.children);
return value;
}).done(function _buildTreeDone() {
self.calculateVisible();
self.calculateHeight();
self.initialized = true;
if ($.isFunction(self.options.ondataload)) {
self.options.ondataload.call(self);
}
});
} else {
// then we assume it's a sring with a valiud url
// I took out url validation because it does more harm than good here.
m.request({method: "GET", url: data})
.then(function _requestBuildtree(value) {
self.treeData = self.buildTree(value);
})
.then(function _requestFlatten() {
Indexes[0] = self.treeData;
self.flatten(self.treeData.children);
})
.then(function _requestCalculate() {
self.calculateVisible();
self.calculateHeight();
self.initialized = true;
if ($.isFunction(self.options.ondataload)) {
self.options.ondataload.call(self);
}
});
}
}
// Rebuilds the tree data with an API
this.buildTree = function _buildTree(data, parent) {
var tree, children, len, child, i;
if (Array.isArray(data)) {
tree = new Item();
children = data;
} else {
tree = new Item(data);
children = data.children;
tree.depth = parent.depth + 1; // Going down the list the parent doesn't yet have depth information
}
if (children) {
len = children.length;
for (i = 0; i < len; i++) {
child = self.buildTree(children[i], tree);
tree.add(child);
}
}
return tree;
};
/**
* Turns the tree structure into a flat index of nodes
* @param {Array} value Array of hierarchical objects
* @param {Number} visibleTop Passes through the beginning point so that refreshes can work, default is 0.
* @return {Array} value Returns a flat version of the hierarchical objects in an array.
*/
this.flatten = function _flatten(value, visibleTop) {
self.flatData = [];
var openLevel,
recursive = function redo(data, show, topLevel) {
var length = data.length, i, children, flat;
for (i = 0; i < length; i++) {
if (openLevel && data[i].depth <= openLevel) {
show = true;
}
children = data[i].children;
flat = {
id: data[i].id,
depth : data[i].depth,
row: data[i].data
};
flat.show = show;
if (data[i].children.length > 0 && !data[i].open) {
show = false;
if (!openLevel || openLevel > data[i].depth) { openLevel = data[i].depth; }
}
self.flatData.push(flat); // add to flatlist
if (children.length > 0) {
redo(children, show, false);
}
Indexes[data[i].id] = data[i];
if (topLevel && i === length - 1) {
self.calculateVisible(visibleTop);
self.calculateHeight();
m.redraw(true);
if(self.options.redrawComplete){
self.options.redrawComplete.call(self);
}
}
}
};
recursive(value, true, true);
return value;
};
/**
* Update view on scrolling the table
*/
this.onScroll = function _scrollHook() {
if (!self.options.paginate) {
var scrollTop, diff, itemsHeight, innerHeight, location, index;
scrollTop = $(this).scrollTop(); // get current scroll top
diff = scrollTop - _lastLocation; //Compare to last scroll location
if (diff > 0 && diff < self.options.rowHeight) { // going down, increase index
$(this).scrollTop(_lastLocation + self.options.rowHeight);
}
if (diff < 0 && diff > -self.options.rowHeight) { // going up, decrease index
$(this).scrollTop(_lastLocation - self.options.rowHeight);
}
itemsHeight = self.calculateHeight();
innerHeight = $(this).children('.tb-tbody-inner').outerHeight();
scrollTop = $(this).scrollTop();
location = scrollTop / innerHeight * 100;
index = Math.round(location / 100 * self.visibleIndexes.length);
self.rangeMargin = Math.floor(itemsHeight * (scrollTop / innerHeight));
self.refreshRange(index);
m.redraw(true);
_lastLocation = scrollTop;
self.highlightMultiselect();
if (self.options.onscrollcomplete) {
self.options.onscrollcomplete.call(self);
}
}
};
/**
* Initialization functions after the main body of the table is loaded
* @param {Object} el The DOM element that config is run on
* @param {Boolean} isInit Whether this function ran once after page load.
*/
this.init = function _init(el, isInit) {
var containerHeight = $('#tb-tbody').height(),
titles = $('.tb-row-titles'),
columns = $('.tb-th');
self.options.showTotal = Math.floor(containerHeight / self.options.rowHeight) + 1;
self.remainder = (containerHeight / self.options.rowHeight) + self.options.rowHeight;
// reapply move on view change.
if (self.options.allowMove) {
self.moveOn();
}
if (isInit) { return; }
self.initializeMove(); // Needed to run once to establish drag and drop options
if (!self.options.rowHeight) { // If row height is not set get it from CSS
self.options.rowHeight = $('.tb-row').height();
}
$('.gridWrapper').mouseleave(function () {
$('.tb-row').removeClass(self.options.hoverClass);
});
// Main scrolling functionality
$('#tb-tbody').scroll(self.onScroll);
function _resizeCols() {
var parentWidth = titles.width(),
percentageTotal = 0,
p;
columns.each(function (index) { // calculate percentages for each column
var col = $(this),
lastWidth;
col.attr('data-tb-size', col.outerWidth());
if (index === $('.tb-th').length - 1) { // last column gets the remainder
lastWidth = 100 - percentageTotal;
self.colsizes[col.attr('data-tb-th-col')] = lastWidth;
col.css('width', lastWidth + '%');
} else {
p = col.outerWidth() / parentWidth * 100;
self.colsizes[col.attr('data-tb-th-col')] = p;
col.css('width', p + '%');
}
percentageTotal += p;
});
}
function convertToPixels() {
var parentWidth = titles.width(),
totalPixels = 0;
columns.each(function (index) {
var col = $(this),
colWidth = parentWidth - totalPixels - 1,
width;
if (index === $('.tb-th').length - 1) { // last column gets the remainder
col.css('width', colWidth + 'px'); // -1 for the border
} else {
width = col.outerWidth();
col.css('width', width);
totalPixels += width;
}
});
}
$('.tb-th.tb-resizable').resizable({
containment : 'parent',
delay : 200,
handles : 'e',
minWidth : 60,
start : function (event, ui) {
convertToPixels();
},
create : function (event, ui) {
// change cursor
$('.ui-resizable-e').css({ "cursor" : "col-resize"} );
},
resize : function (event, ui) {
var thisCol = $(this),
index = $(this).attr('data-tb-th-col'),
totalColumns = columns.length,
// if the overall size is getting bigger than home size, make other items smaller
parentWidth = titles.width() - 1,
childrenWidth = 0,
diff,
nextBigThing,
nextBigThingIndex,
lastBigThing,
lastBigThingIndex,
diff2,
diff3,
w2,
w3,
lastWidth,
colWidth;
columns.each(function () {
childrenWidth = childrenWidth + $(this).outerWidth();
});
if (childrenWidth > parentWidth) {
diff2 = childrenWidth - parentWidth;
nextBigThing = columns.not(ui.element).filter(function () {
var colElement = parseInt($(ui.element).attr('data-tb-th-col')),
colThis = parseInt($(this).attr('data-tb-th-col'));
if (colThis > colElement) {
return $(this).outerWidth() > 40;
}
return false;
}).first();
if (nextBigThing.length > 0) {
w2 = nextBigThing.outerWidth();
nextBigThing.css({ width : (w2 - diff2) + 'px' });
nextBigThingIndex = nextBigThing.attr('data-tb-th-col');
$('.tb-col-' + nextBigThingIndex).css({width : (w2 - diff2) + 'px'});
} else {
$(ui.element).css({ width : $(ui.element).attr('data-tb-currentSize') + 'px'});
return;
}
}
if (childrenWidth < parentWidth) {
diff3 = parentWidth - childrenWidth;
// number of children other than the current element with widths bigger than 40
lastBigThing = columns.not(ui.element).filter(function () {
return $(this).outerWidth() < parseInt($(this).attr('data-tb-size'));
}).last();
if (lastBigThing.length > 0) {
w3 = lastBigThing.outerWidth();
lastBigThing.css({ width : (w3 + diff3) + 'px' });
lastBigThingIndex = lastBigThing.attr('data-tb-th-col');
$('.tb-col-' + lastBigThingIndex).css({width : (w3 + diff3) + 'px'});
} else {
w3 = columns.last().outerWidth();
columns.last().css({width : (w3 + diff3) + 'px'}).attr('data-tb-size', w3 + diff3);
}
}
// make the last column rows be same size as last column header
lastWidth = columns.last().width();
$('.tb-col-' + (totalColumns - 1)).css('width', lastWidth + 'px');
$(ui.element).attr('data-tb-currentSize', $(ui.element).outerWidth());
// change corresponding columns in the table
colWidth = thisCol.outerWidth();
$('.tb-col-' + index).css({width : colWidth + 'px'});
},
stop : function (event, ui) {
_resizeCols();
m.redraw(true);
}
});
if (self.options.uploads) { _applyDropzone(); }
if ($.isFunction(self.options.onload)) {
self.options.onload.call(self);
}
if (self.options.multiselect) {
$(window).keydown(function (event) {
self.pressedKey = event.keyCode;
});
$(window).keyup(function (event) {
self.pressedKey = undefined;
});
}
$(window).keydown(function (event) {
// if escape cancel modal - 27
if (self.modal.on && event.keyCode === 27) {
self.modal.dismiss();
}
// if enter then run the modal - 13
if (self.modal.on && event.keyCode === 13) {
$('.tb-modal-footer .btn-success').trigger('click');
}
});
window.onblur = self.resetKeyPress;
};
/**
* Resets keys that are hung up. Other window onblur event actions can be added in here.
*/
this.resetKeyPress = function () {
self.pressedKey = undefined;
}
/**
* Destroys Treebeard by emptying the DOM object and removing dropzone
* Because DOM objects are removed their events are going to be cleaned up.
*/
this.destroy = function _destroy () {
window.treebeardCounter = -1;
$('#' + self.options.divID).html(''); // Empty HTML
if (self.dropzone) { _destroyDropzone(); } // Destroy existing dropzone setup
};
/**
* Checks if there is filesData option, fails if there isn't, initiates the entire app if it does.
*/
if (self.options.filesData) {
_loadData(self.options.filesData);
} else {
throw new Error("Treebeard Error: You need to define a data source through 'options.filesData'");
}
};
/**
* Mithril View. Documentation is here: (http://lhorie.github.io/mithril/mithril.html) Use m() for templating.
* @param {Object} ctrl The entire Treebeard.controller object with its values and methods. Refer to as ctrl.
*/
Treebeard.view = function treebeardView(ctrl) {
return [
m('.gridWrapper', [
m(".tb-table", [
/**
* Template for the head row, includes whether filter or title should be shown.
*/
(function showHeadA() {
var titleContent = functionOrString(ctrl.options.title);
if (ctrl.options.showFilter || titleContent) {
var filterWidth;
var title = m('.tb-head-title.col-xs-12.col-sm-6', {}, titleContent);
if(ctrl.options.filterFullWidth){
filterWidth = '';
} else {
filterWidth = ctrl.options.title ? '.col-sm-6' : '.col-sm-6.col-sm-offset-6';
}
var filter = m(".tb-head-filter.col-xs-12"+filterWidth, {
}, [
(function showFilterA() {
if (ctrl.options.showFilter) {
return m("input.pull-right.form-control[placeholder='"+ ctrl.options.filterPlaceholder + "'][type='text']", {
style: "width:100%;display:inline;",
onkeyup: ctrl.filter,
value : ctrl.filterText()
}
);
}
}())
]);
if(ctrl.options.title){
return m('.tb-head.row', [
title,
filter
]);
} else {
return m('.tb-head.row', [
filter
]);
}
}
}()),
m(".tb-row-titles", [
/**
* Render column titles based on the columnTitles option.
*/
ctrl.options.columnTitles.call(ctrl).map(function _mapColumnTitles(col, index, arr) {
var sortView = "",
up,
down,
resizable = '.tb-resizable';
var width = ctrl.colsizes[index] ? ctrl.colsizes[index] + '%' : col.width;
if(!ctrl.options.resizeColumns){ // Check if columns can be resized.
resizable = '';
}
if(index === arr.length-1){// Last column itself is not resizable because you don't need to
resizable = '';
}
if (col.sort) { // Add sort buttons with their onclick functions
ctrl.isSorted[index] = { asc : false, desc : false };
if (ctrl.options.sortButtonSelector.up) {
up = ctrl.options.sortButtonSelector.up;
} else {
up = 'i.fa.fa-sort-asc';
}
if (ctrl.options.sortButtonSelector.down) {
down = ctrl.options.sortButtonSelector.down;
} else {
down = 'i.fa.fa-sort-desc';
}
sortView = [
m(up + '.tb-sort-inactive.asc-btn.m-r-xs', {
onclick: ctrl.sortToggle.bind(index),
"data-direction": "asc",
"data-sortType" : col.sortType
}),
m(down + '.tb-sort-inactive.desc-btn', {
onclick: ctrl.sortToggle.bind(index),
"data-direction": "desc",
"data-sortType" : col.sortType
})
];
}
return m('.tb-th'+resizable, { style : "width: " +width, 'data-tb-th-col' : index }, [
m('span.m-r-sm', col.title),
sortView
]);
})
]),
m("#tb-tbody", { config : ctrl.init }, [
/**
* In case a modal needs to be shown, check Modal object
*/
(function showModal() {
if (ctrl.modal.on) {
return m('.tb-modal-shade', { config : ctrl.modal.onmodalshow , style : 'width:' + ctrl.modal.width + 'px; height:' + ctrl.modal.height + 'px;'}, [
m('.tb-modal-inner', { 'class' : ctrl.modal.css }, [
m('.tb-modal-dismiss', { 'onclick' : function () { ctrl.modal.dismiss(); } }, [m('i.icon-remove-sign')]),
m('.tb-modal-content', ctrl.modal.content),
m('.tb-modal-footer', ctrl.modal.actions)])
]);
}
}()),
m('.tb-tbody-inner', [
m('', { style : "margin-top:" + ctrl.rangeMargin + "px" }, [
/**
* showRange has the several items that get shown at a time. It's key to view optimization
* showRange values change with scroll, filter, folder toggling etc.
*/
ctrl.showRange.map(function _mapRangeView(item, index) {
var oddEvenClass = ctrl.options.oddEvenClass.odd,
indent = ctrl.flatData[item].depth,
id = ctrl.flatData[item].id,
tree = Indexes[id],
row = ctrl.flatData[item].row,
padding,
css = tree.css || "",
rowCols = ctrl.options.resolveRows.call(ctrl, tree);
if (index % 2 === 0) {
oddEvenClass = ctrl.options.oddEvenClass.even;
}
if (ctrl.filterOn) {
padding = 20;
} else {
padding = (indent-1) * 20;
}
if (tree.notify.on && !tree.notify.column) { // In case a notification is taking up the column space
return m(".tb-row", [
m('.tb-notify.alert-' + tree.notify.type, { 'class' : tree.notify.css, 'style' : "height: " + ctrl.options.rowHeight + "px;padding-top:4px;" }, [
m('span', tree.notify.message)
])
]);
} else {
return m(".tb-row", { // Events and attribtues for entire row
"key" : id,
"class" : css + " " + oddEvenClass,
"data-id" : id,
"data-level": indent,
"data-index": item,
"data-rIndex": index,
style : "height: " + ctrl.options.rowHeight + "px;",
onclick : function _rowClick(event) {
if (ctrl.options.multiselect) {
ctrl.handleMultiselect(id, index, event);
}
ctrl.selected = id;
if (ctrl.options.onselectrow) {
ctrl.options.onselectrow.call(ctrl, tree, event);
}
},
onmouseover : function _rowMouseover(event) {
ctrl.mouseon = id;
if (ctrl.options.hoverClass && !ctrl.dragOngoing) {
$('.tb-row').removeClass(ctrl.options.hoverClass);
$(this).addClass(ctrl.options.hoverClass);
}
if (ctrl.options.onmouseoverrow) {
ctrl.options.onmouseoverrow.call(ctrl, tree, event);
}
}
}, [
/**
* Build individual columns depending on the resolveRows
*/
rowCols.map(function _mapColumnsContent(col, index) {
var cell,
title,
colInfo = ctrl.options.columnTitles.call(ctrl)[index],
colcss = col.css || '';
var width = ctrl.colsizes[index] ? ctrl.colsizes[index] + '%' : colInfo.width;
cell = m('.tb-td.tb-col-' + index, { 'class' : colcss, style : "width:" + width }, [
m('span', row[col.data])
]);
if (tree.notify.on && tree.notify.column === index) {
return m('.tb-td.tb-col-' + index, { style : "width:" + width }, [
m('.tb-notify.alert-' + tree.notify.type, { 'class' : tree.notify.css }, [
m('span', tree.notify.message)
])
]);
}
if (col.folderIcons) {
if (col.custom) {
title = m("span.title-text", col.custom.call(ctrl, tree, col));
} else {
title = m("span.title-text", row[col.data] + " ");
}
cell = m('.tb-td.td-title.tb-col-' + index, {
"data-id" : id,
'class' : colcss,
style : "padding-left: " + padding + "px; width:" + width
}, [
m("span.tb-td-first", // Where toggling and folder icons are
(function _toggleView() {
var set = [{
'id' : 1,
'css' : 'tb-expand-icon-holder',
'resolve' : ctrl.options.resolveIcon.call(ctrl, tree)
}, {
'id' : 2,
'css' : 'tb-toggle-icon',
'resolve' : ctrl.options.resolveToggle.call(ctrl, tree)
}];
if (ctrl.filterOn) {
return m('span.' + set[0].css, { key : set[0].id }, set[0].resolve);
}
return [m('span.' + set[1].css, { key : set[1].id,
onclick: function _folderToggleClick(event) {
if (ctrl.options.togglecheck.call(ctrl, tree)) {
ctrl.toggleFolder(item, event);
}
}
}, set[1].resolve), m('span.' + set[0].css, { key : set[0].id }, set[0].resolve)];
}())
),
title
]);
}
if (!col.folderIcons && col.custom) { // If there is a custom call.
cell = m('.tb-td.tb-col-' + index, { 'class' : colcss, style : "width:" + width }, [
col.custom.call(ctrl, tree, col)
]);
}
return cell;
})
]);
}
})
])
])
]),
/**
* Footer, scroll/paginate toggle, page numbers.
*/
(function _footer() {
if (ctrl.options.paginate || ctrl.options.paginateToggle) {
return m('.tb-footer', [
m(".row", [
m(".col-xs-4",
(function _showPaginateToggle() {
if (ctrl.options.paginateToggle) {
var activeScroll = "",
activePaginate = "";
if (ctrl.options.paginate) {
activePaginate = "active";
} else {
activeScroll = "active";
}
return m('.btn-group.padder-10', [
m("button.tb-button.tb-scroll",
{ onclick : ctrl.toggleScroll, "class" : activeScroll},
"Scroll"),
m("button.tb-button.tb-paginate",
{ onclick : ctrl.togglePaginate, "class" : activePaginate },
"Paginate")
]);
}
}())
),
m('.col-xs-8', [ m('.padder-10', [
(function _showPaginate() {
if (ctrl.options.paginate) {
var total_visible = ctrl.visibleIndexes.length,
total = Math.ceil(total_visible / ctrl.options.showTotal);
if (ctrl.options.resolvePagination) {
return ctrl.options.resolvePagination.call(ctrl, total, ctrl.currentPage());
}
return m('.tb-pagination.pull-right', [
m('button.tb-pagination-prev.tb-button.m-r-sm',
{ onclick : ctrl.pageDown},
[ m('i.fa.fa-chevron-left')]),
m('input.tb-pagination-input.m-r-sm',
{
type : "text",
style : "width: 30px;",
onkeyup: function (e) {
var page = parseInt(e.target.value, 10);
ctrl.goToPage(page);
},
value : ctrl.currentPage()
}
),
m('span.tb-pagination-span', "/ " + total + " "),
m('button.tb-pagination-next.tb-button',
{ onclick : ctrl.pageUp},
[ m('i.fa.fa-chevron-right')
])
]);
}
}())
])])
])
]);
}
}())
])
])
];
};
/**
* Treebeard default options as a constructor so multiple different types of options can be established.
* Implementations have to declare their own "filesData", "columnTitles", "resolveRows", all others are optional
* @memberof Treebeard
* @param {String} Options.divID - ID of the div that Treebeard draws into
* @param {String|Array} Options.filesData - Data in Array or string url
* @param {Number} Options.rowHeight - Height of rows in pixels
* @param {Boolean} Options.paginate - Whether the applet starts with pagination or not.
* @param {Boolean} Options.paginateToggle - Show the buttons that allow users to switch between scroll and paginate.
* @param {Boolean} Options.uploads - Turns dropzone on/off.
* @param {Boolean} Options.multiselect - turns ability to multiselect with shift or command keys
* @method {Object} Options.columnTitles - Returns an array of column objects
*/
var Options = function() {
this.divID = "myGrid";
this.filesData = "small.json";
this.rowHeight = undefined;
this.paginate = false;
this.paginateToggle = false;
this.uploads = false;
this.multiselect = false;
this.columnTitles = function () { // REQUIRED: Adjust this array based on data needs.
return [
{
title: "Title",
width: "50%",
sortType : "text",
sort : true
},
{
title: "Author",
width : "25%",
sortType : "text"
},
{
title: "Age",
width : "10%",
sortType : "number"
},
{
title: "Actions",
width : "15%"
}
];
};
this.resolveRows = function (item) { // REQUIRED: How rows should be displayed based on data.
return [
{
data : "title", // Data field name
folderIcons : true,
filter : true
}
];
};
this.filterPlaceholder = 'Search';
this.resizeColumns = true; // whether the table columns can be resized.
this.hoverClass = undefined; // Css class for hovering over rows
this.hoverClassMultiselect = 'tb-multiselect'; // Css class for hover on multiselect
this.showFilter = true; // Gives the option to filter by showing the filter box.
this.title = null; // Title of the grid, boolean, string OR function that returns a string.
this.allowMove = true; // Turn moving on or off.
this.moveClass = undefined; // Css class for which elements can be moved. Your login needs to add these to appropriate elements.
this.sortButtonSelector = {}; // custom buttons for sort, needed because not everyone uses FontAwesome
this.dragOptions = {}; // jQuery UI draggable options without the methods
this.dropOptions = {}; // jQuery UI droppable options without the methods
this.dragEvents = {}; // users can override draggable options and events
this.dropEvents = {}; // users can override droppable options and events
this.oddEvenClass = {
odd : 'tb-odd',
even : 'tb-even'
};
this.onload = function () {
// this = treebeard object;
};
this.togglecheck = function (item) {
// this = treebeard object;
// item = folder to toggle
return true;
};
this.onfilter = function (filterText) { // Fires on keyup when filter text is changed.
// this = treebeard object;
// filterText = the value of the filtertext input box.
};
this.onfilterreset = function (filterText) { // Fires when filter text is cleared.
// this = treebeard object;
// filterText = the value of the filtertext input box.
};
this.createcheck = function (item, parent) {
// this = treebeard object;
// item = Item to be added. raw item, not _item object
// parent = parent to be added to = _item object
return true;
};
this.oncreate = function (item, parent) { // When new row is added
// this = treebeard object;
// item = Item to be added. = _item object
// parent = parent to be added to = _item object
};
this.deletecheck = function (item) { // When user attempts to delete a row, allows for checking permissions etc.
// this = treebeard object;
// item = Item to be deleted.
return true;
};
this.ondelete = function () { // When row is deleted successfully
// this = treebeard object;
// item = a shallow copy of the item deleted, not a reference to the actual item
};
this.addcheck = function (treebeard, item, file) { // check is a file can be added to this item
// this = dropzone object
// treebeard = treebeard object
// item = item to be added to
// file = info about the file being added
return true;
};
this.onadd = function (treebeard, item, file, response) {
// this = dropzone object;
// item = item the file was added to
// file = file that was added
// response = what's returned from the server
};
this.onselectrow = function (row, event) {
// this = treebeard object
// row = item selected
// event = mouse click event object
};
this.onmultiselect = function (event, tree) {
// this = treebeard object
// tree = item currently clicked on
// event = mouse click event object
};
this.onmouseoverrow = function (row, event) {
// this = treebeard object
// row = item selected
// event = mouse click event object
};
this.ontogglefolder = function (item) {
// this = treebeard object
// item = toggled folder item
};
this.dropzone = { // All dropzone options.
url: "http://www.torrentplease.com/dropzone.php" // When users provide single URL for all uploads
};
this.dropzoneEvents = {};
this.resolveIcon = function (item) { // Here the user can interject and add their own icons, uses m()
// this = treebeard object;
// Item = item acted on
if (item.kind === "folder") {
if (item.open) {
return m("i.fa.fa-folder-open-o", " ");
}
return m("i.fa.fa-folder-o", " ");
}
if (item.data.icon) {
return m("i.fa." + item.data.icon, " ");
}
return m("i.fa.fa-file ");
};
this.resolveRefreshIcon = function(){
return m('i.icon-refresh.icon-spin');
};
this.resolveToggle = function (item) {
var toggleMinus = m("i.fa.fa-minus-square-o", " "),
togglePlus = m("i.fa.fa-plus-square-o", " ");
if (item.kind === "folder") {
if (item.children.length > 0) {
if (item.open) {
return toggleMinus;
}
return togglePlus;
}
}
return "";
};
this.resolvePagination = function (totalPages, currentPage) {
// this = treebeard object
return m("span", [
m('span', 'Page: '),
m('span.tb-pageCount', currentPage),
m('span', ' / ' + totalPages)
]);
};
this.resolveUploadUrl = function (item) { // Allows the user to calculate the url of each individual row
// this = treebeard object;
// Item = item acted on return item.data.ursl.upload
return "/upload";
};
this.resolveLazyloadUrl = function (item) {
// this = treebeard object;
// Item = item acted on
return false;
};
this.lazyLoadError = function (item) {
// this = treebeard object;
// Item = item acted on
};
this.lazyLoadOnLoad = function (item) {
// this = treebeard object;
// Item = item acted on
};
this.ondataload = function (item) {
// this = treebeard object;
};
};
/**
* Starts treebard with user options
* This may seem convoluted but is useful to encapsulate Treebeard instances.
* @param {Object} options The options user passes in; will be expanded with defaults.
* @returns {*}
*/
var runTB = function _treebeardRun(options) {
var defaults = new Options();
var finalOptions = $.extend(defaults, options);
var tb = {};
tb.controller = function() {
this.tbController = new Treebeard.controller(finalOptions);
};
tb.view = function(ctrl) {
return Treebeard.view(ctrl.tbController);
};
// Weird fix for IE 9, does not harm regular load
if( window.navigator.userAgent.indexOf('MSIE')){
setTimeout(function(){ m.redraw();}, 1000);
}
return m.module(document.getElementById(finalOptions.divID), tb );
};
// Expose some internal classes to the public
runTB.Notify = Notify;
// return runTB;
//}));