diff --git a/api/js/etemplate/et2_dataview.js b/api/js/etemplate/et2_dataview.js index 07ceeea7bc..d7b71b3aef 100644 --- a/api/js/etemplate/et2_dataview.js +++ b/api/js/etemplate/et2_dataview.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - dataview code * @@ -9,17 +10,16 @@ * @copyright Stylite 2011-2012 * @version $Id$ */ - +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_common; + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_common; - et2_dataview_model_columns; - et2_dataview_view_rowProvider; - et2_dataview_view_grid; - et2_dataview_view_resizeable; + et2_dataview_model_columns; + et2_dataview_view_rowProvider; + et2_dataview_view_grid; + et2_dataview_view_resizeable; */ - /** * The et2_dataview class is the main class for displaying a dataview. The * dataview class manages the creation of the outer html nodes (like the table, @@ -29,597 +29,450 @@ * * @augments Class */ -var et2_dataview = (function(){ "use strict"; return Class.extend({ - - /** - * Constant which regulates the column padding. - */ - columnPadding: 2, - - /** - * Some browser dependant variables which will be calculated on creation of - * the first gridContainer object. - */ - scrollbarWidth: false, - headerBorderWidth: false, - columnBorderWidth: false, - - /** - * Hooks to allow parent to keep up to date if things change - */ - onUpdateColumns: false, - selectColumnsClick: false, - - - /** - * Constructor for the grid container - * - * @param {DOMElement} _parentNode is the DOM-Node into which the grid view will be inserted - * @param {egw} _egw - * @memberOf et2_dataview - */ - init: function(_parentNode, _egw) { - - // Copy the arguments - this.parentNode = jQuery(_parentNode); - this.egw = _egw; - - // Initialize some variables - this.columnNodes = []; // Array with the header containers - this.columns = []; - this.columnMgr = null; - this.rowProvider = null; - - this.grid = null; - - this.width = 0; - this.height = 0; - - this.uniqueId = "gridCont_" + this.egw.uid(); - - // Build the base nodes - this._createElements(); - - // Read the browser dependant variables - this._getDepVars(); - }, - - /** - * Destroys the object, removes all dom nodes and clears all references. - */ - destroy: function() { - // Clear the columns - this._clearHeader(); - - // Free the grid - if (this.grid) - { - this.grid.free(); - } - - // Free the row provider - if (this.rowProvider) - { - this.rowProvider.free(); - } - - // Detatch the outer element - this.table.remove(); - }, - - /** - * Clears all data rows and reloads them - */ - clear: function() { - if (this.grid) - { - this.grid.clear(); - } - }, - - /** - * Returns the column container node for the given column index - * - * @param _columnIdx the integer column index - */ - getHeaderContainerNode: function(_columnIdx) { - if (typeof this.columnNodes[_columnIdx] != "undefined") - { - return this.columnNodes[_columnIdx].container[0]; - } - - return null; - }, - - /** - * Sets the column descriptors and creates the column header according to it. - * The inner grid will be emptied if it has already been built. - */ - setColumns: function(_columnData) { - // Free all column objects which have been created till this moment - this._clearHeader(); - - // Copy the given column data - this.columnMgr = new et2_dataview_columns(_columnData); - - // Create the stylesheets - this.updateColumns(); - - // Build the header row - this._buildHeader(); - - // Build the grid - this._buildGrid(); - }, - - /** - * Resizes the grid - */ - resize: function(_w, _h) { - // Not fully initialized yet... - if (!this.columnMgr) return; - - if (this.width != _w) - { - this.width = _w; - - // Take grid border width into account - _w -= (this.table.outerWidth(true) - this.table.innerWidth()); - - // Take grid header border's width into account. eg. category colors may add extra pixel into width - _w = _w - (this.thead.find('tr').outerWidth() - this.thead.find('tr').innerWidth()); - - // Rebuild the column stylesheets - this.columnMgr.setTotalWidth(_w - this.scrollbarWidth); - this._updateColumns(); - } - - if (this.height != _h) - { - this.height = _h; - - // Set the height of the grid. - if (this.grid) - { - this.grid.setScrollHeight(this.height - - this.headTr.outerHeight(true)); - } - } - }, - - /** - * Returns the column manager object. You can use it to set the visibility - * of columns etc. Call "updateHeader" if you did any changes. - */ - getColumnMgr: function() { - return this.columnMgr; - }, - - /** - * Recalculates the stylesheets which determine the column visibility and - * width. - * - * @param setDefault boolean Allow admins to save current settings as default for all users - */ - updateColumns: function(setDefault) { - if (this.columnMgr) - { - this._updateColumns(); - } - - // Ability to notify parent / someone else - if (this.onUpdateColumns) - { - this.onUpdateColumns(setDefault); - } - }, - - - /* --- PRIVATE FUNCTIONS --- */ - - /* --- Code for building the grid container DOM-Tree elements ---- */ - - /** - * Builds the base DOM-Tree elements - */ - _createElements: function() { - /* - Structure: - - - [HEAD] - - - [GRID CONTAINER] - -
- */ - - this.containerTr = jQuery(document.createElement("tr")); - this.headTr = jQuery(document.createElement("tr")); - - this.thead = jQuery(document.createElement("thead")) - .append(this.headTr); - this.tbody = jQuery(document.createElement("tbody")) - .append(this.containerTr); - - this.table = jQuery(document.createElement("table")) - .addClass("egwGridView_outer") - .append(this.thead, this.tbody) - .appendTo(this.parentNode); - }, - - - /* --- Code for building the header row --- */ - - /** - * Clears the header row - */ - _clearHeader: function() { - if (this.columnMgr) - { - this.columnMgr.free(); - this.columnMgr = null; - } - - // Remove dynamic CSS, - for (var i = 0; i < this.columns.length; i++) - { - if(this.columns[i].tdClass) - { - this.egw.css('.'+this.columns[i].tdClass); - } - if(this.columns[i].divClass) - { - this.egw.css('.'+this.columns[i].divClass); - this.egw.css(".egwGridView_outer ." + this.columns[i].divClass); - this.egw.css(".egwGridView_grid ." + this.columns[i].divClass); - } - } - this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow"); - - // Reset the headerColumns array and empty the table row - this.columnNodes = []; - this.columns = []; - this.headTr.empty(); - }, - - /** - * Sets the column data which is retrieved by calling egwGridColumns.getColumnData. - * The columns will be updated. - */ - _updateColumns: function() { - // Copy the columns data - this.columns = this.columnMgr.getColumnData(); - - // Count the visible rows - var total_cnt = 0; - for (var i = 0; i < this.columns.length; i++) - { - if (this.columns[i].visible) - { - total_cnt++; - } - } - - // Set the grid column styles - var first = true; - var vis_col = this.visibleColumnCount = 0; - var totalWidth = 0; - for (var i = 0; i < this.columns.length; i++) - { - var col = this.columns[i]; - - col.tdClass = this.uniqueId + "_td_" + col.id; - col.divClass = this.uniqueId + "_div_" + col.id; - - if (col.visible) - { - vis_col++; - this.visibleColumnCount++; - - // Update the visibility of the column - this.egw.css("." + col.tdClass, - "display: table-cell; " + - "!important;"); - - // Ugly browser dependant code - each browser seems to treat the - // right (collapsed) border of the row differently - var subBorder = 0; - var subHBorder = 0; - /* - if (jQuery.browser.mozilla) - { - var maj = jQuery.browser.version.split(".")[0]; - if (maj < 2) { - subBorder = 1; // Versions <= FF 3.6 - } - } - if (jQuery.browser.webkit) - { - if (!first) - { - subBorder = 1; - } - subHBorder = 1; - } - if ((jQuery.browser.msie || jQuery.browser.opera) && first) - { - subBorder = -1; - } - */ - - // Make the last columns one pixel smaller, to prevent a horizontal - // scrollbar from showing up - if (vis_col == total_cnt) - { - subBorder += 1; - } - - // Write the width of the header columns - var headerWidth = Math.max(0, (col.width - this.headerBorderWidth - subHBorder)); - this.egw.css(".egwGridView_outer ." + col.divClass, - "width: " + headerWidth + "px;"); - - // Write the width of the body-columns - var columnWidth = Math.max(0, (col.width - this.columnBorderWidth - subBorder)); - this.egw.css(".egwGridView_grid ." + col.divClass, - "width: " + columnWidth + "px;"); - - totalWidth += col.width; - - first = false; - } - else - { - this.egw.css("." + col.tdClass, "display: none;"); - } - } - - // Add the full row and spacer class - this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow", - "width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow", - "border-right-width: 0 !important;"); - this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow", - "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;"); - }, - - /** - * Builds the containers for the header row - */ - _buildHeader: function() { - var self = this; - var handler = function(event) { - }; - for (var i = 0; i < this.columns.length; i++) - { - var col = this.columns[i]; - - // Create the column header and the container element - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer") - .addClass(col.divClass); - - var column = jQuery(document.createElement("th")) - .addClass(col.tdClass) - .attr("align", "left") - .append(cont) - .appendTo(this.headTr); - - if(this.columnMgr && this.columnMgr.columns[i]) - { - column.addClass(this.columnMgr.columns[i].fixedWidth ? 'fixedWidth' : 'relativeWidth'); - if(this.columnMgr.columns[i].visibility === ET2_COL_VISIBILITY_ALWAYS_NOSELECT) - { - column.addClass('noResize'); - } - } - - // make column resizable - var enc_column = self.columnMgr.getColumnById(col.id); - if(enc_column.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT) - { - et2_dataview_makeResizeable(column, function(_w) { - - // User wants the column to stay where they put it, even for relative - // width columns, so set it explicitly first and adjust other relative - // columns to match. - if(this.relativeWidth) - { - // Set to selected width - this.set_width(_w + "px"); - self.columnMgr.updated = true; - // Just triggers recalculation - self.columnMgr.getColumnWidth(0); - - // Set relative widths to match - var relative = self.columnMgr.totalWidth - self.columnMgr.totalFixed + _w; - this.set_width(_w / relative); - for(var i = 0; i < self.columnMgr.columns.length; i++) - { - var col = self.columnMgr.columns[i]; - if(col == this || col.fixedWidth) continue; - col.set_width(self.columnMgr.columnWidths[i] / relative); - } - // Triggers column change callback, which saves - self.updateColumns(); - } - else - { - this.set_width(this.relativeWidth ? (_w / self.columnMgr.totalWidth) : _w + "px"); - self.columnMgr.updated = true; - self.updateColumns(); - } - - }, enc_column); - } - - // Store both nodes in the columnNodes array - this.columnNodes.push({ - "column": column, - "container": cont - }); - } - - this._buildSelectCol(); - }, - - /** - * Builds the select cols column - */ - _buildSelectCol: function() { - // Build the "select columns" icon - this.selectColIcon = jQuery(document.createElement("span")) - .addClass("selectcols") - .css('display', 'inline-block'); // otherwise jQuery('span.selectcols',this.dataview.headTr).show() set it to "inline" causing it to not show up because 0 height - - // Build the option column - this.selectCol = jQuery(document.createElement("th")) - .addClass("optcol") - .append(this.selectColIcon) - // Toggle display of option popup - .click(this, function(e) {if(e.data.selectColumnsClick) e.data.selectColumnsClick(e);}) - .appendTo(this.headTr); - - this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth() - + this.selectCol.width() + 1); - }, - - /** - * Builds the inner grid class - */ - _buildGrid: function() { - // Create the collection of column ids - var colIds = new Array(this.columns.length); - for (var i = 0; i < this.columns.length; i++) - { - colIds[i] = this.columns[i].id; - } - - // Create the row provider - if (this.rowProvider) - { - this.rowProvider.free(); - } - - this.rowProvider = new et2_dataview_rowProvider(this.uniqueId, colIds); - - // Create the grid class and pass "19" as the starting average row height - this.grid = new et2_dataview_grid(null, null, this.egw, this.rowProvider, 19); - - // Insert the grid into the DOM-Tree - var tr = jQuery(this.grid._nodes[0]); - this.containerTr.replaceWith(tr); - this.containerTr = tr; - }, - - /* --- Code for calculating the browser/css depending widths --- */ - - /** - * Reads the browser dependant variables - */ - _getDepVars: function() { - if (this.scrollbarWidth === false) - { - // Clone the table and attach it to the outer body tag - var clone = this.table.clone(); - jQuery(window.top.document.getElementsByTagName("body")[0]) - .append(clone); - - // Read the scrollbar width - this.scrollbarWidth = this.constructor.prototype.scrollbarWidth = - this._getScrollbarWidth(clone); - - // Read the header border width - this.headerBorderWidth = this.constructor.prototype.headerBorderWidth = - this._getHeaderBorderWidth(clone); - - // Read the column border width - this.columnBorderWidth = this.constructor.prototype.columnBorderWidth = - this._getColumnBorderWidth(clone); - - // Remove the cloned DOM-Node again from the outer body - clone.remove(); - } - }, - - /** - * Reads the scrollbar width - */ - _getScrollbarWidth: function(_table) { - // Create a temporary td and two divs, which are inserted into the - // DOM-Tree. The outer div has a fixed size and "overflow" set to auto. - // When the second div is inserted, it will be forced to display a scrollbar. - var div_inner = jQuery(document.createElement("div")) - .css("height", "1000px"); - var div_outer = jQuery(document.createElement("div")) - .css("height", "100px") - .css("width", "100px") - .css("overflow", "auto") - .append(div_inner); - var td = jQuery(document.createElement("td")) - .append(div_outer); - - // Store the scrollbar width statically. - jQuery("tbody tr", _table).append(td); - var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth()); - - // Remove the elements again - div_outer.remove(); - - return width; - }, - - /** - * Calculates the total width of the header column border - */ - _getHeaderBorderWidth: function(_table) { - // Create a temporary th which is appended to the outer thead row - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer"); - - var th = jQuery(document.createElement("th")) - .append(cont); - - // Insert the th into the document tree - jQuery("thead tr", _table).append(th); - - // Calculate the total border width - var width = th.outerWidth(true) - cont.width(); - - // Remove the appended element again - th.remove(); - - return width; - }, - - /** - * Calculates the total width of the column border - */ - _getColumnBorderWidth : function(_table) { - // Create a temporary th which is appended to the outer thead row - var cont = jQuery(document.createElement("div")) - .addClass("innerContainer"); - - var td = jQuery(document.createElement("td")) - .append(cont); - - // Insert the th into the document tree - jQuery("tbody tr", _table).append(td); - - // Calculate the total border width - _table.addClass("egwGridView_grid"); - var width = td.outerWidth(true) - cont.width(); - - // Remove the appended element again - td.remove(); - - return width; - } - -});}).call(this); - - +var et2_dataview = /** @class */ (function () { + /** + * Constructor for the grid container + * + * @param {DOMElement} _parentNode is the DOM-Node into which the grid view will be inserted + * @param {egw} _egw + * @memberOf et2_dataview + */ + function et2_dataview(_parentNode, _egw) { + // Copy the arguments + this.parentNode = jQuery(_parentNode); + this.egw = _egw; + // Initialize some variables + this.columnNodes = []; // Array with the header containers + this.columns = []; + this.columnMgr = null; + this.rowProvider = null; + this.width = 0; + this.height = 0; + this.uniqueId = "gridCont_" + this.egw.uid(); + // Build the base nodes + this._createElements(); + // Read the browser dependant variables + this._getDepVars(); + } + /** + * Destroys the object, removes all dom nodes and clears all references. + */ + et2_dataview.prototype.destroy = function () { + // Clear the columns + this._clearHeader(); + // Free the grid + if (this.grid) { + this.grid.free(); + } + // Free the row provider + if (this.rowProvider) { + this.rowProvider.free(); + } + // Detatch the outer element + this.table.remove(); + }; + /** + * Clears all data rows and reloads them + */ + et2_dataview.prototype.clear = function () { + if (this.grid) { + this.grid.clear(); + } + }; + /** + * Returns the column container node for the given column index + * + * @param _columnIdx the integer column index + */ + et2_dataview.prototype.getHeaderContainerNode = function (_columnIdx) { + if (typeof this.columnNodes[_columnIdx] != "undefined") { + return this.columnNodes[_columnIdx].container[0]; + } + return null; + }; + /** + * Sets the column descriptors and creates the column header according to it. + * The inner grid will be emptied if it has already been built. + */ + et2_dataview.prototype.setColumns = function (_columnData) { + // Free all column objects which have been created till this moment + this._clearHeader(); + // Copy the given column data + this.columnMgr = new et2_dataview_columns(_columnData); + // Create the stylesheets + this.updateColumns(); + // Build the header row + this._buildHeader(); + // Build the grid + this._buildGrid(); + }; + /** + * Resizes the grid + */ + et2_dataview.prototype.resize = function (_w, _h) { + // Not fully initialized yet... + if (!this.columnMgr) + return; + if (this.width != _w) { + this.width = _w; + // Take grid border width into account + _w -= (this.table.outerWidth(true) - this.table.innerWidth()); + // Take grid header border's width into account. eg. category colors may add extra pixel into width + _w = _w - (this.thead.find('tr').outerWidth() - this.thead.find('tr').innerWidth()); + // Rebuild the column stylesheets + this.columnMgr.setTotalWidth(_w - this.scrollbarWidth); + this._updateColumns(); + } + if (this.height != _h) { + this.height = _h; + // Set the height of the grid. + if (this.grid) { + this.grid.setScrollHeight(this.height - + this.headTr.outerHeight(true)); + } + } + }; + /** + * Returns the column manager object. You can use it to set the visibility + * of columns etc. Call "updateHeader" if you did any changes. + */ + et2_dataview.prototype.getColumnMgr = function () { + return this.columnMgr; + }; + /** + * Recalculates the stylesheets which determine the column visibility and + * width. + * + * @param setDefault boolean Allow admins to save current settings as default for all users + */ + et2_dataview.prototype.updateColumns = function (setDefault) { + if (setDefault === void 0) { setDefault = false; } + if (this.columnMgr) { + this._updateColumns(); + } + // Ability to notify parent / someone else + if (this.onUpdateColumns) { + this.onUpdateColumns(setDefault); + } + }; + /* --- PRIVATE FUNCTIONS --- */ + /* --- Code for building the grid container DOM-Tree elements ---- */ + /** + * Builds the base DOM-Tree elements + */ + et2_dataview.prototype._createElements = function () { + /* + Structure: + + + [HEAD] + + + [GRID CONTAINER] + +
+ */ + this.containerTr = jQuery(document.createElement("tr")); + this.headTr = jQuery(document.createElement("tr")); + this.thead = jQuery(document.createElement("thead")) + .append(this.headTr); + this.tbody = jQuery(document.createElement("tbody")) + .append(this.containerTr); + this.table = jQuery(document.createElement("table")) + .addClass("egwGridView_outer") + .append(this.thead, this.tbody) + .appendTo(this.parentNode); + }; + /* --- Code for building the header row --- */ + /** + * Clears the header row + */ + et2_dataview.prototype._clearHeader = function () { + if (this.columnMgr) { + this.columnMgr.free(); + this.columnMgr = null; + } + // Remove dynamic CSS, + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i].tdClass) { + this.egw.css('.' + this.columns[i].tdClass); + } + if (this.columns[i].divClass) { + this.egw.css('.' + this.columns[i].divClass); + this.egw.css(".egwGridView_outer ." + this.columns[i].divClass); + this.egw.css(".egwGridView_grid ." + this.columns[i].divClass); + } + } + this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow"); + // Reset the headerColumns array and empty the table row + this.columnNodes = []; + this.columns = []; + this.headTr.empty(); + }; + /** + * Sets the column data which is retrieved by calling egwGridColumns.getColumnData. + * The columns will be updated. + */ + et2_dataview.prototype._updateColumns = function () { + // Copy the columns data + this.columns = this.columnMgr.getColumnData(); + // Count the visible rows + var total_cnt = 0; + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i].visible) { + total_cnt++; + } + } + // Set the grid column styles + var first = true; + var vis_col = this.visibleColumnCount = 0; + var totalWidth = 0; + for (var i = 0; i < this.columns.length; i++) { + var col = this.columns[i]; + col.tdClass = this.uniqueId + "_td_" + col.id; + col.divClass = this.uniqueId + "_div_" + col.id; + if (col.visible) { + vis_col++; + this.visibleColumnCount++; + // Update the visibility of the column + this.egw.css("." + col.tdClass, "display: table-cell; " + + "!important;"); + // Ugly browser dependant code - each browser seems to treat the + // right (collapsed) border of the row differently + var subBorder = 0; + var subHBorder = 0; + /* + if (jQuery.browser.mozilla) + { + var maj = jQuery.browser.version.split(".")[0]; + if (maj < 2) { + subBorder = 1; // Versions <= FF 3.6 + } + } + if (jQuery.browser.webkit) + { + if (!first) + { + subBorder = 1; + } + subHBorder = 1; + } + if ((jQuery.browser.msie || jQuery.browser.opera) && first) + { + subBorder = -1; + } + */ + // Make the last columns one pixel smaller, to prevent a horizontal + // scrollbar from showing up + if (vis_col == total_cnt) { + subBorder += 1; + } + // Write the width of the header columns + var headerWidth = Math.max(0, (col.width - this.headerBorderWidth - subHBorder)); + this.egw.css(".egwGridView_outer ." + col.divClass, "width: " + headerWidth + "px;"); + // Write the width of the body-columns + var columnWidth = Math.max(0, (col.width - this.columnBorderWidth - subBorder)); + this.egw.css(".egwGridView_grid ." + col.divClass, "width: " + columnWidth + "px;"); + totalWidth += col.width; + first = false; + } + else { + this.egw.css("." + col.tdClass, "display: none;"); + } + } + // Add the full row and spacer class + this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow", "width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow", "border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow", "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;"); + }; + /** + * Builds the containers for the header row + */ + et2_dataview.prototype._buildHeader = function () { + var self = this; + var handler = function (event) { + }; + for (var i = 0; i < this.columns.length; i++) { + var col = this.columns[i]; + // Create the column header and the container element + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer") + .addClass(col.divClass); + var column = jQuery(document.createElement("th")) + .addClass(col.tdClass) + .attr("align", "left") + .append(cont) + .appendTo(this.headTr); + if (this.columnMgr && this.columnMgr.columns[i]) { + column.addClass(this.columnMgr.columns[i].fixedWidth ? 'fixedWidth' : 'relativeWidth'); + if (this.columnMgr.columns[i].visibility === et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { + column.addClass('noResize'); + } + } + // make column resizable + var enc_column = self.columnMgr.getColumnById(col.id); + if (enc_column.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { + et2_dataview_view_resizable.makeResizeable(column, function (_w) { + // User wants the column to stay where they put it, even for relative + // width columns, so set it explicitly first and adjust other relative + // columns to match. + if (this.relativeWidth) { + // Set to selected width + this.set_width(_w + "px"); + self.columnMgr.updated = true; + // Just triggers recalculation + self.columnMgr.getColumnWidth(0); + // Set relative widths to match + var relative = self.columnMgr.totalWidth - self.columnMgr.totalFixed + _w; + this.set_width(_w / relative); + for (var i = 0; i < self.columnMgr.columns.length; i++) { + var col = self.columnMgr.columns[i]; + if (col == this || col.fixedWidth) + continue; + col.set_width(self.columnMgr.columnWidths[i] / relative); + } + // Triggers column change callback, which saves + self.updateColumns(); + } + else { + this.set_width(this.relativeWidth ? (_w / self.columnMgr.totalWidth) : _w + "px"); + self.columnMgr.updated = true; + self.updateColumns(); + } + }, enc_column); + } + // Store both nodes in the columnNodes array + this.columnNodes.push({ + "column": column, + "container": cont + }); + } + this._buildSelectCol(); + }; + /** + * Builds the select cols column + */ + et2_dataview.prototype._buildSelectCol = function () { + // Build the "select columns" icon + this.selectColIcon = jQuery(document.createElement("span")) + .addClass("selectcols") + .css('display', 'inline-block'); // otherwise jQuery('span.selectcols',this.dataview.headTr).show() set it to "inline" causing it to not show up because 0 height + // Build the option column + this.selectCol = jQuery(document.createElement("th")) + .addClass("optcol") + .append(this.selectColIcon) + // Toggle display of option popup + .click(this, function (e) { if (e.data.selectColumnsClick) + e.data.selectColumnsClick(e); }) + .appendTo(this.headTr); + this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth() + + this.selectCol.width() + 1); + }; + /** + * Builds the inner grid class + */ + et2_dataview.prototype._buildGrid = function () { + // Create the collection of column ids + var colIds = new Array(this.columns.length); + for (var i = 0; i < this.columns.length; i++) { + colIds[i] = this.columns[i].id; + } + // Create the row provider + if (this.rowProvider) { + this.rowProvider.free(); + } + this.rowProvider = new et2_dataview_rowProvider(this.uniqueId, colIds); + // Create the grid class and pass "19" as the starting average row height + this.grid = new et2_dataview_grid(null, null, this.egw, this.rowProvider, 19); + // Insert the grid into the DOM-Tree + var tr = jQuery(this.grid._nodes[0]); + this.containerTr.replaceWith(tr); + this.containerTr = tr; + }; + /* --- Code for calculating the browser/css depending widths --- */ + /** + * Reads the browser dependant variables + */ + et2_dataview.prototype._getDepVars = function () { + if (typeof this.scrollbarWidth === 'undefined') { + // Clone the table and attach it to the outer body tag + var clone = this.table.clone(); + jQuery(window.top.document.getElementsByTagName("body")[0]) + .append(clone); + // Read the scrollbar width + this.scrollbarWidth = this.constructor.prototype.scrollbarWidth = + this._getScrollbarWidth(clone); + // Read the header border width + this.headerBorderWidth = this.constructor.prototype.headerBorderWidth = + this._getHeaderBorderWidth(clone); + // Read the column border width + this.columnBorderWidth = this.constructor.prototype.columnBorderWidth = + this._getColumnBorderWidth(clone); + // Remove the cloned DOM-Node again from the outer body + clone.remove(); + } + }; + /** + * Reads the scrollbar width + */ + et2_dataview.prototype._getScrollbarWidth = function (_table) { + // Create a temporary td and two divs, which are inserted into the + // DOM-Tree. The outer div has a fixed size and "overflow" set to auto. + // When the second div is inserted, it will be forced to display a scrollbar. + var div_inner = jQuery(document.createElement("div")) + .css("height", "1000px"); + var div_outer = jQuery(document.createElement("div")) + .css("height", "100px") + .css("width", "100px") + .css("overflow", "auto") + .append(div_inner); + var td = jQuery(document.createElement("td")) + .append(div_outer); + // Store the scrollbar width statically. + jQuery("tbody tr", _table).append(td); + var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth()); + // Remove the elements again + div_outer.remove(); + return width; + }; + /** + * Calculates the total width of the header column border + */ + et2_dataview.prototype._getHeaderBorderWidth = function (_table) { + // Create a temporary th which is appended to the outer thead row + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer"); + var th = jQuery(document.createElement("th")) + .append(cont); + // Insert the th into the document tree + jQuery("thead tr", _table).append(th); + // Calculate the total border width + var width = th.outerWidth(true) - cont.width(); + // Remove the appended element again + th.remove(); + return width; + }; + /** + * Calculates the total width of the column border + */ + et2_dataview.prototype._getColumnBorderWidth = function (_table) { + // Create a temporary th which is appended to the outer thead row + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer"); + var td = jQuery(document.createElement("td")) + .append(cont); + // Insert the th into the document tree + jQuery("tbody tr", _table).append(td); + // Calculate the total border width + _table.addClass("egwGridView_grid"); + var width = td.outerWidth(true) - cont.width(); + // Remove the appended element again + td.remove(); + return width; + }; + return et2_dataview; +}()); +exports.et2_dataview = et2_dataview; +//# sourceMappingURL=et2_dataview.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview.ts b/api/js/etemplate/et2_dataview.ts new file mode 100644 index 0000000000..6598a7261e --- /dev/null +++ b/api/js/etemplate/et2_dataview.ts @@ -0,0 +1,664 @@ +/** + * EGroupware eTemplate2 - dataview code + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011-2012 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_common; + + et2_dataview_model_columns; + et2_dataview_view_rowProvider; + et2_dataview_view_grid; + et2_dataview_view_resizeable; +*/ + +/** + * The et2_dataview class is the main class for displaying a dataview. The + * dataview class manages the creation of the outer html nodes (like the table, + * header, etc.) and contains the root container: an instance of + * et2_dataview_view_grid, which can be accessed using the "grid" property of + * this object. + * + * @augments Class + */ +export class et2_dataview +{ + + /** + * Constant which regulates the column padding. + */ + columnPadding: number; + + /** + * Some browser dependant variables which will be calculated on creation of + * the first gridContainer object. + */ + scrollbarWidth: number; + headerBorderWidth: number; + columnBorderWidth: number; + + private width: number; + private height: number; + + private uniqueId: string; + + /** + * Hooks to allow parent to keep up to date if things change + */ + onUpdateColumns: Function; + selectColumnsClick: Function; + + private parentNode: JQuery; + egw: any; + + private columnNodes: any[]; + private columns: any[]; + private columnMgr: et2_dataview_columns; + private rowProvider: et2_dataview_rowProvider; + + private grid: et2_dataview_grid; + + // DOM stuff + private selectColIcon: JQuery; + private headTr: any; + private containerTr: JQuery; + private selectCol: JQuery; + private thead: JQuery; + private tbody: JQuery; + private table: JQuery; + private visibleColumnCount: number; + + + /** + * Constructor for the grid container + * + * @param {DOMElement} _parentNode is the DOM-Node into which the grid view will be inserted + * @param {egw} _egw + * @memberOf et2_dataview + */ + constructor(_parentNode, _egw) { + + // Copy the arguments + this.parentNode = jQuery(_parentNode); + this.egw = _egw; + + // Initialize some variables + this.columnNodes = []; // Array with the header containers + this.columns = []; + this.columnMgr = null; + this.rowProvider = null; + + this.width = 0; + this.height = 0; + + this.uniqueId = "gridCont_" + this.egw.uid(); + + // Build the base nodes + this._createElements(); + + // Read the browser dependant variables + this._getDepVars(); + } + + /** + * Destroys the object, removes all dom nodes and clears all references. + */ + destroy() + { + // Clear the columns + this._clearHeader(); + + // Free the grid + if (this.grid) + { + this.grid.free(); + } + + // Free the row provider + if (this.rowProvider) + { + this.rowProvider.free(); + } + + // Detatch the outer element + this.table.remove(); + } + + /** + * Clears all data rows and reloads them + */ + clear() + { + if (this.grid) + { + this.grid.clear(); + } + } + + /** + * Returns the column container node for the given column index + * + * @param _columnIdx the integer column index + */ + getHeaderContainerNode(_columnIdx) + { + if (typeof this.columnNodes[_columnIdx] != "undefined") + { + return this.columnNodes[_columnIdx].container[0]; + } + + return null; + } + + /** + * Sets the column descriptors and creates the column header according to it. + * The inner grid will be emptied if it has already been built. + */ + setColumns(_columnData) + { + // Free all column objects which have been created till this moment + this._clearHeader(); + + // Copy the given column data + this.columnMgr = new et2_dataview_columns(_columnData); + + // Create the stylesheets + this.updateColumns(); + + // Build the header row + this._buildHeader(); + + // Build the grid + this._buildGrid(); + } + + /** + * Resizes the grid + */ + resize(_w: number, _h: number) + { + // Not fully initialized yet... + if (!this.columnMgr) return; + + if (this.width != _w) + { + this.width = _w; + + // Take grid border width into account + _w -= (this.table.outerWidth(true) - this.table.innerWidth()); + + // Take grid header border's width into account. eg. category colors may add extra pixel into width + _w = _w - (this.thead.find('tr').outerWidth() - this.thead.find('tr').innerWidth()); + + // Rebuild the column stylesheets + this.columnMgr.setTotalWidth(_w - this.scrollbarWidth); + this._updateColumns(); + } + + if (this.height != _h) + { + this.height = _h; + + // Set the height of the grid. + if (this.grid) + { + this.grid.setScrollHeight(this.height - + this.headTr.outerHeight(true)); + } + } + } + + /** + * Returns the column manager object. You can use it to set the visibility + * of columns etc. Call "updateHeader" if you did any changes. + */ + getColumnMgr() { + return this.columnMgr; + } + + /** + * Recalculates the stylesheets which determine the column visibility and + * width. + * + * @param setDefault boolean Allow admins to save current settings as default for all users + */ + updateColumns(setDefault : boolean = false) + { + if (this.columnMgr) + { + this._updateColumns(); + } + + // Ability to notify parent / someone else + if (this.onUpdateColumns) + { + this.onUpdateColumns(setDefault); + } + } + + + /* --- PRIVATE FUNCTIONS --- */ + + /* --- Code for building the grid container DOM-Tree elements ---- */ + + /** + * Builds the base DOM-Tree elements + */ + private _createElements() + { + /* + Structure: + + + [HEAD] + + + [GRID CONTAINER] + +
+ */ + + this.containerTr = jQuery(document.createElement("tr")); + this.headTr = jQuery(document.createElement("tr")); + + this.thead = jQuery(document.createElement("thead")) + .append(this.headTr); + this.tbody = jQuery(document.createElement("tbody")) + .append(this.containerTr); + + this.table = jQuery(document.createElement("table")) + .addClass("egwGridView_outer") + .append(this.thead, this.tbody) + .appendTo(this.parentNode); + } + + + /* --- Code for building the header row --- */ + + /** + * Clears the header row + */ + private _clearHeader () + { + if (this.columnMgr) + { + this.columnMgr.free(); + this.columnMgr = null; + } + + // Remove dynamic CSS, + for (var i = 0; i < this.columns.length; i++) + { + if(this.columns[i].tdClass) + { + this.egw.css('.'+this.columns[i].tdClass); + } + if(this.columns[i].divClass) + { + this.egw.css('.'+this.columns[i].divClass); + this.egw.css(".egwGridView_outer ." + this.columns[i].divClass); + this.egw.css(".egwGridView_grid ." + this.columns[i].divClass); + } + } + this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow"); + + // Reset the headerColumns array and empty the table row + this.columnNodes = []; + this.columns = []; + this.headTr.empty(); + } + + /** + * Sets the column data which is retrieved by calling egwGridColumns.getColumnData. + * The columns will be updated. + */ + private _updateColumns() + { + // Copy the columns data + this.columns = this.columnMgr.getColumnData(); + + // Count the visible rows + var total_cnt = 0; + for (var i = 0; i < this.columns.length; i++) + { + if (this.columns[i].visible) + { + total_cnt++; + } + } + + // Set the grid column styles + var first = true; + var vis_col = this.visibleColumnCount = 0; + var totalWidth = 0; + for (var i = 0; i < this.columns.length; i++) + { + var col = this.columns[i]; + + col.tdClass = this.uniqueId + "_td_" + col.id; + col.divClass = this.uniqueId + "_div_" + col.id; + + if (col.visible) + { + vis_col++; + this.visibleColumnCount++; + + // Update the visibility of the column + this.egw.css("." + col.tdClass, + "display: table-cell; " + + "!important;"); + + // Ugly browser dependant code - each browser seems to treat the + // right (collapsed) border of the row differently + var subBorder = 0; + var subHBorder = 0; + /* + if (jQuery.browser.mozilla) + { + var maj = jQuery.browser.version.split(".")[0]; + if (maj < 2) { + subBorder = 1; // Versions <= FF 3.6 + } + } + if (jQuery.browser.webkit) + { + if (!first) + { + subBorder = 1; + } + subHBorder = 1; + } + if ((jQuery.browser.msie || jQuery.browser.opera) && first) + { + subBorder = -1; + } + */ + + // Make the last columns one pixel smaller, to prevent a horizontal + // scrollbar from showing up + if (vis_col == total_cnt) + { + subBorder += 1; + } + + // Write the width of the header columns + var headerWidth = Math.max(0, (col.width - this.headerBorderWidth - subHBorder)); + this.egw.css(".egwGridView_outer ." + col.divClass, + "width: " + headerWidth + "px;"); + + // Write the width of the body-columns + var columnWidth = Math.max(0, (col.width - this.columnBorderWidth - subBorder)); + this.egw.css(".egwGridView_grid ." + col.divClass, + "width: " + columnWidth + "px;"); + + totalWidth += col.width; + + first = false; + } + else + { + this.egw.css("." + col.tdClass, "display: none;"); + } + } + + // Add the full row and spacer class + this.egw.css(".egwGridView_grid ." + this.uniqueId + "_div_fullRow", + "width: " + (totalWidth - this.columnBorderWidth - 2) + "px; border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_td_fullRow", + "border-right-width: 0 !important;"); + this.egw.css(".egwGridView_outer ." + this.uniqueId + "_spacer_fullRow", + "width: " + (totalWidth - 1) + "px; border-right-width: 0 !important;"); + } + + /** + * Builds the containers for the header row + */ + private _buildHeader() + { + var self = this; + var handler = function(event) { + }; + for (var i = 0; i < this.columns.length; i++) + { + var col = this.columns[i]; + + // Create the column header and the container element + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer") + .addClass(col.divClass); + + var column = jQuery(document.createElement("th")) + .addClass(col.tdClass) + .attr("align", "left") + .append(cont) + .appendTo(this.headTr); + + if(this.columnMgr && this.columnMgr.columns[i]) + { + column.addClass(this.columnMgr.columns[i].fixedWidth ? 'fixedWidth' : 'relativeWidth'); + if(this.columnMgr.columns[i].visibility === et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) + { + column.addClass('noResize'); + } + } + + // make column resizable + var enc_column = self.columnMgr.getColumnById(col.id); + if(enc_column.visibility !== et2_dataview_column.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) + { + et2_dataview_view_resizable.makeResizeable(column, function(_w) { + + // User wants the column to stay where they put it, even for relative + // width columns, so set it explicitly first and adjust other relative + // columns to match. + if(this.relativeWidth) + { + // Set to selected width + this.set_width(_w + "px"); + self.columnMgr.updated = true; + // Just triggers recalculation + self.columnMgr.getColumnWidth(0); + + // Set relative widths to match + var relative = self.columnMgr.totalWidth - self.columnMgr.totalFixed + _w; + this.set_width(_w / relative); + for(var i = 0; i < self.columnMgr.columns.length; i++) + { + var col = self.columnMgr.columns[i]; + if(col == this || col.fixedWidth) continue; + col.set_width(self.columnMgr.columnWidths[i] / relative); + } + // Triggers column change callback, which saves + self.updateColumns(); + } + else + { + this.set_width(this.relativeWidth ? (_w / self.columnMgr.totalWidth) : _w + "px"); + self.columnMgr.updated = true; + self.updateColumns(); + } + + }, enc_column); + } + + // Store both nodes in the columnNodes array + this.columnNodes.push({ + "column": column, + "container": cont + }); + } + + this._buildSelectCol(); + } + + /** + * Builds the select cols column + */ + private _buildSelectCol() + { + // Build the "select columns" icon + this.selectColIcon = jQuery(document.createElement("span")) + .addClass("selectcols") + .css('display', 'inline-block'); // otherwise jQuery('span.selectcols',this.dataview.headTr).show() set it to "inline" causing it to not show up because 0 height + + // Build the option column + this.selectCol = jQuery(document.createElement("th")) + .addClass("optcol") + .append(this.selectColIcon) + // Toggle display of option popup + .click(this, function(e) {if(e.data.selectColumnsClick) e.data.selectColumnsClick(e);}) + .appendTo(this.headTr); + + this.selectCol.css("width", this.scrollbarWidth - this.selectCol.outerWidth() + + this.selectCol.width() + 1); + } + + /** + * Builds the inner grid class + */ + private _buildGrid() + { + // Create the collection of column ids + var colIds = new Array(this.columns.length); + for (var i = 0; i < this.columns.length; i++) + { + colIds[i] = this.columns[i].id; + } + + // Create the row provider + if (this.rowProvider) + { + this.rowProvider.free(); + } + + this.rowProvider = new et2_dataview_rowProvider(this.uniqueId, colIds); + + // Create the grid class and pass "19" as the starting average row height + this.grid = new et2_dataview_grid(null, null, this.egw, this.rowProvider, 19); + + // Insert the grid into the DOM-Tree + var tr = jQuery(this.grid._nodes[0]); + this.containerTr.replaceWith(tr); + this.containerTr = tr; + } + + /* --- Code for calculating the browser/css depending widths --- */ + + /** + * Reads the browser dependant variables + */ + private _getDepVars() + { + if (typeof this.scrollbarWidth === 'undefined') + { + // Clone the table and attach it to the outer body tag + var clone = this.table.clone(); + jQuery(window.top.document.getElementsByTagName("body")[0]) + .append(clone); + + // Read the scrollbar width + this.scrollbarWidth = this.constructor.prototype.scrollbarWidth = + this._getScrollbarWidth(clone); + + // Read the header border width + this.headerBorderWidth = this.constructor.prototype.headerBorderWidth = + this._getHeaderBorderWidth(clone); + + // Read the column border width + this.columnBorderWidth = this.constructor.prototype.columnBorderWidth = + this._getColumnBorderWidth(clone); + + // Remove the cloned DOM-Node again from the outer body + clone.remove(); + } + } + + /** + * Reads the scrollbar width + */ + private _getScrollbarWidth(_table: JQuery) + { + // Create a temporary td and two divs, which are inserted into the + // DOM-Tree. The outer div has a fixed size and "overflow" set to auto. + // When the second div is inserted, it will be forced to display a scrollbar. + var div_inner = jQuery(document.createElement("div")) + .css("height", "1000px"); + var div_outer = jQuery(document.createElement("div")) + .css("height", "100px") + .css("width", "100px") + .css("overflow", "auto") + .append(div_inner); + var td = jQuery(document.createElement("td")) + .append(div_outer); + + // Store the scrollbar width statically. + jQuery("tbody tr", _table).append(td); + var width = Math.max(10, div_outer.outerWidth() - div_inner.outerWidth()); + + // Remove the elements again + div_outer.remove(); + + return width; + } + + /** + * Calculates the total width of the header column border + */ + private _getHeaderBorderWidth(_table: JQuery) + { + // Create a temporary th which is appended to the outer thead row + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer"); + + var th = jQuery(document.createElement("th")) + .append(cont); + + // Insert the th into the document tree + jQuery("thead tr", _table).append(th); + + // Calculate the total border width + var width = th.outerWidth(true) - cont.width(); + + // Remove the appended element again + th.remove(); + + return width; + } + + /** + * Calculates the total width of the column border + */ + private _getColumnBorderWidth(_table: JQuery) + { + // Create a temporary th which is appended to the outer thead row + var cont = jQuery(document.createElement("div")) + .addClass("innerContainer"); + + var td = jQuery(document.createElement("td")) + .append(cont); + + // Insert the th into the document tree + jQuery("tbody tr", _table).append(td); + + // Calculate the total border width + _table.addClass("egwGridView_grid"); + var width = td.outerWidth(true) - cont.width(); + + // Remove the appended element again + td.remove(); + + return width; + } + +} + diff --git a/api/js/etemplate/et2_dataview_interfaces.js b/api/js/etemplate/et2_dataview_interfaces.js index 8a057fafe1..940fafeb1e 100644 --- a/api/js/etemplate/et2_dataview_interfaces.js +++ b/api/js/etemplate/et2_dataview_interfaces.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Contains interfaces used inside the dataview * @@ -9,89 +10,5 @@ * @copyright Stylite 2011 * @version $Id$ */ - -/*egw:uses - et2_core_inheritance; -*/ - -var et2_dataview_IInvalidatable = new Interface({ - - invalidate: function() {} - -}); - -var et2_dataview_IViewRange = new Interface({ - - setViewRange: function(_range) {} - -}); - -/** - * Interface a data provider has to implement. The data provider functions are - * called by the et2_dataview_controller class. The data provider basically acts - * like the egw api egw_data extension, but some etemplate specific stuff has - * been stripped away -- the implementation (for the nextmatch widget that is - * et2_extension_nextmatch_dataprovider) has to take care of that. - */ -var et2_IDataProvider = new Interface({ - - /** - * This function is used by the et2_dataview_controller to fetch data for - * a certain range. The et2_dataview_controller provides data which allows - * to only update elements which really have changed. - * - * @param queriedRange is an object of the following form: - * { - * start: , - * num_rows: - * } - * @param knownRange is an array of the above form and informs the - * implementation which range is already known to the client. This parameter - * may be null in order to indicate that the client currently has no valid - * data. - * @param lastModification is the last timestamp that was returned from the - * data provider and for which the client has data. It may be null in order - * to indicate, that the client currently has no data or needs a complete - * refresh. - * @param callback is the function that should get called, once the data - * is available. The data passed to the callback function has the - * following form: - * { - * order: [uid, ...], - * total: , - * lastModification: - * } - * @param context is the context in which the callback function will get - * called. - */ - dataFetch: function (_queriedRange, _lastModification, _callback, _context) {}, - - /** - * Registers the intrest in a certain uid for a callback function. If - * the data for that uid changes or gets loaded, the given callback - * function is called. If the data for the given uid is available at the - * time of registering the callback, the callback is called immediately. - * - * @param _uid is the uid for which the callback should be registered. - * @param _callback is the callback which should get called. - * @param _context is an optional parameter which can - */ - dataRegisterUID: function (_uid, _callback, _context) {}, - - /** - * Unregisters the intrest of updates for a certain data uid. - * - * @param _uid is the data uid for which the callbacks should be - * unregistered. - * @param _callback specifies the specific callback that should be - * unregistered. If it evaluates to false, all callbacks (or those - * matching the optionally given context) are removed. - * @param _context specifies the callback context that should be - * unregistered. If it evaluates to false, all callbacks (or those - * matching the optionally given callback function) are removed. - */ - dataUnregisterUID: function (_uid, _callback, _context) {} - -}); - - +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=et2_dataview_interfaces.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_interfaces.ts b/api/js/etemplate/et2_dataview_interfaces.ts new file mode 100644 index 0000000000..4a8b0475a8 --- /dev/null +++ b/api/js/etemplate/et2_dataview_interfaces.ts @@ -0,0 +1,100 @@ +/** + * EGroupware eTemplate2 - Contains interfaces used inside the dataview + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + et2_core_inheritance; +*/ + +export interface et2_dataview_IInvalidatable +{ + + invalidate() + +} + +export interface et2_dataview_IViewRange +{ + + setViewRange(_range) + +} + +/** + * Interface a data provider has to implement. The data provider functions are + * called by the et2_dataview_controller class. The data provider basically acts + * like the egw api egw_data extension, but some etemplate specific stuff has + * been stripped away -- the implementation (for the nextmatch widget that is + * et2_extension_nextmatch_dataprovider) has to take care of that. + */ +export interface et2_IDataProvider +{ + + /** + * This function is used by the et2_dataview_controller to fetch data for + * a certain range. The et2_dataview_controller provides data which allows + * to only update elements which really have changed. + * + * @param _queriedRange is an object of the following form: + * { + * start: , + * num_rows: + * } + * @param _knownRange is an array of the above form and informs the + * implementation which range is already known to the client. This parameter + * may be null in order to indicate that the client currently has no valid + * data. + * @param _lastModification is the last timestamp that was returned from the + * data provider and for which the client has data. It may be null in order + * to indicate, that the client currently has no data or needs a complete + * refresh. + * @param _callback is the function that should get called, once the data + * is available. The data passed to the callback function has the + * following form: + * { + * order: [uid, ...], + * total: , + * lastModification: + * } + * @param _context is the context in which the callback function will get + * called. + */ + dataFetch (_queriedRange : {start: number, num_rows:number}, _lastModification, _callback : Function, _context : object) + + /** + * Registers the intrest in a certain uid for a callback function. If + * the data for that uid changes or gets loaded, the given callback + * function is called. If the data for the given uid is available at the + * time of registering the callback, the callback is called immediately. + * + * @param _uid is the uid for which the callback should be registered. + * @param _callback is the callback which should get called. + * @param _context is an optional parameter which can + */ + dataRegisterUID (_uid : string, _callback : Function, _context : object) + + /** + * Unregisters the intrest of updates for a certain data uid. + * + * @param _uid is the data uid for which the callbacks should be + * unregistered. + * @param _callback specifies the specific callback that should be + * unregistered. If it evaluates to false, all callbacks (or those + * matching the optionally given context) are removed. + * @param _context specifies the callback context that should be + * unregistered. If it evaluates to false, all callbacks (or those + * matching the optionally given callback function) are removed. + */ + dataUnregisterUID (_uid : string, _callback : Function, _context : object) + +} + + diff --git a/api/js/etemplate/et2_dataview_view_container.js b/api/js/etemplate/et2_dataview_view_container.js index f7007dcb29..08646d22d2 100644 --- a/api/js/etemplate/et2_dataview_view_container.js +++ b/api/js/etemplate/et2_dataview_view_container.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - dataview code * @@ -9,12 +10,7 @@ * @copyright Stylite 2012 * @version $Id$ */ - -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_dataview_interfaces; -*/ - +Object.defineProperty(exports, "__esModule", { value: true }); /** * The et2_dataview_container class is the main object each dataview consits of. * Each row, spacer as well as the grid itself are containers. A container is @@ -31,352 +27,287 @@ * * @augments Class */ -var et2_dataview_container = (function(){ "use strict"; return Class.extend(et2_dataview_IInvalidatable, -{ - /** - * Initializes the container object. - * - * @param _parent is an object which implements the IInvalidatable - * interface. _parent may not be null. - * @memberOf et2_dataview_container - */ - init: function(_parent) { - // Copy the given invalidation element - this._parent = _parent; - - this._nodes = []; // contains all DOM-Nodes this container exists of - this._inTree = false; // - this._attachData = {"node": null, "prepend": false}; - this._destroyCallback = null; - this._destroyContext = null; - - this._height = false; - this._index = 0; - this._top = 0; - }, - - /** - * Destroys this container. Classes deriving from et2_dataview_container - * should override this method and take care of unregistering all event - * handlers etc. - */ - destroy: function() { - // Remove the nodes from the tree - this.removeFromTree(); - - // Call the callback function (if one is registered) - if (this._destroyCallback) - { - this._destroyCallback.call(this._destroyContext, this); - } - }, - - /** - * Sets the "destroyCallback" -- the given function gets called whenever - * the container is destroyed. This instance is passed as an parameter to - * the callback. - * - * @param {function} _callback - * @param {object} _context - */ - setDestroyCallback: function(_callback, _context) { - this._destroyCallback = _callback; - this._destroyContext = _context; - }, - - /** - * Inserts all container nodes into the DOM tree after or before the given - * element. - * - * @param _node is the node after/before which the container "tr"s should - * get inserted. _node should be a simple DOM node, not a jQuery object. - * @param _prepend specifies whether the container should be inserted before - * or after the given node. Inserting before is needed for inserting the - * first element in front of an spacer. - */ - insertIntoTree: function(_node, _prepend) { - - if (!this._inTree && _node != null && this._nodes.length > 0) - { - // Store the parent node and indicate that this element is now in - // the tree. - this._attachData = {"node": _node, "prepend": _prepend}; - this._inTree = true; - - for (var i = 0; i < this._nodes.length; i++) - { - if (i == 0) - { - if (_prepend) - { - _node.before(this._nodes[0]); - } - else - { - _node.after(this._nodes[0]); - } - } - else - { - // Insert all following nodes after the previous node - this._nodes[i - 1].after(this._nodes[i]); - } - } - - // Invalidate this element in order to update the height of the - // parent - this.invalidate(); - } - }, - - /** - * Removes all container nodes from the tree. - */ - removeFromTree: function() { - if (this._inTree) - { - // Call the jQuery remove function to remove all nodes from the tree - // again. - for (var i = 0; i < this._nodes.length; i++) - { - this._nodes[i].remove(); - } - - // Reset the "attachData" - this._inTree = false; - this._attachData = {"node": null, "prepend": false}; - } - }, - - /** - * Appends a node to the container. - * - * @param _node is the DOM-Node which should be appended. - */ - appendNode: function(_node) { - // Add the given node to the "nodes" array - this._nodes.push(_node); - - // If the container is already in the tree, attach the given node to the - // tree. - if (this._inTree) - { - if (this._nodes.length === 1) - { - if (this._attachData.prepend) - { - this._attachData.node.before(_node); - } - else - { - this._attachData.node.after(_node); - } - } - else - { - this._nodes[this._nodes.length - 2].after(_node); - } - - this.invalidate(); - } - }, - - /** - * Removes a certain node from the container - * - * @param {DOMElement} _node - */ - removeNode: function(_node) { - // Get the index of the node in the nodes array - var idx = this._nodes.indexOf(_node); - - if (idx >= 0) - { - // Remove the node if the container is currently attached - if (this._inTree) - { - _node.parentNode.removeChild(_node); - } - - // Remove the node from the nodes array - this._nodes.splice(idx, 1); - } - }, - - /** - * Returns the last node of the container - new nodes have to be appended - * after it. - */ - getLastNode: function() { - - if (this._nodes.length > 0) - { - return this._nodes[this._nodes.length - 1]; - } - - return null; - }, - - /** - * Returns the first node of the container. - */ - getFirstNode: function() { - return this._nodes.length > 0 ? this._nodes[0] : null; - }, - - /** - * Returns the accumulated height of all container nodes. Only visible nodes - * (without "display: none" etc.) are taken into account. - */ - getHeight: function() { - if (this._height === false && this._inTree) - { - this._height = 0; - - // Setting this before measuring height helps with issues getting the - // wrong height due to margins & collapsed borders - this.tr.css('display','block'); - - // Increment the height value for each visible container node - for (var i = 0; i < this._nodes.length; i++) - { - if (this._isVisible(this._nodes[i][0])) - { - this._height += this._nodeHeight(this._nodes[i][0]); - } - } - this.tr.css('display',''); - } - - return this._height === false ? 0 : this._height; - }, - - /** - * Returns a datastructure containing information used for calculating the - * average row height of a grid. - * The datastructure has the - * { - * avgHeight: , - * avgCount: - * } - */ - getAvgHeightData: function() { - return { - "avgHeight": this.getHeight(), - "avgCount": 1 - }; - }, - - /** - * Returns the previously set "pixel top" of the container. - */ - getTop: function() { - return this._top; - }, - - /** - * Returns the "pixel bottom" of the container. - */ - getBottom: function() { - return this._top + this.getHeight(); - }, - - /** - * Returns the range of the element. - */ - getRange: function() { - return et2_bounds(this.getTop(), this.getBottom()); - }, - - /** - * Returns the index of the element. - */ - getIndex: function() { - return this._index; - }, - - /** - * Returns how many elements this container represents. - */ - getCount: function() { - return 1; - }, - - /** - * Sets the top of the element. - * - * @param {number} _value - */ - setTop: function(_value) { - this._top = _value; - }, - - /** - * Sets the index of the element. - * - * @param {number} _value - */ - setIndex: function(_value) { - this._index = _value; - }, - - /* -- et2_dataview_IInvalidatable -- */ - - /** - * Broadcasts an invalidation through the container tree. Marks the own - * height as invalid. - */ - invalidate: function() { - // Abort if this element is already marked as invalid. - if (this._height !== false) - { - // Delete the own, probably computed height - this._height = false; - - // Broadcast the invalidation to the parent element - this._parent.invalidate(); - } - }, - - - /* -- PRIVATE FUNCTIONS -- */ - - - /** - * Used to check whether an element is visible or not (non recursive). - * - * @param _obj is the element which should be checked for visibility, it is - * only checked whether some stylesheet makes the element invisible, not if - * the given object is actually inside the DOM. - */ - _isVisible: function(_obj) { - - // Check whether the element is localy invisible - if (_obj.style && (_obj.style.display === "none" - || _obj.style.visiblity === "none")) - { - return false; - } - - // Get the computed style of the element - var style = window.getComputedStyle ? window.getComputedStyle(_obj, null) - : _obj.currentStyle; - if (style.display === "none" || style.visibility === "none") - { - return false; - } - - return true; - }, - - /** - * Returns the height of a node in pixels and zero if the element is not - * visible. The height is clamped to positive values. - * - * @param {DOMElement} _node - */ - _nodeHeight: function(_node) - { - return _node.offsetHeight; - } -});}).call(this); +var et2_dataview_container = /** @class */ (function () { + /** + * Initializes the container object. + * + * @param _parent is an object which implements the IInvalidatable + * interface. _parent may not be null. + * @memberOf et2_dataview_container + */ + function et2_dataview_container(_parent) { + // Copy the given invalidation element + this._parent = _parent; + this._nodes = []; + this._inTree = false; + this._attachData = { "node": null, "prepend": false }; + this._destroyCallback = null; + this._destroyContext = null; + this._height = -1; + this._index = 0; + this._top = 0; + } + /** + * Destroys this container. Classes deriving from et2_dataview_container + * should override this method and take care of unregistering all event + * handlers etc. + */ + et2_dataview_container.prototype.destroy = function () { + // Remove the nodes from the tree + this.removeFromTree(); + // Call the callback function (if one is registered) + if (this._destroyCallback) { + this._destroyCallback.call(this._destroyContext, this); + } + }; + /** + * Sets the "destroyCallback" -- the given function gets called whenever + * the container is destroyed. This instance is passed as an parameter to + * the callback. + * + * @param {function} _callback + * @param {object} _context + */ + et2_dataview_container.prototype.setDestroyCallback = function (_callback, _context) { + this._destroyCallback = _callback; + this._destroyContext = _context; + }; + /** + * Inserts all container nodes into the DOM tree after or before the given + * element. + * + * @param _node is the node after/before which the container "tr"s should + * get inserted. _node should be a simple DOM node, not a jQuery object. + * @param _prepend specifies whether the container should be inserted before + * or after the given node. Inserting before is needed for inserting the + * first element in front of an spacer. + */ + et2_dataview_container.prototype.insertIntoTree = function (_node, _prepend) { + if (!this._inTree && _node != null && this._nodes.length > 0) { + // Store the parent node and indicate that this element is now in + // the tree. + this._attachData = { node: _node, prepend: _prepend }; + this._inTree = true; + for (var i = 0; i < this._nodes.length; i++) { + if (i == 0) { + if (_prepend) { + _node.before(this._nodes[0]); + } + else { + _node.after(this._nodes[0]); + } + } + else { + // Insert all following nodes after the previous node + this._nodes[i - 1].after(this._nodes[i]); + } + } + // Invalidate this element in order to update the height of the + // parent + this.invalidate(); + } + }; + /** + * Removes all container nodes from the tree. + */ + et2_dataview_container.prototype.removeFromTree = function () { + if (this._inTree) { + // Call the jQuery remove function to remove all nodes from the tree + // again. + for (var i = 0; i < this._nodes.length; i++) { + this._nodes[i].remove(); + } + // Reset the "attachData" + this._inTree = false; + this._attachData = { "node": null, "prepend": false }; + } + }; + /** + * Appends a node to the container. + * + * @param _node is the DOM-Node which should be appended. + */ + et2_dataview_container.prototype.appendNode = function (_node) { + // Add the given node to the "nodes" array + this._nodes.push(_node); + // If the container is already in the tree, attach the given node to the + // tree. + if (this._inTree) { + if (this._nodes.length === 1) { + if (this._attachData.prepend) { + this._attachData.node.before(_node); + } + else { + this._attachData.node.after(_node); + } + } + else { + this._nodes[this._nodes.length - 2].after(_node); + } + this.invalidate(); + } + }; + /** + * Removes a certain node from the container + * + * @param {HTMLElement} _node + */ + et2_dataview_container.prototype.removeNode = function (_node) { + // Get the index of the node in the nodes array + var idx = this._nodes.indexOf(_node); + if (idx >= 0) { + // Remove the node if the container is currently attached + if (this._inTree) { + _node.parentNode.removeChild(_node); + } + // Remove the node from the nodes array + this._nodes.splice(idx, 1); + } + }; + /** + * Returns the last node of the container - new nodes have to be appended + * after it. + */ + et2_dataview_container.prototype.getLastNode = function () { + if (this._nodes.length > 0) { + return this._nodes[this._nodes.length - 1]; + } + return null; + }; + /** + * Returns the first node of the container. + */ + et2_dataview_container.prototype.getFirstNode = function () { + return this._nodes.length > 0 ? this._nodes[0] : null; + }; + /** + * Returns the accumulated height of all container nodes. Only visible nodes + * (without "display: none" etc.) are taken into account. + */ + et2_dataview_container.prototype.getHeight = function () { + if (this._height === -1 && this._inTree) { + this._height = 0; + // Setting this before measuring height helps with issues getting the + // wrong height due to margins & collapsed borders + this.tr.css('display', 'block'); + // Increment the height value for each visible container node + for (var i = 0; i < this._nodes.length; i++) { + if (et2_dataview_container._isVisible(this._nodes[i][0])) { + this._height += et2_dataview_container._nodeHeight(this._nodes[i][0]); + } + } + this.tr.css('display', ''); + } + return (this._height === -1) ? 0 : this._height; + }; + /** + * Returns a datastructure containing information used for calculating the + * average row height of a grid. + * The datastructure has the + * { + * avgHeight: , + * avgCount: + * } + */ + et2_dataview_container.prototype.getAvgHeightData = function () { + return { + "avgHeight": this.getHeight(), + "avgCount": 1 + }; + }; + /** + * Returns the previously set "pixel top" of the container. + */ + et2_dataview_container.prototype.getTop = function () { + return this._top; + }; + /** + * Returns the "pixel bottom" of the container. + */ + et2_dataview_container.prototype.getBottom = function () { + return this._top + this.getHeight(); + }; + /** + * Returns the range of the element. + */ + et2_dataview_container.prototype.getRange = function () { + return et2_bounds(this.getTop(), this.getBottom()); + }; + /** + * Returns the index of the element. + */ + et2_dataview_container.prototype.getIndex = function () { + return this._index; + }; + /** + * Returns how many elements this container represents. + */ + et2_dataview_container.prototype.getCount = function () { + return 1; + }; + /** + * Sets the top of the element. + * + * @param {number} _value + */ + et2_dataview_container.prototype.setTop = function (_value) { + this._top = _value; + }; + /** + * Sets the index of the element. + * + * @param {number} _value + */ + et2_dataview_container.prototype.setIndex = function (_value) { + this._index = _value; + }; + /* -- et2_dataview_IInvalidatable -- */ + /** + * Broadcasts an invalidation through the container tree. Marks the own + * height as invalid. + */ + et2_dataview_container.prototype.invalidate = function () { + // Abort if this element is already marked as invalid. + if (this._height !== -1) { + // Delete the own, probably computed height + this._height = -1; + // Broadcast the invalidation to the parent element + this._parent.invalidate(); + } + }; + /* -- PRIVATE FUNCTIONS -- */ + /** + * Used to check whether an element is visible or not (non recursive). + * + * @param _obj is the element which should be checked for visibility, it is + * only checked whether some stylesheet makes the element invisible, not if + * the given object is actually inside the DOM. + */ + et2_dataview_container._isVisible = function (_obj) { + // Check whether the element is localy invisible + if (_obj.style && (_obj.style.display === "none" + || _obj.style.visibility === "none")) { + return false; + } + // Get the computed style of the element + var style = window.getComputedStyle ? window.getComputedStyle(_obj, null) + // @ts-ignore + : _obj.currentStyle; + if (style.display === "none" || style.visibility === "none") { + return false; + } + return true; + }; + /** + * Returns the height of a node in pixels and zero if the element is not + * visible. The height is clamped to positive values. + * + * @param {HTMLElement} _node + */ + et2_dataview_container._nodeHeight = function (_node) { + return _node.offsetHeight; + }; + return et2_dataview_container; +}()); +exports.et2_dataview_container = et2_dataview_container; +//# sourceMappingURL=et2_dataview_view_container.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_container.ts b/api/js/etemplate/et2_dataview_view_container.ts new file mode 100644 index 0000000000..0281ce78fd --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_container.ts @@ -0,0 +1,421 @@ +/** + * EGroupware eTemplate2 - dataview code + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2012 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_dataview_interfaces; +*/ + +import {et2_dataview_IInvalidatable} from "./et2_dataview_interfaces"; + +/** + * The et2_dataview_container class is the main object each dataview consits of. + * Each row, spacer as well as the grid itself are containers. A container is + * described by its parent element and a certain height. On the DOM-Level a + * container may consist of multiple "tr" nodes, which are treated as a unit. + * Some containers (like grid containers) are capable of managing a set of child + * containers. Each container can indicate, that it thinks that it's height + * might have changed. In that case it informs its parent element about that. + * The only requirement for the parent element is, that it implements the + * et2_dataview_IInvalidatable interface. + * A container does not know where it resides inside the grid, or whether it is + * currently visible or not -- this information is efficiently managed by the + * et2_dataview_grid container. + * + * @augments Class + */ +export class et2_dataview_container implements et2_dataview_IInvalidatable +{ + protected _parent: any; + + // contains all DOM-Nodes this container exists of + private _nodes: any[]; + private _inTree: boolean; + + private _attachData: { node: JQuery; prepend: boolean }; + + private _destroyCallback: Function; + _destroyContext: any; + + private _height: number; + private _index: number; + private _top: number; + + protected tr: any; + /** + * Initializes the container object. + * + * @param _parent is an object which implements the IInvalidatable + * interface. _parent may not be null. + * @memberOf et2_dataview_container + */ + constructor(_parent) + { + // Copy the given invalidation element + this._parent = _parent; + + this._nodes = []; + this._inTree = false; + this._attachData = {"node": null, "prepend": false}; + this._destroyCallback = null; + this._destroyContext = null; + + this._height = -1; + this._index = 0; + this._top = 0; + } + + /** + * Destroys this container. Classes deriving from et2_dataview_container + * should override this method and take care of unregistering all event + * handlers etc. + */ + destroy() + { + // Remove the nodes from the tree + this.removeFromTree(); + + // Call the callback function (if one is registered) + if (this._destroyCallback) + { + this._destroyCallback.call(this._destroyContext, this); + } + } + + /** + * Sets the "destroyCallback" -- the given function gets called whenever + * the container is destroyed. This instance is passed as an parameter to + * the callback. + * + * @param {function} _callback + * @param {object} _context + */ + setDestroyCallback(_callback : Function, _context : object) { + this._destroyCallback = _callback; + this._destroyContext = _context; + } + + /** + * Inserts all container nodes into the DOM tree after or before the given + * element. + * + * @param _node is the node after/before which the container "tr"s should + * get inserted. _node should be a simple DOM node, not a jQuery object. + * @param _prepend specifies whether the container should be inserted before + * or after the given node. Inserting before is needed for inserting the + * first element in front of an spacer. + */ + insertIntoTree(_node: JQuery, _prepend: boolean) + { + + if (!this._inTree && _node != null && this._nodes.length > 0) + { + // Store the parent node and indicate that this element is now in + // the tree. + this._attachData = {node: _node, prepend: _prepend}; + this._inTree = true; + + for (let i = 0; i < this._nodes.length; i++) + { + if (i == 0) + { + if (_prepend) + { + _node.before(this._nodes[0]); + } + else + { + _node.after(this._nodes[0]); + } + } + else + { + // Insert all following nodes after the previous node + this._nodes[i - 1].after(this._nodes[i]); + } + } + + // Invalidate this element in order to update the height of the + // parent + this.invalidate(); + } + } + + /** + * Removes all container nodes from the tree. + */ + removeFromTree() + { + if (this._inTree) + { + // Call the jQuery remove function to remove all nodes from the tree + // again. + for (let i = 0; i < this._nodes.length; i++) + { + this._nodes[i].remove(); + } + + // Reset the "attachData" + this._inTree = false; + this._attachData = {"node": null, "prepend": false}; + } + } + + /** + * Appends a node to the container. + * + * @param _node is the DOM-Node which should be appended. + */ + appendNode(_node : JQuery | HTMLElement) + { + // Add the given node to the "nodes" array + this._nodes.push(_node); + + // If the container is already in the tree, attach the given node to the + // tree. + if (this._inTree) + { + if (this._nodes.length === 1) + { + if (this._attachData.prepend) + { + this._attachData.node.before(_node); + } + else + { + this._attachData.node.after(_node); + } + } + else + { + this._nodes[this._nodes.length - 2].after(_node); + } + + this.invalidate(); + } + } + + /** + * Removes a certain node from the container + * + * @param {HTMLElement} _node + */ + removeNode(_node: HTMLElement) + { + // Get the index of the node in the nodes array + const idx = this._nodes.indexOf(_node); + + if (idx >= 0) + { + // Remove the node if the container is currently attached + if (this._inTree) + { + _node.parentNode.removeChild(_node); + } + + // Remove the node from the nodes array + this._nodes.splice(idx, 1); + } + } + + /** + * Returns the last node of the container - new nodes have to be appended + * after it. + */ + getLastNode() + { + + if (this._nodes.length > 0) + { + return this._nodes[this._nodes.length - 1]; + } + + return null; + } + + /** + * Returns the first node of the container. + */ + getFirstNode() + { + return this._nodes.length > 0 ? this._nodes[0] : null; + } + + /** + * Returns the accumulated height of all container nodes. Only visible nodes + * (without "display: none" etc.) are taken into account. + */ + getHeight() + { + if (this._height === -1 && this._inTree) + { + this._height = 0; + + // Setting this before measuring height helps with issues getting the + // wrong height due to margins & collapsed borders + this.tr.css('display','block'); + + // Increment the height value for each visible container node + for (let i = 0; i < this._nodes.length; i++) + { + if (et2_dataview_container._isVisible(this._nodes[i][0])) + { + this._height += et2_dataview_container._nodeHeight(this._nodes[i][0]); + } + } + this.tr.css('display',''); + } + + return ( this._height === -1 ) ? 0 : this._height; + } + + /** + * Returns a datastructure containing information used for calculating the + * average row height of a grid. + * The datastructure has the + * { + * avgHeight: , + * avgCount: + * } + */ + getAvgHeightData() + { + return { + "avgHeight": this.getHeight(), + "avgCount": 1 + }; + } + + /** + * Returns the previously set "pixel top" of the container. + */ + getTop() + { + return this._top; + } + + /** + * Returns the "pixel bottom" of the container. + */ + getBottom() + { + return this._top + this.getHeight(); + } + + /** + * Returns the range of the element. + */ + getRange() + { + return et2_bounds(this.getTop(), this.getBottom()); + } + + /** + * Returns the index of the element. + */ + getIndex() + { + return this._index; + } + + /** + * Returns how many elements this container represents. + */ + getCount() + { + return 1; + } + + /** + * Sets the top of the element. + * + * @param {number} _value + */ + setTop(_value) + { + this._top = _value; + } + + /** + * Sets the index of the element. + * + * @param {number} _value + */ + setIndex(_value) + { + this._index = _value; + } + + /* -- et2_dataview_IInvalidatable -- */ + + /** + * Broadcasts an invalidation through the container tree. Marks the own + * height as invalid. + */ + invalidate() + { + // Abort if this element is already marked as invalid. + if ( this._height !== -1) + { + // Delete the own, probably computed height + this._height = -1; + + // Broadcast the invalidation to the parent element + this._parent.invalidate(); + } + } + + + /* -- PRIVATE FUNCTIONS -- */ + + + /** + * Used to check whether an element is visible or not (non recursive). + * + * @param _obj is the element which should be checked for visibility, it is + * only checked whether some stylesheet makes the element invisible, not if + * the given object is actually inside the DOM. + */ + private static _isVisible(_obj : HTMLElement) + { + + // Check whether the element is localy invisible + if (_obj.style && (_obj.style.display === "none" + || _obj.style.visibility === "none")) + { + return false; + } + + // Get the computed style of the element + const style = window.getComputedStyle ? window.getComputedStyle(_obj, null) + // @ts-ignore + : _obj.currentStyle; + + if (style.display === "none" || style.visibility === "none") + { + return false; + } + + return true; + } + + /** + * Returns the height of a node in pixels and zero if the element is not + * visible. The height is clamped to positive values. + * + * @param {HTMLElement} _node + */ + private static _nodeHeight(_node : HTMLElement) + { + return _node.offsetHeight; + } +} diff --git a/api/js/etemplate/et2_dataview_view_grid.js b/api/js/etemplate/et2_dataview_view_grid.js index 7b770f45e0..50af7cd4aa 100644 --- a/api/js/etemplate/et2_dataview_view_grid.js +++ b/api/js/etemplate/et2_dataview_view_grid.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - Class which contains the "grid" base class * @@ -9,1381 +10,1074 @@ * @copyright Stylite 2011 * @version $Id$ */ - -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - et2_core_common; - - et2_dataview_interfaces; - et2_dataview_view_container; - et2_dataview_view_spacer; -*/ - -/** - * Determines how many pixels the view range of the gridview is extended inside - * the scroll callback. - */ -var ET2_GRID_VIEW_EXT = 50; - -/** - * Determines the timeout after which the scroll-event is processed. - */ -var ET2_GRID_SCROLL_TIMEOUT = 50; - -/** - * Determines the timeout after which the invalidate-request gets processed. - */ -var ET2_GRID_INVALIDATE_TIMEOUT = 25; - -/** - * Determines how many elements are kept displayed outside of the current view - * range until they get removed. - */ -var ET2_GRID_HOLD_COUNT = 50; -/** - * @augments et2_dataview_container - */ -var et2_dataview_grid = (function(){ "use strict"; return et2_dataview_container.extend(et2_dataview_IViewRange, -{ - /** - * Creates the grid. - * - * @param _parent is the parent grid class - if null, this means that this - * is the outer grid which manages the scrollarea. If not null, all other - * parameters are ignored and copied from the given grid instance. - * @param _avgHeight is the starting average height of the column rows. - * @memberOf et2_dataview_grid - */ - init: function (_parent, _parentGrid, _egw, _rowProvider, _avgHeight) { - - // Call the inherited constructor - this._super(_parent); - - // If the parent is given, copy all other parameters from it - if (_parentGrid != null) - { - this.egw = _parent.egw; - this._orgAvgHeight = false; - this._rowProvider = _parentGrid._rowProvider; - } - else - { - // Otherwise copy the given parameters - this.egw = _egw; - this._orgAvgHeight = _avgHeight; - this._rowProvider = _rowProvider; - - // As this grid instance has no parent, we need a scroll container - this._scrollHeight = 0; - this._scrollTimeout = null; - } - - this._parentGrid = _parentGrid; - - this._scrollTimeout = null; - - this._invalidateTimeout = null; - - this._invalidateCallback = null; - this._invalidateContext = null; - - // Flag for stopping invalidate while working - this.doInvalidate = true; - - // _map contains a mapping between the grid indices and the elements - // associated to it. The first element in the array always refers to the - // element starting at index zero (being a spacer if the grid currently - // displays another range). - this._map = []; - - // _viewRange contains the current pixel-range of the grid which is - // visible. - this._viewRange = et2_range(0, 0); - - // Holds the maximum count of elements - this._total = 0; - - // Holds data used for storing the current average height data - this._avgHeight = false; - this._avgCount = false; - - // Build the outer grid nodes - this._createNodes(); - }, - - destroy: function () { - // Destroy all containers - this.setTotalCount(0); - - // Stop the scroll timeout - if (this._scrollTimeout) - { - window.clearTimeout(this._scrollTimeout); - } - - // Stop the invalidate timeout - if (this._invalidateTimeout) - { - window.clearTimeout(this._invalidateTimeout); - } - - this._super(); - }, - - clear: function () { - // Store the old total count and rescue the current average height in - // form of the "original average height" - var oldTotalCount = this._total; - this._orgAvgHeight = this.getAverageHeight(); - - // Set the total count to zero - this.setTotalCount(0); - - // Reset the total count value - this.setTotalCount(oldTotalCount); - }, - - /** - * Throws all elements away which are outside the current view range - */ - cleanup: function () { - // Update the pixel positions - this._recalculateElementPosition(); - - // Get the visible mapping indices and recalculate index and pixel - // position of the containers. - var mapVis = this._calculateVisibleMappingIndices(); - - // Delete all invisible elements -- if anything changed, we have to - // recalculate the pixel positions again - this._cleanupOutOfRangeElements(mapVis, 0); - }, - - /** - * The insertRow function can be called to insert the given container(s) at - * the given row index. If there currently is another container at that - * given position, the new container(s) will be inserted above the old - * container. Yet the "total count" of the grid will be preserved by - * removing the correct count of elements from the next possible spacer. If - * no spacer is found, the last containers will be removed. This causes - * inserting new containers at the end of a grid to be immediately removed - * again. - * - * @param _index is the row index at which the given container(s) should be - * inserted. - * @param _container is eiter a single et2_dataview_container instance - * which should be inserted at the given position. Or an array of - * et2_dataview_container instances. If you want to remove the container - * don't do that manually by calling its "destroy" function but use the - * deleteRow function. - */ - insertRow: function (_index, _container) { - // Calculate the map element the given index refers to - var idx = this._calculateMapIndex(_index); - - if (idx !== false) - { - // Wrap the container inside an array - if (_container instanceof et2_dataview_container) - { - _container = [_container]; - } - - // Fetch the average height - var avg = this.getAverageHeight(); - - // Call the internal _doInsertContainer function - for (var i = 0; i < _container.length; i++) - { - this._doInsertContainer(_index, idx, _container[i], avg); - } - - // Schedule an "invalidate" event - this.invalidate(); - } - }, - - /** - * The deleteRow function can be used to remove the element at the given - * index. - * - * @param _index is the index from which should be deleted. If the given - * index is outside the so called "managedRange" nothing will happen, as the - * container has already been destroyed by the grid instance. - */ - deleteRow: function (_index) { - // Calculate the map element the given index refers to - var idx = this._calculateMapIndex(_index); - - if (idx !== false) - { - this._doDeleteContainer(idx, false); - - // Schedule an "invalidate" event - this.invalidate(); - } - }, - - /** - * The given callback gets called whenever the scroll position changed or - * the visible element range changed. The element indices are passed to the - * function as et2_range. - */ - setInvalidateCallback: function (_callback, _context) { - this._invalidateCallback = _callback; - this._invalidateContext = _context; - }, - - /** - * The setDataCallback function is used to set the callback that will be - * called when the grid requires new data. - * - * @param _callback is the callback function which gets called when the grid - * needs some new rows. - * @param _context is the context in which the callback function gets - * called. - */ - setDataCallback: function (_callback, _context) { - this._callback = _callback; - this._context = _context; - }, - - /** - * The updateTotalCount function can be used to update the total count of - * rows that are displayed inside the grid. Changing the count always causes - * the spacer at the bottom (if it exists) to be - * - * @param _count specifies how many entries the grid can show. - */ - setTotalCount: function (_count) { - // Abort if the total count has not changed - if (_count === this._total) - return; - - // Calculate how many elements have to be removed/added - var delta = Math.max(0, _count) - this._total; - - if (delta > 0) - { - this._appendEmptyRows(delta); - } - else - { - this._decreaseTotal(-delta); - } - - this._total = Math.max(0, _count); - - // Schedule an invalidate - this.invalidate(); - }, - - /** - * Returns the current "total" count. - */ - getTotalCount: function () { - return this._total; - }, - - /** - * The setViewRange function updates the range in which rows are shown. - */ - setViewRange: function (_range) { - // Set the new view range - this._viewRange = _range; - - // Immediately call the "invalidate" function - this._doInvalidate(); - }, - - /** - * Return the indices of the currently visible rows. - */ - getVisibleIndexRange: function (_viewRange) { - - function getElemIdx(_elem, _px) - { - if (_elem instanceof et2_dataview_spacer) - { - return _elem.getIndex() - + Math.floor((_px - _elem.getTop()) / this.getAverageHeight()); - } - - return _elem.getIndex(); - } - - var idxTop = 0; - var idxBottom = 0; - var vr; - - if (_viewRange) - { - vr = _viewRange; - } - else - { - // Calculate the "correct" view range by removing ET2_GRID_VIEW_EXT - vr = et2_bounds( - this._viewRange.top + ET2_GRID_VIEW_EXT, - this._viewRange.bottom - ET2_GRID_VIEW_EXT); - } - - // Get the elements at the top and the bottom of the view - var topElem = null; - var botElem = null; - for (var i = 0; i < this._map.length; i++) - { - if (!topElem && this._map[i].getBottom() > vr.top) - { - topElem = this._map[i]; - } - - if (this._map[i].getTop() > vr.bottom) - { - botElem = this._map[i]; - break; - } - } - - if (!botElem) - { - botElem = this._map[this._map.length - 1]; - } - - if (topElem) - { - idxTop = getElemIdx.call(this, topElem, vr.top); - idxBottom = getElemIdx.call(this, botElem, vr.bottom); - } - - // Return the calculated index top and bottom - return et2_bounds(idxTop, idxBottom); - }, - - /** - * Returns index range of all currently managed rows. - */ - getIndexRange: function () { - var idxTop = false; - var idxBottom = false; - - for (var i = 0; i < this._map.length; i++) - { - if (!(this._map[i] instanceof et2_dataview_spacer)) - { - var idx = this._map[i].getIndex(); - - if (idxTop === false) - { - idxTop = idx; - } - - idxBottom = idx; - } - } - - return et2_bounds(idxTop, idxBottom); - }, - - /** - * Updates the scrollheight - */ - setScrollHeight: function (_height) { - this._scrollHeight = _height; - - // Update the height of the outer container - if (this.scrollarea) - { - this.scrollarea.height(_height); - } - - // Update the viewing range - this.setViewRange(et2_range(this._viewRange.top, this._scrollHeight)); - }, - - /** - * Returns the average row height data, overrides the corresponding function - * of the et2_dataview_container. - */ - getAvgHeightData: function () { - - if (this._avgHeight === false) - { - var avgCount = 0; - var avgSum = 0; - - for (var i = 0; i < this._map.length; i++) - { - var data = this._map[i].getAvgHeightData(); - - if (data !== null) - { - avgSum += data.avgHeight * data.avgCount; - avgCount += data.avgCount; - } - } - - // Calculate the average height, but only if we have a height - if (avgCount > 0 && avgSum > 0) - { - this._avgHeight = avgSum / avgCount; - this._avgCount = avgCount; - } - } - - // Return the calculated average height if it is available - if (this._avgHeight !== false) - { - return { - "avgCount": this._avgCount, - "avgHeight": this._avgHeight - }; - } - - // Otherwise return the parent average height - if (this._parent) - { - return this._parent.getAvgHeightData(); - } - - // Otherwise return the original average height given in the constructor - if (this._orgAvgHeight !== false) - { - return { - "avgCount": 1, - "avgHeight": this._orgAvgHeight - }; - } - return null; - }, - - /** - * Returns the average row height in pixels. - */ - getAverageHeight: function () { - var data = this.getAvgHeightData(); - return data ? data.avgHeight : 19; - }, - - /** - * Returns the row provider. - */ - getRowProvider: function () { - return this._rowProvider; - }, - - /** - * Called whenever the size of this or another element in the container tree - * changes. - */ - invalidate: function() { - - // Clear any existing "invalidate" timeout - if (this._invalidateTimeout) - { - window.clearTimeout(this._invalidateTimeout); - } - - if(!this.doInvalidate) - { - return; - } - - var self = this; - var _super = this._super; - this._invalidateTimeout = window.setTimeout(function() { - egw.debug("log","Dataview grid timed invalidate"); - // Clear the "_avgHeight" - self._avgHeight = false; - self._avgCount = false; - self._invalidateTimeout = null; - self._doInvalidate(_super); - }, ET2_GRID_INVALIDATE_TIMEOUT); - }, - - /** - * Makes the given index visible: TODO: Propagate this to the parent grid. - */ - makeIndexVisible: function (_idx) - { - // Get the element range - var elemRange = this._getElementRange(_idx); - - // Abort if the index was out of range - if (!elemRange) - { - return false; - } - - // Calculate the current visible range - var visibleRange = et2_bounds( - this._viewRange.top + ET2_GRID_VIEW_EXT, - this._viewRange.bottom - ET2_GRID_VIEW_EXT); - - // Check whether the element is currently completely visible -- if yes, - // do nothing - if (visibleRange.top < elemRange.top - && visibleRange.bottom > elemRange.bottom) - { - return true; - } - - if (elemRange.top < visibleRange.top) - { - this.scrollarea.scrollTop(elemRange.top); - } - else - { - var h = elemRange.bottom - elemRange.top; - this.scrollarea.scrollTop(elemRange.top - this._scrollHeight + h); - } - - }, - - - /* ---- PRIVATE FUNCTIONS ---- */ - -/* _inspectStructuralIntegrity: function() { - var idx = 0; - for (var i = 0; i < this._map.length; i++) - { - if (this._map[i].getIndex() != idx) - { - throw "Index missmatch!"; - } - idx += this._map[i].getCount(); - } - - if (idx !== this._total) - { - throw "Total count missmatch!"; - } - },*/ - - /** - * Translates the given index to a range, returns false if the index is - * out of range. - */ - _getElementRange: function (_idx) - { - // Recalculate the element positions - this._recalculateElementPosition(); - - // Translate the given index to the map index - var mapIdx = this._calculateMapIndex(_idx); - - // Do nothing if the given index is out of range - if (mapIdx === false) - { - return false; - } - - // Get the map element - var elem = this._map[mapIdx]; - - // Get the element range - if (elem instanceof et2_dataview_spacer) - { - var avg = this.getAverageHeight(); - return et2_range(elem.getTop() + avg * (elem.getIndex() - _idx), - avg); - } - - return elem.getRange(); - }, - - /** - * Recalculates the position of the currently managed containers. This - * routine only updates the pixel position of the elements -- the index of - * the elements is guaranteed to be maintained correctly by all high level - * functions of the grid, as the index position is needed to be correct for - * the "deleteRow" and "insertRow" functions, and we cannot effort to call - * this calculation method after every change in the grid mapping. - */ - _recalculateElementPosition: function() { - for (var i = 0; i < this._map.length; i++) - { - if (i == 0) - { - this._map[i].setTop(0); - } - else - { - this._map[i].setTop(this._map[i - 1].getBottom()); - } - } - }, - - /** - * The "_calculateVisibleMappingIndices" function calculates the indices of - * the _map array, which refer to containers that are currently (partially) - * visible. This function is used internally by "_doInvalidate". - */ - _calculateVisibleMappingIndices: function() { - // First update the "top" and "bottom", and "index" values of all - // managed elements, and at the same time calculate the mapping indices - // of the elements which are inside the current view range. - var mapVis = {"top": false, "bottom": false}; - - for (var i = 0; i < this._map.length; i++) - { - // Update the top of the "map visible index" -- set it to the first - // element index, where the bottom line is beneath the top line - // of the view range. - if (mapVis.top === false - && this._map[i].getBottom() > this._viewRange.top) - { - mapVis.top = i; - } - - // Update the bottom of the "map visible index" -- set it to the - // first element index, where the top line is beneath the bottom - // line of the view range. - if (mapVis.bottom === false - && this._map[i].getTop() > this._viewRange.bottom) - { - mapVis.bottom = i; - break; - } - } - - return mapVis; - }, - - /** - * Deletes all elements which are "out of the view range". This function is - * internally used by "_doInvalidate". How many elements that are out of the - * view range get preserved fully depends on the _holdCount parameter - * variable. - * - * @param _mapVis contains the _map indices of the just visible containers. - * @param _holdCount contains the number of elements that should be kept, - * if not given, this parameter defaults to ET2_GRID_HOLD_COUNT - */ - _cleanupOutOfRangeElements: function(_mapVis, _holdCount) { - - // Iterates over the map from and to the given indices and pushes all - // elements onto the given array, which are more than _holdCount - // elements remote from the start. - function searchElements(_arr, _start, _stop, _dir) - { - var dist = 0; - for (var i = _start; _dir > 0 ? i <= _stop : i >= _stop; i += _dir) - { - if (dist > _holdCount) - { - _arr.push(i); - } - else - { - dist += this._map[i].getCount(); - } - } - } - - // Set _holdCount to ET2_GRID_HOLD_COUNT if the parameters is not given - _holdCount = typeof _holdCount === "undefined" ? ET2_GRID_HOLD_COUNT : - _holdCount; - - // Collect all elements that will be deleted at the top and at the - // bottom of the grid - var deleteTop = []; - var deleteBottom = []; - - if (_mapVis.top !== false) - { - searchElements.call(this, deleteTop, _mapVis.top, 0, -1); - } - - if (_mapVis.bottom !== false) - { - searchElements.call(this, deleteBottom, _mapVis.bottom, - this._map.length - 1, 1); - } - - // The offset variable specifies how many elements have been deleted - // from the map -- this variable is needed as deleting elements from the - // map shifts the map indices. We iterate in oposite direction over the - // elements, as this allows the _doDeleteContainer/ container function - // to extend the (possibly) existing spacer at the top of the grid - var offs = 0; - for (var i = deleteTop.length - 1; i >= 0; i--) - { - // Delete the container and calculate the new offset - var mapLength = this._map.length; - this._doDeleteContainer(deleteTop[i] - offs, true); - offs += mapLength - this._map.length; - } - - for (var i = deleteBottom.length - 1; i >= 0; i--) - { - this._doDeleteContainer(deleteBottom[i] - offs, true); - } - - return deleteBottom.length + deleteTop.length > 0; - }, - - /** - * The _updateContainers function is used internally by "_doInvalidate" in - * order to call the "setViewRange" function of all containers the implement - * that interfaces (needed for nested grids), and to request new elements - * for all currently visible spacers. - */ - _updateContainers: function() { - for (var i = 0; i < this._map.length; i++) - { - var container = this._map[i]; - - // Check which type the container object has - var isSpacer = container instanceof et2_dataview_spacer; - var hasIViewRange = !isSpacer - && container.implements(et2_dataview_IViewRange); - - // If the container has one of those special types, calculate the - // view range and use that to update the view range of the element - // or to request new elements for the spacer - if (isSpacer || hasIViewRange) - { - // Calculate the relative view range and check whether - // the element is really visible - var elemRange = container.getRange(); - - // Abort if the element is not inside the visible range - if (!et2_rangeIntersect(this._viewRange, elemRange)) - { - continue; - } - - if (hasIViewRange) - { - // Update the view range of the container - container.setViewRange(et2_bounds( - this._viewRange.top - elemRange.top, - this._viewRange.bottom - elemRange.top)); - } - else // This container is a spacer - { - // Obtain the average element height - var avg = container._rowHeight; - - // Get the visible container range (vcr) - var vcr_top = Math.max(this._viewRange.top, elemRange.top); - var vcr_bot = Math.min(this._viewRange.bottom, elemRange.bottom); - - // Calculate the indices of the elements which will be - // requested - var cidx = container.getIndex(); - var ccnt = container.getCount(); - - // Calculate the start index -- prevent vtop from getting - // negative (and so idxStart being smaller than cidx) and - // ensure that idxStart is not larger than the maximum - // container index. - var vtop = Math.max(0, vcr_top); - var idxStart = Math.floor( - Math.min(cidx + ccnt - 1, - cidx + (vtop - elemRange.top) / avg, - this._total - )); - - // Calculate the end index -- prevent vtop from getting - // negative (and so idxEnd being smaller than cidx) and - // ensure that idxEnd is not larger than the maximum - // container index. - var vbot = Math.max(0, vcr_bot); - var idxEnd = Math.ceil( - Math.min(cidx + ccnt - 1, - cidx + (vbot - elemRange.top) / avg, - this._total - )); - - // Initial resize while the grid is hidden will give NaN - // This is an important optimisation, as it is involved in not - // loading all rows, so we override in that case so - // there are more than the 2-3 that fit in the min height. - if(isNaN(idxStart) && isSpacer) idxStart = cidx-1; - if(isNaN(idxEnd) && isSpacer && this._scrollHeight > 0 && elemRange.bottom == 0) - { - idxEnd = Math.min(ccnt,cidx + Math.ceil( - (this._viewRange.bottom - container._top) / this._orgAvgHeight - )); - } - - // Call the data callback - if (this._callback) - { - var self = this; - egw.debug("log","Dataview grid flag for update: ", {start:idxStart,end:idxEnd}); - window.setTimeout(function() { - // If row template changes, self._callback might disappear - if(typeof self._callback != "undefined") - { - self._callback.call(self._context, idxStart, idxEnd); - } - }, 0); - } - } - } - } - }, - - /** - * Invalidate iterates over the "mapping" array. It calculates which - * containers have to be removed and where new containers should be added. - */ - _doInvalidate: function(_super) { - if(!this.doInvalidate) return; - - // Update the pixel positions - this._recalculateElementPosition(); - - // Call the callback - if (this._invalidateCallback) - { - var range = this.getVisibleIndexRange( - et2_range(this.scrollarea.scrollTop(), this._scrollHeight)); - this._invalidateCallback.call(this._invalidateContext, range); - } - - // Get the visible mapping indices and recalculate index and pixel - // position of the containers. - var mapVis = this._calculateVisibleMappingIndices(); - - // Delete all invisible elements -- if anything changed, we have to - // recalculate the pixel positions again - if (this._cleanupOutOfRangeElements(mapVis)) - { - this._recalculateElementPosition(); - } - - // Update the view range of all visible elements that implement the - // corresponding interface and request elements for all visible spacers - this._updateContainers(); - - // Call the inherited invalidate function, broadcast the invalidation - // through the container tree. - if (this._parent && _super) - { - _super.call(this); - } - }, - - /** - * Translates the given grid index into the element index of the map. If the - * given index is completely out of the range, "false" is returned. - */ - _calculateMapIndex: function(_index) { - - var top = 0; - var bot = this._map.length - 1; - - while (top <= bot) - { - var idx = Math.floor((top + bot) / 2); - var elem = this._map[idx]; - - var realIdx = elem.getIndex(); - var realCnt = elem.getCount(); - - if (_index >= realIdx && _index < realIdx + realCnt) - { - return idx; - } - else if (_index < realIdx) - { - bot = idx - 1; - } - else - { - top = idx + 1; - } - } - - return false; - }, - - _insertContainerAtSpacer: function(_index, _mapIndex, _mapElem, _container, - _avg) { - // Set the index of the new container - _container.setIndex(_index); - - // Calculate at which position the spacer has to be splitted - var splitIdx = _index - _mapElem.getIndex(); - - // Get the count of elements that remain at the top of the splitter - var cntTop = splitIdx; - - // Get the count of elements that remain at the bottom of the splitter - // -- it has to be one element less than before - var cntBottom = _mapElem.getCount() - splitIdx - 1; - - // Split the containers if cntTop and cntBottom are larger than zero - if (cntTop > 0 && cntBottom > 0) - { - // Set the new count of the currently existing container, preserving - // its height as it was - _mapElem.setCount(cntTop); - - // Add the new element after the old container - _container.insertIntoTree(_mapElem.getLastNode()); - - // Create a new spacer and add it after the newly inserted container - var newSpacer = new et2_dataview_spacer(this, - this._rowProvider); - newSpacer.setCount(cntBottom, _avg); - newSpacer.setIndex(_index + 1); - newSpacer.insertIntoTree(_container.getLastNode()); - - // Insert the container and the new spacer into the map - this._map.splice(_mapIndex + 1, 0, _container, newSpacer); - } - else if (cntTop === 0 && cntBottom > 0) - { - // Simply adjust the size of the old spacer and insert the new - // container in front of it - _container.insertIntoTree(_mapElem.getFirstNode(), true); - _mapElem.setIndex(_index + 1); - _mapElem.setCount(cntBottom, _avg); - - this._map.splice(_mapIndex, 0, _container); - } - else if (cntTop > 0 && cntBottom === 0) - { - // Simply add the new container to the end of the old container and - // adjust the count of the old spacer to the remaining count. - _container.insertIntoTree(_mapElem.getLastNode()); - _mapElem.setCount(cntTop); - - this._map.splice(_mapIndex + 1, 0, _container); - } - else // if (cntTop === 0 && cntBottom === 0) - { - // Append the new container to the current container and then - // destroy the old container - _container.insertIntoTree(_mapElem.getLastNode()); - _mapElem.free(); - - this._map.splice(_mapIndex, 1, _container); - } - }, - - _insertContainerAtElement: function(_index, _mapIndex, _mapElem, _container, - _avg) { - // In a first step, simply insert the element at the specified position, - // in front of the element _mapElem. - _container.setIndex(_index); - _container.insertIntoTree(_mapElem.getFirstNode(), true); - this._map.splice(_mapIndex, 0, _container); - - // Search for the next spacer and increment the indices of all other - // elements until there - var _newIndex = _index + 1; - for (var i = _mapIndex + 1; i < this._map.length; i++) - { - // Update the index of the element - this._map[i].setIndex(_newIndex++); - - // We've found a spacer -- decrement its element count and abort - if (this._map[i] instanceof et2_dataview_spacer) - { - this._decrementSpacerCount(i, _avg); - return; - } - } - - // We've found no spacer so far, remove the last element from the map in - // order to obtain the "totalCount" (especially the last element is no - // spacer, so the following code cannot remove a spacer) - this._map.pop().free(); - }, - - /** - * Inserts the given container at the given index. - */ - _doInsertContainer: function(_index, _mapIndex, _container, _avg) { - // Check whether the given element at the map index is a spacer. If - // yes, we have to split the spacer at that position. - var mapElem = this._map[_mapIndex]; - - if (mapElem instanceof et2_dataview_spacer) - { - this._insertContainerAtSpacer(_index, _mapIndex, mapElem, - _container, _avg); - } - else - { - this._insertContainerAtElement(_index, _mapIndex, mapElem, - _container, _avg); - } - -// this._inspectStructuralIntegrity(); - }, - - /** - * Replaces the container at the given index with a spacer. The function - * tries to extend any spacer lying above or below the given _mapIndex. - * This code does not destroy the given container, but maintains its map - * index. - * - * @param _mapIndex is the index of _mapElem in the _map array. - * @param _mapElem is the container which should be replaced. - */ - _replaceContainerWithSpacer: function(_mapIndex, _mapElem) { - // Check whether a spacer can be extended above or below the given - // _mapIndex - var spacerAbove = null; - var spacerBelow = null; - - if (_mapIndex > 0 - && this._map[_mapIndex - 1] instanceof et2_dataview_spacer) - { - spacerAbove = this._map[_mapIndex - 1]; - } - - if (_mapIndex < this._map.length - 1 - && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) - { - spacerBelow = this._map[_mapIndex + 1]; - } - - if (!spacerAbove && !spacerBelow) - { - // No spacer can be extended -- simply create a new one - var spacer = new et2_dataview_spacer(this, this._rowProvider); - spacer.setIndex(_mapElem.getIndex()); - spacer.addAvgHeight(_mapElem.getHeight()); - spacer.setCount(1, _mapElem.getHeight()); - - // Insert the new spacer at the correct place into the DOM tree and - // the mapping - spacer.insertIntoTree(_mapElem.getLastNode()); - this._map.splice(_mapIndex + 1, 0, spacer); - } - else if (spacerAbove && spacerBelow) - { - // We're going to consolidate the upper and the lower spacer. To do - // that we'll calculate a new count of elements and a new average - // height, so that the upper container can get the height of all - // three elements together - var totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight() - + _mapElem.getHeight(); - var totalCount = spacerAbove.getCount() + spacerBelow.getCount() - + 1; - var newAvg = totalHeight / totalCount; - - // Update the upper spacer - spacerAbove.addAvgHeight(_mapElem.getHeight()); - spacerAbove.setCount(totalCount, newAvg); - - // Delete the lower spacer and remove it from the mapping - spacerBelow.free(); - this._map.splice(_mapIndex + 1, 1); - } - else - { - // One of the two spacers is available - var spacer = spacerAbove || spacerBelow; - - // Calculate the new count and the new average height of that spacer - var totalCount = spacer.getCount() + 1; - var totalHeight = spacer.getHeight() + _mapElem.getHeight(); - var newAvg = totalHeight / totalCount; - - // Set the new container height - spacer.setIndex(Math.min(spacer.getIndex(), _mapElem.getIndex())); - spacer.addAvgHeight(_mapElem.getHeight()); - spacer.setCount(totalCount, newAvg); - } - }, - - /** - * Checks whether there is another spacer below the given map index and if - * yes, consolidates the two. - */ - _consolidateSpacers: function(_mapIndex) { - if (_mapIndex < this._map.length - 1 - && this._map[_mapIndex] instanceof et2_dataview_spacer - && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) - { - var spacerAbove = this._map[_mapIndex]; - var spacerBelow = this._map[_mapIndex + 1]; - - // Calculate the new height/count of both containers - var totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight(); - var totalCount = spacerAbove.getCount() + spacerBelow.getCount(); - var newAvg = totalCount / totalHeight; - - // Extend the new spacer - spacerAbove.setCount(totalCount, newAvg); - - // Delete the old spacer - spacerBelow.free(); - this._map.splice(_mapIndex + 1, 1); - } - }, - - /** - * Decrements the count of the spacer at the given _mapIndex by one. If the - * given spacer has no more elements, it will be removed from the mapping. - * Note that this function does not update the indices of the following - * elements, this function is only used internally by the - * _insertContainerAtElement function and the _doDeleteContainer function - * where appropriate adjustments to the map data structure are done. - * - * @param _mapIndex is the index of the spacer in the "map" data structure. - * @param _avg is the new average height of the container, may be - * "undefined" in which case the height of the spacer rows is kept as it - * was. - */ - _decrementSpacerCount: function(_mapIndex, _avg) { - var cnt = this._map[_mapIndex].getCount() - 1; - if (cnt > 0) - { - this._map[_mapIndex].setCount(cnt, _avg); - } - else - { - this._map[_mapIndex].free(); - this._map.splice(_mapIndex, 1); - } - }, - - /** - * Deletes the container at the given index. - */ - _doDeleteContainer: function(_mapIndex, _replaceWithSpacer) { - // _replaceWithSpacer defaults to false - _replaceWithSpacer = _replaceWithSpacer ? _replaceWithSpacer : false; - - // Fetch the element at the given map index - var mapElem = this._map[_mapIndex]; - - // Indicates whether an element has really been removed -- if yes, the - // bottom spacer will be extended - var removedElement = false; - - // Check whether the map element is a spacer -- if yes, we have to do - // some special treatment - if (mapElem instanceof et2_dataview_spacer) - { - // Do nothing if the "_replaceWithSpacer" flag is true as the - // element already is a spacer - if (!_replaceWithSpacer) - { - this._decrementSpacerCount(_mapIndex); - removedElement = true; - } - } - else - { - if (_replaceWithSpacer) - { - this._replaceContainerWithSpacer(_mapIndex, mapElem); - } - else - { - removedElement = true; - } - - // Remove the complete (current) container, decrement the _mapIndex - this._map[_mapIndex].free(); - this._map.splice(_mapIndex, 1); - _mapIndex--; - - // The delete operation may have created two joining spacers -- this - // is highly suboptimal, so we'll consolidate those two spacers - this._consolidateSpacers(_mapIndex); - } - - // Update the indices of all elements after the current one, if we've - // really removed an element - if (removedElement) - { - for (var i = _mapIndex + 1; i < this._map.length; i++) - { - this._map[i].setIndex(this._map[i].getIndex() - 1); - } - - // Extend the last spacer as we have to maintain the spacer count - this._appendEmptyRows(1); - } - -// this._inspectStructuralIntegrity(); - }, - - /** - * The appendEmptyRows function is used internally to append empty rows to - * the end of the table. This functionality is needed in order to maintain - * the "total count" in the _doDeleteContainer function and to increase the - * "total count" in the "setCount" function. - * - * @param _count specifies the count of empty rows that will be added to the - * end of the table. - */ - _appendEmptyRows: function(_count) { - // Special case -- the last element in the "_map" is no spacer -- this - // means, that the "managedRange" is currently at the bottom of the list - // -- so we have to insert a new spacer - var spacer = null; - var lastIndex = this._map.length - 1; - if (this._map.length === 0 || - !(this._map[lastIndex] instanceof et2_dataview_spacer)) - { - // Create a new spacer - spacer = new et2_dataview_spacer(this, this._rowProvider); - - // Insert the spacer -- we have a special case if there currently is - // no element inside the mapping - if (this._map.length === 0) - { - // Add a dummy element to the grid - var dummy = jQuery(document.createElement("tr")); - this.innerTbody.append(dummy); - - // Append the spacer to the grid - spacer.setIndex(0); - spacer.insertIntoTree(dummy, false); - - // Remove the dummy element - dummy.remove(); - } - else - { - // Insert the new spacer after the last element - spacer.setIndex(this._map[lastIndex].getIndex() + 1); - spacer.insertIntoTree(this._map[lastIndex].getLastNode()); - } - - // Add the new spacer to the mapping - this._map.push(spacer); - } - else - { - // Get the spacer at the bottom of the mapping - spacer = this._map[lastIndex]; - } - - // Update the spacer count - spacer.setCount(_count + spacer.getCount(), this.getAverageHeight()); - }, - - /** - * The _decreaseTotal function is used to decrease the total row count in - * the grid. It tries to remove the given count of rows from the spacer - * located at the bottom of the grid, if this is not possible, it starts - * removing complete rows. - * - * @param _delta specifies how many rows should be removed. - */ - _decreaseTotal: function(_delta) { - // Iterate over the current mapping, starting at the bottom and delete - // rows. _delta is decreased for each removed row. Abort when delta is - // zero or the map is empty - while (_delta > 0 && this._map.length > 0) - { - var cont = this._map[this._map.length - 1]; - - // Remove as many containers as possible from spacers - if (cont instanceof et2_dataview_spacer) - { - var diff = cont.getCount() - _delta; - - if (diff > 0) - { - // We're done as the spacer still has entries left - _delta = 0; - cont.setCount(diff, this.getAverageHeight()); - break; - } - else - { - // Decrease _delta by the count of rows the spacer had - _delta -= diff + _delta; - } - } - else - { - // We're going to remove a single row: remove it - _delta -= 1; - } - - // Destroy the container if there are no rows left - cont.free(); - this._map.pop(); - } - - // Check whether _delta is really zero - if (_delta > 0) - { - this.egw.debug('error', "Error while decreasing the total count - requested to remove more rows than available."); - } - }, - - /** - * Creates the grid DOM-Nodes - */ - _createNodes: function() { - - this.tr = jQuery(document.createElement("tr")); - - this.outerCell = jQuery(document.createElement("td")) - .addClass("frame") - .attr("colspan", this._rowProvider.getColumnCount() - + (this._parentGrid ? 0 : 1)) - .appendTo(this.tr); - - // Create the scrollarea div if this is the outer grid - this.scrollarea = null; - if (this._parentGrid == null) - { - this.scrollarea = jQuery(document.createElement("div")) - .addClass("egwGridView_scrollarea") - .scroll(this, function(e) { - - // Clear any older scroll timeout - if (e.data._scrollTimeout) - { - window.clearTimeout(e.data._scrollTimeout); - } - - // Clear any existing "invalidate" timeout (as the - // "setViewRange" function triggered by the scroll event - // forces an "invalidate"). - if (e.data._invalidateTimeout) - { - window.clearTimeout(e.data._invalidateTimeout); - e.data._invalidateTimeout = null; - } - - // Set a new timeout which calls the setViewArea - // function - e.data._scrollTimeout = window.setTimeout(jQuery.proxy(function() { - var newRange = et2_range( - this.scrollarea.scrollTop() - ET2_GRID_VIEW_EXT, - this._scrollHeight + ET2_GRID_VIEW_EXT * 2 - ); - - if (!et2_rangeEqual(newRange, this._viewRange)) - { - this.setViewRange(newRange); - } - },e.data), ET2_GRID_SCROLL_TIMEOUT); - }) - .height(this._scrollHeight) - .appendTo(this.outerCell); - } - - // Create the inner table - var table = jQuery(document.createElement("table")) - .addClass("egwGridView_grid") - .appendTo(this.scrollarea ? this.scrollarea : this.outerCell); - - this.innerTbody = jQuery(document.createElement("tbody")) - .appendTo(table); - - // Set the tr as container element - this.appendNode(jQuery(this.tr[0])); - } - -});}).call(this); - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var et2_dataview_view_container_1 = require("./et2_dataview_view_container"); +var et2_dataview_grid = /** @class */ (function (_super_1) { + __extends(et2_dataview_grid, _super_1); + /** + * Creates the grid. + * + * @param _parent is the parent grid class - if null, this means that this + * is the outer grid which manages the scrollarea. If not null, all other + * parameters are ignored and copied from the given grid instance. + * @param _parentGrid + * @param _egw + * @param _rowProvider + * @param _avgHeight is the starting average height of the column rows. + * @memberOf et2_dataview_grid + */ + function et2_dataview_grid(_parent, _parentGrid, _egw, _rowProvider, _avgHeight) { + var _this = + // Call the inherited constructor + _super_1.call(this, _parent) || this; + // If the parent is given, copy all other parameters from it + if (_parentGrid != null) { + _this.egw = _parent.egw; + _this._orgAvgHeight = false; + _this._rowProvider = _parentGrid._rowProvider; + } + else { + // Otherwise copy the given parameters + _this.egw = _egw; + _this._orgAvgHeight = _avgHeight; + _this._rowProvider = _rowProvider; + // As this grid instance has no parent, we need a scroll container + _this._scrollHeight = 0; + _this._scrollTimeout = null; + } + _this._parentGrid = _parentGrid; + _this._scrollTimeout = null; + _this._invalidateTimeout = null; + _this._invalidateCallback = null; + _this._invalidateContext = null; + // Flag for stopping invalidate while working + _this.doInvalidate = true; + // _map contains a mapping between the grid indices and the elements + // associated to it. The first element in the array always refers to the + // element starting at index zero (being a spacer if the grid currently + // displays another range). + _this._map = []; + // _viewRange contains the current pixel-range of the grid which is + // visible. + _this._viewRange = et2_range(0, 0); + // Holds the maximum count of elements + _this._total = 0; + // Holds data used for storing the current average height data + _this._avgHeight = false; + _this._avgCount = -1; + // Build the outer grid nodes + _this._createNodes(); + return _this; + } + et2_dataview_grid.prototype.destroy = function () { + // Destroy all containers + this.setTotalCount(0); + // Stop the scroll timeout + if (this._scrollTimeout) { + window.clearTimeout(this._scrollTimeout); + } + // Stop the invalidate timeout + if (this._invalidateTimeout) { + window.clearTimeout(this._invalidateTimeout); + } + _super_1.prototype.destroy.call(this); + }; + et2_dataview_grid.prototype.clear = function () { + // Store the old total count and rescue the current average height in + // form of the "original average height" + var oldTotalCount = this._total; + this._orgAvgHeight = this.getAverageHeight(); + // Set the total count to zero + this.setTotalCount(0); + // Reset the total count value + this.setTotalCount(oldTotalCount); + }; + /** + * Throws all elements away which are outside the current view range + */ + et2_dataview_grid.prototype.cleanup = function () { + // Update the pixel positions + this._recalculateElementPosition(); + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + var mapVis = this._calculateVisibleMappingIndices(); + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + this._cleanupOutOfRangeElements(mapVis, 0); + }; + /** + * The insertRow function can be called to insert the given container(s) at + * the given row index. If there currently is another container at that + * given position, the new container(s) will be inserted above the old + * container. Yet the "total count" of the grid will be preserved by + * removing the correct count of elements from the next possible spacer. If + * no spacer is found, the last containers will be removed. This causes + * inserting new containers at the end of a grid to be immediately removed + * again. + * + * @param _index is the row index at which the given container(s) should be + * inserted. + * @param _container is eiter a single et2_dataview_container instance + * which should be inserted at the given position. Or an array of + * et2_dataview_container instances. If you want to remove the container + * don't do that manually by calling its "destroy" function but use the + * deleteRow function. + */ + et2_dataview_grid.prototype.insertRow = function (_index, _container) { + // Calculate the map element the given index refers to + var idx = this._calculateMapIndex(_index); + if (idx !== false) { + // Wrap the container inside an array + if (_container instanceof et2_dataview_view_container_1.et2_dataview_container) { + _container = [_container]; + } + // Fetch the average height + var avg = this.getAverageHeight(); + // Call the internal _doInsertContainer function + for (var i = 0; i < _container.length; i++) { + this._doInsertContainer(_index, idx, _container[i], avg); + } + // Schedule an "invalidate" event + this.invalidate(); + } + }; + /** + * The deleteRow function can be used to remove the element at the given + * index. + * + * @param _index is the index from which should be deleted. If the given + * index is outside the so called "managedRange" nothing will happen, as the + * container has already been destroyed by the grid instance. + */ + et2_dataview_grid.prototype.deleteRow = function (_index) { + // Calculate the map element the given index refers to + var idx = this._calculateMapIndex(_index); + if (idx !== false) { + this._doDeleteContainer(idx, false); + // Schedule an "invalidate" event + this.invalidate(); + } + }; + /** + * The given callback gets called whenever the scroll position changed or + * the visible element range changed. The element indices are passed to the + * function as et2_range. + */ + et2_dataview_grid.prototype.setInvalidateCallback = function (_callback, _context) { + this._invalidateCallback = _callback; + this._invalidateContext = _context; + }; + /** + * The setDataCallback function is used to set the callback that will be + * called when the grid requires new data. + * + * @param _callback is the callback function which gets called when the grid + * needs some new rows. + * @param _context is the context in which the callback function gets + * called. + */ + et2_dataview_grid.prototype.setDataCallback = function (_callback, _context) { + this._callback = _callback; + this._context = _context; + }; + /** + * The updateTotalCount function can be used to update the total count of + * rows that are displayed inside the grid. Changing the count always causes + * the spacer at the bottom (if it exists) to be + * + * @param _count specifies how many entries the grid can show. + */ + et2_dataview_grid.prototype.setTotalCount = function (_count) { + // Abort if the total count has not changed + if (_count === this._total) + return; + // Calculate how many elements have to be removed/added + var delta = Math.max(0, _count) - this._total; + if (delta > 0) { + this._appendEmptyRows(delta); + } + else { + this._decreaseTotal(-delta); + } + this._total = Math.max(0, _count); + // Schedule an invalidate + this.invalidate(); + }; + /** + * Returns the current "total" count. + */ + et2_dataview_grid.prototype.getTotalCount = function () { + return this._total; + }; + /** + * The setViewRange function updates the range in which rows are shown. + */ + et2_dataview_grid.prototype.setViewRange = function (_range) { + // Set the new view range + this._viewRange = _range; + // Immediately call the "invalidate" function + this._doInvalidate(); + }; + /** + * Return the indices of the currently visible rows. + */ + et2_dataview_grid.prototype.getVisibleIndexRange = function (_viewRange) { + function getElemIdx(_elem, _px) { + if (_elem instanceof et2_dataview_spacer) { + return _elem.getIndex() + + Math.floor((_px - _elem.getTop()) / this.getAverageHeight()); + } + return _elem.getIndex(); + } + var idxTop = 0; + var idxBottom = 0; + var vr; + if (_viewRange) { + vr = _viewRange; + } + else { + // Calculate the "correct" view range by removing ET2_GRID_VIEW_EXT + vr = et2_bounds(this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT); + } + // Get the elements at the top and the bottom of the view + var topElem = null; + var botElem = null; + for (var i = 0; i < this._map.length; i++) { + if (!topElem && this._map[i].getBottom() > vr.top) { + topElem = this._map[i]; + } + if (this._map[i].getTop() > vr.bottom) { + botElem = this._map[i]; + break; + } + } + if (!botElem) { + botElem = this._map[this._map.length - 1]; + } + if (topElem) { + idxTop = getElemIdx.call(this, topElem, vr.top); + idxBottom = getElemIdx.call(this, botElem, vr.bottom); + } + // Return the calculated index top and bottom + return et2_bounds(idxTop, idxBottom); + }; + /** + * Returns index range of all currently managed rows. + */ + et2_dataview_grid.prototype.getIndexRange = function () { + var idxTop = false; + var idxBottom = false; + for (var i = 0; i < this._map.length; i++) { + if (!(this._map[i] instanceof et2_dataview_spacer)) { + var idx = this._map[i].getIndex(); + if (idxTop === false) { + idxTop = idx; + } + idxBottom = idx; + } + } + return et2_bounds(idxTop, idxBottom); + }; + /** + * Updates the scrollheight + */ + et2_dataview_grid.prototype.setScrollHeight = function (_height) { + this._scrollHeight = _height; + // Update the height of the outer container + if (this.scrollarea) { + this.scrollarea.height(_height); + } + // Update the viewing range + this.setViewRange(et2_range(this._viewRange.top, this._scrollHeight)); + }; + /** + * Returns the average row height data, overrides the corresponding function + * of the et2_dataview_container. + */ + et2_dataview_grid.prototype.getAvgHeightData = function () { + if (this._avgHeight === false) { + var avgCount = 0; + var avgSum = 0; + for (var i = 0; i < this._map.length; i++) { + var data = this._map[i].getAvgHeightData(); + if (data !== null) { + avgSum += data.avgHeight * data.avgCount; + avgCount += data.avgCount; + } + } + // Calculate the average height, but only if we have a height + if (avgCount > 0 && avgSum > 0) { + this._avgHeight = avgSum / avgCount; + this._avgCount = avgCount; + } + } + // Return the calculated average height if it is available + if (this._avgHeight !== false) { + return { + "avgCount": this._avgCount, + "avgHeight": this._avgHeight + }; + } + // Otherwise return the parent average height + if (this._parent) { + return this._parent.getAvgHeightData(); + } + // Otherwise return the original average height given in the constructor + if (this._orgAvgHeight !== false) { + return { + "avgCount": 1, + "avgHeight": this._orgAvgHeight + }; + } + return null; + }; + /** + * Returns the average row height in pixels. + */ + et2_dataview_grid.prototype.getAverageHeight = function () { + var data = this.getAvgHeightData(); + return data ? data.avgHeight : 19; + }; + /** + * Returns the row provider. + */ + et2_dataview_grid.prototype.getRowProvider = function () { + return this._rowProvider; + }; + /** + * Called whenever the size of this or another element in the container tree + * changes. + */ + et2_dataview_grid.prototype.invalidate = function () { + // Clear any existing "invalidate" timeout + if (this._invalidateTimeout) { + window.clearTimeout(this._invalidateTimeout); + } + if (!this.doInvalidate) { + return; + } + var self = this; + var _super = _super_1.prototype.invalidate.call(this); + this._invalidateTimeout = window.setTimeout(function () { + egw.debug("log", "Dataview grid timed invalidate"); + // Clear the "_avgHeight" + self._avgHeight = false; + self._avgCount = -1; + self._invalidateTimeout = null; + self._doInvalidate(_super); + }, et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + }; + /** + * Makes the given index visible: TODO: Propagate this to the parent grid. + */ + et2_dataview_grid.prototype.makeIndexVisible = function (_idx) { + // Get the element range + var elemRange = this._getElementRange(_idx); + // Abort if the index was out of range + if (!elemRange) { + return false; + } + // Calculate the current visible range + var visibleRange = et2_bounds(this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT); + // Check whether the element is currently completely visible -- if yes, + // do nothing + if (visibleRange.top < elemRange.top + && visibleRange.bottom > elemRange.bottom) { + return true; + } + if (elemRange.top < visibleRange.top) { + this.scrollarea.scrollTop(elemRange.top); + } + else { + var h = elemRange.bottom - elemRange.top; + this.scrollarea.scrollTop(elemRange.top - this._scrollHeight + h); + } + }; + /* ---- PRIVATE FUNCTIONS ---- */ + /* _inspectStructuralIntegrity: function() { + var idx = 0; + for (var i = 0; i < this._map.length; i++) + { + if (this._map[i].getIndex() != idx) + { + throw "Index missmatch!"; + } + idx += this._map[i].getCount(); + } + + if (idx !== this._total) + { + throw "Total count missmatch!"; + } + },*/ + /** + * Translates the given index to a range, returns false if the index is + * out of range. + */ + et2_dataview_grid.prototype._getElementRange = function (_idx) { + // Recalculate the element positions + this._recalculateElementPosition(); + // Translate the given index to the map index + var mapIdx = this._calculateMapIndex(_idx); + // Do nothing if the given index is out of range + if (mapIdx === false) { + return false; + } + // Get the map element + var elem = this._map[mapIdx]; + // Get the element range + if (elem instanceof et2_dataview_spacer) { + var avg = this.getAverageHeight(); + return et2_range(elem.getTop() + avg * (elem.getIndex() - _idx), avg); + } + return elem.getRange(); + }; + /** + * Recalculates the position of the currently managed containers. This + * routine only updates the pixel position of the elements -- the index of + * the elements is guaranteed to be maintained correctly by all high level + * functions of the grid, as the index position is needed to be correct for + * the "deleteRow" and "insertRow" functions, and we cannot effort to call + * this calculation method after every change in the grid mapping. + */ + et2_dataview_grid.prototype._recalculateElementPosition = function () { + for (var i = 0; i < this._map.length; i++) { + if (i == 0) { + this._map[i].setTop(0); + } + else { + this._map[i].setTop(this._map[i - 1].getBottom()); + } + } + }; + /** + * The "_calculateVisibleMappingIndices" function calculates the indices of + * the _map array, which refer to containers that are currently (partially) + * visible. This function is used internally by "_doInvalidate". + */ + et2_dataview_grid.prototype._calculateVisibleMappingIndices = function () { + // First update the "top" and "bottom", and "index" values of all + // managed elements, and at the same time calculate the mapping indices + // of the elements which are inside the current view range. + var mapVis = { "top": -1, "bottom": -1 }; + for (var i = 0; i < this._map.length; i++) { + // Update the top of the "map visible index" -- set it to the first + // element index, where the bottom line is beneath the top line + // of the view range. + if (mapVis.top === -1 + && this._map[i].getBottom() > this._viewRange.top) { + mapVis.top = i; + } + // Update the bottom of the "map visible index" -- set it to the + // first element index, where the top line is beneath the bottom + // line of the view range. + if (mapVis.bottom === -1 + && this._map[i].getTop() > this._viewRange.bottom) { + mapVis.bottom = i; + break; + } + } + return mapVis; + }; + /** + * Deletes all elements which are "out of the view range". This function is + * internally used by "_doInvalidate". How many elements that are out of the + * view range get preserved fully depends on the _holdCount parameter + * variable. + * + * @param _mapVis contains the _map indices of the just visible containers. + * @param _holdCount contains the number of elements that should be kept, + * if not given, this parameter defaults to ET2_GRID_HOLD_COUNT + */ + et2_dataview_grid.prototype._cleanupOutOfRangeElements = function (_mapVis, _holdCount) { + // Iterates over the map from and to the given indices and pushes all + // elements onto the given array, which are more than _holdCount + // elements remote from the start. + function searchElements(_arr, _start, _stop, _dir) { + var dist = 0; + for (var i_1 = _start; _dir > 0 ? i_1 <= _stop : i_1 >= _stop; i_1 += _dir) { + if (dist > _holdCount) { + _arr.push(i_1); + } + else { + dist += this._map[i_1].getCount(); + } + } + } + // Set _holdCount to ET2_GRID_HOLD_COUNT if the parameters is not given + _holdCount = typeof _holdCount === "undefined" ? et2_dataview_grid.ET2_GRID_HOLD_COUNT : + _holdCount; + // Collect all elements that will be deleted at the top and at the + // bottom of the grid + var deleteTop = []; + var deleteBottom = []; + if (_mapVis.top !== -1) { + searchElements.call(this, deleteTop, _mapVis.top, 0, -1); + } + if (_mapVis.bottom !== -1) { + searchElements.call(this, deleteBottom, _mapVis.bottom, this._map.length - 1, 1); + } + // The offset variable specifies how many elements have been deleted + // from the map -- this variable is needed as deleting elements from the + // map shifts the map indices. We iterate in oposite direction over the + // elements, as this allows the _doDeleteContainer/ container function + // to extend the (possibly) existing spacer at the top of the grid + var offs = 0; + for (var i = deleteTop.length - 1; i >= 0; i--) { + // Delete the container and calculate the new offset + var mapLength = this._map.length; + this._doDeleteContainer(deleteTop[i] - offs, true); + offs += mapLength - this._map.length; + } + for (var i = deleteBottom.length - 1; i >= 0; i--) { + this._doDeleteContainer(deleteBottom[i] - offs, true); + } + return deleteBottom.length + deleteTop.length > 0; + }; + /** + * The _updateContainers function is used internally by "_doInvalidate" in + * order to call the "setViewRange" function of all containers the implement + * that interfaces (needed for nested grids), and to request new elements + * for all currently visible spacers. + */ + et2_dataview_grid.prototype._updateContainers = function () { + var _loop_1 = function (i) { + var container = this_1._map[i]; + // Check which type the container object has + var isSpacer = container instanceof et2_dataview_spacer; + var hasIViewRange = !isSpacer + && container.implements(et2_dataview_IViewRange); + // If the container has one of those special types, calculate the + // view range and use that to update the view range of the element + // or to request new elements for the spacer + if (isSpacer || hasIViewRange) { + // Calculate the relative view range and check whether + // the element is really visible + var elemRange = container.getRange(); + // Abort if the element is not inside the visible range + if (!et2_rangeIntersect(this_1._viewRange, elemRange)) { + return "continue"; + } + if (hasIViewRange) { + // Update the view range of the container + container.setViewRange(et2_bounds(this_1._viewRange.top - elemRange.top, this_1._viewRange.bottom - elemRange.top)); + } + else // This container is a spacer + { + // Obtain the average element height + var avg = container._rowHeight; + // Get the visible container range (vcr) + var vcr_top = Math.max(this_1._viewRange.top, elemRange.top); + var vcr_bot = Math.min(this_1._viewRange.bottom, elemRange.bottom); + // Calculate the indices of the elements which will be + // requested + var cidx = container.getIndex(); + var ccnt = container.getCount(); + // Calculate the start index -- prevent vtop from getting + // negative (and so idxStart being smaller than cidx) and + // ensure that idxStart is not larger than the maximum + // container index. + var vtop = Math.max(0, vcr_top); + var idxStart_1 = Math.floor(Math.min(cidx + ccnt - 1, cidx + (vtop - elemRange.top) / avg, this_1._total)); + // Calculate the end index -- prevent vtop from getting + // negative (and so idxEnd being smaller than cidx) and + // ensure that idxEnd is not larger than the maximum + // container index. + var vbot = Math.max(0, vcr_bot); + var idxEnd_1 = Math.ceil(Math.min(cidx + ccnt - 1, cidx + (vbot - elemRange.top) / avg, this_1._total)); + // Initial resize while the grid is hidden will give NaN + // This is an important optimisation, as it is involved in not + // loading all rows, so we override in that case so + // there are more than the 2-3 that fit in the min height. + if (isNaN(idxStart_1) && isSpacer) + idxStart_1 = cidx - 1; + if (isNaN(idxEnd_1) && isSpacer && this_1._scrollHeight > 0 && elemRange.bottom == 0) { + idxEnd_1 = Math.min(ccnt, cidx + Math.ceil((this_1._viewRange.bottom - container._top) / (this_1._orgAvgHeight || 0))); + } + // Call the data callback + if (this_1._callback) { + var self_1 = this_1; + egw.debug("log", "Dataview grid flag for update: ", { start: idxStart_1, end: idxEnd_1 }); + window.setTimeout(function () { + // If row template changes, self._callback might disappear + if (typeof self_1._callback != "undefined") { + self_1._callback.call(self_1._context, idxStart_1, idxEnd_1); + } + }, 0); + } + } + } + }; + var this_1 = this; + for (var i = 0; i < this._map.length; i++) { + _loop_1(i); + } + }; + /** + * Invalidate iterates over the "mapping" array. It calculates which + * containers have to be removed and where new containers should be added. + */ + et2_dataview_grid.prototype._doInvalidate = function (_super) { + if (!this.doInvalidate) + return; + // Update the pixel positions + this._recalculateElementPosition(); + // Call the callback + if (this._invalidateCallback) { + var range = this.getVisibleIndexRange(et2_range(this.scrollarea.scrollTop(), this._scrollHeight)); + this._invalidateCallback.call(this._invalidateContext, range); + } + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + var mapVis = this._calculateVisibleMappingIndices(); + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + if (this._cleanupOutOfRangeElements(mapVis)) { + this._recalculateElementPosition(); + } + // Update the view range of all visible elements that implement the + // corresponding interface and request elements for all visible spacers + this._updateContainers(); + // Call the inherited invalidate function, broadcast the invalidation + // through the container tree. + if (this._parent && _super) { + _super._doInvalidate(); + } + }; + /** + * Translates the given grid index into the element index of the map. If the + * given index is completely out of the range, "false" is returned. + */ + et2_dataview_grid.prototype._calculateMapIndex = function (_index) { + var top = 0; + var bot = this._map.length - 1; + while (top <= bot) { + var idx = Math.floor((top + bot) / 2); + var elem = this._map[idx]; + var realIdx = elem.getIndex(); + var realCnt = elem.getCount(); + if (_index >= realIdx && _index < realIdx + realCnt) { + return idx; + } + else if (_index < realIdx) { + bot = idx - 1; + } + else { + top = idx + 1; + } + } + return false; + }; + et2_dataview_grid.prototype._insertContainerAtSpacer = function (_index, _mapIndex, _mapElem, _container, _avg) { + // Set the index of the new container + _container.setIndex(_index); + // Calculate at which position the spacer has to be splitted + var splitIdx = _index - _mapElem.getIndex(); + // Get the count of elements that remain at the top of the splitter + var cntTop = splitIdx; + // Get the count of elements that remain at the bottom of the splitter + // -- it has to be one element less than before + var cntBottom = _mapElem.getCount() - splitIdx - 1; + // Split the containers if cntTop and cntBottom are larger than zero + if (cntTop > 0 && cntBottom > 0) { + // Set the new count of the currently existing container, preserving + // its height as it was + _mapElem.setCount(cntTop); + // Add the new element after the old container + _container.insertIntoTree(_mapElem.getLastNode()); + // Create a new spacer and add it after the newly inserted container + var newSpacer = new et2_dataview_spacer(this, this._rowProvider); + newSpacer.setCount(cntBottom, _avg); + newSpacer.setIndex(_index + 1); + newSpacer.insertIntoTree(_container.getLastNode()); + // Insert the container and the new spacer into the map + this._map.splice(_mapIndex + 1, 0, _container, newSpacer); + } + else if (cntTop === 0 && cntBottom > 0) { + // Simply adjust the size of the old spacer and insert the new + // container in front of it + _container.insertIntoTree(_mapElem.getFirstNode(), true); + _mapElem.setIndex(_index + 1); + _mapElem.setCount(cntBottom, _avg); + this._map.splice(_mapIndex, 0, _container); + } + else if (cntTop > 0 && cntBottom === 0) { + // Simply add the new container to the end of the old container and + // adjust the count of the old spacer to the remaining count. + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.setCount(cntTop); + this._map.splice(_mapIndex + 1, 0, _container); + } + else // if (cntTop === 0 && cntBottom === 0) + { + // Append the new container to the current container and then + // destroy the old container + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.free(); + this._map.splice(_mapIndex, 1, _container); + } + }; + et2_dataview_grid.prototype._insertContainerAtElement = function (_index, _mapIndex, _mapElem, _container, _avg) { + // In a first step, simply insert the element at the specified position, + // in front of the element _mapElem. + _container.setIndex(_index); + _container.insertIntoTree(_mapElem.getFirstNode(), true); + this._map.splice(_mapIndex, 0, _container); + // Search for the next spacer and increment the indices of all other + // elements until there + var _newIndex = _index + 1; + for (var i = _mapIndex + 1; i < this._map.length; i++) { + // Update the index of the element + this._map[i].setIndex(_newIndex++); + // We've found a spacer -- decrement its element count and abort + if (this._map[i] instanceof et2_dataview_spacer) { + this._decrementSpacerCount(i, _avg); + return; + } + } + // We've found no spacer so far, remove the last element from the map in + // order to obtain the "totalCount" (especially the last element is no + // spacer, so the following code cannot remove a spacer) + this._map.pop().free(); + }; + /** + * Inserts the given container at the given index. + */ + et2_dataview_grid.prototype._doInsertContainer = function (_index, _mapIndex, _container, _avg) { + // Check whether the given element at the map index is a spacer. If + // yes, we have to split the spacer at that position. + var mapElem = this._map[_mapIndex]; + if (mapElem instanceof et2_dataview_spacer) { + this._insertContainerAtSpacer(_index, _mapIndex, mapElem, _container, _avg); + } + else { + this._insertContainerAtElement(_index, _mapIndex, mapElem, _container, _avg); + } + }; + /** + * Replaces the container at the given index with a spacer. The function + * tries to extend any spacer lying above or below the given _mapIndex. + * This code does not destroy the given container, but maintains its map + * index. + * + * @param _mapIndex is the index of _mapElem in the _map array. + * @param _mapElem is the container which should be replaced. + */ + et2_dataview_grid.prototype._replaceContainerWithSpacer = function (_mapIndex, _mapElem) { + var newAvg; + var spacer; + var totalHeight; + var totalCount; + // Check whether a spacer can be extended above or below the given + // _mapIndex + var spacerAbove = null; + var spacerBelow = null; + if (_mapIndex > 0 + && this._map[_mapIndex - 1] instanceof et2_dataview_spacer) { + spacerAbove = this._map[_mapIndex - 1]; + } + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) { + spacerBelow = this._map[_mapIndex + 1]; + } + if (!spacerAbove && !spacerBelow) { + // No spacer can be extended -- simply create a new one + spacer = new et2_dataview_spacer(this, this._rowProvider); + spacer.setIndex(_mapElem.getIndex()); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(1, _mapElem.getHeight()); + // Insert the new spacer at the correct place into the DOM tree and + // the mapping + spacer.insertIntoTree(_mapElem.getLastNode()); + this._map.splice(_mapIndex + 1, 0, spacer); + } + else if (spacerAbove && spacerBelow) { + // We're going to consolidate the upper and the lower spacer. To do + // that we'll calculate a new count of elements and a new average + // height, so that the upper container can get the height of all + // three elements together + totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight() + + _mapElem.getHeight(); + totalCount = spacerAbove.getCount() + spacerBelow.getCount() + + 1; + newAvg = totalHeight / totalCount; + // Update the upper spacer + spacerAbove.addAvgHeight(_mapElem.getHeight()); + spacerAbove.setCount(totalCount, newAvg); + // Delete the lower spacer and remove it from the mapping + spacerBelow.free(); + this._map.splice(_mapIndex + 1, 1); + } + else { + // One of the two spacers is available + spacer = spacerAbove || spacerBelow; + // Calculate the new count and the new average height of that spacer + totalCount = spacer.getCount() + 1; + totalHeight = spacer.getHeight() + _mapElem.getHeight(); + newAvg = totalHeight / totalCount; + // Set the new container height + spacer.setIndex(Math.min(spacer.getIndex(), _mapElem.getIndex())); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(totalCount, newAvg); + } + }; + /** + * Checks whether there is another spacer below the given map index and if + * yes, consolidates the two. + */ + et2_dataview_grid.prototype._consolidateSpacers = function (_mapIndex) { + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex] instanceof et2_dataview_spacer + && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) { + var spacerAbove = this._map[_mapIndex]; + var spacerBelow = this._map[_mapIndex + 1]; + // Calculate the new height/count of both containers + var totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight(); + var totalCount = spacerAbove.getCount() + spacerBelow.getCount(); + var newAvg = totalCount / totalHeight; + // Extend the new spacer + spacerAbove.setCount(totalCount, newAvg); + // Delete the old spacer + spacerBelow.free(); + this._map.splice(_mapIndex + 1, 1); + } + }; + /** + * Decrements the count of the spacer at the given _mapIndex by one. If the + * given spacer has no more elements, it will be removed from the mapping. + * Note that this function does not update the indices of the following + * elements, this function is only used internally by the + * _insertContainerAtElement function and the _doDeleteContainer function + * where appropriate adjustments to the map data structure are done. + * + * @param _mapIndex is the index of the spacer in the "map" data structure. + * @param _avg is the new average height of the container, may be + * "undefined" in which case the height of the spacer rows is kept as it + * was. + */ + et2_dataview_grid.prototype._decrementSpacerCount = function (_mapIndex, _avg) { + var cnt = this._map[_mapIndex].getCount() - 1; + if (cnt > 0) { + this._map[_mapIndex].setCount(cnt, _avg); + } + else { + this._map[_mapIndex].free(); + this._map.splice(_mapIndex, 1); + } + }; + /** + * Deletes the container at the given index. + */ + et2_dataview_grid.prototype._doDeleteContainer = function (_mapIndex, _replaceWithSpacer) { + // _replaceWithSpacer defaults to false + _replaceWithSpacer = _replaceWithSpacer ? _replaceWithSpacer : false; + // Fetch the element at the given map index + var mapElem = this._map[_mapIndex]; + // Indicates whether an element has really been removed -- if yes, the + // bottom spacer will be extended + var removedElement = false; + // Check whether the map element is a spacer -- if yes, we have to do + // some special treatment + if (mapElem instanceof et2_dataview_spacer) { + // Do nothing if the "_replaceWithSpacer" flag is true as the + // element already is a spacer + if (!_replaceWithSpacer) { + this._decrementSpacerCount(_mapIndex); + removedElement = true; + } + } + else { + if (_replaceWithSpacer) { + this._replaceContainerWithSpacer(_mapIndex, mapElem); + } + else { + removedElement = true; + } + // Remove the complete (current) container, decrement the _mapIndex + this._map[_mapIndex].free(); + this._map.splice(_mapIndex, 1); + _mapIndex--; + // The delete operation may have created two joining spacers -- this + // is highly suboptimal, so we'll consolidate those two spacers + this._consolidateSpacers(_mapIndex); + } + // Update the indices of all elements after the current one, if we've + // really removed an element + if (removedElement) { + for (var i = _mapIndex + 1; i < this._map.length; i++) { + this._map[i].setIndex(this._map[i].getIndex() - 1); + } + // Extend the last spacer as we have to maintain the spacer count + this._appendEmptyRows(1); + } + }; + /** + * The appendEmptyRows function is used internally to append empty rows to + * the end of the table. This functionality is needed in order to maintain + * the "total count" in the _doDeleteContainer function and to increase the + * "total count" in the "setCount" function. + * + * @param _count specifies the count of empty rows that will be added to the + * end of the table. + */ + et2_dataview_grid.prototype._appendEmptyRows = function (_count) { + // Special case -- the last element in the "_map" is no spacer -- this + // means, that the "managedRange" is currently at the bottom of the list + // -- so we have to insert a new spacer + var spacer = null; + var lastIndex = this._map.length - 1; + if (this._map.length === 0 || + !(this._map[lastIndex] instanceof et2_dataview_spacer)) { + // Create a new spacer + spacer = new et2_dataview_spacer(this, this._rowProvider); + // Insert the spacer -- we have a special case if there currently is + // no element inside the mapping + if (this._map.length === 0) { + // Add a dummy element to the grid + var dummy = jQuery(document.createElement("tr")); + this.innerTbody.append(dummy); + // Append the spacer to the grid + spacer.setIndex(0); + spacer.insertIntoTree(dummy, false); + // Remove the dummy element + dummy.remove(); + } + else { + // Insert the new spacer after the last element + spacer.setIndex(this._map[lastIndex].getIndex() + 1); + spacer.insertIntoTree(this._map[lastIndex].getLastNode()); + } + // Add the new spacer to the mapping + this._map.push(spacer); + } + else { + // Get the spacer at the bottom of the mapping + spacer = this._map[lastIndex]; + } + // Update the spacer count + spacer.setCount(_count + spacer.getCount(), this.getAverageHeight()); + }; + /** + * The _decreaseTotal function is used to decrease the total row count in + * the grid. It tries to remove the given count of rows from the spacer + * located at the bottom of the grid, if this is not possible, it starts + * removing complete rows. + * + * @param _delta specifies how many rows should be removed. + */ + et2_dataview_grid.prototype._decreaseTotal = function (_delta) { + // Iterate over the current mapping, starting at the bottom and delete + // rows. _delta is decreased for each removed row. Abort when delta is + // zero or the map is empty + while (_delta > 0 && this._map.length > 0) { + var cont = this._map[this._map.length - 1]; + // Remove as many containers as possible from spacers + if (cont instanceof et2_dataview_spacer) { + var diff = cont.getCount() - _delta; + if (diff > 0) { + // We're done as the spacer still has entries left + _delta = 0; + cont.setCount(diff, this.getAverageHeight()); + break; + } + else { + // Decrease _delta by the count of rows the spacer had + _delta -= diff + _delta; + } + } + else { + // We're going to remove a single row: remove it + _delta -= 1; + } + // Destroy the container if there are no rows left + cont.free(); + this._map.pop(); + } + // Check whether _delta is really zero + if (_delta > 0) { + this.egw.debug('error', "Error while decreasing the total count - requested to remove more rows than available."); + } + }; + /** + * Creates the grid DOM-Nodes + */ + et2_dataview_grid.prototype._createNodes = function () { + this.tr = jQuery(document.createElement("tr")); + this.outerCell = jQuery(document.createElement("td")) + .addClass("frame") + .attr("colspan", this._rowProvider.getColumnCount() + + (this._parentGrid ? 0 : 1)) + .appendTo(this.tr); + // Create the scrollarea div if this is the outer grid + this.scrollarea = null; + if (this._parentGrid == null) { + this.scrollarea = jQuery(document.createElement("div")) + .addClass("egwGridView_scrollarea") + .scroll(this, function (e) { + // Clear any older scroll timeout + if (e.data._scrollTimeout) { + window.clearTimeout(e.data._scrollTimeout); + } + // Clear any existing "invalidate" timeout (as the + // "setViewRange" function triggered by the scroll event + // forces an "invalidate"). + if (e.data._invalidateTimeout) { + window.clearTimeout(e.data._invalidateTimeout); + e.data._invalidateTimeout = null; + } + // Set a new timeout which calls the setViewArea + // function + e.data._scrollTimeout = window.setTimeout(jQuery.proxy(function () { + var newRange = et2_range(this.scrollarea.scrollTop() - et2_dataview_grid.ET2_GRID_VIEW_EXT, this._scrollHeight + et2_dataview_grid.ET2_GRID_VIEW_EXT * 2); + if (!et2_rangeEqual(newRange, this._viewRange)) { + this.setViewRange(newRange); + } + }, e.data), et2_dataview_grid.ET2_GRID_SCROLL_TIMEOUT); + }) + .height(this._scrollHeight) + .appendTo(this.outerCell); + } + // Create the inner table + var table = jQuery(document.createElement("table")) + .addClass("egwGridView_grid") + .appendTo(this.scrollarea ? this.scrollarea : this.outerCell); + this.innerTbody = jQuery(document.createElement("tbody")) + .appendTo(table); + // Set the tr as container element + this.appendNode(jQuery(this.tr[0])); + }; + /** + * Determines how many pixels the view range of the gridview is extended inside + * the scroll callback. + */ + et2_dataview_grid.ET2_GRID_VIEW_EXT = 50; + /** + * Determines the timeout after which the scroll-event is processed. + */ + et2_dataview_grid.ET2_GRID_SCROLL_TIMEOUT = 50; + /** + * Determines the timeout after which the invalidate-request gets processed. + */ + et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT = 25; + /** + * Determines how many elements are kept displayed outside of the current view + * range until they get removed. + */ + et2_dataview_grid.ET2_GRID_HOLD_COUNT = 50; + return et2_dataview_grid; +}(et2_dataview_view_container_1.et2_dataview_container)); +exports.et2_dataview_grid = et2_dataview_grid; +//# sourceMappingURL=et2_dataview_view_grid.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_dataview_view_grid.ts b/api/js/etemplate/et2_dataview_view_grid.ts new file mode 100644 index 0000000000..5e39b42bfb --- /dev/null +++ b/api/js/etemplate/et2_dataview_view_grid.ts @@ -0,0 +1,1456 @@ +/** + * EGroupware eTemplate2 - Class which contains the "grid" base class + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage dataview + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + et2_core_common; + + et2_dataview_interfaces; + et2_dataview_view_container; + et2_dataview_view_spacer; +*/ + +import {et2_dataview_IViewRange} from "./et2_dataview_interfaces"; +import {et2_dataview_container} from "./et2_dataview_view_container"; + +export class et2_dataview_grid extends et2_dataview_container implements et2_dataview_IViewRange +{ + + /** + * Determines how many pixels the view range of the gridview is extended inside + * the scroll callback. + */ + public static readonly ET2_GRID_VIEW_EXT = 50; + + /** + * Determines the timeout after which the scroll-event is processed. + */ + public static readonly ET2_GRID_SCROLL_TIMEOUT = 50; + + /** + * Determines the timeout after which the invalidate-request gets processed. + */ + public static readonly ET2_GRID_INVALIDATE_TIMEOUT = 25; + + /** + * Determines how many elements are kept displayed outside of the current view + * range until they get removed. + */ + public static readonly ET2_GRID_HOLD_COUNT = 50; + + + + egw: any; + + private _orgAvgHeight: number | boolean; + private _rowProvider: et2_dataview_rowProvider; + private _scrollHeight: number; + private _scrollTimeout: null; + + private _parentGrid: any; + + private _callback: Function; + private _context: object; + + private _invalidateTimeout: number; + private _invalidateCallback: Function; + private _invalidateContext: null; + private doInvalidate : boolean; + + private _map: any[]; + private _viewRange: { top: any; bottom: any }; + private _total: number; + private _avgHeight: number | boolean; + private _avgCount: number; + + private scrollarea: any; + private innerTbody: any; + private outerCell: JQuery; + + + /** + * Creates the grid. + * + * @param _parent is the parent grid class - if null, this means that this + * is the outer grid which manages the scrollarea. If not null, all other + * parameters are ignored and copied from the given grid instance. + * @param _parentGrid + * @param _egw + * @param _rowProvider + * @param _avgHeight is the starting average height of the column rows. + * @memberOf et2_dataview_grid + */ + constructor (_parent, _parentGrid, _egw, _rowProvider, _avgHeight : number) + { + // Call the inherited constructor + super(_parent); + + // If the parent is given, copy all other parameters from it + if (_parentGrid != null) + { + this.egw = _parent.egw; + this._orgAvgHeight = false; + this._rowProvider = _parentGrid._rowProvider; + } + else + { + // Otherwise copy the given parameters + this.egw = _egw; + this._orgAvgHeight = _avgHeight; + this._rowProvider = _rowProvider; + + // As this grid instance has no parent, we need a scroll container + this._scrollHeight = 0; + this._scrollTimeout = null; + } + + this._parentGrid = _parentGrid; + + this._scrollTimeout = null; + + this._invalidateTimeout = null; + + this._invalidateCallback = null; + this._invalidateContext = null; + + // Flag for stopping invalidate while working + this.doInvalidate = true; + + // _map contains a mapping between the grid indices and the elements + // associated to it. The first element in the array always refers to the + // element starting at index zero (being a spacer if the grid currently + // displays another range). + this._map = []; + + // _viewRange contains the current pixel-range of the grid which is + // visible. + this._viewRange = et2_range(0, 0); + + // Holds the maximum count of elements + this._total = 0; + + // Holds data used for storing the current average height data + this._avgHeight = false; + this._avgCount = -1; + + // Build the outer grid nodes + this._createNodes(); + } + + destroy () + { + // Destroy all containers + this.setTotalCount(0); + + // Stop the scroll timeout + if (this._scrollTimeout) + { + window.clearTimeout(this._scrollTimeout); + } + + // Stop the invalidate timeout + if (this._invalidateTimeout) + { + window.clearTimeout(this._invalidateTimeout); + } + + super.destroy(); + } + + clear () + { + // Store the old total count and rescue the current average height in + // form of the "original average height" + const oldTotalCount = this._total; + this._orgAvgHeight = this.getAverageHeight(); + + // Set the total count to zero + this.setTotalCount(0); + + // Reset the total count value + this.setTotalCount(oldTotalCount); + } + + /** + * Throws all elements away which are outside the current view range + */ + cleanup () + { + // Update the pixel positions + this._recalculateElementPosition(); + + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + const mapVis = this._calculateVisibleMappingIndices(); + + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + this._cleanupOutOfRangeElements(mapVis, 0); + } + + /** + * The insertRow function can be called to insert the given container(s) at + * the given row index. If there currently is another container at that + * given position, the new container(s) will be inserted above the old + * container. Yet the "total count" of the grid will be preserved by + * removing the correct count of elements from the next possible spacer. If + * no spacer is found, the last containers will be removed. This causes + * inserting new containers at the end of a grid to be immediately removed + * again. + * + * @param _index is the row index at which the given container(s) should be + * inserted. + * @param _container is eiter a single et2_dataview_container instance + * which should be inserted at the given position. Or an array of + * et2_dataview_container instances. If you want to remove the container + * don't do that manually by calling its "destroy" function but use the + * deleteRow function. + */ + insertRow (_index, _container) + { + // Calculate the map element the given index refers to + const idx = this._calculateMapIndex(_index); + + if (idx !== false) + { + // Wrap the container inside an array + if (_container instanceof et2_dataview_container) + { + _container = [_container]; + } + + // Fetch the average height + const avg = this.getAverageHeight(); + + // Call the internal _doInsertContainer function + for (let i = 0; i < _container.length; i++) + { + this._doInsertContainer(_index, idx, _container[i], avg); + } + + // Schedule an "invalidate" event + this.invalidate(); + } + } + + /** + * The deleteRow function can be used to remove the element at the given + * index. + * + * @param _index is the index from which should be deleted. If the given + * index is outside the so called "managedRange" nothing will happen, as the + * container has already been destroyed by the grid instance. + */ + deleteRow (_index) + { + // Calculate the map element the given index refers to + const idx = this._calculateMapIndex(_index); + + if (idx !== false) + { + this._doDeleteContainer(idx, false); + + // Schedule an "invalidate" event + this.invalidate(); + } + } + + /** + * The given callback gets called whenever the scroll position changed or + * the visible element range changed. The element indices are passed to the + * function as et2_range. + */ + setInvalidateCallback (_callback, _context) + { + this._invalidateCallback = _callback; + this._invalidateContext = _context; + } + + /** + * The setDataCallback function is used to set the callback that will be + * called when the grid requires new data. + * + * @param _callback is the callback function which gets called when the grid + * needs some new rows. + * @param _context is the context in which the callback function gets + * called. + */ + setDataCallback (_callback : Function, _context : object) + { + this._callback = _callback; + this._context = _context; + } + + /** + * The updateTotalCount function can be used to update the total count of + * rows that are displayed inside the grid. Changing the count always causes + * the spacer at the bottom (if it exists) to be + * + * @param _count specifies how many entries the grid can show. + */ + setTotalCount (_count : number) + { + // Abort if the total count has not changed + if (_count === this._total) + return; + + // Calculate how many elements have to be removed/added + const delta = Math.max(0, _count) - this._total; + + if (delta > 0) + { + this._appendEmptyRows(delta); + } + else + { + this._decreaseTotal(-delta); + } + + this._total = Math.max(0, _count); + + // Schedule an invalidate + this.invalidate(); + } + + /** + * Returns the current "total" count. + */ + getTotalCount() : number + { + return this._total; + } + + /** + * The setViewRange function updates the range in which rows are shown. + */ + setViewRange (_range) + { + // Set the new view range + this._viewRange = _range; + + // Immediately call the "invalidate" function + this._doInvalidate(); + } + + /** + * Return the indices of the currently visible rows. + */ + getVisibleIndexRange (_viewRange) + { + + function getElemIdx(_elem, _px) + { + if (_elem instanceof et2_dataview_spacer) + { + return _elem.getIndex() + + Math.floor((_px - _elem.getTop()) / this.getAverageHeight()); + } + + return _elem.getIndex(); + } + + let idxTop = 0; + let idxBottom = 0; + let vr; + + if (_viewRange) + { + vr = _viewRange; + } + else + { + // Calculate the "correct" view range by removing ET2_GRID_VIEW_EXT + vr = et2_bounds( + this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, + this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT); + } + + // Get the elements at the top and the bottom of the view + let topElem = null; + let botElem = null; + for (let i = 0; i < this._map.length; i++) + { + if (!topElem && this._map[i].getBottom() > vr.top) + { + topElem = this._map[i]; + } + + if (this._map[i].getTop() > vr.bottom) + { + botElem = this._map[i]; + break; + } + } + + if (!botElem) + { + botElem = this._map[this._map.length - 1]; + } + + if (topElem) + { + idxTop = getElemIdx.call(this, topElem, vr.top); + idxBottom = getElemIdx.call(this, botElem, vr.bottom); + } + + // Return the calculated index top and bottom + return et2_bounds(idxTop, idxBottom); + } + + /** + * Returns index range of all currently managed rows. + */ + getIndexRange () + { + let idxTop = false; + let idxBottom = false; + + for (let i = 0; i < this._map.length; i++) + { + if (!(this._map[i] instanceof et2_dataview_spacer)) + { + const idx = this._map[i].getIndex(); + + if (idxTop === false) + { + idxTop = idx; + } + + idxBottom = idx; + } + } + + return et2_bounds(idxTop, idxBottom); + } + + /** + * Updates the scrollheight + */ + setScrollHeight (_height) + { + this._scrollHeight = _height; + + // Update the height of the outer container + if (this.scrollarea) + { + this.scrollarea.height(_height); + } + + // Update the viewing range + this.setViewRange(et2_range(this._viewRange.top, this._scrollHeight)); + } + + /** + * Returns the average row height data, overrides the corresponding function + * of the et2_dataview_container. + */ + getAvgHeightData () + { + + if (this._avgHeight === false) + { + let avgCount = 0; + let avgSum = 0; + + for (let i = 0; i < this._map.length; i++) + { + const data = this._map[i].getAvgHeightData(); + + if (data !== null) + { + avgSum += data.avgHeight * data.avgCount; + avgCount += data.avgCount; + } + } + + // Calculate the average height, but only if we have a height + if (avgCount > 0 && avgSum > 0) + { + this._avgHeight = avgSum / avgCount; + this._avgCount = avgCount; + } + } + + // Return the calculated average height if it is available + if (this._avgHeight !== false) + { + return { + "avgCount": this._avgCount, + "avgHeight": this._avgHeight + }; + } + + // Otherwise return the parent average height + if (this._parent) + { + return this._parent.getAvgHeightData(); + } + + // Otherwise return the original average height given in the constructor + if (this._orgAvgHeight !== false) + { + return { + "avgCount": 1, + "avgHeight": this._orgAvgHeight + }; + } + return null; + } + + /** + * Returns the average row height in pixels. + */ + getAverageHeight () + { + const data = this.getAvgHeightData(); + return data ? data.avgHeight : 19; + } + + /** + * Returns the row provider. + */ + getRowProvider () + { + return this._rowProvider; + } + + /** + * Called whenever the size of this or another element in the container tree + * changes. + */ + invalidate() + { + + // Clear any existing "invalidate" timeout + if (this._invalidateTimeout) + { + window.clearTimeout(this._invalidateTimeout); + } + + if(!this.doInvalidate) + { + return; + } + + const self = this; + const _super = super.invalidate(); + this._invalidateTimeout = window.setTimeout(function() { + egw.debug("log","Dataview grid timed invalidate"); + // Clear the "_avgHeight" + self._avgHeight = false; + self._avgCount = -1; + self._invalidateTimeout = null; + self._doInvalidate(_super); + }, et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + } + + /** + * Makes the given index visible: TODO: Propagate this to the parent grid. + */ + makeIndexVisible(_idx) + { + // Get the element range + const elemRange = this._getElementRange(_idx); + + // Abort if the index was out of range + if (!elemRange) + { + return false; + } + + // Calculate the current visible range + const visibleRange = et2_bounds( + this._viewRange.top + et2_dataview_grid.ET2_GRID_VIEW_EXT, + this._viewRange.bottom - et2_dataview_grid.ET2_GRID_VIEW_EXT + ); + + // Check whether the element is currently completely visible -- if yes, + // do nothing + if (visibleRange.top < elemRange.top + && visibleRange.bottom > elemRange.bottom) + { + return true; + } + + if (elemRange.top < visibleRange.top) + { + this.scrollarea.scrollTop(elemRange.top); + } + else + { + const h = elemRange.bottom - elemRange.top; + this.scrollarea.scrollTop(elemRange.top - this._scrollHeight + h); + } + + } + + + /* ---- PRIVATE FUNCTIONS ---- */ + +/* _inspectStructuralIntegrity: function() { + var idx = 0; + for (var i = 0; i < this._map.length; i++) + { + if (this._map[i].getIndex() != idx) + { + throw "Index missmatch!"; + } + idx += this._map[i].getCount(); + } + + if (idx !== this._total) + { + throw "Total count missmatch!"; + } + },*/ + + /** + * Translates the given index to a range, returns false if the index is + * out of range. + */ + _getElementRange( _idx : number) + { + // Recalculate the element positions + this._recalculateElementPosition(); + + // Translate the given index to the map index + const mapIdx = this._calculateMapIndex(_idx); + + // Do nothing if the given index is out of range + if (mapIdx === false) + { + return false; + } + + // Get the map element + const elem = this._map[mapIdx]; + + // Get the element range + if (elem instanceof et2_dataview_spacer) + { + const avg = this.getAverageHeight(); + return et2_range(elem.getTop() + avg * (elem.getIndex() - _idx), + avg); + } + + return elem.getRange(); + } + + /** + * Recalculates the position of the currently managed containers. This + * routine only updates the pixel position of the elements -- the index of + * the elements is guaranteed to be maintained correctly by all high level + * functions of the grid, as the index position is needed to be correct for + * the "deleteRow" and "insertRow" functions, and we cannot effort to call + * this calculation method after every change in the grid mapping. + */ + _recalculateElementPosition() + { + for (let i = 0; i < this._map.length; i++) + { + if (i == 0) + { + this._map[i].setTop(0); + } + else + { + this._map[i].setTop(this._map[i - 1].getBottom()); + } + } + } + + /** + * The "_calculateVisibleMappingIndices" function calculates the indices of + * the _map array, which refer to containers that are currently (partially) + * visible. This function is used internally by "_doInvalidate". + */ + _calculateVisibleMappingIndices() : {top: number, bottom: number} + { + // First update the "top" and "bottom", and "index" values of all + // managed elements, and at the same time calculate the mapping indices + // of the elements which are inside the current view range. + const mapVis = {"top": -1, "bottom": -1}; + + for (let i = 0; i < this._map.length; i++) + { + // Update the top of the "map visible index" -- set it to the first + // element index, where the bottom line is beneath the top line + // of the view range. + if (mapVis.top === -1 + && this._map[i].getBottom() > this._viewRange.top) + { + mapVis.top = i; + } + + // Update the bottom of the "map visible index" -- set it to the + // first element index, where the top line is beneath the bottom + // line of the view range. + if (mapVis.bottom === -1 + && this._map[i].getTop() > this._viewRange.bottom) + { + mapVis.bottom = i; + break; + } + } + + return mapVis; + } + + /** + * Deletes all elements which are "out of the view range". This function is + * internally used by "_doInvalidate". How many elements that are out of the + * view range get preserved fully depends on the _holdCount parameter + * variable. + * + * @param _mapVis contains the _map indices of the just visible containers. + * @param _holdCount contains the number of elements that should be kept, + * if not given, this parameter defaults to ET2_GRID_HOLD_COUNT + */ + _cleanupOutOfRangeElements( _mapVis : {top : number, bottom: number}, _holdCount? : number) + { + + // Iterates over the map from and to the given indices and pushes all + // elements onto the given array, which are more than _holdCount + // elements remote from the start. + function searchElements(_arr, _start, _stop, _dir) + { + let dist = 0; + for (let i = _start; _dir > 0 ? i <= _stop : i >= _stop; i += _dir) + { + if (dist > _holdCount) + { + _arr.push(i); + } + else + { + dist += this._map[i].getCount(); + } + } + } + + // Set _holdCount to ET2_GRID_HOLD_COUNT if the parameters is not given + _holdCount = typeof _holdCount === "undefined" ? et2_dataview_grid.ET2_GRID_HOLD_COUNT : + _holdCount; + + // Collect all elements that will be deleted at the top and at the + // bottom of the grid + const deleteTop = []; + const deleteBottom = []; + + if (_mapVis.top !== -1) + { + searchElements.call(this, deleteTop, _mapVis.top, 0, -1); + } + + if (_mapVis.bottom !== -1) + { + searchElements.call(this, deleteBottom, _mapVis.bottom, + this._map.length - 1, 1); + } + + // The offset variable specifies how many elements have been deleted + // from the map -- this variable is needed as deleting elements from the + // map shifts the map indices. We iterate in oposite direction over the + // elements, as this allows the _doDeleteContainer/ container function + // to extend the (possibly) existing spacer at the top of the grid + let offs = 0; + for (var i = deleteTop.length - 1; i >= 0; i--) + { + // Delete the container and calculate the new offset + const mapLength = this._map.length; + this._doDeleteContainer(deleteTop[i] - offs, true); + offs += mapLength - this._map.length; + } + + for (var i = deleteBottom.length - 1; i >= 0; i--) + { + this._doDeleteContainer(deleteBottom[i] - offs, true); + } + + return deleteBottom.length + deleteTop.length > 0; + } + + /** + * The _updateContainers function is used internally by "_doInvalidate" in + * order to call the "setViewRange" function of all containers the implement + * that interfaces (needed for nested grids), and to request new elements + * for all currently visible spacers. + */ + _updateContainers() + { + for (let i = 0; i < this._map.length; i++) + { + const container = this._map[i]; + + // Check which type the container object has + const isSpacer = container instanceof et2_dataview_spacer; + const hasIViewRange = !isSpacer + && container.implements(et2_dataview_IViewRange); + + // If the container has one of those special types, calculate the + // view range and use that to update the view range of the element + // or to request new elements for the spacer + if (isSpacer || hasIViewRange) + { + // Calculate the relative view range and check whether + // the element is really visible + const elemRange = container.getRange(); + + // Abort if the element is not inside the visible range + if (!et2_rangeIntersect(this._viewRange, elemRange)) + { + continue; + } + + if (hasIViewRange) + { + // Update the view range of the container + container.setViewRange(et2_bounds( + this._viewRange.top - elemRange.top, + this._viewRange.bottom - elemRange.top)); + } + else // This container is a spacer + { + // Obtain the average element height + const avg = container._rowHeight; + + // Get the visible container range (vcr) + const vcr_top = Math.max(this._viewRange.top, elemRange.top); + const vcr_bot = Math.min(this._viewRange.bottom, elemRange.bottom); + + // Calculate the indices of the elements which will be + // requested + const cidx = container.getIndex(); + const ccnt = container.getCount(); + + // Calculate the start index -- prevent vtop from getting + // negative (and so idxStart being smaller than cidx) and + // ensure that idxStart is not larger than the maximum + // container index. + const vtop = Math.max(0, vcr_top); + let idxStart = Math.floor( + Math.min(cidx + ccnt - 1, + cidx + (vtop - elemRange.top) / avg, + this._total + )); + + // Calculate the end index -- prevent vtop from getting + // negative (and so idxEnd being smaller than cidx) and + // ensure that idxEnd is not larger than the maximum + // container index. + const vbot = Math.max(0, vcr_bot); + let idxEnd = Math.ceil( + Math.min(cidx + ccnt - 1, + cidx + (vbot - elemRange.top) / avg, + this._total + )); + + // Initial resize while the grid is hidden will give NaN + // This is an important optimisation, as it is involved in not + // loading all rows, so we override in that case so + // there are more than the 2-3 that fit in the min height. + if(isNaN(idxStart) && isSpacer) idxStart = cidx-1; + if(isNaN(idxEnd) && isSpacer && this._scrollHeight > 0 && elemRange.bottom == 0) + { + idxEnd = Math.min(ccnt,cidx + Math.ceil( + (this._viewRange.bottom - container._top) / (this._orgAvgHeight || 0) + )); + } + + // Call the data callback + if (this._callback) + { + const self = this; + egw.debug("log","Dataview grid flag for update: ", {start:idxStart,end:idxEnd}); + window.setTimeout(function() { + // If row template changes, self._callback might disappear + if(typeof self._callback != "undefined") + { + self._callback.call(self._context, idxStart, idxEnd); + } + }, 0); + } + } + } + } + } + + /** + * Invalidate iterates over the "mapping" array. It calculates which + * containers have to be removed and where new containers should be added. + */ + _doInvalidate( _super?) + { + if(!this.doInvalidate) return; + + // Update the pixel positions + this._recalculateElementPosition(); + + // Call the callback + if (this._invalidateCallback) + { + const range = this.getVisibleIndexRange( + et2_range(this.scrollarea.scrollTop(), this._scrollHeight)); + this._invalidateCallback.call(this._invalidateContext, range); + } + + // Get the visible mapping indices and recalculate index and pixel + // position of the containers. + const mapVis = this._calculateVisibleMappingIndices(); + + // Delete all invisible elements -- if anything changed, we have to + // recalculate the pixel positions again + if (this._cleanupOutOfRangeElements(mapVis)) + { + this._recalculateElementPosition(); + } + + // Update the view range of all visible elements that implement the + // corresponding interface and request elements for all visible spacers + this._updateContainers(); + + // Call the inherited invalidate function, broadcast the invalidation + // through the container tree. + if (this._parent && _super) + { + _super._doInvalidate() + } + } + + /** + * Translates the given grid index into the element index of the map. If the + * given index is completely out of the range, "false" is returned. + */ + _calculateMapIndex( _index) + { + + let top = 0; + let bot = this._map.length - 1; + + while (top <= bot) + { + const idx = Math.floor((top + bot) / 2); + const elem = this._map[idx]; + + const realIdx = elem.getIndex(); + const realCnt = elem.getCount(); + + if (_index >= realIdx && _index < realIdx + realCnt) + { + return idx; + } + else if (_index < realIdx) + { + bot = idx - 1; + } + else + { + top = idx + 1; + } + } + + return false; + } + + _insertContainerAtSpacer(_index, _mapIndex, _mapElem, _container,_avg) + { + // Set the index of the new container + _container.setIndex(_index); + + // Calculate at which position the spacer has to be splitted + const splitIdx = _index - _mapElem.getIndex(); + + // Get the count of elements that remain at the top of the splitter + const cntTop = splitIdx; + + // Get the count of elements that remain at the bottom of the splitter + // -- it has to be one element less than before + const cntBottom = _mapElem.getCount() - splitIdx - 1; + + // Split the containers if cntTop and cntBottom are larger than zero + if (cntTop > 0 && cntBottom > 0) + { + // Set the new count of the currently existing container, preserving + // its height as it was + _mapElem.setCount(cntTop); + + // Add the new element after the old container + _container.insertIntoTree(_mapElem.getLastNode()); + + // Create a new spacer and add it after the newly inserted container + const newSpacer = new et2_dataview_spacer(this, + this._rowProvider); + newSpacer.setCount(cntBottom, _avg); + newSpacer.setIndex(_index + 1); + newSpacer.insertIntoTree(_container.getLastNode()); + + // Insert the container and the new spacer into the map + this._map.splice(_mapIndex + 1, 0, _container, newSpacer); + } + else if (cntTop === 0 && cntBottom > 0) + { + // Simply adjust the size of the old spacer and insert the new + // container in front of it + _container.insertIntoTree(_mapElem.getFirstNode(), true); + _mapElem.setIndex(_index + 1); + _mapElem.setCount(cntBottom, _avg); + + this._map.splice(_mapIndex, 0, _container); + } + else if (cntTop > 0 && cntBottom === 0) + { + // Simply add the new container to the end of the old container and + // adjust the count of the old spacer to the remaining count. + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.setCount(cntTop); + + this._map.splice(_mapIndex + 1, 0, _container); + } + else // if (cntTop === 0 && cntBottom === 0) + { + // Append the new container to the current container and then + // destroy the old container + _container.insertIntoTree(_mapElem.getLastNode()); + _mapElem.free(); + + this._map.splice(_mapIndex, 1, _container); + } + } + + _insertContainerAtElement(_index, _mapIndex, _mapElem, _container, _avg) + { + // In a first step, simply insert the element at the specified position, + // in front of the element _mapElem. + _container.setIndex(_index); + _container.insertIntoTree(_mapElem.getFirstNode(), true); + this._map.splice(_mapIndex, 0, _container); + + // Search for the next spacer and increment the indices of all other + // elements until there + let _newIndex = _index + 1; + for (let i = _mapIndex + 1; i < this._map.length; i++) + { + // Update the index of the element + this._map[i].setIndex(_newIndex++); + + // We've found a spacer -- decrement its element count and abort + if (this._map[i] instanceof et2_dataview_spacer) + { + this._decrementSpacerCount(i, _avg); + return; + } + } + + // We've found no spacer so far, remove the last element from the map in + // order to obtain the "totalCount" (especially the last element is no + // spacer, so the following code cannot remove a spacer) + this._map.pop().free(); + } + + /** + * Inserts the given container at the given index. + */ + _doInsertContainer( _index, _mapIndex, _container, _avg) + { + // Check whether the given element at the map index is a spacer. If + // yes, we have to split the spacer at that position. + const mapElem = this._map[_mapIndex]; + + if (mapElem instanceof et2_dataview_spacer) + { + this._insertContainerAtSpacer(_index, _mapIndex, mapElem, + _container, _avg); + } + else + { + this._insertContainerAtElement(_index, _mapIndex, mapElem, + _container, _avg); + } + } + + /** + * Replaces the container at the given index with a spacer. The function + * tries to extend any spacer lying above or below the given _mapIndex. + * This code does not destroy the given container, but maintains its map + * index. + * + * @param _mapIndex is the index of _mapElem in the _map array. + * @param _mapElem is the container which should be replaced. + */ + _replaceContainerWithSpacer( _mapIndex : number, _mapElem) + { + let newAvg; + let spacer; + let totalHeight; + let totalCount; + // Check whether a spacer can be extended above or below the given + // _mapIndex + let spacerAbove = null; + let spacerBelow = null; + + if (_mapIndex > 0 + && this._map[_mapIndex - 1] instanceof et2_dataview_spacer) + { + spacerAbove = this._map[_mapIndex - 1]; + } + + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) + { + spacerBelow = this._map[_mapIndex + 1]; + } + + if (!spacerAbove && !spacerBelow) + { + // No spacer can be extended -- simply create a new one + spacer = new et2_dataview_spacer(this, this._rowProvider); + spacer.setIndex(_mapElem.getIndex()); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(1, _mapElem.getHeight()); + + // Insert the new spacer at the correct place into the DOM tree and + // the mapping + spacer.insertIntoTree(_mapElem.getLastNode()); + this._map.splice(_mapIndex + 1, 0, spacer); + } + else if (spacerAbove && spacerBelow) + { + // We're going to consolidate the upper and the lower spacer. To do + // that we'll calculate a new count of elements and a new average + // height, so that the upper container can get the height of all + // three elements together + totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight() + + _mapElem.getHeight(); + totalCount = spacerAbove.getCount() + spacerBelow.getCount() + + 1; + newAvg = totalHeight / totalCount; + + // Update the upper spacer + spacerAbove.addAvgHeight(_mapElem.getHeight()); + spacerAbove.setCount(totalCount, newAvg); + + // Delete the lower spacer and remove it from the mapping + spacerBelow.free(); + this._map.splice(_mapIndex + 1, 1); + } + else + { + // One of the two spacers is available + spacer = spacerAbove || spacerBelow; + + // Calculate the new count and the new average height of that spacer + totalCount = spacer.getCount() + 1; + totalHeight = spacer.getHeight() + _mapElem.getHeight(); + newAvg = totalHeight / totalCount; + + // Set the new container height + spacer.setIndex(Math.min(spacer.getIndex(), _mapElem.getIndex())); + spacer.addAvgHeight(_mapElem.getHeight()); + spacer.setCount(totalCount, newAvg); + } + } + + /** + * Checks whether there is another spacer below the given map index and if + * yes, consolidates the two. + */ + _consolidateSpacers( _mapIndex) + { + if (_mapIndex < this._map.length - 1 + && this._map[_mapIndex] instanceof et2_dataview_spacer + && this._map[_mapIndex + 1] instanceof et2_dataview_spacer) + { + const spacerAbove = this._map[_mapIndex]; + const spacerBelow = this._map[_mapIndex + 1]; + + // Calculate the new height/count of both containers + const totalHeight = spacerAbove.getHeight() + spacerBelow.getHeight(); + const totalCount = spacerAbove.getCount() + spacerBelow.getCount(); + const newAvg = totalCount / totalHeight; + + // Extend the new spacer + spacerAbove.setCount(totalCount, newAvg); + + // Delete the old spacer + spacerBelow.free(); + this._map.splice(_mapIndex + 1, 1); + } + } + + /** + * Decrements the count of the spacer at the given _mapIndex by one. If the + * given spacer has no more elements, it will be removed from the mapping. + * Note that this function does not update the indices of the following + * elements, this function is only used internally by the + * _insertContainerAtElement function and the _doDeleteContainer function + * where appropriate adjustments to the map data structure are done. + * + * @param _mapIndex is the index of the spacer in the "map" data structure. + * @param _avg is the new average height of the container, may be + * "undefined" in which case the height of the spacer rows is kept as it + * was. + */ + _decrementSpacerCount( _mapIndex : number, _avg? : number) + { + const cnt = this._map[_mapIndex].getCount() - 1; + if (cnt > 0) + { + this._map[_mapIndex].setCount(cnt, _avg); + } + else + { + this._map[_mapIndex].free(); + this._map.splice(_mapIndex, 1); + } + } + + /** + * Deletes the container at the given index. + */ + _doDeleteContainer( _mapIndex, _replaceWithSpacer) + { + // _replaceWithSpacer defaults to false + _replaceWithSpacer = _replaceWithSpacer ? _replaceWithSpacer : false; + + // Fetch the element at the given map index + const mapElem = this._map[_mapIndex]; + + // Indicates whether an element has really been removed -- if yes, the + // bottom spacer will be extended + let removedElement = false; + + // Check whether the map element is a spacer -- if yes, we have to do + // some special treatment + if (mapElem instanceof et2_dataview_spacer) + { + // Do nothing if the "_replaceWithSpacer" flag is true as the + // element already is a spacer + if (!_replaceWithSpacer) + { + this._decrementSpacerCount(_mapIndex); + removedElement = true; + } + } + else + { + if (_replaceWithSpacer) + { + this._replaceContainerWithSpacer(_mapIndex, mapElem); + } + else + { + removedElement = true; + } + + // Remove the complete (current) container, decrement the _mapIndex + this._map[_mapIndex].free(); + this._map.splice(_mapIndex, 1); + _mapIndex--; + + // The delete operation may have created two joining spacers -- this + // is highly suboptimal, so we'll consolidate those two spacers + this._consolidateSpacers(_mapIndex); + } + + // Update the indices of all elements after the current one, if we've + // really removed an element + if (removedElement) + { + for (let i = _mapIndex + 1; i < this._map.length; i++) + { + this._map[i].setIndex(this._map[i].getIndex() - 1); + } + + // Extend the last spacer as we have to maintain the spacer count + this._appendEmptyRows(1); + } + } + + /** + * The appendEmptyRows function is used internally to append empty rows to + * the end of the table. This functionality is needed in order to maintain + * the "total count" in the _doDeleteContainer function and to increase the + * "total count" in the "setCount" function. + * + * @param _count specifies the count of empty rows that will be added to the + * end of the table. + */ + _appendEmptyRows( _count : number) + { + // Special case -- the last element in the "_map" is no spacer -- this + // means, that the "managedRange" is currently at the bottom of the list + // -- so we have to insert a new spacer + let spacer = null; + const lastIndex = this._map.length - 1; + if (this._map.length === 0 || + !(this._map[lastIndex] instanceof et2_dataview_spacer)) + { + // Create a new spacer + spacer = new et2_dataview_spacer(this, this._rowProvider); + + // Insert the spacer -- we have a special case if there currently is + // no element inside the mapping + if (this._map.length === 0) + { + // Add a dummy element to the grid + const dummy = jQuery(document.createElement("tr")); + this.innerTbody.append(dummy); + + // Append the spacer to the grid + spacer.setIndex(0); + spacer.insertIntoTree(dummy, false); + + // Remove the dummy element + dummy.remove(); + } + else + { + // Insert the new spacer after the last element + spacer.setIndex(this._map[lastIndex].getIndex() + 1); + spacer.insertIntoTree(this._map[lastIndex].getLastNode()); + } + + // Add the new spacer to the mapping + this._map.push(spacer); + } + else + { + // Get the spacer at the bottom of the mapping + spacer = this._map[lastIndex]; + } + + // Update the spacer count + spacer.setCount(_count + spacer.getCount(), this.getAverageHeight()); + } + + /** + * The _decreaseTotal function is used to decrease the total row count in + * the grid. It tries to remove the given count of rows from the spacer + * located at the bottom of the grid, if this is not possible, it starts + * removing complete rows. + * + * @param _delta specifies how many rows should be removed. + */ + _decreaseTotal( _delta : number) + { + // Iterate over the current mapping, starting at the bottom and delete + // rows. _delta is decreased for each removed row. Abort when delta is + // zero or the map is empty + while (_delta > 0 && this._map.length > 0) + { + const cont = this._map[this._map.length - 1]; + + // Remove as many containers as possible from spacers + if (cont instanceof et2_dataview_spacer) + { + const diff = cont.getCount() - _delta; + + if (diff > 0) + { + // We're done as the spacer still has entries left + _delta = 0; + cont.setCount(diff, this.getAverageHeight()); + break; + } + else + { + // Decrease _delta by the count of rows the spacer had + _delta -= diff + _delta; + } + } + else + { + // We're going to remove a single row: remove it + _delta -= 1; + } + + // Destroy the container if there are no rows left + cont.free(); + this._map.pop(); + } + + // Check whether _delta is really zero + if (_delta > 0) + { + this.egw.debug('error', "Error while decreasing the total count - requested to remove more rows than available."); + } + } + + /** + * Creates the grid DOM-Nodes + */ + _createNodes() + { + + this.tr = jQuery(document.createElement("tr")); + + this.outerCell = jQuery(document.createElement("td")) + .addClass("frame") + .attr("colspan", this._rowProvider.getColumnCount() + + (this._parentGrid ? 0 : 1)) + .appendTo(this.tr); + + // Create the scrollarea div if this is the outer grid + this.scrollarea = null; + if (this._parentGrid == null) + { + this.scrollarea = jQuery(document.createElement("div")) + .addClass("egwGridView_scrollarea") + .scroll(this, function(e) { + + // Clear any older scroll timeout + if (e.data._scrollTimeout) + { + window.clearTimeout(e.data._scrollTimeout); + } + + // Clear any existing "invalidate" timeout (as the + // "setViewRange" function triggered by the scroll event + // forces an "invalidate"). + if (e.data._invalidateTimeout) + { + window.clearTimeout(e.data._invalidateTimeout); + e.data._invalidateTimeout = null; + } + + // Set a new timeout which calls the setViewArea + // function + e.data._scrollTimeout = window.setTimeout(jQuery.proxy(function() { + const newRange = et2_range( + this.scrollarea.scrollTop() - et2_dataview_grid.ET2_GRID_VIEW_EXT, + this._scrollHeight + et2_dataview_grid.ET2_GRID_VIEW_EXT * 2 + ); + + if (!et2_rangeEqual(newRange, this._viewRange)) + { + this.setViewRange(newRange); + } + },e.data), et2_dataview_grid.ET2_GRID_SCROLL_TIMEOUT); + }) + .height(this._scrollHeight) + .appendTo(this.outerCell); + } + + // Create the inner table + const table = jQuery(document.createElement("table")) + .addClass("egwGridView_grid") + .appendTo(this.scrollarea ? this.scrollarea : this.outerCell); + + this.innerTbody = jQuery(document.createElement("tbody")) + .appendTo(table); + + // Set the tr as container element + this.appendNode(jQuery(this.tr[0])); + } + +} + diff --git a/api/js/etemplate/et2_extension_nextmatch.js b/api/js/etemplate/et2_extension_nextmatch.js index e2f4ed2a33..dbaa66a36e 100644 --- a/api/js/etemplate/et2_extension_nextmatch.js +++ b/api/js/etemplate/et2_extension_nextmatch.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Nextmatch object * @@ -9,59 +10,58 @@ * @copyright Stylite 2011 * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - // Include the action system - egw_action.egw_action; - egw_action.egw_action_popup; - egw_action.egw_action_dragdrop; - egw_action.egw_menu_dhtmlx; + // Include the action system + egw_action.egw_action; + egw_action.egw_action_popup; + egw_action.egw_action_dragdrop; + egw_action.egw_menu_dhtmlx; - // Include some core classes - et2_core_widget; - et2_core_interfaces; - et2_core_DOMWidget; + // Include some core classes + et2_core_widget; + et2_core_interfaces; + et2_core_DOMWidget; - // Include all widgets the nextmatch extension will create - et2_widget_template; - et2_widget_grid; - et2_widget_selectbox; - et2_widget_selectAccount; - et2_widget_taglist; - et2_extension_customfields; + // Include all widgets the nextmatch extension will create + et2_widget_template; + et2_widget_grid; + et2_widget_selectbox; + et2_widget_selectAccount; + et2_widget_taglist; + et2_extension_customfields; - // Include all nextmatch subclasses - et2_extension_nextmatch_controller; - et2_extension_nextmatch_rowProvider; - et2_extension_nextmatch_dynheight; + // Include all nextmatch subclasses + et2_extension_nextmatch_controller; + et2_extension_nextmatch_rowProvider; + et2_extension_nextmatch_dynheight; - // Include the grid classes - et2_dataview; + // Include the grid classes + et2_dataview; */ - -/** - * Interface all special nextmatch header elements have to implement. - */ -var et2_INextmatchHeader = new Interface({ - - /** - * The 'setNextmatch' function is called by the parent nextmatch widget - * and tells the nextmatch header widgets which widget they should direct - * their 'sort', 'search' or 'filter' calls to. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) {} -}); - -var et2_INextmatchSortable = new Interface({ - - setSortmode: function(_mode) {} - -}); - +require("./et2_core_common"); +require("./et2_core_interfaces"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_DOMWidget_1 = require("./et2_core_DOMWidget"); +var et2_core_baseWidget_1 = require("./et2_core_baseWidget"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_widget_selectbox_1 = require("./et2_widget_selectbox"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements the "nextmatch" XET-Tag * @@ -81,2428 +81,1935 @@ var et2_INextmatchSortable = new Interface({ * +--------------+-----------+-------+ * @augments et2_DOMWidget */ -var et2_nextmatch = (function(){ "use strict"; return et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrint], -{ - attributes: { - // These normally set in settings, but broken out into attributes to allow run-time changes - "template": { - "name": "Template", - "type": "string", - "description": "The id of the template which contains the grid layout." - }, - "hide_header": { - "name": "Hide header", - "type": "boolean", - "description": "Hide the header", - "default": false - }, - "header_left": { - "name": "Left custom template", - "type": "string", - "description": "Customise the nextmatch - left side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "header_right": { - "name": "Right custom template", - "type": "string", - "description": "Customise the nextmatch - right side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "header_row": { - "name": "Inline custom template", - "type": "string", - "description": "Customise the nextmatch - inline, after row count. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", - "default": "" - }, - "no_filter": { - "name": "No filter", - "type": "boolean", - "description": "Hide the first filter", - "default": et2_no_init - }, - "no_filter2": { - "name": "No filter2", - "type": "boolean", - "description": "Hide the second filter", - "default": et2_no_init - }, - "view": { - "name": "View", - "type": "string", - "description": "Display entries as either 'row' or 'tile'. A matching template must also be set after changing this.", - "default": et2_no_init - }, - "onselect": { - "name": "onselect", - "type": "js", - "default": et2_no_init, - "description": "JS code which gets executed when rows are selected. Can also be a app.appname.func(selected) style method" - }, - "onfiledrop": { - "name": "onFileDrop", - "type": "js", - "default": et2_no_init, - "description": "JS code that gets executed when a _file_ is dropped on a row. Other drop interactions are handled by the action system. Return false to prevent the default link action." - }, - "settings": { - "name": "Settings", - "type": "any", - "description": "The nextmatch settings", - "default": {} - } - }, - - legacyOptions: ["template","hide_header","header_left","header_right"], - createNamespace: true, - - columns: [], - - // Current view, either row or tile. We store it here as controllers are - // recreated when the template changes. - view: 'row', - - /** - * Constructor - * - * @memberOf et2_nextmatch - */ - init: function() { - this._super.apply(this, arguments); - this.activeFilters = {col_filter:{}}; - - // Directly set current col_filters from settings - jQuery.extend(this.activeFilters.col_filter, this.options.settings.col_filter); - - /* - Process selected custom fields here, so that the settings are correctly - set before the row template is parsed - */ - var prefs = this._getPreferences(); - var cfs = {}; - for(var i = 0; i < prefs.visible.length; i++) - { - if(prefs.visible[i].indexOf(et2_nextmatch_customfields.prototype.prefix) == 0) - { - cfs[prefs.visible[i].substr(1)] = !prefs.negated; - } - } - var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); - if(typeof global_data == 'object' && global_data != null) - { - global_data.fields = cfs; - } - - this.div = jQuery(document.createElement("div")) - .addClass("et2_nextmatch"); - - - this.header = et2_createWidget("nextmatch_header_bar", {}, this); - this.innerDiv = jQuery(document.createElement("div")) - .appendTo(this.div); - - // Create the dynheight component which dynamically scales the inner - // container. - this.dynheight = this._getDynheight(); - - // Create the outer grid container - this.dataview = new et2_dataview(this.innerDiv, this.egw()); - - // Blank placeholder - this.blank = jQuery(document.createElement("div")) - .appendTo(this.dataview.table); - - // We cannot create the grid controller now, as this depends on the grid - // instance, which can first be created once we have the columns - this.controller = null; - this.rowProvider = null; - - // keeps sorted columns - this.sortedColumnsList = []; - }, - - /** - * Destroys all - */ - destroy: function() { - // Stop autorefresh - if(this._autorefresh_timer) - { - window.clearInterval(this._autorefresh_timer); - this._autorefresh_timer = null; - } - // Unbind handler used for toggling autorefresh - jQuery(this.getInstanceManager().DOMContainer.parentNode).off('show.et2_nextmatch'); - jQuery(this.getInstanceManager().DOMContainer.parentNode).off('hide.et2_nextmatch'); - - // Free the grid components - this.dataview.free(); - if(this.rowProvider) - { - this.rowProvider.free(); - } - if(this.controller) - { - this.controller.free(); - } - this.dynheight.free(); - - this._super.apply(this, arguments); - }, - - /** - * Loads the nextmatch settings - * - * @param {object} _attrs - */ - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - if (this.id) - { - var entry = this.getArrayMgr("content").data; - _attrs["settings"] = {}; - - if (entry) - { - _attrs["settings"] = entry; - - // Make sure there's an action var parameter - if(_attrs["settings"]["actions"] && !_attrs.settings["action_var"]) - { - _attrs.settings.action_var = "action"; - } - - // Merge settings mess into attributes - for(var attr in this.attributes) - { - if(_attrs.settings[attr]) - { - _attrs[attr] = _attrs.settings[attr]; - delete _attrs.settings[attr]; - } - } - } - } - }, - - doLoadingFinished: function() { - this._super.apply(this, arguments); - - if(!this.dynheight) - { - this.dynheight = this._getDynheight(); - } - - // Register handler for dropped files, if possible - if(this.options.settings.row_id) - { - // Appname should be first part of the template name - var split = this.options.template.split('.'); - var appname = split[0]; - - // Check link registry - if(this.egw().link_get_registry(appname)) - { - var self = this; - // Register a handler - jQuery(this.div) - .on('dragenter','.egwGridView_grid tr',function(e) { - // Figure out _which_ row - var row = self.controller.getRowByNode(this); - - if(!row || !row.uid) - { - return false; - } - e.stopPropagation(); e.preventDefault(); - - // Indicate acceptance - if(row.controller && row.controller._selectionMgr) - { - row.controller._selectionMgr.setFocused(row.uid,true); - } - return false; - }) - .on('dragexit','.egwGridView_grid tr', function(e) { - self.controller._selectionMgr.setFocused(); - }) - .on('dragover','.egwGridView_grid tr',false).attr("dropzone","copy") - - .on('drop', '.egwGridView_grid tr',function(e) { - self.handle_drop(e,this); - return false; - }); - } - } - // stop invalidation in no visible tabs - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) { - if(this.controller && this.controller._grid) - { - this.controller._grid.doInvalidate = false; - } - },this)); - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) { - if(this.controller && this.controller._grid) - { - this.controller._grid.doInvalidate = true; - } - },this)); - - return true; - }, - - /** - * Implements the et2_IResizeable interface - lets the dynheight manager - * update the width and height and then update the dataview container. - */ - resize: function() - { - if (this.dynheight) - { - this.dynheight.update(function(_w, _h) { - this.dataview.resize(_w, _h); - }, this); - } - }, - - /** - * Sorts the nextmatch widget by the given ID. - * - * @param {string} _id is the id of the data entry which should be sorted. - * @param {boolean} _asc if true, the elements are sorted ascending, otherwise - * descending. If not set, the sort direction will be determined - * automatically. - * @param {boolean} _update true/undefined: call applyFilters, false: only set sort - */ - sortBy: function(_id, _asc, _update) { - if (typeof _update == "undefined") - { - _update = true; - } - - // Create the "sort" entry in the active filters if it did not exist - // yet. - if (typeof this.activeFilters["sort"] == "undefined") - { - this.activeFilters["sort"] = { - "id": null, - "asc": true - }; - } - - // Determine the sort direction automatically if it is not set - if (typeof _asc == "undefined") - { - _asc = true; - if (this.activeFilters["sort"].id == _id) - { - _asc = !this.activeFilters["sort"].asc; - } - } - - // Set the sortmode display - this.iterateOver(function(_widget) { - _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none"); - }, this, et2_INextmatchSortable); - - if (_update) - { - this.applyFilters({sort: { id: _id, asc: _asc}}); - } - else - { - // Update the entry in the activeFilters object - this.activeFilters["sort"] = { - "id": _id, - "asc": _asc - }; - } - }, - - /** - * Removes the sort entry from the active filters object and thus returns to - * the natural sort order. - */ - resetSort: function() { - // Check whether the nextmatch widget is currently sorted - if (typeof this.activeFilters["sort"] != "undefined") - { - // Reset the sortmode - this.iterateOver(function(_widget) { - _widget.setSortmode("none"); - }, this, et2_INextmatchSortable); - - // Delete the "sort" filter entry - this.applyFilters({sort: undefined}); - } - }, - - /** - * Apply current or modified filters on NM widget (updating rows accordingly) - * - * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header - */ - applyFilters: function(_set) { - var changed = false; - var keep_selection = false; - - // Avoid loops cause by change events - if(this.update_in_progress) return; - this.update_in_progress = true; - - // Cleared explicitly - if(typeof _set != 'undefined' && jQuery.isEmptyObject(_set)) - { - changed = true; - this.activeFilters = {}; - } - if(typeof this.activeFilters == "undefined") - { - this.activeFilters = {col_filter: {}}; - } - if(typeof this.activeFilters.col_filter == "undefined") - { - this.activeFilters.col_filter = {}; - } - - if (typeof _set == 'object') - { - for(var s in _set) - { - if (s == 'col_filter') - { - // allow apps setState() to reset all col_filter by using undefined or null for it - // they can not pass {} for _set / state.state, if they need to set something - if (_set.col_filter === undefined || _set.col_filter === null) - { - this.activeFilters.col_filter = {}; - changed = true; - } - else - { - for(var c in _set.col_filter) - { - if (this.activeFilters.col_filter[c] !== _set.col_filter[c]) - { - if (_set.col_filter[c]) - { - this.activeFilters.col_filter[c] = _set.col_filter[c]; - } - else - { - delete this.activeFilters.col_filter[c]; - } - changed = true; - } - } - } - } - else if (s === 'selected') - { - changed = true; - keep_selection = true; - this.controller._selectionMgr.resetSelection(); - this.controller._objectManager.clear(); - for(var i in _set.selected) - { - this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::'+_set.selected[i],true); - } - delete _set.selected; - } - else if (this.activeFilters[s] !== _set[s]) - { - this.activeFilters[s] = _set[s]; - changed = true; - } - } - } - - this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); - - // Keep the selection after applying filters, but only if unchanged - if(!changed || keep_selection) - { - this.controller.keepSelection(); - } - else - { - // Do not keep selection +var et2_nextmatch = /** @class */ (function (_super_1) { + __extends(et2_nextmatch, _super_1); + /** + * Constructor + * + * @memberOf et2_nextmatch + */ + function et2_nextmatch(_parent, _attrs, _child) { + var _this = _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch._attributes, _child || {})) || this; + _this.activeFilters = { col_filter: {} }; + // Directly set current col_filters from settings + jQuery.extend(_this.activeFilters.col_filter, _this.options.settings.col_filter); + /* + Process selected custom fields here, so that the settings are correctly + set before the row template is parsed + */ + var prefs = _this._getPreferences(); + var cfs = {}; + for (var i = 0; i < prefs.visible.length; i++) { + if (prefs.visible[i].indexOf(et2_nextmatch_customfields.prefix) == 0) { + cfs[prefs.visible[i].substr(1)] = !prefs.negated; + } + } + var global_data = _this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if (typeof global_data == 'object' && global_data != null) { + global_data.fields = cfs; + } + _this.div = jQuery(document.createElement("div")) + .addClass("et2_nextmatch"); + _this.header = et2_createWidget("nextmatch_header_bar", {}, _this); + _this.innerDiv = jQuery(document.createElement("div")) + .appendTo(_this.div); + // Create the dynheight component which dynamically scales the inner + // container. + _this.dynheight = _this._getDynheight(); + // Create the outer grid container + _this.dataview = new et2_dataview(_this.innerDiv, _this.egw()); + // Blank placeholder + _this.blank = jQuery(document.createElement("div")) + .appendTo(_this.dataview.table); + // We cannot create the grid controller now, as this depends on the grid + // instance, which can first be created once we have the columns + _this.controller = null; + _this.rowProvider = null; + // keeps sorted columns + _this.sortedColumnsList = []; + return _this; + } + /** + * Destroys all + */ + et2_nextmatch.prototype.destroy = function () { + // Stop autorefresh + if (this._autorefresh_timer) { + window.clearInterval(this._autorefresh_timer); + this._autorefresh_timer = null; + } + // Unbind handler used for toggling autorefresh + jQuery(this.getInstanceManager().DOMContainer.parentNode).off('show.et2_nextmatch'); + jQuery(this.getInstanceManager().DOMContainer.parentNode).off('hide.et2_nextmatch'); + // Free the grid components + this.dataview.free(); + if (this.rowProvider) { + this.rowProvider.free(); + } + if (this.controller) { + this.controller.free(); + } + this.dynheight.free(); + _super_1.prototype.destroy.call(this); + }; + /** + * Loads the nextmatch settings + * + * @param {object} _attrs + */ + et2_nextmatch.prototype.transformAttributes = function (_attrs) { + _super_1.prototype.transformAttributes.call(this, _attrs); + if (this.id) { + var entry = this.getArrayMgr("content").data; + _attrs["settings"] = {}; + if (entry) { + _attrs["settings"] = entry; + // Make sure there's an action var parameter + if (_attrs["settings"]["actions"] && !_attrs.settings["action_var"]) { + _attrs.settings.action_var = "action"; + } + // Merge settings mess into attributes + for (var attr in this.attributes) { + if (_attrs.settings[attr]) { + _attrs[attr] = _attrs.settings[attr]; + delete _attrs.settings[attr]; + } + } + } + } + }; + et2_nextmatch.prototype.doLoadingFinished = function () { + _super_1.prototype.doLoadingFinished.call(this); + if (!this.dynheight) { + this.dynheight = this._getDynheight(); + } + // Register handler for dropped files, if possible + if (this.options.settings.row_id) { + // Appname should be first part of the template name + var split = this.options.template.split('.'); + var appname = split[0]; + // Check link registry + if (this.egw().link_get_registry(appname)) { + var self = this; + // Register a handler + // @ts-ignore + jQuery(this.div) + .on('dragenter', '.egwGridView_grid tr', function (e) { + // Figure out _which_ row + var row = self.controller.getRowByNode(this); + if (!row || !row.uid) { + return false; + } + e.stopPropagation(); + e.preventDefault(); + // Indicate acceptance + if (row.controller && row.controller._selectionMgr) { + row.controller._selectionMgr.setFocused(row.uid, true); + } + return false; + }) + .on('dragexit', '.egwGridView_grid tr', function (e) { + self.controller._selectionMgr.setFocused(); + }) + .on('dragover', '.egwGridView_grid tr', false).attr("dropzone", "copy") + .on('drop', '.egwGridView_grid tr', function (e) { + self.handle_drop(e, this); + return false; + }); + } + } + // stop invalidation in no visible tabs + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function (e) { + if (this.controller && this.controller._grid) { + this.controller._grid.doInvalidate = false; + } + }, this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function (e) { + if (this.controller && this.controller._grid) { + this.controller._grid.doInvalidate = true; + } + }, this)); + return true; + }; + /** + * Implements the et2_IResizeable interface - lets the dynheight manager + * update the width and height and then update the dataview container. + */ + et2_nextmatch.prototype.resize = function () { + if (this.dynheight) { + this.dynheight.update(function (_w, _h) { + this.dataview.resize(_w, _h); + }, this); + } + }; + /** + * Sorts the nextmatch widget by the given ID. + * + * @param {string} _id is the id of the data entry which should be sorted. + * @param {boolean} _asc if true, the elements are sorted ascending, otherwise + * descending. If not set, the sort direction will be determined + * automatically. + * @param {boolean} _update true/undefined: call applyFilters, false: only set sort + */ + et2_nextmatch.prototype.sortBy = function (_id, _asc, _update) { + if (typeof _update == "undefined") { + _update = true; + } + // Create the "sort" entry in the active filters if it did not exist + // yet. + if (typeof this.activeFilters["sort"] == "undefined") { + this.activeFilters["sort"] = { + "id": null, + "asc": true + }; + } + // Determine the sort direction automatically if it is not set + if (typeof _asc == "undefined") { + _asc = true; + if (this.activeFilters["sort"].id == _id) { + _asc = !this.activeFilters["sort"].asc; + } + } + // Set the sortmode display + this.iterateOver(function (_widget) { + _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc" : "desc") : "none"); + }, this, et2_INextmatchSortable); + if (_update) { + this.applyFilters({ sort: { id: _id, asc: _asc } }); + } + else { + // Update the entry in the activeFilters object + this.activeFilters["sort"] = { + "id": _id, + "asc": _asc + }; + } + }; + /** + * Removes the sort entry from the active filters object and thus returns to + * the natural sort order. + */ + et2_nextmatch.prototype.resetSort = function () { + // Check whether the nextmatch widget is currently sorted + if (typeof this.activeFilters["sort"] != "undefined") { + // Reset the sortmode + this.iterateOver(function (_widget) { + _widget.setSortmode("none"); + }, this, et2_INextmatchSortable); + // Delete the "sort" filter entry + this.applyFilters({ sort: undefined }); + } + }; + /** + * Apply current or modified filters on NM widget (updating rows accordingly) + * + * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header + */ + et2_nextmatch.prototype.applyFilters = function (_set) { + var changed = false; + var keep_selection = false; + // Avoid loops cause by change events + if (this.update_in_progress) + return; + this.update_in_progress = true; + // Cleared explicitly + if (typeof _set != 'undefined' && jQuery.isEmptyObject(_set)) { + changed = true; + this.activeFilters = { col_filter: {} }; + } + if (typeof this.activeFilters == "undefined") { + this.activeFilters = { col_filter: {} }; + } + if (typeof this.activeFilters.col_filter == "undefined") { + this.activeFilters.col_filter = {}; + } + if (typeof _set == 'object') { + for (var s in _set) { + if (s == 'col_filter') { + // allow apps setState() to reset all col_filter by using undefined or null for it + // they can not pass {} for _set / state.state, if they need to set something + if (_set.col_filter === undefined || _set.col_filter === null) { + this.activeFilters.col_filter = {}; + changed = true; + } + else { + for (var c in _set.col_filter) { + if (this.activeFilters.col_filter[c] !== _set.col_filter[c]) { + if (_set.col_filter[c]) { + this.activeFilters.col_filter[c] = _set.col_filter[c]; + } + else { + delete this.activeFilters.col_filter[c]; + } + changed = true; + } + } + } + } + else if (s === 'selected') { + changed = true; + keep_selection = true; + this.controller._selectionMgr.resetSelection(); + this.controller._objectManager.clear(); + for (var i in _set.selected) { + this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::' + _set.selected[i], true); + } + delete _set.selected; + } + else if (this.activeFilters[s] !== _set[s]) { + this.activeFilters[s] = _set[s]; + changed = true; + } + } + } + this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); + // Keep the selection after applying filters, but only if unchanged + if (!changed || keep_selection) { + this.controller.keepSelection(); + } + else { + // Do not keep selection this.controller._selectionMgr.resetSelection(); - this.controller._objectManager.clear(); - this.controller.keepSelection(); - } - - // Update the filters in the grid controller - this.controller.setFilters(this.activeFilters); - - // Update the header - this.header.setFilters(this.activeFilters); - - // Update any column filters - this.iterateOver(function(column) { - // Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter - if(typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) return; - - if(typeof column.set_value != "undefined" && column.id) - { - column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]); - } - if (column.id && typeof column.get_value == "function") - { - this[column.id] = column.get_value(); - } - }, this.activeFilters.col_filter, et2_INextmatchHeader); - - // Trigger an update - this.controller.update(true); - - if(changed) - { - // Highlight matching favorite in sidebox - if(this.getInstanceManager().app) - { - var appname = this.getInstanceManager().app; - if(app[appname] && app[appname].highlight_favorite) - { - app[appname].highlight_favorite(); - } - } - } - - this.update_in_progress = false; - }, - - /** - * Refresh given rows for specified change - * - * Change type parameters allows for quicker refresh then complete server side reload: - * - update: request just modified data from given rows. Sorting is not considered, - * so if the sort field is changed, the row will not be moved. - * - edit: rows changed, but sorting may be affected. Requires full reload. - * - delete: just delete the given rows clientside (no server interaction neccessary) - * - add: requires full reload - * - * @param {string[]|string} _row_ids rows to refresh - * @param {?string} _type "update", "edit", "delete" or "add" - * - * @see jsapi.egw_refresh() - * @fires refresh from the widget itself - */ - refresh: function(_row_ids, _type) { - // Framework trying to refresh, but nextmatch not fully initialized - if(this.controller === null || !this.div) - { - return; - } - if (!this.div.is(':visible')) // run refresh, once we become visible again - { - jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', - // Important to use anonymous function instead of just 'this.refresh' because - // of the parameters passed - jQuery.proxy(function() {this.refresh();},this) - ); - return; - } - if (typeof _type == 'undefined') _type = 'edit'; - if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids]; - if (typeof _row_ids == "undefined" || _row_ids === null) - { - this.applyFilters(); - - // Trigger an event so app code can act on it - jQuery(this).triggerHandler("refresh",[this]); - - return; - } - - if(_type == "delete") - { - // Record current & next index - var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; - var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); - var next = (entry.ao?entry.ao.getNext(_row_ids.length):null); - if(next == null || !next.id || next.id == uid) - { - // No next, select previous - next = (entry.ao?entry.ao.getPrevious(1):null); - } - - // Stop automatic updating - this.dataview.grid.doInvalidate = false; - for(var i = 0; i < _row_ids.length; i++) - { - uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; - - // Delete from internal references - this.controller.deleteRow(uid); - } - - // Select & focus next row - if(next && next.id) - { - this.controller._selectionMgr.setSelected(next.id,true); - this.controller._selectionMgr.setFocused(next.id,true); - } - - // Update the count - var total = this.dataview.grid._total - _row_ids.length; - // This will remove the last row! - // That's OK, because grid adds one in this.controller.deleteRow() - this.dataview.grid.setTotalCount(total); - // Re-enable automatic updating - this.dataview.grid.doInvalidate = true; - this.dataview.grid.invalidate(); - } - - id_loop: - for(var i = 0; i < _row_ids.length; i++) - { - var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; - switch(_type) - { - case "update": - if(!this.egw().dataRefreshUID(uid)) - { - // Could not update just that row - this.applyFilters(); - break id_loop; - } - break; - case "delete": - // Handled above, more code to execute after loop - break; - case "edit": - case "add": - default: - // Trigger refresh - this.applyFilters(); - break id_loop; - } - } - // Trigger an event so app code can act on it - jQuery(this).triggerHandler("refresh",[this,_row_ids,_type]); - }, - - /** - * Gets the selection - * - * @return Object { ids: [UIDs], inverted: boolean} - */ - getSelection: function() { - var selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null; - if(typeof selected == "object" && selected != null) - { - return selected; - } - return {ids:[],all:false}; - }, - - /** - * Event handler for when the selection changes - * - * If the onselect attribute was set to a string with javascript code, it will - * be executed "legacy style". You can get the selected values with getSelection(). - * If the onselect attribute is in app.appname.function style, it will be called - * with the nextmatch and an array of selected row IDs. - * - * The array can be empty, if user cleared the selection. - * - * @param action ActionObject From action system. Ignored. - * @param senders ActionObjectImplemetation From action system. Ignored. - */ - onselect: function(action,senders) { - // Execute the JS code connected to the event handler - if (typeof this.options.onselect == 'function') - { - return this.options.onselect.call(this, this.getSelection().ids, this); - } - }, - - /** - * Create the dynamic height so nm fills all available space - * - * @returns {undefined} - */ - _getDynheight: function() { - // Find the parent container, either a tab or the main container - var tab = this.get_tab_info(); - - if(!tab) - { - return new et2_dynheight(this.getInstanceManager().DOMContainer, this.innerDiv, 100); - } - - else if (tab && tab.contentDiv) - { - return new et2_dynheight(tab.contentDiv, this.innerDiv, 100); - } - - return false; - }, - - /** - * Generates the column caption for the given column widget - * - * @param {et2_widget} _widget - */ - _genColumnCaption: function(_widget) { - var result = null; - - if(typeof _widget._genColumnCaption == "function") return _widget._genColumnCaption(); - var self = this; - - _widget.iterateOver(function(_widget) { - var label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); - if (!label) return; // skip empty, undefined or null labels - if (!result) - { - result = label; - } - else - { - result += ", " + label; - } - }, this, et2_INextmatchHeader); - - return result; - }, - - /** - * Generates the column name (internal) for the given column widget - * Used in preferences to refer to the columns by name instead of position - * - * See _getColumnCaption() for human fiendly captions - * - * @param {et2_widget} _widget - */ - _getColumnName: function(_widget) { - if(typeof _widget._getColumnName == 'function') return _widget._getColumnName(); - - var name = _widget.id; - var child_names = []; - var children = _widget.getChildren(); - for(var i = 0; i < children.length; i++) { - if(children[i].id) child_names.push(children[i].id); - } - - var colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_"); - if(colName == "") { - this.egw().debug("info", "Unable to generate nm column name for ", _widget); - } - return colName; - }, - - - /** - * Retrieve the user's preferences for this nextmatch merged with defaults - * Column display, column size, etc. - */ - _getPreferences: function() { - // Read preference or default for column visibility - var negated = false; - var columnPreference = ""; - if(this.options.settings.default_cols) - { - negated = this.options.settings.default_cols[0] == "!"; - columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols; - } - if(this.options.settings.selectcols && this.options.settings.selectcols.length) - { - columnPreference = this.options.settings.selectcols; - negated = false; - } - if(!this.options.settings.columnselection_pref) - { - // Set preference name so changes are saved - this.options.settings.columnselection_pref = this.options.template; - } - - var app = ''; - var list = []; - if(this.options.settings.columnselection_pref) { - var pref = {}; - list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); - if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) - { - app = list[0].substring('nextmatch'.length+1); - pref = egw.preference(this.options.settings.columnselection_pref, app); - } - else - { - app = list[0]; - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); - } - if(pref) - { - negated = (pref[0] == "!"); - columnPreference = negated ? pref.substring(1) : pref; - } - } - - // If no column preference or default set, use all columns - if(typeof columnPreference =="string" && columnPreference.length == 0) - { - columnDisplay = {}; - negated = true; - } - - var columnDisplay = typeof columnPreference === "string" - ? et2_csvSplit(columnPreference,null,",") : columnPreference; - - // Adjusted column sizes - var size = {}; - if(this.options.settings.columnselection_pref && app) - { - var size_pref = this.options.settings.columnselection_pref +"-size"; - - // If columnselection pref is missing prefix, add it in - if(size_pref.indexOf('nextmatch') == -1) - { - size_pref = 'nextmatch-'+size_pref; - } - size = this.egw().preference(size_pref, app); - } - if(!size) size = {}; - - // Column order - var order = {}; - for(var i = 0; i < columnDisplay.length; i++) - { - order[columnDisplay[i]] = i; - } - return { - visible: columnDisplay, - visible_negated: negated, - size: size, - order: order - }; - }, - - /** - * Apply stored user preferences to discovered columns - * - * @param {array} _row - * @param {array} _colData - */ - _applyUserPreferences: function(_row, _colData) { - var prefs = this._getPreferences(); - var columnDisplay = prefs.visible; - var size = prefs.size; - var negated = prefs.visible_negated; - var order = prefs.order; - var colName = ''; - - // Add in display preferences - if(columnDisplay && columnDisplay.length > 0) - { - RowLoop: - for(var i = 0; i < _row.length; i++) - { - colName = ''; - if(_row[i].disabled === true) - { - _colData[i].visible = false; - continue; - } - - // Customfields needs special processing - if(_row[i].widget.instanceOf(et2_nextmatch_customfields)) - { - // Find cf field - for(var j = 0; j < columnDisplay.length; j++) - { - if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) { - _row[i].widget.options.fields = {}; - for(var k = j; k < columnDisplay.length; k++) - { - if(columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) - { - _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; - } - } - // Resets field visibility too - _row[i].widget._getColumnName(); - _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); - break; - } - } - // Disable if there are no custom fields - if(jQuery.isEmptyObject(_row[i].widget.customfields)) - { - _colData[i].visible = false; - continue; - } - colName = _row[i].widget.id; - } - else - { - colName = this._getColumnName(_row[i].widget); - } - if(!colName) continue; - - if(size[colName]) - { - // Make sure percentages stay percentages, and forget any preference otherwise - if(_colData[i].width.charAt(_colData[i].width.length - 1) == "%") - { - _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; - } - else - { - _colData[i].width = parseInt(size[colName])+'px'; - } - } - if(!negated) - { - _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; - } - for(var j = 0; j < columnDisplay.length; j++) - { - if(columnDisplay[j] == colName) - { - _colData[i].visible = !negated; - - continue RowLoop; - } - } - _colData[i].visible = negated; - } - } - - _colData.sort(function(a,b) { - return a.order - b.order; - }); - _row.sort(function(a,b) { - if(typeof a.colData !== 'undefined' && typeof b.colData !== 'undefined') - { - return a.colData.order - b.colData.order; - } - else if (typeof a.order !== 'undefined' && typeof b.order !== 'undefined') - { - return a.order - b.order; - } - }); - }, - - /** - * Take current column display settings and store them in this.egw().preferences - * for next time - */ - _updateUserPreferences: function() { - var colMgr = this.dataview.getColumnMgr(); - var app = ""; - if(!this.options.settings.columnselection_pref) { - this.options.settings.columnselection_pref = this.options.template; - } - - var visibility = colMgr.getColumnVisibilitySet(); - var colDisplay = []; - var colSize = {}; - var custom_fields = []; - - // visibility is indexed by internal ID, widget is referenced by position, preference needs name - for(var i = 0; i < colMgr.columns.length; i++) - { - var widget = this.columns[i].widget; - var colName = this._getColumnName(widget); - if(colName) { - // Server side wants each cf listed as a seperate column - if(widget.instanceOf(et2_nextmatch_customfields)) - { - // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields - colName = widget.id; - for(var name in widget.options.fields) { - if(widget.options.fields[name]) custom_fields.push(widget.prefix+name); - } - } - if(visibility[colMgr.columns[i].id].visible) colDisplay.push(colName); - - // When saving sizes, only save columns with explicit values, preserving relative vs fixed - // Others will be left to flex if width changes or more columns are added - if(colMgr.columns[i].relativeWidth) - { - colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%"; - } - else if (colMgr.columns[i].fixedWidth) - { - colSize[colName] = colMgr.columns[i].fixedWidth; - } - } else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) { - this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id); - } - } - - var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); - var pref = this.options.settings.columnselection_pref; - if(pref.indexOf('nextmatch') == 0) - { - app = list[0].substring('nextmatch'.length+1); - } - else - { - app = list[0]; - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = "nextmatch-"+this.options.settings.columnselection_pref; - } - - // Server side wants each cf listed as a seperate column - jQuery.merge(colDisplay, custom_fields); - - // Update query value, so data source can use visible columns to exclude expensive sub-queries - var oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : []; - - this.activeFilters.selectcols = this.sortedColumnsList ? this.sortedColumnsList : colDisplay; - - // We don't need to re-query if they've removed a column - var changed = []; - ColLoop: - for(var i = 0; i < colDisplay.length; i++) - { - for(var j = 0; j < oldCols.length; j++) { - if(colDisplay[i] == oldCols[j]) continue ColLoop; - } - changed.push(colDisplay[i]); - } - - // If a custom field column was added, throw away cache to deal with - // efficient apps that didn't send all custom fields in the first request - var cf_added = jQuery(changed).filter(jQuery(custom_fields)).length > 0; - - // Save visible columns - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - this.egw().set_preference(app, pref, this.activeFilters.selectcols.join(","), - // Use callback after the preference gets set to trigger refresh, in case app - // isn't looking at selectcols and just uses preference - cf_added ? jQuery.proxy(function() {if(this.controller) this.controller.update(true);}, this):null - ); - - // Save adjusted column sizes - this.egw().set_preference(app, pref+"-size", colSize); - - // No significant change (just normal columns shown) and no need to wait, - // but the grid still needs to be redrawn if a custom field was removed because - // the cell content changed. This is a cheaper refresh than the callback, - // this.controller.update(true) - if((changed.length || custom_fields.length) && !cf_added) this.applyFilters(); - }, - - _parseHeaderRow: function(_row, _colData) { - - // Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget - - for (var x = 0; x < _row.length; x++) - { - if(!_row[x].widget) - { - _row[x].widget = et2_createWidget("label"); - } - } - - // Get column display preference - this._applyUserPreferences(_row, _colData); - - // Go over the header row and create the column entries - this.columns = new Array(_row.length); - var columnData = new Array(_row.length); - - // No action columns in et2 - var remove_action_index = null; - - for (var x = 0; x < _row.length; x++) - { - this.columns[x] = jQuery.extend({ - "order": _colData[x] && typeof _colData[x].order !== 'undefined' ? _colData[x].order : x, - "widget": _row[x].widget - },_colData[x]); - - var visibility = (!_colData[x] || _colData[x].visible) ? - ET2_COL_VISIBILITY_VISIBLE : - ET2_COL_VISIBILITY_INVISIBLE; - if(_colData[x].disabled && _colData[x].disabled !=='' && - this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled)) - { - visibility = ET2_COL_VISIBILITY_DISABLED; - } - columnData[x] = { - "id": "col_" + x, - "order": this.columns[x].order, - "caption": this._genColumnCaption(_row[x].widget), - "visibility": visibility, - "width": _colData[x] ? _colData[x].width : 0 - }; - if(_colData[x].width === 'auto') - { - // Column manager does not understand 'auto', which grid widget - // uses if width is not set - columnData[x].width = '100%'; - } - if(_colData[x].minWidth) - { - columnData[x].minWidth = _colData[x].minWidth; - } - if(_colData[x].maxWidth) - { - columnData[x].maxWidth = _colData[x].maxWidth; - } - - // No action columns in et2 - var colName = this._getColumnName(_row[x].widget); - if(colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all') - { - remove_action_index = x; - continue; - } - else if (!colName) - { - // Unnamed column cannot be toggled or saved - columnData[x].visibility = ET2_COL_VISIBILITY_ALWAYS_NOSELECT; - } - - } - - // Remove action column - if(remove_action_index != null) - { - this.columns.splice(remove_action_index,remove_action_index); - columnData.splice(remove_action_index,remove_action_index); - _colData.splice(remove_action_index,remove_action_index); - } - - // Create the column manager and update the grid container - this.dataview.setColumns(columnData); - - for (var x = 0; x < _row.length; x++) - { - // Append the widget to this container - this.addChild(_row[x].widget); - } - - // Create the nextmatch row provider - this.rowProvider = new et2_nextmatch_rowProvider( - this.dataview.rowProvider, this._getSubgrid, this); - - // Register handler to update preferences when column properties are changed - var self = this; - this.dataview.onUpdateColumns = function() { - // Use apply to make sure context is there - self._updateUserPreferences.apply(self); - - // Allow column widgets a chance to resize - self.iterateOver(function(widget) {widget.resize();}, self, et2_IResizeable); - }; - - // Register handler for column selection popup, or disable - if(this.selectPopup) - { - this.selectPopup.remove(); - this.selectPopup = null; - } - if(this.options.settings.no_columnselection) - { - this.dataview.selectColumnsClick = function() {return false;}; - jQuery('span.selectcols',this.dataview.headTr).hide(); - } - else - { - jQuery('span.selectcols',this.dataview.headTr).show(); - this.dataview.selectColumnsClick = function(event) { - self._selectColumnsClick(event); - }; - } - }, - - _parseDataRow: function(_row, _rowData, _colData) { - var columnWidgets = new Array(this.columns.length); - - _row.sort(function(a,b) { - return a.colData.order - b.colData.order; - }); - - for (var x = 0; x < columnWidgets.length; x++) - { - if (typeof _row[x] != "undefined" && _row[x].widget) - { - columnWidgets[x] = _row[x].widget; - - // Append the widget to this container - this.addChild(_row[x].widget); - } - else - { - columnWidgets[x] = _row[x].widget; - } - // Pass along column alignment - if(_row[x].align && columnWidgets[x]) - { - columnWidgets[x].align = _row[x].align; - } - } - - this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this); - - - // Create the grid controller - this.controller = new et2_nextmatch_controller( - null, - this.egw(), - this.getInstanceManager().etemplate_exec_id, - this, - null, - this.dataview.grid, - this.rowProvider, - this.options.settings.action_links, - null, - this.options.actions - ); - - // Need to trigger empty row the first time - if(total == 0) this.controller._emptyRow(); - - // Set data cache prefix to either provided custom or auto - if(!this.options.settings.dataStorePrefix && this.options.settings.get_rows) - { - // Use jsapi data module to update - var list = this.options.settings.get_rows.split('.', 2); - if (list.length < 2) list = this.options.settings.get_rows.split('_'); // support "app_something::method" - this.options.settings.dataStorePrefix = list[0]; - } - this.controller.setPrefix(this.options.settings.dataStorePrefix); - - // Set the view - this.controller._view = this.view; - - // Load the initial order - /*this.controller.loadInitialOrder(this._getInitialOrder( - this.options.settings.rows, this.options.settings.row_id - ));*/ - - // Set the initial row count - var total = typeof this.options.settings.total != "undefined" ? - this.options.settings.total : 0; - // This triggers an invalidate, which updates the grid - this.dataview.grid.setTotalCount(total); - - // Insert any data sent from server, so invalidate finds data already - if(this.options.settings.rows && this.options.settings.num_rows) - { - this.controller.loadInitialData( - this.options.settings.dataStorePrefix, - this.options.settings.row_id, - this.options.settings.rows - ); - // Remove, to prevent duplication - delete this.options.settings.rows; - } - }, - - _parseGrid: function(_grid) { - // Search the rows for a header-row - if one is found, parse it - for (var y = 0; y < _grid.rowData.length; y++) - { - // Parse the first row as a header, need header to parse the data rows - if (_grid.rowData[y]["class"] == "th" || y == 0) - { - this._parseHeaderRow(_grid.cells[y], _grid.colData); - } - else - { - this._parseDataRow(_grid.cells[y], _grid.rowData[y], - _grid.colData); - } - } - this.dataview.table.resize(); - }, - - _getSubgrid: function (_row, _data, _controller) { - // Fetch the id of the element described by _data, this will be the - // parent_id of the elements in the subgrid - var rowId = _data.content[this.options.settings.row_id]; - - // Create a new grid with the row as parent and the dataview grid as - // parent grid - var grid = new et2_dataview_grid(_row, this.dataview.grid); - - // Create a new controller for the grid - var controller = new et2_nextmatch_controller( - _controller, - this.egw(), - this.getInstanceManager().etemplate_exec_id, - this, - rowId, - grid, - this.rowProvider, - this.options.settings.action_links, - _controller.getObjectManager() - ); - controller.update(); - - // Register inside the destruction callback of the grid - grid.setDestroyCallback(function () { - controller.free(); - }); - - return grid; - }, - - _getInitialOrder: function (_rows, _rowId) { - - var _order = []; - - // Get the length of the non-numerical rows arra - var len = 0; - for (var key in _rows) { - if (!isNaN(key) && parseInt(key) > len) - len = parseInt(key); - } - - // Iterate over the rows - for (var i = 0; i < len; i++) - { - // Get the uid from the data - var uid = this.egw().appName + '::' + _rows[i][_rowId]; - - // Store the data for that uid - this.egw().dataStoreUID(uid, _rows[i]); - - // Push the uid onto the order array - _order.push(uid); - } - - return _order; - }, - - _selectColumnsClick: function(e) { - var self = this; - var columnMgr = this.dataview.getColumnMgr(); - - // ID for faking letter selection in column selection - var LETTERS = '~search_letter~'; - - var columns = {}; - var columns_selected = []; - - for (var i = 0; i < columnMgr.columns.length; i++) - { - var col = columnMgr.columns[i]; - var widget = this.columns[i].widget; - - if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== ET2_COL_VISIBILITY_DISABLED) - { - columns[col.id] = col.caption; - if(col.visibility == ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(col.id); - } - // Custom fields get listed separately - if(widget.instanceOf(et2_nextmatch_customfields)) - { - if(jQuery.isEmptyObject(widget.customfields)) - { - // No customfields defined, don't show column - delete(columns[col.id]); - continue; - } - for(var field_name in widget.customfields) - { - columns[widget.prefix+field_name] = " - "+widget.customfields[field_name].label; - if(widget.options.fields[field_name]) columns_selected.push(et2_customfields_list.prototype.prefix+field_name); - } - } - } - - // Letter search - if(this.options.settings.lettersearch) - { - columns[LETTERS] = egw.lang('Search letter'); - if(this.header.lettersearch.is(':visible')) columns_selected.push(LETTERS); - } - - // Build the popup - if(!this.selectPopup) - { - var select = et2_createWidget("select", { - multiple: true, - rows: 8, - empty_label:this.egw().lang("select columns"), - selected_first: false, - value_class:"selcolumn_sortable_" - }, this); - select.set_select_options(columns); - select.set_value(columns_selected); - - var autoRefresh = et2_createWidget("select", { - "empty_label":"Refresh" - }, this); - autoRefresh.set_id("nm_autorefresh"); - autoRefresh.set_select_options({ - // Cause [unknown] problems with mail - //30: "30 seconds", - //60: "1 Minute", - 300: "5 Minutes", - 900: "15 Minutes", - 1800: "30 Minutes" - }); - autoRefresh.set_value(this._get_autorefresh()); - autoRefresh.set_statustext(egw.lang("Automatically refresh list")); - - var defaultCheck = et2_createWidget("select", {"empty_label":"Preference"}, this); - defaultCheck.set_id('nm_col_preference'); - defaultCheck.set_select_options({ - 'default': {label: 'Default',title:'Set these columns as the default'}, - 'reset': {label: 'Reset', title:"Reset all user's column preferences"}, - 'force': {label: 'Force', title:'Force column preference so users cannot change it'} - }); - defaultCheck.set_value(this.options.settings.columns_forced ? 'force': ''); - - var okButton = et2_createWidget("buttononly", {"background_image":true, image:"check"}, this); - okButton.set_label(this.egw().lang("ok")); - okButton.onclick = function() { - // Update visibility - var visibility = {}; - for (var i = 0; i < columnMgr.columns.length; i++) - { - var col = columnMgr.columns[i]; - if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== ET2_COL_VISIBILITY_DISABLED ) - { - visibility[col.id] = {visible: false}; - } - } - var value = select.getValue(); - - // Update & remove letter filter - if(self.header.lettersearch) - { - var show_letters = true; - if(value.indexOf(LETTERS) >= 0) - { - value.splice(value.indexOf(LETTERS),1); - } - else - { - show_letters = false; - } - self._set_lettersearch(show_letters); - } - - var column = 0; - for(var i = 0; i < value.length; i++) - { - // Handle skipped columns - while(value[i] != "col_"+column && column < columnMgr.columns.length) - { - column++; - } - if(visibility[value[i]]) - { - visibility[value[i]].visible = true; - } - // Custom fields are listed seperately in column list, but are only 1 column - if(self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) { - var cf = self.columns[column].widget.options.customfields; - var visible = self.columns[column].widget.options.fields; - - // Turn off all custom fields - for(var field_name in cf) - { - visible[field_name] = false; - } - // Turn on selected custom fields - start from 0 in case they're not in order - for(var j = 0; j < value.length; j++) - { - if(value[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue; - visible[value[j].substring(1)] = true; - i++; - } - self.columns[column].widget.set_visible(visible); - } - } - columnMgr.setColumnVisibilitySet(visibility); - - this.sortedColumnsList = []; - jQuery(select.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function(i,v){ - var data_id = v.getAttribute('data-value'); - var value = select.getValue(); - if (data_id.match(/^col_/) && value.indexOf(data_id) != -1) - { - var col_id = data_id.replace('col_','') - var col_widget = self.columns[col_id].widget; - if (col_widget.customfields) - { - self.sortedColumnsList.push(col_widget.id); - for(var field_name in col_widget.customfields) - { - if(jQuery.isEmptyObject(col_widget.options.fields) || col_widget.options.fields[field_name] == true) - { - self.sortedColumnsList.push(et2_customfields_list.prototype.prefix + field_name); - } - } - } - else - { - self.sortedColumnsList.push(self._getColumnName(col_widget)); - } - } - }); - - // Hide popup - self.selectPopup.toggle(); - - self.dataview.updateColumns(); - - // Auto refresh - self._set_autorefresh(autoRefresh.get_value()); - - // Set default or clear forced - if(show_letters) - { - self.activeFilters.selectcols.push('lettersearch'); - } - self.getInstanceManager().submit(); - - self.selectPopup = null; - }; - - var cancelButton = et2_createWidget("buttononly", {"background_image":true, image:"cancel"}, this); - cancelButton.set_label(this.egw().lang("cancel")); - cancelButton.onclick = function() { - self.selectPopup.toggle(); - self.selectPopup = null; - }; - var $select = jQuery(select.getDOMNode()); - $select.find('.ui-multiselect-checkboxes').sortable({ - placeholder:'ui-fav-sortable-placeholder', - items:'li[class^="selcolumn_sortable_col"]', - cancel: 'li[class^="selcolumn_sortable_#"]', - cursor: "move", - tolerance: "pointer", - axis: 'y', - containment: "parent", - delay: 250, //(millisecond) delay before the sorting should start - beforeStop: function(event, ui) { - jQuery('li[class^="selcolumn_sortable_#"]', this).css({ - opacity: 1 - }); - }, - start: function(event, ui){ - jQuery('li[class^="selcolumn_sortable_#"]', this).css({ - opacity: 0.5 - }); - }, - sort: function (event, ui) - { - jQuery( this ).sortable("refreshPositions" ); - } - }); - $select.disableSelection(); - $select.find('li[class^="selcolumn_sortable_"]').each(function(i,v){ - jQuery(v).attr('data-value',(jQuery(v).find('input')[0].value)) - }); - var $footerWrap = jQuery(document.createElement("div")) - .addClass('dialogFooterToolbar') - .append(okButton.getDOMNode()) - .append(cancelButton.getDOMNode()); - this.selectPopup = jQuery(document.createElement("div")) - .addClass("colselection ui-dialog ui-widget-content") - .append(select.getDOMNode()) - .append($footerWrap) - .appendTo(this.innerDiv); - - // Add autorefresh - $footerWrap.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode())); - - // Add default checkbox for admins - var apps = this.egw().user('apps'); - if(apps['admin']) - { - $footerWrap.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode())); - } - } - else - { - this.selectPopup.toggle(); - } - var t_position = jQuery(e.target).position(); - var s_position = this.div.position(); - var max_height = this.getDOMNode().getElementsByClassName('egwGridView_outer')[0]['tBodies'][0].clientHeight - - (2 * this.selectPopup.find('.dialogFooterToolbar').height()); - this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height',max_height); - this.selectPopup.css("top", t_position.top) - .css("left", s_position.left + this.div.width() - this.selectPopup.width()); - }, - - /** - * Set the currently displayed columns, without updating user's preference - * - * @param {string[]} column_list List of column names - * @param {boolean} trigger_update =false - explicitly trigger an update - */ - set_columns: function(column_list, trigger_update) - { - var columnMgr = this.dataview.getColumnMgr(); - var visibility = {}; - - // Initialize to false - for (var i = 0; i < columnMgr.columns.length; i++) - { - var col = columnMgr.columns[i]; - if(col.caption && col.visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT ) - { - visibility[col.id] = {visible: false}; - } - } - for(var i = 0; i < this.columns.length; i++) - { - - var widget = this.columns[i].widget; - var colName = this._getColumnName(widget); - if(column_list.indexOf(colName) !== -1 && - typeof visibility[columnMgr.columns[i].id] !== 'undefined' - ) - { - visibility[columnMgr.columns[i].id].visible = true; - } - // Custom fields are listed seperately in column list, but are only 1 column - if(widget && widget.instanceOf(et2_nextmatch_customfields)) { - - // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields - colName = widget.id; - if(column_list.indexOf(colName) !== -1) - { - visibility[columnMgr.columns[i].id].visible = true; - } - - var cf = this.columns[i].widget.options.customfields; - var visible = this.columns[i].widget.options.fields; - - // Turn off all custom fields - for(var field_name in cf) - { - visible[field_name] = false; - } - // Turn on selected custom fields - start from 0 in case they're not in order - for(var j = 0; j < column_list.length; j++) - { - if(column_list[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue; - visible[column_list[j].substring(1)] = true; - } - widget.set_visible(visible); - } - } - columnMgr.setColumnVisibilitySet(visibility); - - // We don't want to update user's preference, so directly update - this.dataview._updateColumns(); - - // Allow column widgets a chance to resize - this.iterateOver(function(widget) {widget.resize();}, this, et2_IResizeable); - }, - - /** - * Set the letter search preference, and update the UI - * - * @param {boolean} letters_on - */ - _set_lettersearch: function(letters_on) { - if(letters_on) - { - this.header.lettersearch.show(); - } - else - { - this.header.lettersearch.hide(); - } - var lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch"; - this.egw().set_preference(this.egw().getAppName(),lettersearch_preference,letters_on); - }, - - /** - * Set the auto-refresh time period, and starts the timer if not started - * - * @param time int Refresh period, in seconds - */ - _set_autorefresh: function(time) { - // Store preference - var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; - var app = this.options.template.split("."); - if(this._get_autorefresh() != time) - { - this.egw().set_preference(app[0],refresh_preference,time); - } - - // Start / update timer - if (this._autorefresh_timer) - { - window.clearInterval(this._autorefresh_timer); - delete this._autorefresh_timer; - } - if(time > 0) - { - this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000); - - // Bind to tab show/hide events, so that we don't bother refreshing in the background - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) { - // Stop - window.clearInterval(this._autorefresh_timer); - jQuery(e.target).off(e); - - // If the autorefresh time is up, bind once to trigger a refresh - // (if needed) when tab is activated again - this._autorefresh_timer = setTimeout(jQuery.proxy(function() { - // Check in case it was stopped / destroyed since - if(!this._autorefresh_timer || !this.getInstanceManager()) return; - - jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', - // Important to use anonymous function instead of just 'this.refresh' because - // of the parameters passed - jQuery.proxy(function() {this.refresh();},this) - ); - },this), time*1000); - },this)); - jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) { - // Start normal autorefresh timer again - this._set_autorefresh(this._get_autorefresh()); - jQuery(e.target).off(e); - },this)); - } - }, - - /** - * Get the auto-refresh timer - * - * @return int Refresh period, in secods - */ - _get_autorefresh: function() { - var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; - var app = this.options.template.split("."); - return this.egw().preference(refresh_preference,app[0]); - }, - - /** - * When the template attribute is set, the nextmatch widget tries to load - * that template and to fetch the grid which is inside of it. It then calls - * - * @param {string} _value template name - */ - set_template: function(_value) { - if(this.template) - { - // Stop early to prevent unneeded processing, and prevent infinite - // loops if the server changes the template in get_rows - if(this.template == _value) - { - return; - } - - // Free the grid components - they'll be re-created as the template is processed - this.dataview.free(); - this.rowProvider.free(); - this.controller.free(); - - // Free any children from previous template - // They may get left behind because of how detached nodes are processed - // We don't use iterateOver because it checks sub-children - for(var i = this._children.length-1; i >=0 ; i--) - { - var _node = this._children[i]; - if(_node != this.header) { - this.removeChild(_node); - _node.destroy(); - } - } - - // Clear this setting if it's the same as the template, or - // the columns will not be loaded - if(this.template == this.options.settings.columnselection_pref) - { - this.options.settings.columnselection_pref = _value; - } - this.dataview = new et2_dataview(this.innerDiv, this.egw()); - } - - // Create the template - var template = et2_createWidget("template", {"id": _value}, this); - - if (!template) - { - this.egw().debug("error", "Error while loading definition template for " + - "nextmatch widget.",_value); - return; - } - - if(this.options.disabled) - { - return; - } - - // Deferred parse function - template might not be fully loaded - var parse = function(template) - { - // Keep the name of the template, as we'll free up the widget after parsing - this.template = _value; - - // Fetch the grid element and parse it - var definitionGrid = template.getChildren()[0]; - if (definitionGrid && definitionGrid instanceof et2_grid) - { - this._parseGrid(definitionGrid); - } - else - { - this.egw().debug("error", "Nextmatch widget expects a grid to be the " + - "first child of the defined template."); - return; - } - - // Free the template again, but don't remove it - setTimeout(function() { - template.free(); - },1); - - // Call the "setNextmatch" function of all registered - // INextmatchHeader widgets. This updates this.activeFilters.col_filters according - // to what's in the template. - this.iterateOver(function (_node) { - _node.setNextmatch(this); - }, this, et2_INextmatchHeader); - - // Set filters to current values - this.controller.setFilters(this.activeFilters); - - // If no data was sent from the server, and num_rows is 0, the nm will be empty. - // This triggers a cache check. - if(!this.options.settings.num_rows) - { - this.controller.update(); - } - - // Load the default sort order - if (this.options.settings.order && this.options.settings.sort) - { - this.sortBy(this.options.settings.order, - this.options.settings.sort == "ASC", false); - } - - // Start auto-refresh - this._set_autorefresh(this._get_autorefresh()); - }; - - // Template might not be loaded yet, defer parsing - var promise = []; - template.loadingFinished(promise); - - // Wait until template (& children) are done - jQuery.when.apply(null, promise).done( - jQuery.proxy(function() { - parse.call(this, template); - if(!this.dynheight) - { - this.dynheight = this._getDynheight(); - } - this.dynheight.initialized = false; - this.resize(); - }, this) - ); - }, - - // Some accessors to match conventions - set_hide_header: function(hide) { - (hide ? this.header.div.hide() : this.header.div.show()); - }, - - set_header_left: function(template) { - this.header._build_header("left",template); - }, - set_header_right: function(template) { - this.header._build_header("right",template); - }, - set_header_row: function(template) { - this.header._build_header("row",template); - }, - set_no_filter: function(bool, filter_name) { - if(typeof filter_name == 'undefined') - { - filter_name = 'filter'; - } - this.options['no_'+filter_name] = bool; - - var filter = this.header[filter_name]; - if(filter) - { - filter.set_disabled(bool); - } - else if (bool) - { - filter = this.header._build_select(filter_name, 'select', - this.settings[filter_name], this.settings[filter_name+'_no_lang']); - } - }, - set_no_filter2: function(bool) { - this.set_no_filter(bool,'filter2'); - }, - - /** - * Directly change filter value, with no server query. - * - * This allows the server app code to change filter value, and have it - * updated in the client UI. - * - * @param {String|number} value - */ - set_filter: function(value) { - var update = this.update_in_progress; - this.update_in_progress = true; - - this.activeFilters.filter = value; - - // Update the header - this.header.setFilters(this.activeFilters); - - this.update_in_progress = update; - }, - - /** - * Directly change filter2 value, with no server query. - * - * This allows the server app code to change filter2 value, and have it - * updated in the client UI. - * - * @param {String|number} value - */ - set_filter2: function(value) { - var update = this.update_in_progress; - this.update_in_progress = true; - - this.activeFilters.filter2 = value; - - // Update the header - this.header.setFilters(this.activeFilters); - - this.update_in_progress = update; - }, - - /** - * If nextmatch starts disabled, it will need a resize after being shown - * to get all the sizing correct. Override the parent to add the resize - * when enabling. - * - * @param {boolean} _value - */ - set_disabled: function(_value) - { - var previous = this.disabled; - this._super.apply(this, arguments); - - if(previous && !_value) - { - this.resize(); - } - }, - - /** - * Actions are handled by the controller, so ignore these during init. - * - * @param {object} actions - */ - set_actions: function(actions) { - if(actions != this.options.actions && this.controller != null && this.controller._actionManager) - { - for(var i = this.controller._actionManager.children.length - 1; i >= 0; i--) - { - this.controller._actionManager.children[i].remove(); - } - this.options.actions = actions; - this.options.settings.action_links = this.controller._actionLinks = this._get_action_links(actions); - - this.controller._initActions(actions); - } - }, - - /** - * Switch view between row and tile. - * This should be followed by a call to change the template to match, which - * will cause a reload of the grid using the new settings. - * - * @param {string} view Either 'tile' or 'row' - */ - set_view: function(view) - { - // Restrict to the only 2 accepted values - if(view == 'tile') - { - this.view = 'tile'; - } - else - { - this.view = 'row'; - } - }, - - /** - * Set a different / additional handler for dropped files. - * - * File dropping doesn't work with the action system, so we handle it in the - * nextmatch by linking automatically to the target row. This allows an additional handler. - * It should accept a row UID and a File[], and return a boolean Execute the default (link) action - * - * @param {String|Function} handler - */ - set_onfiledrop: function(handler) { - this.options.onfiledrop = handler; - }, - - /** - * Handle drops of files by linking to the row, if possible. - * - * HTML5 / native file drops conflict with jQueryUI draggable, which handles - * all our drop actions. So we side-step the issue by registering an additional - * drop handler on the rows parent. If the row/actions itself doesn't handle - * the drop, it should bubble and get handled here. - * - * @param {object} event - * @param {object} target - */ - handle_drop: function(event, target) { - // Check to see if we can handle the link - // First, find the UID - var row = this.controller.getRowByNode(target); - var uid = row.uid || null; - - // Get the file information - var files = []; - if(event.originalEvent && event.originalEvent.dataTransfer && - event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0) - { - files = event.originalEvent.dataTransfer.files; - } - else - { - return false; - } - - // Exectute the custom handler code - if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) - { - return false; - } - event.stopPropagation(); - event.preventDefault(); - - if(!row || !row.uid) return false; - - // Link the file to the row - // just use a link widget, it's all already done - var split = uid.split('::'); - var link_value = { - to_app: split.shift(), - to_id: split.join('::') - }; - // Create widget and mangle to our needs - var link = et2_createWidget("link-to", {value: link_value}, this); - link.loadingFinished(); - link.file_upload.set_drop_target(false); - - if(row.row.tr) - { - // Ignore most of the UI, just use the status indicators - var status = jQuery(document.createElement("div")) - .addClass('et2_link_to') - .width(row.row.tr.width()) - .position({my: "left top", at: "left top", of: row.row.tr}) - .append(link.status_span) - .append(link.file_upload.progress) - .appendTo(row.row.tr); - - // Bind to link event so we can remove when done - link.div.on('link.et2_link_to', function(e, linked) { - if(!linked) - { - jQuery("li.success", link.file_upload.progress) - .removeClass('success').addClass('validation_error'); - } - else - { - // Update row - link._parent.refresh(uid,'edit'); - } - // Fade out nicely - status.delay(linked ? 1 : 2000) - .fadeOut(500, function() { - link.free(); - status.remove(); - }); - - }); - } - - // Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh - link.file_upload.set_value(files); - }, - - getDOMNode: function(_sender) { - if (_sender == this || typeof _sender === 'undefined') - { - return this.div[0]; - } - if (_sender == this.header) - { - return this.header.div[0]; - } - for (var i = 0; i < this.columns.length; i++) - { - if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) - { - return this.dataview.getHeaderContainerNode(i); - } - } - - // Let header have a chance - if(_sender && _sender._parent && _sender._parent == this) - { - return this.header.getDOMNode(_sender); - } - - return null; - }, - - // Input widget - - /** - * Get the current 'value' for the nextmatch - */ - getValue: function() { - var _ids = this.getSelection(); - - // Translate the internal uids back to server uids - var idsArr = _ids.ids; - for (var i = 0; i < idsArr.length; i++) - { - idsArr[i] = idsArr[i].split("::").pop(); - } - var value = { - "selected": idsArr - }; - jQuery.extend(value, this.activeFilters, this.value); - return value; - }, - resetDirty: function() {}, - isDirty: function() { return typeof this.value !== 'undefined';}, - isValid: function() { return true;}, - set_value: function(_value) - { - this.value = _value; - }, - - // Printing - /** - * Prepare for printing - * - * We check for un-loaded rows, and ask the user what they want to do about them. - * If they want to print them all, we ask the server and print when they're loaded. - */ - beforePrint: function() { - // Add the class, if needed - this.div.addClass('print'); - - // Trigger resize, so we can fit on a page - this.dynheight.outerNode.css('max-width',this.div.css('max-width')); - this.resize(); - // Reset height to auto (after width resize) so there's no restrictions - this.dynheight.innerNode.css('height', 'auto'); - - // Check for rows that aren't loaded yet, or lots of rows - var range = this.controller._grid.getIndexRange(); - this.old_height = this.controller._grid._scrollHeight; - var loaded_count = range.bottom - range.top +1; - var total = this.controller._grid.getTotalCount(); - - // Defer the printing to ask about columns & rows - var defer = jQuery.Deferred(); - - - var pref = this.options.settings.columnselection_pref; - if(pref.indexOf('nextmatch') == 0) - { - pref = 'nextmatch-'+pref; - } - var app = this.getInstanceManager().app; - - var columns = {}; - var columnMgr = this.dataview.getColumnMgr(); - pref += '_print'; - var columns_selected = []; - - // Get column names - for (var i = 0; i < columnMgr.columns.length; i++) - { - var col = columnMgr.columns[i]; - var widget = this.columns[i].widget; - var colName = this._getColumnName(widget); - - if(col.caption && col.visibility !== ET2_COL_VISIBILITY_ALWAYS_NOSELECT && - col.visibility !== ET2_COL_VISIBILITY_DISABLED) - { - columns[colName] = col.caption; - if(col.visibility === ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(colName); - } - // Custom fields get listed separately - if(widget.instanceOf(et2_nextmatch_customfields)) - { - delete(columns[colName]); - colName = widget.id; - if(col.visibility === ET2_COL_VISIBILITY_VISIBLE && ! - jQuery.isEmptyObject(widget.customfields) - ) - { - columns[colName] = col.caption; - for(var field_name in widget.customfields) - { - columns[widget.prefix+field_name] = " - "+widget.customfields[field_name].label; - if(widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0) - { - columns_selected.push(et2_customfields_list.prototype.prefix+field_name); - } - } - } - } - } - - // Preference exists? Set it now - if(this.egw().preference(pref,app)) - { - this.set_columns(jQuery.extend([],this.egw().preference(pref,app))); - } - - var callback = jQuery.proxy(function(button, value) { - if(button === et2_dialog.CANCEL_BUTTON) { - // Give dialog a chance to close, or it will be in the print - window.setTimeout(function() {defer.reject();}, 0); - return; - } - - // Set CSS for orientation - this.div.addClass(value.orientation); - this.egw().set_preference(app,pref+'_orientation',value.orientation); - - - // Try to tell browser about orientation - var css = '@page { size: '+ value.orientation + '; }', - head = document.head || document.getElementsByTagName('head')[0], - style = document.createElement('style'); - - style.type = 'text/css'; - style.media = 'print'; - - if (style.styleSheet){ - style.styleSheet.cssText = css; - } else { - style.appendChild(document.createTextNode(css)); - } - - head.appendChild(style); - this.orientation_style = style; - - // Trigger resize, so we can fit on a page - this.dynheight.outerNode.css('max-width',this.div.css('max-width')); - - // Handle columns - this.set_columns(value.columns); - this.egw().set_preference(app,pref,value.columns); - - var rows = parseInt(value.row_count); - if(rows > total) - { - rows = total; - } - - // If they want the whole thing, style it as all - if(button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) - { - // Add the class, gives more reliable sizing - this.div.addClass('print'); - // Show it all - jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); - } - // We need more rows - if(button === 'dialog[all]' || rows > loaded_count) - { - var count = 0; - var fetchedCount = 0; - var cancel = false; - var nm = this; - var dialog = et2_dialog.show_dialog( - // Abort the long task if they canceled the data load - function() {count = total; cancel=true;window.setTimeout(function() {defer.reject();},0);}, - egw.lang('Loading'), egw.lang('please wait...'),{},[ - {"button_id": et2_dialog.CANCEL_BUTTON,"text": 'cancel',id: 'dialog[cancel]',image: 'cancel'} - ] - ); - - // dataFetch() is asyncronous, so all these requests just get fired off... - // 200 rows chosen arbitrarily to reduce requests. - do { - var ctx = { - "self": this.controller, - "start": count, - "count": Math.min(rows,200), - "lastModification": this.controller._lastModification - }; - if(nm.controller.dataStorePrefix) - { - ctx.prefix = nm.controller.dataStorePrefix; - } - nm.controller.dataFetch({start:count, num_rows: Math.min(rows,200)}, function(data) { - // Keep track - if(data && data.order) - { - fetchedCount += data.order.length; - } - nm.controller._fetchCallback.apply(this, arguments); - - if(fetchedCount >= rows) - { - if(cancel) - { - dialog.destroy(); - defer.reject(); - return; - } - // Use CSS to hide all but the requested rows - // Prevents us from showing more than requested, if actual height was less than average - nm.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+rows+"))"; - egw.css(nm.print_row_selector, 'display: none'); - - // No scrollbar in print view - jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','hidden'); - // Show it all - jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); - - // Grid needs to redraw before it can be printed, so wait - window.setTimeout(jQuery.proxy(function() { - dialog.destroy(); - - // Should be OK to print now - defer.resolve(); - },nm),ET2_GRID_INVALIDATE_TIMEOUT); - - } - - },ctx); - count += 200; - } while (count < rows) - nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (rows+1)); - } - else - { - // Don't need more rows, limit to requested and finish - - // Show it all - jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); - - // Use CSS to hide all but the requested rows - // Prevents us from showing more than requested, if actual height was less than average - this.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+rows+"))"; - egw.css(this.print_row_selector, 'display: none'); - - // No scrollbar in print view - jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','hidden'); - // Give dialog a chance to close, or it will be in the print - window.setTimeout(function() {defer.resolve();}, 0); - } - },this); - var value = { - content: { - row_count: Math.min(100,total), - columns: this.egw().preference(pref,app) || columns_selected, - orientation: this.egw().preference(pref+'_orientation',app) - }, - sel_options: { - columns: columns - } - }; - this._create_print_dialog.call(this, value, callback); - - return defer; - }, - - /** - * Create and show the print dialog, which calls the provided callback when - * done. Broken out for overriding if needed. - * - * @param {Object} value Current settings and preferences, passed to the dialog for - * the template - * @param {Object} value.content - * @param {Object} value.sel_options - * - * @param {function(int, Object)} callback - Process the dialog response, - * format things according to the specified orientation and fetch any needed - * rows. - * - */ - _create_print_dialog: function _create_print_dialog(value, callback) - { - var base_url = this.getInstanceManager().template_base_url; - if (base_url.substr(base_url.length - 1) == '/') base_url = base_url.slice (0, -1); // otherwise we generate a url //api/templates, which is wrong - var tab = this.get_tab_info(); - // Get title for print dialog from settings or tab, if available - var title = this.options.settings.label ? this.options.settings.label : (tab ? tab.label : ''); - var dialog = et2_createWidget("dialog",{ - // If you use a template, the second parameter will be the value of the template, as if it were submitted. - callback: callback, // return false to prevent dialog closing - buttons: et2_dialog.BUTTONS_OK_CANCEL, - title: this.egw().lang('Print') + ' ' + this.egw().lang(title), - template:this.egw().link(base_url+'/api/templates/default/nm_print_dialog.xet'), - value: value - }); - }, - - /** - * Try to clean up the mess we made getting ready for printing - * in beforePrint() - */ - afterPrint: function() { - - this.div.removeClass('print landscape portrait'); - jQuery(this.orientation_style).remove(); - delete this.orientation_style; - - // Put scrollbar back - jQuery('.egwGridView_scrollarea',this.div).css('overflow-y',''); - - // Correct size of grid, and trigger resize to fix it - this.controller._grid.setScrollHeight(this.old_height); - delete this.old_height; - - // Remove CSS rule hiding extra rows - if(this.print_row_selector) - { - egw.css(this.print_row_selector, false); - delete this.print_row_selector; - } - - // Restore columns - var pref = []; - var app = this.getInstanceManager().app; - if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) - { - pref = egw.preference(this.options.settings.columnselection_pref, app); - } - else - { - // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in - pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); - } - if(pref) - { - if(typeof pref === 'string') pref = pref.split(','); - this.set_columns(pref,app); - } - this.dynheight.outerNode.css('max-width','inherit'); - this.resize(); - } -});}).call(this); -et2_register_widget(et2_nextmatch, ["nextmatch"]); - + this.controller._objectManager.clear(); + this.controller.keepSelection(); + } + // Update the filters in the grid controller + this.controller.setFilters(this.activeFilters); + // Update the header + this.header.setFilters(this.activeFilters); + // Update any column filters + this.iterateOver(function (column) { + // Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter + if (typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) + return; + if (typeof column.set_value != "undefined" && column.id) { + column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]); + } + if (column.id && typeof column.get_value == "function") { + this[column.id] = column.get_value(); + } + }, this.activeFilters.col_filter, et2_INextmatchHeader); + // Trigger an update + this.controller.update(true); + if (changed) { + // Highlight matching favorite in sidebox + if (this.getInstanceManager().app) { + var appname = this.getInstanceManager().app; + if (app[appname] && app[appname].highlight_favorite) { + app[appname].highlight_favorite(); + } + } + } + this.update_in_progress = false; + }; + /** + * Refresh given rows for specified change + * + * Change type parameters allows for quicker refresh then complete server side reload: + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload + * + * @param {string[]|string} _row_ids rows to refresh + * @param {?string} _type "update", "edit", "delete" or "add" + * + * @see jsapi.egw_refresh() + * @fires refresh from the widget itself + */ + et2_nextmatch.prototype.refresh = function (_row_ids, _type) { + // Framework trying to refresh, but nextmatch not fully initialized + if (this.controller === null || !this.div) { + return; + } + if (!this.div.is(':visible')) // run refresh, once we become visible again + { + jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function () { this.refresh(); }, this)); + return; + } + if (typeof _type == 'undefined') + _type = 'edit'; + if (typeof _row_ids == 'string' || typeof _row_ids == 'number') + _row_ids = [_row_ids]; + if (typeof _row_ids == "undefined" || _row_ids === null) { + this.applyFilters(); + // Trigger an event so app code can act on it + jQuery(this).triggerHandler("refresh", [this]); + return; + } + if (_type == "delete") { + // Record current & next index + var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; + var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); + var next = (entry.ao ? entry.ao.getNext(_row_ids.length) : null); + if (next == null || !next.id || next.id == uid) { + // No next, select previous + next = (entry.ao ? entry.ao.getPrevious(1) : null); + } + // Stop automatic updating + this.dataview.grid.doInvalidate = false; + for (var i = 0; i < _row_ids.length; i++) { + uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + // Delete from internal references + this.controller.deleteRow(uid); + } + // Select & focus next row + if (next && next.id) { + this.controller._selectionMgr.setSelected(next.id, true); + this.controller._selectionMgr.setFocused(next.id, true); + } + // Update the count + var total = this.dataview.grid._total - _row_ids.length; + // This will remove the last row! + // That's OK, because grid adds one in this.controller.deleteRow() + this.dataview.grid.setTotalCount(total); + // Re-enable automatic updating + this.dataview.grid.doInvalidate = true; + this.dataview.grid.invalidate(); + } + id_loop: for (var i = 0; i < _row_ids.length; i++) { + var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + switch (_type) { + case "update": + if (!this.egw().dataRefreshUID(uid)) { + // Could not update just that row + this.applyFilters(); + break id_loop; + } + break; + case "delete": + // Handled above, more code to execute after loop + break; + case "edit": + case "add": + default: + // Trigger refresh + this.applyFilters(); + break id_loop; + } + } + // Trigger an event so app code can act on it + jQuery(this).triggerHandler("refresh", [this, _row_ids, _type]); + }; + /** + * Gets the selection + * + * @return Object { ids: [UIDs], inverted: boolean} + */ + et2_nextmatch.prototype.getSelection = function () { + var selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null; + if (typeof selected == "object" && selected != null) { + return selected; + } + return { ids: [], all: false }; + }; + /** + * Event handler for when the selection changes + * + * If the onselect attribute was set to a string with javascript code, it will + * be executed "legacy style". You can get the selected values with getSelection(). + * If the onselect attribute is in app.appname.function style, it will be called + * with the nextmatch and an array of selected row IDs. + * + * The array can be empty, if user cleared the selection. + * + * @param action ActionObject From action system. Ignored. + * @param senders ActionObjectImplemetation From action system. Ignored. + */ + et2_nextmatch.prototype.onselect = function (action, senders) { + // Execute the JS code connected to the event handler + if (typeof this.options.onselect == 'function') { + return this.options.onselect.call(this, this.getSelection().ids, this); + } + }; + /** + * Create the dynamic height so nm fills all available space + * + * @returns {undefined} + */ + et2_nextmatch.prototype._getDynheight = function () { + // Find the parent container, either a tab or the main container + var tab = this.get_tab_info(); + if (!tab) { + return new et2_dynheight(this.getInstanceManager().DOMContainer, this.innerDiv, 100); + } + else if (tab && tab.contentDiv) { + return new et2_dynheight(tab.contentDiv, this.innerDiv, 100); + } + return false; + }; + /** + * Generates the column caption for the given column widget + * + * @param {et2_widget} _widget + */ + et2_nextmatch.prototype._genColumnCaption = function (_widget) { + var result = null; + if (typeof _widget._genColumnCaption == "function") + return _widget._genColumnCaption(); + var self = this; + _widget.iterateOver(function (_widget) { + var label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); + if (!label) + return; // skip empty, undefined or null labels + if (!result) { + result = label; + } + else { + result += ", " + label; + } + }, this, et2_INextmatchHeader); + return result; + }; + /** + * Generates the column name (internal) for the given column widget + * Used in preferences to refer to the columns by name instead of position + * + * See _getColumnCaption() for human fiendly captions + * + * @param {et2_widget} _widget + */ + et2_nextmatch.prototype._getColumnName = function (_widget) { + if (typeof _widget._getColumnName == 'function') + return _widget._getColumnName(); + var name = _widget.id; + var child_names = []; + var children = _widget.getChildren(); + for (var i = 0; i < children.length; i++) { + if (children[i].id) + child_names.push(children[i].id); + } + var colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_"); + if (colName == "") { + this.egw().debug("info", "Unable to generate nm column name for ", _widget); + } + return colName; + }; + /** + * Retrieve the user's preferences for this nextmatch merged with defaults + * Column display, column size, etc. + */ + et2_nextmatch.prototype._getPreferences = function () { + // Read preference or default for column visibility + var negated = false; + var columnPreference = ""; + if (this.options.settings.default_cols) { + negated = this.options.settings.default_cols[0] == "!"; + columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols; + } + if (this.options.settings.selectcols && this.options.settings.selectcols.length) { + columnPreference = this.options.settings.selectcols; + negated = false; + } + if (!this.options.settings.columnselection_pref) { + // Set preference name so changes are saved + this.options.settings.columnselection_pref = this.options.template; + } + var app = ''; + var list = []; + if (this.options.settings.columnselection_pref) { + var pref = {}; + list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); + if (this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { + app = list[0].substring('nextmatch'.length + 1); + pref = egw.preference(this.options.settings.columnselection_pref, app); + } + else { + app = list[0]; + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = egw.preference("nextmatch-" + this.options.settings.columnselection_pref, app); + } + if (pref) { + negated = (pref[0] == "!"); + columnPreference = negated ? pref.substring(1) : pref; + } + } + var columnDisplay = []; + // If no column preference or default set, use all columns + if (typeof columnPreference == "string" && columnPreference.length == 0) { + columnDisplay = []; + negated = true; + } + columnDisplay = typeof columnPreference === "string" + ? et2_csvSplit(columnPreference, null, ",") : columnPreference; + // Adjusted column sizes + var size = {}; + if (this.options.settings.columnselection_pref && app) { + var size_pref = this.options.settings.columnselection_pref + "-size"; + // If columnselection pref is missing prefix, add it in + if (size_pref.indexOf('nextmatch') == -1) { + size_pref = 'nextmatch-' + size_pref; + } + size = this.egw().preference(size_pref, app); + } + if (!size) + size = {}; + // Column order + var order = {}; + for (var i = 0; i < columnDisplay.length; i++) { + order[columnDisplay[i]] = i; + } + return { + visible: columnDisplay, + visible_negated: negated, + negated: negated, + size: size, + order: order + }; + }; + /** + * Apply stored user preferences to discovered columns + * + * @param {array} _row + * @param {array} _colData + */ + et2_nextmatch.prototype._applyUserPreferences = function (_row, _colData) { + var prefs = this._getPreferences(); + var columnDisplay = prefs.visible; + var size = prefs.size; + var negated = prefs.visible_negated; + var order = prefs.order; + var colName = ''; + // Add in display preferences + if (columnDisplay && columnDisplay.length > 0) { + RowLoop: for (var i = 0; i < _row.length; i++) { + colName = ''; + if (_row[i].disabled === true) { + _colData[i].visible = false; + continue; + } + // Customfields needs special processing + if (_row[i].widget.instanceOf(et2_nextmatch_customfields)) { + // Find cf field + for (var j = 0; j < columnDisplay.length; j++) { + if (columnDisplay[j].indexOf(_row[i].widget.id) == 0) { + _row[i].widget.options.fields = {}; + for (var k = j; k < columnDisplay.length; k++) { + if (columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) { + _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; + } + } + // Resets field visibility too + _row[i].widget._getColumnName(); + _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); + break; + } + } + // Disable if there are no custom fields + if (jQuery.isEmptyObject(_row[i].widget.customfields)) { + _colData[i].visible = false; + continue; + } + colName = _row[i].widget.id; + } + else { + colName = this._getColumnName(_row[i].widget); + } + if (!colName) + continue; + if (size[colName]) { + // Make sure percentages stay percentages, and forget any preference otherwise + if (_colData[i].width.charAt(_colData[i].width.length - 1) == "%") { + _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; + } + else { + _colData[i].width = parseInt(size[colName]) + 'px'; + } + } + if (!negated) { + _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; + } + for (var j = 0; j < columnDisplay.length; j++) { + if (columnDisplay[j] == colName) { + _colData[i].visible = !negated; + continue RowLoop; + } + } + _colData[i].visible = negated; + } + } + _colData.sort(function (a, b) { + return a.order - b.order; + }); + _row.sort(function (a, b) { + if (typeof a.colData !== 'undefined' && typeof b.colData !== 'undefined') { + return a.colData.order - b.colData.order; + } + else if (typeof a.order !== 'undefined' && typeof b.order !== 'undefined') { + return a.order - b.order; + } + }); + }; + /** + * Take current column display settings and store them in this.egw().preferences + * for next time + */ + et2_nextmatch.prototype._updateUserPreferences = function () { + var colMgr = this.dataview.getColumnMgr(); + var app = ""; + if (!this.options.settings.columnselection_pref) { + this.options.settings.columnselection_pref = this.options.template; + } + var visibility = colMgr.getColumnVisibilitySet(); + var colDisplay = []; + var colSize = {}; + var custom_fields = []; + // visibility is indexed by internal ID, widget is referenced by position, preference needs name + for (var i = 0; i < colMgr.columns.length; i++) { + // @ts-ignore + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if (colName) { + // Server side wants each cf listed as a seperate column + if (widget.instanceOf(et2_nextmatch_customfields)) { + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + for (var name in widget.options.fields) { + if (widget.options.fields[name]) + custom_fields.push(et2_nextmatch_customfields.prefix + name); + } + } + if (visibility[colMgr.columns[i].id].visible) + colDisplay.push(colName); + // When saving sizes, only save columns with explicit values, preserving relative vs fixed + // Others will be left to flex if width changes or more columns are added + if (colMgr.columns[i].relativeWidth) { + colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%"; + } + else if (colMgr.columns[i].fixedWidth) { + colSize[colName] = colMgr.columns[i].fixedWidth; + } + } + else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) { + this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id); + } + } + var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); + var pref = this.options.settings.columnselection_pref; + if (pref.indexOf('nextmatch') == 0) { + app = list[0].substring('nextmatch'.length + 1); + } + else { + app = list[0]; + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = "nextmatch-" + this.options.settings.columnselection_pref; + } + // Server side wants each cf listed as a seperate column + jQuery.merge(colDisplay, custom_fields); + // Update query value, so data source can use visible columns to exclude expensive sub-queries + var oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : []; + this.activeFilters.selectcols = this.sortedColumnsList ? this.sortedColumnsList : colDisplay; + // We don't need to re-query if they've removed a column + var changed = []; + ColLoop: for (var i = 0; i < colDisplay.length; i++) { + for (var j = 0; j < oldCols.length; j++) { + if (colDisplay[i] == oldCols[j]) + continue ColLoop; + } + changed.push(colDisplay[i]); + } + // If a custom field column was added, throw away cache to deal with + // efficient apps that didn't send all custom fields in the first request + var cf_added = jQuery(changed).filter(jQuery(custom_fields)).length > 0; + // Save visible columns + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + this.egw().set_preference(app, pref, this.activeFilters.selectcols.join(","), + // Use callback after the preference gets set to trigger refresh, in case app + // isn't looking at selectcols and just uses preference + cf_added ? jQuery.proxy(function () { if (this.controller) + this.controller.update(true); }, this) : null); + // Save adjusted column sizes + this.egw().set_preference(app, pref + "-size", colSize); + // No significant change (just normal columns shown) and no need to wait, + // but the grid still needs to be redrawn if a custom field was removed because + // the cell content changed. This is a cheaper refresh than the callback, + // this.controller.update(true) + if ((changed.length || custom_fields.length) && !cf_added) + this.applyFilters(); + }; + et2_nextmatch.prototype._parseHeaderRow = function (_row, _colData) { + // Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget + for (var x = 0; x < _row.length; x++) { + if (!_row[x].widget) { + _row[x].widget = et2_createWidget("label", {}); + } + } + // Get column display preference + this._applyUserPreferences(_row, _colData); + // Go over the header row and create the column entries + this.columns = new Array(_row.length); + var columnData = new Array(_row.length); + // No action columns in et2 + var remove_action_index = null; + for (var x = 0; x < _row.length; x++) { + this.columns[x] = jQuery.extend({ + "order": _colData[x] && typeof _colData[x].order !== 'undefined' ? _colData[x].order : x, + "widget": _row[x].widget + }, _colData[x]); + var visibility = (!_colData[x] || _colData[x].visible) ? + et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE : + et2_dataview_grid.ET2_COL_VISIBILITY_INVISIBLE; + if (_colData[x].disabled && _colData[x].disabled !== '' && + this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled)) { + visibility = et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED; + } + columnData[x] = { + "id": "col_" + x, + // @ts-ignore + "order": this.columns[x].order, + "caption": this._genColumnCaption(_row[x].widget), + "visibility": visibility, + "width": _colData[x] ? _colData[x].width : 0 + }; + if (_colData[x].width === 'auto') { + // Column manager does not understand 'auto', which grid widget + // uses if width is not set + columnData[x].width = '100%'; + } + if (_colData[x].minWidth) { + columnData[x].minWidth = _colData[x].minWidth; + } + if (_colData[x].maxWidth) { + columnData[x].maxWidth = _colData[x].maxWidth; + } + // No action columns in et2 + var colName = this._getColumnName(_row[x].widget); + if (colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all') { + remove_action_index = x; + } + else if (!colName) { + // Unnamed column cannot be toggled or saved + columnData[x].visibility = et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT; + } + } + // Remove action column + if (remove_action_index != null) { + this.columns.splice(remove_action_index, remove_action_index); + columnData.splice(remove_action_index, remove_action_index); + _colData.splice(remove_action_index, remove_action_index); + } + // Create the column manager and update the grid container + this.dataview.setColumns(columnData); + for (var x = 0; x < _row.length; x++) { + // Append the widget to this container + this.addChild(_row[x].widget); + } + // Create the nextmatch row provider + this.rowProvider = new et2_nextmatch_rowProvider(this.dataview.rowProvider, this._getSubgrid, this); + // Register handler to update preferences when column properties are changed + var self = this; + this.dataview.onUpdateColumns = function () { + // Use apply to make sure context is there + self._updateUserPreferences.apply(self); + // Allow column widgets a chance to resize + self.iterateOver(function (widget) { widget.resize(); }, self, et2_IResizeable); + }; + // Register handler for column selection popup, or disable + if (this.selectPopup) { + this.selectPopup.remove(); + this.selectPopup = null; + } + if (this.options.settings.no_columnselection) { + this.dataview.selectColumnsClick = function () { return false; }; + jQuery('span.selectcols', this.dataview.headTr).hide(); + } + else { + jQuery('span.selectcols', this.dataview.headTr).show(); + this.dataview.selectColumnsClick = function (event) { + self._selectColumnsClick(event); + }; + } + }; + et2_nextmatch.prototype._parseDataRow = function (_row, _rowData, _colData) { + var columnWidgets = new Array(this.columns.length); + _row.sort(function (a, b) { + return a.colData.order - b.colData.order; + }); + for (var x = 0; x < columnWidgets.length; x++) { + if (typeof _row[x] != "undefined" && _row[x].widget) { + columnWidgets[x] = _row[x].widget; + // Append the widget to this container + this.addChild(_row[x].widget); + } + else { + columnWidgets[x] = _row[x].widget; + } + // Pass along column alignment + if (_row[x].align && columnWidgets[x]) { + columnWidgets[x].align = _row[x].align; + } + } + this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this); + // Create the grid controller + this.controller = new et2_nextmatch_controller(null, this.egw(), this.getInstanceManager().etemplate_exec_id, this, null, this.dataview.grid, this.rowProvider, this.options.settings.action_links, null, this.options.actions); + // Need to trigger empty row the first time + if (total == 0) + this.controller._emptyRow(); + // Set data cache prefix to either provided custom or auto + if (!this.options.settings.dataStorePrefix && this.options.settings.get_rows) { + // Use jsapi data module to update + var list = this.options.settings.get_rows.split('.', 2); + if (list.length < 2) + list = this.options.settings.get_rows.split('_'); // support "app_something::method" + this.options.settings.dataStorePrefix = list[0]; + } + this.controller.setPrefix(this.options.settings.dataStorePrefix); + // Set the view + this.controller._view = this.view; + // Load the initial order + /*this.controller.loadInitialOrder(this._getInitialOrder( + this.options.settings.rows, this.options.settings.row_id + ));*/ + // Set the initial row count + var total = typeof this.options.settings.total != "undefined" ? + this.options.settings.total : 0; + // This triggers an invalidate, which updates the grid + this.dataview.grid.setTotalCount(total); + // Insert any data sent from server, so invalidate finds data already + if (this.options.settings.rows && this.options.settings.num_rows) { + this.controller.loadInitialData(this.options.settings.dataStorePrefix, this.options.settings.row_id, this.options.settings.rows); + // Remove, to prevent duplication + delete this.options.settings.rows; + } + }; + et2_nextmatch.prototype._parseGrid = function (_grid) { + // Search the rows for a header-row - if one is found, parse it + for (var y = 0; y < _grid.rowData.length; y++) { + // Parse the first row as a header, need header to parse the data rows + if (_grid.rowData[y]["class"] == "th" || y == 0) { + this._parseHeaderRow(_grid.cells[y], _grid.colData); + } + else { + this._parseDataRow(_grid.cells[y], _grid.rowData[y], _grid.colData); + } + } + this.dataview.table.resize(); + }; + et2_nextmatch.prototype._getSubgrid = function (_row, _data, _controller) { + // Fetch the id of the element described by _data, this will be the + // parent_id of the elements in the subgrid + var rowId = _data.content[this.options.settings.row_id]; + // Create a new grid with the row as parent and the dataview grid as + // parent grid + var grid = new et2_dataview_grid(_row, this.dataview.grid); + // Create a new controller for the grid + var controller = new et2_nextmatch_controller(_controller, this.egw(), this.getInstanceManager().etemplate_exec_id, this, rowId, grid, this.rowProvider, this.options.settings.action_links, _controller.getObjectManager()); + controller.update(); + // Register inside the destruction callback of the grid + grid.setDestroyCallback(function () { + controller.free(); + }); + return grid; + }; + et2_nextmatch.prototype._getInitialOrder = function (_rows, _rowId) { + var _order = []; + // Get the length of the non-numerical rows arra + var len = 0; + for (var key in _rows) { + if (!isNaN(parseInt(key)) && parseInt(key) > len) + len = parseInt(key); + } + // Iterate over the rows + for (var i = 0; i < len; i++) { + // Get the uid from the data + var uid = this.egw().appName + '::' + _rows[i][_rowId]; + // Store the data for that uid + this.egw().dataStoreUID(uid, _rows[i]); + // Push the uid onto the order array + _order.push(uid); + } + return _order; + }; + et2_nextmatch.prototype._selectColumnsClick = function (e) { + var self = this; + var columnMgr = this.dataview.getColumnMgr(); + // ID for faking letter selection in column selection + var LETTERS = '~search_letter~'; + var columns = {}; + var columns_selected = []; + for (var i = 0; i < columnMgr.columns.length; i++) { + var col = columnMgr.columns[i]; + var widget = this.columns[i].widget; + if (col.caption && col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED) { + columns[col.id] = col.caption; + if (col.visibility == et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE) + columns_selected.push(col.id); + } + // Custom fields get listed separately + if (widget.instanceOf(et2_nextmatch_customfields)) { + if (jQuery.isEmptyObject(widget.customfields)) { + // No customfields defined, don't show column + delete (columns[col.id]); + continue; + } + for (var field_name in widget.customfields) { + columns[et2_nextmatch_customfields.prefix + field_name] = " - " + + widget.customfields[field_name].label; + if (widget.options.fields[field_name]) + columns_selected.push(et2_customfields_list.prefix + field_name); + } + } + } + // Letter search + if (this.options.settings.lettersearch) { + columns[LETTERS] = egw.lang('Search letter'); + if (this.header.lettersearch.is(':visible')) + columns_selected.push(LETTERS); + } + // Build the popup + if (!this.selectPopup) { + var select = et2_createWidget("select", { + multiple: true, + rows: 8, + empty_label: this.egw().lang("select columns"), + selected_first: false, + value_class: "selcolumn_sortable_" + }, this); + select.set_select_options(columns); + select.set_value(columns_selected); + var autoRefresh = et2_createWidget("select", { + "empty_label": "Refresh" + }, this); + autoRefresh.set_id("nm_autorefresh"); + autoRefresh.set_select_options({ + // Cause [unknown] problems with mail + //30: "30 seconds", + //60: "1 Minute", + 300: "5 Minutes", + 900: "15 Minutes", + 1800: "30 Minutes" + }); + autoRefresh.set_value(this._get_autorefresh()); + autoRefresh.set_statustext(egw.lang("Automatically refresh list")); + var defaultCheck = et2_createWidget("select", { "empty_label": "Preference" }, this); + defaultCheck.set_id('nm_col_preference'); + defaultCheck.set_select_options({ + 'default': { label: 'Default', title: 'Set these columns as the default' }, + 'reset': { label: 'Reset', title: "Reset all user's column preferences" }, + 'force': { label: 'Force', title: 'Force column preference so users cannot change it' } + }); + defaultCheck.set_value(this.options.settings.columns_forced ? 'force' : ''); + var okButton = et2_createWidget("buttononly", { "background_image": true, image: "check" }, this); + okButton.set_label(this.egw().lang("ok")); + okButton.onclick = function () { + // Update visibility + var visibility = {}; + for (var i = 0; i < columnMgr.columns.length; i++) { + var col = columnMgr.columns[i]; + if (col.caption && col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED) { + visibility[col.id] = { visible: false }; + } + } + var value = select.getValue(); + // Update & remove letter filter + if (self.header.lettersearch) { + var show_letters = true; + if (value.indexOf(LETTERS) >= 0) { + value.splice(value.indexOf(LETTERS), 1); + } + else { + show_letters = false; + } + self._set_lettersearch(show_letters); + } + var column = 0; + for (var i = 0; i < value.length; i++) { + // Handle skipped columns + while (value[i] != "col_" + column && column < columnMgr.columns.length) { + column++; + } + if (visibility[value[i]]) { + visibility[value[i]].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if (self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) { + var cf = self.columns[column].widget.options.customfields; + var visible = self.columns[column].widget.options.fields; + // Turn off all custom fields + for (var field_name in cf) { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for (var j = 0; j < value.length; j++) { + if (value[j].indexOf(et2_customfields_list.prefix) != 0) + continue; + visible[value[j].substring(1)] = true; + i++; + } + self.columns[column].widget.set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + this.sortedColumnsList = []; + jQuery(select.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function (i, v) { + var data_id = v.getAttribute('data-value'); + var value = select.getValue(); + if (data_id.match(/^col_/) && value.indexOf(data_id) != -1) { + var col_id = data_id.replace('col_', ''); + var col_widget = self.columns[col_id].widget; + if (col_widget.customfields) { + self.sortedColumnsList.push(col_widget.id); + for (var field_name in col_widget.customfields) { + if (jQuery.isEmptyObject(col_widget.options.fields) || col_widget.options.fields[field_name] == true) { + self.sortedColumnsList.push(et2_customfields_list.prefix + field_name); + } + } + } + else { + self.sortedColumnsList.push(self._getColumnName(col_widget)); + } + } + }); + // Hide popup + self.selectPopup.toggle(); + self.dataview.updateColumns(); + // Auto refresh + self._set_autorefresh(autoRefresh.get_value()); + // Set default or clear forced + if (show_letters) { + self.activeFilters.selectcols.push('lettersearch'); + } + self.getInstanceManager().submit(); + self.selectPopup = null; + }; + var cancelButton = et2_createWidget("buttononly", { "background_image": true, image: "cancel" }, this); + cancelButton.set_label(this.egw().lang("cancel")); + cancelButton.onclick = function () { + self.selectPopup.toggle(); + self.selectPopup = null; + }; + var $select = jQuery(select.getDOMNode()); + $select.find('.ui-multiselect-checkboxes').sortable({ + placeholder: 'ui-fav-sortable-placeholder', + items: 'li[class^="selcolumn_sortable_col"]', + cancel: 'li[class^="selcolumn_sortable_#"]', + cursor: "move", + tolerance: "pointer", + axis: 'y', + containment: "parent", + delay: 250, + beforeStop: function (event, ui) { + jQuery('li[class^="selcolumn_sortable_#"]', this).css({ + opacity: 1 + }); + }, + start: function (event, ui) { + jQuery('li[class^="selcolumn_sortable_#"]', this).css({ + opacity: 0.5 + }); + }, + sort: function (event, ui) { + jQuery(this).sortable("refreshPositions"); + } + }); + $select.disableSelection(); + $select.find('li[class^="selcolumn_sortable_"]').each(function (i, v) { + // @ts-ignore + jQuery(v).attr('data-value', (jQuery(v).find('input')[0].value)); + }); + var $footerWrap = jQuery(document.createElement("div")) + .addClass('dialogFooterToolbar') + .append(okButton.getDOMNode()) + .append(cancelButton.getDOMNode()); + this.selectPopup = jQuery(document.createElement("div")) + .addClass("colselection ui-dialog ui-widget-content") + .append(select.getDOMNode()) + .append($footerWrap) + .appendTo(this.innerDiv); + // Add autorefresh + $footerWrap.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode())); + // Add default checkbox for admins + var apps = this.egw().user('apps'); + if (apps['admin']) { + $footerWrap.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode())); + } + } + else { + this.selectPopup.toggle(); + } + var t_position = jQuery(e.target).position(); + var s_position = this.div.position(); + var max_height = this.getDOMNode().getElementsByClassName('egwGridView_outer')[0]['tBodies'][0].clientHeight - + (2 * this.selectPopup.find('.dialogFooterToolbar').height()); + this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height', max_height); + this.selectPopup.css("top", t_position.top) + .css("left", s_position.left + this.div.width() - this.selectPopup.width()); + }; + /** + * Set the currently displayed columns, without updating user's preference + * + * @param {string[]} column_list List of column names + * @param {boolean} trigger_update =false - explicitly trigger an update + */ + et2_nextmatch.prototype.set_columns = function (column_list, trigger_update) { + if (trigger_update === void 0) { trigger_update = false; } + var columnMgr = this.dataview.getColumnMgr(); + var visibility = {}; + // Initialize to false + for (var i = 0; i < columnMgr.columns.length; i++) { + var col = columnMgr.columns[i]; + if (col.caption && col.visibility != et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT) { + visibility[col.id] = { visible: false }; + } + } + for (var i = 0; i < this.columns.length; i++) { + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if (column_list.indexOf(colName) !== -1 && + typeof visibility[columnMgr.columns[i].id] !== 'undefined') { + visibility[columnMgr.columns[i].id].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if (widget && widget.instanceOf(et2_nextmatch_customfields)) { + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + if (column_list.indexOf(colName) !== -1) { + visibility[columnMgr.columns[i].id].visible = true; + } + var cf = this.columns[i].widget.options.customfields; + var visible = this.columns[i].widget.options.fields; + // Turn off all custom fields + for (var field_name in cf) { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for (var j = 0; j < column_list.length; j++) { + if (column_list[j].indexOf(et2_customfields_list.prefix) != 0) + continue; + visible[column_list[j].substring(1)] = true; + } + widget.set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + // We don't want to update user's preference, so directly update + this.dataview._updateColumns(); + // Allow column widgets a chance to resize + this.iterateOver(function (widget) { widget.resize(); }, this, et2_IResizeable); + }; + /** + * Set the letter search preference, and update the UI + * + * @param {boolean} letters_on + */ + et2_nextmatch.prototype._set_lettersearch = function (letters_on) { + if (letters_on) { + this.header.lettersearch.show(); + } + else { + this.header.lettersearch.hide(); + } + var lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch"; + this.egw().set_preference(this.egw().getAppName(), lettersearch_preference, letters_on); + }; + /** + * Set the auto-refresh time period, and starts the timer if not started + * + * @param time int Refresh period, in seconds + */ + et2_nextmatch.prototype._set_autorefresh = function (time) { + // Store preference + var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; + var app = this.options.template.split("."); + if (this._get_autorefresh() != time) { + this.egw().set_preference(app[0], refresh_preference, time); + } + // Start / update timer + if (this._autorefresh_timer) { + window.clearInterval(this._autorefresh_timer); + delete this._autorefresh_timer; + } + if (time > 0) { + this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000); + // Bind to tab show/hide events, so that we don't bother refreshing in the background + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function (e) { + // Stop + window.clearInterval(this._autorefresh_timer); + jQuery(e.target).off(e); + // If the autorefresh time is up, bind once to trigger a refresh + // (if needed) when tab is activated again + this._autorefresh_timer = setTimeout(jQuery.proxy(function () { + // Check in case it was stopped / destroyed since + if (!this._autorefresh_timer || !this.getInstanceManager()) + return; + jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function () { this.refresh(); }, this)); + }, this), time * 1000); + }, this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function (e) { + // Start normal autorefresh timer again + this._set_autorefresh(this._get_autorefresh()); + jQuery(e.target).off(e); + }, this)); + } + }; + /** + * Get the auto-refresh timer + * + * @return int Refresh period, in secods + */ + et2_nextmatch.prototype._get_autorefresh = function () { + var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; + var app = this.options.template.split("."); + return this.egw().preference(refresh_preference, app[0]); + }; + /** + * When the template attribute is set, the nextmatch widget tries to load + * that template and to fetch the grid which is inside of it. It then calls + * + * @param {string} _value template name + */ + et2_nextmatch.prototype.set_template = function (template_name) { + if (this.template) { + // Stop early to prevent unneeded processing, and prevent infinite + // loops if the server changes the template in get_rows + if (this.template == template_name) { + return; + } + // Free the grid components - they'll be re-created as the template is processed + this.dataview.free(); + this.rowProvider.free(); + this.controller.free(); + // Free any children from previous template + // They may get left behind because of how detached nodes are processed + // We don't use iterateOver because it checks sub-children + for (var i = this._children.length - 1; i >= 0; i--) { + var _node = this._children[i]; + if (_node != this.header) { + this.removeChild(_node); + _node.destroy(); + } + } + // Clear this setting if it's the same as the template, or + // the columns will not be loaded + if (this.template == this.options.settings.columnselection_pref) { + this.options.settings.columnselection_pref = template_name; + } + this.dataview = new et2_dataview(this.innerDiv, this.egw()); + } + // Create the template + var template = et2_createWidget("template", { "id": template_name }, this); + if (!template) { + this.egw().debug("error", "Error while loading definition template for " + + "nextmatch widget.", template_name); + return; + } + if (this.options.disabled) { + return; + } + // Deferred parse function - template might not be fully loaded + var parse = function (template) { + // Keep the name of the template, as we'll free up the widget after parsing + this.template = template_name; + // Fetch the grid element and parse it + var definitionGrid = template.getChildren()[0]; + if (definitionGrid && definitionGrid instanceof et2_grid) { + this._parseGrid(definitionGrid); + } + else { + this.egw().debug("error", "Nextmatch widget expects a grid to be the " + + "first child of the defined template."); + return; + } + // Free the template again, but don't remove it + setTimeout(function () { + template.free(); + }, 1); + // Call the "setNextmatch" function of all registered + // INextmatchHeader widgets. This updates this.activeFilters.col_filters according + // to what's in the template. + this.iterateOver(function (_node) { + _node.setNextmatch(this); + }, this, et2_INextmatchHeader); + // Set filters to current values + this.controller.setFilters(this.activeFilters); + // If no data was sent from the server, and num_rows is 0, the nm will be empty. + // This triggers a cache check. + if (!this.options.settings.num_rows) { + this.controller.update(); + } + // Load the default sort order + if (this.options.settings.order && this.options.settings.sort) { + this.sortBy(this.options.settings.order, this.options.settings.sort == "ASC", false); + } + // Start auto-refresh + this._set_autorefresh(this._get_autorefresh()); + }; + // Template might not be loaded yet, defer parsing + var promise = []; + template.loadingFinished(promise); + // Wait until template (& children) are done + jQuery.when.apply(null, promise).done(jQuery.proxy(function () { + parse.call(this, template); + if (!this.dynheight) { + this.dynheight = this._getDynheight(); + } + this.dynheight.initialized = false; + this.resize(); + }, this)); + }; + // Some accessors to match conventions + et2_nextmatch.prototype.set_hide_header = function (hide) { + (hide ? this.header.div.hide() : this.header.div.show()); + }; + et2_nextmatch.prototype.set_header_left = function (template) { + this.header._build_header("left", template); + }; + et2_nextmatch.prototype.set_header_right = function (template) { + this.header._build_header("right", template); + }; + et2_nextmatch.prototype.set_header_row = function (template) { + this.header._build_header("row", template); + }; + et2_nextmatch.prototype.set_no_filter = function (bool, filter_name) { + if (typeof filter_name == 'undefined') { + filter_name = 'filter'; + } + this.options['no_' + filter_name] = bool; + var filter = this.header[filter_name]; + if (filter) { + filter.set_disabled(bool); + } + else if (bool) { + filter = this.header._build_select(filter_name, 'select', this.settings[filter_name], this.settings[filter_name + '_no_lang']); + } + }; + et2_nextmatch.prototype.set_no_filter2 = function (bool) { + this.set_no_filter(bool, 'filter2'); + }; + /** + * Directly change filter value, with no server query. + * + * This allows the server app code to change filter value, and have it + * updated in the client UI. + * + * @param {String|number} value + */ + et2_nextmatch.prototype.set_filter = function (value) { + var update = this.update_in_progress; + this.update_in_progress = true; + this.activeFilters.filter = value; + // Update the header + this.header.setFilters(this.activeFilters); + this.update_in_progress = update; + }; + /** + * Directly change filter2 value, with no server query. + * + * This allows the server app code to change filter2 value, and have it + * updated in the client UI. + * + * @param {String|number} value + */ + et2_nextmatch.prototype.set_filter2 = function (value) { + var update = this.update_in_progress; + this.update_in_progress = true; + this.activeFilters.filter2 = value; + // Update the header + this.header.setFilters(this.activeFilters); + this.update_in_progress = update; + }; + /** + * If nextmatch starts disabled, it will need a resize after being shown + * to get all the sizing correct. Override the parent to add the resize + * when enabling. + * + * @param {boolean} _value + */ + et2_nextmatch.prototype.set_disabled = function (_value) { + var previous = this.disabled; + _super_1.prototype.set_disabled.call(this, _value); + if (previous && !_value) { + this.resize(); + } + }; + /** + * Actions are handled by the controller, so ignore these during init. + * + * @param {object} actions + */ + et2_nextmatch.prototype.set_actions = function (actions) { + if (actions != this.options.actions && this.controller != null && this.controller._actionManager) { + for (var i = this.controller._actionManager.children.length - 1; i >= 0; i--) { + this.controller._actionManager.children[i].remove(); + } + this.options.actions = actions; + this.options.settings.action_links = this.controller._actionLinks = this._get_action_links(actions); + this.controller._initActions(actions); + } + }; + /** + * Switch view between row and tile. + * This should be followed by a call to change the template to match, which + * will cause a reload of the grid using the new settings. + * + * @param {string} view Either 'tile' or 'row' + */ + et2_nextmatch.prototype.set_view = function (view) { + // Restrict to the only 2 accepted values + if (view == 'tile') { + this.view = 'tile'; + } + else { + this.view = 'row'; + } + }; + /** + * Set a different / additional handler for dropped files. + * + * File dropping doesn't work with the action system, so we handle it in the + * nextmatch by linking automatically to the target row. This allows an additional handler. + * It should accept a row UID and a File[], and return a boolean Execute the default (link) action + * + * @param {String|Function} handler + */ + et2_nextmatch.prototype.set_onfiledrop = function (handler) { + this.options.onfiledrop = handler; + }; + /** + * Handle drops of files by linking to the row, if possible. + * + * HTML5 / native file drops conflict with jQueryUI draggable, which handles + * all our drop actions. So we side-step the issue by registering an additional + * drop handler on the rows parent. If the row/actions itself doesn't handle + * the drop, it should bubble and get handled here. + * + * @param {object} event + * @param {object} target + */ + et2_nextmatch.prototype.handle_drop = function (event, target) { + // Check to see if we can handle the link + // First, find the UID + var row = this.controller.getRowByNode(target); + var uid = row.uid || null; + // Get the file information + var files = []; + if (event.originalEvent && event.originalEvent.dataTransfer && + event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0) { + files = event.originalEvent.dataTransfer.files; + } + else { + return false; + } + // Exectute the custom handler code + if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) { + return false; + } + event.stopPropagation(); + event.preventDefault(); + if (!row || !row.uid) + return false; + // Link the file to the row + // just use a link widget, it's all already done + var split = uid.split('::'); + var link_value = { + to_app: split.shift(), + to_id: split.join('::') + }; + // Create widget and mangle to our needs + var link = et2_createWidget("link-to", { value: link_value }, this); + link.loadingFinished(); + link.file_upload.set_drop_target(false); + if (row.row.tr) { + // Ignore most of the UI, just use the status indicators + var status = jQuery(document.createElement("div")) + .addClass('et2_link_to') + .width(row.row.tr.width()) + .position({ my: "left top", at: "left top", of: row.row.tr }) + .append(link.status_span) + .append(link.file_upload.progress) + .appendTo(row.row.tr); + // Bind to link event so we can remove when done + link.div.on('link.et2_link_to', function (e, linked) { + if (!linked) { + jQuery("li.success", link.file_upload.progress) + .removeClass('success').addClass('validation_error'); + } + else { + // Update row + link._parent.refresh(uid, 'edit'); + } + // Fade out nicely + status.delay(linked ? 1 : 2000) + .fadeOut(500, function () { + link.free(); + status.remove(); + }); + }); + } + // Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh + link.file_upload.set_value(files); + }; + et2_nextmatch.prototype.getDOMNode = function (_sender) { + if (_sender == this || typeof _sender === 'undefined') { + return this.div[0]; + } + if (_sender == this.header) { + return this.header.div[0]; + } + for (var i = 0; i < this.columns.length; i++) { + if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) { + return this.dataview.getHeaderContainerNode(i); + } + } + // Let header have a chance + if (_sender && _sender._parent && _sender._parent == this) { + return this.header.getDOMNode(_sender); + } + return null; + }; + // Input widget + /** + * Get the current 'value' for the nextmatch + */ + et2_nextmatch.prototype.getValue = function () { + var _ids = this.getSelection(); + // Translate the internal uids back to server uids + var idsArr = _ids.ids; + for (var i = 0; i < idsArr.length; i++) { + idsArr[i] = idsArr[i].split("::").pop(); + } + var value = { + "selected": idsArr + }; + jQuery.extend(value, this.activeFilters, this.value); + return value; + }; + et2_nextmatch.prototype.resetDirty = function () { }; + et2_nextmatch.prototype.isDirty = function () { return typeof this.value !== 'undefined'; }; + et2_nextmatch.prototype.isValid = function () { return true; }; + et2_nextmatch.prototype.set_value = function (_value) { + this.value = _value; + }; + // Printing + /** + * Prepare for printing + * + * We check for un-loaded rows, and ask the user what they want to do about them. + * If they want to print them all, we ask the server and print when they're loaded. + */ + et2_nextmatch.prototype.beforePrint = function () { + // Add the class, if needed + this.div.addClass('print'); + // Trigger resize, so we can fit on a page + this.dynheight.outerNode.css('max-width', this.div.css('max-width')); + this.resize(); + // Reset height to auto (after width resize) so there's no restrictions + this.dynheight.innerNode.css('height', 'auto'); + // Check for rows that aren't loaded yet, or lots of rows + var range = this.controller._grid.getIndexRange(); + this.print.old_height = this.controller._grid._scrollHeight; + var loaded_count = range.bottom - range.top + 1; + var total = this.controller._grid.getTotalCount(); + // Defer the printing to ask about columns & rows + var defer = jQuery.Deferred(); + var pref = this.options.settings.columnselection_pref; + if (pref.indexOf('nextmatch') == 0) { + pref = 'nextmatch-' + pref; + } + var app = this.getInstanceManager().app; + var columns = {}; + var columnMgr = this.dataview.getColumnMgr(); + pref += '_print'; + var columns_selected = []; + // Get column names + for (var i = 0; i < columnMgr.columns.length; i++) { + var col = columnMgr.columns[i]; + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if (col.caption && col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED) { + columns[colName] = col.caption; + if (col.visibility === et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE) + columns_selected.push(colName); + } + // Custom fields get listed separately + if (widget.instanceOf(et2_nextmatch_customfields)) { + delete (columns[colName]); + colName = widget.id; + if (col.visibility === et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE && !jQuery.isEmptyObject(widget.customfields)) { + columns[colName] = col.caption; + for (var field_name in widget.customfields) { + columns[et2_nextmatch_customfields.prefix + field_name] = " - " + widget.customfields[field_name].label; + if (widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0) { + columns_selected.push(et2_nextmatch_customfields.prefix + field_name); + } + } + } + } + } + // Preference exists? Set it now + if (this.egw().preference(pref, app)) { + this.set_columns(jQuery.extend([], this.egw().preference(pref, app))); + } + var callback = jQuery.proxy(function (button, value) { + if (button === et2_dialog.CANCEL_BUTTON) { + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function () { defer.reject(); }, 0); + return; + } + // Set CSS for orientation + this.div.addClass(value.orientation); + this.egw().set_preference(app, pref + '_orientation', value.orientation); + // Try to tell browser about orientation + var css = '@page { size: ' + value.orientation + '; }', head = document.head || document.getElementsByTagName('head')[0], style = document.createElement('style'); + style.type = 'text/css'; + style.media = 'print'; + // @ts-ignore + if (style.styleSheet) { + // @ts-ignore + style.styleSheet.cssText = css; + } + else { + style.appendChild(document.createTextNode(css)); + } + head.appendChild(style); + this.print.orientation_style = style; + // Trigger resize, so we can fit on a page + this.dynheight.outerNode.css('max-width', this.div.css('max-width')); + // Handle columns + this.set_columns(value.columns); + this.egw().set_preference(app, pref, value.columns); + var rows = parseInt(value.row_count); + if (rows > total) { + rows = total; + } + // If they want the whole thing, style it as all + if (button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) { + // Add the class, gives more reliable sizing + this.div.addClass('print'); + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + } + // We need more rows + if (button === 'dialog[all]' || rows > loaded_count) { + var count = 0; + var fetchedCount = 0; + var cancel = false; + var nm = this; + var dialog = et2_dialog.show_dialog( + // Abort the long task if they canceled the data load + function () { count = total; cancel = true; window.setTimeout(function () { defer.reject(); }, 0); }, egw.lang('Loading'), egw.lang('please wait...'), {}, [ + { "button_id": et2_dialog.CANCEL_BUTTON, "text": 'cancel', id: 'dialog[cancel]', image: 'cancel' } + ]); + // dataFetch() is asyncronous, so all these requests just get fired off... + // 200 rows chosen arbitrarily to reduce requests. + do { + var ctx = { + "self": this.controller, + "start": count, + "count": Math.min(rows, 200), + "lastModification": this.controller._lastModification + }; + if (nm.controller.dataStorePrefix) { + // @ts-ignore + ctx.prefix = nm.controller.dataStorePrefix; + } + nm.controller.dataFetch({ start: count, num_rows: Math.min(rows, 200) }, function (data) { + // Keep track + if (data && data.order) { + fetchedCount += data.order.length; + } + nm.controller._fetchCallback.apply(this, arguments); + if (fetchedCount >= rows) { + if (cancel) { + dialog.destroy(); + defer.reject(); + return; + } + // Use CSS to hide all but the requested rows + // Prevents us from showing more than requested, if actual height was less than average + nm.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+" + rows + "))"; + egw.css(nm.print_row_selector, 'display: none'); + // No scrollbar in print view + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + // Grid needs to redraw before it can be printed, so wait + window.setTimeout(jQuery.proxy(function () { + dialog.destroy(); + // Should be OK to print now + defer.resolve(); + }, nm), et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + } + }, ctx); + count += 200; + } while (count < rows); + nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (rows + 1)); + } + else { + // Don't need more rows, limit to requested and finish + // Show it all + jQuery('.egwGridView_scrollarea', this.div).css('height', 'auto'); + // Use CSS to hide all but the requested rows + // Prevents us from showing more than requested, if actual height was less than average + this.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+" + rows + "))"; + egw.css(this.print_row_selector, 'display: none'); + // No scrollbar in print view + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', 'hidden'); + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function () { defer.resolve(); }, 0); + } + }, this); + var value = { + content: { + row_count: Math.min(100, total), + columns: this.egw().preference(pref, app) || columns_selected, + orientation: this.egw().preference(pref + '_orientation', app) + }, + sel_options: { + columns: columns + } + }; + this._create_print_dialog.call(this, value, callback); + return defer; + }; + /** + * Create and show the print dialog, which calls the provided callback when + * done. Broken out for overriding if needed. + * + * @param {Object} value Current settings and preferences, passed to the dialog for + * the template + * @param {Object} value.content + * @param {Object} value.sel_options + * + * @param {function(int, Object)} callback - Process the dialog response, + * format things according to the specified orientation and fetch any needed + * rows. + * + */ + et2_nextmatch.prototype._create_print_dialog = function (value, callback) { + var base_url = this.getInstanceManager().template_base_url; + if (base_url.substr(base_url.length - 1) == '/') + base_url = base_url.slice(0, -1); // otherwise we generate a url //api/templates, which is wrong + var tab = this.get_tab_info(); + // Get title for print dialog from settings or tab, if available + var title = this.options.settings.label ? this.options.settings.label : (tab ? tab.label : ''); + var dialog = et2_createWidget("dialog", { + // If you use a template, the second parameter will be the value of the template, as if it were submitted. + callback: callback, + buttons: et2_dialog.BUTTONS_OK_CANCEL, + title: this.egw().lang('Print') + ' ' + this.egw().lang(title), + template: this.egw().link(base_url + '/api/templates/default/nm_print_dialog.xet'), + value: value + }); + }; + /** + * Try to clean up the mess we made getting ready for printing + * in beforePrint() + */ + et2_nextmatch.prototype.afterPrint = function () { + this.div.removeClass('print landscape portrait'); + jQuery(this.print.orientation_style).remove(); + delete this.print.orientation_style; + // Put scrollbar back + jQuery('.egwGridView_scrollarea', this.div).css('overflow-y', ''); + // Correct size of grid, and trigger resize to fix it + this.controller._grid.setScrollHeight(this.print.old_height); + delete this.print.old_height; + // Remove CSS rule hiding extra rows + if (this.print.row_selector) { + egw.css(this.print.row_selector, false); + delete this.print.row_selector; + } + // Restore columns + var pref = []; + var app = this.getInstanceManager().app; + if (this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) { + pref = egw.preference(this.options.settings.columnselection_pref, app); + } + else { + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = egw.preference("nextmatch-" + this.options.settings.columnselection_pref, app); + } + if (pref) { + if (typeof pref === 'string') + pref = pref.split(','); + this.set_columns(pref, app); + } + this.dynheight.outerNode.css('max-width', 'inherit'); + this.resize(); + }; + et2_nextmatch._attributes = { + // These normally set in settings, but broken out into attributes to allow run-time changes + "template": { + "name": "Template", + "type": "string", + "description": "The id of the template which contains the grid layout." + }, + "hide_header": { + "name": "Hide header", + "type": "boolean", + "description": "Hide the header", + "default": false + }, + "header_left": { + "name": "Left custom template", + "type": "string", + "description": "Customise the nextmatch - left side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "header_right": { + "name": "Right custom template", + "type": "string", + "description": "Customise the nextmatch - right side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "header_row": { + "name": "Inline custom template", + "type": "string", + "description": "Customise the nextmatch - inline, after row count. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "no_filter": { + "name": "No filter", + "type": "boolean", + "description": "Hide the first filter", + "default": et2_no_init + }, + "no_filter2": { + "name": "No filter2", + "type": "boolean", + "description": "Hide the second filter", + "default": et2_no_init + }, + "view": { + "name": "View", + "type": "string", + "description": "Display entries as either 'row' or 'tile'. A matching template must also be set after changing this.", + "default": et2_no_init + }, + "onselect": { + "name": "onselect", + "type": "js", + "default": et2_no_init, + "description": "JS code which gets executed when rows are selected. Can also be a app.appname.func(selected) style method" + }, + "onfiledrop": { + "name": "onFileDrop", + "type": "js", + "default": et2_no_init, + "description": "JS code that gets executed when a _file_ is dropped on a row. Other drop interactions are handled by the action system. Return false to prevent the default link action." + }, + "settings": { + "name": "Settings", + "type": "any", + "description": "The nextmatch settings", + "default": {} + } + }; + return et2_nextmatch; +}(et2_core_DOMWidget_1.et2_DOMWidget)); +exports.et2_nextmatch = et2_nextmatch; +et2_core_widget_1.et2_register_widget(et2_nextmatch, ["nextmatch"]); /** * Standard nextmatch header bar, containing filters, search, record count, letter filters, etc. * @@ -2510,1368 +2017,1116 @@ et2_register_widget(et2_nextmatch, ["nextmatch"]); * actually load templates from the server. * @augments et2_DOMWidget */ -var et2_nextmatch_header_bar = (function(){ "use strict"; return et2_DOMWidget.extend(et2_INextmatchHeader, -{ - attributes: { - "filter_label": { - "name": "Filter label", - "type": "string", - "description": "Label for filter", - "default": "", - "translate": true - }, - "filter_help": { - "name": "Filter help", - "type": "string", - "description": "Help message for filter", - "default": "", - "translate": true - }, - "filter": { - "name": "Filter value", - "type": "any", - "description": "Current value for filter", - "default": "" - }, - "no_filter": { - "name": "No filter", - "type": "boolean", - "description": "Remove filter", - "default": false - } - }, - headers: [], - header_div: [], - - /** - * Constructor - * - * @param nextmatch - * @param nm_div - * @memberOf et2_nextmatch_header_bar - */ - init: function(nextmatch, nm_div) { - this._super.apply(this, [nextmatch,nextmatch.options.settings]); - this.nextmatch = nextmatch; - this.div = jQuery(document.createElement("div")) - .addClass("nextmatch_header"); - this._createHeader(); - - // Flag to avoid loops while updating filters - this.update_in_progress = false; - }, - - destroy: function() { - this.nextmatch = null; - - this._super.apply(this, arguments); - this.div = null; - }, - - setNextmatch: function(nextmatch) { - var create_once = (this.nextmatch == null); - this.nextmatch = nextmatch; - if(create_once) - { - this._createHeader(); - } - - // Bind row count - this.nextmatch.dataview.grid.setInvalidateCallback(function () { - this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + ""); - }, this); - }, - - /** - * Actions are handled by the controller, so ignore these - * - * @param {object} actions - */ - set_actions: function(actions) {}, - - _createHeader: function() { - - var self = this; - var nm_div = this.nextmatch.div; - var settings = this.nextmatch.options.settings; - - this.div.prependTo(nm_div); - - // Left & Right (& row) headers - this.headers = [ - {id:this.nextmatch.options.header_left}, - {id:this.nextmatch.options.header_right}, - {id:this.nextmatch.options.header_row} - ]; - - // The rest of the header - this.header_div = this.row_div = jQuery(document.createElement("div")) - .addClass("nextmatch_header_row") - .appendTo(this.div); - this.filter_div = jQuery(document.createElement("div")) - .addClass('filtersContainer') - .appendTo(this.row_div); - - // Search - this.search_box = jQuery(document.createElement("div")) - .addClass('search') - .prependTo(egwIsMobile()?this.nextmatch.div:this.row_div); - - // searchbox widget options - var searchbox_options = { - id:"search", - overlay:(typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined')?settings.searchbox.overlay:false, - onchange:function(){ - self.nextmatch.applyFilters({search: this.get_value()}); - }, - value:settings.search, - fix:!egwIsMobile() - }; - // searchbox widget - this.et2_searchbox = et2_createWidget('searchbox', searchbox_options,this); - - // Set activeFilters to current value - this.nextmatch.activeFilters.search = settings.search; - - this.et2_searchbox.set_value(settings.search); - /** - * Mobile theme specific part for nm header - * nm header has very different behaivior for mobile theme and basically - * it has its own markup separately from nm header in normal templates. - */ - if (egwIsMobile()) - { - this.search_box.addClass('nm-mob-header'); - jQuery(this.div).css({display:'inline-block'}).addClass('nm_header_hide'); - - //indicates appname in header - jQuery(document.createElement('div')) - .addClass('nm_appname_header') - .text(egw.lang(egw.app_name())) - .appendTo(this.search_box); - - this.delete_action = jQuery(document.createElement('div')) - .addClass('nm_delete_action') - .prependTo(this.search_box); - // toggle header - // add new button - this.fav_span = jQuery(document.createElement('div')) - .addClass('nm_favorites_div') - .prependTo(this.search_box); - // toggle header menu - this.toggle_header = jQuery(document.createElement('button')) - .addClass('nm_toggle_header') - .click(function(){ - jQuery(self.div).toggleClass('nm_header_hide'); - jQuery(this).toggleClass('nm_toggle_header_on'); - window.setTimeout(function(){self.nextmatch.resize();},800); - }) - .prependTo(this.search_box); - // Context menu - this.action_header = jQuery(document.createElement('button')) - .addClass('nm_action_header') - .hide() - .click (function(e){ - jQuery('tr.selected',self.nextmatch.div).trigger({type:'contextmenu',which:3,originalEvent:e}); - }) - .prependTo(this.search_box); - } - - // Add category - if(!settings.no_cat) { - if (typeof settings.cat_id_label == 'undefined') settings.cat_id_label = ''; - this.category = this._build_select('cat_id', settings.cat_is_select ? - 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { - multiple: false, - tags: true, - class: "select-cat", - value_class: settings.cat_id_class - }); - } - - // Filter 1 - if(!settings.no_filter) { - this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); - } - - // Filter 2 - if(!settings.no_filter2) { - this.filter2 = this._build_select('filter2', 'select', settings.filter2, - settings.filter2_no_lang, { - multiple: false, - tags: settings.filter2_tags, - class: "select-cat", - value_class: settings.filter2_class - }); - } - - // Other stuff - this.right_div = jQuery(document.createElement("div")) - .addClass('header_row_right').appendTo(this.row_div); - - // Record count - this.count = jQuery(document.createElement("span")) - .addClass("header_count ui-corner-all"); - - // Need to figure out how to update this as grid scrolls - // this.count.append("? - ? ").append(egw.lang("of")).append(" "); - this.count_total = jQuery(document.createElement("span")) - .appendTo(this.count) - .text(settings.total + ""); - this.count.prependTo(this.right_div); - - // Favorites - this._setup_favorites(settings['favorites']); - - // Export - if(typeof settings.csv_fields != "undefined" && settings.csv_fields != false) - { - var definition = settings.csv_fields; - if(settings.csv_fields === true) - { - definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().getAppName()); - } - var button = et2_createWidget("buttononly", {id: "export", "statustext": "Export", image: "download", "background_image": true}, this); - jQuery(button.getDOMNode()) - .click(this.nextmatch, function(event) { - egw_openWindowCentered2( egw.link('/index.php', { - 'menuaction': 'importexport.importexport_export_ui.export_dialog', - 'appname': event.data.egw().getAppName(), - 'definition': definition - }), '_blank', 850, 440, 'yes'); - }); - } - - // Another place to customize nextmatch - this.header_row = jQuery(document.createElement("div")) - .addClass('header_row').appendTo(this.right_div); - - // Letter search - var current_letter = this.nextmatch.options.settings.searchletter ? - this.nextmatch.options.settings.searchletter : - (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); - if(this.nextmatch.options.settings.lettersearch || current_letter) - { - this.lettersearch = jQuery(document.createElement("table")) - .addClass('nextmatch_lettersearch') - .css("width", "100%") - .appendTo(this.div); - var tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch); - var row = jQuery(document.createElement("tr")).appendTo(tbody); - - // Capitals, A-Z - var letters = this.egw().lang('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''); - for(var i in letters) { - var button = jQuery(document.createElement("td")) - .addClass("lettersearch") - .appendTo(row) - .attr("id", letters[i]) - .text(letters[i]); - if(letters[i] == current_letter) button.addClass("lettersearch_active"); - } - button = jQuery(document.createElement("td")) - .addClass("lettersearch") - .appendTo(row) - .attr("id", "") - .text(egw.lang("all")); - if(!current_letter) button.addClass("lettersearch_active"); - - this.lettersearch.click(this.nextmatch, function(event) { - // this is the lettersearch table - jQuery("td",this).removeClass("lettersearch_active"); - jQuery(event.target).addClass("lettersearch_active"); - event.data.applyFilters({searchletter: event.target.id || false}); - }); - // Set activeFilters to current value - this.nextmatch.activeFilters.searchletter = current_letter; - } - // Apply letter search preference - var lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch"; - if(this.lettersearch && !egw.preference(lettersearch_preference,this.nextmatch.egw().getAppName())) - { - this.lettersearch.hide(); - } - }, - - - /** - * Build & bind to a sub-template into the header - * - * @param {string} location One of left, right, or row - * @param {string} template_name Name of the template to load into the location - */ - _build_header: function(location, template_name) - { - var id = location == "left" ? 0 : (location == "right" ? 1 : 2); - var existing = this.headers[id]; - if(existing && existing._type) - { - if(existing.id == template_name) return; - existing.free(); - this.headers[id] = ''; - } - - // Load the template - var self = this; - var header = et2_createWidget("template", {"id": template_name}, this); - this.headers[id] = header; - var deferred = []; - header.loadingFinished(deferred); - - // Wait until all child widgets are loaded, then bind - jQuery.when.apply(jQuery,deferred).then(function() { - // fix order in DOM by reattaching templates in correct position - switch (id) { - case 0: // header_left: prepend - jQuery(header.getDOMNode()).prependTo(self.header_div); - break; - case 1: // header_right: before favorites and count - jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')); - break; - case 2: // header_row: after search - window.setTimeout(function(){ // otherwise we might end up after filters - jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search')); - }, 1); - break; - } - self._bindHeaderInput(header); - }); - }, - - /** - * Build the selectbox filters in the header bar - * Sets value, options, labels, and change handlers - * - * @param {string} name - * @param {string} type - * @param {string} value - * @param {string} lang - * @param {object} extra - */ - _build_select: function(name, type, value, lang, extra) { - var widget_options = jQuery.extend({ - "id": name, - "label": this.nextmatch.options.settings[name+"_label"], - "no_lang": lang, - "disabled": this.nextmatch.options['no_'+name] - }, extra); - - // Set select options - // Check in content for options- - var mgr = this.nextmatch.getArrayMgr("content"); - var options = mgr.getEntry("options-" + name); - // Look in sel_options - if(!options) options = this.nextmatch.getArrayMgr("sel_options").getEntry(name); - // Check parent sel_options, because those are usually global and don't get passed down - if(!options) options = this.nextmatch.getArrayMgr("sel_options").parentMgr.getEntry(name); - // Sometimes legacy stuff puts it in here - if(!options) options = mgr.getEntry('rows[sel_options]['+name+']'); - - // Maybe in a row, and options got stuck in ${row} instead of top level - var row_stuck = ['${row}','{$row}']; - for(var i = 0; !options && i < row_stuck.length; i++) - { - var row_id = ''; - if((!options || options.length == 0) && ( - // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid - this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]])) - { - var row_id = name.replace(/[0-9]+/,row_stuck[i]); - options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); - if(!options) - { - row_id = row_stuck[i] + "["+name+"]"; - options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); - } - } - if(options) - { - this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].',row_id,name); - } - } - // Legacy: Add in 'All' option for cat_id, if not provided. - if(name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != '')) - { - widget_options.empty_label = this.egw().lang('All categories'); - } - - // Create widget - var select = et2_createWidget(type, widget_options, this); - - if(options) select.set_select_options(options); - - // Set value - select.set_value(value); - - // Set activeFilters to current value - this.nextmatch.activeFilters[select.id] = select.get_value(); - - // Set onChange - var input = select.input; - - // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() - select.attributes.select_options.ignore = true; - - if (this.nextmatch.options.settings[name+"_onchange"]) - { - // Get the onchange function string - var onchange = this.nextmatch.options.settings[name+"_onchange"]; - - // Real submits cause all sorts of problems - if(onchange.match(/this\.form\.submit/)) - { - this.egw().debug("warn","%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.",name); - onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/,'return true;'); - } - - // Connect it to the onchange event of the input element - may submit - select.change = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()); - this._bindHeaderInput(select); - } - else // default request changed rows with new filters, previous this.form.submit() - { - input.change(this.nextmatch, function(event) { - var set = {}; - set[name] = select.getValue(); - event.data.applyFilters(set); - }); - } - return select; - }, - - /** - * Set up the favorites UI control - * - * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of - * additional fields/settings to add in to the favorite. - */ - _setup_favorites: function(filters) { - if(typeof filters == "undefined" || filters === false) - { - // No favorites configured - return; - } - - var list = et2_csvSplit(this.options.get_rows, 2, "."); - var widget_options = { - default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite", - app: list[0], - filters: filters, - sidebox_target:'favorite_sidebox_'+list[0] - }; - this.favorites = et2_createWidget('favorites', widget_options, this); - - // Add into header - jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile()?this.search_box.find('.nm_favorites_div').show():this.right_div); - }, - - /** - * Updates all the filter elements in the header - * - * Does not actually refresh the data, just sets values to match those given. - * Called by et2_nextmatch.applyFilters(). - * - * @param filters Array Key => Value pairs of current filters - */ - setFilters: function(filters) { - - // Avoid loops cause by change events - if(this.update_in_progress) return; - this.update_in_progress = true; - - // Use an array mgr to hande non-simple IDs - var mgr = new et2_arrayMgr(filters); - - this.iterateOver(function(child) { - // Skip favorites, don't want them in the filter - if(typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) return; - - var value = ''; - if(typeof child.set_value != "undefined" && child.id) - { - value = mgr.getEntry(child.id); - if (value == null) value = ''; - /** - * Sometimes a filter value is not in current options. This can - * happen in a saved favorite, for example, or if server changes - * some filter options, and the order doesn't work out. The normal behaviour - * is to warn & not set it, but for nextmatch we'll just add it - * in, and let the server either set it properly, or ignore. - */ - if(value && typeof value != 'object' && child.instanceOf(et2_selectbox)) - { - var found = typeof child.options.select_options[value] != 'undefined'; - // options is array of objects with attribute value&label - if (jQuery.isArray(child.options.select_options)) - { - for(var o=0; o < child.options.select_options.length; ++o) - { - if (child.options.select_options[o].value == value) - { - found = true; - break; - } - } - } - if (!found) - { - var old_options = child.options.select_options; - // Actual label is not available, obviously, or it would be there - old_options[value] = child.egw().lang("Loading"); - child.set_select_options(old_options); - } - } - child.set_value(value); - } - if(typeof child.get_value == "function" && child.id) - { - // Put data in the proper place - var target = this; - var value = child.get_value(); - - // Split up indexes - var indexes = child.id.replace(/[/g,'[').split('['); - - for(var i = 0; i < indexes.length; i++) - { - indexes[i] = indexes[i].replace(/]/g,'').replace(']',''); - if (i < indexes.length-1) - { - if(typeof target[indexes[i]] == "undefined") target[indexes[i]] = {}; - target = target[indexes[i]]; - } - else - { - target[indexes[i]] = value; - } - } - } - }, filters); - - // Letter search - if(this.nextmatch.options.settings.lettersearch) - { - jQuery("td",this.lettersearch).removeClass("lettersearch_active"); - jQuery(filters.searchletter ? "td#"+filters.searchletter : "td.lettersearch[id='']",this.lettersearch).addClass("lettersearch_active"); - - // Set activeFilters to current value - filters.searchletter = jQuery("td.lettersearch_active",this.lettersearch).attr("id") || false; - } - - // Reset flag - this.update_in_progress = false; - }, - - /** - * Help out nextmatch / widget stuff by checking to see if sender is part of header - * - * @param {et2_widget} _sender - */ - getDOMNode: function(_sender) { - var filters = [this.category, this.filter, this.filter2]; - for(var i = 0; i < filters.length; i++) - { - if(_sender == filters[i]) - { - // Give them the filter div - return this.filter_div[0]; - } - } - if(_sender == this.et2_searchbox) return this.search_box[0]; - if(_sender.id == 'export') return this.right_div[0]; - - if(_sender && _sender._type == "template") - { - for(var i = 0; i < this.headers.length; i++) - { - if(_sender.id == this.headers[i].id && _sender._parent == this) return i == 2 ? this.header_row[0] : this.header_div[0]; - } - } - return null; - }, - - /** - * Bind all the inputs in the header sub-templates to update the filters - * on change, and update current filter with the inputs' current values - * - * @param {et2_template} sub_header - */ - _bindHeaderInput: function(sub_header) { - var header = this; - - var bind_change = function(_widget){ - // Previously set change function - var widget_change = _widget.change; - - var change = function(_node) { - // Call previously set change function - var result = widget_change.call(_widget,_node); - - // Update filters, if we're not already doing so - if((result || typeof result === 'undefined') && _widget.isDirty() && !header.update_in_progress) { - // Update dirty - _widget._oldValue = _widget.getValue(); - - // Widget will not have an entry in getValues() because nulls - // are not returned, we remove it from activeFilters - if(_widget._oldValue == null) - { - var path = _widget.getArrayMgr('content').explodeKey(_widget.id); - if(path.length > 0) - { - var entry = header.nextmatch.activeFilters; - var i = 0; - for(; i < path.length-1; i++) - { - entry = entry[path[i]]; - } - delete entry[path[i]]; - } - header.nextmatch.applyFilters(header.nextmatch.activeFilters); - } - else - { - // Not null is easy, just get values - var value = this.getInstanceManager().getValues(sub_header); - header.nextmatch.applyFilters(value[header.nextmatch.id]); - } - } - // In case this gets bound twice, it's important to return - return true; - }; - - _widget.change = change; - - // Set activeFilters to current value - // Use an array mgr to hande non-simple IDs - var value = {}; - value[_widget.id] = _widget._oldValue = _widget.getValue(); - var mgr = new et2_arrayMgr(value); - jQuery.extend(true, this.nextmatch.activeFilters,mgr.data); - }; - if(sub_header.instanceOf(et2_inputWidget)) - { - bind_change.call(this, sub_header); - } - else - { - sub_header.iterateOver(bind_change, this, et2_inputWidget); - } - } -});}).call(this); -et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); - +var et2_nextmatch_header_bar = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_header_bar, _super_1); + /** + * Constructor + * + * @param nextmatch + * @param nm_div + * @memberOf et2_nextmatch_header_bar + */ + function et2_nextmatch_header_bar(_parent, _attrs, _child) { + var _this = _super_1.call(this, _parent, [_parent, _parent.options.settings], et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_header_bar._attributes, _child || {})) || this; + _this.nextmatch = _parent; + _this.div = jQuery(document.createElement("div")) + .addClass("nextmatch_header"); + _this._createHeader(); + // Flag to avoid loops while updating filters + _this.update_in_progress = false; + return _this; + } + et2_nextmatch_header_bar.prototype.destroy = function () { + this.nextmatch = null; + _super_1.prototype.destroy.call(this); + this.div = null; + }; + et2_nextmatch_header_bar.prototype.setNextmatch = function (nextmatch) { + var create_once = (this.nextmatch == null); + this.nextmatch = nextmatch; + if (create_once) { + this._createHeader(); + } + // Bind row count + this.nextmatch.dataview.grid.setInvalidateCallback(function () { + this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + ""); + }, this); + }; + /** + * Actions are handled by the controller, so ignore these + * + * @param {object} actions + */ + et2_nextmatch_header_bar.prototype.set_actions = function (actions) { }; + et2_nextmatch_header_bar.prototype._createHeader = function () { + var button; + var self = this; + var nm_div = this.nextmatch.getDOMNode(); + var settings = this.nextmatch.options.settings; + this.div.prependTo(nm_div); + // Left & Right (& row) headers + this.headers = [ + { id: this.nextmatch.options.header_left }, + { id: this.nextmatch.options.header_right }, + { id: this.nextmatch.options.header_row } + ]; + // The rest of the header + this.header_div = this.row_div = jQuery(document.createElement("div")) + .addClass("nextmatch_header_row") + .appendTo(this.div); + this.filter_div = jQuery(document.createElement("div")) + .addClass('filtersContainer') + .appendTo(this.row_div); + // Search + this.search_box = jQuery(document.createElement("div")) + .addClass('search') + .prependTo(egwIsMobile() ? this.nextmatch.getDOMNode() : this.row_div); + // searchbox widget options + var searchbox_options = { + id: "search", + overlay: (typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined') ? settings.searchbox.overlay : false, + onchange: function () { + self.nextmatch.applyFilters({ search: this.get_value() }); + }, + value: settings.search, + fix: !egwIsMobile() + }; + // searchbox widget + this.et2_searchbox = et2_createWidget('searchbox', searchbox_options, this); + // Set activeFilters to current value + this.nextmatch.activeFilters.search = settings.search; + this.et2_searchbox.set_value(settings.search); + /** + * Mobile theme specific part for nm header + * nm header has very different behaivior for mobile theme and basically + * it has its own markup separately from nm header in normal templates. + */ + if (egwIsMobile()) { + this.search_box.addClass('nm-mob-header'); + jQuery(this.div).css({ display: 'inline-block' }).addClass('nm_header_hide'); + //indicates appname in header + jQuery(document.createElement('div')) + .addClass('nm_appname_header') + .text(egw.lang(egw.app_name())) + .appendTo(this.search_box); + this.delete_action = jQuery(document.createElement('div')) + .addClass('nm_delete_action') + .prependTo(this.search_box); + // toggle header + // add new button + this.fav_span = jQuery(document.createElement('div')) + .addClass('nm_favorites_div') + .prependTo(this.search_box); + // toggle header menu + this.toggle_header = jQuery(document.createElement('button')) + .addClass('nm_toggle_header') + .click(function () { + jQuery(self.div).toggleClass('nm_header_hide'); + jQuery(this).toggleClass('nm_toggle_header_on'); + window.setTimeout(function () { self.nextmatch.resize(); }, 800); + }) + .prependTo(this.search_box); + // Context menu + this.action_header = jQuery(document.createElement('button')) + .addClass('nm_action_header') + .hide() + .click(function (e) { + // @ts-ignore + jQuery('tr.selected', self.nextmatch.getDOMNode()).trigger({ type: 'contextmenu', which: 3, originalEvent: e }); + }) + .prependTo(this.search_box); + } + // Add category + if (!settings.no_cat) { + if (typeof settings.cat_id_label == 'undefined') + settings.cat_id_label = ''; + this.category = this._build_select('cat_id', settings.cat_is_select ? + 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { + multiple: false, + tags: true, + class: "select-cat", + value_class: settings.cat_id_class + }); + } + // Filter 1 + if (!settings.no_filter) { + this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); + } + // Filter 2 + if (!settings.no_filter2) { + this.filter2 = this._build_select('filter2', 'select', settings.filter2, settings.filter2_no_lang, { + multiple: false, + tags: settings.filter2_tags, + class: "select-cat", + value_class: settings.filter2_class + }); + } + // Other stuff + this.right_div = jQuery(document.createElement("div")) + .addClass('header_row_right').appendTo(this.row_div); + // Record count + this.count = jQuery(document.createElement("span")) + .addClass("header_count ui-corner-all"); + // Need to figure out how to update this as grid scrolls + // this.count.append("? - ? ").append(egw.lang("of")).append(" "); + this.count_total = jQuery(document.createElement("span")) + .appendTo(this.count) + .text(settings.total + ""); + this.count.prependTo(this.right_div); + // Favorites + this._setup_favorites(settings['favorites']); + // Export + if (typeof settings.csv_fields != "undefined" && settings.csv_fields != false) { + var definition = settings.csv_fields; + if (settings.csv_fields === true) { + definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().getAppName()); + } + var button_1 = et2_createWidget("buttononly", { id: "export", "statustext": "Export", image: "download", "background_image": true }, this); + jQuery(button_1.getDOMNode()) + .click(this.nextmatch, function (event) { + // @ts-ignore + egw_openWindowCentered2(egw.link('/index.php', { + 'menuaction': 'importexport.importexport_export_ui.export_dialog', + 'appname': event.data.egw().getAppName(), + 'definition': definition + }), '_blank', 850, 440, 'yes'); + }); + } + // Another place to customize nextmatch + this.header_row = jQuery(document.createElement("div")) + .addClass('header_row').appendTo(this.right_div); + // Letter search + var current_letter = this.nextmatch.options.settings.searchletter ? + this.nextmatch.options.settings.searchletter : + (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); + if (this.nextmatch.options.settings.lettersearch || current_letter) { + this.lettersearch = jQuery(document.createElement("table")) + .addClass('nextmatch_lettersearch') + .css("width", "100%") + .appendTo(this.div); + var tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch); + var row = jQuery(document.createElement("tr")).appendTo(tbody); + // Capitals, A-Z + var letters = this.egw().lang('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''); + for (var i in letters) { + button = jQuery(document.createElement("td")) + .addClass("lettersearch") + .appendTo(row) + .attr("id", letters[i]) + .text(letters[i]); + if (letters[i] == current_letter) + button.addClass("lettersearch_active"); + } + button = jQuery(document.createElement("td")) + .addClass("lettersearch") + .appendTo(row) + .attr("id", "") + .text(egw.lang("all")); + if (!current_letter) + button.addClass("lettersearch_active"); + this.lettersearch.click(this.nextmatch, function (event) { + // this is the lettersearch table + jQuery("td", this).removeClass("lettersearch_active"); + jQuery(event.target).addClass("lettersearch_active"); + event.data.applyFilters({ searchletter: event.target.id || false }); + }); + // Set activeFilters to current value + this.nextmatch.activeFilters.searchletter = current_letter; + } + // Apply letter search preference + var lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch"; + if (this.lettersearch && !egw.preference(lettersearch_preference, this.nextmatch.egw().getAppName())) { + this.lettersearch.hide(); + } + }; + /** + * Build & bind to a sub-template into the header + * + * @param {string} location One of left, right, or row + * @param {string} template_name Name of the template to load into the location + */ + et2_nextmatch_header_bar.prototype._build_header = function (location, template_name) { + var id = location == "left" ? 0 : (location == "right" ? 1 : 2); + var existing = this.headers[id]; + // @ts-ignore + if (existing && existing._type) { + if (existing.id == template_name) + return; + existing.destroy(); + this.headers[id] = null; + } + // Load the template + var self = this; + var header = et2_createWidget("template", { "id": template_name }, this); + this.headers[id] = header; + var deferred = []; + header.loadingFinished(deferred); + // Wait until all child widgets are loaded, then bind + jQuery.when.apply(jQuery, deferred).then(function () { + // fix order in DOM by reattaching templates in correct position + switch (id) { + case 0: // header_left: prepend + jQuery(header.getDOMNode()).prependTo(self.header_div); + break; + case 1: // header_right: before favorites and count + jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')); + break; + case 2: // header_row: after search + window.setTimeout(function () { + jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search')); + }, 1); + break; + } + self._bindHeaderInput(header); + }); + }; + /** + * Build the selectbox filters in the header bar + * Sets value, options, labels, and change handlers + * + * @param {string} name + * @param {string} type + * @param {string} value + * @param {string} lang + * @param {object} extra + */ + et2_nextmatch_header_bar.prototype._build_select = function (name, type, value, lang, extra) { + var widget_options = jQuery.extend({ + "id": name, + "label": this.nextmatch.options.settings[name + "_label"], + "no_lang": lang, + "disabled": this.nextmatch.options['no_' + name] + }, extra); + // Set select options + // Check in content for options- + var mgr = this.nextmatch.getArrayMgr("content"); + var options = mgr.getEntry("options-" + name); + // Look in sel_options + if (!options) + options = this.nextmatch.getArrayMgr("sel_options").getEntry(name); + // Check parent sel_options, because those are usually global and don't get passed down + if (!options) + options = this.nextmatch.getArrayMgr("sel_options").parentMgr.getEntry(name); + // Sometimes legacy stuff puts it in here + if (!options) + options = mgr.getEntry('rows[sel_options][' + name + ']'); + // Maybe in a row, and options got stuck in ${row} instead of top level + var row_stuck = ['${row}', '{$row}']; + for (var i = 0; !options && i < row_stuck.length; i++) { + var row_id = ''; + if ((!options || options.length == 0) && ( + // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid + this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]])) { + var row_id = name.replace(/[0-9]+/, row_stuck[i]); + options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); + if (!options) { + row_id = row_stuck[i] + "[" + name + "]"; + options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); + } + } + if (options) { + this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].', row_id, name); + } + } + // Legacy: Add in 'All' option for cat_id, if not provided. + if (name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != '')) { + widget_options.empty_label = this.egw().lang('All categories'); + } + // Create widget + var select = et2_createWidget(type, widget_options, this); + if (options) + select.set_select_options(options); + // Set value + select.set_value(value); + // Set activeFilters to current value + this.nextmatch.activeFilters[select.id] = select.get_value(); + // Set onChange + var input = select.input; + // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() + select.attributes.select_options.ignore = true; + if (this.nextmatch.options.settings[name + "_onchange"]) { + // Get the onchange function string + var onchange = this.nextmatch.options.settings[name + "_onchange"]; + // Real submits cause all sorts of problems + if (onchange.match(/this\.form\.submit/)) { + this.egw().debug("warn", "%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.", name); + onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/, 'return true;'); + } + // Connect it to the onchange event of the input element - may submit + select.change = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()); + this._bindHeaderInput(select); + } + else // default request changed rows with new filters, previous this.form.submit() + { + input.change(this.nextmatch, function (event) { + var set = {}; + set[name] = select.getValue(); + event.data.applyFilters(set); + }); + } + return select; + }; + /** + * Set up the favorites UI control + * + * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of + * additional fields/settings to add in to the favorite. + */ + et2_nextmatch_header_bar.prototype._setup_favorites = function (filters) { + if (typeof filters == "undefined" || filters === false) { + // No favorites configured + return; + } + var list = et2_csvSplit(this.options.get_rows, 2, "."); + var widget_options = { + default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite", + app: list[0], + filters: filters, + sidebox_target: 'favorite_sidebox_' + list[0] + }; + this.favorites = et2_createWidget('favorites', widget_options, this); + // Add into header + jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile() ? this.search_box.find('.nm_favorites_div').show() : this.right_div); + }; + /** + * Updates all the filter elements in the header + * + * Does not actually refresh the data, just sets values to match those given. + * Called by et2_nextmatch.applyFilters(). + * + * @param filters Array Key => Value pairs of current filters + */ + et2_nextmatch_header_bar.prototype.setFilters = function (filters) { + // Avoid loops cause by change events + if (this.update_in_progress) + return; + this.update_in_progress = true; + // Use an array mgr to hande non-simple IDs + var mgr = new et2_arrayMgr(filters); + this.iterateOver(function (child) { + // Skip favorites, don't want them in the filter + if (typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) + return; + var value = ''; + if (typeof child.set_value != "undefined" && child.id) { + value = mgr.getEntry(child.id); + if (value == null) + value = ''; + /** + * Sometimes a filter value is not in current options. This can + * happen in a saved favorite, for example, or if server changes + * some filter options, and the order doesn't work out. The normal behaviour + * is to warn & not set it, but for nextmatch we'll just add it + * in, and let the server either set it properly, or ignore. + */ + if (value && typeof value != 'object' && child.instanceOf(et2_widget_selectbox_1.et2_selectbox)) { + var found = typeof child.options.select_options[value] != 'undefined'; + // options is array of objects with attribute value&label + if (jQuery.isArray(child.options.select_options)) { + for (var o = 0; o < child.options.select_options.length; ++o) { + if (child.options.select_options[o].value == value) { + found = true; + break; + } + } + } + if (!found) { + var old_options = child.options.select_options; + // Actual label is not available, obviously, or it would be there + old_options[value] = child.egw().lang("Loading"); + child.set_select_options(old_options); + } + } + child.set_value(value); + } + if (typeof child.get_value == "function" && child.id) { + // Put data in the proper place + var target = this; + value = child.get_value(); + // Split up indexes + var indexes = child.id.replace(/[/g, '[').split('['); + for (var i = 0; i < indexes.length; i++) { + indexes[i] = indexes[i].replace(/]/g, '').replace(']', ''); + if (i < indexes.length - 1) { + if (typeof target[indexes[i]] == "undefined") + target[indexes[i]] = {}; + target = target[indexes[i]]; + } + else { + target[indexes[i]] = value; + } + } + } + }, filters); + // Letter search + if (this.nextmatch.options.settings.lettersearch) { + jQuery("td", this.lettersearch).removeClass("lettersearch_active"); + jQuery(filters.searchletter ? "td#" + filters.searchletter : "td.lettersearch[id='']", this.lettersearch).addClass("lettersearch_active"); + // Set activeFilters to current value + filters.searchletter = jQuery("td.lettersearch_active", this.lettersearch).attr("id") || false; + } + // Reset flag + this.update_in_progress = false; + }; + /** + * Help out nextmatch / widget stuff by checking to see if sender is part of header + * + * @param {et2_widget} _sender + */ + et2_nextmatch_header_bar.prototype.getDOMNode = function (_sender) { + var filters = [this.category, this.filter, this.filter2]; + for (var i = 0; i < filters.length; i++) { + if (_sender == filters[i]) { + // Give them the filter div + return this.filter_div[0]; + } + } + if (_sender == this.et2_searchbox) + return this.search_box[0]; + if (_sender.id == 'export') + return this.right_div[0]; + if (_sender && _sender._type == "template") { + for (var i = 0; i < this.headers.length; i++) { + if (_sender.id == this.headers[i].id && _sender._parent == this) + return i == 2 ? this.header_row[0] : this.header_div[0]; + } + } + return null; + }; + /** + * Bind all the inputs in the header sub-templates to update the filters + * on change, and update current filter with the inputs' current values + * + * @param {et2_template} sub_header + */ + et2_nextmatch_header_bar.prototype._bindHeaderInput = function (sub_header) { + var header = this; + var bind_change = function (_widget) { + // Previously set change function + var widget_change = _widget.change; + var change = function (_node) { + // Call previously set change function + var result = widget_change.call(_widget, _node); + // Update filters, if we're not already doing so + if ((result || typeof result === 'undefined') && _widget.isDirty() && !header.update_in_progress) { + // Update dirty + _widget._oldValue = _widget.getValue(); + // Widget will not have an entry in getValues() because nulls + // are not returned, we remove it from activeFilters + if (_widget._oldValue == null) { + var path = _widget.getArrayMgr('content').explodeKey(_widget.id); + if (path.length > 0) { + var entry = header.nextmatch.activeFilters; + var i = 0; + for (; i < path.length - 1; i++) { + entry = entry[path[i]]; + } + delete entry[path[i]]; + } + header.nextmatch.applyFilters(header.nextmatch.activeFilters); + } + else { + // Not null is easy, just get values + var value = this.getInstanceManager().getValues(sub_header); + header.nextmatch.applyFilters(value[header.nextmatch.id]); + } + } + // In case this gets bound twice, it's important to return + return true; + }; + _widget.change = change; + // Set activeFilters to current value + // Use an array mgr to hande non-simple IDs + var value = {}; + value[_widget.id] = _widget._oldValue = _widget.getValue(); + var mgr = new et2_arrayMgr(value); + jQuery.extend(true, this.nextmatch.activeFilters, mgr.data); + }; + if (sub_header.instanceOf(et2_core_inputWidget_1.et2_inputWidget)) { + bind_change.call(this, sub_header); + } + else { + sub_header.iterateOver(bind_change, this, et2_core_inputWidget_1.et2_inputWidget); + } + }; + return et2_nextmatch_header_bar; +}(et2_core_DOMWidget_1.et2_DOMWidget)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); /** * Classes for the nextmatch sortheaders etc. * * @augments et2_baseWidget */ -var et2_nextmatch_header = (function(){ "use strict"; return et2_baseWidget.extend(et2_INextmatchHeader, -{ - attributes: { - "label": { - "name": "Caption", - "type": "string", - "description": "Caption for the nextmatch header", - "translate": true - } - }, - - /** - * Constructor - * - * @memberOf et2_nextmatch_header - */ - init: function() { - this._super.apply(this, arguments); - - this.labelNode = jQuery(document.createElement("span")); - this.nextmatch = null; - - this.setDOMNode(this.labelNode[0]); - }, - - destroy: function() { - this._super.apply(this, arguments); - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - }, - - set_label: function(_value) { - this.label = _value; - - this.labelNode.text(_value); - - // add class if label is empty - this.labelNode.toggleClass('et2_label_empty', !_value); - } -});}).call(this); -et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); - +var et2_nextmatch_header = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_header, _super_1); + /** + * Constructor + * + * @memberOf et2_nextmatch_header + */ + function et2_nextmatch_header(_parent, _attrs, _child) { + var _this = _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_header._attributes, _child || {})) || this; + _this.labelNode = jQuery(document.createElement("span")); + _this.nextmatch = null; + _this.setDOMNode(_this.labelNode[0]); + return _this; + } + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_header.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + }; + et2_nextmatch_header.prototype.set_label = function (_value) { + this.label = _value; + this.labelNode.text(_value); + // add class if label is empty + this.labelNode.toggleClass('et2_label_empty', !_value); + }; + return et2_nextmatch_header; +}(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_nextmatch_header = et2_nextmatch_header; +et2_core_widget_1.et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); /** * Extend header to process customfields * * @augments et2_customfields_list */ -var et2_nextmatch_customfields = (function(){ "use strict"; return et2_customfields_list.extend(et2_INextmatchHeader, -{ - attributes: { - 'customfields': { - 'name': 'Custom fields', - 'description': 'Auto filled' - }, - 'fields': { - 'name': "Visible fields", - "description": "Auto filled" - } - }, - - /** - * Constructor - * - * @memberOf et2_nextmatch_customfields - */ - init: function() { - this.nextmatch = null; - this._super.apply(this, arguments); - - // Specifically take the whole column - this.table.css("width", "100%"); - }, - - destroy: function() { - this.nextmatch = null; - this._super.apply(this, arguments); - }, - - transformAttributes: function(_attrs) { - this._super.apply(this, arguments); - - // Add in settings that are objects - if(!_attrs.customfields) - { - // Check for custom stuff (unlikely) - var data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for global settings - if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); - for(var key in data) - { - if(typeof data[key] === 'object' && ! _attrs[key]) _attrs[key] = data[key]; - } - } - }, - - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - this.loadFields(); - }, - - /** - * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio - */ - loadFields: function() { - if(this.nextmatch == null) - { - // not ready yet - return; - } - var columnMgr = this.nextmatch.dataview.getColumnMgr(); - var nm_column = null; - var set_fields = {}; - for(var i = 0; i < this.nextmatch.columns.length; i++) - { - if(this.nextmatch.columns[i].widget == this) - { - nm_column = columnMgr.columns[i]; - break; - } - } - if(!nm_column) return; - - // Check for global setting changes (visibility) - var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); - if(global_data != null && global_data.fields) this.options.fields = global_data.fields; - - var apps = egw.link_app_list(); - for(var field_name in this.options.customfields) - { - var field = this.options.customfields[field_name]; - var cf_id = et2_customfields_list.prototype.prefix + field_name; - - - if(this.rows[field_name]) continue; - - // Table row - var row = jQuery(document.createElement("tr")) - .appendTo(this.tbody); - var cf = jQuery(document.createElement("td")) - .appendTo(row); - this.rows[cf_id] = cf[0]; - - // Create widget by type - var widget = null; - if(field.type == 'select' || field.type == 'select-account') - { - if(field.values && typeof field.values[''] !== 'undefined') - { - delete(field.values['']); - } - widget = et2_createWidget( - field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", - { - id: cf_id, - empty_label: field.label, - select_options: field.values - }, - this - ); - } - else if (apps[field.type]) - { - widget = et2_createWidget("nextmatch-entryheader", { - id: cf_id, - only_app: field.type, - blur: field.label - }, this); - } - else - { - widget = et2_createWidget("nextmatch-sortheader", { - id: cf_id, - label: field.label - }, this); - } - - // If this is already attached, widget needs to be finished explicitly - if(this.isAttached() && !widget.isAttached()) - { - widget.loadingFinished(); - } - // Check for column filter - if(!jQuery.isEmptyObject(this.options.fields) && ( - this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined')) - { - cf.hide(); - } - else if (jQuery.isEmptyObject(this.options.fields)) - { - // If we're showing it make sure it's set, but only after - set_fields[field_name] = true; - } - } - jQuery.extend(this.options.fields, set_fields); - }, - - /** - * Override parent so we can update the nextmatch row too - * - * @param {array} _fields - */ - set_visible: function(_fields) { - this._super.apply(this, arguments); - - // Find data row, and do it too - var self = this; - if(this.nextmatch) - { - this.nextmatch.iterateOver( - function(widget) { - if(widget == self) return; - widget.set_visible(_fields); - }, this, et2_customfields_list - ); - } - }, - - /** - * Provide own column caption (column selection) - * - * If only one custom field, just use that, otherwise use "custom fields" - */ - _genColumnCaption: function() { - return egw.lang("Custom fields"); - }, - - /** - * Provide own column naming, including only selected columns - only useful - * to nextmatch itself, not for sending server-side - */ - _getColumnName: function() { - var name = this.id; - var visible = []; - for(var field_name in this.options.customfields) - { - if(jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) - { - visible.push(et2_customfields_list.prototype.prefix + field_name); - jQuery(this.rows[field_name]).show(); - } - else if (typeof this.rows[field_name] != "undefined") - { - jQuery(this.rows[field_name]).hide(); - } - } - - if(visible.length) { - name +="_"+ visible.join("_"); - } - else - { - // None hidden means all visible - jQuery(this.rows[field_name]).parent().parent().children().show(); - } - - // Update global custom fields column(s) - widgets will check on their own - - // Check for custom stuff (unlikely) - var data = this.getArrayMgr("modifications").getEntry(this.id); - // Check for global settings - if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {}; - if(!data.fields) data.fields = {}; - for(var field in this.options.customfields) - { - data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]); - } - return name; - } -});}).call(this); -et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); - +var et2_nextmatch_customfields = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_customfields, _super_1); + /** + * Constructor + * + * @memberOf et2_nextmatch_customfields + */ + function et2_nextmatch_customfields(_parent, _attrs, _child) { + var _this = _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_customfields._attributes, _child || {})) || this; + // Specifically take the whole column + _this.table.css("width", "100%"); + return _this; + } + et2_nextmatch_customfields.prototype.destroy = function () { + this.nextmatch = null; + _super_1.prototype.destroy.call(this); + }; + et2_nextmatch_customfields.prototype.transformAttributes = function (_attrs) { + _super_1.prototype.transformAttributes.call(this, _attrs); + // Add in settings that are objects + if (!_attrs.customfields) { + // Check for custom stuff (unlikely) + var data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + if (!data) + data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); + for (var key in data) { + if (typeof data[key] === 'object' && !_attrs[key]) + _attrs[key] = data[key]; + } + } + }; + et2_nextmatch_customfields.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + this.loadFields(); + }; + /** + * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio + */ + et2_nextmatch_customfields.prototype.loadFields = function () { + if (this.nextmatch == null) { + // not ready yet + return; + } + var columnMgr = this.nextmatch.dataview.getColumnMgr(); + var nm_column = null; + var set_fields = {}; + for (var i = 0; i < this.nextmatch.columns.length; i++) { + if (this.nextmatch.columns[i].widget == this) { + nm_column = columnMgr.columns[i]; + break; + } + } + if (!nm_column) + return; + // Check for global setting changes (visibility) + var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if (global_data != null && global_data.fields) + this.options.fields = global_data.fields; + var apps = egw.link_app_list(); + for (var field_name in this.options.customfields) { + var field = this.options.customfields[field_name]; + var cf_id = et2_customfields_list.prefix + field_name; + if (this.rows[field_name]) + continue; + // Table row + var row = jQuery(document.createElement("tr")) + .appendTo(this.tbody); + var cf = jQuery(document.createElement("td")) + .appendTo(row); + this.rows[cf_id] = cf[0]; + // Create widget by type + var widget = null; + if (field.type == 'select' || field.type == 'select-account') { + if (field.values && typeof field.values[''] !== 'undefined') { + delete (field.values['']); + } + widget = et2_createWidget(field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", { + id: cf_id, + empty_label: field.label, + select_options: field.values + }, this); + } + else if (apps[field.type]) { + widget = et2_createWidget("nextmatch-entryheader", { + id: cf_id, + only_app: field.type, + blur: field.label + }, this); + } + else { + widget = et2_createWidget("nextmatch-sortheader", { + id: cf_id, + label: field.label + }, this); + } + // If this is already attached, widget needs to be finished explicitly + if (this.isAttached() && !widget.isAttached()) { + widget.loadingFinished(); + } + // Check for column filter + if (!jQuery.isEmptyObject(this.options.fields) && (this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined')) { + cf.hide(); + } + else if (jQuery.isEmptyObject(this.options.fields)) { + // If we're showing it make sure it's set, but only after + set_fields[field_name] = true; + } + } + jQuery.extend(this.options.fields, set_fields); + }; + /** + * Override parent so we can update the nextmatch row too + * + * @param {array} _fields + */ + et2_nextmatch_customfields.prototype.set_visible = function (_fields) { + _super_1.prototype.set_visible.call(this, _fields); + // Find data row, and do it too + var self = this; + if (this.nextmatch) { + this.nextmatch.iterateOver(function (widget) { + if (widget == self) + return; + widget.set_visible(_fields); + }, this, et2_customfields_list); + } + }; + /** + * Provide own column caption (column selection) + * + * If only one custom field, just use that, otherwise use "custom fields" + */ + et2_nextmatch_customfields.prototype._genColumnCaption = function () { + return egw.lang("Custom fields"); + }; + /** + * Provide own column naming, including only selected columns - only useful + * to nextmatch itself, not for sending server-side + */ + et2_nextmatch_customfields.prototype._getColumnName = function () { + var name = this.id; + var visible = []; + for (var field_name in this.options.customfields) { + if (jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) { + visible.push(et2_customfields_list.prefix + field_name); + jQuery(this.rows[field_name]).show(); + } + else if (typeof this.rows[field_name] != "undefined") { + jQuery(this.rows[field_name]).hide(); + } + } + if (visible.length) { + name += "_" + visible.join("_"); + } + else { + // None hidden means all visible + jQuery(this.rows[field_name]).parent().parent().children().show(); + } + // Update global custom fields column(s) - widgets will check on their own + // Check for custom stuff (unlikely) + var data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + if (!data) + data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {}; + if (!data.fields) + data.fields = {}; + for (var field in this.options.customfields) { + data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]); + } + return name; + }; + return et2_nextmatch_customfields; +}(et2_customfields_list)); +exports.et2_nextmatch_customfields = et2_nextmatch_customfields; +et2_core_widget_1.et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); /** * @augments et2_nextmatch_header */ -var et2_nextmatch_sortheader = (function(){ "use strict"; return et2_nextmatch_header.extend(et2_INextmatchSortable, -{ - attributes: { - "sortmode": { - "name": "Sort order", - "type": "string", - "description": "Default sort order", - "translate": false - } - }, - legacyOptions: ['sortmode'], - - /** - * Constructor - * - * @memberOf et2_nextmatch_sortheader - */ - init: function() { - this._super.apply(this, arguments); - - this.sortmode = "none"; - - this.labelNode.addClass("nextmatch_sortheader none"); - }, - - click: function() { - if (this.nextmatch && this._super.apply(this, arguments)) - { - // Send default sort mode if not sorted, otherwise send undefined to calculate - this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined); - return true; - } - - return false; - }, - - /** - * Wrapper to join up interface * framework - * - * @param {string} _mode - */ - set_sortmode: function(_mode) - { - // Set via nextmatch after setup - if(this.nextmatch) return; - - this.setSortmode(_mode); - }, - - /** - * Function which implements the et2_INextmatchSortable function. - * - * @param {string} _mode - */ - setSortmode: function(_mode) { - // Remove the last sortmode class and add the new one - this.labelNode.removeClass(this.sortmode) - .addClass(_mode); - - this.sortmode = _mode; - } - -});}).call(this); -et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); - +// @ts-ignore +var et2_nextmatch_sortheader = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_sortheader, _super_1); + /** + * Constructor + * + * @memberOf et2_nextmatch_sortheader + */ + function et2_nextmatch_sortheader(_parent, _attrs, _child) { + var _this = _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_sortheader._attributes, _child || {})) || this; + _this.sortmode = "none"; + _this.labelNode.addClass("nextmatch_sortheader none"); + return _this; + } + et2_nextmatch_sortheader.prototype.click = function (_event) { + if (this.nextmatch && _super_1.prototype.click.call(this, _event)) { + // Send default sort mode if not sorted, otherwise send undefined to calculate + this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined); + return true; + } + return false; + }; + /** + * Wrapper to join up interface * framework + * + * @param {string} _mode + */ + et2_nextmatch_sortheader.prototype.set_sortmode = function (_mode) { + // Set via nextmatch after setup + if (this.nextmatch) + return; + this.setSortmode(_mode); + }; + /** + * Function which implements the et2_INextmatchSortable function. + * + * @param {string} _mode + */ + et2_nextmatch_sortheader.prototype.setSortmode = function (_mode) { + // Remove the last sortmode class and add the new one + this.labelNode.removeClass(this.sortmode) + .addClass(_mode); + this.sortmode = _mode; + }; + return et2_nextmatch_sortheader; +}(et2_nextmatch_header)); +exports.et2_nextmatch_sortheader = et2_nextmatch_sortheader; +et2_core_widget_1.et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); /** * @augments et2_selectbox */ -var et2_nextmatch_filterheader = (function(){ "use strict"; return et2_selectbox.extend([et2_INextmatchHeader, et2_IResizeable], -{ - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_filterheader - */ - createInputWidget: function() { - // Make sure there's an option for all - if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - this._super.apply(this, arguments); - - this.input.change(this, function(event) { - if(typeof event.data.nextmatch == 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[event.data.id] = event.data.input.val(); - // Set value so it's there for response (otherwise it gets cleared if options are updated) - event.data.set_value(event.data.input.val()); - - event.data.nextmatch.applyFilters({col_filter: col_filter}); - }); - - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - - // Make sure it's set in the nextmatch - _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - }, - - // Make sure selectbox is not longer than the column - resize: function() { - this.input.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); - } - -});}).call(this); -et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); - +var et2_nextmatch_filterheader = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_filterheader, _super_1); + function et2_nextmatch_filterheader() { + return _super_1 !== null && _super_1.apply(this, arguments) || this; + } + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_filterheader + */ + et2_nextmatch_filterheader.prototype.createInputWidget = function () { + // Make sure there's an option for all + if (!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + _super_1.prototype.createInputWidget.call(this); + jQuery(this.getInputNode()).change(this, function (event) { + if (typeof event.data.nextmatch == 'undefined') { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[event.data.id] = event.data.input.val(); + // Set value so it's there for response (otherwise it gets cleared if options are updated) + event.data.set_value(event.data.input.val()); + event.data.nextmatch.applyFilters({ col_filter: col_filter }); + }); + }; + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_filterheader.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + // Set current filter value from nextmatch settings + if (this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + // Make sure it's set in the nextmatch + _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + }; + // Make sure selectbox is not longer than the column + et2_nextmatch_filterheader.prototype.resize = function () { + this.input.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); + }; + return et2_nextmatch_filterheader; +}(et2_widget_selectbox_1.et2_selectbox)); +exports.et2_nextmatch_filterheader = et2_nextmatch_filterheader; +et2_core_widget_1.et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); /** * @augments et2_selectAccount */ -var et2_nextmatch_accountfilterheader = (function(){ "use strict"; return et2_selectAccount.extend([et2_INextmatchHeader, et2_IResizeable], -{ - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_accountfilterheader - */ - createInputWidget: function() { - // Make sure there's an option for all - if(!this.options.empty_label && !this.options.select_options[""]) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - this._super.apply(this, arguments); - - this.input.change(this, function(event) { - if(typeof event.data.nextmatch == 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[event.data.id] = event.data.getValue(); - event.data.nextmatch.applyFilters({col_filter: col_filter}); - }); - - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - } - }, - // Make sure selectbox is not longer than the column - resize: function() { - var max = jQuery(this.parentNode).innerWidth() - 4; - var surroundings = this.getSurroundings()._widgetSurroundings; - for(var i = 0; i < surroundings.length; i++) - { - max -= jQuery(surroundings[i]).outerWidth(); - } - this.input.css("max-width",max + "px"); - } - -});}).call(this); -et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); - +var et2_nextmatch_accountfilterheader = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_accountfilterheader, _super_1); + function et2_nextmatch_accountfilterheader() { + return _super_1 !== null && _super_1.apply(this, arguments) || this; + } + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_accountfilterheader + */ + et2_nextmatch_accountfilterheader.prototype.createInputWidget = function () { + // Make sure there's an option for all + if (!this.options.empty_label && !this.options.select_options[""]) { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + this._super.apply(this, arguments); + this.input.change(this, function (event) { + if (typeof event.data.nextmatch == 'undefined') { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[event.data.id] = event.data.getValue(); + event.data.nextmatch.applyFilters({ col_filter: col_filter }); + }); + }; + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_accountfilterheader.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + // Set current filter value from nextmatch settings + if (this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + } + }; + // Make sure selectbox is not longer than the column + et2_nextmatch_accountfilterheader.prototype.resize = function () { + var max = jQuery(this.parentNode).innerWidth() - 4; + var surroundings = this.getSurroundings()._widgetSurroundings; + for (var i = 0; i < surroundings.length; i++) { + max -= jQuery(surroundings[i]).outerWidth(); + } + this.input.css("max-width", max + "px"); + }; + return et2_nextmatch_accountfilterheader; +}(et2_selectAccount)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); /** * Filter allowing multiple values to be selected, base on a taglist instead * of a regular selectbox * * @augments et2_taglist */ -var et2_nextmatch_taglistheader = (function(){ "use strict"; return et2_taglist.extend([et2_INextmatchHeader, et2_IResizeable], -{ - attributes: { - autocomplete_url: { default: ''}, - multiple: { default: 'toggle'}, - onchange: { - default: function(event) { - if(typeof this.nextmatch === 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[this.id] = this.getValue(); - // Set value so it's there for response (otherwise it gets cleared if options are updated) - //event.data.set_value(event.data.input.val()); - - this.nextmatch.applyFilters({col_filter: col_filter}); - } - }, - rows: { default: 2}, - class: {default: 'nm_filterheader_taglist'} - }, - - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_filterheader - */ - createInputWidget: function() { - // Make sure there's an option for all - if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - this._super.apply(this, arguments); - }, - - /** - * Disable toggle if there are 2 or less options - * @param {Object[]} options - */ - set_select_options: function(options) - { - if(options && options.length <= 2 && this.options.multiple == 'toggle') - { - this.set_multiple(false); - } - this._super.apply(this, arguments); - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - - // Make sure it's set in the nextmatch - _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - }, - - // Make sure selectbox is not longer than the column - resize: function() { - this.div.css("height",''); - this.div.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); - this._super.apply(this, arguments); - } - -});}).call(this); -et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); - +var et2_nextmatch_taglistheader = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_taglistheader, _super_1); + function et2_nextmatch_taglistheader() { + return _super_1 !== null && _super_1.apply(this, arguments) || this; + } + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_filterheader + */ + et2_nextmatch_taglistheader.prototype.createInputWidget = function () { + // Make sure there's an option for all + if (!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + _super_1.prototype.createInputWidget.call(this); + }; + /** + * Disable toggle if there are 2 or less options + * @param {Object[]} options + */ + et2_nextmatch_taglistheader.prototype.set_select_options = function (options) { + if (options && options.length <= 2 && this.options.multiple == 'toggle') { + this.set_multiple(false); + } + _super_1.prototype.set_select_options.call(this, options); + }; + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_taglistheader.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + // Set current filter value from nextmatch settings + if (this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + // Make sure it's set in the nextmatch + _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + }; + // Make sure selectbox is not longer than the column + et2_nextmatch_taglistheader.prototype.resize = function () { + this.div.css("height", ''); + this.div.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); + _super_1.prototype.resize.call(this); + }; + et2_nextmatch_taglistheader._attributes = { + autocomplete_url: { default: '' }, + multiple: { default: 'toggle' }, + onchange: { + // @ts-ignore + default: function (event) { + if (typeof this.nextmatch === 'undefined') { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[this.id] = this.getValue(); + // Set value so it's there for response (otherwise it gets cleared if options are updated) + //event.data.set_value(event.data.input.val()); + this.nextmatch.applyFilters({ col_filter: col_filter }); + } + }, + rows: { default: 2 }, + class: { default: 'nm_filterheader_taglist' } + }; + return et2_nextmatch_taglistheader; +}(et2_taglist)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); /** * @augments et2_link_entry */ -var et2_nextmatch_entryheader = (function(){ "use strict"; return et2_link_entry.extend(et2_INextmatchHeader, -{ - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_entryheader - * @param {object} event - * @param {object} selected - */ - onchange: function(event, selected) { - var col_filter = {}; - col_filter[this.id] = this.get_value(); - this.nextmatch.applyFilters.call(this.nextmatch, {col_filter: col_filter}); - }, - - /** - * Override to always return a string appname:id (or just id) for simple (one real selection) - * cases, parent returns an object. If multiple are selected, or anything other than app and - * id, the original parent value is returned. - */ - getValue: function() { - var value = this._super.apply(this, arguments); - if(typeof value == "object" && value != null) - { - if(!value.app || !value.id) return null; - - // If array with just one value, use a string instead for legacy server handling - if(typeof value.id == 'object' && value.id.shift && value.id.length == 1) - { - value.id = value.id.shift(); - } - // If simple value, format it legacy string style, otherwise - // we return full value - if(typeof value.id == 'string') - { - value = value.app +":"+value.id; - } - } - return value; - }, - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch: function(_nextmatch) { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id]) - { - this.set_value(this.nextmatch.options.settings.col_filter[this.id]); - - if(this.getValue() != this.nextmatch.activeFilters.col_filter[this.id]) - { - this.nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - - // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() - this.attributes.value.ignore = true; - //this.attributes.select_options.ignore = true; - } - var self = this; - // Fire on lost focus, clear filter if user emptied box - } -});}).call(this); -et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); - +var et2_nextmatch_entryheader = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_entryheader, _super_1); + function et2_nextmatch_entryheader() { + return _super_1 !== null && _super_1.apply(this, arguments) || this; + } + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_entryheader + * @param {object} event + * @param {object} selected + */ + et2_nextmatch_entryheader.prototype.onchange = function (event, selected) { + var col_filter = {}; + col_filter[this.id] = this.get_value(); + this.nextmatch.applyFilters.call(this.nextmatch, { col_filter: col_filter }); + }; + /** + * Override to always return a string appname:id (or just id) for simple (one real selection) + * cases, parent returns an object. If multiple are selected, or anything other than app and + * id, the original parent value is returned. + */ + et2_nextmatch_entryheader.prototype.getValue = function () { + var value = _super_1.prototype.getValue.call(this); + if (typeof value == "object" && value != null) { + if (!value.app || !value.id) + return null; + // If array with just one value, use a string instead for legacy server handling + if (typeof value.id == 'object' && value.id.shift && value.id.length == 1) { + value.id = value.id.shift(); + } + // If simple value, format it legacy string style, otherwise + // we return full value + if (typeof value.id == 'string') { + value = value.app + ":" + value.id; + } + } + return value; + }; + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + et2_nextmatch_entryheader.prototype.setNextmatch = function (_nextmatch) { + this.nextmatch = _nextmatch; + // Set current filter value from nextmatch settings + if (this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id]) { + this.set_value(this.nextmatch.options.settings.col_filter[this.id]); + if (this.getValue() != this.nextmatch.activeFilters.col_filter[this.id]) { + this.nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() + this.attributes.value.ignore = true; + //this.attributes.select_options.ignore = true; + } + var self = this; + // Fire on lost focus, clear filter if user emptied box + }; + return et2_nextmatch_entryheader; +}(et2_link_entry)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); /** * @augments et2_nextmatch_filterheader */ -var et2_nextmatch_customfilter = (function(){ "use strict"; return et2_nextmatch_filterheader.extend( -{ - attributes: { - "widget_type": { - "name": "Actual type", - "type": "string", - "description": "The actual type of widget you should use", - "no_lang": 1 - }, - "widget_options": { - "name": "Actual options", - "type": "any", - "description": "The options for the actual widget", - "no_lang": 1, - "default": {} - } - }, - legacyOptions: ["widget_type","widget_options"], - - real_node: null, - - /** - * Constructor - * - * @param _parent - * @param _attrs - * @memberOf et2_nextmatch_customfilter - */ - init: function(_parent, _attrs) { - - switch(_attrs.widget_type) - { - case "link-entry": - _attrs.type = 'nextmatch-entryheader'; - break; - default: - if(_attrs.widget_type.indexOf('select') === 0) - { - _attrs.type = 'nextmatch-filterheader'; - } - else - { - _attrs.type = _attrs.widget_type; - } - } - jQuery.extend(_attrs.widget_options,{id: this.id}); - - _attrs.id = ''; - this._super.apply(this, arguments); - this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this._parent); - var select_options = []; - var correct_type = _attrs.type; - this.real_node._type = _attrs.widget_type; - et2_selectbox.find_select_options(this.real_node, select_options, _attrs); - this.real_node._type = correct_type; - if(typeof this.real_node.set_select_options === 'function') - { - this.real_node.set_select_options(select_options); - } - }, - - // Just pass the real DOM node through, in case anybody asks - getDOMNode: function(_sender) { - return this.real_node ? this.real_node.getDOMNode(_sender) : null; - }, - - // Also need to pass through real children - getChildren: function() { - return this.real_node.getChildren() || []; - }, - setNextmatch: function(_nextmatch) - { - if(this.real_node && this.real_node.setNextmatch) - { - return this.real_node.setNextmatch(_nextmatch); - } - } -});}).call(this); -et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); +var et2_nextmatch_customfilter = /** @class */ (function (_super_1) { + __extends(et2_nextmatch_customfilter, _super_1); + /** + * Constructor + * + * @param _parent + * @param _attrs + * @memberOf et2_nextmatch_customfilter + */ + function et2_nextmatch_customfilter(_parent, _attrs, _child) { + var _this = _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})) || this; + switch (_attrs.widget_type) { + case "link-entry": + _attrs.type = 'nextmatch-entryheader'; + break; + default: + if (_attrs.widget_type.indexOf('select') === 0) { + _attrs.type = 'nextmatch-filterheader'; + } + else { + _attrs.type = _attrs.widget_type; + } + } + jQuery.extend(_attrs.widget_options, { id: _this.id }); + _attrs.id = ''; + _this = _super_1.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})) || this; + _this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, _this.getParent()); + var select_options = []; + var correct_type = _attrs.type; + _this.real_node['type'] = _attrs.widget_type; + et2_widget_selectbox_1.et2_selectbox.find_select_options(_this.real_node, select_options, _attrs); + _this.real_node["_type"] = correct_type; + if (typeof _this.real_node.set_select_options === 'function') { + _this.real_node.set_select_options(select_options); + } + return _this; + } + // Just pass the real DOM node through, in case anybody asks + et2_nextmatch_customfilter.prototype.getDOMNode = function (_sender) { + return this.real_node ? this.real_node.getDOMNode(_sender) : null; + }; + // Also need to pass through real children + et2_nextmatch_customfilter.prototype.getChildren = function () { + return this.real_node.getChildren() || []; + }; + et2_nextmatch_customfilter.prototype.setNextmatch = function (_nextmatch) { + if (this.real_node && this.real_node.instanceOf(et2_INextmatchHeader)) { + return this.real_node.setNextmatch(_nextmatch); + } + }; + return et2_nextmatch_customfilter; +}(et2_nextmatch_filterheader)); +et2_core_widget_1.et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); +//# sourceMappingURL=et2_extension_nextmatch.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_extension_nextmatch.ts b/api/js/etemplate/et2_extension_nextmatch.ts new file mode 100644 index 0000000000..d2815f4770 --- /dev/null +++ b/api/js/etemplate/et2_extension_nextmatch.ts @@ -0,0 +1,4058 @@ +/** + * EGroupware eTemplate2 - JS Nextmatch object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Andreas Stöckel + * @copyright Stylite 2011 + * @version $Id$ + */ + +/*egw:uses + + // Include the action system + egw_action.egw_action; + egw_action.egw_action_popup; + egw_action.egw_action_dragdrop; + egw_action.egw_menu_dhtmlx; + + // Include some core classes + et2_core_widget; + et2_core_interfaces; + et2_core_DOMWidget; + + // Include all widgets the nextmatch extension will create + et2_widget_template; + et2_widget_grid; + et2_widget_selectbox; + et2_widget_selectAccount; + et2_widget_taglist; + et2_extension_customfields; + + // Include all nextmatch subclasses + et2_extension_nextmatch_controller; + et2_extension_nextmatch_rowProvider; + et2_extension_nextmatch_dynheight; + + // Include the grid classes + et2_dataview; + +*/ + +import './et2_core_common'; +import './et2_core_interfaces'; +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_widget} from "./et2_core_widget"; +import {et2_DOMWidget} from "./et2_core_DOMWidget"; +import {et2_baseWidget} from "./et2_core_baseWidget"; +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {et2_selectbox} from "./et2_widget_selectbox"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Interface all special nextmatch header elements have to implement. + */ +interface et2_INextmatchHeader { + + /** + * The 'setNextmatch' function is called by the parent nextmatch widget + * and tells the nextmatch header widgets which widget they should direct + * their 'sort', 'search' or 'filter' calls to. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch(nextmatch : et2_nextmatch) +} + +interface et2_INextmatchSortable{ + + setSortmode(_sort_mode) + +} + +/** + * Class which implements the "nextmatch" XET-Tag + * + * NM header is build like this in DOM + * + * +- nextmatch_header -----+------------+----------+--------+---------+--------------+-----------+-------+ + * + header_left | search.. | header_row | category | filter | filter2 | header_right | favorites | count | + * +-------------+----------+------------+----------+--------+---------+--------------+-----------+-------+ + * + * everything left incl. standard filters is floated left: + * +- nextmatch_header -----+------------+----------+--------+---------+ + * + header_left | search.. | header_row | category | filter | filter2 | + * +-------------+----------+------------+----------+--------+---------+ + * everything from header_right on is floated right: + * +--------------+-----------+-------+ + * | header_right | favorites | count | + * +--------------+-----------+-------+ + * @augments et2_DOMWidget + */ +export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2_IInput, et2_IPrint +{ + static readonly _attributes = { + // These normally set in settings, but broken out into attributes to allow run-time changes + "template": { + "name": "Template", + "type": "string", + "description": "The id of the template which contains the grid layout." + }, + "hide_header": { + "name": "Hide header", + "type": "boolean", + "description": "Hide the header", + "default": false + }, + "header_left": { + "name": "Left custom template", + "type": "string", + "description": "Customise the nextmatch - left side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "header_right": { + "name": "Right custom template", + "type": "string", + "description": "Customise the nextmatch - right side. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "header_row": { + "name": "Inline custom template", + "type": "string", + "description": "Customise the nextmatch - inline, after row count. Provided template becomes a child of nextmatch, and any input widgets are automatically bound to refresh the nextmatch on change. Any inputs with an onChange attribute can trigger the nextmatch to refresh by returning true.", + "default": "" + }, + "no_filter": { + "name": "No filter", + "type": "boolean", + "description": "Hide the first filter", + "default": et2_no_init + }, + "no_filter2": { + "name": "No filter2", + "type": "boolean", + "description": "Hide the second filter", + "default": et2_no_init + }, + "view": { + "name": "View", + "type": "string", + "description": "Display entries as either 'row' or 'tile'. A matching template must also be set after changing this.", + "default": et2_no_init + }, + "onselect": { + "name": "onselect", + "type": "js", + "default": et2_no_init, + "description": "JS code which gets executed when rows are selected. Can also be a app.appname.func(selected) style method" + }, + "onfiledrop": { + "name": "onFileDrop", + "type": "js", + "default": et2_no_init, + "description": "JS code that gets executed when a _file_ is dropped on a row. Other drop interactions are handled by the action system. Return false to prevent the default link action." + }, + "settings": { + "name": "Settings", + "type": "any", + "description": "The nextmatch settings", + "default": {} + } + }; + + // Currently active filters + activeFilters: { + search? : string, + filter? : any, + filter2? : any, + col_filter: {}, + selectcols?: string[], + searchletter?: string + }; + + // DOM / jQuery stuff + private div: JQuery; + private innerDiv: JQuery; + private dynheight: any; + private blank: JQuery; + + // Popup to select columns + private selectPopup: any; + + legacyOptions: ["template","hide_header","header_left","header_right"]; + createNamespace: true; + + private template: any; + columns: {widget: et2_widget}[]; + private sortedColumnsList: string[]; + + // If we need the nextmatch to have a value, keep it here. + // Normally this is used in actions, and is the action and selected rows. + private value: any; + + // Big old bag of settings + private settings: any; + + // Current view, either row or tile. We store it here as controllers are + // recreated when the template changes. + view: string; + + // Sub-objects used for actual work + private header: et2_nextmatch_header_bar; + dataview: any; + private controller: any; + private rowProvider: any; + + + // Flag for an update is currently being done, to avoid a loop + private update_in_progress: boolean; + + // Window timer for automatically refreshing + private _autorefresh_timer: number; + + // When printing, we change the layout around. Keep some values so it can be restored after + private print: { + old_height: number, + row_selector: string, + orientation_style: HTMLStyleElement + }; + + /** + * Constructor + * + * @memberOf et2_nextmatch + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch._attributes, _child || {})); + + this.activeFilters = {col_filter:{}}; + + // Directly set current col_filters from settings + jQuery.extend(this.activeFilters.col_filter, this.options.settings.col_filter); + + /* + Process selected custom fields here, so that the settings are correctly + set before the row template is parsed + */ + var prefs = this._getPreferences(); + var cfs = {}; + for(var i = 0; i < prefs.visible.length; i++) + { + if(prefs.visible[i].indexOf(et2_nextmatch_customfields.prefix) == 0) + { + cfs[prefs.visible[i].substr(1)] = !prefs.negated; + } + } + var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if(typeof global_data == 'object' && global_data != null) + { + global_data.fields = cfs; + } + + this.div = jQuery(document.createElement("div")) + .addClass("et2_nextmatch"); + + + this.header = et2_createWidget("nextmatch_header_bar", {}, this); + this.innerDiv = jQuery(document.createElement("div")) + .appendTo(this.div); + + // Create the dynheight component which dynamically scales the inner + // container. + this.dynheight = this._getDynheight(); + + // Create the outer grid container + this.dataview = new et2_dataview(this.innerDiv, this.egw()); + + // Blank placeholder + this.blank = jQuery(document.createElement("div")) + .appendTo(this.dataview.table); + + // We cannot create the grid controller now, as this depends on the grid + // instance, which can first be created once we have the columns + this.controller = null; + this.rowProvider = null; + + // keeps sorted columns + this.sortedColumnsList = []; + } + + /** + * Destroys all + */ + destroy() + { + // Stop autorefresh + if(this._autorefresh_timer) + { + window.clearInterval(this._autorefresh_timer); + this._autorefresh_timer = null; + } + // Unbind handler used for toggling autorefresh + jQuery(this.getInstanceManager().DOMContainer.parentNode).off('show.et2_nextmatch'); + jQuery(this.getInstanceManager().DOMContainer.parentNode).off('hide.et2_nextmatch'); + + // Free the grid components + this.dataview.free(); + if(this.rowProvider) + { + this.rowProvider.free(); + } + if(this.controller) + { + this.controller.free(); + } + this.dynheight.free(); + + super.destroy(); + } + + /** + * Loads the nextmatch settings + * + * @param {object} _attrs + */ + transformAttributes(_attrs) + { + super.transformAttributes(_attrs); + + if (this.id) + { + var entry = this.getArrayMgr("content").data; + _attrs["settings"] = {}; + + if (entry) + { + _attrs["settings"] = entry; + + // Make sure there's an action var parameter + if(_attrs["settings"]["actions"] && !_attrs.settings["action_var"]) + { + _attrs.settings.action_var = "action"; + } + + // Merge settings mess into attributes + for(var attr in this.attributes) + { + if(_attrs.settings[attr]) + { + _attrs[attr] = _attrs.settings[attr]; + delete _attrs.settings[attr]; + } + } + } + } + } + + doLoadingFinished() + { + super.doLoadingFinished(); + + if(!this.dynheight) + { + this.dynheight = this._getDynheight(); + } + + // Register handler for dropped files, if possible + if(this.options.settings.row_id) + { + // Appname should be first part of the template name + var split = this.options.template.split('.'); + var appname = split[0]; + + // Check link registry + if(this.egw().link_get_registry(appname)) + { + var self = this; + // Register a handler + // @ts-ignore + jQuery(this.div) + .on('dragenter','.egwGridView_grid tr',function(e) { + // Figure out _which_ row + var row = self.controller.getRowByNode(this); + + if(!row || !row.uid) + { + return false; + } + e.stopPropagation(); e.preventDefault(); + + // Indicate acceptance + if(row.controller && row.controller._selectionMgr) + { + row.controller._selectionMgr.setFocused(row.uid,true); + } + return false; + }) + .on('dragexit','.egwGridView_grid tr', function(e) { + self.controller._selectionMgr.setFocused(); + }) + .on('dragover','.egwGridView_grid tr',false).attr("dropzone","copy") + + .on('drop', '.egwGridView_grid tr',function(e) { + self.handle_drop(e,this); + return false; + }); + } + } + // stop invalidation in no visible tabs + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) { + if(this.controller && this.controller._grid) + { + this.controller._grid.doInvalidate = false; + } + },this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) { + if(this.controller && this.controller._grid) + { + this.controller._grid.doInvalidate = true; + } + },this)); + + return true; + } + + /** + * Implements the et2_IResizeable interface - lets the dynheight manager + * update the width and height and then update the dataview container. + */ + resize() + { + if (this.dynheight) + { + this.dynheight.update(function(_w, _h) { + this.dataview.resize(_w, _h); + }, this); + } + } + + /** + * Sorts the nextmatch widget by the given ID. + * + * @param {string} _id is the id of the data entry which should be sorted. + * @param {boolean} _asc if true, the elements are sorted ascending, otherwise + * descending. If not set, the sort direction will be determined + * automatically. + * @param {boolean} _update true/undefined: call applyFilters, false: only set sort + */ + sortBy( _id, _asc, _update? : boolean) + { + if (typeof _update == "undefined") + { + _update = true; + } + + // Create the "sort" entry in the active filters if it did not exist + // yet. + if (typeof this.activeFilters["sort"] == "undefined") + { + this.activeFilters["sort"] = { + "id": null, + "asc": true + }; + } + + // Determine the sort direction automatically if it is not set + if (typeof _asc == "undefined") + { + _asc = true; + if (this.activeFilters["sort"].id == _id) + { + _asc = !this.activeFilters["sort"].asc; + } + } + + // Set the sortmode display + this.iterateOver(function(_widget) { + _widget.setSortmode((_widget.id == _id) ? (_asc ? "asc": "desc") : "none"); + }, this, et2_INextmatchSortable); + + if (_update) + { + this.applyFilters({sort: { id: _id, asc: _asc}}); + } + else + { + // Update the entry in the activeFilters object + this.activeFilters["sort"] = { + "id": _id, + "asc": _asc + }; + } + } + + /** + * Removes the sort entry from the active filters object and thus returns to + * the natural sort order. + */ + resetSort() + { + // Check whether the nextmatch widget is currently sorted + if (typeof this.activeFilters["sort"] != "undefined") + { + // Reset the sortmode + this.iterateOver(function(_widget) { + _widget.setSortmode("none"); + }, this, et2_INextmatchSortable); + + // Delete the "sort" filter entry + this.applyFilters({sort: undefined}); + } + } + + /** + * Apply current or modified filters on NM widget (updating rows accordingly) + * + * @param _set filter(s) to set eg. { filter: '' } to reset filter in NM header + */ + applyFilters( _set? : object | any) + { + var changed = false; + var keep_selection = false; + + // Avoid loops cause by change events + if(this.update_in_progress) return; + this.update_in_progress = true; + + // Cleared explicitly + if(typeof _set != 'undefined' && jQuery.isEmptyObject(_set)) + { + changed = true; + this.activeFilters = {col_filter: {}}; + } + if(typeof this.activeFilters == "undefined") + { + this.activeFilters = {col_filter: {}}; + } + if(typeof this.activeFilters.col_filter == "undefined") + { + this.activeFilters.col_filter = {}; + } + + if (typeof _set == 'object') + { + for(var s in _set) + { + if (s == 'col_filter') + { + // allow apps setState() to reset all col_filter by using undefined or null for it + // they can not pass {} for _set / state.state, if they need to set something + if (_set.col_filter === undefined || _set.col_filter === null) + { + this.activeFilters.col_filter = {}; + changed = true; + } + else + { + for(var c in _set.col_filter) + { + if (this.activeFilters.col_filter[c] !== _set.col_filter[c]) + { + if (_set.col_filter[c]) + { + this.activeFilters.col_filter[c] = _set.col_filter[c]; + } + else + { + delete this.activeFilters.col_filter[c]; + } + changed = true; + } + } + } + } + else if (s === 'selected') + { + changed = true; + keep_selection = true; + this.controller._selectionMgr.resetSelection(); + this.controller._objectManager.clear(); + for(var i in _set.selected) + { + this.controller._selectionMgr.setSelected(_set.selected[i].indexOf('::') > 0 ? _set.selected[i] : this.controller.dataStorePrefix + '::'+_set.selected[i],true); + } + delete _set.selected; + } + else if (this.activeFilters[s] !== _set[s]) + { + this.activeFilters[s] = _set[s]; + changed = true; + } + } + } + + this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); + + // Keep the selection after applying filters, but only if unchanged + if(!changed || keep_selection) + { + this.controller.keepSelection(); + } + else + { + // Do not keep selection + this.controller._selectionMgr.resetSelection(); + this.controller._objectManager.clear(); + this.controller.keepSelection(); + } + + // Update the filters in the grid controller + this.controller.setFilters(this.activeFilters); + + // Update the header + this.header.setFilters(this.activeFilters); + + // Update any column filters + this.iterateOver(function(column) { + // Skip favorites - it implements et2_INextmatchHeader, but we don't want it in the filter + if(typeof column.id != "undefined" && column.id.indexOf('favorite') == 0) return; + + if(typeof column.set_value != "undefined" && column.id) + { + column.set_value(typeof this[column.id] == "undefined" || this[column.id] == null ? "" : this[column.id]); + } + if (column.id && typeof column.get_value == "function") + { + this[column.id] = column.get_value(); + } + }, this.activeFilters.col_filter, et2_INextmatchHeader); + + // Trigger an update + this.controller.update(true); + + if(changed) + { + // Highlight matching favorite in sidebox + if(this.getInstanceManager().app) + { + var appname = this.getInstanceManager().app; + if(app[appname] && app[appname].highlight_favorite) + { + app[appname].highlight_favorite(); + } + } + } + + this.update_in_progress = false; + } + + /** + * Refresh given rows for specified change + * + * Change type parameters allows for quicker refresh then complete server side reload: + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload + * + * @param {string[]|string} _row_ids rows to refresh + * @param {?string} _type "update", "edit", "delete" or "add" + * + * @see jsapi.egw_refresh() + * @fires refresh from the widget itself + */ + refresh( _row_ids, _type) + { + // Framework trying to refresh, but nextmatch not fully initialized + if(this.controller === null || !this.div) + { + return; + } + if (!this.div.is(':visible')) // run refresh, once we become visible again + { + jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function() {this.refresh();},this) + ); + return; + } + if (typeof _type == 'undefined') _type = 'edit'; + if (typeof _row_ids == 'string' || typeof _row_ids == 'number') _row_ids = [_row_ids]; + if (typeof _row_ids == "undefined" || _row_ids === null) + { + this.applyFilters(); + + // Trigger an event so app code can act on it + jQuery(this).triggerHandler("refresh",[this]); + + return; + } + + if(_type == "delete") + { + // Record current & next index + var uid = _row_ids[0].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[0] : this.controller.dataStorePrefix + "::" + _row_ids[0]; + var entry = this.controller._selectionMgr._getRegisteredRowsEntry(uid); + var next = (entry.ao?entry.ao.getNext(_row_ids.length):null); + if(next == null || !next.id || next.id == uid) + { + // No next, select previous + next = (entry.ao?entry.ao.getPrevious(1):null); + } + + // Stop automatic updating + this.dataview.grid.doInvalidate = false; + for(var i = 0; i < _row_ids.length; i++) + { + uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + + // Delete from internal references + this.controller.deleteRow(uid); + } + + // Select & focus next row + if(next && next.id) + { + this.controller._selectionMgr.setSelected(next.id,true); + this.controller._selectionMgr.setFocused(next.id,true); + } + + // Update the count + var total = this.dataview.grid._total - _row_ids.length; + // This will remove the last row! + // That's OK, because grid adds one in this.controller.deleteRow() + this.dataview.grid.setTotalCount(total); + // Re-enable automatic updating + this.dataview.grid.doInvalidate = true; + this.dataview.grid.invalidate(); + } + + id_loop: + for(var i = 0; i < _row_ids.length; i++) + { + var uid = _row_ids[i].toString().indexOf(this.controller.dataStorePrefix) == 0 ? _row_ids[i] : this.controller.dataStorePrefix + "::" + _row_ids[i]; + switch(_type) + { + case "update": + if(!this.egw().dataRefreshUID(uid)) + { + // Could not update just that row + this.applyFilters(); + break id_loop; + } + break; + case "delete": + // Handled above, more code to execute after loop + break; + case "edit": + case "add": + default: + // Trigger refresh + this.applyFilters(); + break id_loop; + } + } + // Trigger an event so app code can act on it + jQuery(this).triggerHandler("refresh",[this,_row_ids,_type]); + } + + /** + * Gets the selection + * + * @return Object { ids: [UIDs], inverted: boolean} + */ + getSelection() : {ids : string[], all : boolean} + { + var selected = this.controller && this.controller._selectionMgr ? this.controller._selectionMgr.getSelected() : null; + if(typeof selected == "object" && selected != null) + { + return selected; + } + return {ids:[],all:false}; + } + + /** + * Event handler for when the selection changes + * + * If the onselect attribute was set to a string with javascript code, it will + * be executed "legacy style". You can get the selected values with getSelection(). + * If the onselect attribute is in app.appname.function style, it will be called + * with the nextmatch and an array of selected row IDs. + * + * The array can be empty, if user cleared the selection. + * + * @param action ActionObject From action system. Ignored. + * @param senders ActionObjectImplemetation From action system. Ignored. + */ + onselect( action,senders) + { + // Execute the JS code connected to the event handler + if (typeof this.options.onselect == 'function') + { + return this.options.onselect.call(this, this.getSelection().ids, this); + } + } + + /** + * Create the dynamic height so nm fills all available space + * + * @returns {undefined} + */ + _getDynheight() + { + // Find the parent container, either a tab or the main container + var tab = this.get_tab_info(); + + if(!tab) + { + return new et2_dynheight(this.getInstanceManager().DOMContainer, this.innerDiv, 100); + } + + else if (tab && tab.contentDiv) + { + return new et2_dynheight(tab.contentDiv, this.innerDiv, 100); + } + + return false; + } + + /** + * Generates the column caption for the given column widget + * + * @param {et2_widget} _widget + */ + _genColumnCaption( _widget) + { + var result = null; + + if(typeof _widget._genColumnCaption == "function") return _widget._genColumnCaption(); + var self = this; + + _widget.iterateOver(function(_widget) { + var label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); + if (!label) return; // skip empty, undefined or null labels + if (!result) + { + result = label; + } + else + { + result += ", " + label; + } + }, this, et2_INextmatchHeader); + + return result; + } + + /** + * Generates the column name (internal) for the given column widget + * Used in preferences to refer to the columns by name instead of position + * + * See _getColumnCaption() for human fiendly captions + * + * @param {et2_widget} _widget + */ + _getColumnName( _widget) + { + if(typeof _widget._getColumnName == 'function') return _widget._getColumnName(); + + var name = _widget.id; + var child_names = []; + var children = _widget.getChildren(); + for(var i = 0; i < children.length; i++) { + if(children[i].id) child_names.push(children[i].id); + } + + var colName = name + (name != "" && child_names.length > 0 ? "_" : "") + child_names.join("_"); + if(colName == "") { + this.egw().debug("info", "Unable to generate nm column name for ", _widget); + } + return colName; + } + + + /** + * Retrieve the user's preferences for this nextmatch merged with defaults + * Column display, column size, etc. + */ + _getPreferences() + { + // Read preference or default for column visibility + var negated = false; + var columnPreference = ""; + if(this.options.settings.default_cols) + { + negated = this.options.settings.default_cols[0] == "!"; + columnPreference = negated ? this.options.settings.default_cols.substring(1) : this.options.settings.default_cols; + } + if(this.options.settings.selectcols && this.options.settings.selectcols.length) + { + columnPreference = this.options.settings.selectcols; + negated = false; + } + if(!this.options.settings.columnselection_pref) + { + // Set preference name so changes are saved + this.options.settings.columnselection_pref = this.options.template; + } + + var app = ''; + var list = []; + if(this.options.settings.columnselection_pref) { + var pref = {}; + list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); + if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) + { + app = list[0].substring('nextmatch'.length+1); + pref = egw.preference(this.options.settings.columnselection_pref, app); + } + else + { + app = list[0]; + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); + } + if(pref) + { + negated = (pref[0] == "!"); + columnPreference = negated ? (pref).substring(1) : pref; + } + } + + let columnDisplay = []; + // If no column preference or default set, use all columns + if(typeof columnPreference =="string" && columnPreference.length == 0) + { + columnDisplay = []; + negated = true; + } + + columnDisplay = typeof columnPreference === "string" + ? et2_csvSplit(columnPreference,null,",") : columnPreference; + + // Adjusted column sizes + var size = {}; + if(this.options.settings.columnselection_pref && app) + { + var size_pref = this.options.settings.columnselection_pref +"-size"; + + // If columnselection pref is missing prefix, add it in + if(size_pref.indexOf('nextmatch') == -1) + { + size_pref = 'nextmatch-'+size_pref; + } + size = this.egw().preference(size_pref, app); + } + if(!size) size = {}; + + // Column order + var order = {}; + for(var i = 0; i < columnDisplay.length; i++) + { + order[columnDisplay[i]] = i; + } + return { + visible: columnDisplay, + visible_negated: negated, + negated: negated, + size: size, + order: order + }; + } + + /** + * Apply stored user preferences to discovered columns + * + * @param {array} _row + * @param {array} _colData + */ + _applyUserPreferences( _row, _colData) + { + var prefs = this._getPreferences(); + var columnDisplay = prefs.visible; + var size = prefs.size; + var negated = prefs.visible_negated; + var order = prefs.order; + var colName = ''; + + // Add in display preferences + if(columnDisplay && columnDisplay.length > 0) + { + RowLoop: + for(var i = 0; i < _row.length; i++) + { + colName = ''; + if(_row[i].disabled === true) + { + _colData[i].visible = false; + continue; + } + + // Customfields needs special processing + if(_row[i].widget.instanceOf(et2_nextmatch_customfields)) + { + // Find cf field + for(var j = 0; j < columnDisplay.length; j++) + { + if(columnDisplay[j].indexOf(_row[i].widget.id) == 0) { + _row[i].widget.options.fields = {}; + for(var k = j; k < columnDisplay.length; k++) + { + if(columnDisplay[k].indexOf(_row[i].widget.prefix) == 0) + { + _row[i].widget.options.fields[columnDisplay[k].substr(1)] = true; + } + } + // Resets field visibility too + _row[i].widget._getColumnName(); + _colData[i].visible = !(negated || jQuery.isEmptyObject(_row[i].widget.options.fields)); + break; + } + } + // Disable if there are no custom fields + if(jQuery.isEmptyObject(_row[i].widget.customfields)) + { + _colData[i].visible = false; + continue; + } + colName = _row[i].widget.id; + } + else + { + colName = this._getColumnName(_row[i].widget); + } + if(!colName) continue; + + if(size[colName]) + { + // Make sure percentages stay percentages, and forget any preference otherwise + if(_colData[i].width.charAt(_colData[i].width.length - 1) == "%") + { + _colData[i].width = typeof size[colName] == 'string' && size[colName].charAt(size[colName].length - 1) == "%" ? size[colName] : _colData[i].width; + } + else + { + _colData[i].width = parseInt(size[colName])+'px'; + } + } + if(!negated) + { + _colData[i].order = typeof order[colName] === 'undefined' ? i : order[colName]; + } + for(var j = 0; j < columnDisplay.length; j++) + { + if(columnDisplay[j] == colName) + { + _colData[i].visible = !negated; + + continue RowLoop; + } + } + _colData[i].visible = negated; + } + } + + _colData.sort(function(a,b) { + return a.order - b.order; + }); + _row.sort(function(a,b) { + if(typeof a.colData !== 'undefined' && typeof b.colData !== 'undefined') + { + return a.colData.order - b.colData.order; + } + else if (typeof a.order !== 'undefined' && typeof b.order !== 'undefined') + { + return a.order - b.order; + } + }); + } + + /** + * Take current column display settings and store them in this.egw().preferences + * for next time + */ + _updateUserPreferences() + { + var colMgr = this.dataview.getColumnMgr(); + var app = ""; + if(!this.options.settings.columnselection_pref) { + this.options.settings.columnselection_pref = this.options.template; + } + + var visibility = colMgr.getColumnVisibilitySet(); + var colDisplay = []; + var colSize = {}; + var custom_fields = []; + + // visibility is indexed by internal ID, widget is referenced by position, preference needs name + for(var i = 0; i < colMgr.columns.length; i++) + { + // @ts-ignore + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if(colName) { + // Server side wants each cf listed as a seperate column + if(widget.instanceOf(et2_nextmatch_customfields)) + { + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + for(var name in widget.options.fields) { + if(widget.options.fields[name]) custom_fields.push(et2_nextmatch_customfields.prefix+name); + } + } + if(visibility[colMgr.columns[i].id].visible) colDisplay.push(colName); + + // When saving sizes, only save columns with explicit values, preserving relative vs fixed + // Others will be left to flex if width changes or more columns are added + if(colMgr.columns[i].relativeWidth) + { + colSize[colName] = (colMgr.columns[i].relativeWidth * 100) + "%"; + } + else if (colMgr.columns[i].fixedWidth) + { + colSize[colName] = colMgr.columns[i].fixedWidth; + } + } else if (colMgr.columns[i].fixedWidth || colMgr.columns[i].relativeWidth) { + this.egw().debug("info", "Could not save column width - no name", colMgr.columns[i].id); + } + } + + var list = et2_csvSplit(this.options.settings.columnselection_pref, 2, "."); + var pref = this.options.settings.columnselection_pref; + if(pref.indexOf('nextmatch') == 0) + { + app = list[0].substring('nextmatch'.length+1); + } + else + { + app = list[0]; + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = "nextmatch-"+this.options.settings.columnselection_pref; + } + + // Server side wants each cf listed as a seperate column + jQuery.merge(colDisplay, custom_fields); + + // Update query value, so data source can use visible columns to exclude expensive sub-queries + var oldCols = this.activeFilters.selectcols ? this.activeFilters.selectcols : []; + + this.activeFilters.selectcols = this.sortedColumnsList ? this.sortedColumnsList : colDisplay; + + // We don't need to re-query if they've removed a column + var changed = []; + ColLoop: + for(var i = 0; i < colDisplay.length; i++) + { + for(var j = 0; j < oldCols.length; j++) { + if(colDisplay[i] == oldCols[j]) continue ColLoop; + } + changed.push(colDisplay[i]); + } + + // If a custom field column was added, throw away cache to deal with + // efficient apps that didn't send all custom fields in the first request + var cf_added = jQuery(changed).filter(jQuery(custom_fields)).length > 0; + + // Save visible columns + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + this.egw().set_preference(app, pref, this.activeFilters.selectcols.join(","), + // Use callback after the preference gets set to trigger refresh, in case app + // isn't looking at selectcols and just uses preference + cf_added ? jQuery.proxy(function() {if(this.controller) this.controller.update(true);}, this):null + ); + + // Save adjusted column sizes + this.egw().set_preference(app, pref+"-size", colSize); + + // No significant change (just normal columns shown) and no need to wait, + // but the grid still needs to be redrawn if a custom field was removed because + // the cell content changed. This is a cheaper refresh than the callback, + // this.controller.update(true) + if((changed.length || custom_fields.length) && !cf_added) this.applyFilters(); + } + + _parseHeaderRow( _row, _colData) + { + + // Make sure there's a widget - cols disabled in template can be missing them, and the header really likes to have a widget + + for (var x = 0; x < _row.length; x++) + { + if(!_row[x].widget) + { + _row[x].widget = et2_createWidget("label", {}); + } + } + + // Get column display preference + this._applyUserPreferences(_row, _colData); + + // Go over the header row and create the column entries + this.columns = new Array(_row.length); + var columnData = new Array(_row.length); + + // No action columns in et2 + var remove_action_index = null; + + for (var x = 0; x < _row.length; x++) + { + this.columns[x] = jQuery.extend({ + "order": _colData[x] && typeof _colData[x].order !== 'undefined' ? _colData[x].order : x, + "widget": _row[x].widget + },_colData[x]); + + var visibility = (!_colData[x] || _colData[x].visible) ? + et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE : + et2_dataview_grid.ET2_COL_VISIBILITY_INVISIBLE; + if(_colData[x].disabled && _colData[x].disabled !=='' && + this.getArrayMgr("content").parseBoolExpression(_colData[x].disabled)) + { + visibility = et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED; + } + columnData[x] = { + "id": "col_" + x, + // @ts-ignore + "order": this.columns[x].order, + "caption": this._genColumnCaption(_row[x].widget), + "visibility": visibility, + "width": _colData[x] ? _colData[x].width : 0 + }; + if(_colData[x].width === 'auto') + { + // Column manager does not understand 'auto', which grid widget + // uses if width is not set + columnData[x].width = '100%'; + } + if(_colData[x].minWidth) + { + columnData[x].minWidth = _colData[x].minWidth; + } + if(_colData[x].maxWidth) + { + columnData[x].maxWidth = _colData[x].maxWidth; + } + + // No action columns in et2 + var colName = this._getColumnName(_row[x].widget); + if(colName == 'actions' || colName == 'legacy_actions' || colName == 'legacy_actions_check_all') + { + remove_action_index = x; + + } + else if (!colName) + { + // Unnamed column cannot be toggled or saved + columnData[x].visibility = et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT; + } + + } + + // Remove action column + if(remove_action_index != null) + { + this.columns.splice(remove_action_index,remove_action_index); + columnData.splice(remove_action_index,remove_action_index); + _colData.splice(remove_action_index,remove_action_index); + } + + // Create the column manager and update the grid container + this.dataview.setColumns(columnData); + + for (var x = 0; x < _row.length; x++) + { + // Append the widget to this container + this.addChild(_row[x].widget); + } + + // Create the nextmatch row provider + this.rowProvider = new et2_nextmatch_rowProvider( + this.dataview.rowProvider, this._getSubgrid, this); + + // Register handler to update preferences when column properties are changed + var self = this; + this.dataview.onUpdateColumns = function() { + // Use apply to make sure context is there + self._updateUserPreferences.apply(self); + + // Allow column widgets a chance to resize + self.iterateOver(function(widget) {widget.resize();}, self, et2_IResizeable); + }; + + // Register handler for column selection popup, or disable + if(this.selectPopup) + { + this.selectPopup.remove(); + this.selectPopup = null; + } + if(this.options.settings.no_columnselection) + { + this.dataview.selectColumnsClick = function() {return false;}; + jQuery('span.selectcols',this.dataview.headTr).hide(); + } + else + { + jQuery('span.selectcols',this.dataview.headTr).show(); + this.dataview.selectColumnsClick = function(event) { + self._selectColumnsClick(event); + }; + } + } + + _parseDataRow( _row, _rowData, _colData) + { + var columnWidgets = new Array(this.columns.length); + + _row.sort(function(a,b) { + return a.colData.order - b.colData.order; + }); + + for (var x = 0; x < columnWidgets.length; x++) + { + if (typeof _row[x] != "undefined" && _row[x].widget) + { + columnWidgets[x] = _row[x].widget; + + // Append the widget to this container + this.addChild(_row[x].widget); + } + else + { + columnWidgets[x] = _row[x].widget; + } + // Pass along column alignment + if(_row[x].align && columnWidgets[x]) + { + columnWidgets[x].align = _row[x].align; + } + } + + this.rowProvider.setDataRowTemplate(columnWidgets, _rowData, this); + + + // Create the grid controller + this.controller = new et2_nextmatch_controller( + null, + this.egw(), + this.getInstanceManager().etemplate_exec_id, + this, + null, + this.dataview.grid, + this.rowProvider, + this.options.settings.action_links, + null, + this.options.actions + ); + + // Need to trigger empty row the first time + if(total == 0) this.controller._emptyRow(); + + // Set data cache prefix to either provided custom or auto + if(!this.options.settings.dataStorePrefix && this.options.settings.get_rows) + { + // Use jsapi data module to update + var list = this.options.settings.get_rows.split('.', 2); + if (list.length < 2) list = this.options.settings.get_rows.split('_'); // support "app_something::method" + this.options.settings.dataStorePrefix = list[0]; + } + this.controller.setPrefix(this.options.settings.dataStorePrefix); + + // Set the view + this.controller._view = this.view; + + // Load the initial order + /*this.controller.loadInitialOrder(this._getInitialOrder( + this.options.settings.rows, this.options.settings.row_id + ));*/ + + // Set the initial row count + var total = typeof this.options.settings.total != "undefined" ? + this.options.settings.total : 0; + // This triggers an invalidate, which updates the grid + this.dataview.grid.setTotalCount(total); + + // Insert any data sent from server, so invalidate finds data already + if(this.options.settings.rows && this.options.settings.num_rows) + { + this.controller.loadInitialData( + this.options.settings.dataStorePrefix, + this.options.settings.row_id, + this.options.settings.rows + ); + // Remove, to prevent duplication + delete this.options.settings.rows; + } + } + + _parseGrid( _grid) + { + // Search the rows for a header-row - if one is found, parse it + for (var y = 0; y < _grid.rowData.length; y++) + { + // Parse the first row as a header, need header to parse the data rows + if (_grid.rowData[y]["class"] == "th" || y == 0) + { + this._parseHeaderRow(_grid.cells[y], _grid.colData); + } + else + { + this._parseDataRow(_grid.cells[y], _grid.rowData[y], + _grid.colData); + } + } + this.dataview.table.resize(); + } + + _getSubgrid( _row, _data, _controller) + { + // Fetch the id of the element described by _data, this will be the + // parent_id of the elements in the subgrid + var rowId = _data.content[this.options.settings.row_id]; + + // Create a new grid with the row as parent and the dataview grid as + // parent grid + var grid = new et2_dataview_grid(_row, this.dataview.grid); + + // Create a new controller for the grid + var controller = new et2_nextmatch_controller( + _controller, + this.egw(), + this.getInstanceManager().etemplate_exec_id, + this, + rowId, + grid, + this.rowProvider, + this.options.settings.action_links, + _controller.getObjectManager() + ); + controller.update(); + + // Register inside the destruction callback of the grid + grid.setDestroyCallback(function () { + controller.free(); + }); + + return grid; + } + + _getInitialOrder( _rows, _rowId) + { + + var _order = []; + + // Get the length of the non-numerical rows arra + var len = 0; + for (var key in _rows) { + if (!isNaN(parseInt(key)) && parseInt(key) > len) + len = parseInt(key); + } + + // Iterate over the rows + for (var i = 0; i < len; i++) + { + // Get the uid from the data + var uid = this.egw().appName + '::' + _rows[i][_rowId]; + + // Store the data for that uid + this.egw().dataStoreUID(uid, _rows[i]); + + // Push the uid onto the order array + _order.push(uid); + } + + return _order; + } + + _selectColumnsClick( e) + { + var self = this; + var columnMgr = this.dataview.getColumnMgr(); + + // ID for faking letter selection in column selection + var LETTERS = '~search_letter~'; + + var columns = {}; + var columns_selected = []; + + for (var i = 0; i < columnMgr.columns.length; i++) + { + var col = columnMgr.columns[i]; + var widget = this.columns[i].widget; + + if(col.caption && col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED) + { + columns[col.id] = col.caption; + if(col.visibility == et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(col.id); + } + // Custom fields get listed separately + if(widget.instanceOf(et2_nextmatch_customfields)) + { + if(jQuery.isEmptyObject((widget).customfields)) + { + // No customfields defined, don't show column + delete(columns[col.id]); + continue; + } + for(var field_name in (widget).customfields) + { + columns[et2_nextmatch_customfields.prefix+field_name] = " - "+ + (widget).customfields[field_name].label; + if(widget.options.fields[field_name]) columns_selected.push(et2_customfields_list.prefix+field_name); + } + } + } + + // Letter search + if(this.options.settings.lettersearch) + { + columns[LETTERS] = egw.lang('Search letter'); + if(this.header.lettersearch.is(':visible')) columns_selected.push(LETTERS); + } + + // Build the popup + if(!this.selectPopup) + { + var select = et2_createWidget("select", { + multiple: true, + rows: 8, + empty_label:this.egw().lang("select columns"), + selected_first: false, + value_class:"selcolumn_sortable_" + }, this); + select.set_select_options(columns); + select.set_value(columns_selected); + + var autoRefresh = et2_createWidget("select", { + "empty_label":"Refresh" + }, this); + autoRefresh.set_id("nm_autorefresh"); + autoRefresh.set_select_options({ + // Cause [unknown] problems with mail + //30: "30 seconds", + //60: "1 Minute", + 300: "5 Minutes", + 900: "15 Minutes", + 1800: "30 Minutes" + }); + autoRefresh.set_value(this._get_autorefresh()); + autoRefresh.set_statustext(egw.lang("Automatically refresh list")); + + var defaultCheck = et2_createWidget("select", {"empty_label":"Preference"}, this); + defaultCheck.set_id('nm_col_preference'); + defaultCheck.set_select_options({ + 'default': {label: 'Default',title:'Set these columns as the default'}, + 'reset': {label: 'Reset', title:"Reset all user's column preferences"}, + 'force': {label: 'Force', title:'Force column preference so users cannot change it'} + }); + defaultCheck.set_value(this.options.settings.columns_forced ? 'force': ''); + + var okButton = et2_createWidget("buttononly", {"background_image":true, image:"check"}, this); + okButton.set_label(this.egw().lang("ok")); + okButton.onclick = function() { + // Update visibility + var visibility = {}; + for (var i = 0; i < columnMgr.columns.length; i++) + { + var col = columnMgr.columns[i]; + if(col.caption && col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED ) + { + visibility[col.id] = {visible: false}; + } + } + var value = select.getValue(); + + // Update & remove letter filter + if(self.header.lettersearch) + { + var show_letters = true; + if(value.indexOf(LETTERS) >= 0) + { + value.splice(value.indexOf(LETTERS),1); + } + else + { + show_letters = false; + } + self._set_lettersearch(show_letters); + } + + var column = 0; + for(var i = 0; i < value.length; i++) + { + // Handle skipped columns + while(value[i] != "col_"+column && column < columnMgr.columns.length) + { + column++; + } + if(visibility[value[i]]) + { + visibility[value[i]].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if(self.columns[column] && self.columns[column].widget.instanceOf(et2_nextmatch_customfields)) { + var cf = self.columns[column].widget.options.customfields; + var visible = self.columns[column].widget.options.fields; + + // Turn off all custom fields + for(var field_name in cf) + { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for(var j = 0; j < value.length; j++) + { + if(value[j].indexOf(et2_customfields_list.prefix) != 0) continue; + visible[value[j].substring(1)] = true; + i++; + } + (self.columns[column].widget).set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + + this.sortedColumnsList = []; + jQuery(select.getDOMNode()).find('li[class^="selcolumn_sortable_"]').each(function(i,v){ + var data_id = v.getAttribute('data-value'); + var value = select.getValue(); + if (data_id.match(/^col_/) && value.indexOf(data_id) != -1) + { + var col_id = data_id.replace('col_',''); + var col_widget = self.columns[col_id].widget; + if (col_widget.customfields) + { + self.sortedColumnsList.push(col_widget.id); + for(var field_name in col_widget.customfields) + { + if(jQuery.isEmptyObject(col_widget.options.fields) || col_widget.options.fields[field_name] == true) + { + self.sortedColumnsList.push(et2_customfields_list.prefix + field_name); + } + } + } + else + { + self.sortedColumnsList.push(self._getColumnName(col_widget)); + } + } + }); + + // Hide popup + self.selectPopup.toggle(); + + self.dataview.updateColumns(); + + // Auto refresh + self._set_autorefresh(autoRefresh.get_value()); + + // Set default or clear forced + if(show_letters) + { + self.activeFilters.selectcols.push('lettersearch'); + } + self.getInstanceManager().submit(); + + self.selectPopup = null; + }; + + var cancelButton = et2_createWidget("buttononly", {"background_image":true, image:"cancel"}, this); + cancelButton.set_label(this.egw().lang("cancel")); + cancelButton.onclick = function() { + self.selectPopup.toggle(); + self.selectPopup = null; + }; + var $select = jQuery(select.getDOMNode()); + $select.find('.ui-multiselect-checkboxes').sortable({ + placeholder:'ui-fav-sortable-placeholder', + items:'li[class^="selcolumn_sortable_col"]', + cancel: 'li[class^="selcolumn_sortable_#"]', + cursor: "move", + tolerance: "pointer", + axis: 'y', + containment: "parent", + delay: 250, //(millisecond) delay before the sorting should start + beforeStop: function(event, ui) { + jQuery('li[class^="selcolumn_sortable_#"]', this).css({ + opacity: 1 + }); + }, + start: function(event, ui){ + jQuery('li[class^="selcolumn_sortable_#"]', this).css({ + opacity: 0.5 + }); + }, + sort: function (event, ui) + { + jQuery( this ).sortable("refreshPositions" ); + } + }); + $select.disableSelection(); + $select.find('li[class^="selcolumn_sortable_"]').each(function(i,v){ + // @ts-ignore + jQuery(v).attr('data-value',(jQuery(v).find('input')[0].value)) + }); + var $footerWrap = jQuery(document.createElement("div")) + .addClass('dialogFooterToolbar') + .append(okButton.getDOMNode()) + .append(cancelButton.getDOMNode()); + this.selectPopup = jQuery(document.createElement("div")) + .addClass("colselection ui-dialog ui-widget-content") + .append(select.getDOMNode()) + .append($footerWrap) + .appendTo(this.innerDiv); + + // Add autorefresh + $footerWrap.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode())); + + // Add default checkbox for admins + var apps = this.egw().user('apps'); + if(apps['admin']) + { + $footerWrap.append(defaultCheck.getSurroundings().getDOMNode(defaultCheck.getDOMNode())); + } + } + else + { + this.selectPopup.toggle(); + } + var t_position = jQuery(e.target).position(); + var s_position = this.div.position(); + var max_height = this.getDOMNode().getElementsByClassName('egwGridView_outer')[0]['tBodies'][0].clientHeight - + (2 * this.selectPopup.find('.dialogFooterToolbar').height()); + this.selectPopup.find('.ui-multiselect-checkboxes').css('max-height',max_height); + this.selectPopup.css("top", t_position.top) + .css("left", s_position.left + this.div.width() - this.selectPopup.width()); + } + + /** + * Set the currently displayed columns, without updating user's preference + * + * @param {string[]} column_list List of column names + * @param {boolean} trigger_update =false - explicitly trigger an update + */ + set_columns(column_list : string[], trigger_update = false) + { + var columnMgr = this.dataview.getColumnMgr(); + var visibility = {}; + + // Initialize to false + for (var i = 0; i < columnMgr.columns.length; i++) + { + var col = columnMgr.columns[i]; + if(col.caption && col.visibility != et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT ) + { + visibility[col.id] = {visible: false}; + } + } + for(var i = 0; i < this.columns.length; i++) + { + + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if(column_list.indexOf(colName) !== -1 && + typeof visibility[columnMgr.columns[i].id] !== 'undefined' + ) + { + visibility[columnMgr.columns[i].id].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if(widget && widget.instanceOf(et2_nextmatch_customfields)) { + + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + if(column_list.indexOf(colName) !== -1) + { + visibility[columnMgr.columns[i].id].visible = true; + } + + var cf = this.columns[i].widget.options.customfields; + var visible = this.columns[i].widget.options.fields; + + // Turn off all custom fields + for(var field_name in cf) + { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for(var j = 0; j < column_list.length; j++) + { + if(column_list[j].indexOf(et2_customfields_list.prefix) != 0) continue; + visible[column_list[j].substring(1)] = true; + } + (widget).set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + + // We don't want to update user's preference, so directly update + this.dataview._updateColumns(); + + // Allow column widgets a chance to resize + this.iterateOver(function(widget) {widget.resize();}, this, et2_IResizeable); + } + + /** + * Set the letter search preference, and update the UI + * + * @param {boolean} letters_on + */ + _set_lettersearch( letters_on) + { + if(letters_on) + { + this.header.lettersearch.show(); + } + else + { + this.header.lettersearch.hide(); + } + var lettersearch_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-lettersearch"; + this.egw().set_preference(this.egw().getAppName(),lettersearch_preference,letters_on); + } + + /** + * Set the auto-refresh time period, and starts the timer if not started + * + * @param time int Refresh period, in seconds + */ + _set_autorefresh( time) + { + // Store preference + var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; + var app = this.options.template.split("."); + if(this._get_autorefresh() != time) + { + this.egw().set_preference(app[0],refresh_preference,time); + } + + // Start / update timer + if (this._autorefresh_timer) + { + window.clearInterval(this._autorefresh_timer); + delete this._autorefresh_timer; + } + if(time > 0) + { + this._autorefresh_timer = setInterval(jQuery.proxy(this.controller.update, this.controller), time * 1000); + + // Bind to tab show/hide events, so that we don't bother refreshing in the background + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('hide.et2_nextmatch', jQuery.proxy(function(e) { + // Stop + window.clearInterval(this._autorefresh_timer); + jQuery(e.target).off(e); + + // If the autorefresh time is up, bind once to trigger a refresh + // (if needed) when tab is activated again + this._autorefresh_timer = setTimeout(jQuery.proxy(function() { + // Check in case it was stopped / destroyed since + if(!this._autorefresh_timer || !this.getInstanceManager()) return; + + jQuery(this.getInstanceManager().DOMContainer.parentNode).one('show.et2_nextmatch', + // Important to use anonymous function instead of just 'this.refresh' because + // of the parameters passed + jQuery.proxy(function() {this.refresh();},this) + ); + },this), time*1000); + },this)); + jQuery(this.getInstanceManager().DOMContainer.parentNode).on('show.et2_nextmatch', jQuery.proxy(function(e) { + // Start normal autorefresh timer again + this._set_autorefresh(this._get_autorefresh()); + jQuery(e.target).off(e); + },this)); + } + } + + /** + * Get the auto-refresh timer + * + * @return int Refresh period, in secods + */ + _get_autorefresh( ) + { + var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; + var app = this.options.template.split("."); + return this.egw().preference(refresh_preference,app[0]); + } + + /** + * When the template attribute is set, the nextmatch widget tries to load + * that template and to fetch the grid which is inside of it. It then calls + * + * @param {string} _value template name + */ + set_template( template_name : string) + { + if(this.template) + { + // Stop early to prevent unneeded processing, and prevent infinite + // loops if the server changes the template in get_rows + if(this.template == template_name) + { + return; + } + + // Free the grid components - they'll be re-created as the template is processed + this.dataview.free(); + this.rowProvider.free(); + this.controller.free(); + + // Free any children from previous template + // They may get left behind because of how detached nodes are processed + // We don't use iterateOver because it checks sub-children + for(var i = this._children.length-1; i >=0 ; i--) + { + var _node = this._children[i]; + if(_node != this.header) { + this.removeChild(_node); + _node.destroy(); + } + } + + // Clear this setting if it's the same as the template, or + // the columns will not be loaded + if(this.template == this.options.settings.columnselection_pref) + { + this.options.settings.columnselection_pref = template_name; + } + this.dataview = new et2_dataview(this.innerDiv, this.egw()); + } + + // Create the template + var template = et2_createWidget("template", {"id": template_name}, this); + + if (!template) + { + this.egw().debug("error", "Error while loading definition template for " + + "nextmatch widget.",template_name); + return; + } + + if(this.options.disabled) + { + return; + } + + // Deferred parse function - template might not be fully loaded + var parse = function(template) + { + // Keep the name of the template, as we'll free up the widget after parsing + this.template = template_name; + + // Fetch the grid element and parse it + var definitionGrid = template.getChildren()[0]; + if (definitionGrid && definitionGrid instanceof et2_grid) + { + this._parseGrid(definitionGrid); + } + else + { + this.egw().debug("error", "Nextmatch widget expects a grid to be the " + + "first child of the defined template."); + return; + } + + // Free the template again, but don't remove it + setTimeout(function() { + template.free(); + },1); + + // Call the "setNextmatch" function of all registered + // INextmatchHeader widgets. This updates this.activeFilters.col_filters according + // to what's in the template. + this.iterateOver(function (_node) { + _node.setNextmatch(this); + }, this, et2_INextmatchHeader); + + // Set filters to current values + this.controller.setFilters(this.activeFilters); + + // If no data was sent from the server, and num_rows is 0, the nm will be empty. + // This triggers a cache check. + if(!this.options.settings.num_rows) + { + this.controller.update(); + } + + // Load the default sort order + if (this.options.settings.order && this.options.settings.sort) + { + this.sortBy(this.options.settings.order, + this.options.settings.sort == "ASC", false); + } + + // Start auto-refresh + this._set_autorefresh(this._get_autorefresh()); + }; + + // Template might not be loaded yet, defer parsing + var promise = []; + template.loadingFinished(promise); + + // Wait until template (& children) are done + jQuery.when.apply(null, promise).done( + jQuery.proxy(function() { + parse.call(this, template); + if(!this.dynheight) + { + this.dynheight = this._getDynheight(); + } + this.dynheight.initialized = false; + this.resize(); + }, this) + ); + } + + // Some accessors to match conventions + set_hide_header( hide : boolean) + { + (hide ? this.header.div.hide() : this.header.div.show()); + } + + set_header_left( template : string) + { + this.header._build_header("left",template); + } + set_header_right( template : string) + { + this.header._build_header("right",template); + } + set_header_row( template : string) + { + this.header._build_header("row",template); + } + set_no_filter( bool, filter_name) + { + if(typeof filter_name == 'undefined') + { + filter_name = 'filter'; + } + this.options['no_'+filter_name] = bool; + + var filter = this.header[filter_name]; + if(filter) + { + filter.set_disabled(bool); + } + else if (bool) + { + filter = this.header._build_select(filter_name, 'select', + this.settings[filter_name], this.settings[filter_name+'_no_lang']); + } + } + set_no_filter2( bool) + { + this.set_no_filter(bool,'filter2'); + } + + /** + * Directly change filter value, with no server query. + * + * This allows the server app code to change filter value, and have it + * updated in the client UI. + * + * @param {String|number} value + */ + set_filter( value) + { + var update = this.update_in_progress; + this.update_in_progress = true; + + this.activeFilters.filter = value; + + // Update the header + this.header.setFilters(this.activeFilters); + + this.update_in_progress = update; + } + + /** + * Directly change filter2 value, with no server query. + * + * This allows the server app code to change filter2 value, and have it + * updated in the client UI. + * + * @param {String|number} value + */ + set_filter2( value) + { + var update = this.update_in_progress; + this.update_in_progress = true; + + this.activeFilters.filter2 = value; + + // Update the header + this.header.setFilters(this.activeFilters); + + this.update_in_progress = update; + } + + /** + * If nextmatch starts disabled, it will need a resize after being shown + * to get all the sizing correct. Override the parent to add the resize + * when enabling. + * + * @param {boolean} _value + */ + set_disabled(_value : boolean) + { + var previous = this.disabled; + super.set_disabled(_value); + + if(previous && !_value) + { + this.resize(); + } + } + + /** + * Actions are handled by the controller, so ignore these during init. + * + * @param {object} actions + */ + set_actions( actions : object[]) + { + if(actions != this.options.actions && this.controller != null && this.controller._actionManager) + { + for(var i = this.controller._actionManager.children.length - 1; i >= 0; i--) + { + this.controller._actionManager.children[i].remove(); + } + this.options.actions = actions; + this.options.settings.action_links = this.controller._actionLinks = this._get_action_links(actions); + + this.controller._initActions(actions); + } + } + + /** + * Switch view between row and tile. + * This should be followed by a call to change the template to match, which + * will cause a reload of the grid using the new settings. + * + * @param {string} view Either 'tile' or 'row' + */ + set_view(view : "tile" | "row") + { + // Restrict to the only 2 accepted values + if(view == 'tile') + { + this.view = 'tile'; + } + else + { + this.view = 'row'; + } + } + + /** + * Set a different / additional handler for dropped files. + * + * File dropping doesn't work with the action system, so we handle it in the + * nextmatch by linking automatically to the target row. This allows an additional handler. + * It should accept a row UID and a File[], and return a boolean Execute the default (link) action + * + * @param {String|Function} handler + */ + set_onfiledrop( handler) + { + this.options.onfiledrop = handler; + } + + /** + * Handle drops of files by linking to the row, if possible. + * + * HTML5 / native file drops conflict with jQueryUI draggable, which handles + * all our drop actions. So we side-step the issue by registering an additional + * drop handler on the rows parent. If the row/actions itself doesn't handle + * the drop, it should bubble and get handled here. + * + * @param {object} event + * @param {object} target + */ + handle_drop( event, target) + { + // Check to see if we can handle the link + // First, find the UID + var row = this.controller.getRowByNode(target); + var uid = row.uid || null; + + // Get the file information + var files = []; + if(event.originalEvent && event.originalEvent.dataTransfer && + event.originalEvent.dataTransfer.files && event.originalEvent.dataTransfer.files.length > 0) + { + files = event.originalEvent.dataTransfer.files; + } + else + { + return false; + } + + // Exectute the custom handler code + if (this.options.onfiledrop && !this.options.onfiledrop.call(this, uid, files)) + { + return false; + } + event.stopPropagation(); + event.preventDefault(); + + if(!row || !row.uid) return false; + + // Link the file to the row + // just use a link widget, it's all already done + var split = uid.split('::'); + var link_value = { + to_app: split.shift(), + to_id: split.join('::') + }; + // Create widget and mangle to our needs + var link = et2_createWidget("link-to", {value: link_value}, this); + link.loadingFinished(); + link.file_upload.set_drop_target(false); + + if(row.row.tr) + { + // Ignore most of the UI, just use the status indicators + var status = jQuery(document.createElement("div")) + .addClass('et2_link_to') + .width(row.row.tr.width()) + .position({my: "left top", at: "left top", of: row.row.tr}) + .append(link.status_span) + .append(link.file_upload.progress) + .appendTo(row.row.tr); + + // Bind to link event so we can remove when done + link.div.on('link.et2_link_to', function(e, linked) { + if(!linked) + { + jQuery("li.success", link.file_upload.progress) + .removeClass('success').addClass('validation_error'); + } + else + { + // Update row + link._parent.refresh(uid,'edit'); + } + // Fade out nicely + status.delay(linked ? 1 : 2000) + .fadeOut(500, function() { + link.free(); + status.remove(); + }); + + }); + } + + // Upload and link - this triggers the upload, which triggers the link, which triggers the cleanup and refresh + link.file_upload.set_value(files); + } + + getDOMNode( _sender?) + { + if (_sender == this || typeof _sender === 'undefined') + { + return this.div[0]; + } + if (_sender == this.header) + { + return this.header.div[0]; + } + for (var i = 0; i < this.columns.length; i++) + { + if (this.columns[i] && this.columns[i].widget && _sender == this.columns[i].widget) + { + return this.dataview.getHeaderContainerNode(i); + } + } + + // Let header have a chance + if(_sender && _sender._parent && _sender._parent == this) + { + return this.header.getDOMNode(_sender); + } + + return null; + } + + // Input widget + + /** + * Get the current 'value' for the nextmatch + */ + getValue( ) + { + var _ids = this.getSelection(); + + // Translate the internal uids back to server uids + var idsArr = _ids.ids; + for (var i = 0; i < idsArr.length; i++) + { + idsArr[i] = idsArr[i].split("::").pop(); + } + var value = { + "selected": idsArr + }; + jQuery.extend(value, this.activeFilters, this.value); + return value; + } + resetDirty( ) + {} + isDirty() { return typeof this.value !== 'undefined';} + isValid( ) { return true;} + set_value(_value) + { + this.value = _value; + } + + // Printing + /** + * Prepare for printing + * + * We check for un-loaded rows, and ask the user what they want to do about them. + * If they want to print them all, we ask the server and print when they're loaded. + */ + beforePrint( ) + { + // Add the class, if needed + this.div.addClass('print'); + + // Trigger resize, so we can fit on a page + this.dynheight.outerNode.css('max-width',this.div.css('max-width')); + this.resize(); + // Reset height to auto (after width resize) so there's no restrictions + this.dynheight.innerNode.css('height', 'auto'); + + // Check for rows that aren't loaded yet, or lots of rows + var range = this.controller._grid.getIndexRange(); + this.print.old_height = this.controller._grid._scrollHeight; + var loaded_count = range.bottom - range.top +1; + var total = this.controller._grid.getTotalCount(); + + // Defer the printing to ask about columns & rows + var defer = jQuery.Deferred(); + + + var pref = this.options.settings.columnselection_pref; + if(pref.indexOf('nextmatch') == 0) + { + pref = 'nextmatch-'+pref; + } + var app = this.getInstanceManager().app; + + var columns = {}; + var columnMgr = this.dataview.getColumnMgr(); + pref += '_print'; + var columns_selected = []; + + // Get column names + for (var i = 0; i < columnMgr.columns.length; i++) + { + var col = columnMgr.columns[i]; + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + + if(col.caption && col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_ALWAYS_NOSELECT && + col.visibility !== et2_dataview_grid.ET2_COL_VISIBILITY_DISABLED) + { + columns[colName] = col.caption; + if(col.visibility === et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE) columns_selected.push(colName); + } + // Custom fields get listed separately + if(widget.instanceOf(et2_nextmatch_customfields)) + { + delete(columns[colName]); + colName = widget.id; + if(col.visibility === et2_dataview_grid.ET2_COL_VISIBILITY_VISIBLE && ! + jQuery.isEmptyObject((widget).customfields) + ) + { + columns[colName] = col.caption; + for(var field_name in (widget).customfields) + { + columns[et2_nextmatch_customfields.prefix+field_name] = " - "+(widget).customfields[field_name].label; + if(widget.options.fields[field_name] && columns_selected.indexOf(colName) >= 0) + { + columns_selected.push(et2_nextmatch_customfields.prefix+field_name); + } + } + } + } + } + + // Preference exists? Set it now + if(this.egw().preference(pref,app)) + { + this.set_columns(jQuery.extend([],this.egw().preference(pref,app))); + } + + var callback = jQuery.proxy(function(button, value) { + if(button === et2_dialog.CANCEL_BUTTON) { + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function() {defer.reject();}, 0); + return; + } + + // Set CSS for orientation + this.div.addClass(value.orientation); + this.egw().set_preference(app,pref+'_orientation',value.orientation); + + + // Try to tell browser about orientation + var css = '@page { size: '+ value.orientation + '; }', + head = document.head || document.getElementsByTagName('head')[0], + style = document.createElement('style'); + + style.type = 'text/css'; + style.media = 'print'; + + // @ts-ignore + if (style.styleSheet){ + // @ts-ignore + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + + head.appendChild(style); + this.print.orientation_style = style; + + // Trigger resize, so we can fit on a page + this.dynheight.outerNode.css('max-width',this.div.css('max-width')); + + // Handle columns + this.set_columns(value.columns); + this.egw().set_preference(app,pref,value.columns); + + var rows = parseInt(value.row_count); + if(rows > total) + { + rows = total; + } + + // If they want the whole thing, style it as all + if(button === et2_dialog.OK_BUTTON && rows == this.controller._grid.getTotalCount()) + { + // Add the class, gives more reliable sizing + this.div.addClass('print'); + // Show it all + jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); + } + // We need more rows + if(button === 'dialog[all]' || rows > loaded_count) + { + var count = 0; + var fetchedCount = 0; + var cancel = false; + var nm = this; + var dialog = et2_dialog.show_dialog( + // Abort the long task if they canceled the data load + function() {count = total; cancel=true;window.setTimeout(function() {defer.reject();},0);}, + egw.lang('Loading'), egw.lang('please wait...'),{},[ + {"button_id": et2_dialog.CANCEL_BUTTON,"text": 'cancel',id: 'dialog[cancel]',image: 'cancel'} + ] + ); + + // dataFetch() is asyncronous, so all these requests just get fired off... + // 200 rows chosen arbitrarily to reduce requests. + do { + var ctx = { + "self": this.controller, + "start": count, + "count": Math.min(rows,200), + "lastModification": this.controller._lastModification + }; + if(nm.controller.dataStorePrefix) + { + // @ts-ignore + ctx.prefix = nm.controller.dataStorePrefix; + } + nm.controller.dataFetch({start:count, num_rows: Math.min(rows,200)}, function(data) { + // Keep track + if(data && data.order) + { + fetchedCount += data.order.length; + } + nm.controller._fetchCallback.apply(this, arguments); + + if(fetchedCount >= rows) + { + if(cancel) + { + dialog.destroy(); + defer.reject(); + return; + } + // Use CSS to hide all but the requested rows + // Prevents us from showing more than requested, if actual height was less than average + nm.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+rows+"))"; + egw.css(nm.print_row_selector, 'display: none'); + + // No scrollbar in print view + jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','hidden'); + // Show it all + jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); + + // Grid needs to redraw before it can be printed, so wait + window.setTimeout(jQuery.proxy(function() { + dialog.destroy(); + + // Should be OK to print now + defer.resolve(); + },nm),et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT); + + } + + },ctx); + count += 200; + } while (count < rows); + nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (rows+1)); + } + else + { + // Don't need more rows, limit to requested and finish + + // Show it all + jQuery('.egwGridView_scrollarea',this.div).css('height','auto'); + + // Use CSS to hide all but the requested rows + // Prevents us from showing more than requested, if actual height was less than average + this.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+rows+"))"; + egw.css(this.print_row_selector, 'display: none'); + + // No scrollbar in print view + jQuery('.egwGridView_scrollarea',this.div).css('overflow-y','hidden'); + // Give dialog a chance to close, or it will be in the print + window.setTimeout(function() {defer.resolve();}, 0); + } + },this); + var value = { + content: { + row_count: Math.min(100,total), + columns: this.egw().preference(pref,app) || columns_selected, + orientation: this.egw().preference(pref+'_orientation',app) + }, + sel_options: { + columns: columns + } + }; + this._create_print_dialog.call(this, value, callback); + + return defer; + } + + /** + * Create and show the print dialog, which calls the provided callback when + * done. Broken out for overriding if needed. + * + * @param {Object} value Current settings and preferences, passed to the dialog for + * the template + * @param {Object} value.content + * @param {Object} value.sel_options + * + * @param {function(int, Object)} callback - Process the dialog response, + * format things according to the specified orientation and fetch any needed + * rows. + * + */ + _create_print_dialog(value, callback) + { + var base_url = this.getInstanceManager().template_base_url; + if (base_url.substr(base_url.length - 1) == '/') base_url = base_url.slice (0, -1); // otherwise we generate a url //api/templates, which is wrong + var tab = this.get_tab_info(); + // Get title for print dialog from settings or tab, if available + var title = this.options.settings.label ? this.options.settings.label : (tab ? tab.label : ''); + var dialog = et2_createWidget("dialog",{ + // If you use a template, the second parameter will be the value of the template, as if it were submitted. + callback: callback, // return false to prevent dialog closing + buttons: et2_dialog.BUTTONS_OK_CANCEL, + title: this.egw().lang('Print') + ' ' + this.egw().lang(title), + template:this.egw().link(base_url+'/api/templates/default/nm_print_dialog.xet'), + value: value + }); + } + + /** + * Try to clean up the mess we made getting ready for printing + * in beforePrint() + */ + afterPrint( ) + { + + this.div.removeClass('print landscape portrait'); + jQuery(this.print.orientation_style).remove(); + delete this.print.orientation_style; + + // Put scrollbar back + jQuery('.egwGridView_scrollarea',this.div).css('overflow-y',''); + + // Correct size of grid, and trigger resize to fix it + this.controller._grid.setScrollHeight(this.print.old_height); + delete this.print.old_height; + + // Remove CSS rule hiding extra rows + if(this.print.row_selector) + { + egw.css(this.print.row_selector, false); + delete this.print.row_selector; + } + + // Restore columns + var pref = []; + var app = this.getInstanceManager().app; + if(this.options.settings.columnselection_pref.indexOf('nextmatch') == 0) + { + pref = egw.preference(this.options.settings.columnselection_pref, app); + } + else + { + // 'nextmatch-' prefix is there in preference name, but not in setting, so add it in + pref = egw.preference("nextmatch-"+this.options.settings.columnselection_pref, app); + } + if(pref) + { + if(typeof pref === 'string') pref = (pref).split(','); + this.set_columns(pref,app); + } + this.dynheight.outerNode.css('max-width','inherit'); + this.resize(); + } +} +et2_register_widget(et2_nextmatch, ["nextmatch"]); + +/** + * Standard nextmatch header bar, containing filters, search, record count, letter filters, etc. + * + * Unable to use an existing template for this because parent (nm) doesn't, and template widget doesn't + * actually load templates from the server. + * @augments et2_DOMWidget + */ +class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INextmatchHeader +{ + static readonly _attributes: { + "filter_label": { + "name": "Filter label", + "type": "string", + "description": "Label for filter", + "default": "", + "translate": true + }, + "filter_help": { + "name": "Filter help", + "type": "string", + "description": "Help message for filter", + "default": "", + "translate": true + }, + "filter": { + "name": "Filter value", + "type": "any", + "description": "Current value for filter", + "default": "" + }, + "no_filter": { + "name": "No filter", + "type": "boolean", + "description": "Remove filter", + "default": false + } + }; + headers: {id: string}[] | et2_widget[]; + et2_searchbox: et2_inputWidget; + private favorites: et2_DOMWidget; // Actually favorite + + private nextmatch: et2_nextmatch; + div: JQuery; + private update_in_progress: boolean; + + header_div: JQuery; + private header_row: JQuery; + private filter_div: JQuery; + private row_div: JQuery; + private fav_span: JQuery; + private toggle_header: JQuery; + lettersearch: JQuery; + + private delete_action: JQuery; + private action_header: JQuery; + + private search_box: JQuery; + private category: any; + private filter: et2_selectbox; + private filter2: et2_selectbox; + private right_div: JQuery; + private count: JQuery; + private count_total: JQuery; + + /** + * Constructor + * + * @param nextmatch + * @param nm_div + * @memberOf et2_nextmatch_header_bar + */ + constructor(_parent : et2_nextmatch, _attrs? : WidgetConfig, _child? : object) { + super(_parent, [_parent,_parent.options.settings], ClassWithAttributes.extendAttributes(et2_nextmatch_header_bar._attributes, _child || {})); + this.nextmatch = _parent; + this.div = jQuery(document.createElement("div")) + .addClass("nextmatch_header"); + this._createHeader(); + + // Flag to avoid loops while updating filters + this.update_in_progress = false; + } + + destroy( ) + { + this.nextmatch = null; + + super.destroy(); + this.div = null; + } + + setNextmatch( nextmatch) + { + var create_once = (this.nextmatch == null); + this.nextmatch = nextmatch; + if(create_once) + { + this._createHeader(); + } + + // Bind row count + this.nextmatch.dataview.grid.setInvalidateCallback(function () { + this.count_total.text(this.nextmatch.dataview.grid.getTotalCount() + ""); + }, this); + } + + /** + * Actions are handled by the controller, so ignore these + * + * @param {object} actions + */ + set_actions( actions : object[]) + {} + + _createHeader( ) + { + + let button; + var self = this; + var nm_div = this.nextmatch.getDOMNode(); + var settings = this.nextmatch.options.settings; + + this.div.prependTo(nm_div); + + // Left & Right (& row) headers + this.headers = [ + {id:this.nextmatch.options.header_left}, + {id:this.nextmatch.options.header_right}, + {id:this.nextmatch.options.header_row} + ]; + + // The rest of the header + this.header_div = this.row_div = jQuery(document.createElement("div")) + .addClass("nextmatch_header_row") + .appendTo(this.div); + this.filter_div = jQuery(document.createElement("div")) + .addClass('filtersContainer') + .appendTo(this.row_div); + + // Search + this.search_box = jQuery(document.createElement("div")) + .addClass('search') + .prependTo(egwIsMobile()?this.nextmatch.getDOMNode():this.row_div); + + // searchbox widget options + var searchbox_options = { + id:"search", + overlay:(typeof settings.searchbox != 'undefined' && typeof settings.searchbox.overlay != 'undefined')?settings.searchbox.overlay:false, + onchange:function(){ + self.nextmatch.applyFilters({search: this.get_value()}); + }, + value:settings.search, + fix:!egwIsMobile() + }; + // searchbox widget + this.et2_searchbox = et2_createWidget('searchbox', searchbox_options,this); + + // Set activeFilters to current value + this.nextmatch.activeFilters.search = settings.search; + + this.et2_searchbox.set_value(settings.search); + /** + * Mobile theme specific part for nm header + * nm header has very different behaivior for mobile theme and basically + * it has its own markup separately from nm header in normal templates. + */ + if (egwIsMobile()) + { + this.search_box.addClass('nm-mob-header'); + jQuery(this.div).css({display:'inline-block'}).addClass('nm_header_hide'); + + //indicates appname in header + jQuery(document.createElement('div')) + .addClass('nm_appname_header') + .text(egw.lang(egw.app_name())) + .appendTo(this.search_box); + + this.delete_action = jQuery(document.createElement('div')) + .addClass('nm_delete_action') + .prependTo(this.search_box); + // toggle header + // add new button + this.fav_span = jQuery(document.createElement('div')) + .addClass('nm_favorites_div') + .prependTo(this.search_box); + // toggle header menu + this.toggle_header = jQuery(document.createElement('button')) + .addClass('nm_toggle_header') + .click(function(){ + jQuery(self.div).toggleClass('nm_header_hide'); + jQuery(this).toggleClass('nm_toggle_header_on'); + window.setTimeout(function(){self.nextmatch.resize();},800); + }) + .prependTo(this.search_box); + // Context menu + this.action_header = jQuery(document.createElement('button')) + .addClass('nm_action_header') + .hide() + .click (function(e){ + // @ts-ignore + jQuery('tr.selected',self.nextmatch.getDOMNode()).trigger({type:'contextmenu',which:3,originalEvent:e}); + }) + .prependTo(this.search_box); + } + + // Add category + if(!settings.no_cat) { + if (typeof settings.cat_id_label == 'undefined') settings.cat_id_label = ''; + this.category = this._build_select('cat_id', settings.cat_is_select ? + 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { + multiple: false, + tags: true, + class: "select-cat", + value_class: settings.cat_id_class + }); + } + + // Filter 1 + if(!settings.no_filter) { + this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); + } + + // Filter 2 + if(!settings.no_filter2) { + this.filter2 = this._build_select('filter2', 'select', settings.filter2, + settings.filter2_no_lang, { + multiple: false, + tags: settings.filter2_tags, + class: "select-cat", + value_class: settings.filter2_class + }); + } + + // Other stuff + this.right_div = jQuery(document.createElement("div")) + .addClass('header_row_right').appendTo(this.row_div); + + // Record count + this.count = jQuery(document.createElement("span")) + .addClass("header_count ui-corner-all"); + + // Need to figure out how to update this as grid scrolls + // this.count.append("? - ? ").append(egw.lang("of")).append(" "); + this.count_total = jQuery(document.createElement("span")) + .appendTo(this.count) + .text(settings.total + ""); + this.count.prependTo(this.right_div); + + // Favorites + this._setup_favorites(settings['favorites']); + + // Export + if(typeof settings.csv_fields != "undefined" && settings.csv_fields != false) + { + var definition = settings.csv_fields; + if(settings.csv_fields === true) + { + definition = egw.preference('nextmatch-export-definition', this.nextmatch.egw().getAppName()); + } + let button = et2_createWidget("buttononly", {id: "export", "statustext": "Export", image: "download", "background_image": true}, this); + jQuery(button.getDOMNode()) + .click(this.nextmatch, function(event) { + // @ts-ignore + egw_openWindowCentered2( egw.link('/index.php', { + 'menuaction': 'importexport.importexport_export_ui.export_dialog', + 'appname': event.data.egw().getAppName(), + 'definition': definition + }), '_blank', 850, 440, 'yes'); + }); + } + + // Another place to customize nextmatch + this.header_row = jQuery(document.createElement("div")) + .addClass('header_row').appendTo(this.right_div); + + // Letter search + var current_letter = this.nextmatch.options.settings.searchletter ? + this.nextmatch.options.settings.searchletter : + (this.nextmatch.activeFilters ? this.nextmatch.activeFilters.searchletter : false); + if(this.nextmatch.options.settings.lettersearch || current_letter) + { + this.lettersearch = jQuery(document.createElement("table")) + .addClass('nextmatch_lettersearch') + .css("width", "100%") + .appendTo(this.div); + var tbody = jQuery(document.createElement("tbody")).appendTo(this.lettersearch); + var row = jQuery(document.createElement("tr")).appendTo(tbody); + + // Capitals, A-Z + var letters = this.egw().lang('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split(''); + for(var i in letters) { + button = jQuery(document.createElement("td")) + .addClass("lettersearch") + .appendTo(row) + .attr("id", letters[i]) + .text(letters[i]); + if(letters[i] == current_letter) button.addClass("lettersearch_active"); + } + button = jQuery(document.createElement("td")) + .addClass("lettersearch") + .appendTo(row) + .attr("id", "") + .text(egw.lang("all")); + if(!current_letter) button.addClass("lettersearch_active"); + + this.lettersearch.click(this.nextmatch, function(event) { + // this is the lettersearch table + jQuery("td",this).removeClass("lettersearch_active"); + jQuery(event.target).addClass("lettersearch_active"); + event.data.applyFilters({searchletter: event.target.id || false}); + }); + // Set activeFilters to current value + this.nextmatch.activeFilters.searchletter = current_letter; + } + // Apply letter search preference + var lettersearch_preference = "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-lettersearch"; + if(this.lettersearch && !egw.preference(lettersearch_preference,this.nextmatch.egw().getAppName())) + { + this.lettersearch.hide(); + } + } + + + /** + * Build & bind to a sub-template into the header + * + * @param {string} location One of left, right, or row + * @param {string} template_name Name of the template to load into the location + */ + _build_header( location : "left" | "right" | "row", template_name : string) + { + var id = location == "left" ? 0 : (location == "right" ? 1 : 2); + var existing = this.headers[id]; + // @ts-ignore + if(existing && existing._type) + { + if(existing.id == template_name) return; + (existing).destroy(); + this.headers[id] = null; + } + + // Load the template + var self = this; + var header = et2_createWidget("template", {"id": template_name}, this); + this.headers[id] = header; + var deferred = []; + header.loadingFinished(deferred); + + // Wait until all child widgets are loaded, then bind + jQuery.when.apply(jQuery,deferred).then(function() { + // fix order in DOM by reattaching templates in correct position + switch (id) { + case 0: // header_left: prepend + jQuery(header.getDOMNode()).prependTo(self.header_div); + break; + case 1: // header_right: before favorites and count + jQuery(header.getDOMNode()).prependTo(self.header_div.find('div.header_row_right')); + break; + case 2: // header_row: after search + window.setTimeout(function(){ // otherwise we might end up after filters + jQuery(header.getDOMNode()).insertAfter(self.header_div.find('div.search')); + }, 1); + break; + } + self._bindHeaderInput(header); + }); + } + + /** + * Build the selectbox filters in the header bar + * Sets value, options, labels, and change handlers + * + * @param {string} name + * @param {string} type + * @param {string} value + * @param {string} lang + * @param {object} extra + */ + _build_select( name: string, type: string, value: string, lang: string|boolean, extra?: object) : et2_selectbox + { + var widget_options = jQuery.extend({ + "id": name, + "label": this.nextmatch.options.settings[name+"_label"], + "no_lang": lang, + "disabled": this.nextmatch.options['no_'+name] + }, extra); + + // Set select options + // Check in content for options- + var mgr = this.nextmatch.getArrayMgr("content"); + var options = mgr.getEntry("options-" + name); + // Look in sel_options + if(!options) options = this.nextmatch.getArrayMgr("sel_options").getEntry(name); + // Check parent sel_options, because those are usually global and don't get passed down + if(!options) options = this.nextmatch.getArrayMgr("sel_options").parentMgr.getEntry(name); + // Sometimes legacy stuff puts it in here + if(!options) options = mgr.getEntry('rows[sel_options]['+name+']'); + + // Maybe in a row, and options got stuck in ${row} instead of top level + var row_stuck = ['${row}','{$row}']; + for(var i = 0; !options && i < row_stuck.length; i++) + { + var row_id = ''; + if((!options || options.length == 0) && ( + // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid + this.nextmatch.getArrayMgr("sel_options").perspectiveData.row || this.nextmatch.getArrayMgr("sel_options").data[row_stuck[i]])) + { + var row_id = name.replace(/[0-9]+/,row_stuck[i]); + options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); + if(!options) + { + row_id = row_stuck[i] + "["+name+"]"; + options = this.nextmatch.getArrayMgr("sel_options").getEntry(row_id); + } + } + if(options) + { + this.egw().debug('warn', 'Nextmatch filter options in a weird place - "%s". Should be in sel_options[%s].',row_id,name); + } + } + // Legacy: Add in 'All' option for cat_id, if not provided. + if(name == 'cat_id' && options != null && (typeof options[''] == 'undefined' && typeof options[0] != 'undefined' && options[0].value != '')) + { + widget_options.empty_label = this.egw().lang('All categories'); + } + + // Create widget + var select = et2_createWidget(type, widget_options, this); + + if(options) select.set_select_options(options); + + // Set value + select.set_value(value); + + // Set activeFilters to current value + this.nextmatch.activeFilters[select.id] = select.get_value(); + + // Set onChange + var input = select.input; + + // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() + select.attributes.select_options.ignore = true; + + if (this.nextmatch.options.settings[name+"_onchange"]) + { + // Get the onchange function string + var onchange = this.nextmatch.options.settings[name+"_onchange"]; + + // Real submits cause all sorts of problems + if(onchange.match(/this\.form\.submit/)) + { + this.egw().debug("warn","%s tries to submit form, which is not allowed. Filter changes automatically refresh data with no reload.",name); + onchange = onchange.replace(/this\.form\.submit\([^)]*\);?/,'return true;'); + } + + // Connect it to the onchange event of the input element - may submit + select.change = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()); + this._bindHeaderInput(select); + } + else // default request changed rows with new filters, previous this.form.submit() + { + input.change(this.nextmatch, function(event) { + var set = {}; + set[name] = select.getValue(); + event.data.applyFilters(set); + }); + } + return select; + } + + /** + * Set up the favorites UI control + * + * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of + * additional fields/settings to add in to the favorite. + */ + _setup_favorites( filters) + { + if(typeof filters == "undefined" || filters === false) + { + // No favorites configured + return; + } + + var list = et2_csvSplit(this.options.get_rows, 2, "."); + var widget_options = { + default_pref: "nextmatch-" + this.nextmatch.options.settings.columnselection_pref + "-favorite", + app: list[0], + filters: filters, + sidebox_target:'favorite_sidebox_'+list[0] + }; + this.favorites = et2_createWidget('favorites', widget_options, this); + + // Add into header + jQuery(this.favorites.getDOMNode(this.favorites)).prependTo(egwIsMobile()?this.search_box.find('.nm_favorites_div').show():this.right_div); + } + + /** + * Updates all the filter elements in the header + * + * Does not actually refresh the data, just sets values to match those given. + * Called by et2_nextmatch.applyFilters(). + * + * @param filters Array Key => Value pairs of current filters + */ + setFilters( filters) + { + + // Avoid loops cause by change events + if(this.update_in_progress) return; + this.update_in_progress = true; + + // Use an array mgr to hande non-simple IDs + var mgr = new et2_arrayMgr(filters); + + this.iterateOver(function(child) { + // Skip favorites, don't want them in the filter + if(typeof child.id != "undefined" && child.id.indexOf("favorite") == 0) return; + + let value : string | object = ''; + if(typeof child.set_value != "undefined" && child.id) + { + value = mgr.getEntry(child.id); + if (value == null) value = ''; + /** + * Sometimes a filter value is not in current options. This can + * happen in a saved favorite, for example, or if server changes + * some filter options, and the order doesn't work out. The normal behaviour + * is to warn & not set it, but for nextmatch we'll just add it + * in, and let the server either set it properly, or ignore. + */ + if(value && typeof value != 'object' && child.instanceOf(et2_selectbox)) + { + var found = typeof child.options.select_options[value] != 'undefined'; + // options is array of objects with attribute value&label + if (jQuery.isArray(child.options.select_options)) + { + for(var o=0; o < child.options.select_options.length; ++o) + { + if (child.options.select_options[o].value == value) + { + found = true; + break; + } + } + } + if (!found) + { + var old_options = child.options.select_options; + // Actual label is not available, obviously, or it would be there + old_options[value] = child.egw().lang("Loading"); + child.set_select_options(old_options); + } + } + child.set_value(value); + } + if(typeof child.get_value == "function" && child.id) + { + // Put data in the proper place + var target = this; + value = child.get_value(); + + // Split up indexes + var indexes = child.id.replace(/[/g,'[').split('['); + + for(var i = 0; i < indexes.length; i++) + { + indexes[i] = indexes[i].replace(/]/g,'').replace(']',''); + if (i < indexes.length-1) + { + if(typeof target[indexes[i]] == "undefined") target[indexes[i]] = {}; + target = target[indexes[i]]; + } + else + { + target[indexes[i]] = value; + } + } + } + }, filters); + + // Letter search + if(this.nextmatch.options.settings.lettersearch) + { + jQuery("td",this.lettersearch).removeClass("lettersearch_active"); + jQuery(filters.searchletter ? "td#"+filters.searchletter : "td.lettersearch[id='']",this.lettersearch).addClass("lettersearch_active"); + + // Set activeFilters to current value + filters.searchletter = jQuery("td.lettersearch_active",this.lettersearch).attr("id") || false; + } + + // Reset flag + this.update_in_progress = false; + } + + /** + * Help out nextmatch / widget stuff by checking to see if sender is part of header + * + * @param {et2_widget} _sender + */ + getDOMNode( _sender) + { + var filters = [this.category, this.filter, this.filter2]; + for(var i = 0; i < filters.length; i++) + { + if(_sender == filters[i]) + { + // Give them the filter div + return this.filter_div[0]; + } + } + if(_sender == this.et2_searchbox) return this.search_box[0]; + if(_sender.id == 'export') return this.right_div[0]; + + if(_sender && _sender._type == "template") + { + for(var i = 0; i < this.headers.length; i++) + { + if(_sender.id == this.headers[i].id && _sender._parent == this) return i == 2 ? this.header_row[0] : this.header_div[0]; + } + } + return null; + } + + /** + * Bind all the inputs in the header sub-templates to update the filters + * on change, and update current filter with the inputs' current values + * + * @param {et2_template} sub_header + */ + _bindHeaderInput( sub_header) + { + var header = this; + + var bind_change = function(_widget){ + // Previously set change function + var widget_change = _widget.change; + + var change = function(_node) { + // Call previously set change function + var result = widget_change.call(_widget,_node); + + // Update filters, if we're not already doing so + if((result || typeof result === 'undefined') && _widget.isDirty() && !header.update_in_progress) { + // Update dirty + _widget._oldValue = _widget.getValue(); + + // Widget will not have an entry in getValues() because nulls + // are not returned, we remove it from activeFilters + if(_widget._oldValue == null) + { + var path = _widget.getArrayMgr('content').explodeKey(_widget.id); + if(path.length > 0) + { + var entry = header.nextmatch.activeFilters; + var i = 0; + for(; i < path.length-1; i++) + { + entry = entry[path[i]]; + } + delete entry[path[i]]; + } + header.nextmatch.applyFilters(header.nextmatch.activeFilters); + } + else + { + // Not null is easy, just get values + var value = this.getInstanceManager().getValues(sub_header); + header.nextmatch.applyFilters(value[header.nextmatch.id]); + } + } + // In case this gets bound twice, it's important to return + return true; + }; + + _widget.change = change; + + // Set activeFilters to current value + // Use an array mgr to hande non-simple IDs + var value = {}; + value[_widget.id] = _widget._oldValue = _widget.getValue(); + var mgr = new et2_arrayMgr(value); + jQuery.extend(true, this.nextmatch.activeFilters,mgr.data); + }; + if(sub_header.instanceOf(et2_inputWidget)) + { + bind_change.call(this, sub_header); + } + else + { + sub_header.iterateOver(bind_change, this, et2_inputWidget); + } + } +} +et2_register_widget(et2_nextmatch_header_bar, ["nextmatch_header_bar"]); + +/** + * Classes for the nextmatch sortheaders etc. + * + * @augments et2_baseWidget + */ +export class et2_nextmatch_header extends et2_baseWidget implements et2_INextmatchHeader +{ + static readonly _attributes: { + "label": { + "name": "Caption", + "type": "string", + "description": "Caption for the nextmatch header", + "translate": true + } + }; + protected labelNode: JQuery; + protected nextmatch: et2_nextmatch; + private label: string; + + /** + * Constructor + * + * @memberOf et2_nextmatch_header + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_header._attributes, _child || {})); + + + this.labelNode = jQuery(document.createElement("span")); + this.nextmatch = null; + + this.setDOMNode(this.labelNode[0]); + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + } + + set_label( _value) + { + this.label = _value; + + this.labelNode.text(_value); + + // add class if label is empty + this.labelNode.toggleClass('et2_label_empty', !_value); + } +} +et2_register_widget(et2_nextmatch_header, ['nextmatch-header']); + +/** + * Extend header to process customfields + * + * @augments et2_customfields_list + */ +export class et2_nextmatch_customfields extends et2_customfields_list implements et2_INextmatchHeader +{ + static readonly _attributes: { + 'customfields': { + 'name': 'Custom fields', + 'description': 'Auto filled' + }, + 'fields': { + 'name': "Visible fields", + "description": "Auto filled" + } + }; + private nextmatch: et2_nextmatch; + + /** + * Constructor + * + * @memberOf et2_nextmatch_customfields + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfields._attributes, _child || {})); + + // Specifically take the whole column + this.table.css("width", "100%"); + } + + destroy( ) + { + this.nextmatch = null; + super.destroy(); + } + + transformAttributes( _attrs) + { + super.transformAttributes(_attrs); + + // Add in settings that are objects + if(!_attrs.customfields) + { + // Check for custom stuff (unlikely) + var data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true); + for(var key in data) + { + if(typeof data[key] === 'object' && ! _attrs[key]) _attrs[key] = data[key]; + } + } + } + + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + this.loadFields(); + } + + /** + * Build widgets for header - sortable for numeric, text, etc., filterables for selectbox, radio + */ + loadFields( ) + { + if(this.nextmatch == null) + { + // not ready yet + return; + } + var columnMgr = this.nextmatch.dataview.getColumnMgr(); + var nm_column = null; + var set_fields = {}; + for(var i = 0; i < this.nextmatch.columns.length; i++) + { + if(this.nextmatch.columns[i].widget == this) + { + nm_column = columnMgr.columns[i]; + break; + } + } + if(!nm_column) return; + + // Check for global setting changes (visibility) + var global_data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~'); + if(global_data != null && global_data.fields) this.options.fields = global_data.fields; + + var apps = egw.link_app_list(); + for(var field_name in this.options.customfields) + { + var field = this.options.customfields[field_name]; + var cf_id = et2_customfields_list.prefix + field_name; + + + if(this.rows[field_name]) continue; + + // Table row + var row = jQuery(document.createElement("tr")) + .appendTo(this.tbody); + var cf = jQuery(document.createElement("td")) + .appendTo(row); + this.rows[cf_id] = cf[0]; + + // Create widget by type + var widget = null; + if(field.type == 'select' || field.type == 'select-account') + { + if(field.values && typeof field.values[''] !== 'undefined') + { + delete(field.values['']); + } + widget = et2_createWidget( + field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", + { + id: cf_id, + empty_label: field.label, + select_options: field.values + }, + this + ); + } + else if (apps[field.type]) + { + widget = et2_createWidget("nextmatch-entryheader", { + id: cf_id, + only_app: field.type, + blur: field.label + }, this); + } + else + { + widget = et2_createWidget("nextmatch-sortheader", { + id: cf_id, + label: field.label + }, this); + } + + // If this is already attached, widget needs to be finished explicitly + if(this.isAttached() && !widget.isAttached()) + { + widget.loadingFinished(); + } + // Check for column filter + if(!jQuery.isEmptyObject(this.options.fields) && ( + this.options.fields[field_name] == false || typeof this.options.fields[field_name] == 'undefined')) + { + cf.hide(); + } + else if (jQuery.isEmptyObject(this.options.fields)) + { + // If we're showing it make sure it's set, but only after + set_fields[field_name] = true; + } + } + jQuery.extend(this.options.fields, set_fields); + } + + /** + * Override parent so we can update the nextmatch row too + * + * @param {array} _fields + */ + set_visible( _fields) + { + super.set_visible(_fields); + + // Find data row, and do it too + var self = this; + if(this.nextmatch) + { + this.nextmatch.iterateOver( + function(widget) { + if(widget == self) return; + widget.set_visible(_fields); + }, this, et2_customfields_list + ); + } + } + + /** + * Provide own column caption (column selection) + * + * If only one custom field, just use that, otherwise use "custom fields" + */ + _genColumnCaption( ) + { + return egw.lang("Custom fields"); + } + + /** + * Provide own column naming, including only selected columns - only useful + * to nextmatch itself, not for sending server-side + */ + _getColumnName( ) + { + var name = this.id; + var visible = []; + for(var field_name in this.options.customfields) + { + if(jQuery.isEmptyObject(this.options.fields) || this.options.fields[field_name] == true) + { + visible.push(et2_customfields_list.prefix + field_name); + jQuery(this.rows[field_name]).show(); + } + else if (typeof this.rows[field_name] != "undefined") + { + jQuery(this.rows[field_name]).hide(); + } + } + + if(visible.length) { + name +="_"+ visible.join("_"); + } + else + { + // None hidden means all visible + jQuery(this.rows[field_name]).parent().parent().children().show(); + } + + // Update global custom fields column(s) - widgets will check on their own + + // Check for custom stuff (unlikely) + var data = this.getArrayMgr("modifications").getEntry(this.id); + // Check for global settings + if(!data) data = this.getArrayMgr("modifications").getRoot().getEntry('~custom_fields~', true) || {}; + if(!data.fields) data.fields = {}; + for(var field in this.options.customfields) + { + data.fields[field] = (this.options.fields == null || typeof this.options.fields[field] == 'undefined' ? false : this.options.fields[field]); + } + return name; + } +} +et2_register_widget(et2_nextmatch_customfields, ['nextmatch-customfields']); + +/** + * @augments et2_nextmatch_header + */ +// @ts-ignore +export class et2_nextmatch_sortheader extends et2_nextmatch_header implements et2_INextmatchSortable +{ + static readonly _attributes: { + "sortmode": { + "name": "Sort order", + "type": "string", + "description": "Default sort order", + "translate": false + } + }; + legacyOptions: ['sortmode']; + private sortmode: string; + + /** + * Constructor + * + * @memberOf et2_nextmatch_sortheader + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_sortheader._attributes, _child || {})); + + this.sortmode = "none"; + + this.labelNode.addClass("nextmatch_sortheader none"); + } + + click( _event ) + { + if (this.nextmatch && super.click( _event )) + { + // Send default sort mode if not sorted, otherwise send undefined to calculate + this.nextmatch.sortBy(this.id, this.sortmode == "none" ? !(this.options.sortmode.toUpperCase() == "DESC") : undefined); + return true; + } + + return false; + } + + /** + * Wrapper to join up interface * framework + * + * @param {string} _mode + */ + set_sortmode(_mode) + { + // Set via nextmatch after setup + if(this.nextmatch) return; + + this.setSortmode(_mode); + } + + /** + * Function which implements the et2_INextmatchSortable function. + * + * @param {string} _mode + */ + setSortmode( _mode) + { + // Remove the last sortmode class and add the new one + this.labelNode.removeClass(this.sortmode) + .addClass(_mode); + + this.sortmode = _mode; + } + +} +et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); + +/** + * @augments et2_selectbox + */ +export class et2_nextmatch_filterheader extends et2_selectbox implements et2_INextmatchHeader, et2_IResizeable +{ + private nextmatch: et2_nextmatch; + + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_filterheader + */ + createInputWidget( ) + { + // Make sure there's an option for all + if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) + { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + super.createInputWidget(); + + jQuery(this.getInputNode()).change(this, function(event) { + if(typeof event.data.nextmatch == 'undefined') + { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[event.data.id] = event.data.input.val(); + // Set value so it's there for response (otherwise it gets cleared if options are updated) + event.data.set_value(event.data.input.val()); + + event.data.nextmatch.applyFilters({col_filter: col_filter}); + }); + + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") + { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + + // Make sure it's set in the nextmatch + _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + } + + // Make sure selectbox is not longer than the column + resize( ) + { + this.input.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); + } + +} +et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); + +/** + * @augments et2_selectAccount + */ +class et2_nextmatch_accountfilterheader extends et2_selectAccount implements et2_INextmatchHeader, et2_IResizeable +{ + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_accountfilterheader + */ + createInputWidget( ) + { + // Make sure there's an option for all + if(!this.options.empty_label && !this.options.select_options[""]) + { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + this._super.apply(this, arguments); + + this.input.change(this, function(event) { + if(typeof event.data.nextmatch == 'undefined') + { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[event.data.id] = event.data.getValue(); + event.data.nextmatch.applyFilters({col_filter: col_filter}); + }); + + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) + { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + } + } + // Make sure selectbox is not longer than the column + resize( ) + { + var max = jQuery(this.parentNode).innerWidth() - 4; + var surroundings = this.getSurroundings()._widgetSurroundings; + for(var i = 0; i < surroundings.length; i++) + { + max -= jQuery(surroundings[i]).outerWidth(); + } + this.input.css("max-width",max + "px"); + } + +} +et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); + +/** + * Filter allowing multiple values to be selected, base on a taglist instead + * of a regular selectbox + * + * @augments et2_taglist + */ +class et2_nextmatch_taglistheader extends et2_taglist implements et2_INextmatchHeader, et2_IResizeable +{ + static readonly _attributes = { + autocomplete_url: { default: ''}, + multiple: { default: 'toggle'}, + onchange: { + // @ts-ignore + default: function(event) { + if(typeof this.nextmatch === 'undefined') + { + // Not fully set up yet + return; + } + var col_filter = {}; + col_filter[this.id] = this.getValue(); + // Set value so it's there for response (otherwise it gets cleared if options are updated) + //event.data.set_value(event.data.input.val()); + + this.nextmatch.applyFilters({col_filter: col_filter}); + } + }, + rows: { default: 2}, + class: {default: 'nm_filterheader_taglist'} + }; + private nextmatch: et2_nextmatch; + + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_filterheader + */ + createInputWidget( ) + { + // Make sure there's an option for all + if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) + { + this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); + } + super.createInputWidget(); + } + + /** + * Disable toggle if there are 2 or less options + * @param {Object[]} options + */ + set_select_options(options) + { + if(options && options.length <= 2 && this.options.multiple == 'toggle') + { + this.set_multiple(false); + } + super.set_select_options(options) + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") + { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + + // Make sure it's set in the nextmatch + _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + } + + // Make sure selectbox is not longer than the column + resize( ) + { + this.div.css("height",''); + this.div.css("max-width",jQuery(this.parentNode).innerWidth() + "px"); + super.resize(); + } + +} +et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); + +/** + * @augments et2_link_entry + */ +class et2_nextmatch_entryheader extends et2_link_entry implements et2_INextmatchHeader +{ + /** + * Override to add change handler + * + * @memberOf et2_nextmatch_entryheader + * @param {object} event + * @param {object} selected + */ + onchange( event, selected) + { + var col_filter = {}; + col_filter[this.id] = this.get_value(); + this.nextmatch.applyFilters.call(this.nextmatch, {col_filter: col_filter}); + } + + /** + * Override to always return a string appname:id (or just id) for simple (one real selection) + * cases, parent returns an object. If multiple are selected, or anything other than app and + * id, the original parent value is returned. + */ + getValue( ) + { + var value = super.getValue(); + if(typeof value == "object" && value != null) + { + if(!value.app || !value.id) return null; + + // If array with just one value, use a string instead for legacy server handling + if(typeof value.id == 'object' && value.id.shift && value.id.length == 1) + { + value.id = value.id.shift(); + } + // If simple value, format it legacy string style, otherwise + // we return full value + if(typeof value.id == 'string') + { + value = value.app +":"+value.id; + } + } + return value; + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch( _nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id]) + { + this.set_value(this.nextmatch.options.settings.col_filter[this.id]); + + if(this.getValue() != this.nextmatch.activeFilters.col_filter[this.id]) + { + this.nextmatch.activeFilters.col_filter[this.id] = this.getValue(); + } + + // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() + this.attributes.value.ignore = true; + //this.attributes.select_options.ignore = true; + } + var self = this; + // Fire on lost focus, clear filter if user emptied box + } +} +et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); + +/** + * @augments et2_nextmatch_filterheader + */ +class et2_nextmatch_customfilter extends et2_nextmatch_filterheader +{ + static readonly _attributes: { + "widget_type": { + "name": "Actual type", + "type": "string", + "description": "The actual type of widget you should use", + "no_lang": 1 + }, + "widget_options": { + "name": "Actual options", + "type": "any", + "description": "The options for the actual widget", + "no_lang": 1, + "default": {} + } + }; + legacyOptions: ["widget_type","widget_options"]; + + real_node: et2_selectbox; + + /** + * Constructor + * + * @param _parent + * @param _attrs + * @memberOf et2_nextmatch_customfilter + */ + constructor(_parent?, _attrs? : WidgetConfig, _child? : object) + { + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); + + switch(_attrs.widget_type) + { + case "link-entry": + _attrs.type = 'nextmatch-entryheader'; + break; + default: + if(_attrs.widget_type.indexOf('select') === 0) + { + _attrs.type = 'nextmatch-filterheader'; + } + else + { + _attrs.type = _attrs.widget_type; + } + } + jQuery.extend(_attrs.widget_options,{id: this.id}); + + _attrs.id = ''; + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); + + this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this.getParent()); + const select_options = []; + const correct_type = _attrs.type; + this.real_node['type'] = _attrs.widget_type; + et2_selectbox.find_select_options(this.real_node, select_options, _attrs); + this.real_node["_type"] = correct_type; + if(typeof this.real_node.set_select_options === 'function') + { + this.real_node.set_select_options(select_options); + } + } + + // Just pass the real DOM node through, in case anybody asks + getDOMNode( _sender) + { + return this.real_node ? this.real_node.getDOMNode(_sender) : null; + } + + // Also need to pass through real children + getChildren( ) + { + return this.real_node.getChildren() || []; + } + setNextmatch(_nextmatch : et2_nextmatch) + { + if(this.real_node && this.real_node.instanceOf(et2_INextmatchHeader)) + { + return (this.real_node).setNextmatch(_nextmatch); + } + } +} +et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index ad66ffbd44..f0fe1837b8 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -8,7 +8,9 @@ declare class et2_DOMWidget extends et2_widget{} declare class et2_baseWidget extends et2_DOMWidget{} declare class et2_valueWidget extends et2_baseWidget{} declare class et2_inputWidget extends et2_valueWidget{ - getInputNode() : HTMLElement + getInputNode() : HTMLElement; + public set_value(value: string | object | number); + public getValue() : any; } declare class et2_tabbox extends et2_valueWidget { tabData : any; @@ -49,7 +51,13 @@ declare var et2_dataview_row : any; declare var et2_dataview_rowProvider : any; declare var et2_dataview_spacer : any; declare var et2_dataview_tile : any; -declare var et2_customfields_list : any; +declare class et2_customfields_list extends et2_valueWidget { + constructor(_parent: any, _attrs: WidgetConfig, object: object); + + public static readonly prefix : string; + public customfields : any; + set_visible(visible : boolean); +} declare var et2_INextmatchHeader : any; declare var et2_INextmatchSortable : any; declare var et2_nextmatch : any; @@ -116,12 +124,16 @@ declare var et2_selectAccount_ro : any; declare class et2_selectbox extends et2_inputWidget { protected options : any; public createInputWidget(); + public set_multiple(boolean); + public set_select_options(options: any); } declare var et2_selectbox_ro : any; declare var et2_menulist : any; declare var et2_split : any; declare var et2_styles : any; -declare class et2_taglist extends et2_selectbox {} +declare class et2_taglist extends et2_selectbox { + protected div : JQuery; +} declare var et2_taglist_account : any; declare var et2_taglist_email : any; declare var et2_taglist_category : any;