diff --git a/src/opnsense/mvc/app/views/layouts/default.volt b/src/opnsense/mvc/app/views/layouts/default.volt index 30eced9dd..7bcc5eeb7 100644 --- a/src/opnsense/mvc/app/views/layouts/default.volt +++ b/src/opnsense/mvc/app/views/layouts/default.volt @@ -62,6 +62,10 @@ + + + + diff --git a/src/opnsense/www/css/jquery.bootgrid.css b/src/opnsense/www/css/jquery.bootgrid.css new file mode 100644 index 000000000..141855afb --- /dev/null +++ b/src/opnsense/www/css/jquery.bootgrid.css @@ -0,0 +1,147 @@ +/*! + * jQuery Bootgrid v1.2.0 - 05/02/2015 + * Copyright (c) 2014-2015 Rafael Staib (http://www.jquery-bootgrid.com) + * Licensed under MIT http://www.opensource.org/licenses/MIT + */ +.bootgrid-header, +.bootgrid-footer { + margin: 15px 0; +} +.bootgrid-header a, +.bootgrid-footer a { + outline: 0; +} +.bootgrid-header .search, +.bootgrid-footer .search { + display: inline-block; + margin: 0 20px 0 0; + vertical-align: middle; + width: 180px; +} +.bootgrid-header .search .glyphicon, +.bootgrid-footer .search .glyphicon { + top: 0; +} +.bootgrid-header .search .fa, +.bootgrid-footer .search .fa { + display: table-cell; +} +.bootgrid-header .search.search-field::-ms-clear, +.bootgrid-footer .search.search-field::-ms-clear, +.bootgrid-header .search .search-field::-ms-clear, +.bootgrid-footer .search .search-field::-ms-clear { + display: none; +} +.bootgrid-header .pagination, +.bootgrid-footer .pagination { + margin: 0 !important; +} +.bootgrid-header .actionBar, +.bootgrid-footer .infoBar { + text-align: right; +} +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu { + text-align: left; +} +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item { + cursor: pointer; + display: block; + margin: 0; + padding: 3px 20px; + white-space: nowrap; +} +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item:hover, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item:hover, +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item:focus, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item.dropdown-item-checkbox, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item.dropdown-item-checkbox, +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item .dropdown-item-checkbox, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item .dropdown-item-checkbox { + margin: 0 2px 4px 0; + vertical-align: middle; +} +.bootgrid-header .actionBar .btn-group > .btn-group .dropdown-menu .dropdown-item.disabled, +.bootgrid-footer .infoBar .btn-group > .btn-group .dropdown-menu .dropdown-item.disabled { + cursor: not-allowed; +} +.bootgrid-table { + table-layout: fixed; +} +.bootgrid-table a { + outline: 0; +} +.bootgrid-table th > .column-header-anchor { + color: #333; + cursor: not-allowed; + display: block; + position: relative; + text-decoration: none; +} +.bootgrid-table th > .column-header-anchor.sortable { + cursor: pointer; +} +.bootgrid-table th > .column-header-anchor > .text { + display: block; + margin: 0 16px 0 0; + overflow: hidden; + -ms-text-overflow: ellipsis; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; +} +.bootgrid-table th > .column-header-anchor > .icon { + display: block; + position: absolute; + right: 0; + top: 2px; +} +.bootgrid-table th:hover, +.bootgrid-table th:active { + background: #fafafa; +} +.bootgrid-table td { + overflow: hidden; + -ms-text-overflow: ellipsis; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; +} +.bootgrid-table td.loading, +.bootgrid-table td.no-results { + background: #fff; + text-align: center; +} +.bootgrid-table th.select-cell, +.bootgrid-table td.select-cell { + text-align: center; + width: 30px; +} +.bootgrid-table th.select-cell .select-box, +.bootgrid-table td.select-cell .select-box { + margin: 0; + outline: 0; +} +.table-responsive .bootgrid-table { + table-layout: inherit !important; +} +.table-responsive .bootgrid-table th > .column-header-anchor > .text { + overflow: inherit !important; + -ms-text-overflow: inherit !important; + -o-text-overflow: inherit !important; + text-overflow: inherit !important; + white-space: inherit !important; +} +.table-responsive .bootgrid-table td { + overflow: inherit !important; + -ms-text-overflow: inherit !important; + -o-text-overflow: inherit !important; + text-overflow: inherit !important; + white-space: inherit !important; +} diff --git a/src/opnsense/www/js/jquery.bootgrid.LICENSE b/src/opnsense/www/js/jquery.bootgrid.LICENSE new file mode 100644 index 000000000..6c7128534 --- /dev/null +++ b/src/opnsense/www/js/jquery.bootgrid.LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 Rafael J. Staib + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/src/opnsense/www/js/jquery.bootgrid.js b/src/opnsense/www/js/jquery.bootgrid.js new file mode 100644 index 000000000..469cc6a1d --- /dev/null +++ b/src/opnsense/www/js/jquery.bootgrid.js @@ -0,0 +1,2037 @@ +/*! + * jQuery Bootgrid v1.2.0 - 05/02/2015 + * Copyright (c) 2014-2015 Rafael Staib (http://www.jquery-bootgrid.com) + * Licensed under MIT http://www.opensource.org/licenses/MIT + */ +;(function ($, window, undefined) +{ + /*jshint validthis: true */ + "use strict"; + + // GRID INTERNAL FIELDS + // ==================== + + var namespace = ".rs.jquery.bootgrid"; + + // GRID INTERNAL FUNCTIONS + // ===================== + + function appendRow(row) + { + var that = this; + + function exists(item) + { + return that.identifier && item[that.identifier] === row[that.identifier]; + } + + if (!this.rows.contains(exists)) + { + this.rows.push(row); + return true; + } + + return false; + } + + function findFooterAndHeaderItems(selector) + { + var footer = (this.footer) ? this.footer.find(selector) : $(), + header = (this.header) ? this.header.find(selector) : $(); + return $.merge(footer, header); + } + + function getParams(context) + { + return (context) ? $.extend({}, this.cachedParams, { ctx: context }) : + this.cachedParams; + } + + function getRequest() + { + var request = { + current: this.current, + rowCount: this.rowCount, + sort: this.sortDictionary, + searchPhrase: this.searchPhrase + }, + post = this.options.post; + + post = ($.isFunction(post)) ? post() : post; + return this.options.requestHandler($.extend(true, request, post)); + } + + function getCssSelector(css) + { + return "." + $.trim(css).replace(/\s+/gm, "."); + } + + function getUrl() + { + var url = this.options.url; + return ($.isFunction(url)) ? url() : url; + } + + function init() + { + this.element.trigger("initialize" + namespace); + + loadColumns.call(this); // Loads columns from HTML thead tag + this.selection = this.options.selection && this.identifier != null; + loadRows.call(this); // Loads rows from HTML tbody tag if ajax is false + prepareTable.call(this); + renderTableHeader.call(this); + renderSearchField.call(this); + renderActions.call(this); + loadData.call(this); + + this.element.trigger("initialized" + namespace); + } + + function highlightAppendedRows(rows) + { + if (this.options.highlightRows) + { + // todo: implement + } + } + + function isVisible(column) + { + return column.visible; + } + + function loadColumns() + { + var that = this, + firstHeadRow = this.element.find("thead > tr").first(), + sorted = false; + + /*jshint -W018*/ + firstHeadRow.children().each(function () + { + var $this = $(this), + data = $this.data(), + column = { + id: data.columnId, + identifier: that.identifier == null && data.identifier || false, + converter: that.options.converters[data.converter || data.type] || that.options.converters["string"], + text: $this.text(), + align: data.align || "left", + headerAlign: data.headerAlign || "left", + cssClass: data.cssClass || "", + headerCssClass: data.headerCssClass || "", + formatter: that.options.formatters[data.formatter] || null, + order: (!sorted && (data.order === "asc" || data.order === "desc")) ? data.order : null, + searchable: !(data.searchable === false), // default: true + sortable: !(data.sortable === false), // default: true + visible: !(data.visible === false), // default: true + width: ($.isNumeric(data.width)) ? data.width + "px" : + (typeof(data.width) === "string") ? data.width : null + }; + that.columns.push(column); + if (column.order != null) + { + that.sortDictionary[column.id] = column.order; + } + + // Prevents multiple identifiers + if (column.identifier) + { + that.identifier = column.id; + that.converter = column.converter; + } + + // ensures that only the first order will be applied in case of multi sorting is disabled + if (!that.options.multiSort && column.order !== null) + { + sorted = true; + } + }); + /*jshint +W018*/ + } + + /* + response = { + current: 1, + rowCount: 10, + rows: [{}, {}], + sort: [{ "columnId": "asc" }], + total: 101 + } + */ + + function loadData() + { + var that = this; + + this.element._bgBusyAria(true).trigger("load" + namespace); + showLoading.call(this); + + function containsPhrase(row) + { + var column, + searchPattern = new RegExp(that.searchPhrase, (that.options.caseSensitive) ? "g" : "gi"); + + for (var i = 0; i < that.columns.length; i++) + { + column = that.columns[i]; + if (column.searchable && column.visible && + column.converter.to(row[column.id]).search(searchPattern) > -1) + { + return true; + } + } + + return false; + } + + function update(rows, total) + { + that.currentRows = rows; + setTotals.call(that, total); + + if (!that.options.keepSelection) + { + that.selectedRows = []; + } + + renderRows.call(that, rows); + renderInfos.call(that); + renderPagination.call(that); + + that.element._bgBusyAria(false).trigger("loaded" + namespace); + } + + if (this.options.ajax) + { + var request = getRequest.call(this), + url = getUrl.call(this); + + if (url == null || typeof url !== "string" || url.length === 0) + { + throw new Error("Url setting must be a none empty string or a function that returns one."); + } + + // aborts the previous ajax request if not already finished or failed + if (this.xqr) + { + this.xqr.abort(); + } + + var settings = { + url: url, + data: request, + success: function(response) + { + that.xqr = null; + + if (typeof (response) === "string") + { + response = $.parseJSON(response); + } + + response = that.options.responseHandler(response); + + that.current = response.current; + update(response.rows, response.total); + }, + error: function (jqXHR, textStatus, errorThrown) + { + that.xqr = null; + + if (textStatus !== "abort") + { + renderNoResultsRow.call(that); // overrides loading mask + that.element._bgBusyAria(false).trigger("loaded" + namespace); + } + } + }; + settings = $.extend(this.options.ajaxSettings, settings); + + this.xqr = $.ajax(settings); + } + else + { + var rows = (this.searchPhrase.length > 0) ? this.rows.where(containsPhrase) : this.rows, + total = rows.length; + if (this.rowCount !== -1) + { + rows = rows.page(this.current, this.rowCount); + } + + // todo: improve the following comment + // setTimeout decouples the initialization so that adding event handlers happens before + window.setTimeout(function () { update(rows, total); }, 10); + } + } + + function loadRows() + { + if (!this.options.ajax) + { + var that = this, + rows = this.element.find("tbody > tr"); + + rows.each(function () + { + var $this = $(this), + cells = $this.children("td"), + row = {}; + + $.each(that.columns, function (i, column) + { + row[column.id] = column.converter.from(cells.eq(i).text()); + }); + + appendRow.call(that, row); + }); + + setTotals.call(this, this.rows.length); + sortRows.call(this); + } + } + + function setTotals(total) + { + this.total = total; + this.totalPages = (this.rowCount === -1) ? 1 : + Math.ceil(this.total / this.rowCount); + } + + function prepareTable() + { + var tpl = this.options.templates, + wrapper = (this.element.parent().hasClass(this.options.css.responsiveTable)) ? + this.element.parent() : this.element; + + this.element.addClass(this.options.css.table); + + // checks whether there is an tbody element; otherwise creates one + if (this.element.children("tbody").length === 0) + { + this.element.append(tpl.body); + } + + if (this.options.navigation & 1) + { + this.header = $(tpl.header.resolve(getParams.call(this, { id: this.element._bgId() + "-header" }))); + wrapper.before(this.header); + } + + if (this.options.navigation & 2) + { + this.footer = $(tpl.footer.resolve(getParams.call(this, { id: this.element._bgId() + "-footer" }))); + wrapper.after(this.footer); + } + } + + function renderActions() + { + if (this.options.navigation !== 0) + { + var css = this.options.css, + selector = getCssSelector(css.actions), + actionItems = findFooterAndHeaderItems.call(this, selector); + + if (actionItems.length > 0) + { + var that = this, + tpl = this.options.templates, + actions = $(tpl.actions.resolve(getParams.call(this))); + + // Refresh Button + if (this.options.ajax) + { + var refreshIcon = tpl.icon.resolve(getParams.call(this, { iconCss: css.iconRefresh })), + refresh = $(tpl.actionButton.resolve(getParams.call(this, + { content: refreshIcon, text: this.options.labels.refresh }))) + .on("click" + namespace, function (e) + { + // todo: prevent multiple fast clicks (fast click detection) + e.stopPropagation(); + that.current = 1; + loadData.call(that); + }); + actions.append(refresh); + } + + // Row count selection + renderRowCountSelection.call(this, actions); + + // Column selection + renderColumnSelection.call(this, actions); + + replacePlaceHolder.call(this, actionItems, actions); + } + } + } + + function renderColumnSelection(actions) + { + if (this.options.columnSelection && this.columns.length > 1) + { + var that = this, + css = this.options.css, + tpl = this.options.templates, + icon = tpl.icon.resolve(getParams.call(this, { iconCss: css.iconColumns })), + dropDown = $(tpl.actionDropDown.resolve(getParams.call(this, { content: icon }))), + selector = getCssSelector(css.dropDownItem), + checkboxSelector = getCssSelector(css.dropDownItemCheckbox), + itemsSelector = getCssSelector(css.dropDownMenuItems); + + $.each(this.columns, function (i, column) + { + var item = $(tpl.actionDropDownCheckboxItem.resolve(getParams.call(that, + { name: column.id, label: column.text, checked: column.visible }))) + .on("click" + namespace, selector, function (e) + { + e.stopPropagation(); + + var $this = $(this), + checkbox = $this.find(checkboxSelector); + if (!checkbox.prop("disabled")) + { + column.visible = checkbox.prop("checked"); + var enable = that.columns.where(isVisible).length > 1; + $this.parents(itemsSelector).find(selector + ":has(" + checkboxSelector + ":checked)") + ._bgEnableAria(enable).find(checkboxSelector)._bgEnableField(enable); + + that.element.find("tbody").empty(); // Fixes an column visualization bug + renderTableHeader.call(that); + loadData.call(that); + } + }); + dropDown.find(getCssSelector(css.dropDownMenuItems)).append(item); + }); + actions.append(dropDown); + } + } + + function renderInfos() + { + if (this.options.navigation !== 0) + { + var selector = getCssSelector(this.options.css.infos), + infoItems = findFooterAndHeaderItems.call(this, selector); + + if (infoItems.length > 0) + { + var end = (this.current * this.rowCount), + infos = $(this.options.templates.infos.resolve(getParams.call(this, { + end: (this.total === 0 || end === -1 || end > this.total) ? this.total : end, + start: (this.total === 0) ? 0 : (end - this.rowCount + 1), + total: this.total + }))); + + replacePlaceHolder.call(this, infoItems, infos); + } + } + } + + function renderNoResultsRow() + { + var tbody = this.element.children("tbody").first(), + tpl = this.options.templates, + count = this.columns.where(isVisible).length; + + if (this.selection) + { + count = count + 1; + } + tbody.html(tpl.noResults.resolve(getParams.call(this, { columns: count }))); + } + + function renderPagination() + { + if (this.options.navigation !== 0) + { + var selector = getCssSelector(this.options.css.pagination), + paginationItems = findFooterAndHeaderItems.call(this, selector)._bgShowAria(this.rowCount !== -1); + + if (this.rowCount !== -1 && paginationItems.length > 0) + { + var tpl = this.options.templates, + current = this.current, + totalPages = this.totalPages, + pagination = $(tpl.pagination.resolve(getParams.call(this))), + offsetRight = totalPages - current, + offsetLeft = (this.options.padding - current) * -1, + startWith = ((offsetRight >= this.options.padding) ? + Math.max(offsetLeft, 1) : + Math.max((offsetLeft - this.options.padding + offsetRight), 1)), + maxCount = this.options.padding * 2 + 1, + count = (totalPages >= maxCount) ? maxCount : totalPages; + + renderPaginationItem.call(this, pagination, "first", "«", "first") + ._bgEnableAria(current > 1); + renderPaginationItem.call(this, pagination, "prev", "<", "prev") + ._bgEnableAria(current > 1); + + for (var i = 0; i < count; i++) + { + var pos = i + startWith; + renderPaginationItem.call(this, pagination, pos, pos, "page-" + pos) + ._bgEnableAria()._bgSelectAria(pos === current); + } + + if (count === 0) + { + renderPaginationItem.call(this, pagination, 1, 1, "page-" + 1) + ._bgEnableAria(false)._bgSelectAria(); + } + + renderPaginationItem.call(this, pagination, "next", ">", "next") + ._bgEnableAria(totalPages > current); + renderPaginationItem.call(this, pagination, "last", "»", "last") + ._bgEnableAria(totalPages > current); + + replacePlaceHolder.call(this, paginationItems, pagination); + } + } + } + + function renderPaginationItem(list, uri, text, markerCss) + { + var that = this, + tpl = this.options.templates, + css = this.options.css, + values = getParams.call(this, { css: markerCss, text: text, uri: "#" + uri }), + item = $(tpl.paginationItem.resolve(values)) + .on("click" + namespace, getCssSelector(css.paginationButton), function (e) + { + e.stopPropagation(); + + var $this = $(this), + parent = $this.parent(); + if (!parent.hasClass("active") && !parent.hasClass("disabled")) + { + var commandList = { + first: 1, + prev: that.current - 1, + next: that.current + 1, + last: that.totalPages + }; + var command = $this.attr("href").substr(1); + that.current = commandList[command] || +command; // + converts string to int + loadData.call(that); + } + $this.trigger("blur"); + }); + + list.append(item); + return item; + } + + function renderRowCountSelection(actions) + { + var that = this, + rowCountList = this.options.rowCount; + + function getText(value) + { + return (value === -1) ? that.options.labels.all : value; + } + + if ($.isArray(rowCountList)) + { + var css = this.options.css, + tpl = this.options.templates, + dropDown = $(tpl.actionDropDown.resolve(getParams.call(this, { content: getText(this.rowCount) }))), + menuSelector = getCssSelector(css.dropDownMenu), + menuTextSelector = getCssSelector(css.dropDownMenuText), + menuItemsSelector = getCssSelector(css.dropDownMenuItems), + menuItemSelector = getCssSelector(css.dropDownItemButton); + + $.each(rowCountList, function (index, value) + { + var item = $(tpl.actionDropDownItem.resolve(getParams.call(that, + { text: getText(value), uri: "#" + value }))) + ._bgSelectAria(value === that.rowCount) + .on("click" + namespace, menuItemSelector, function (e) + { + e.preventDefault(); + + var $this = $(this), + newRowCount = +$this.attr("href").substr(1); + if (newRowCount !== that.rowCount) + { + // todo: sophisticated solution needed for calculating which page is selected + that.current = 1; // that.rowCount === -1 ---> All + that.rowCount = newRowCount; + $this.parents(menuItemsSelector).children().each(function () + { + var $item = $(this), + currentRowCount = +$item.find(menuItemSelector).attr("href").substr(1); + $item._bgSelectAria(currentRowCount === newRowCount); + }); + $this.parents(menuSelector).find(menuTextSelector).text(getText(newRowCount)); + loadData.call(that); + } + }); + dropDown.find(menuItemsSelector).append(item); + }); + actions.append(dropDown); + } + } + + function renderRows(rows) + { + if (rows.length > 0) + { + var that = this, + css = this.options.css, + tpl = this.options.templates, + tbody = this.element.children("tbody").first(), + allRowsSelected = true, + html = ""; + + $.each(rows, function (index, row) + { + var cells = "", + rowAttr = " data-row-id=\"" + ((that.identifier == null) ? index : row[that.identifier]) + "\"", + rowCss = ""; + + if (that.selection) + { + var selected = ($.inArray(row[that.identifier], that.selectedRows) !== -1), + selectBox = tpl.select.resolve(getParams.call(that, + { type: "checkbox", value: row[that.identifier], checked: selected })); + cells += tpl.cell.resolve(getParams.call(that, { content: selectBox, css: css.selectCell })); + allRowsSelected = (allRowsSelected && selected); + if (selected) + { + rowCss += css.selected; + rowAttr += " aria-selected=\"true\""; + } + } + + var status = row.status != null && that.options.statusMapping[row.status]; + if (status) + { + rowCss += status; + } + + $.each(that.columns, function (j, column) + { + if (column.visible) + { + var value = ($.isFunction(column.formatter)) ? + column.formatter.call(that, column, row) : + column.converter.to(row[column.id]), + cssClass = (column.cssClass.length > 0) ? " " + column.cssClass : ""; + cells += tpl.cell.resolve(getParams.call(that, { + content: (value == null || value === "") ? " " : value, + css: ((column.align === "right") ? css.right : (column.align === "center") ? + css.center : css.left) + cssClass, + style: (column.width == null) ? "" : "width:" + column.width + ";" })); + } + }); + + if (rowCss.length > 0) + { + rowAttr += " class=\"" + rowCss + "\""; + } + html += tpl.row.resolve(getParams.call(that, { attr: rowAttr, cells: cells })); + }); + + // sets or clears multi selectbox state + that.element.find("thead " + getCssSelector(that.options.css.selectBox)) + .prop("checked", allRowsSelected); + + tbody.html(html); + + registerRowEvents.call(this, tbody); + } + else + { + renderNoResultsRow.call(this); + } + } + + function registerRowEvents(tbody) + { + var that = this, + selectBoxSelector = getCssSelector(this.options.css.selectBox); + + if (this.selection) + { + tbody.off("click" + namespace, selectBoxSelector) + .on("click" + namespace, selectBoxSelector, function(e) + { + e.stopPropagation(); + + var $this = $(this), + id = that.converter.from($this.val()); + + if ($this.prop("checked")) + { + that.select([id]); + } + else + { + that.deselect([id]); + } + }); + } + + tbody.off("click" + namespace, "> tr") + .on("click" + namespace, "> tr", function(e) + { + e.stopPropagation(); + + var $this = $(this), + id = (that.identifier == null) ? $this.data("row-id") : + that.converter.from($this.data("row-id") + ""), + row = (that.identifier == null) ? that.currentRows[id] : + that.currentRows.first(function (item) { return item[that.identifier] === id; }); + + if (that.selection && that.options.rowSelect) + { + if ($this.hasClass(that.options.css.selected)) + { + that.deselect([id]); + } + else + { + that.select([id]); + } + } + + that.element.trigger("click" + namespace, [that.columns, row]); + }); + } + + function renderSearchField() + { + if (this.options.navigation !== 0) + { + var css = this.options.css, + selector = getCssSelector(css.search), + searchItems = findFooterAndHeaderItems.call(this, selector); + + if (searchItems.length > 0) + { + var that = this, + tpl = this.options.templates, + timer = null, // fast keyup detection + currentValue = "", + searchFieldSelector = getCssSelector(css.searchField), + search = $(tpl.search.resolve(getParams.call(this))), + searchField = (search.is(searchFieldSelector)) ? search : + search.find(searchFieldSelector); + + searchField.on("keyup" + namespace, function (e) + { + e.stopPropagation(); + var newValue = $(this).val(); + if (currentValue !== newValue || (e.which === 13 && newValue !== "")) + { + currentValue = newValue; + if (e.which === 13 || newValue.length === 0 || newValue.length >= that.options.searchSettings.characters) + { + window.clearTimeout(timer); + timer = window.setTimeout(function () + { + executeSearch.call(that, newValue); + }, that.options.searchSettings.delay); + } + } + }); + + replacePlaceHolder.call(this, searchItems, search); + } + } + } + + function executeSearch(phrase) + { + if (this.searchPhrase !== phrase) + { + this.current = 1; + this.searchPhrase = phrase; + loadData.call(this); + } + } + + function renderTableHeader() + { + var that = this, + headerRow = this.element.find("thead > tr"), + css = this.options.css, + tpl = this.options.templates, + html = "", + sorting = this.options.sorting; + + if (this.selection) + { + var selectBox = (this.options.multiSelect) ? + tpl.select.resolve(getParams.call(that, { type: "checkbox", value: "all" })) : ""; + html += tpl.rawHeaderCell.resolve(getParams.call(that, { content: selectBox, + css: css.selectCell })); + } + + $.each(this.columns, function (index, column) + { + if (column.visible) + { + var sortOrder = that.sortDictionary[column.id], + iconCss = ((sorting && sortOrder && sortOrder === "asc") ? css.iconUp : + (sorting && sortOrder && sortOrder === "desc") ? css.iconDown : ""), + icon = tpl.icon.resolve(getParams.call(that, { iconCss: iconCss })), + align = column.headerAlign, + cssClass = (column.headerCssClass.length > 0) ? " " + column.headerCssClass : ""; + html += tpl.headerCell.resolve(getParams.call(that, { + column: column, icon: icon, sortable: sorting && column.sortable && css.sortable || "", + css: ((align === "right") ? css.right : (align === "center") ? + css.center : css.left) + cssClass, + style: (column.width == null) ? "" : "width:" + column.width + ";" })); + } + }); + + headerRow.html(html); + + if (sorting) + { + var sortingSelector = getCssSelector(css.sortable); + headerRow.off("click" + namespace, sortingSelector) + .on("click" + namespace, sortingSelector, function (e) + { + e.preventDefault(); + + setTableHeaderSortDirection.call(that, $(this)); + sortRows.call(that); + loadData.call(that); + }); + } + + // todo: create a own function for that piece of code + if (this.selection && this.options.multiSelect) + { + var selectBoxSelector = getCssSelector(css.selectBox); + headerRow.off("click" + namespace, selectBoxSelector) + .on("click" + namespace, selectBoxSelector, function(e) + { + e.stopPropagation(); + + if ($(this).prop("checked")) + { + that.select(); + } + else + { + that.deselect(); + } + }); + } + } + + function setTableHeaderSortDirection(element) + { + var css = this.options.css, + iconSelector = getCssSelector(css.icon), + columnId = element.data("column-id") || element.parents("th").first().data("column-id"), + sortOrder = this.sortDictionary[columnId], + icon = element.find(iconSelector); + + if (!this.options.multiSort) + { + element.parents("tr").first().find(iconSelector).removeClass(css.iconDown + " " + css.iconUp); + this.sortDictionary = {}; + } + + if (sortOrder && sortOrder === "asc") + { + this.sortDictionary[columnId] = "desc"; + icon.removeClass(css.iconUp).addClass(css.iconDown); + } + else if (sortOrder && sortOrder === "desc") + { + if (this.options.multiSort) + { + var newSort = {}; + for (var key in this.sortDictionary) + { + if (key !== columnId) + { + newSort[key] = this.sortDictionary[key]; + } + } + this.sortDictionary = newSort; + icon.removeClass(css.iconDown); + } + else + { + this.sortDictionary[columnId] = "asc"; + icon.removeClass(css.iconDown).addClass(css.iconUp); + } + } + else + { + this.sortDictionary[columnId] = "asc"; + icon.addClass(css.iconUp); + } + } + + function replacePlaceHolder(placeholder, element) + { + placeholder.each(function (index, item) + { + // todo: check how append is implemented. Perhaps cloning here is superfluous. + $(item).before(element.clone(true)).remove(); + }); + } + + function showLoading() + { + var that = this; + + window.setTimeout(function() + { + if (that.element._bgAria("busy") === "true") + { + var tpl = that.options.templates, + thead = that.element.children("thead").first(), + tbody = that.element.children("tbody").first(), + firstCell = tbody.find("tr > td").first(), + padding = (that.element.height() - thead.height()) - (firstCell.height() + 20), + count = that.columns.where(isVisible).length; + + if (that.selection) + { + count = count + 1; + } + tbody.html(tpl.loading.resolve(getParams.call(that, { columns: count }))); + if (that.rowCount !== -1 && padding > 0) + { + tbody.find("tr > td").css("padding", "20px 0 " + padding + "px"); + } + } + }, 250); + } + + function sortRows() + { + var sortArray = []; + + function sort(x, y, current) + { + current = current || 0; + var next = current + 1, + item = sortArray[current]; + + function sortOrder(value) + { + return (item.order === "asc") ? value : value * -1; + } + + return (x[item.id] > y[item.id]) ? sortOrder(1) : + (x[item.id] < y[item.id]) ? sortOrder(-1) : + (sortArray.length > next) ? sort(x, y, next) : 0; + } + + if (!this.options.ajax) + { + var that = this; + + for (var key in this.sortDictionary) + { + if (this.options.multiSort || sortArray.length === 0) + { + sortArray.push({ + id: key, + order: this.sortDictionary[key] + }); + } + } + + if (sortArray.length > 0) + { + this.rows.sort(sort); + } + } + } + + // GRID PUBLIC CLASS DEFINITION + // ==================== + + /** + * Represents the jQuery Bootgrid plugin. + * + * @class Grid + * @constructor + * @param element {Object} The corresponding DOM element. + * @param options {Object} The options to override default settings. + * @chainable + **/ + var Grid = function(element, options) + { + this.element = $(element); + this.origin = this.element.clone(); + this.options = $.extend(true, {}, Grid.defaults, this.element.data(), options); + // overrides rowCount explicitly because deep copy ($.extend) leads to strange behaviour + var rowCount = this.options.rowCount = this.element.data().rowCount || options.rowCount || this.options.rowCount; + this.columns = []; + this.current = 1; + this.currentRows = []; + this.identifier = null; // The first column ID that is marked as identifier + this.selection = false; + this.converter = null; // The converter for the column that is marked as identifier + this.rowCount = ($.isArray(rowCount)) ? rowCount[0] : rowCount; + this.rows = []; + this.searchPhrase = ""; + this.selectedRows = []; + this.sortDictionary = {}; + this.total = 0; + this.totalPages = 0; + this.cachedParams = { + lbl: this.options.labels, + css: this.options.css, + ctx: {} + }; + this.header = null; + this.footer = null; + this.xqr = null; + + // todo: implement cache + }; + + /** + * An object that represents the default settings. + * + * @static + * @class defaults + * @for Grid + * @example + * // Global approach + * $.bootgrid.defaults.selection = true; + * @example + * // Initialization approach + * $("#bootgrid").bootgrid({ selection = true }); + **/ + Grid.defaults = { + navigation: 3, // it's a flag: 0 = none, 1 = top, 2 = bottom, 3 = both (top and bottom) + padding: 2, // page padding (pagination) + columnSelection: true, + rowCount: [10, 25, 50, -1], // rows per page int or array of int (-1 represents "All") + + /** + * Enables row selection (to enable multi selection see also `multiSelect`). Default value is `false`. + * + * @property selection + * @type Boolean + * @default false + * @for defaults + * @since 1.0.0 + **/ + selection: false, + + /** + * Enables multi selection (`selection` must be set to `true` as well). Default value is `false`. + * + * @property multiSelect + * @type Boolean + * @default false + * @for defaults + * @since 1.0.0 + **/ + multiSelect: false, + + /** + * Enables entire row click selection (`selection` must be set to `true` as well). Default value is `false`. + * + * @property rowSelect + * @type Boolean + * @default false + * @for defaults + * @since 1.1.0 + **/ + rowSelect: false, + + /** + * Defines whether the row selection is saved internally on filtering, paging and sorting + * (even if the selected rows are not visible). + * + * @property keepSelection + * @type Boolean + * @default false + * @for defaults + * @since 1.1.0 + **/ + keepSelection: false, + + highlightRows: false, // highlights new rows (find the page of the first new row) + sorting: true, + multiSort: false, + + /** + * General search settings to configure the search field behaviour. + * + * @property searchSettings + * @type Object + * @for defaults + * @since 1.2.0 + **/ + searchSettings: { + /** + * The time in milliseconds to wait before search gets executed. + * + * @property delay + * @type Number + * @default 250 + * @for searchSettings + **/ + delay: 250, + + /** + * The characters to type before the search gets executed. + * + * @property characters + * @type Number + * @default 1 + * @for searchSettings + **/ + characters: 1 + }, + + /** + * Defines whether the data shall be loaded via an asynchronous HTTP (Ajax) request. + * + * @property ajax + * @type Boolean + * @default false + * @for defaults + **/ + ajax: false, + + /** + * Ajax request settings that shall be used for server-side communication. + * All setting except data, error, success and url can be overridden. + * For the full list of settings go to http://api.jquery.com/jQuery.ajax/. + * + * @property ajaxSettings + * @type Object + * @for defaults + * @since 1.2.0 + **/ + ajaxSettings: { + /** + * Specifies the HTTP method which shall be used when sending data to the server. + * Go to http://api.jquery.com/jQuery.ajax/ for more details. + * This setting is overriden for backward compatibility. + * + * @property method + * @type String + * @default "POST" + * @for ajaxSettings + **/ + method: "POST" + }, + + /** + * Enriches the request object with additional properties. Either a `PlainObject` or a `Function` + * that returns a `PlainObject` can be passed. Default value is `{}`. + * + * @property post + * @type Object|Function + * @default function (request) { return request; } + * @for defaults + * @deprecated Use instead `requestHandler` + **/ + post: {}, // or use function () { return {}; } (reserved properties are "current", "rowCount", "sort" and "searchPhrase") + + /** + * Sets the data URL to a data service (e.g. a REST service). Either a `String` or a `Function` + * that returns a `String` can be passed. Default value is `""`. + * + * @property url + * @type String|Function + * @default "" + * @for defaults + **/ + url: "", // or use function () { return ""; } + + /** + * Defines whether the search is case sensitive or insensitive. + * + * @property caseSensitive + * @type Boolean + * @default true + * @for defaults + * @since 1.1.0 + **/ + caseSensitive: true, + + // note: The following properties should not be used via data-api attributes + + /** + * Transforms the JSON request object in what ever is needed on the server-side implementation. + * + * @property requestHandler + * @type Function + * @default function (request) { return request; } + * @for defaults + * @since 1.1.0 + **/ + requestHandler: function (request) { return request; }, + + /** + * Transforms the response object into the expected JSON response object. + * + * @property responseHandler + * @type Function + * @default function (response) { return response; } + * @for defaults + * @since 1.1.0 + **/ + responseHandler: function (response) { return response; }, + + /** + * A list of converters. + * + * @property converters + * @type Object + * @for defaults + * @since 1.0.0 + **/ + converters: { + numeric: { + from: function (value) { return +value; }, // converts from string to numeric + to: function (value) { return value + ""; } // converts from numeric to string + }, + string: { + // default converter + from: function (value) { return value; }, + to: function (value) { return value; } + } + }, + + /** + * Contains all css classes. + * + * @property css + * @type Object + * @for defaults + **/ + css: { + actions: "actions btn-group", // must be a unique class name or constellation of class names within the header and footer + center: "text-center", + columnHeaderAnchor: "column-header-anchor", // must be a unique class name or constellation of class names within the column header cell + columnHeaderText: "text", + dropDownItem: "dropdown-item", // must be a unique class name or constellation of class names within the actionDropDown, + dropDownItemButton: "dropdown-item-button", // must be a unique class name or constellation of class names within the actionDropDown + dropDownItemCheckbox: "dropdown-item-checkbox", // must be a unique class name or constellation of class names within the actionDropDown + dropDownMenu: "dropdown btn-group", // must be a unique class name or constellation of class names within the actionDropDown + dropDownMenuItems: "dropdown-menu pull-right", // must be a unique class name or constellation of class names within the actionDropDown + dropDownMenuText: "dropdown-text", // must be a unique class name or constellation of class names within the actionDropDown + footer: "bootgrid-footer container-fluid", + header: "bootgrid-header container-fluid", + icon: "icon glyphicon", + iconColumns: "glyphicon-th-list", + iconDown: "glyphicon-chevron-down", + iconRefresh: "glyphicon-refresh", + iconSearch: "glyphicon-search", + iconUp: "glyphicon-chevron-up", + infos: "infos", // must be a unique class name or constellation of class names within the header and footer, + left: "text-left", + pagination: "pagination", // must be a unique class name or constellation of class names within the header and footer + paginationButton: "button", // must be a unique class name or constellation of class names within the pagination + + /** + * CSS class to select the parent div which activates responsive mode. + * + * @property responsiveTable + * @type String + * @default "table-responsive" + * @for css + * @since 1.1.0 + **/ + responsiveTable: "table-responsive", + + right: "text-right", + search: "search form-group", // must be a unique class name or constellation of class names within the header and footer + searchField: "search-field form-control", + selectBox: "select-box", // must be a unique class name or constellation of class names within the entire table + selectCell: "select-cell", // must be a unique class name or constellation of class names within the entire table + + /** + * CSS class to highlight selected rows. + * + * @property selected + * @type String + * @default "active" + * @for css + * @since 1.1.0 + **/ + selected: "active", + + sortable: "sortable", + table: "bootgrid-table table" + }, + + /** + * A dictionary of formatters. + * + * @property formatters + * @type Object + * @for defaults + * @since 1.0.0 + **/ + formatters: {}, + + /** + * Contains all labels. + * + * @property labels + * @type Object + * @for defaults + **/ + labels: { + all: "All", + infos: "Showing {{ctx.start}} to {{ctx.end}} of {{ctx.total}} entries", + loading: "Loading...", + noResults: "No results found!", + refresh: "Refresh", + search: "Search" + }, + + /** + * Specifies the mapping between status and contextual classes to color rows. + * + * @property statusMapping + * @type Object + * @for defaults + * @since 1.2.0 + **/ + statusMapping: { + /** + * Specifies a successful or positive action. + * + * @property 0 + * @type String + * @for statusMapping + **/ + 0: "success", + + /** + * Specifies a neutral informative change or action. + * + * @property 1 + * @type String + * @for statusMapping + **/ + 1: "info", + + /** + * Specifies a warning that might need attention. + * + * @property 2 + * @type String + * @for statusMapping + **/ + 2: "warning", + + /** + * Specifies a dangerous or potentially negative action. + * + * @property 3 + * @type String + * @for statusMapping + **/ + 3: "danger" + }, + + /** + * Contains all templates. + * + * @property templates + * @type Object + * @for defaults + **/ + templates: { + actionButton: "", + actionDropDown: "