////////////////////////////////////
//
// Grid
// MIT License
// Copyright (c) 2018 Matt V. Murphy
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
//
////////////////////////////////////
(function(window, document, undefined) {
"use strict";
var GridProto;
var Grid = function(element, options) {
if ((this.element = (typeof(element) === "string") ? $(element) : element)) {
this.css = { idRulePrefix : "#" + this.element.id + " ", sheet : null, rules : {} };
this.columns = 0;
this.columnWidths = [];
this.cellData = { head : [], body : [], foot : [] };
this.alignTimer = null;
this.rawData = [];
this.sortCache = {};
this.lastSortedColumn = [-1, null];
this.selectedIndexes = [];
this.usesTouch = (window.ontouchstart !== undefined);
this.startEvt = (this.usesTouch) ? "touchstart" : "mousedown";
this.moveEvt = (this.usesTouch) ? "touchmove" : "mousemove";
this.endEvt = (this.usesTouch) ? "touchend" : "mouseup";
this.setOptions(options);
this.init();
}
};
//////////////////////////////////////////////////////////////////////////////////
(GridProto = Grid.prototype).nothing = function(){};
//////////////////////////////////////////////////////////////////////////////////
GridProto.setOptions = function(options) {
var hasOwnProp = Object.prototype.hasOwnProperty,
option;
this.options = {
srcType : "", // "dom", "json", "xml"
srcData : "",
allowGridResize : false,
allowColumnResize : false,
allowClientSideSorting : false,
allowSelections : false,
allowMultipleSelections : false,
showSelectionColumn : false,
onColumnSort : this.nothing,
onResizeGrid : this.nothing,
onResizeGridEnd : this.nothing,
onResizeColumn : this.nothing,
onResizeColumnEnd : this.nothing,
onRowSelect : this.nothing,
onLoad : this.nothing,
supportMultipleGridsInView : false,
fixedCols : 0,
selectedBgColor : "#eaf1f7",
fixedSelectedBgColor : "#dce7f0",
colAlign : [], // "left", "center", "right"
colBGColors : [],
colSortTypes : [], // "string", "number", "date", "custom", "none"
customSortCleaner : null
};
if (options) {
for (option in this.options) {
if (hasOwnProp.call(this.options, option) && options[option] !== undefined) {
this.options[option] = options[option];
}
}
}
this.options.allowColumnResize = this.options.allowColumnResize && !this.usesTouch;
this.options.allowMultipleSelections = this.options.allowMultipleSelections && this.options.allowSelections;
this.options.showSelectionColumn = this.options.showSelectionColumn && this.options.allowSelections;
this.options.fixedCols = (!this.usesTouch) ? this.options.fixedCols : 0;
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.init = function() {
var srcType = this.options.srcType,
srcData = this.options.srcData,
data;
this.generateSkeleton();
this.addEvents();
// DOM:
if (srcType === "dom" && (srcData = (typeof(srcData) === "string") ? $(srcData) : srcData)) {
this.convertData(this.convertDomDataToJsonData(srcData));
// JSON:
} else if (srcType === "json" && (data = parseJSON(srcData))) {
this.convertData(data);
// XML:
} else if (srcType === "xml" && (data = parseXML(srcData))) {
this.convertData(this.convertXmlDataToJsonData(data));
}
this.generateGrid();
this.displayGrid();
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.generateSkeleton = function() {
var doc = document,
elems = [["base", "g_Base", "docFrag"],
["head", "g_Head", "base"],
["headFixed", "g_HeadFixed", "head"],
["headStatic", "g_HeadStatic", "head"],
["foot", "g_Foot", "base"],
["footFixed", "g_FootFixed", "foot"],
["footStatic", "g_FootStatic", "foot"],
["body", "g_Body", "base"],
["bodyFixed", "g_BodyFixed", "body"],
["bodyFixed2", "g_BodyFixed2", "bodyFixed"],
["bodyStatic", "g_BodyStatic", "body"]];
this.parentDimensions = { x : this.element.offsetWidth, y : this.element.offsetHeight };
this.docFrag = doc.createDocumentFragment();
for (var i=0, elem; elem=elems[i]; i++) {
(this[elem[0]] = doc.createElement("DIV")).className = elem[1];
this[elem[2]].appendChild(this[elem[0]]);
}
if (this.options.allowGridResize) {
(this.baseResize = doc.createElement("DIV")).className = "g_BaseResize";
this.base.appendChild(this.baseResize);
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.addEvents = function() {
var wheelEvent;
// Simulate mouse scrolling over non-scrollable content:
if (this.options.fixedCols > 0 && !this.usesTouch && !msie) {
try {
wheelEvent = (WheelEvent("wheel")) ? "wheel" : undefined;
} catch (e) {
wheelEvent = (document.onmousewheel !== undefined) ? "mousewheel" : "DOMMouseScroll";
}
if (wheelEvent) {
addEvent(this.bodyFixed, wheelEvent, bind(this.simulateMouseScroll, this));
}
}
// Grid resizing:
if (this.options.allowGridResize) {
addEvent(this.baseResize, this.startEvt, bind(this.initResizeGrid, this));
}
// Column resizing and client side sorting:
if (this.options.allowColumnResize || this.options.allowClientSideSorting) {
addEvent(this.head, this.startEvt, bind(this.delegateHeaderEvent, this));
}
// Row selection:
if (this.options.allowSelections) {
addEvent(this.body, this.startEvt, bind(this.selectRange, this));
if (this.options.showSelectionColumn) {
addEvent(this.body, "click", bind(this.preventSelectionInputStateChange, this));
}
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.convertDomDataToJsonData = function(data) {
var sections = { "thead" : "Head", "tbody" : "Body", "tfoot" : "Foot" },
section, rows, row, cells, arr, arr2, i, j, k,
json = {};
// Cycle through all table rows, change sections when needed:
if (((data || {}).tagName || "").toLowerCase() === "table") {
for (i=0, j=0, rows=data.rows; row=rows[i]; i++) {
if (row.sectionRowIndex === 0 && (section = sections[row.parentNode.tagName.toLowerCase()])) {
json[section] = arr = (json[section] || []);
j = arr.length;
}
arr[j++] = arr2 = [];
k = (cells = row.cells).length;
while (k) { arr2[--k] = cells[k].innerHTML; }
}
}
return json;
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.convertXmlDataToJsonData = function(data) {
var sections = { "thead" : "Head", "tbody" : "Body", "tfoot" : "Foot" },
cellText = (msie < 9) ? "text" : "textContent",
nodes, node, section, rows, row, cells, cell, tag, n, i, j,
arr, arr2, a, a2,
json = {};
// By section:
if ((nodes = (data.getElementsByTagName("table")[0] || {}).childNodes)) {
for (n=0; node=nodes[n]; n++) {
if ((section = sections[node.nodeName]) && (rows = node.childNodes)) {
json[section] = arr = (json[section] || []);
a = arr.length;
// By row:
for (i=0; row=rows[i]; i++) {
if (row.nodeName === "tr" && (cells = row.childNodes)) {
arr[a++] = arr2 = [];
a2 = 0;
// By cell:
for (j=0; cell=cells[j]; j++) {
if ((tag = cell.nodeName) === "td" || tag === "th") {
arr2[a2++] = cell[cellText] || "";
}
}
}
}
}
}
}
return json;
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.convertData = function(data) {
var base, cols, h, b, f;
this.addSelectionColumn(data);
this.rawData = data.Body || [];
if ((base = data.Head || data.Body || data.Foot || null)) {
cols = this.columns = base[0].length;
h = this.cellData.head;
b = this.cellData.body;
f = this.cellData.foot;
while (cols) { h[--cols] = []; b[cols] = []; f[cols] = []; }
cols = this.columns;
if (data.Head) {
this.convertDataItem(h, data.Head, "
";
row = rows[rowIdx];
colIdx = cols;
while (colIdx) {
arr[--colIdx][rowIdx] = rowDiv + (row[colIdx] || " ");
}
}
if (allowColResize && (rowIdx = rows.length)) {
colIdx = cols;
while (colIdx) {
arr[--colIdx][0] = ("
") + arr[colIdx][0];
}
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.addSelectionColumn = function(data) {
var html, rows, i;
if (this.options.showSelectionColumn) {
this.options.colBGColors.unshift(this.options.colBGColors[0] || "");
this.options.colSortTypes.unshift("none");
this.options.colAlign.unshift("left");
if (!this.usesTouch) {
this.options.fixedCols++;
}
if ((rows = data.Head) && (i = rows.length)) {
while (i) { rows[--i].unshift(""); }
}
if ((rows = data.Body) && (i = rows.length)) {
html = "
";
while (i) { rows[--i].unshift(html); }
}
if ((rows = data.Foot) && (i = rows.length)) {
while (i) { rows[--i].unshift(""); }
}
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.generateGrid = function() {
this.hasHead = ((this.cellData.head[0] || []).length > 0);
this.hasBody = ((this.cellData.body[0] || []).length > 0);
this.hasFoot = ((this.cellData.foot[0] || []).length > 0);
this.hasHeadOrFoot = (this.hasHead || this.hasFoot);
this.hasFixedCols = (this.options.fixedCols > 0);
this.generateGridHead();
this.generateGridBody();
this.generateGridFoot();
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.generateGridHead = function() {
var hHTML;
if (this.hasHead) {
hHTML = this.generateGridSection(this.cellData.head);
this.headStatic.innerHTML = hHTML.fullHTML;
if (this.hasFixedCols) {
this.headFixed.innerHTML = hHTML.fixedHTML;
}
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.generateGridBody = function() {
var bHTML;
if (this.hasBody) {
bHTML = this.generateGridSection(this.cellData.body);
this.bodyStatic.innerHTML = bHTML.fullHTML;
if (this.hasFixedCols) {
this.bodyFixed2.innerHTML = bHTML.fixedHTML;
}
} else {
this.bodyStatic.innerHTML = "
No results returned.
";
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.generateGridFoot = function() {
var fHTML;
if (this.hasFoot) {
fHTML = this.generateGridSection(this.cellData.foot);
this.footStatic.innerHTML = fHTML.fullHTML;
if (this.hasFixedCols) {
this.footFixed.innerHTML = fHTML.fixedHTML;
}
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.generateGridSection = function(cols) {
var replaceFunc = function($1, $2) { return cols[parseInt($2, 10)].join("
"); },
replaceRgx = /@(\d+)@/g,
fixedCols = this.options.fixedCols,
fHtml = [], sHtml = [],
colIdx = cols.length;
while (colIdx) {
if ((--colIdx) < fixedCols) {
fHtml[colIdx] = "@" + colIdx + "@
";
sHtml[colIdx] = "";
} else {
sHtml[colIdx] = "@" + colIdx + "@
";
}
}
return { fixedHTML : (fixedCols) ? fHtml.join("").replace(replaceRgx, replaceFunc) : "",
fullHTML : sHtml.join("").replace(replaceRgx, replaceFunc) };
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.displayGrid = function() {
var srcType = this.options.srcType,
srcData = this.options.srcData,
replace = false;
// Setup scrolling:
this.lastScrollLeft = 0;
this.lastScrollTop = 0;
this.body.onscroll = bind(this.syncScrolls, this);
// Prep style element:
try {
this.css.sheet.parentNode.removeChild(this.css.sheet);
} catch (e) {
(this.css.sheet = document.getElementById(this.element.id + "SS") ||document.createElement("STYLE")).id = this.element.id + "SS";
this.css.sheet.type = "text/css";
}
// Insert grid into DOM:
if (srcType === "dom" && (srcData = (typeof(srcData) === "string") ? $(srcData) : srcData)) {
if ((replace = (this.element === srcData.parentNode))) {
this.element.replaceChild(this.docFrag, srcData);
}
}
if (!replace) {
this.element.appendChild(this.docFrag);
}
// Align columns:
this.alignTimer = window.setTimeout(bind(this.alignColumns, this, false, true), 16);
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.alignColumns = function(reAlign, fromInit) {
var sNodes = [this.headStatic.children || [], this.bodyStatic.children || [], this.footStatic.children || []],
fNodes = [this.headFixed.children || [], this.bodyFixed2.children || [], this.footFixed.children || []],
allowColumnResize = this.options.allowColumnResize,
colBGColors = this.options.colBGColors,
colAlign = this.options.colAlign,
fixedCols = this.options.fixedCols,
rules = this.css.rules,
colWidth, nodes;
// Compute base styles first, or remove old column width styling if realigning the columns:
if (reAlign !== true) {
this.computeBaseStyles();
} else {
for (var i=0, len=this.columns; i 0) ? this.tmp.origWidth + xDif : this.tmp.origWidth - Math.abs(xDif));
newHeight = Math.max(30, (yDif > 0) ? this.tmp.origHeight + yDif : this.tmp.origHeight - Math.abs(yDif));
elemStyle = this.element.style;
elemStyle.width = newWidth + "px";
elemStyle.height = newHeight + "px";
this.parentDimensions = { x : newWidth, y : newHeight };
this.syncScrolls();
clearTextSelections();
this.options.onResizeGrid.apply(this, [newWidth, newHeight]);
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.endResizeGrid = function(event) {
removeEvent(document, this.moveEvt, this.tmp.boundMoveEvt);
removeEvent(document, this.endEvt, this.tmp.boundEndEvt);
this.options.onResizeGridEnd.apply(this, [this.parentDimensions.x, this.parentDimensions.y]);
this.tmp = undefined;
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.delegateHeaderEvent = function(event) {
var event = event || window.event,
target = event.target || event.srcElement,
targetClass = target.className || "";
if (event.button !== 2) {
if (this.options.allowColumnResize && targetClass.indexOf("g_RS") > -1) {
return this.initResizeColumn(event, target, targetClass);
} else if (this.hasBody && this.options.allowClientSideSorting) {
while (targetClass.indexOf("g_Cl") === -1 && targetClass !== "g_Head") {
targetClass = (target = target.parentNode).className || "";
}
if (targetClass.indexOf("g_Cl") > -1) {
this.sortColumn(parseInt(/g_Cl(\d+)/.exec(targetClass)[1], 10));
}
}
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.initResizeColumn = function(event, target, targetClass) {
var colIdx = parseInt(targetClass.replace(/g_RS/g, ""), 10),
doc = document;
this.tmp = {
lastLeft : -1,
colIdx : colIdx,
origX : getEventPositions(event, "client").x,
origWidth : this.columnWidths[colIdx],
origLeft : target.offsetLeft,
boundMoveEvt : bind(this.resizeColumn, this),
boundEndEvt : bind(this.endResizeColumn, this),
dragger : doc.createElement("DIV")
};
this.tmp.dragger.className = "g_ResizeDragger";
this.tmp.dragger.style.left = this.tmp.origLeft + "px";
this.base.insertBefore(this.tmp.dragger, this.base.firstChild);
addEvent(doc, this.moveEvt, this.tmp.boundMoveEvt);
addEvent(doc, this.endEvt, this.tmp.boundEndEvt);
return stopEvent(event);
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.resizeColumn = function(event) {
var clientX = getEventPositions(event || window.event, "client").x,
xDif = clientX - this.tmp.origX,
newWidth = Math.max(15, (xDif > 0) ? this.tmp.origWidth + xDif : this.tmp.origWidth - Math.abs(xDif)),
newLeft = (xDif > 0) ? this.tmp.origLeft + xDif : this.tmp.origLeft - Math.abs(xDif);
this.tmp.newWidth = newWidth;
if (this.tmp.lastLeft !== newLeft && newWidth > 15) {
this.tmp.dragger.style.left = newLeft + "px";
this.tmp.lastLeft = newLeft;
}
clearTextSelections();
this.options.onResizeColumn.apply(this, [this.tmp.colIdx, newWidth]);
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.endResizeColumn = function(event) {
var newWidth = this.tmp.newWidth || this.tmp.origWidth,
colIdx = this.tmp.colIdx;
removeEvent(document, this.moveEvt, this.tmp.boundMoveEvt);
removeEvent(document, this.endEvt, this.tmp.boundEndEvt);
this.tmp.dragger.parentNode.removeChild(this.tmp.dragger);
this.css.rules[".g_Cl" + colIdx]["width"] = newWidth + "px";
this.css.rules[".g_RS" + colIdx]["margin-left"] = (newWidth - 2) + "px";
this.columnWidths[colIdx] = newWidth;
this.setRules();
this.syncScrolls();
this.options.onResizeColumnEnd.apply(this, [colIdx, newWidth]);
this.tmp = undefined;
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.sortColumn = function(colIdx, sortAsc) {
var colIdx = parseInt(colIdx, 10) || ((colIdx === 0) ? 0 : -1),
colSortAs = (colIdx > -1) ? this.options.colSortTypes[colIdx] || "string" : "none",
lastCol = this.lastSortedColumn;
if (colSortAs !== "none") {
sortAsc = (sortAsc === undefined) ? ((colIdx === lastCol[0]) ? !lastCol[1] : true) : !!sortAsc;
this.sortRawData(colIdx, colSortAs, sortAsc);
}
};
//////////////////////////////////////////////////////////////////////////////////
GridProto.sortRawData = function(colIdx, colSortAs, sortAsc) {
var selIndexes, ltVal, gtVal, i,
rawData = this.rawData,
newSelIndexes = [],
newIdxOrder = [],
that = this;
// Store prior index order:
i = rawData.length;
while (i) { rawData[--i].pIdx = i; }
// Sort the body data by type:
ltVal = (sortAsc) ? -1 : 1;
gtVal = (sortAsc) ? 1 : -1;
rawData.sort(function(a, b) {
return that.getSortResult(colSortAs, colIdx, ltVal, gtVal, a[colIdx], b[colIdx]);
});
// Update the grid body HTML:
this.convertDataItem(this.cellData.body, rawData, "