From e6fc4b831d64e044d2aaecb9febfdd0c3b6ae10b Mon Sep 17 00:00:00 2001 From: ZitRo Date: Tue, 27 Jan 2015 22:15:13 +0200 Subject: [PATCH] listing search, fixed IE/FF header columns, style fixes --- example/index.html | 1 + export/LightPivotTable-DeepSeePortlet.xml | 10 +- package.json | 2 +- readme.md | 1 + source/css/LightPivot.css | 109 ++++++++++++-- source/js/DataController.js | 44 +++++- source/js/LightPivotTable.js | 1 + source/js/PivotView.js | 170 ++++++++++++++++++---- 8 files changed, 295 insertions(+), 43 deletions(-) diff --git a/example/index.html b/example/index.html index 9c5bb99..53a9d9d 100644 --- a/example/index.html +++ b/example/index.html @@ -119,6 +119,7 @@ //, listingColumnMinWidth: 200 // minimal width of column in listing //, maxHeaderWidth: 100 // maximum width of header //, columnResizing: true // make columns resizable (default: true) + //, enableSearch: false // enables search panel in listing }; if (req.DrillDownExpression) { // set custom DrillDown on variant 10 diff --git a/export/LightPivotTable-DeepSeePortlet.xml b/export/LightPivotTable-DeepSeePortlet.xml index 9121d75..fa5b1b6 100644 --- a/export/LightPivotTable-DeepSeePortlet.xml +++ b/export/LightPivotTable-DeepSeePortlet.xml @@ -11,7 +11,7 @@ %DeepSee.Component.Portlet.abstractPortlet -63578,68626.779569 +63579,79833.277338 63515,61322.546099 @@ -50,6 +50,10 @@ %Boolean + +%Boolean + + 1 %String @@ -79,6 +83,7 @@ set pInfo($I(pInfo)) = $LB("ListingColumnMinWidth", 0, "%Integer", $$$Text("Min cell width for listing", "%DeepSee"), "Minimal column width in listing") set pInfo($I(pInfo)) = $LB("MaxHeaderWidth", 0, "%Integer", $$$Text("Max column width", "%DeepSee"), "Maximal column width for headers") set pInfo($I(pInfo)) = $LB("ColumnResizing", 1, "%Boolean", $$$Text("Column resizing", "%DeepSee"), "Allow resizing columns with cursor") + set pInfo($I(pInfo)) = $LB("EnableSearch", 1, "%Boolean", $$$Text("Enable listing search", "%DeepSee"), "Show search tools in listing mode") quit $$$OK ]]> @@ -254,6 +259,7 @@ setup["showSummary"] = !!parseInt(container.getAttribute("show-summary")); setup["attachTotals"] = !!parseInt(container.getAttribute("fixTotals")); setup["columnResizing"] = !!parseInt(container.getAttribute("columnResizing")); + setup["enableSearch"] = !!parseInt(container.getAttribute("enableSearch")); if (parseInt(container.getAttribute("pagination"))) { setup["pagination"] = parseInt(container.getAttribute("pagination")) } @@ -388,7 +394,7 @@ } &html< -
+
> diff --git a/package.json b/package.json index d22e195..bca78cd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "LightPivotTable", "author": "ZitRo", - "version": "1.0.0-beta.7", + "version": "1.0.0-beta.8", "description": "A lightweight pivot table for MDX2JSON source for InterSystems Cache", "main": "test/testServer.js", "repository": { diff --git a/readme.md b/readme.md index 8c89d84..7009a4e 100644 --- a/readme.md +++ b/readme.md @@ -66,6 +66,7 @@ var setup = { // Object that contain settings. Any setting may be missed. [ , listingColumnMinWidth: 200 ] // minimal width of column in listing [ , maxHeaderWidth: 100 ] // maximum width of header [ , columnResizing: true ] // make columns resizable (default: true) + [ , enableSearch: true ] // enables search panel in listing (default: true) }, lp = new LightPivotTable(setup); diff --git a/source/css/LightPivot.css b/source/css/LightPivot.css index 0ed9943..0485c26 100644 --- a/source/css/LightPivot.css +++ b/source/css/LightPivot.css @@ -173,7 +173,6 @@ .lpt .lpt-pageSwitcher { position: absolute; - display: table; box-sizing: border-box; bottom: 0; width: 100%; @@ -185,8 +184,6 @@ .lpt .lpt-pageSwitcher > div { position: relative; - display: table-cell; - vertical-align: middle; height: 100%; text-align: center; } @@ -368,17 +365,109 @@ padding: 0 !important; } +.lpt-searchBlock { + position: absolute; + box-sizing: border-box; + overflow: hidden; + bottom: 20px; + width: 100%; + height: 20px; + font-size: 14px; + border: 1px solid rgb(208, 208, 208); + background: #F0F0F0; +} + +.lpt-searchSelectOuter { + position: relative; + display: inline-block; + width: 150px; + margin: 1px 0 0 5px; + overflow: hidden; + vertical-align: top; + background: #F0F0F0; + border: 1px solid #D0D0D0; + border-top: none; + border-bottom: none; + border-radius: 10px; +} + +.lpt-searchSelectOuter:after { + content: "\25bc"; + position: absolute; + top: 0; + right: 5px; + color: gray; +} + +.lpt-searchSelectOuter select { + font-size: 13px; + padding: 0 0 0 2px; + width: 170px; + vertical-align: top; + height: 17px; + max-height: 20px; + background: transparent; + outline: medium none; + border: 0; + cursor: pointer; +} + +/* relative positioning is incorrect here */ .lpt-resizableColumn { + +} + +.lpt-searchIcon { position: relative; + display: inline-block; + vertical-align: top; + box-sizing: border-box; + margin: 1px 0 0 1px; + width: 17px; + height: 17px; + border-radius: 9px; + background: #ffffff; + border: 1px solid rgb(208, 208, 208); } -.lpt-resizableColumn:after { - display: block; +.lpt-searchIcon:before { position: absolute; content: ""; - right: -5px; - top: 0; - width: 10px; - cursor: col-resize !important; - height: 100%; + left: 1px; + top: 1px; + width: 4px; + height: 4px; + border: 3px solid grey; + border-radius: 5px; +} + +.lpt-searchIcon:after { + position: absolute; + content: ""; + left: 9px; + top: 7px; + width: 3px; + height: 6px; + background: grey; + border-radius: 1px; + -webkit-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -o-transform: rotate(-45deg); + transform: rotate(-45deg); +} + +.lpt-searchInput { + display: inline-block; + box-sizing: border-box; + font-size: 13px; + padding: 0 4px 0 4px; + margin: 1px 0 0 5px; + border-radius: 10px; + height: 17px; + background: #FFFFFF; + border: 1px solid #D0D0D0; + border-top: none; + border-bottom: none; + outline: none; } \ No newline at end of file diff --git a/source/js/DataController.js b/source/js/DataController.js index c95aa40..38a865b 100644 --- a/source/js/DataController.js +++ b/source/js/DataController.js @@ -530,8 +530,7 @@ DataController.prototype._trigger = function () { DataController.prototype.sortByColumn = function (columnIndex) { var data = this._dataStack[this._dataStack.length - 1].data, - totalsAttached = this.SUMMARY_SHOWN - && this.controller.CONFIG["attachTotals"] ? 1 : 0; + totalsAttached = this.SUMMARY_SHOWN && this.controller.CONFIG["attachTotals"] ? 1 : 0; if (this.SORT_STATE.column !== columnIndex) { order = this.SORT_STATE.order = 0; @@ -579,4 +578,45 @@ DataController.prototype.sortByColumn = function (columnIndex) { this._trigger(); +}; + +/** + * Filter raw data by part of value. + * + * @param {string} valuePart + * @param {number} columnIndex + */ +DataController.prototype.filterByValue = function (valuePart, columnIndex) { + + var data = this._dataStack[this._dataStack.length - 1].data, + totalsAttached = this.SUMMARY_SHOWN && this.controller.CONFIG["attachTotals"] ? 1 : 0, + newRawData = data._rawDataOrigin.slice( + data.info.topHeaderRowsNumber, + data._rawDataOrigin.length - (this.SUMMARY_SHOWN && !totalsAttached ? 1 : 0) + ), + re = null; + + try { + re = new RegExp(valuePart, "i"); + } catch (e) { + try { + re = new RegExp(valuePart.replace(/([()[{*+.$^\\|?])/g, "\\$1"), "i"); + } catch (e) { + return; + } + } + + newRawData = newRawData.filter(function (row) { + return (row[columnIndex].value || "").toString().match(re); + }); + + data.rawData = data._rawDataOrigin.slice(0, data.info.topHeaderRowsNumber) + .concat(newRawData) + .concat(this.SUMMARY_SHOWN && !totalsAttached + ? [data._rawDataOrigin[data._rawDataOrigin.length - 1]] + : [] + ); + + this._trigger(); + }; \ No newline at end of file diff --git a/source/js/LightPivotTable.js b/source/js/LightPivotTable.js index 7dbb155..e743115 100644 --- a/source/js/LightPivotTable.js +++ b/source/js/LightPivotTable.js @@ -297,6 +297,7 @@ LightPivotTable.prototype.getPivotProperty = function (path) { LightPivotTable.prototype.normalizeConfiguration = function (config) { if (typeof config["columnResizing"] === "undefined") config.columnResizing = true; if (typeof config["pagination"] === "undefined") config.pagination = 200; + if (typeof config["enableSearch"] === "undefined") config.enableSearch = true; if (!config["triggers"]) config.triggers = {}; if (!config["dataSource"]) config.dataSource = {}; }; diff --git a/source/js/PivotView.js b/source/js/PivotView.js index 8c2bc34..b62ce87 100644 --- a/source/js/PivotView.js +++ b/source/js/PivotView.js @@ -14,7 +14,9 @@ var PivotView = function (controller, container) { container: container, base: document.createElement("div"), tableContainer: undefined, - messageElement: undefined + messageElement: undefined, + searchSelect: undefined, + searchInput: undefined }; /** @@ -41,6 +43,14 @@ var PivotView = function (controller, container) { this.PAGINATION_BLOCK_HEIGHT = 20; this.ANIMATION_TIMEOUT = 500; + this.SEARCH_ENABLED = false; + this.SEARCHBOX_LEFT_MARGIN = 191; + this.savedSearch = { + restore: false, + value: "", + columnIndex: 0 + }; + this.controller = controller; this.SCROLLBAR_WIDTH = (function () { @@ -137,6 +147,8 @@ PivotView.prototype.pushTable = function (opts) { tableElement.className = "tableContainer"; if (this.tablesStack.length) { this.tablesStack[this.tablesStack.length - 1].FIXED_COLUMN_SIZES = this.FIXED_COLUMN_SIZES; + this.tablesStack[this.tablesStack.length - 1].savedSearch = this.savedSearch; + this.savedSearch = { restore: false, value: "", columnIndex: 0 }; tableElement.style.left = "100%"; } @@ -174,6 +186,7 @@ PivotView.prototype.popTable = function () { this.pagination = (currentTable = this.tablesStack[this.tablesStack.length - 1]).pagination; if (currentTable.FIXED_COLUMN_SIZES) this.FIXED_COLUMN_SIZES = currentTable.FIXED_COLUMN_SIZES; + if (currentTable.savedSearch) this.savedSearch = currentTable.savedSearch; setTimeout(function () { garbage.element.parentNode.removeChild(garbage.element); @@ -448,6 +461,27 @@ PivotView.prototype.applyConditionalFormatting = function (rules, key, value, el }; +/** + * DeepSee-defined colors. + * + * @param {string} name - name of color. F.e. "red". + * @returns {{ r: number, g: number, b: number }} + */ +PivotView.prototype.colorNameToRGB = function (name) { + var c = function (r, g, b) { return { r: r, g: g, b: b } }; + switch (name) { + case "red": return c(255, 0, 0); + case "green": return c(0, 255, 0); + case "blue": return c(0, 0, 255); + case "purple": return c(102, 0, 153); + case "salmon": return c(255, 140, 105); + case "white": return c(255, 255, 255); + case "black": return c(0, 0, 0); + case "gray": return c(128, 128, 128); + default: return c(255, 255, 255); + } +}; + /** * @param container */ @@ -472,6 +506,8 @@ PivotView.prototype.recalculateSizes = function (container) { lTableHead = leftHeader.getElementsByTagName("thead")[0], tableBlock = container.getElementsByClassName("lpt-tableBlock")[0], pTableHead = tableBlock.getElementsByTagName("tbody")[0], + searchInput = container.getElementsByClassName("lpt-searchInput")[0], + searchInputSize = searchInput ? container.offsetWidth - this.SEARCHBOX_LEFT_MARGIN : 0, tableTr = tableBlock.getElementsByTagName("tr")[0]; if (tTableHead.childNodes[0] && tTableHead.childNodes[0].lastChild["_extraCell"]) { @@ -481,7 +517,8 @@ PivotView.prototype.recalculateSizes = function (container) { lTableHead.removeChild(lTableHead.lastChild); } - var pagedHeight = this.pagination.on ? this.PAGINATION_BLOCK_HEIGHT : 0, + var pagedHeight = (this.pagination.on ? this.PAGINATION_BLOCK_HEIGHT : 0) + + (this.SEARCH_ENABLED ? this.PAGINATION_BLOCK_HEIGHT : 0), headerW = Math.max(leftHeader.offsetWidth, headerContainer.offsetWidth), headerH = topHeader.offsetHeight, containerHeight = container.offsetHeight, @@ -551,11 +588,15 @@ PivotView.prototype.recalculateSizes = function (container) { cell.style.height = (this.SCROLLBAR_WIDTH ? this.SCROLLBAR_WIDTH + 1 : 0) + "px"; } + if (searchInput) { + searchInput.style.width = searchInputSize + "px"; + } + if (hasVerticalScrollBar) { leftHeader.className = leftHeader.className.replace(/\sbordered/, "") + " bordered"; } - for (i in tableTr.childNodes) { + if (tableTr) for (i in tableTr.childNodes) { if (tableTr.childNodes[i].tagName !== "TD") continue; tableTr.childNodes[i].style.width = cellWidths[i] + "px"; } @@ -577,27 +618,6 @@ PivotView.prototype.recalculateSizes = function (container) { }; -/** - * DeepSee-defined colors. - * - * @param {string} name - name of color. F.e. "red". - * @returns {{ r: number, g: number, b: number }} - */ -PivotView.prototype.colorNameToRGB = function (name) { - var c = function (r, g, b) { return { r: r, g: g, b: b } }; - switch (name) { - case "red": return c(255, 0, 0); - case "green": return c(0, 255, 0); - case "blue": return c(0, 0, 255); - case "purple": return c(102, 0, 153); - case "salmon": return c(255, 140, 105); - case "white": return c(255, 255, 255); - case "black": return c(0, 0, 0); - case "gray": return c(128, 128, 128); - default: return c(255, 255, 255); - } -}; - /** * Raw data - plain 2-dimensional array of data to render. * @@ -624,6 +644,8 @@ PivotView.prototype.renderRawData = function (data) { CLICK_EVENT = this.controller.CONFIG["triggerEvent"] || "click", ATTACH_TOTALS = info.SUMMARY_SHOWN && this.controller.CONFIG["attachTotals"] ? 1 : 0, COLUMN_RESIZE_ON = !!this.controller.CONFIG.columnResizing, + LISTING = info.leftHeaderColumnsNumber === 0, + SEARCH_ENABLED = LISTING && this.controller.CONFIG["enableSearch"], container = this.elements.tableContainer, pivotTopSection = document.createElement("div"), @@ -639,15 +661,38 @@ PivotView.prototype.renderRawData = function (data) { LHTHead = document.createElement("thead"), mainTable = document.createElement("table"), mainTBody = document.createElement("tbody"), + pageSwitcher = this.pagination.on ? document.createElement("div") : null, pageNumbers = this.pagination.on ? [] : null, pageSwitcherContainer = pageSwitcher ? document.createElement("div") : null, + + searchBlock = SEARCH_ENABLED ? document.createElement("div") : null, + searchIcon = SEARCH_ENABLED ? document.createElement("span") : null, + searchSelect = SEARCH_ENABLED ? document.createElement("select") : null, + searchSelectOuter = SEARCH_ENABLED ? document.createElement("span") : null, + searchInput = SEARCH_ENABLED ? document.createElement("input") : null, + searchFields = SEARCH_ENABLED ? (function () { + var arr = [], + x = info.leftHeaderColumnsNumber, + y = info.topHeaderRowsNumber - 1 - ATTACH_TOTALS; + for (var i = x; i < rawData[y].length; i++) { + arr.push({ + value: rawData[y][i].value, + source: rawData[y][i].source, + columnIndex: i + }); + } + return arr; + })() : null, + _RESIZING = false, _RESIZING_ELEMENT = null, _RESIZING_COLUMN_INDEX = 0, _RESIZING_ELEMENT_BASE_WIDTH, _RESIZING_ELEMENT_BASE_X, renderedGroups = {}, // keys of rendered groups; key = group, value = { x, y, element } i, x, y, tr = null, th, td, primaryColumns = [], primaryRows = [], ratio, cellStyle, tempI, tempJ; + this.SEARCH_ENABLED = SEARCH_ENABLED; + var formatContent = function (value, element, format) { if (!isFinite(value)) { element.className += " formatLeft"; @@ -665,6 +710,28 @@ PivotView.prototype.renderRawData = function (data) { } }; + var setCaretPosition = function (elem, caretPos) { + var range; + if (elem.createTextRange) { + range = elem.createTextRange(); + range.move("character", caretPos); + range.select(); + } else { + elem.setSelectionRange(caretPos, caretPos); + } + }; + + var getMouseXY = function (e) { + var element = e.target || e.srcElement, offsetX = 0, offsetY = 0; + if (element.offsetParent) { + do { + offsetX += element.offsetLeft; + offsetY += element.offsetTop; + } while ((element = element.offsetParent)); + } + return { x: e.pageX - offsetX, y: e.pageY - offsetY }; + }; + var bindResize = function (element, column) { var el = element, @@ -684,7 +751,8 @@ PivotView.prototype.renderRawData = function (data) { } el.addEventListener("mousedown", function (e) { - if (((e.layerX || e.offsetX) < el.offsetWidth - 5) && (e.layerX || e.offsetX) > 5) { + var cursorX = getMouseXY(e).x; + if (cursorX < el.offsetWidth - 5 && cursorX > 5) { return; } e.cancelBubble = true; @@ -814,9 +882,7 @@ PivotView.prototype.renderRawData = function (data) { } }; - //console.log("Data to render: ", data); - - // fill header + // top left header setup header.textContent = info.leftHeaderColumnsNumber ? rawData[0][0].value : ""; if (rawData[0][0].style) header.setAttribute("style", rawData[0][0].style); if (this.tablesStack.length > 1 && !this.controller.CONFIG["hideButtons"]) { @@ -957,6 +1023,7 @@ PivotView.prototype.renderRawData = function (data) { pivotBottomSection.appendChild(tableBlock); container.appendChild(pivotTopSection); container.appendChild(pivotBottomSection); + if (pageSwitcher) { pageSwitcher.className = "lpt-pageSwitcher"; pageNumbers = (function getPageNumbersArray (currentPage, pages) { // minPage = 1 @@ -990,9 +1057,56 @@ PivotView.prototype.renderRawData = function (data) { pageSwitcher.appendChild(pageSwitcherContainer); container.appendChild(pageSwitcher); } + + if (SEARCH_ENABLED) { + searchIcon.className = "lpt-searchIcon"; + searchSelectOuter.className = "lpt-searchSelectOuter"; + searchBlock.className = "lpt-searchBlock"; + searchInput.className = "lpt-searchInput"; + searchSelect.className = "lpt-searchSelect"; + if (pageSwitcher) { + searchBlock.style.borderBottom = "none"; + } else { searchBlock.style.bottom = "0"; } + for (i in searchFields) { + td = document.createElement("option"); + td.setAttribute("value", searchFields[i].columnIndex.toString()); + td.textContent = searchFields[i].value; + searchSelect.appendChild(td); + } + searchInput.addEventListener("input", function () { + var colIndex = parseInt(searchSelect.options[searchSelect.selectedIndex].value), + value = searchInput.value; + _.saveScrollPosition(); + _.savedSearch.value = value; + _.savedSearch.columnIndex = colIndex; + _.savedSearch.restore = true; + _.controller.dataController.filterByValue(value, colIndex); + _.restoreScrollPosition(); + }); + searchBlock.appendChild(searchIcon); + searchSelectOuter.appendChild(searchSelect); + searchBlock.appendChild(searchSelectOuter); + searchBlock.appendChild(searchInput); + container.appendChild(searchBlock); + this.elements.searchInput = searchInput; + this.elements.searchSelect = searchSelect; + if (this.savedSearch.restore) { + this.elements.searchInput.value = this.savedSearch.value; + this.elements.searchSelect.value = this.savedSearch.columnIndex; + } + } else { + this.elements.searchInput = undefined; + this.elements.searchSelect = undefined; + } + container["_primaryColumns"] = primaryColumns; container["_primaryRows"] = primaryRows; this.recalculateSizes(container); + if (this.savedSearch.restore) { + this.elements.searchInput.focus(); + setCaretPosition(this.elements.searchInput, this.savedSearch.value.length); + } + };