Grid.js 81 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2016, 2019
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. define(['../../../lib/@waca/core-client/js/core-client/ui/core/View', '../../../lib/@waca/core-client/js/core-client/utils/UniqueId', '../../../lib/@waca/core-client/js/core-client/utils/BrowserUtils', 'underscore', 'jquery', './GridDataProvider', '../../../util/KeyCodes'], function (BaseView, UniqueId, BrowserUtils, _, $, DataProvider, KeyCodes) {
  8. 'use strict';
  9. var cbGetScrolledFastPinnedCornerCellIdx = function cbGetScrolledFastPinnedCornerCellIdx(tdIndex, fixedColumns, fixedRows) {
  10. //Corner cells positioning in rows are static (never change in tables). When fast horizontal scrolling had
  11. //taken place and when there is need to resize or update after data change, do not move the corner cells to a another location
  12. //within a row
  13. return fixedColumns && fixedRows && tdIndex < fixedColumns ? tdIndex : null;
  14. };
  15. var ATTRIBUTES = {
  16. COLUMN_INDEX: 'col',
  17. ROW_INDEX: 'row'
  18. };
  19. var View = null;
  20. /**
  21. * Class representing a grid.
  22. * @extends {lib/@waca/core-client/js/core-client/ui/core/View}
  23. */
  24. View = BaseView.extend({
  25. events: {
  26. 'keydown': 'onKeyDown'
  27. },
  28. /**
  29. * @constructs
  30. * Initialize the class. Takes in an options object that gives some configuration details to the grid class. The options object's
  31. * parameters are listed below:
  32. *
  33. * @param {object} options: An options object
  34. * @param {array} dataProvider: the data provider object
  35. * @param {boolean} isGroupEnabled:
  36. * @param {number} minCellWidth: minimum width of each grid cell when empty text in pixels
  37. * @param {number} minCellHeight: minimum height of each grid cell when empty text in pixels
  38. * @param {number} height: 100,
  39. * @param {function} formatCell: an optional function that takes 4 parameters and will be called per grid cell/DOM node to format that cell.
  40. * Function declaration should be something like: function formatCell(td, value, rowIndex, colIndex) where
  41. * @param [object] td: cell node
  42. * @param [string] value: the value of the cell
  43. * @param [number] rowIndex: number representing the row index. 0-indexed
  44. * @param [ number] colIndex: number representing the column index. 0-indexed
  45. * @param {function} getMoreData: optional function that will be called if we scroll towards the bottom of the grid and there are less than or
  46. * equal to 5 rows left to render. Useful if there is additional data to be fetched from the server that should be rendered.
  47. * @param {function} onStartScroll:
  48. * @param {function} onFinishScroll:
  49. * @param {object} el:
  50. * @param {number} fixedRows: indicates how many rows starting from row 0 to pin to the grid (eg. 1 to pin the headers row)
  51. * @param {number} fixedColumns: indicates how many columns starting from col 0 to pin to the grid (eg. 1 to pin the headers column)
  52. */
  53. init: function init() {
  54. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  55. View.inherited('init', this, arguments);
  56. this.dataProvider = options.dataProvider || new DataProvider(options.data);
  57. this.offsetCount = 1;
  58. //Right now, only autoResizeHorizontal is used - but want to keep the
  59. //autoResizeVertical logic in case it is used in the future
  60. this.autoResizeHorizontal = options.autoResizeHorizontal || options.autoResize;
  61. this.autoResizeVertical = options.autoResizeVertical || options.autoResize;
  62. this.isRendered = false;
  63. // Minimum width and height allowed for the cell
  64. // when we have an empty text. Large text will push
  65. // the size of the cell.
  66. this.minCellWidth = options.minCellWidth || 150;
  67. this.minCellHeight = options.minCellHeight || 30;
  68. // Used to enable grouping of cells.
  69. this.isGroupEnabled = options.isGroupEnabled || false;
  70. this.options = options;
  71. this.translateX = 0;
  72. this.translateY = 0;
  73. this.verticalScrollConfig = {
  74. isVerticalScrolling: true,
  75. minSizeProp: 'min-height',
  76. defaultSize: function () {
  77. return this.minCellHeight;
  78. }.bind(this),
  79. sizeFunction: function sizeFunction($node) {
  80. return $node.outerHeight();
  81. },
  82. paddingProp: 'padding-top',
  83. translate: function (value) {
  84. this.translateX = value;
  85. this.$virtualTable.css('padding-top', value + 'px');
  86. this.$rowHeadersTable.css('margin-top', value - this.$scrollableArea.scrollTop() + 'px');
  87. }.bind(this),
  88. getTranslateValue: function () {
  89. return this.translateX;
  90. }.bind(this),
  91. scrolledRowIdx: 0, //The row (TR Row) that was scrolled to,
  92. renderedCount: 0,
  93. scrollFunction: 'scrollTop',
  94. isOutOfView: function (offset) {
  95. if (offset > 0) {
  96. return this.isOutOfViewFromTop(this.$firstRow());
  97. } else {
  98. return this.isOutOfViewFromBottom(this.$lastRow());
  99. }
  100. }.bind(this)
  101. };
  102. if (BrowserUtils.isIE11()) {
  103. //Work around a bug where the jQuery size functions can return incorrect values for these nodes in IE11 using jQuery 3.3.1.
  104. this.verticalScrollConfig.sizeFunction = function ($node) {
  105. return $node[0].offsetHeight;
  106. };
  107. }
  108. this.verticalScrollConfig.getScrolledIdx = function () {
  109. return this.scrolledRowIdx;
  110. }.bind(this.verticalScrollConfig);
  111. this.verticalScrollConfig.setScrolledIdx = function (value) {
  112. this.scrolledRowIdx = value;
  113. }.bind(this.verticalScrollConfig);
  114. this.verticalScrollConfig.recalculateRenderingIndexes = function () {
  115. if (this.count - this.scrolledRowIdx < this.renderedCount) {
  116. this.scrolledRowIdx = Math.max(0, this.count - this.renderedCount);
  117. }
  118. }.bind(this.verticalScrollConfig);
  119. this.createCellStat(this.verticalScrollConfig);
  120. this.horizontalScrollConfig = {
  121. isVerticalScrolling: false,
  122. minSizeProp: 'min-width',
  123. sizeFunction: function sizeFunction($node) {
  124. return $node.outerWidth();
  125. },
  126. defaultSize: function () {
  127. return this.minCellWidth;
  128. }.bind(this),
  129. translate: function (value) {
  130. this.translateY = value;
  131. this.$virtualTable.css('padding-left', value + 'px');
  132. this.$columnHeadersTable.css('margin-left', value - this.$scrollableArea.scrollLeft() + 'px');
  133. }.bind(this),
  134. getTranslateValue: function () {
  135. return this.translateY;
  136. }.bind(this),
  137. scrolledColumnIdx: 0, //The column (TD Cell) that was scrolled to,
  138. renderedCount: 0,
  139. scrollFunction: 'scrollLeft',
  140. isOutOfView: function (offset) {
  141. if (offset > 0) {
  142. return this.isOutOfViewFromLeft(this.$firstRowFirstCell());
  143. } else {
  144. return this.isOutOfViewFromRight(this.$firstRowLastCell());
  145. }
  146. }.bind(this)
  147. };
  148. if (BrowserUtils.isIE11()) {
  149. //Work around a bug where the jQuery size functions can return incorrect values for these nodes in IE11 using jQuery 3.3.1.
  150. this.horizontalScrollConfig.sizeFunction = function ($node) {
  151. return $node[0].offsetWidth;
  152. };
  153. }
  154. this.horizontalScrollConfig.getScrolledIdx = function () {
  155. return this.scrolledColumnIdx;
  156. }.bind(this.horizontalScrollConfig);
  157. this.horizontalScrollConfig.setScrolledIdx = function (value) {
  158. this.scrolledColumnIdx = value;
  159. }.bind(this.horizontalScrollConfig);
  160. this.horizontalScrollConfig.recalculateRenderingIndexes = function () {
  161. if (this.count - this.scrolledColumnIdx < this.renderedCount) {
  162. this.scrolledColumnIdx = Math.max(0, this.count - this.renderedCount);
  163. }
  164. }.bind(this.horizontalScrollConfig);
  165. this.createCellStat(this.horizontalScrollConfig);
  166. this.dataProvider = options.dataProvider || new DataProvider(options.data);
  167. this._setProvideDataCount();
  168. this._setFixedRowsAndColumns({
  169. fixedRows: options.fixedRows,
  170. fixedColumns: options.fixedColumns
  171. });
  172. },
  173. /**
  174. * Dump the grid data and size.
  175. * Used to generate testcase for the unit test
  176. *
  177. * @returns
  178. */
  179. _getGridState: function _getGridState() {
  180. var state = {};
  181. state.height = this.$scrollableArea.height();
  182. state.width = this.$scrollableArea.width();
  183. state.minCellWidth = this.minCellWidth;
  184. state.minCellHeight = this.minCellHeight;
  185. state.rows = this.dataProvider.getNumRows();
  186. state.columns = this.dataProvider.getNumColumns();
  187. state.renderedRows = this.verticalScrollConfig.renderedCount;
  188. state.renderedColumns = this.horizontalScrollConfig.renderedCount;
  189. state.scrollTop = this.$scrollableArea.scrollTop();
  190. state.scrollLeft = this.$scrollableArea.scrollLeft();
  191. state.data = [];
  192. var i, j, row;
  193. for (i = 0; i < this.dataProvider.getNumRows(); i++) {
  194. row = [];
  195. state.data.push(row);
  196. for (j = 0; j < this.dataProvider.getNumColumns(); j++) {
  197. var value = this.dataProvider.getValue(i, j);
  198. var cellValue;
  199. if (this.options.setValue) {
  200. var $cell = $('<div>');
  201. this.options.setValue($cell, value, i, j);
  202. cellValue = $cell.text();
  203. } else if (_.isObject(value)) {
  204. cellValue = value.value;
  205. } else {
  206. cellValue = value || '';
  207. }
  208. row.push(cellValue);
  209. }
  210. }
  211. return JSON.stringify(state);
  212. },
  213. /**
  214. * Update the dataprovider with a new one, and call onDataChange for a rerender
  215. * @param {object} dataProvider - the new dataProvider used by the grid
  216. * @param {object} options - an options object which contains fixedRows and fixedColumns
  217. * @public
  218. */
  219. updateDataProvider: function updateDataProvider(dataProvider, options) {
  220. this.dataProvider = dataProvider;
  221. this.onDataChange({
  222. fixedRows: options.fixedRows,
  223. fixedColumns: options.fixedColumns
  224. });
  225. },
  226. /**
  227. * Notify the grid whenever the data provider is modified. This will update the number of rows/columns,
  228. * number of fixed rows/columns. Takes an options object:
  229. * isNewDataAvailable flag (boolean) used to keep track whether or not we're
  230. * appending data to the dataprovider or completely changing the data. If we're merely appending, then we should keep existing cell
  231. * stats and handle the grid scrolling to keep the right positioning for the grid. Else we can create new cell stats.
  232. * @param {object} options
  233. * @public
  234. */
  235. onDataChange: function onDataChange() {
  236. var _this = this;
  237. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  238. var isForceRedraw = this._setProvideDataCount();
  239. // Reapply the pinned rows/columns
  240. this._setFixedRowsAndColumns(options);
  241. // If this parameter is passed in as true, then we assumed that we were appending data to the dataprovider.
  242. // In this case we'd like to keep our position in the grid so scrolling vertically will appear seamless. For now
  243. // we have to do this.scrollTopOffset - 1 pixel because if you scroll down fast enough, scrolling will hang at the bottom
  244. // of the grid when we fetch additional data (async operation) since handleOffset() will set this.scrollTopOffset to be equal to
  245. // the top offset of the scrollable area. By subtracting a single pixel, this is inconspicuous to the eye and essentially forces
  246. // a recalculation of the scrolling.
  247. if (options.isNewDataAvailable) {
  248. this.scrollTopOffset -= 1;
  249. this.handleOffset();
  250. return Promise.resolve(this.isRendered);
  251. } else {
  252. //The data has changed. Force a rerender
  253. return this.resolveDelayedPromises(options).then(function () {
  254. _this.reRenderAfterDataChange(isForceRedraw, options);
  255. return _this.isRendered;
  256. });
  257. }
  258. },
  259. reRenderAfterDataChange: function reRenderAfterDataChange(isForceRedraw, options) {
  260. var _this2 = this;
  261. //silent scroll is used to handle the case where a filter was being set
  262. //and we didnt want it to trigger a scroll event as this hid the the flyout
  263. //for the filter
  264. this.silentScroll = true;
  265. if (isForceRedraw) {
  266. this.createCellStat(this.verticalScrollConfig);
  267. this.createCellStat(this.horizontalScrollConfig);
  268. }
  269. this._recalculateRenderingIndexes();
  270. if (isForceRedraw || !this.isRendered) {
  271. // redraw the table cells
  272. this.prepareTableForRendering();
  273. }
  274. options.forceScrollUpdate = true;
  275. this.updateTables(options);
  276. setTimeout(function () {
  277. _this2.silentScroll = false;
  278. }, 500);
  279. },
  280. updateTables: function updateTables() {
  281. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  282. this.updateTableValues();
  283. //Recalculate stat size statistics after all values have been updated in the UI
  284. this._reCalcTableStats();
  285. //Now resize the tables with new stats so all tables line up
  286. this.updateTableSize();
  287. this.updateScroll(options.forceScrollUpdate);
  288. this.autoFitContentIfNeeded();
  289. },
  290. resolveDelayedPromises: function resolveDelayedPromises() {
  291. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  292. return options.onDelayedUpdateReady ? options.onDelayedUpdateReady() : Promise.resolve();
  293. },
  294. /**
  295. * A cell width/height can change after all values are rendered in the grid. When this happens it can cause the tables not to line up.
  296. * This function fixes this by resetting the column header cells to initial minimum size, recalculate all table stats then resize the table headers so that
  297. * they line up with the virtual table cells
  298. */
  299. _reCalcTableStats: function _reCalcTableStats() {
  300. this._resetColumnHeaderTableMinCellWidth();
  301. this._createCellStats();
  302. this.recordTableStats(this.$table);
  303. this.recordTableStats(this.$columnHeadersTable);
  304. this.recordTableStats(this.$rowHeadersTable);
  305. this.recordTableStats(this.$cornerTable);
  306. this.resizeTableIfNeeded(this.$table);
  307. this.resizeTableIfNeeded(this.$columnHeadersTable);
  308. this.resizeTableIfNeeded(this.$rowHeadersTable);
  309. this.resizeTableIfNeeded(this.$cornerTable);
  310. },
  311. /**
  312. * Reset column header cells to initial minimum sizes
  313. *
  314. */
  315. _resetColumnHeaderTableMinCellWidth: function _resetColumnHeaderTableMinCellWidth() {
  316. var $columnHeaderRows = this.$columnHeadersTable.find('tr');
  317. var $tr = void 0,
  318. $columns = void 0,
  319. $td = void 0;
  320. for (var index = 0; index < this.fixedRows; ++index) {
  321. $tr = $columnHeaderRows.eq(index);
  322. $columns = $tr.find('td');
  323. for (var i = 0; i < $columns.length; i++) {
  324. $td = $columns.eq(i);
  325. this.setCellMinWidth($td);
  326. }
  327. }
  328. },
  329. /**
  330. * Make sure that the current padding is not too big.
  331. *
  332. * If we are re-rendering after an initial render and we have scrolled and then the grid changes size,
  333. * then we need to update the padding in case it is now too big after resizing the table
  334. * @private
  335. */
  336. _recalculateTablePositionForInitialRendering: function _recalculateTablePositionForInitialRendering() {
  337. var padding = this.$scrollableArea[this.verticalScrollConfig.scrollFunction]();
  338. var scrollInfo = this.getScrollIncrement(padding, this.verticalScrollConfig);
  339. this.verticalScrollConfig.setScrolledIdx(scrollInfo.steps);
  340. this.verticalScrollConfig.translate(scrollInfo.padding);
  341. padding = this.$scrollableArea[this.horizontalScrollConfig.scrollFunction]();
  342. scrollInfo = this.getScrollIncrement(padding, this.horizontalScrollConfig);
  343. this.horizontalScrollConfig.setScrolledIdx(scrollInfo.steps);
  344. this.horizontalScrollConfig.translate(scrollInfo.padding);
  345. },
  346. /**
  347. * Recalculate the rendering indexes
  348. * If the rendering index will only render less than what we are currently rendering,
  349. * then we need to asjust the index to render more
  350. *
  351. */
  352. _recalculateRenderingIndexes: function _recalculateRenderingIndexes() {
  353. this.verticalScrollConfig.recalculateRenderingIndexes();
  354. this.horizontalScrollConfig.recalculateRenderingIndexes();
  355. },
  356. _setProvideDataCount: function _setProvideDataCount() {
  357. var isCountChanged = false;
  358. // Get the new data count
  359. if (this.verticalScrollConfig.count !== this.dataProvider.getNumRows()) {
  360. this.verticalScrollConfig.count = this.dataProvider.getNumRows();
  361. isCountChanged = true;
  362. }
  363. if (this.horizontalScrollConfig.count !== this.dataProvider.getNumColumns()) {
  364. this.horizontalScrollConfig.count = this.dataProvider.getNumColumns();
  365. isCountChanged = true;
  366. }
  367. return isCountChanged;
  368. },
  369. _setFixedRowsAndColumns: function _setFixedRowsAndColumns(options) {
  370. this.verticalScrollConfig.count = this.dataProvider.getNumRows();
  371. this.horizontalScrollConfig.count = this.dataProvider.getNumColumns();
  372. this.fixedRows = !_.isUndefined(options.fixedRows) ? options.fixedRows : this.fixedRows;
  373. this.fixedColumns = !_.isUndefined(options.fixedColumns) ? options.fixedColumns : this.fixedColumns;
  374. },
  375. /**
  376. * Recalculate positioning for the grid table and then rerender
  377. * @public
  378. */
  379. onResize: function onResize() {
  380. if (this.$scrollableArea.scrollTop() === 0 && this.$scrollableArea.scrollLeft() === 0 && (this.scrollTopOffset !== 0 || this.scrollLeftOffset !== 0)) {
  381. // We lost the scrolling state. We need to restore it. This might happen if the node was removed from the dom and restored
  382. this.resetScroll();
  383. }
  384. //reset the cell stats if we have maximized the content so that they don't affect the new rendering
  385. // We will only clear the affected orientation
  386. if (this.autoResizeVerticalEnabled) {
  387. this.createCellStat(this.verticalScrollConfig);
  388. }
  389. if (this.autoResizeHorizontalEnabled) {
  390. this.createCellStat(this.horizontalScrollConfig);
  391. }
  392. // Adjust the padding in case we are at the end of the scrolling and we are showing less than what we can after the resize.
  393. this._recalculateTablePositionForInitialRendering();
  394. // re-render
  395. return this.render();
  396. },
  397. /**
  398. * Returns an object detailing some information about the cells
  399. * @private
  400. */
  401. createCellStat: function createCellStat(config) {
  402. config.stats = {
  403. map: {},
  404. totalRecordedSize: 0,
  405. count: 0,
  406. minSize: config.defaultSize(),
  407. maxSize: config.defaultSize(),
  408. averageSize: config.defaultSize(),
  409. totalSize: config.count * config.defaultSize()
  410. };
  411. },
  412. _createCellStats: function _createCellStats() {
  413. this.createCellStat(this.verticalScrollConfig);
  414. this.createCellStat(this.horizontalScrollConfig);
  415. },
  416. cacheMostUsedValues: function cacheMostUsedValues() {
  417. // Cache the most common used elements so that we
  418. // can easily have access to them
  419. this.$cells = this.$table.find('td');
  420. this.$rows = this.$table.find('tr');
  421. this.$firstRow = function () {
  422. return this.$table.find('tr:first');
  423. }.bind(this);
  424. this.$lastRow = function () {
  425. return this.$table.find('tr:last');
  426. }.bind(this);
  427. this.$firstRowFirstCell = function () {
  428. return this.$firstRow().find('td:first');
  429. }.bind(this);
  430. this.$firstRowLastCell = function () {
  431. return this.$firstRow().find('td:last');
  432. }.bind(this);
  433. this.horizontalScrollConfig.$firstCell = function () {
  434. return this.$firstRow().find('td:first');
  435. }.bind(this); //this.$firstRowFirstCell;
  436. this.horizontalScrollConfig.$lastCell = function () {
  437. return this.$firstRow().find('td:last');
  438. }.bind(this); //this.$firstRowLastCell;
  439. this.verticalScrollConfig.$firstCell = function () {
  440. return this.$table.find('tr:first');
  441. }.bind(this); //this.$firstRow;
  442. this.verticalScrollConfig.$lastCell = function () {
  443. return this.$table.find('tr:last');
  444. }.bind(this); // this.$lastRow;
  445. },
  446. /**
  447. * Render the grid table
  448. * @public
  449. */
  450. render: function render(options) {
  451. var _this3 = this;
  452. if (!this.$table) {
  453. this.$table = $('<table role="grid" cellspacing="0" cellpadding="0" class="gridTable userPrimaryGridFGPaletteColor" style="box-sizing: border-box; border-collapse:collapse;table-layout: fixed"></table>');
  454. if (this.options.label) {
  455. this.$table.attr('aria-label', this.options.label);
  456. }
  457. this.$columnHeadersTable = $('<table cellspacing="0" cellpadding="0" class="gridColumnHeaders" style="box-sizing: border-box; border-collapse:collapse;table-layout: fixed; position:absolute; top:0px; left:0px; pointer-events:all"></table>');
  458. this.$rowHeadersTable = $('<table cellspacing="0" cellpadding="0" class="gridRowHeaders" style="box-sizing: border-box; border-collapse:collapse;table-layout: fixed; position:absolute; top:0px; left:0px; pointer-events:all"></table>');
  459. this.$cornerTable = $('<table cellspacing="0" cellpadding="0" class="cornerTable" style="box-sizing: border-box; border-collapse:collapse;table-layout: fixed; position:absolute; top:0px; left:0px; pointer-events:all"></table>');
  460. this.$scrollableArea = $('<div style="height:100%; width:100%; overflow:auto;position:relative;text-align:left"></div>');
  461. this.$scrollableArea.attr('id', UniqueId.get('scrollableArea_'));
  462. this.$virtualTable = $('<div role="presentation" style="box-sizing: border-box; padding:0px; display:inline-block; min-width:10000px; min-height:10000px">');
  463. this.$virtualTable.attr('id', UniqueId.get('virtualTable_'));
  464. this.$virtualTable.append(this.$table);
  465. this.$scrollableArea.append(this.$virtualTable);
  466. this.$headerContainer = $('<div style="top:0px;left:0px;overflow:hidden;position:absolute;text-align:left; pointer-events:none"></div>');
  467. this.$headerContainer.attr('id', UniqueId.get('headerContainer_'));
  468. this.$headerContainer.append(this.$columnHeadersTable);
  469. if (this.fixedColumns && this.fixedColumns > 0) {
  470. this.bHasCornerTable = true;
  471. this.$headerContainer.append(this.$rowHeadersTable);
  472. this.$headerContainer.append(this.$cornerTable);
  473. }
  474. this.$contentWrapper = $('<div style="height:100%; width:100%;position:relative;" role="application"></div>');
  475. this.$contentWrapper.attr('id', UniqueId.get('contentWrapper_'));
  476. this.$contentWrapper.append(this.$scrollableArea);
  477. this.$contentWrapper.append(this.$headerContainer);
  478. this.$el.append(this.$contentWrapper);
  479. this.scrollTopOffset = 0;
  480. this.scrollLeftOffset = 0;
  481. this.registerScrollEvents();
  482. }
  483. return this.resolveDelayedPromises(options).then(function () {
  484. _this3.prepareTableForRendering();
  485. if (_this3.isRendered) {
  486. _this3.updateTables(options);
  487. }
  488. return _this3.isRendered;
  489. });
  490. },
  491. prepareTableForRendering: function prepareTableForRendering() {
  492. this.$scrollableArea.css('overflow-x', 'auto');
  493. this.$scrollableArea.css('overflow-y', 'auto');
  494. this.$headerContainer.height(this.$scrollableArea[0].clientHeight);
  495. this.$headerContainer.width(this.$scrollableArea[0].clientWidth);
  496. this._clearTables();
  497. if (!this.dataProvider.getNumRows()) {
  498. this.isRendered = false;
  499. return;
  500. }
  501. this.isRendered = this.renderTable();
  502. if (this.isRendered) {
  503. this.renderPinnedColumnHeaders();
  504. if (this.fixedColumns && this.fixedColumns > 0) {
  505. this.renderPinnedRowHeaders();
  506. this.renderPinnedCorner();
  507. }
  508. }
  509. this.cacheMostUsedValues();
  510. },
  511. _clearTables: function _clearTables() {
  512. this.$table.empty();
  513. this.$columnHeadersTable.empty();
  514. this.$rowHeadersTable.empty();
  515. this.$cornerTable.empty();
  516. },
  517. autoFitContentIfNeeded: function autoFitContentIfNeeded() {
  518. if (this.autoResizeHorizontal || this.autoResizeVertical) {
  519. this.autoResizeHorizontalEnabled = false;
  520. this.autoResizeVerticalEnabled = false;
  521. var extra, nodes, index, i;
  522. if (this.autoResizeHorizontal && this.$virtualTable.outerWidth() <= this.$el.innerWidth()) {
  523. var width = this.$scrollableArea[0].clientWidth / this.horizontalScrollConfig.count - 2;
  524. extra = 0;
  525. nodes = [];
  526. for (index in this.horizontalScrollConfig.stats.map) {
  527. if (this.horizontalScrollConfig.stats.map.hasOwnProperty(index)) {
  528. if (this.horizontalScrollConfig.stats.map[index] > width) {
  529. extra += this.horizontalScrollConfig.stats.map[index] - width;
  530. } else {
  531. nodes.push(this.$el.find('td[col="' + index + '"]'));
  532. }
  533. }
  534. }
  535. width -= extra / nodes.length;
  536. for (i = 0; i < nodes.length; i++) {
  537. nodes[i].css('min-width', width + 'px');
  538. }
  539. this.$virtualTable.css(this.horizontalScrollConfig.minSizeProp, '0px');
  540. this.$scrollableArea.css('overflow-x', 'visible');
  541. this.autoResizeHorizontalEnabled = true;
  542. }
  543. if (this.autoResizeVertical && this.$virtualTable.outerHeight() <= this.$el.innerHeight()) {
  544. var height = this.$scrollableArea[0].clientHeight / this.verticalScrollConfig.count - 2;
  545. extra = 0;
  546. nodes = [];
  547. for (index in this.verticalScrollConfig.stats.map) {
  548. if (this.verticalScrollConfig.stats.map[index] > height) {
  549. extra += this.verticalScrollConfig.stats.map[index] - height;
  550. } else {
  551. nodes.push(this.$el.find('td[row="' + index + '"]'));
  552. }
  553. }
  554. height -= extra / nodes.length;
  555. for (i = 0; i < nodes.length; i++) {
  556. nodes[i].css('height', height + 'px');
  557. }
  558. this.$virtualTable.css(this.verticalScrollConfig.minSizeProp, '0px');
  559. this.$scrollableArea.css('overflow-y', 'visible');
  560. this.autoResizeVerticalEnabled = true;
  561. }
  562. if (this.autoResizeVerticalEnabled || this.autoResizeHorizontalEnabled) {
  563. this.recordTableStats(this.$table);
  564. this.resizeTableIfNeeded(this.$columnHeadersTable);
  565. this.resizeTableIfNeeded(this.$rowHeadersTable);
  566. }
  567. }
  568. },
  569. registerScrollEvents: function registerScrollEvents() {
  570. this.$scrollableArea.on('scroll.virtualScroll', this.onScroll.bind(this));
  571. this.$headerContainer.on('wheel', this.onWheel.bind(this));
  572. },
  573. onWheel: function onWheel(event) {
  574. var scrollTop = this.$scrollableArea.scrollTop();
  575. if (event.originalEvent.deltaY < 0) {
  576. this.$scrollableArea.scrollTop(this.$scrollableArea.scrollTop() - 90);
  577. } else if (event.originalEvent.deltaY > 0) {
  578. this.$scrollableArea.scrollTop(this.$scrollableArea.scrollTop() + 90);
  579. }
  580. if (event.originalEvent.deltaX < 0) {
  581. this.$scrollableArea.scrollLeft(this.$scrollableArea.scrollLeft() - 90);
  582. } else if (event.originalEvent.deltaX > 0) {
  583. this.$scrollableArea.scrollLeft(this.$scrollableArea.scrollLeft() + 90);
  584. }
  585. if (scrollTop !== this.$scrollableArea.scrollTop()) {
  586. event.preventDefault();
  587. }
  588. },
  589. remove: function remove() {
  590. if (this.scrollTimer) {
  591. clearTimeout(this.scrollTimer);
  592. }
  593. this.$scrollableArea && this.$scrollableArea.remove();
  594. this.$headerContainer && this.$headerContainer.remove();
  595. if (this.$contentWrapper) {
  596. this.$contentWrapper.remove();
  597. this.$contentWrapper = null;
  598. }
  599. this.dataProvider = null;
  600. View.inherited('remove', this, arguments);
  601. },
  602. /**
  603. * scroll handler. Called when the user scrolls the
  604. * virtual table
  605. */
  606. onScroll: function onScroll() {
  607. var isScrollStarted = false;
  608. if (this.scrollTimer) {
  609. clearTimeout(this.scrollTimer);
  610. isScrollStarted = true;
  611. }
  612. // Notify the start scroll handler
  613. if (!isScrollStarted && this.options.onStartScroll && !this.silentScroll) {
  614. this.options.onStartScroll();
  615. }
  616. this.scrollTimer = setTimeout(function () {
  617. this.scrollTimer = null;
  618. if (this.options.onFinishScroll && !this.silentScroll) {
  619. this.options.onFinishScroll();
  620. }
  621. this._setFocusAfterScroll();
  622. }.bind(this), 800);
  623. this.updateScroll();
  624. },
  625. /**
  626. * Update the positions of the pinned headers. Also, if there are 5 or fewer rowers left to render, determine whether or not we
  627. * need to fetch more data from our server by checking if we passed in a callback function inside our options object in init(). If
  628. * so, make the call to fetch more data, wait for the response, and then update position and offsets.
  629. * @private
  630. */
  631. updateScroll: function updateScroll(isForceUpdate) {
  632. var scrollTopOffset = this.$scrollableArea.scrollTop();
  633. var scrollLeftOffset = this.$scrollableArea.scrollLeft();
  634. this.updatePinnedHeaderPosition(scrollTopOffset, scrollLeftOffset);
  635. var scrolledIdx = this.verticalScrollConfig.getScrolledIdx();
  636. var numberOfDataRowsInGrid = this.options.getMoreData && this.verticalScrollConfig.count - (scrolledIdx + this.verticalScrollConfig.renderedCount);
  637. if (numberOfDataRowsInGrid !== false && numberOfDataRowsInGrid >= 0 && numberOfDataRowsInGrid < 5 && this.scrollTopOffset <= scrollTopOffset) {
  638. this.options.getMoreData();
  639. }
  640. this.handleOffset(scrollTopOffset, scrollLeftOffset, isForceUpdate);
  641. },
  642. resetScroll: function resetScroll() {
  643. this.$scrollableArea.scrollTop(this.scrollTopOffset);
  644. this.$scrollableArea.scrollLeft(this.scrollLeftOffset);
  645. },
  646. /**
  647. * Handles the scrolling if we have scrolled horizontally or vertically by changing the vertical/horizontal offsets.
  648. * @private
  649. */
  650. handleOffset: function handleOffset(scrollTop, scrollLeft, isForceUpdate) {
  651. var scrollTopOffset = scrollTop || this.$scrollableArea.scrollTop();
  652. var scrollLeftOffset = scrollLeft || this.$scrollableArea.scrollLeft();
  653. if (this.scrollTopOffset !== scrollTopOffset || isForceUpdate) {
  654. this.handleScroll(scrollTopOffset - this.scrollTopOffset, this.verticalScrollConfig, isForceUpdate);
  655. }
  656. if (this.scrollLeftOffset !== scrollLeftOffset || isForceUpdate) {
  657. this.handleScroll(scrollLeftOffset - this.scrollLeftOffset, this.horizontalScrollConfig, isForceUpdate);
  658. }
  659. // update the current scrolltop and scrollLeft
  660. // values.
  661. // Don't use the variables above
  662. // (scrollTopOffset/scrollLeftOffset), the scroll
  663. // values might have changed while processing the
  664. // scroll event
  665. this.scrollTopOffset = this.$scrollableArea.scrollTop();
  666. this.scrollLeftOffset = this.$scrollableArea.scrollLeft();
  667. },
  668. /**
  669. * Remove any colspan/rowspan that was set on the cell
  670. * when the grouping is enabled This method will save
  671. * the current state of the grouping so it can be
  672. * restored using the restoreGrouping method
  673. */
  674. removeGrouping: function removeGrouping() {
  675. if (!this.isGroupEnabled) {
  676. return;
  677. }
  678. this.$cells.each(function () {
  679. // we use font-size set to 0px to hide the text to avoid the height/width impact on the cell
  680. var $td = $(this);
  681. var display = $td.css('display');
  682. var fontSize = $td.css('font-size');
  683. if (display === 'none') {
  684. $td.css({
  685. 'display': '',
  686. 'visibility': 'hidden',
  687. 'font-size': '0px'
  688. });
  689. }
  690. if (fontSize) {
  691. $td.attr({ '_fontSize': fontSize });
  692. }
  693. var colspan = $td.attr('colspan');
  694. $td.attr({ '_colspan': null });
  695. if (colspan) {
  696. $td.attr({
  697. 'colspan': null,
  698. '_colspan': colspan
  699. });
  700. $td.css({ 'font-size': '0px' });
  701. }
  702. var rowspan = $td.attr('rowspan');
  703. $td.attr({ '_rowspan': null });
  704. if (rowspan) {
  705. $td.attr({
  706. 'rowspan': null,
  707. '_rowspan': rowspan
  708. });
  709. $td.css({ 'font-size': '0px' });
  710. }
  711. });
  712. },
  713. /**
  714. * Restore any grouping by adding colspan/rowspan to the
  715. * cells. This method is supposed to restore the
  716. * grouping that was removed using the removeGrouping
  717. * method
  718. * @private
  719. */
  720. restoreGrouping: function restoreGrouping() {
  721. if (!this.isGroupEnabled) {
  722. return;
  723. }
  724. this.$cells.each(function () {
  725. var $td = $(this);
  726. var display = $td.css('visibility');
  727. var fontSize = $td.attr('_fontSize');
  728. //when restore happens, we used the saved state of the fontSize to set the font-size value
  729. if (display === 'hidden') {
  730. $td.css({
  731. 'display': 'none',
  732. 'visibility': '',
  733. 'font-size': fontSize ? fontSize : ''
  734. });
  735. $td.attr({
  736. '_fontSize': null
  737. });
  738. }
  739. var colspan = $td.attr('_colspan');
  740. if (colspan) {
  741. $td.attr({
  742. '_colspan': null,
  743. 'colspan': colspan,
  744. '_fontSize': null
  745. });
  746. $td.css({
  747. 'font-size': fontSize ? fontSize : ''
  748. });
  749. }
  750. var rowspan = $td.attr('_rowspan');
  751. if (rowspan) {
  752. $td.attr({
  753. '_rowspan': null,
  754. 'rowspan': rowspan,
  755. '_fontSize': null
  756. });
  757. $td.css({
  758. 'font-size': fontSize ? fontSize : ''
  759. });
  760. }
  761. });
  762. },
  763. /**
  764. * Offset is positive when moving right and negative
  765. * when moving left.
  766. *
  767. * @param offset
  768. */
  769. handleScroll: function handleScroll(offset, config, isForceUpdate) {
  770. if (!this.dataProvider.getNumRows()) {
  771. return;
  772. }
  773. // We need to temporarily remove the group to
  774. // determine the cell width so that we can properly
  775. // do the proper horizontal scrolling
  776. this.removeGrouping();
  777. var shifted = false;
  778. // Calculate how many columns we are shifting based on the current scroll position
  779. // if the increment steps is different than the current index we are rendering, then we need to redraw
  780. var increment = this.getScrollIncrement(this.$scrollableArea[config.scrollFunction](), config);
  781. if (increment.steps !== config.getScrolledIdx() || isForceUpdate) {
  782. shifted = this.redrawTable(increment.steps, increment.padding, config);
  783. }
  784. if (!shifted) {
  785. // If nothing has changed, we restore the
  786. // grouping. Otherwise the grouping would be
  787. // properly rendered when we shift the values
  788. this.restoreGrouping();
  789. } else if (this.options.onAfterGridUpdate) {
  790. // Cells have been updated as a result of a scroll. Notify the callback if it exists
  791. this.options.onAfterGridUpdate();
  792. }
  793. },
  794. redrawTable: function redrawTable(newScrolledToRowOrColumnIndex, paddingOffset, config) {
  795. this.updateTablePadding(paddingOffset, config);
  796. var redrawn = this.updateIndexAndValues(newScrolledToRowOrColumnIndex, config);
  797. this.validateTablePadding(paddingOffset, config);
  798. return redrawn;
  799. },
  800. updateIndexAndValues: function updateIndexAndValues(newScrolledToRowOrColumnIndex, config) {
  801. if (newScrolledToRowOrColumnIndex < 0 || newScrolledToRowOrColumnIndex + config.renderedCount > config.count) {
  802. return false;
  803. }
  804. var previousScrolledIdx = config.getScrolledIdx();
  805. config.setScrolledIdx(newScrolledToRowOrColumnIndex);
  806. this.updateTableValues({ previousScrolledIdx: previousScrolledIdx, isVerticalScrolling: config.isVerticalScrolling });
  807. return true;
  808. },
  809. resizeTableIfNeeded: function resizeTableIfNeeded($table) {
  810. //update the size of the columns in the header here if they differ from stats
  811. var $rows = $table.find('tr');
  812. for (var i = 0; i < $rows.length; i++) {
  813. var $tr = $rows.eq(i);
  814. var $columns = $tr.find('td');
  815. for (var j = 0; j < $columns.length; j++) {
  816. var $td = $columns.eq(j);
  817. if ($td.hasClass('corner')) {
  818. if ($td.is(':visible')) {
  819. this.resizeCorner($td);
  820. }
  821. } else {
  822. this.updateCellSize($td);
  823. }
  824. }
  825. }
  826. this.resizeCornerTable();
  827. },
  828. /** in the column header table, must size the corner width independently when multiple row nesting is involved since it arbitrarily chooses
  829. * the 2nd cell in the first array to be the visible cell and sizes it according to that cell stat when it should
  830. * in fact be sized based on the width of the 1st and 2nd(0th and 1st) cell width stat.
  831. * @param {object} td - the cell
  832. * @private
  833. */
  834. resizeCorner: function resizeCorner($td) {
  835. var colspan = parseInt($td.attr('colspan'), 10);
  836. var rowspan = parseInt($td.attr('rowspan'), 10);
  837. var i;
  838. var width = 0;
  839. var height = 0;
  840. for (i = 0; i < this.fixedColumns; i++) {
  841. width += this.horizontalScrollConfig.stats.map[i];
  842. }
  843. for (i = 0; i < this.fixedRows; i++) {
  844. height += this.verticalScrollConfig.stats.map[i];
  845. }
  846. //prevents the corner from becoming the size of more cells than are visible after scrolling to the right
  847. if (colspan !== this.fixedColumns) {
  848. for (i = 0; i < this.fixedColumns - colspan; i++) {
  849. width -= this.horizontalScrollConfig.stats.map[i];
  850. }
  851. }
  852. if (rowspan !== this.fixedRows) {
  853. for (i = 0; i < this.fixedRows - rowspan; i++) {
  854. height -= this.verticalScrollConfig.stats.map[i];
  855. }
  856. }
  857. $td.css('min-width', width + 'px');
  858. $td.css('height', height + 'px');
  859. },
  860. /**
  861. * Resize the corner table to match the stats set by _resizeCorner
  862. * @private
  863. */
  864. resizeCornerTable: function resizeCornerTable() {
  865. // we only care about the corner table if we have a pinned header
  866. if (this.fixedColumns > 0) {
  867. var width = 0;
  868. var height = 0;
  869. for (var i = 0; i < this.fixedColumns; i++) {
  870. width += this.horizontalScrollConfig.stats.map[i];
  871. }
  872. for (var k = 0; k < this.fixedRows; k++) {
  873. height += this.verticalScrollConfig.stats.map[k];
  874. }
  875. this.$corner.css({
  876. 'min-width': width
  877. });
  878. this.$corner.css({
  879. 'height': height
  880. });
  881. }
  882. },
  883. /**
  884. * Update the positions of the pinned column/row headers tables
  885. * @param {number} scrollTopOffset - the offset we should pad to the top
  886. * @param {number} scrollLeftOffset - the offset we should pad to te left
  887. * @private
  888. */
  889. updatePinnedHeaderPosition: function updatePinnedHeaderPosition() /*scrollTopOffset, scrollLeftOffset*/{
  890. this.$columnHeadersTable.css('margin-left', this.horizontalScrollConfig.getTranslateValue() - this.$scrollableArea.scrollLeft() + 'px');
  891. this.$rowHeadersTable.css('margin-top', this.verticalScrollConfig.getTranslateValue() - this.$scrollableArea.scrollTop() + 'px');
  892. },
  893. /**
  894. * based on the cells that are currently visible, update the minimum widths and heights for the columns and rows, respectively.
  895. * @param {object} table - the table
  896. * @private
  897. */
  898. recordTableStats: function recordTableStats($table) {
  899. var $rows = $table.find('tr');
  900. for (var index = $rows.length - 1; index >= 0; index--) {
  901. var $tr = $rows.eq(index);
  902. var $columns = $tr.find('td:visible');
  903. var $td;
  904. // set the values and the formats
  905. for (var i = 0; i < $columns.length; i++) {
  906. $td = $columns.eq(i);
  907. var colspan = parseInt($td.attr('colspan'), 10) || 1;
  908. var rowspan = parseInt($td.attr('rowspan'), 10) || 1;
  909. //only record the width of cells that have col span 1 not including corners that span multiple columns
  910. if (colspan === 1 && (!$td.hasClass('corner') || this.fixedColumns === 1)) {
  911. var col = $td.attr(ATTRIBUTES.COLUMN_INDEX);
  912. this.recordCellStats($td, this.horizontalScrollConfig, col);
  913. }
  914. if (rowspan === 1 && (!$td.hasClass('corner') || this.fixedRows === 1)) {
  915. var row = $td.attr(ATTRIBUTES.ROW_INDEX);
  916. this.recordCellStats($td, this.verticalScrollConfig, row);
  917. }
  918. }
  919. }
  920. },
  921. /**
  922. * adjust the size of the virtual table
  923. * @private
  924. */
  925. updateTableSize: function updateTableSize() {
  926. this.$virtualTable.css(this.horizontalScrollConfig.minSizeProp, this.horizontalScrollConfig.stats.totalSize + 'px');
  927. this.$virtualTable.css(this.verticalScrollConfig.minSizeProp, this.verticalScrollConfig.stats.totalSize + 'px');
  928. },
  929. /**
  930. * Updates the table padding in the vertical or horizontal directionshi
  931. * @param {number} newPadding - the new padding for the table
  932. * @param {config} config - the vertical/horizontal config
  933. * @private
  934. */
  935. updateTablePadding: function updateTablePadding(newPadding, config) {
  936. // adjust the padding to simulate the scroll
  937. var padding = newPadding;
  938. config.translate(padding);
  939. },
  940. validateTablePadding: function validateTablePadding(newPadding, config) {
  941. this.updateTableSize();
  942. var previousScrolledIdx = void 0;
  943. if (newPadding <= 0) {
  944. newPadding = 0;
  945. if (config.getScrolledIdx() !== 0) {
  946. previousScrolledIdx = config.getScrolledIdx();
  947. config.setScrolledIdx(0);
  948. this.updateTableValues({ previousScrolledIdx: config.getScrolledIdx(), isVerticalScrolling: config.isVerticalScrolling });
  949. }
  950. config.translate(newPadding);
  951. } else if (newPadding > this.$scrollableArea[config.scrollFunction]()) {
  952. newPadding = this.$scrollableArea[config.scrollFunction]();
  953. var scrollInfo = this.getScrollIncrement(newPadding, config);
  954. if (config.getScrolledIdx() !== scrollInfo.steps) {
  955. previousScrolledIdx = config.getScrolledIdx();
  956. config.setScrolledIdx(scrollInfo.steps);
  957. this.updateTableValues({ previousScrolledIdx: previousScrolledIdx, isVerticalScrolling: config.isVerticalScrolling });
  958. }
  959. config.translate(newPadding);
  960. }
  961. // The virtual table is too big or too small(some
  962. // cell sizes have changed).. adjust it.
  963. var expectedTableSize = config.sizeFunction(this.$table) + newPadding;
  964. var currentTableSize = config.sizeFunction(this.$virtualTable);
  965. if (config.getScrolledIdx() + config.renderedCount === config.count && expectedTableSize !== currentTableSize) {
  966. config.stats.totalSize -= currentTableSize - expectedTableSize;
  967. this.updateTableSize();
  968. }
  969. },
  970. /**
  971. * @private
  972. */
  973. updateTableValues: function updateTableValues() {
  974. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  975. var previousScrolledIdx = options.previousScrolledIdx;
  976. var isVerticalScrolling = options.isVerticalScrolling;
  977. if (this.dataProvider.getNumRows() && !this.partialValuesUpdate(previousScrolledIdx, isVerticalScrolling)) {
  978. this.allValuesUpdate();
  979. }
  980. },
  981. /**
  982. * @private
  983. */
  984. updateDataVertically: function updateDataVertically(previousScrolledIdx, delta) {
  985. this.resetTableGroups(this.$table);
  986. this._updateTableScrolledVertically({ $table: this.$table, previousScrolledIdx: previousScrolledIdx, delta: delta, handleGrouping: true });
  987. this.updateTableGroups(this.$table);
  988. //Remove a row and placing it at top or end of the table at the current scroll position.
  989. // Also, updating the table groups might change the current scroll position
  990. // We need to restore the scroll position
  991. var scrollTop = this.$scrollableArea.scrollTop();
  992. this.$scrollableArea.scrollTop(scrollTop);
  993. this.recordTableStats(this.$table);
  994. this.resizeTableIfNeeded(this.$columnHeadersTable);
  995. this.resizeTableIfNeeded(this.$rowHeadersTable);
  996. },
  997. /**
  998. * @private
  999. */
  1000. updateRowHeaderVertically: function updateRowHeaderVertically(previousScrolledIdx, delta) {
  1001. this.resetTableGroups(this.$rowHeadersTable);
  1002. this._updateTableScrolledVertically({ $table: this.$rowHeadersTable, previousScrolledIdx: previousScrolledIdx, delta: delta, handleGrouping: true, isRowHeader: true });
  1003. this.updateTableGroups(this.$rowHeadersTable);
  1004. this.recordTableStats(this.$rowHeadersTable);
  1005. },
  1006. /**
  1007. *
  1008. * Do a partial update of the values in the given $rows array.
  1009. * If we are not changing all values, we simply move the rows/columns and keep the unchanged cells. This is done for better performance.
  1010. *
  1011. * @param previousScrolledIdx - old scrolled row or columnd index location before the updated
  1012. * @param isVerticalScrolling - true if scrolling vertically else false
  1013. * @param $rows
  1014. * @returns {Boolean}
  1015. */
  1016. partialValuesUpdate: function partialValuesUpdate(previousScrolledIdx, isVerticalScrolling) {
  1017. var _this4 = this;
  1018. var scrolledVerticalIdx = this.verticalScrollConfig.getScrolledIdx();
  1019. var index = isVerticalScrolling ? scrolledVerticalIdx : this.horizontalScrollConfig.getScrolledIdx();
  1020. var diff = Math.abs(previousScrolledIdx - index);
  1021. var $columnHeaderRows = this.$columnHeadersTable.find('tr');
  1022. var $tableRows = this.$table.find('tr');
  1023. var scrollVertical = isVerticalScrolling && diff < this.verticalScrollConfig.renderedCount;
  1024. var scrollHorizontal = !isVerticalScrolling && diff < this.horizontalScrollConfig.renderedCount;
  1025. if (scrollVertical) {
  1026. var delta = this.fixedRows && this.fixedRows > 0 ? this.fixedRows : 0;
  1027. //updateRowHeaders
  1028. this.updateRowHeaderVertically(previousScrolledIdx, delta);
  1029. //update data
  1030. this.updateDataVertically(previousScrolledIdx, delta);
  1031. this.resizeTableIfNeeded(this.$columnHeadersTable);
  1032. this.resizeTableIfNeeded(this.$rowHeadersTable);
  1033. } else if (scrollHorizontal) {
  1034. this.resetTableGroups(this.$columnHeadersTable);
  1035. var _delta = this.fixedColumns && this.fixedColumns > 0 ? this.fixedColumns : 0;
  1036. var getTableRowIndex = function getTableRowIndex(rowIndex) {
  1037. return scrolledVerticalIdx + rowIndex;
  1038. };
  1039. var options = {
  1040. previousScrolledIdx: previousScrolledIdx,
  1041. delta: _delta,
  1042. cbUpdateCell: function cbUpdateCell($td, isRowHeaders, rowIndex, tdIndex) {
  1043. rowIndex = isRowHeaders ? rowIndex : getTableRowIndex(rowIndex);
  1044. if (previousScrolledIdx < _this4.horizontalScrollConfig.getScrolledIdx()) {
  1045. tdIndex = _this4.horizontalScrollConfig.renderedCount - tdIndex - 1;
  1046. } else {
  1047. tdIndex = tdIndex + _delta;
  1048. }
  1049. _this4.updateCell($td, rowIndex, tdIndex);
  1050. },
  1051. isRowHeaders: true
  1052. };
  1053. //update column headers
  1054. this._updateColumnHeadersWhenScrolleFastdHorizontally($columnHeaderRows, options);
  1055. this.updateTableGroups(this.$columnHeadersTable);
  1056. this.recordTableStats(this.$columnHeadersTable);
  1057. //update data
  1058. this.resetTableGroups(this.$table);
  1059. options.isRowHeaders = false;
  1060. this._updateColumnHeadersWhenScrolleFastdHorizontally($tableRows, options);
  1061. this.updateTableGroups(this.$table);
  1062. this.recordTableStats(this.$table);
  1063. this.resizeTableIfNeeded(this.$columnHeadersTable);
  1064. }
  1065. if (scrollVertical || scrollHorizontal) {
  1066. this.cacheMostUsedValues();
  1067. }
  1068. return scrollVertical || scrollHorizontal;
  1069. },
  1070. /**
  1071. * Updates the table cell column headers after scrolled fast horizontally
  1072. *
  1073. * @param {object} $rows - the jQuery rows object
  1074. * @param {object} options - options to update the table cells position and their contents within an table row
  1075. */
  1076. _updateColumnHeadersWhenScrolleFastdHorizontally: function _updateColumnHeadersWhenScrolleFastdHorizontally($rows) {
  1077. var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  1078. if ($rows instanceof $) {
  1079. var $td = void 0;
  1080. var i = void 0;
  1081. var j = void 0;
  1082. var isRowHeaders = options.isRowHeaders;
  1083. var delta = options.delta;
  1084. var previousScrolledIdx = options.previousScrolledIdx;
  1085. for (i = 0; i < $rows.length; i++) {
  1086. var $row = $rows.eq(i);
  1087. var $tds = $row.children();
  1088. if (previousScrolledIdx < this.horizontalScrollConfig.getScrolledIdx()) {
  1089. for (j = this.horizontalScrollConfig.getScrolledIdx() - previousScrolledIdx - 1; j >= 0; j--) {
  1090. $td = $tds.eq(j + delta);
  1091. $td.appendTo($row);
  1092. options.cbUpdateCell($td, isRowHeaders, i, j);
  1093. }
  1094. } else {
  1095. var $tdAfter = this.fixedColumns && this.fixedColumns > 0 ? $tds.eq(this.fixedColumns - 1) : null;
  1096. for (j = previousScrolledIdx - this.horizontalScrollConfig.getScrolledIdx() - 1; j >= 0; j--) {
  1097. $td = $tds.eq($tds.length - j - 1);
  1098. if ($tdAfter) {
  1099. $td.insertAfter($tdAfter);
  1100. } else {
  1101. $td.prependTo($row);
  1102. }
  1103. options.cbUpdateCell($td, isRowHeaders, i, j);
  1104. }
  1105. }
  1106. }
  1107. }
  1108. },
  1109. /**
  1110. * Update all the values in the given $rows array.
  1111. *
  1112. * @param $rows
  1113. */
  1114. allValuesUpdate: function allValuesUpdate() {
  1115. var _this5 = this;
  1116. var _cbGetScrolledPinnedRowHeaders = function _cbGetScrolledPinnedRowHeaders(rowIndex) {
  1117. //row headers are pinned, should not be moved when scrolled vertically
  1118. return _this5.fixedRows && rowIndex < _this5.fixedRows ? rowIndex : rowIndex + _this5.verticalScrollConfig.getScrolledIdx();
  1119. };
  1120. //update column headers
  1121. var $columnHeaderRows = this.$columnHeadersTable.find('tr');
  1122. var $tr;
  1123. for (var index = this.fixedRows - 1; index >= 0; index--) {
  1124. $tr = $columnHeaderRows.eq(index);
  1125. this.updateRow({ $tr: $tr, rowIndex: index, handleGrouping: true, isRowHeader: false, cbGetScrolledFastPinnedCornerCellIdx: cbGetScrolledFastPinnedCornerCellIdx });
  1126. }
  1127. this.recordTableStats(this.$columnHeadersTable);
  1128. //update row headers
  1129. var $rowHeaderRows = this.$rowHeadersTable.find('tr');
  1130. for (index = $rowHeaderRows.length - 1; index >= 0; index--) {
  1131. $tr = $rowHeaderRows.eq(index);
  1132. this.updateRow({ $tr: $tr, rowIndex: _cbGetScrolledPinnedRowHeaders(index), handleGrouping: true, isRowHeader: true });
  1133. }
  1134. this.recordTableStats(this.$rowHeadersTable);
  1135. //update corner table
  1136. var $cornerTableRows = this.$cornerTable.find('tr');
  1137. for (index = this.fixedRows - 1; index >= 0; index--) {
  1138. $tr = $cornerTableRows.eq(index);
  1139. this.updateRow({ $tr: $tr, rowIndex: index, handleGrouping: true, isRowHeader: false, cbGetScrolledFastPinnedCornerCellIdx: cbGetScrolledFastPinnedCornerCellIdx });
  1140. }
  1141. this.recordTableStats(this.$cornerTable);
  1142. //update data
  1143. var $tableRows = this.$table.find('tr');
  1144. for (index = $tableRows.length - 1; index >= 0; index--) {
  1145. $tr = $tableRows.eq(index);
  1146. this.updateRow({ $tr: $tr, rowIndex: _cbGetScrolledPinnedRowHeaders(index), handleGrouping: true, isRowHeader: false, cbGetScrolledFastPinnedCornerCellIdx: cbGetScrolledFastPinnedCornerCellIdx });
  1147. }
  1148. this.recordTableStats(this.$table);
  1149. this.resizeTableIfNeeded(this.$columnHeadersTable);
  1150. this.resizeTableIfNeeded(this.$rowHeadersTable);
  1151. },
  1152. /**
  1153. * Updates the passed in row from the table
  1154. * @param {object} tr - the row
  1155. * @param {number} rowIndex - the rowIndex of the cell
  1156. * @param {boolean} handleGrouping
  1157. * @param {boolean} isRowHeader
  1158. * @param {function} cbGetScrolledFastPinnedCornerCellIdx - optional callback to get the correct corner cell colunm index within a row
  1159. * @private
  1160. */
  1161. updateRow: function updateRow() {
  1162. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  1163. var $tr = options.$tr;
  1164. var rowIndex = options.rowIndex;
  1165. var handleGrouping = options.handleGrouping;
  1166. var isRowHeader = options.isRowHeader;
  1167. var cbGetScrolledFastPinnedCornerCellIdx = options.cbGetScrolledFastPinnedCornerCellIdx;
  1168. var $columns = $tr.find('td');
  1169. var $td;
  1170. $tr.attr(ATTRIBUTES.ROW_INDEX, rowIndex);
  1171. if (this.options.formatRow) {
  1172. this.options.formatRow($tr, rowIndex);
  1173. }
  1174. // set the values and the formats
  1175. for (var i = 0; i < $columns.length; i++) {
  1176. $td = $columns.eq(i);
  1177. this.updateCell($td, rowIndex, i, handleGrouping, isRowHeader, cbGetScrolledFastPinnedCornerCellIdx);
  1178. }
  1179. },
  1180. /**
  1181. * Updates and, if necessary, formats the passed in cell with new styles and text. Called whenever we need to render the cell,
  1182. * eg. when we scroll/resize, etc
  1183. * @param {object} td - a jQuery object representing the cell
  1184. * @param {number} rowIndex - the rowIndex of the cell
  1185. * @param {number} tdIndex - the column index of the cell. If the grid has been scrolled horizontally, we must manually calculate this value
  1186. * @param {boolean} handleGrouping
  1187. * @param {boolean} isRowHeader
  1188. * @param {function} cbGetScrolledFastPinnedCornerCellIdx - optional callback to get the correct corner cell colunm index within a row
  1189. * @private
  1190. */
  1191. updateCell: function updateCell($td, rowIndex, tdIndex, handleGrouping, isRowHeader, cbGetScrolledFastPinnedCornerCellIdx) {
  1192. var colIndex = cbGetScrolledFastPinnedCornerCellIdx ? cbGetScrolledFastPinnedCornerCellIdx(tdIndex, this.fixedColumns, this.fixedRows) : null;
  1193. if (!_.isNumber(colIndex)) {
  1194. colIndex = isRowHeader ? tdIndex : this.horizontalScrollConfig.getScrolledIdx() + tdIndex;
  1195. }
  1196. var value = this.dataProvider.getValue(rowIndex, colIndex);
  1197. $td.attr(ATTRIBUTES.COLUMN_INDEX, colIndex);
  1198. $td.attr('row', rowIndex);
  1199. this.setCellMinWidth($td);
  1200. $td.attr('data-group', null);
  1201. if (handleGrouping) {
  1202. this.resetGroupSettings($td);
  1203. }
  1204. if (this.options.setValue) {
  1205. this.options.setValue($td, value, rowIndex, colIndex);
  1206. } else {
  1207. var cellValue;
  1208. if (_.isObject(value)) {
  1209. cellValue = value.value;
  1210. } else {
  1211. cellValue = value || '';
  1212. }
  1213. $td.attr('aria-label', cellValue);
  1214. $td.text(cellValue);
  1215. }
  1216. if (this.options.formatCell) {
  1217. this.options.formatCell($td, value, rowIndex, colIndex);
  1218. }
  1219. this.updateCellSize($td);
  1220. if (handleGrouping) {
  1221. this.renderGroup($td, tdIndex);
  1222. }
  1223. },
  1224. /**
  1225. * Updates the cell value with value when GridDataProvider cell value changes
  1226. * @param {number} rowIndex - the rowIndex of the cell
  1227. * @param {number} colIndex - the column index of the cell.
  1228. * @param {number} value - the new value to replace the old one
  1229. * @private
  1230. */
  1231. onCellDataChange: function onCellDataChange(rowIndex, colIndex, value) {
  1232. var $td = this.$table.find('td[' + ATTRIBUTES.ROW_INDEX + '="' + rowIndex + '"][' + ATTRIBUTES.COLUMN_INDEX + '="' + colIndex + '"]');
  1233. if ($td && $td.length && $td.is(':visible') && this.options.setValue) {
  1234. this.options.setValue($td, value, rowIndex, colIndex);
  1235. }
  1236. },
  1237. /**
  1238. * Updates the cell size based on the largest width/height of a cell in the same row/column
  1239. * @param {object} td - a jQuery object represeting the table cell
  1240. * @private
  1241. */
  1242. updateCellSize: function updateCellSize($td) {
  1243. var colIndex = $td.attr(ATTRIBUTES.COLUMN_INDEX);
  1244. var rowIndex = $td.attr(ATTRIBUTES.ROW_INDEX);
  1245. var width = this.horizontalScrollConfig.stats.map[colIndex];
  1246. if (width) {
  1247. //var borderleftSize = parseInt($td.css('border-left-width'), 10);
  1248. $td.css('min-width', width + 'px');
  1249. }
  1250. var height = this.verticalScrollConfig.stats.map[rowIndex];
  1251. if (height) {
  1252. // var borderTopSize = parseInt($td).css('border-top-width'), 10);
  1253. $td.css('height', height + 'px');
  1254. }
  1255. },
  1256. /**
  1257. * @private
  1258. */
  1259. resetTableGroups: function resetTableGroups($table) {
  1260. var $rows = $table.find('tr');
  1261. for (var index = $rows.length - 1; index >= 0; index--) {
  1262. var $tr = $rows.eq(index);
  1263. var $columns = $tr.find('td');
  1264. var $td;
  1265. // set the values and the formats
  1266. for (var i = 0; i < $columns.length; i++) {
  1267. $td = $columns.eq(i);
  1268. this.resetGroupSettings($td);
  1269. }
  1270. }
  1271. },
  1272. /**
  1273. * @private
  1274. */
  1275. updateTableGroups: function updateTableGroups($table) {
  1276. var $rows = $table.find('tr');
  1277. for (var index = $rows.length - 1; index >= 0; index--) {
  1278. var $tr = $rows.eq(index);
  1279. var $columns = $tr.find('td');
  1280. var $td;
  1281. // set the values and the formats
  1282. for (var i = 0; i < $columns.length; i++) {
  1283. $td = $columns.eq(i);
  1284. this.renderGroup($td, i);
  1285. }
  1286. }
  1287. },
  1288. /**
  1289. * @private
  1290. */
  1291. resetGroupSettings: function resetGroupSettings($td) {
  1292. if (this.isGroupEnabled) {
  1293. var fontSize = $td.attr('_fontSize') ? $td.attr('_fontSize') : $td.css('font-size');
  1294. $td.attr({
  1295. 'colspan': null,
  1296. 'rowspan': null,
  1297. '_colspan': null,
  1298. '_rowspan': null,
  1299. '_fontSize': null
  1300. });
  1301. $td.css({
  1302. 'display': '',
  1303. 'visibility': '',
  1304. 'font-size': fontSize
  1305. });
  1306. }
  1307. },
  1308. /**
  1309. * @private
  1310. */
  1311. renderGroup: function renderGroup($td, i) {
  1312. if (this.isGroupEnabled) {
  1313. var group = $td.attr('data-group');
  1314. if (group) {
  1315. var $prev = $td.prev();
  1316. var prevGroup = $prev.attr('data-group');
  1317. if (prevGroup === group) {
  1318. var colspan = $prev.attr('colspan');
  1319. $td.attr('colspan', 1 + parseInt(colspan, 10));
  1320. $td.attr('rowspan', $prev.attr('rowspan'));
  1321. $prev.css('display', 'none');
  1322. } else {
  1323. $td.attr('colspan', '1');
  1324. }
  1325. var $next = $td.parent().next().children().eq(i);
  1326. prevGroup = $next.attr('data-group');
  1327. if (prevGroup === group) {
  1328. var rowspan = $next.attr('rowspan');
  1329. $td.attr('rowspan', 1 + parseInt(rowspan, 10));
  1330. $td.attr('colspan', $next.attr('colspan'));
  1331. $next.css('display', 'none');
  1332. } else {
  1333. $td.attr('rowspan', '1');
  1334. }
  1335. }
  1336. }
  1337. },
  1338. /**
  1339. * @private
  1340. */
  1341. recordCellStats: function recordCellStats($td, config, index) {
  1342. var width = this.getCellSize($td, config);
  1343. var minWidth = config.stats.map[index] || 0;
  1344. if (width > minWidth) {
  1345. this.addCellStat(config.stats, width, index, config.count);
  1346. return width;
  1347. }
  1348. },
  1349. /**
  1350. * Returns the size of a cell
  1351. * @param {object} td - the cell
  1352. * @param {object} config - the horizontal/vertical config object for the cell
  1353. * @private
  1354. */
  1355. getCellSize: function getCellSize($td, config) {
  1356. return config.sizeFunction($td);
  1357. },
  1358. /**
  1359. * @private
  1360. */
  1361. getScrollIncrement: function getScrollIncrement(scrollOffset, config) {
  1362. var index = 0;
  1363. var stats = config.stats;
  1364. var stopAt = config.count;
  1365. var currentOffset = scrollOffset;
  1366. var rendered = config.renderedCount;
  1367. var step;
  1368. var steps = 0;
  1369. var padding = 0;
  1370. while (index < stopAt - rendered && currentOffset > 0) {
  1371. step = stats.map[index];
  1372. step = step ? step : stats.maxSize;
  1373. currentOffset -= step;
  1374. index++;
  1375. if (currentOffset >= 0) {
  1376. padding += step;
  1377. steps++;
  1378. }
  1379. }
  1380. return {
  1381. steps: steps,
  1382. padding: padding
  1383. };
  1384. },
  1385. /**
  1386. * @private
  1387. */
  1388. addCellStat: function addCellStat(stat, size, index, cellCount) {
  1389. var current = stat.map[index];
  1390. if (current && size <= current) {
  1391. return;
  1392. }
  1393. stat.map[index] = size;
  1394. stat.totalRecordedSize += size;
  1395. if (current) {
  1396. stat.totalRecordedSize -= current;
  1397. } else {
  1398. stat.count++;
  1399. }
  1400. stat.minSize = stat.minSize ? Math.min(stat.minSize, size) : size;
  1401. stat.maxSize = Math.max(stat.maxSize || 0, size);
  1402. stat.totalSize = stat.totalRecordedSize + (cellCount - stat.count) * stat.maxSize;
  1403. stat.averageSize = stat.totalSize / cellCount;
  1404. },
  1405. /**
  1406. * Renders the table row by row
  1407. * @private
  1408. */
  1409. renderTable: function renderTable() {
  1410. var currentRow = this.verticalScrollConfig.getScrolledIdx();
  1411. var $tr = null;
  1412. var bottomOffset = 0;
  1413. this.horizontalScrollConfig.renderedCount = 0;
  1414. this.verticalScrollConfig.renderedCount = 0;
  1415. if (this.$el.is(':hidden')) {
  1416. return false;
  1417. }
  1418. do {
  1419. $tr = this.appendRow(this.$table, currentRow, false, this.horizontalScrollConfig.renderedCount);
  1420. if ($tr) {
  1421. if ($tr.is(':hidden')) {
  1422. return false;
  1423. }
  1424. this.verticalScrollConfig.renderedCount++;
  1425. if (!this.horizontalScrollConfig.renderedCount) {
  1426. this.horizontalScrollConfig.renderedCount = $tr.children().length;
  1427. }
  1428. currentRow++;
  1429. if (this.isOutOfViewFromBottom($tr)) {
  1430. bottomOffset++;
  1431. }
  1432. if (bottomOffset === this.offsetCount) {
  1433. break;
  1434. }
  1435. }
  1436. } while ($tr);
  1437. return true;
  1438. },
  1439. /**
  1440. * Renders the pinned column headers
  1441. * @private
  1442. */
  1443. renderPinnedColumnHeaders: function renderPinnedColumnHeaders() {
  1444. for (var i = 0; i < this.fixedRows; i++) {
  1445. this.appendRow(this.$columnHeadersTable, i, false, this.horizontalScrollConfig.renderedCount);
  1446. }
  1447. },
  1448. /**
  1449. * Renders the pineed row headers
  1450. * @private
  1451. */
  1452. renderPinnedRowHeaders: function renderPinnedRowHeaders() {
  1453. var $tr = void 0;
  1454. var scrolledVerticalIdx = this.verticalScrollConfig.getScrolledIdx();
  1455. for (var i = scrolledVerticalIdx; i < scrolledVerticalIdx + this.verticalScrollConfig.renderedCount; i++) {
  1456. if (i === this.verticalScrollConfig.count) {
  1457. break;
  1458. }
  1459. $tr = $('<tr>');
  1460. this.$rowHeadersTable.append($tr);
  1461. $tr.attr(ATTRIBUTES.ROW_INDEX, i);
  1462. for (var j = 0; j < this.fixedColumns; j++) {
  1463. this.appendCell($tr, i, j, false);
  1464. }
  1465. }
  1466. },
  1467. /**
  1468. * Renders the pinned corners
  1469. * @private
  1470. */
  1471. renderPinnedCorner: function renderPinnedCorner() {
  1472. this.$corner = $('<td tabindex="0" style="box-sizing: border-box; min-width: ' + this.minCellWidth + 'px; height: ' + this.minCellHeight + 'px;" col="0" row="0" class="cell corner" data-group="crosstab_corner" colspan="1" rowspan="1">' + this.dataProvider.getValue(0, 0).value + '</td>');
  1473. this.$cornerTable.append($('<tr row=0></tr>'));
  1474. this.$cornerTable.find('tr:first').append(this.$corner);
  1475. },
  1476. /**
  1477. * Append/Prepend a row to the table, and then append/create all the cells for that row
  1478. * @param {object} table - the grid table
  1479. * @param {number} rowIndex - a number representing the row index
  1480. * @param {boolean} isPrepend - a boolean indicating whether to append or prepend the row to the table
  1481. * @param {number} renderColumnCount - number indicating how many columns are rendered to the DOM
  1482. * @private
  1483. */
  1484. appendRow: function appendRow($table, rowIndex, isPrepend, renderColumnCount) {
  1485. var $tr = null;
  1486. if (rowIndex < this.verticalScrollConfig.count) {
  1487. $tr = $('<tr>');
  1488. if (isPrepend) {
  1489. $table.prepend($tr);
  1490. } else {
  1491. $table.append($tr);
  1492. }
  1493. $tr.attr(ATTRIBUTES.ROW_INDEX, rowIndex);
  1494. var $td;
  1495. var colIndex = this.horizontalScrollConfig.getScrolledIdx();
  1496. var rightOffset = 0;
  1497. for (var i = colIndex; i < this.horizontalScrollConfig.count; i++) {
  1498. $td = this.appendCell($tr, rowIndex, i);
  1499. if (!renderColumnCount) {
  1500. if (this.isOutOfViewFromRight($td)) {
  1501. rightOffset++;
  1502. }
  1503. if (rightOffset === this.offsetCount) {
  1504. break;
  1505. }
  1506. } else {
  1507. if (renderColumnCount === i - this.horizontalScrollConfig.getScrolledIdx() + 1) {
  1508. break;
  1509. }
  1510. }
  1511. }
  1512. }
  1513. return $tr;
  1514. },
  1515. /**
  1516. * Create the actual cell, and then set the cell's minimum width
  1517. * @private
  1518. */
  1519. createCell: function createCell() {
  1520. var $td = $('<td tabindex="0" style="box-sizing: border-box">');
  1521. this.setCellMinWidth($td);
  1522. return $td;
  1523. },
  1524. /**
  1525. * Set's the cell's minimum width in pixels
  1526. * @param {object} td - the cell
  1527. * @private
  1528. */
  1529. setCellMinWidth: function setCellMinWidth($td) {
  1530. $td.css({
  1531. 'min-width': this.minCellWidth + 'px',
  1532. 'height': this.minCellHeight + 'px'
  1533. });
  1534. },
  1535. /**
  1536. * Create the actual cell, set the cell's minimum width, and then append/prepend it to the table
  1537. * @param {object} tr - the row in the table to append/prepend the cell to
  1538. * @param {number} rowIndex - number representing the rowIndex of the cell
  1539. * @param {number} colIndex - number representing the colIndex of the cell
  1540. * @param {boolean} isPrepend - a boolean representing whether to prepend the cell to the given row, or to append it to the row
  1541. * @private
  1542. */
  1543. appendCell: function appendCell($tr, rowIndex, colIndex, isPrepend) {
  1544. var $td = this.createCell(rowIndex, colIndex);
  1545. if (isPrepend) {
  1546. $tr.prepend($td);
  1547. } else {
  1548. $tr.append($td);
  1549. }
  1550. return $td;
  1551. },
  1552. _getPositionFromTop: function _getPositionFromTop($el) {
  1553. var top = 0;
  1554. $el.prevAll().each(function () {
  1555. top += $(this).outerHeight();
  1556. });
  1557. return top;
  1558. },
  1559. _getPositionFromLeft: function _getPositionFromLeft($el) {
  1560. var left = 0;
  1561. $el.prevAll().each(function () {
  1562. left += $(this).outerWidth();
  1563. });
  1564. return left;
  1565. },
  1566. /**
  1567. * Function that returns whether or not the element is out of view from the top
  1568. * @param {object} el - a jQuery object
  1569. * @private
  1570. */
  1571. isOutOfViewFromTop: function isOutOfViewFromTop($el) {
  1572. return this._getPositionFromTop($el) + $el.outerHeight() < 0;
  1573. },
  1574. /**
  1575. * Function that returns whether or not the element is out of view from the bottom
  1576. * @param {object} el - a jQuery object
  1577. * @private
  1578. */
  1579. isOutOfViewFromBottom: function isOutOfViewFromBottom($el) {
  1580. return this._getPositionFromTop($el) > this.$scrollableArea.height();
  1581. },
  1582. /**
  1583. * Function that returns whether or not the element is out of view from the right
  1584. * @param {object} el - a jQuery object
  1585. * @private
  1586. */
  1587. isOutOfViewFromRight: function isOutOfViewFromRight($el) {
  1588. return this._getPositionFromLeft($el) > this.$scrollableArea.width();
  1589. },
  1590. /**
  1591. * Function that returns whether or not the element is out of view from the left
  1592. * @param {object} el - a jQuery object
  1593. * @private
  1594. */
  1595. isOutOfViewFromLeft: function isOutOfViewFromLeft($el) {
  1596. return this._getPositionFromLeft($el) + $el.outerWidth() < 0;
  1597. },
  1598. getRowValues: function getRowValues(rowIndex) {
  1599. var rowData = [];
  1600. var nodes = this.getVisibleRowNodes(rowIndex);
  1601. for (var i = 0; i < nodes.length; i++) {
  1602. var $cell = $(nodes[i]);
  1603. rowData.push($cell.text());
  1604. }
  1605. return rowData;
  1606. },
  1607. //Returns an array of tds for the given row index. If there is grouping, it returns
  1608. //the visible td (which may be on a different row)
  1609. getVisibleRowNodes: function getVisibleRowNodes(rowIndex) {
  1610. var nodes = this.$rows.find('[' + ATTRIBUTES.ROW_INDEX + '="' + rowIndex + '"]');
  1611. for (var i = 0; i < nodes.length; i++) {
  1612. var $cell = $(nodes[i]);
  1613. if (!$cell.is(':visible')) {
  1614. var datagroup = $cell.attr('data-group');
  1615. if (!datagroup || !datagroup.length) {
  1616. continue;
  1617. }
  1618. var element = this._getPreviousClosestDatagroupElement($cell, datagroup);
  1619. nodes.splice(i, 1, element[0]);
  1620. }
  1621. }
  1622. return nodes;
  1623. },
  1624. getDisplayedRows: function getDisplayedRows() {
  1625. var a_rowCells = [];
  1626. this.$rows.each(function (idx, row) {
  1627. a_rowCells.push($(row).find('td'));
  1628. }.bind(this));
  1629. return a_rowCells;
  1630. },
  1631. /** return a visible element that matches the given datagroup in the previous rows.
  1632. * @private
  1633. */
  1634. _getPreviousClosestDatagroupElement: function _getPreviousClosestDatagroupElement($srcElement, datagroup) {
  1635. var allPreviousRows = $srcElement.parent().prevAll('tr');
  1636. if (!allPreviousRows.length) {
  1637. return null;
  1638. }
  1639. var prevClosestVisibleDatagroupElement = allPreviousRows.find('td[data-group="' + datagroup + '"]:visible');
  1640. if (!prevClosestVisibleDatagroupElement.length) {
  1641. return null;
  1642. }
  1643. return prevClosestVisibleDatagroupElement.last();
  1644. },
  1645. _targetIsOnTheFirstRowAfterFixedRowHeadersTable: function _targetIsOnTheFirstRowAfterFixedRowHeadersTable($target) {
  1646. return Number($target.attr(ATTRIBUTES.ROW_INDEX)) === this.fixedRows;
  1647. },
  1648. _moveUpToColumnHeadersTableOrCornerTable: function _moveUpToColumnHeadersTableOrCornerTable($target) {
  1649. if ($target.closest('table.gridRowHeaders').length) {
  1650. this.setInitialFocus();
  1651. return;
  1652. }
  1653. var position = this._getCurrentTargetPosition($target);
  1654. var columnPosition = position.column + 1;
  1655. var newRowPosition = position.row;
  1656. var previousRowElement = this.$columnHeadersTable.find('tr:nth-child(' + newRowPosition + ') td:nth-child(' + columnPosition + ')');
  1657. if (previousRowElement.length) {
  1658. this._setFocus(previousRowElement, 'up');
  1659. }
  1660. },
  1661. _moveUp: function _moveUp($target) {
  1662. //:nth-child() is 1-based so need to add 1
  1663. var columnIdx = $target.index() + 1;
  1664. var previousRow = $target.parent().prev('tr');
  1665. if (!previousRow.length) {
  1666. var bTargetIsAtTheTopOfGrid = Number($target.attr(ATTRIBUTES.ROW_INDEX)) === 0;
  1667. if (bTargetIsAtTheTopOfGrid) {
  1668. return;
  1669. }
  1670. var index = this.verticalScrollConfig.getScrolledIdx() - 1;
  1671. this._addColumnOrRowAndSetFocus(this.verticalScrollConfig, index, $target, this._moveUp.bind(this));
  1672. return;
  1673. }
  1674. if (this._targetIsOnTheFirstRowAfterFixedRowHeadersTable($target)) {
  1675. this._moveUpToColumnHeadersTableOrCornerTable($target);
  1676. return;
  1677. }
  1678. var previousRowElement = previousRow.find('td:nth-child(' + columnIdx + ')');
  1679. if (previousRowElement.length > 0 && previousRowElement.is(':visible')) {
  1680. this._setFocus(previousRowElement, 'up');
  1681. return;
  1682. }
  1683. //previous row element that is in the same column is not visible, see if it is in a different column
  1684. var newDatagroup = previousRowElement.attr('data-group');
  1685. //get next visible item in same newDatagroup in same row
  1686. var visibleDatagroupItem = previousRowElement.nextAll('td[data-group="' + newDatagroup + '"]:visible:first');
  1687. if (visibleDatagroupItem && visibleDatagroupItem.length) {
  1688. visibleDatagroupItem.focus();
  1689. return;
  1690. }
  1691. // no visible item found in same row, try previous rows
  1692. previousRowElement = this._getPreviousClosestDatagroupElement(previousRowElement, newDatagroup);
  1693. if (previousRowElement && previousRowElement.length) {
  1694. this._setFocus(previousRowElement, 'up');
  1695. }
  1696. },
  1697. _moveDownToGridFromColumnHeaders: function _moveDownToGridFromColumnHeaders($target, columnIdx) {
  1698. //to get to the next row, add 1 and another 1 as row is 0 based but nth-child is 1-based
  1699. var rowIdx = $target.parent().index() + 2;
  1700. var nextRowElement = this.$table.find('tr:nth-child(' + rowIdx + ') td:nth-child(' + columnIdx + ')');
  1701. if (nextRowElement.length) {
  1702. nextRowElement.focus();
  1703. }
  1704. },
  1705. _moveDown: function _moveDown($target) {
  1706. var columnIdx = $target.index() + 1;
  1707. var colSpan = Number($target.attr('colSpan'));
  1708. if (colSpan > 1) {
  1709. //if colspan is > 1, then on the next row, we want to focus on the first element in the span
  1710. columnIdx = columnIdx - colSpan + 1;
  1711. }
  1712. var oldDatagroup = $target.attr('data-group');
  1713. var nextRows = $target.parent().nextAll('tr');
  1714. if (!nextRows.length) {
  1715. var bTargetIsOnColumnHeaderTable = $target.closest('table.gridColumnHeaders').length > 0;
  1716. if (bTargetIsOnColumnHeaderTable) {
  1717. this._moveDownToGridFromColumnHeaders($target, columnIdx);
  1718. return;
  1719. }
  1720. //the corner table cell's row attribute does not accurately reflect the
  1721. var bTargetIsOnCornerTable = $target.closest('table.cornerTable').length > 0;
  1722. if (bTargetIsOnCornerTable) {
  1723. //setup the nextRows for moving from cornerTable to rowHeadersTable
  1724. nextRows = this.$rowHeadersTable.find('tr');
  1725. }
  1726. }
  1727. //handles moving down on nested row header
  1728. var nextRowElement = nextRows.find('td[data-group!="' + oldDatagroup + '"]:nth-child(' + columnIdx + ')').first();
  1729. var newDatagroup;
  1730. if (nextRowElement.length) {
  1731. if (nextRowElement.is(':visible')) {
  1732. nextRowElement.focus();
  1733. return;
  1734. }
  1735. newDatagroup = nextRowElement.attr('data-group');
  1736. }
  1737. //handles moving down on nested column headers
  1738. //next row element in the same column idx is not visible, see if it is in a different column
  1739. var sameDatagropuItemInRow = nextRowElement.nextAll('td[data-group="' + newDatagroup + '"]:visible:first');
  1740. if (sameDatagropuItemInRow && sameDatagropuItemInRow.length > 0) {
  1741. sameDatagropuItemInRow.focus();
  1742. return;
  1743. }
  1744. //if there is no more visible item, check if all the rows had been retrieved
  1745. var rowIndex = Number(nextRowElement.attr(ATTRIBUTES.ROW_INDEX));
  1746. var totalNumberOfRows = this.verticalScrollConfig.count;
  1747. if (rowIndex < totalNumberOfRows) {
  1748. return;
  1749. }
  1750. //if we get here, it means that the new data-group is not fully rendered so get more rows
  1751. var index = this.verticalScrollConfig.getScrolledIdx() + 1;
  1752. this._addColumnOrRowAndSetFocus(this.verticalScrollConfig, index, $target, this._moveDown.bind(this));
  1753. },
  1754. _moveRightFromCornerTableToColumnHeader: function _moveRightFromCornerTableToColumnHeader($target, oldDatagroup) {
  1755. var firstCellElement = this.$columnHeadersTable.find('tr:visible:first td:first');
  1756. var nextElement = firstCellElement.nextAll('td[data-group!="' + oldDatagroup + '"]:visible:first');
  1757. if (nextElement.length) {
  1758. nextElement.focus();
  1759. }
  1760. },
  1761. _getCurrentTargetPosition: function _getCurrentTargetPosition($target) {
  1762. var colPosition = $target.index();
  1763. var rowPosition = $target.parent().index();
  1764. return {
  1765. 'column': colPosition,
  1766. 'row': rowPosition
  1767. };
  1768. },
  1769. _moveRightFromRowHeadersToGrid: function _moveRightFromRowHeadersToGrid($target) {
  1770. var position = this._getCurrentTargetPosition($target);
  1771. var columnIdx = position.column + 2;
  1772. var rowIdx = position.row + 1;
  1773. var nextColumnElement = this.$table.find('tr:nth-child(' + rowIdx + ') td:nth-child(' + columnIdx + ')');
  1774. if (nextColumnElement.length && nextColumnElement.is(':visible')) {
  1775. nextColumnElement.focus();
  1776. return;
  1777. }
  1778. var datagroup = nextColumnElement.attr('data-group');
  1779. nextColumnElement.find('td[data-group="' + datagroup + '"]:visible:first');
  1780. if (nextColumnElement.length) {
  1781. nextColumnElement.focus();
  1782. }
  1783. },
  1784. _targetIsOnLastColumnInRowHeadersTable: function _targetIsOnLastColumnInRowHeadersTable($target) {
  1785. if (!$target.closest('table.gridRowHeaders').length) {
  1786. return false;
  1787. }
  1788. var columnPosition = Number($target.attr(ATTRIBUTES.COLUMN_INDEX)) + 1;
  1789. return columnPosition === this.fixedColumns;
  1790. },
  1791. _moveRight: function _moveRight($target) {
  1792. var bTargetIsOnCornerTable = $target.closest('table.cornerTable').length > 0;
  1793. var oldDatagroup = $target.attr('data-group');
  1794. if (bTargetIsOnCornerTable) {
  1795. this._moveRightFromCornerTableToColumnHeader($target, oldDatagroup);
  1796. return;
  1797. }
  1798. var nextElement;
  1799. if (this._targetIsOnLastColumnInRowHeadersTable($target)) {
  1800. this._moveRightFromRowHeadersToGrid($target);
  1801. return;
  1802. }
  1803. nextElement = $target.nextAll('td:visible:first');
  1804. if (nextElement.length) {
  1805. nextElement.focus();
  1806. return;
  1807. }
  1808. //There are no visible item to focus on, so check if there's a different data-group following $target
  1809. var nextGroupDataElement = $target.nextAll('td[data-group!="' + oldDatagroup + '"]:first');
  1810. if (!nextGroupDataElement.length) {
  1811. var colIndex = Number($target.attr(ATTRIBUTES.COLUMN_INDEX)) + 1;
  1812. var totalNumberOfColumns = this.horizontalScrollConfig.count;
  1813. if (colIndex === totalNumberOfColumns) {
  1814. return;
  1815. }
  1816. //There isn't a different datagroup and no visible element, so the current datagroup must be partially rendered,
  1817. //so get more data
  1818. var index = this.horizontalScrollConfig.getScrolledIdx() + 1;
  1819. this._addColumnOrRowAndSetFocus(this.horizontalScrollConfig, index, $target, this._moveRight.bind(this));
  1820. return;
  1821. }
  1822. var newDatagroup = nextGroupDataElement.attr('data-group');
  1823. nextElement = this._getPreviousClosestDatagroupElement(nextGroupDataElement, newDatagroup);
  1824. if (nextElement && nextElement.length) {
  1825. nextElement.focus();
  1826. return;
  1827. }
  1828. nextElement = this.$rowHeadersTable.nextAll('td[data-group!="' + oldDatagroup + '"]:first');
  1829. if (nextElement.length) {
  1830. nextElement.focus();
  1831. }
  1832. },
  1833. _addColumnOrRowAndSetFocus: function _addColumnOrRowAndSetFocus(config, index, $target, setFocusFunction) {
  1834. if (typeof setFocusFunction !== 'function') {
  1835. return;
  1836. }
  1837. if (!this.updateIndexAndValues(index, config)) {
  1838. return;
  1839. }
  1840. var colIdx = $target.attr(ATTRIBUTES.COLUMN_INDEX);
  1841. var rowIdx = $target.attr(ATTRIBUTES.ROW_INDEX);
  1842. //find the target node in the updated table
  1843. var table = $target.closest('table');
  1844. var currentElement = table.find('td[col="' + colIdx + '"][row="' + rowIdx + '"]');
  1845. if (!currentElement.length) {
  1846. return;
  1847. }
  1848. setFocusFunction(currentElement);
  1849. },
  1850. _moveLeftToRowHeadersTable: function _moveLeftToRowHeadersTable($target) {
  1851. var position = this._getCurrentTargetPosition($target);
  1852. var rowPosition = position.row + 1; //nth-child is 1-based
  1853. var newColumnPosition = position.column; // column preceding
  1854. var previousColumnElement = this.$rowHeadersTable.find('tr:nth-child(' + rowPosition + ') td:nth-child(' + newColumnPosition + ')');
  1855. previousColumnElement.focus();
  1856. },
  1857. _moveLeftToRowHeadersOrCornerTable: function _moveLeftToRowHeadersOrCornerTable($target) {
  1858. var bTargetIsOnColumnHeaderTable = $target.closest('table.gridColumnHeaders').length > 0;
  1859. if (bTargetIsOnColumnHeaderTable) {
  1860. this.setInitialFocus();
  1861. return;
  1862. }
  1863. this._moveLeftToRowHeadersTable($target);
  1864. },
  1865. _targetIsOnColumnOrDataGroupBeforeRowHeadersTable: function _targetIsOnColumnOrDataGroupBeforeRowHeadersTable($target) {
  1866. var colSpan = $target.attr('colspan');
  1867. if (colSpan) {
  1868. colSpan = Number(colSpan);
  1869. }
  1870. var columnIndex = $target.index();
  1871. var columnIdxToUse;
  1872. if (colSpan && colSpan > 1) {
  1873. columnIdxToUse = columnIndex - colSpan + 1;
  1874. } else {
  1875. columnIdxToUse = columnIndex;
  1876. }
  1877. return this.fixedColumns && columnIdxToUse === this.fixedColumns;
  1878. },
  1879. _moveLeft: function _moveLeft($target) {
  1880. var bTargetIsOnFirstColumn = Number($target.attr(ATTRIBUTES.COLUMN_INDEX)) === 0;
  1881. if (bTargetIsOnFirstColumn) {
  1882. return;
  1883. }
  1884. if (this._targetIsOnColumnOrDataGroupBeforeRowHeadersTable($target)) {
  1885. this._moveLeftToRowHeadersOrCornerTable($target);
  1886. return;
  1887. }
  1888. var previousElement = $target.prevAll('td:visible:first');
  1889. if (previousElement.length) {
  1890. this._setFocus(previousElement, 'left');
  1891. return;
  1892. }
  1893. //There are no visible element to focus on, so check if there is a different datagroup preceding $target
  1894. var oldDatagroup = $target.attr('data-group');
  1895. var previousGroupDataElement = $target.prevAll('td[data-group!="' + oldDatagroup + '"]:first');
  1896. if (!previousGroupDataElement.length) {
  1897. var colIndex = Number($target.attr(ATTRIBUTES.COLUMN_INDEX));
  1898. if (colIndex === 0) {
  1899. return;
  1900. }
  1901. // there isn't a different datagroup, therefore, the current datagroup must be only partially rendered
  1902. // so get more data
  1903. this._addColumnOrRowAndSetFocus(this.horizontalScrollConfig, this.horizontalScrollConfig.getScrolledIdx() - 1, $target, this._moveLeft.bind(this));
  1904. return;
  1905. }
  1906. // There is a different datagroup but since there is no visible element on the same row, it means that the visible element must be on one of the
  1907. // previous rows
  1908. var newDatagroup = previousGroupDataElement.attr('data-group');
  1909. previousElement = this._getPreviousClosestDatagroupElement(previousGroupDataElement, newDatagroup);
  1910. if (previousElement && previousElement.length > 0) {
  1911. this._setFocus(previousElement, 'left');
  1912. return;
  1913. }
  1914. },
  1915. _setFocus: function _setFocus($nodeToFocus, moveDirection) {
  1916. $nodeToFocus.focus();
  1917. var nodeToFocusRect = $nodeToFocus[0].getBoundingClientRect();
  1918. switch (moveDirection) {
  1919. case 'left':
  1920. {
  1921. var rowHeadersTableRect = this.$rowHeadersTable[0].getBoundingClientRect();
  1922. if (nodeToFocusRect.left + 1 > rowHeadersTableRect.right) {
  1923. return;
  1924. }
  1925. var leftScrollAmount = rowHeadersTableRect.width - (nodeToFocusRect.left - rowHeadersTableRect.left);
  1926. this.$scrollableArea.scrollLeft(this.$scrollableArea.scrollLeft() - leftScrollAmount);
  1927. break;
  1928. }
  1929. case 'up':
  1930. {
  1931. var columnHeadersTable = this.$columnHeadersTable[0].getBoundingClientRect();
  1932. if (nodeToFocusRect.top + 1 > columnHeadersTable.bottom) {
  1933. return;
  1934. }
  1935. var topScrollAmount = columnHeadersTable.height - (nodeToFocusRect.top - columnHeadersTable.top);
  1936. this.$scrollableArea.scrollTop(this.$scrollableArea.scrollTop() - topScrollAmount);
  1937. break;
  1938. }
  1939. }
  1940. },
  1941. _moveToFirstRow: function _moveToFirstRow() {
  1942. this.setInitialFocus();
  1943. },
  1944. _moveToLastRow: function _moveToLastRow() {
  1945. this.$table.find('tr:visible:last td:visible:last').focus();
  1946. },
  1947. onKeyDown: function onKeyDown(event) {
  1948. var $target = $(event.target);
  1949. var bStopPropagation = false;
  1950. switch (event.keyCode) {
  1951. case KeyCodes.KEY_UP:
  1952. {
  1953. bStopPropagation = true;
  1954. this._moveUp($target);
  1955. break;
  1956. }
  1957. case KeyCodes.KEY_DOWN:
  1958. {
  1959. bStopPropagation = true;
  1960. this._moveDown($target);
  1961. break;
  1962. }
  1963. case KeyCodes.KEY_RIGHT:
  1964. {
  1965. bStopPropagation = true;
  1966. this._moveRight($target);
  1967. break;
  1968. }
  1969. case KeyCodes.KEY_LEFT:
  1970. {
  1971. bStopPropagation = true;
  1972. this._moveLeft($target);
  1973. break;
  1974. }
  1975. case KeyCodes.KEY_h:
  1976. case KeyCodes.KEY_HOME:
  1977. {
  1978. if (!event.ctrlKey) {
  1979. return;
  1980. }
  1981. bStopPropagation = true;
  1982. this._moveToFirstRow();
  1983. break;
  1984. }
  1985. case KeyCodes.KEY_e:
  1986. case KeyCodes.KEY_END:
  1987. {
  1988. if (!event.ctrlKey) {
  1989. return;
  1990. }
  1991. bStopPropagation = true;
  1992. this._moveToLastRow();
  1993. break;
  1994. }
  1995. }
  1996. if (bStopPropagation) {
  1997. this._savedKeyboardNavigationInfo = {
  1998. 'keycode': event.keyCode,
  1999. '$target': $target
  2000. };
  2001. event.preventDefault();
  2002. event.stopPropagation();
  2003. }
  2004. },
  2005. setInitialFocus: function setInitialFocus() {
  2006. var startingTable = this.bHasCornerTable ? this.$cornerTable : this.$columnHeadersTable;
  2007. var initialNode = startingTable.find('tr:visible:first td:visible:first');
  2008. if (initialNode.length) {
  2009. initialNode.focus();
  2010. }
  2011. },
  2012. _setFocusAfterScroll: function _setFocusAfterScroll() {
  2013. if (!this._savedKeyboardNavigationInfo) {
  2014. return;
  2015. }
  2016. switch (this._savedKeyboardNavigationInfo.keycode) {
  2017. case KeyCodes.KEY_UP:
  2018. this._moveUp(this._savedKeyboardNavigationInfo.$target);
  2019. break;
  2020. case KeyCodes.KEY_DOWN:
  2021. this._moveDown(this._savedKeyboardNavigationInfo.$target);
  2022. break;
  2023. case KeyCodes.KEY_RIGHT:
  2024. this._moveRight(this._savedKeyboardNavigationInfo.$target);
  2025. break;
  2026. case KeyCodes.KEY_LEFT:
  2027. this._moveLeft(this._savedKeyboardNavigationInfo.$target);
  2028. break;
  2029. case KeyCodes.KEY_e:
  2030. case KeyCodes.KEY_END:
  2031. this._moveToLastRow();
  2032. break;
  2033. case KeyCodes.KEY_h:
  2034. case KeyCodes.KEY_HOME:
  2035. this._moveToFirstRow();
  2036. break;
  2037. }
  2038. },
  2039. /**
  2040. * Update the specified table when scrollled vertically
  2041. *
  2042. * @param {object} options - the options to scroll the table vertically
  2043. *
  2044. */
  2045. _updateTableScrolledVertically: function _updateTableScrolledVertically() {
  2046. var _this6 = this;
  2047. var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  2048. var $table = options.$table;
  2049. var previousScrolledIdx = options.previousScrolledIdx;
  2050. var delta = options.delta;
  2051. var handleGrouping = options.handleGrouping || false;
  2052. var isRowHeader = options.isRowHeader || false;
  2053. var scrolledVerticalIdx = this.verticalScrollConfig.getScrolledIdx();
  2054. var scrollDown = previousScrolledIdx < scrolledVerticalIdx;
  2055. var $rows = $table.find('tr');
  2056. var $trAfter = delta > 0 ? $rows.eq(delta - 1) : null;
  2057. var count = scrollDown ? scrolledVerticalIdx - previousScrolledIdx - 1 : previousScrolledIdx - scrolledVerticalIdx - 1;
  2058. var $row = void 0;
  2059. var rowContext = void 0;
  2060. var _getRowUpdateContext = function _getRowUpdateContext($row, idx) {
  2061. return {
  2062. $tr: $row,
  2063. rowIndex: scrollDown ? _this6.verticalScrollConfig.renderedCount + scrolledVerticalIdx - 1 - idx : idx + scrolledVerticalIdx + delta,
  2064. handleGrouping: handleGrouping,
  2065. isRowHeader: isRowHeader,
  2066. cbGetScrolledFastPinnedCornerCellIdx: cbGetScrolledFastPinnedCornerCellIdx
  2067. };
  2068. };
  2069. for (var idx = count; idx >= 0; idx--) {
  2070. rowContext = null;
  2071. if (scrollDown) {
  2072. $row = $rows.eq(idx + delta);
  2073. $row.appendTo($rows.parent());
  2074. } else {
  2075. $row = $rows.eq($rows.length - idx - 1);
  2076. if ($trAfter) {
  2077. $row.insertAfter($trAfter);
  2078. } else {
  2079. $row.prependTo($rows.parent());
  2080. }
  2081. }
  2082. rowContext = _getRowUpdateContext($row, idx);
  2083. this.updateRow(rowContext);
  2084. }
  2085. }
  2086. });
  2087. View.getColumnPosition = function ($tdNode) {
  2088. return Number($tdNode.attr(ATTRIBUTES.COLUMN_INDEX));
  2089. };
  2090. View.getRowPosition = function ($tdNode) {
  2091. return Number($tdNode.attr(ATTRIBUTES.ROW_INDEX));
  2092. };
  2093. return View;
  2094. });
  2095. //# sourceMappingURL=Grid.js.map