//////////////////////////////////// // // 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 = "
"); }, 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, "