//==============================================================================
//
// 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