//============================================================================== // // Purpose: Builds a table, client-side. // // The public surface is a set of data types (MWClientTable, // MWColumn, MWCustomCellSpec, MWCellLinkDetail) instantiated by // callers. All of the actual behavior - rendering, sorting, // filtering and DOM linking - lives in the MWTable module // defined at the bottom of this file. The data types delegate to // it. This keeps the behavior in a single, testable module while // leaving the public constructor API (used across the codebase) // unchanged. // //============================================================================== /*global htmlEncode */ // MWTable is the module defined near the bottom of this file. The public data // types above delegate down to it, so their references to MWTable are // legitimately before its definition. /* eslint-disable no-use-before-define */ //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ function MWCellLinkDetail(options_) { var self = this; self.simpleLink = options_.simpleLink; self.doubleQuoteReadyJS = options_.doubleQuoteReadyJS; self.openInOtherWindow = !!options_.openInOtherWindow; self.dialogLink = !!options_.dialogLink; } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ // eslint-disable-next-line no-unused-vars function mwBuildSimpleLink(url_, openInOtherWindow_) { return new MWCellLinkDetail({ simpleLink: url_, openInOtherWindow: openInOtherWindow_ }); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ // eslint-disable-next-line no-unused-vars function mwBuildLinkScript(doubleQuoteReadyJS_, openInOtherWindow_) { return new MWCellLinkDetail({ doubleQuoteReadyJS: doubleQuoteReadyJS_, openInOtherWindow: openInOtherWindow_ }); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ // eslint-disable-next-line no-unused-vars function MWCustomCellSpec(options_) { if (null === options_ || typeof options_ === 'string') { options_ = { rawValue: options_ || '' }; } else { options_ = options_ || {}; } var self = this, colSpan = options_.colSpan || 1, rowSpan = options_.rowSpan || 1, strValueHTML = options_.valueHTML || htmlEncode(options_.rawValue || ''), cellId = (options_.cellId || '') + ''; self.getColSpan = function () { return colSpan; }; self.getRowSpan = function () { return rowSpan; }; self.getValueHTML = function () { return strValueHTML; }; self.getCellId = function () { return cellId; }; } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ var g_nextGeneratedColLinkIdNum = 1; function MWColumn(options_) { options_ = options_ || {}; var self = this, sortInfo = options_.sortInfo; self.columnName = options_.columnName; self.simpleDisplayColumnPropertyName = options_.simpleDisplayColumnPropertyName; // Name of the property to grab from the array being rendered. self.simpleDisplayColumnNeedsMultilineEncoding = options_.simpleDisplayColumnNeedsMultilineEncoding; // If set for a simple display column, it will be htmlMultilineEncoded. self.simpleDisplayColumnIsAlreadyEncoded = options_.simpleDisplayColumnIsAlreadyEncoded; // If not set for a simple display column, it will be htmlEncoded. self.colLinkId = 'mwColLinkId.' + g_nextGeneratedColLinkIdNum++; // Function provided for custom rendering. Takes the following form: // // fnCustomRenderCellContents( // objRow_, // objColumn_, // array_, // rowIndex_) self.fnCustomRenderCellContents = options_.fnCustomRenderCellContents; self.customRenderCellParams = options_.customRenderCellParams; self.additionalCellClasses = options_.additionalCellClasses || ''; self.additionalHeaderClasses = options_.additionalHeaderClasses || ''; // If the sortInfo property includes a fnCustomSort property, and the // MWClientTable object has been linked to dom elements, the function will // be invoked as follows: // // sortInfo.fnCustomSort( // objMWColumn_, // The MWColumn object // ascending_, // Indicates whether the // // sort should be ascending // columnHeaderCell_) // Column header DOM element // // Otherwise, sorting relies on a data attribute associated with the row, // and a specific property on that object. By default, the attribute used // has the name 'data-rowData' and the property used will have the same // name as that given by the "simpleDisplayColumnPropertyName" option. // // If these defaults are appropriate for the sort, it's only necessary to // assign an empty object as the "sortInfo" property: // // { // ... // sortInfo: {}, // ... // } // // These defaults can be overridden by setting the "rowDataAttrName" // and "rowDataAttrPropertyName" properties on the sortInfo. // if (sortInfo) { self.sortInfo = sortInfo; sortInfo.rowDataAttrName = sortInfo.rowDataAttrName || 'data-rowData'; sortInfo.rowDataAttrPropertyName = sortInfo.rowDataAttrPropertyName || options_.simpleDisplayColumnPropertyName; } self.fnAdditionalCellClasses = options_.fnAdditionalCellClasses; // // When set, called to retrieve classes to add to the cell. Usage: // // fnAdditionalCellClasses( // objRow_, // objColumn_, // array_, // idxRow_) // // returns: // "classes to add to the cell" // self.fnBuildCellLinkDetail = options_.fnBuildCellLinkDetail; // // When set, used to build a link/click handler for the cells // of this column. Usage: // // fnBuildCellLinkDetail( // objRow_, // objColumn_, // array_, // rowIndex_) // // returns: // An instance of MWCellLinkDetail // (or nothing if no linking/scripting for the given cell) // // Sub-columns are used to support grouping headers. self.arrSubColumns = options_.arrSubColumns || []; } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ function MWClientTable(options_) { options_ = options_ || {}; var self = this; self.noDataMessage = options_.noDataMessage || 'No Data'; self.arrColumns = options_.arrColumns || []; self.tableId = options_.tableId || ''; self.tableScrollName = options_.tableScrollName || ''; self.additionalTableClasses = options_.additionalTableClasses || ''; self.suppressReportTableClass = options_.suppressReportTableClass; self.suppressTableHeader = options_.suppressTableHeader; self.fnAdditionalRowClasses = options_.fnAdditionalRowClasses; self.cssClassPrefix = options_.cssClassPrefix; self.tableHeaderClass = options_.tableHeaderClass; self.suppressTableHeaderRowClass = options_.suppressTableHeaderRowClass; self.showTableIfNoData = options_.showTableIfNoData || false; self.dataSource = options_.dataSource; self.paging = options_.paging; // // paging (server-side): { pageSize: N, mode: 'append', chunkSeparator: bool } // (chunkSeparator optional). When set alongside dataSource, the table fetches // one page at a time. fnBuildPayload receives the request state (see below). // Append consumers should supply fnExtractTotalCount so the row count is a // stable server-side total; absent it, the count falls back to the rows // returned in the response. // // - mode: 'append' ("load more"): loadInto()/showFirstPage() renders the // first page plus a "(Rows 1 to X of N)" + Load more control; loadMore() // fetches the next page and appends its rows to the ones already shown // (nothing is discarded), so earlier pages' DOM - including any // checkbox/field state - stays live. Use this for a large result set the // user selects across. // // When set, the table can fetch its own rows from the server via // loadInto()/reload() instead of having them serialized into the page. // // Every dataSource callback receives a single params_ object. All of them // carry the live "table" and the "dataSource" config; each also carries its // own data (json / error / command+payload+etc.). // // dataSource: { // command: 'Module_FunctionName', // remote-scripting command // fnBuildPayload: function (params_) {...}, // { table, dataSource } // // -> request payload, re-run each load // fnExtractArray: function (params_) {...}, // { table, dataSource, json } // // -> row array // doingWhat: 'Loading...', // optional status text // loadingHtml: '
...
', // optional placeholder // errorMessage: 'Could not load.', // optional; shown in a // // full-width row if the load fails // fnBuildErrorMessage: function (params_) {...}, // optional; // // { table, dataSource, error } // // -> error-row text // fnTransport: function (params_) {...} // optional; params_ is // // { table, dataSource, command, // // payload, doingWhat, fnOnSuccess, // // fnOnError }. Defaults to the // // jsrsExecuteWithErrorP adapter. // // Call params_.fnOnSuccess(json_) // // to succeed or // // params_.fnOnError(error_) to fail. // // Call fnOnError_(error_) to fail. // fnOnRowsRendered: function (targetElement_) {...} // optional; called // // after every render AND every // // append, to (re-)wire per-row // // behavior not baked into the // // markup (e.g. handlers on rows // // added by a later loadMore()). // fnGetRowKey: function (row_) {...} // optional; append paging // // only. Returns a stable // // unique key for a row. When // // set, a row whose key was // // already rendered is dropped // // from a later appended page // // instead of shown twice (the // // result set can shift between // // fetches). Omit to keep every // // fetched row. // } // // // When set, called to retrieve classes to add to the row. Usage: // // fnAdditionalRowClasses( // objRow_, // array_, // idxRow_, // cssClassPrefix_) // // returns: // "classes to add to the row" // self.fnOverrideGetRowClass = options_.fnOverrideGetRowClass; // // When set, called to retrieve the primary class to use for the row. Usage: // // fnOverrideGetRowClass( // objRow_, // array_, // idxRow_, // cssClassPrefix_) // // returns: // "primary class(es) to use for the row" // // When not set, "oddRow" and "evenRow" will be used // (with the table's "cssClassPrefix" prepended, if appropriate). // self.fnGetCheckDetailsForRow = options_.fnGetCheckDetailsForRow; // // When set, called to retrieve details about how to handle a given row's checkbox. Usage: // // fnGetCheckDetailsForRow( // objRow_, // array_, // idxRow_) // // returns: // { // value: "string-value-to-assign-cb", // checked: true if the cb should be checked. // } // self.flagCBsToFireOnChange = options_.flagCBsToFireOnChange; // When set, if the table includes a checkbox column, those checkboxes will // be configured to have the "fireOnChange_" parameter set when either of // "checkAllRows()" or "checkSingleRow()" is called. self.fnAdditionalRowAttrs = options_.fnAdditionalRowAttrs; // // When set, called to retrieve a string of additional attribute definitions. // // fnAdditionalRowAttrs( // objRow_, // array_, // idxRow_) // // returns: "data-attr1='' data-attr2='' ..." // } //============================================================================== // MWColumn - thin delegators to MWTable. //============================================================================== //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWColumn.prototype.addSubColumn = function (objColumn_) { this.arrSubColumns.push(objColumn_); return objColumn_; }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWColumn.prototype.getFlattenedColumnCount = function () { return MWTable.getFlattenedColumnCount(this); }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWColumn.prototype.flattenToEndOfArrayOfLeafColumns = function (optionalLeafColumnArray_) { return MWTable.flattenColumnToLeaves(this, optionalLeafColumnArray_); }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWColumn.prototype.getColumnName = function () { return this.columnName; }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWColumn.prototype.getCustomRenderCellParams = function () { return this.customRenderCellParams; }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWColumn.prototype.buildCellContents = function (array_, rowIndex_) { return MWTable.buildCellContents(this, array_, rowIndex_); }; //============================================================================== // MWClientTable - thin delegators to MWTable. //============================================================================== //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWClientTable.prototype.getTableId = function() { return this.tableId; }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWClientTable.prototype.flattenToEndOfArrayOfLeafColumns = function () { return MWTable.flattenColumnsToLeaves(this.arrColumns); }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWClientTable.prototype.addColumn = function (objColumn_) { this.arrColumns.push(objColumn_); return objColumn_; }; //------------------------------------------------------------------------------ // // Removes the column. If found and removed, the column is returned. // If the column is included multiple times, it will be removed in each case. // //------------------------------------------------------------------------------ MWClientTable.prototype.removeColumn = function (objColumn_) { var arrColumns = this.arrColumns, i, rc; for (i = 0; i < arrColumns.length; ++i) { if (objColumn_ === arrColumns[i]) { arrColumns.splice( i, // start 1); // deleteCount rc = objColumn_; } } return rc; }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWClientTable.prototype.buildArrayOfHeaderRowColumnObjects = function () { return MWTable.buildArrayOfHeaderRowColumnObjects(this); }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWClientTable.prototype.getCheckboxFunction = function () { return this.fnGetCheckDetailsForRow; }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWClientTable.prototype.setCheckboxFunction = function (fnGetCheckDetailsForRow_) { this.fnGetCheckDetailsForRow = fnGetCheckDetailsForRow_; }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWClientTable.prototype.buildTableForArray = function (array_) { return MWTable.buildTableForArray(this, array_); }; //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ MWClientTable.prototype.linkTableObjectsToElement = function (objTableAncestorElement_) { return MWTable.linkTableObjectsToElement(this, objTableAncestorElement_); }; //------------------------------------------------------------------------------ // Fetches rows from the server (per the table's dataSource) and renders them // into objTargetElement_, remembering it for later reload() calls. //------------------------------------------------------------------------------ MWClientTable.prototype.loadInto = function (objTargetElement_) { return MWTable.loadTableData(this, objTargetElement_); }; //------------------------------------------------------------------------------ // Re-fetches into the element last passed to loadInto(). Throws if loadInto() // has not been called. //------------------------------------------------------------------------------ MWClientTable.prototype.reload = function () { return MWTable.reloadTableData(this); }; //------------------------------------------------------------------------------ // Append-paging navigation: loads the next page and appends its rows to those // already shown. A no-op once every row is loaded. //------------------------------------------------------------------------------ MWClientTable.prototype.loadMore = function () { return MWTable.loadMore(this); }; //------------------------------------------------------------------------------ // Seeds an append-paged table with a first page the caller already fetched // (rendering it into objTargetElement_ and wiring up loadMore()), instead of // having the table fetch page 0. tmpTotalCount_ is the full server-side match // count. //------------------------------------------------------------------------------ MWClientTable.prototype.showFirstPage = function (objTargetElement_, array_, tmpTotalCount_) { return MWTable.showFirstPage(this, objTargetElement_, array_, tmpTotalCount_); }; //============================================================================== // Global call-through shims. // // Kept global for the many existing call sites. (Sorting is now handled by the // delegated click listener inside MWTable, so no mwSortClientColumn global is // needed.) //============================================================================== //------------------------------------------------------------------------------ // Filters rows to show if any cell in the row matches the filter text //------------------------------------------------------------------------------ // eslint-disable-next-line no-unused-vars function mwFilterTableRowsToFilterText(table_, filterText_) { return MWTable.filterTableRowsToFilterText(table_, filterText_); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ // eslint-disable-next-line no-unused-vars function mwRemoveTableRows(table_, fnShouldRemoveRow_) { return MWTable.removeTableRows(table_, fnShouldRemoveRow_); } //============================================================================== // // Purpose: Client-side table module. // //============================================================================== /*global cdBuildFontIconElem checkAttribute FontIconId_Enum g_dcForcedIgnoreClass getEventElement getRawFontId HtmlUtilities htmlMultilineEncode jsrsExecuteWithErrorP mjtElemData rsCallbackHandleStandardJSONResponse trim */ //============================================================================== // MWTable - owns all behavior for the public data types (rendering, sorting, // filtering and DOM linking). The data types (MWClientTable, MWColumn, etc.) // delegate to it. //============================================================================== var MWTable = (function () { //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvIsCustomCellSpecObject(object_) { if(object_ && typeof object_ === 'object' && object_.constructor && object_.constructor.name === 'MWCustomCellSpec') { return true; } } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pbGetFlattenedColumnCount(objColumn_) { var arrSubColumns = objColumn_.arrSubColumns, idxSubColumn, rc = 0; if(arrSubColumns.length) { for(idxSubColumn = 0; idxSubColumn < arrSubColumns.length; ++idxSubColumn) { rc += pbGetFlattenedColumnCount(arrSubColumns[idxSubColumn]); } } else { rc = 1; } return rc; } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pbFlattenColumnToLeaves(objColumn_, optionalLeafColumnArray_) { optionalLeafColumnArray_ = optionalLeafColumnArray_ || []; var arrSubColumns = objColumn_.arrSubColumns; if(arrSubColumns.length) { for(var idxSubColumn = 0; idxSubColumn < arrSubColumns.length; ++idxSubColumn) { pbFlattenColumnToLeaves(arrSubColumns[idxSubColumn], optionalLeafColumnArray_); } } else { optionalLeafColumnArray_.push(objColumn_); } return optionalLeafColumnArray_; } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pbFlattenColumnsToLeaves(arrColumnObjects_, optionalLeafColumnArray_) { optionalLeafColumnArray_ = optionalLeafColumnArray_ || []; if(arrColumnObjects_.length) { for(var idxColumn = 0; idxColumn < arrColumnObjects_.length; ++idxColumn) { pbFlattenColumnToLeaves(arrColumnObjects_[idxColumn], optionalLeafColumnArray_); } } return optionalLeafColumnArray_; } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- // rowIndex_ is the position within array_ (used to look the row up). // logicalRowIndex_ is the row's index within the overall result set; it // differs from rowIndex_ only for an appended page (see pbBuildTableForArray's // indexOffset), and is what the custom renderer sees so per-row ids/field // names stay globally unique. Defaults to rowIndex_ when not supplied. function pbBuildCellContents(objColumn_, array_, rowIndex_, logicalRowIndex_) { var objRow = array_[rowIndex_], tmpLogicalRowIndex = logicalRowIndex_ === undefined ? rowIndex_ : logicalRowIndex_, fnCustomRenderCellContents = objColumn_.fnCustomRenderCellContents, rc; if(fnCustomRenderCellContents) { rc = fnCustomRenderCellContents( objRow, objColumn_, array_, tmpLogicalRowIndex); } else { // Simple column... var simpleDisplayColumnPropertyName = objColumn_.simpleDisplayColumnPropertyName, simpleDisplayColumnNeedsMultilineEncoding = objColumn_.simpleDisplayColumnNeedsMultilineEncoding, simpleDisplayColumnIsAlreadyEncoded = objColumn_.simpleDisplayColumnIsAlreadyEncoded, objPropertyValue = objRow ? objRow[simpleDisplayColumnPropertyName] : undefined; if(objPropertyValue !== undefined && objPropertyValue !== null) { rc = objPropertyValue.toString(); if(simpleDisplayColumnNeedsMultilineEncoding) { rc = htmlMultilineEncode(rc); } else if(true === simpleDisplayColumnIsAlreadyEncoded) { // no-op - already encoded } else { rc = htmlEncode(rc); } } else { rc = ''; } } return rc; } //-------------------------------------------------------------------------- // // Given a column object, this recursive function will build out an array // in which each successive element of the array corresponds to an array of // column objects that together define a single row of the overall, // potentially multi-rowed header. // //-------------------------------------------------------------------------- function pvExtendArrayOfHeaderRowColumnObjects(objColumn_, arrHeaderRowColumnObjects_, zeroBasedDepthIndex_) { if(zeroBasedDepthIndex_ > arrHeaderRowColumnObjects_.length - 1) { // We haven't yet added the row we're about to be dealing with, so add it now... arrHeaderRowColumnObjects_.push([]); } var idxSubColumn, arrSubColumns = objColumn_.arrSubColumns, arrCurrLevelColumns = arrHeaderRowColumnObjects_[zeroBasedDepthIndex_]; // Add this column arrCurrLevelColumns.push(objColumn_); // Add any subcolumns for(idxSubColumn = 0; idxSubColumn < arrSubColumns.length; ++idxSubColumn) { pvExtendArrayOfHeaderRowColumnObjects( arrSubColumns[idxSubColumn], // objColumn_ arrHeaderRowColumnObjects_, zeroBasedDepthIndex_ + 1); // 1 row deeper... } } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pbBuildArrayOfHeaderRowColumnObjects(objTable_) { var rc = [], idxColumn, arrColumns = objTable_.arrColumns; for(idxColumn = 0; idxColumn < arrColumns.length; ++idxColumn) { pvExtendArrayOfHeaderRowColumnObjects( arrColumns[idxColumn], // objColumn_ rc, // arrHeaderRowColumnObjects_ 0); // zeroBasedDepthIndex_ } return rc; } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvGetTableCssClass(descendentElem_) { var objParentTable = checkAttribute(descendentElem_, 'data-mwClientTable'), strCssClassPrefix = (objParentTable ? objParentTable.getAttribute('data-cssClassPrefix') : '') || ''; return strCssClassPrefix; } //-------------------------------------------------------------------------- // This signature must match fnOverrideGetRowClass, hence the unused parameters. //-------------------------------------------------------------------------- function pvStandardGetClientTableRowClass(objRow_, array_, idxRow_, cssClassPrefix_) { // As a sanity check, ensure that "cssClassPrefix_" is actually a string. cssClassPrefix_ = ( typeof cssClassPrefix_ !== 'string' ? '' : cssClassPrefix_) || ''; return cssClassPrefix_ + (idxRow_ % 2 ? 'evenRow' : 'oddRow'); } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvApplyOddOrEvenClassToRow(objRow_, idxRow_, cssClassPrefix_) { if(objRow_.classList) { // As a sanity check, ensure that "cssClassPrefix_" is actually a string. cssClassPrefix_ = (typeof cssClassPrefix_ !== 'string' ? '' : cssClassPrefix_) || ''; // First, get rid of any exiting odd/even classes objRow_.classList.remove(cssClassPrefix_ + 'evenRow'); objRow_.classList.remove(cssClassPrefix_ + 'oddRow'); // Now add the correct one back objRow_.classList.add( pvStandardGetClientTableRowClass( null, null, idxRow_, cssClassPrefix_)); } } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pvBuildSortAddendum(sortDir_) { var rc = ''; if(sortDir_) { rc = cdBuildFontIconElem({ fontId: sortDir_ === 1 ? FontIconId_Enum.fiiArrowSortUp : FontIconId_Enum.fiiArrowSortDown, terseFontIcon: 1, additionalFontIconClasses: 'mwSortIcon paddingLeftXSmall' }); } return rc; } //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- function pbSortClientColumn(columnHeaderCell_, sortInfo_, cssClassPrefix_) { var strRowDataAttrName = sortInfo_.rowDataAttrName, strRowDataAttrPropertyName = sortInfo_.rowDataAttrPropertyName, strRowDataValue, objRowData, idxVisible, objAncestor = columnHeaderCell_, strSortDir = columnHeaderCell_.getAttribute('data-mwSortDir'), tmpSortDir = strSortDir === '1' ? 1 : strSortDir === '-1' ? -1 : 0, tmpNewSortDir = tmpSortDir === 1 ? -1 : 1, strSortAddendum = pvBuildSortAddendum(tmpNewSortDir), objTable, trs, tr, idxRow, arrRowInfos = [], objColumn = mjtElemData(columnHeaderCell_, 'data-mwColLinkObj'), objSortInfo = objColumn ? objColumn.sortInfo || sortInfo_ : sortInfo_, fnCustomSort = objSortInfo ? objSortInfo.fnCustomSort : 0; while(objAncestor && !objTable) { if(objAncestor.nodeName === 'TABLE') { objTable = objAncestor; } else { objAncestor = objAncestor.parentNode; } } if(fnCustomSort) { fnCustomSort( objColumn, tmpNewSortDir === 1, // ascending_ columnHeaderCell_); } else { // Clear away the sort icon... objTable.querySelectorAll('.mwSortIcon').forEach( function (object_) { object_.parentNode.removeChild(object_); }); // ...and "sortSelected" indicator... objTable.querySelectorAll('.sortSelected').forEach( function (object_) { object_.className = object_.className.replace('sortSelected', ''); }); columnHeaderCell_.parentNode.querySelectorAll('[data-mwSortDir]').forEach( function (object_) { object_.setAttribute('data-mwSortDir', 0); }); columnHeaderCell_.setAttribute('data-mwSortDir', tmpNewSortDir); columnHeaderCell_.innerHTML = columnHeaderCell_.innerHTML + strSortAddendum; columnHeaderCell_.className = trim(columnHeaderCell_.className) + ' sortSelected'; trs = objTable.rows; for(idxRow = trs.length - 1; idxRow >= 0; --idxRow) { tr = trs[idxRow]; strRowDataValue = tr.getAttribute(strRowDataAttrName); if(strRowDataValue) { objRowData = JSON.parse(unescape(strRowDataValue)); arrRowInfos.push({ rowData: objRowData, idxRow: idxRow, tr: tr, parentNode: tr.parentNode }); tr.parentNode.removeChild(tr); } } arrRowInfos.sort( function (o1_, o2_) { var rd1 = o1_.rowData, p1 = rd1[strRowDataAttrPropertyName], rd2 = o2_.rowData, p2 = rd2[strRowDataAttrPropertyName], rc; if(p1 === p2) { // Since the values compare equally, maintain the original // sort order by now comparing the original row indices. if(o1_.idxRow < o2_.idxRow) { rc = -1; } else if(o1_.idxRow > o2_.idxRow) { rc = 1; } else { rc = 0; } // Need to multiply by the new sort order (1/-1) so that if // we're sorting descending, the sort order maintenance will // hold. rc = tmpNewSortDir * rc; } else if(p1 === null || p1 === undefined) { rc = -1; } else if(p2 === null || p2 === undefined) { rc = 1; } else { if(sortInfo_.isNumber) { var f1 = parseFloat(p1), f2 = parseFloat(p2); rc = f1 === f2 ? 0 : f1 < f2 ? -1 : 1; } else { rc = p1.toString().toLowerCase().localeCompare(p2.toString().toLowerCase()); } if(rc === 0) { // The values compare equal (e.g. they differ only by // case, or are numerically equal), so maintain the // original sort order by comparing the original row // indices - the engine's sort is not stable. Multiply // by the new sort order (1/-1) so the descending // negation below cancels out and equal rows keep their // original order in both directions. rc = tmpNewSortDir * (o1_.idxRow - o2_.idxRow); } } return tmpNewSortDir === 1 ? rc : -rc; }); idxVisible = 0; arrRowInfos.forEach(function (objRowInfo_) { objRowInfo_.parentNode.appendChild(objRowInfo_.tr); if(objRowInfo_.tr.style.display !== 'none') { pvApplyOddOrEvenClassToRow(objRowInfo_.tr, idxVisible, cssClassPrefix_); ++idxVisible; } }); } } //-------------------------------------------------------------------------- // Builds the header rows of the view-model: one array of header cells per // header level (grouped headers produce more than one). Each cell carries // only its rendered content for now; spans/classes are added as the // serializer needs them. //-------------------------------------------------------------------------- function pvBuildHeaderRowsViewModel(objTable_, arrHeaderRowColumnObjects_) { var tmpHeaderRowCount = arrHeaderRowColumnObjects_.length, strCssClassPrefix = objTable_.cssClassPrefix || '', tmpSuppressHeaderRowClass = objTable_.suppressTableHeaderRowClass, arrRC = [], idxHeaderRow, arrColumnsOfRow, arrCells, idxCol, objColumn, objSortInfo, tmpInitialSortDir, tmpFlattenedColumnCount, tmpIsLastHeaderRow, strHeaderClass, strAdditionalHeaderClasses; for(idxHeaderRow = 0; idxHeaderRow < tmpHeaderRowCount; ++idxHeaderRow) { arrColumnsOfRow = arrHeaderRowColumnObjects_[idxHeaderRow]; tmpIsLastHeaderRow = idxHeaderRow + 1 >= tmpHeaderRowCount; arrCells = []; for(idxCol = 0; idxCol < arrColumnsOfRow.length; ++idxCol) { objColumn = arrColumnsOfRow[idxCol]; objSortInfo = objColumn.sortInfo; tmpInitialSortDir = objSortInfo ? objSortInfo.initialSortDir : 0; tmpFlattenedColumnCount = pbGetFlattenedColumnCount(objColumn); // Suppressing the header-row class zeroes the base class but // still keeps any per-column additional header classes. strHeaderClass = tmpSuppressHeaderRowClass ? '' : strCssClassPrefix + (objSortInfo ? 'headerSortableCol' : 'headerCol'); strAdditionalHeaderClasses = objColumn.additionalHeaderClasses ? ' ' + objColumn.additionalHeaderClasses : ''; arrCells.push({ contentHtml: objColumn.getColumnName() + pvBuildSortAddendum(tmpInitialSortDir), // A parent header spans its leaf columns; a standalone leaf // on a non-final header row instead spans down to the last // header row. colSpan: tmpFlattenedColumnCount, rowSpan: tmpFlattenedColumnCount === 1 && !tmpIsLastHeaderRow ? tmpHeaderRowCount - idxHeaderRow : 1, classes: strHeaderClass + strAdditionalHeaderClasses, colLinkId: objColumn.colLinkId, sortInfo: objSortInfo, sortDir: tmpInitialSortDir }); } arrRC.push(arrCells); } return arrRC; } //-------------------------------------------------------------------------- // Describes a row's leading checkbox cell, or undefined when the table has // no checkbox column at all. Returns null when the column exists but this // row supplies no details (an empty cell). The id deliberately mirrors the // string builder's format (tableScrollName + 'ChkRow' + tableId + index) so // per-row ids stay stable, including for appended pages via the logical // index. //-------------------------------------------------------------------------- function pvBuildRowCheckboxViewModel( objTable_, objRow_, array_, tmpLogicalRowIndex_) { var fnGetCheckDetailsForRow = objTable_.fnGetCheckDetailsForRow, objCheckDetails; if(!fnGetCheckDetailsForRow) { return undefined; } objCheckDetails = fnGetCheckDetailsForRow(objRow_, array_, tmpLogicalRowIndex_); if(!objCheckDetails) { return null; } return { name: 'chkRow' + objTable_.tableId, id: objTable_.tableScrollName + 'ChkRow' + objTable_.tableId + tmpLogicalRowIndex_, value: objCheckDetails.value, disabled: !!objCheckDetails.disabled, checked: !!objCheckDetails.checked, additionalAttributes: objCheckDetails.additionalAttributes || '', onClickJs: 'checkSingleRow(event,this,\'' + objTable_.tableId + '\'' + (objTable_.flagCBsToFireOnChange ? ',undefined,undefined,true' : '') + ')' }; } //-------------------------------------------------------------------------- // Describes a single body cell: its rendered content plus any additional // classes (static additionalCellClasses combined with per-row // fnAdditionalCellClasses). Content reuses pbBuildCellContents so custom // renderers and simple-column encoding behave exactly as in the string // builder. //-------------------------------------------------------------------------- function pvBuildBodyCellViewModel( objColumn_, array_, idxRow_, tmpLogicalRowIndex_) { var objRow = array_[idxRow_], strAdditionalCellClasses = objColumn_.additionalCellClasses || '', objCellLinkDetail, strSimpleLink, doubleQuoteReadyJS, tmpIsDialogLink, strOnClickJs = '', strLinkHref = '', objCellContents, tmpIsCustomCellSpecObject, strContentHtml, tmpColSpan, tmpRowSpan, strCellId; if(objColumn_.fnAdditionalCellClasses) { strAdditionalCellClasses = trim( strAdditionalCellClasses + ' ' + objColumn_.fnAdditionalCellClasses( objRow, objColumn_, array_, tmpLogicalRowIndex_)); } // A column may attach a link/click behaviour to each cell. When it // yields a simpleLink or a script, the cell becomes a linkedCell and // gets an onclick; a dialog link additionally wraps its content in an // (the serializer does that wrapping). if(objColumn_.fnBuildCellLinkDetail) { objCellLinkDetail = objColumn_.fnBuildCellLinkDetail( objRow, objColumn_, array_, tmpLogicalRowIndex_); } if(objCellLinkDetail) { tmpIsDialogLink = objCellLinkDetail.dialogLink; strSimpleLink = objCellLinkDetail.simpleLink; doubleQuoteReadyJS = objCellLinkDetail.doubleQuoteReadyJS; if(strSimpleLink || doubleQuoteReadyJS) { strAdditionalCellClasses = trim(strAdditionalCellClasses + ' linkedCell'); if(strSimpleLink) { if(objCellLinkDetail.openInOtherWindow) { strOnClickJs = 'window.open(\'' + strSimpleLink + '\',\'_blank\');'; } else if(tmpIsDialogLink) { strOnClickJs = 'clickedDialogLink(\'' + strSimpleLink + '\');'; } else { strOnClickJs = 'window.location=\'' + strSimpleLink + '\';'; } } else { strOnClickJs = doubleQuoteReadyJS; } } if(strSimpleLink && tmpIsDialogLink) { strLinkHref = strSimpleLink; } } // A cell's contents are either a plain HTML string or an // MWCustomCellSpec carrying its own span/id. Normalise to a string // plus the span/id fields the serializer needs. objCellContents = pbBuildCellContents( objColumn_, array_, idxRow_, tmpLogicalRowIndex_); tmpIsCustomCellSpecObject = pvIsCustomCellSpecObject(objCellContents); strContentHtml = tmpIsCustomCellSpecObject ? objCellContents.getValueHTML() : objCellContents; tmpColSpan = tmpIsCustomCellSpecObject ? objCellContents.getColSpan() : 1; tmpRowSpan = tmpIsCustomCellSpecObject ? objCellContents.getRowSpan() : 1; strCellId = tmpIsCustomCellSpecObject ? objCellContents.getCellId() : ''; return { contentHtml: strContentHtml, classes: strAdditionalCellClasses, onClickJs: strOnClickJs, linkHref: strLinkHref, colSpan: tmpColSpan, rowSpan: tmpRowSpan, cellId: strCellId }; } //-------------------------------------------------------------------------- // Builds the body rows of the view-model: one row per array element, each // with an optional leading checkbox and a cell per leaf column carrying that // cell's rendered content. The logical row index (offset for appended pages) // is what per-row ids/field names key off of. //-------------------------------------------------------------------------- function pvBuildBodyRowsViewModel(objTable_, array_, objBuildOptions_) { var arrLeafColumns = pbFlattenColumnsToLeaves(objTable_.arrColumns), tmpIndexOffset = objBuildOptions_.indexOffset || 0, strCssClassPrefix = objTable_.cssClassPrefix || '', fnGetRowClass = objTable_.fnOverrideGetRowClass || pvStandardGetClientTableRowClass, fnAdditionalRowClasses = objTable_.fnAdditionalRowClasses, fnAdditionalRowAttrs = objTable_.fnAdditionalRowAttrs, arrRC = [], idxRow, objRow, arrCells, idxCol, objColumn, objCell, objCheckbox, strCheckboxId, tmpColSpan, tmpRowSpan, iSpan, tmpLogicalRowIndex, strFnAdditionalRowClasses, strAdditionalRowClasses, strAdditionalRowAttrs, // Tracks, per leaf column, how many further rows are still covered // by a rowspan started above - those positions emit no cell. arrRemainingRowSpans = []; if(!array_) { return arrRC; } for(idxCol = 0; idxCol < arrLeafColumns.length; ++idxCol) { arrRemainingRowSpans[idxCol] = 0; } for(idxRow = 0; idxRow < array_.length; ++idxRow) { objRow = array_[idxRow]; tmpLogicalRowIndex = idxRow + tmpIndexOffset; strAdditionalRowClasses = ''; if(fnAdditionalRowClasses) { strFnAdditionalRowClasses = fnAdditionalRowClasses(objRow, array_, tmpLogicalRowIndex); if(strFnAdditionalRowClasses) { strAdditionalRowClasses = ' ' + strFnAdditionalRowClasses; } } strAdditionalRowAttrs = ''; if(fnAdditionalRowAttrs) { strAdditionalRowAttrs = ' ' + fnAdditionalRowAttrs(objRow, array_, tmpLogicalRowIndex); } // A row's leading checkbox (if any); its id also labels the // first column's cell via a wrapping