virtual-util.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. var PREVIOUS_CELL = {
  2. row: 0,
  3. width: 0,
  4. height: 0,
  5. top: 0,
  6. left: 0,
  7. tmpl: -1
  8. };
  9. /**
  10. * NO DOM
  11. */
  12. export function processRecords(stopAtHeight, records, cells, headerFn, footerFn, data) {
  13. var record;
  14. var startRecordIndex;
  15. var previousCell;
  16. var tmpData;
  17. var lastRecordIndex = records ? (records.length - 1) : -1;
  18. if (cells.length) {
  19. // we already have cells
  20. previousCell = cells[cells.length - 1];
  21. if (previousCell.top + previousCell.height > stopAtHeight) {
  22. return;
  23. }
  24. startRecordIndex = (previousCell.record + 1);
  25. }
  26. else {
  27. // no cells have been created yet
  28. previousCell = PREVIOUS_CELL;
  29. startRecordIndex = 0;
  30. }
  31. var processedTotal = 0;
  32. for (var recordIndex = startRecordIndex; recordIndex <= lastRecordIndex; recordIndex++) {
  33. record = records[recordIndex];
  34. if (headerFn) {
  35. tmpData = headerFn(record, recordIndex, records);
  36. if (tmpData !== null) {
  37. // add header data
  38. previousCell = addCell(previousCell, recordIndex, 1 /* Header */, tmpData, data.hdrWidth, data.hdrHeight, data.viewWidth);
  39. cells.push(previousCell);
  40. }
  41. }
  42. // add item data
  43. previousCell = addCell(previousCell, recordIndex, 0 /* Item */, null, data.itmWidth, data.itmHeight, data.viewWidth);
  44. cells.push(previousCell);
  45. if (footerFn) {
  46. tmpData = footerFn(record, recordIndex, records);
  47. if (tmpData !== null) {
  48. // add footer data
  49. previousCell = addCell(previousCell, recordIndex, 2 /* Footer */, tmpData, data.ftrWidth, data.ftrHeight, data.viewWidth);
  50. cells.push(previousCell);
  51. }
  52. }
  53. if (previousCell.record === lastRecordIndex) {
  54. previousCell.isLast = true;
  55. }
  56. // should always process at least 3 records
  57. processedTotal++;
  58. if (previousCell.top + previousCell.height + data.itmHeight > stopAtHeight && processedTotal > 3) {
  59. return;
  60. }
  61. }
  62. }
  63. function addCell(previousCell, recordIndex, tmpl, tmplData, cellWidth, cellHeight, viewportWidth) {
  64. var newCell = {
  65. record: recordIndex,
  66. tmpl: tmpl,
  67. width: cellWidth,
  68. height: cellHeight,
  69. reads: 0
  70. };
  71. if (previousCell.left + previousCell.width + cellWidth > viewportWidth) {
  72. // add a new cell in a new row
  73. newCell.row = (previousCell.row + 1);
  74. newCell.top = (previousCell.top + previousCell.height);
  75. newCell.left = 0;
  76. }
  77. else {
  78. // add a new cell in the same row
  79. newCell.row = previousCell.row;
  80. newCell.top = previousCell.top;
  81. newCell.left = (previousCell.left + previousCell.width);
  82. }
  83. if (tmplData) {
  84. newCell.data = tmplData;
  85. }
  86. return newCell;
  87. }
  88. /**
  89. * NO DOM
  90. */
  91. export function populateNodeData(startCellIndex, endCellIndex, scrollingDown, cells, records, nodes, viewContainer, itmTmp, hdrTmp, ftrTmp) {
  92. if (!records || records.length === 0) {
  93. nodes.length = 0;
  94. viewContainer.clear();
  95. return true;
  96. }
  97. var recordsLength = records.length;
  98. var hasChanges = false;
  99. // let node: VirtualNode;
  100. var availableNode;
  101. var cell;
  102. var viewInsertIndex = null;
  103. var totalNodes = nodes.length;
  104. var templateRef;
  105. startCellIndex = Math.max(startCellIndex, 0);
  106. endCellIndex = Math.min(endCellIndex, cells.length - 1);
  107. var usedNodes = [];
  108. for (var cellIndex = startCellIndex; cellIndex <= endCellIndex; cellIndex++) {
  109. cell = cells[cellIndex];
  110. availableNode = null;
  111. // find the first one that's available
  112. var existingNode = nodes.find(function (n) { return n.cell === cellIndex && n.tmpl === cell.tmpl; });
  113. if (existingNode) {
  114. if (existingNode.view.context.$implicit === records[cell.record]) {
  115. usedNodes.push(existingNode);
  116. continue; // optimization: node data is the same no need to update
  117. }
  118. (void 0) /* console.debug */;
  119. availableNode = existingNode; // update existing node
  120. }
  121. else {
  122. (void 0) /* console.debug */;
  123. for (var i = 0; i < totalNodes; i++) {
  124. var node = nodes[i];
  125. if (cell.tmpl !== node.tmpl || i === 0 && cellIndex !== 0) {
  126. // the cell must use the correct template
  127. // first node can only be used by the first cell (css :first-child reasons)
  128. // this node is never available to be reused
  129. continue;
  130. }
  131. if (node.cell < startCellIndex || node.cell > endCellIndex) {
  132. if (!availableNode) {
  133. // havent gotten an available node yet
  134. availableNode = node;
  135. (void 0) /* console.debug */;
  136. }
  137. else if (scrollingDown) {
  138. // scrolling down
  139. if (node.cell < availableNode.cell) {
  140. availableNode = node;
  141. (void 0) /* console.debug */;
  142. }
  143. }
  144. else {
  145. // scrolling up
  146. if (node.cell > availableNode.cell) {
  147. availableNode = node;
  148. (void 0) /* console.debug */;
  149. }
  150. }
  151. }
  152. }
  153. }
  154. if (!availableNode) {
  155. // did not find an available node to put the cell data into
  156. // insert a new node after existing ones
  157. if (viewInsertIndex === null) {
  158. viewInsertIndex = -1;
  159. for (var j = totalNodes - 1; j >= 0; j--) {
  160. var node = nodes[j];
  161. if (node) {
  162. viewInsertIndex = viewContainer.indexOf(node.view);
  163. break;
  164. }
  165. }
  166. }
  167. // select which templateRef should be used for this cell
  168. templateRef = cell.tmpl === 1 /* Header */ ? hdrTmp : cell.tmpl === 2 /* Footer */ ? ftrTmp : itmTmp;
  169. if (!templateRef) {
  170. console.error("virtual" + (cell.tmpl === 1 /* Header */ ? 'Header' : cell.tmpl === 2 /* Footer */ ? 'Footer' : 'Item') + " template required");
  171. continue;
  172. }
  173. availableNode = {
  174. tmpl: cell.tmpl,
  175. view: viewContainer.createEmbeddedView(templateRef, new VirtualContext(null, null, null), viewInsertIndex)
  176. };
  177. totalNodes = nodes.push(availableNode);
  178. }
  179. // assign who's the new cell index for this node
  180. availableNode.cell = cellIndex;
  181. // apply the cell's data to this node
  182. var context = availableNode.view.context;
  183. context.$implicit = cell.data || records[cell.record];
  184. context.index = cellIndex;
  185. context.count = recordsLength;
  186. availableNode.hasChanges = true;
  187. availableNode.lastTransform = null;
  188. hasChanges = true;
  189. usedNodes.push(availableNode);
  190. }
  191. var unusedNodes = nodes.filter(function (n) { return usedNodes.indexOf(n) < 0; });
  192. unusedNodes.forEach(function (node) {
  193. var index = viewContainer.indexOf(node.view);
  194. viewContainer.remove(index);
  195. var removeIndex = nodes.findIndex(function (n) { return n === node; });
  196. nodes.splice(removeIndex, 1);
  197. });
  198. usedNodes.length = 0;
  199. unusedNodes.length = 0;
  200. return hasChanges;
  201. }
  202. /**
  203. * DOM READ
  204. */
  205. export function initReadNodes(plt, nodes, cells, data) {
  206. if (nodes.length && cells.length) {
  207. // first node
  208. // ******** DOM READ ****************
  209. var ele = getElement(nodes[0]);
  210. var firstCell = cells[0];
  211. firstCell.top = ele.clientTop;
  212. firstCell.left = ele.clientLeft;
  213. firstCell.row = 0;
  214. // ******** DOM READ ****************
  215. updateDimensions(plt, nodes, cells, data, true);
  216. }
  217. }
  218. /**
  219. * DOM READ
  220. */
  221. export function updateDimensions(plt, nodes, cells, data, initialUpdate) {
  222. var node;
  223. var element;
  224. var cell;
  225. var previousCell;
  226. var totalCells = cells.length;
  227. for (var i = 0; i < nodes.length; i++) {
  228. node = nodes[i];
  229. cell = cells[node.cell];
  230. // read element dimensions if they haven't been checked enough times
  231. if (cell && cell.reads < REQUIRED_DOM_READS) {
  232. element = getElement(node);
  233. // ******** DOM READ ****************
  234. readElements(plt, cell, element);
  235. if (initialUpdate) {
  236. // update estimated dimensions with more accurate dimensions
  237. if (cell.tmpl === 1 /* Header */) {
  238. data.hdrHeight = cell.height;
  239. if (cell.left === 0) {
  240. data.hdrWidth = cell.width;
  241. }
  242. }
  243. else if (cell.tmpl === 2 /* Footer */) {
  244. data.ftrHeight = cell.height;
  245. if (cell.left === 0) {
  246. data.ftrWidth = cell.width;
  247. }
  248. }
  249. else {
  250. data.itmHeight = cell.height;
  251. if (cell.left === 0) {
  252. data.itmWidth = cell.width;
  253. }
  254. }
  255. }
  256. cell.reads++;
  257. }
  258. }
  259. // figure out which cells are currently viewable within the viewport
  260. var viewableBottom = (data.scrollTop + data.viewHeight);
  261. data.topViewCell = totalCells;
  262. data.bottomViewCell = 0;
  263. if (totalCells > 0) {
  264. // completely realign position to ensure they're all accurately placed
  265. cell = cells[0];
  266. previousCell = {
  267. row: 0,
  268. width: 0,
  269. height: 0,
  270. top: cell.top,
  271. left: 0,
  272. tmpl: -1
  273. };
  274. for (var i_1 = 0; i_1 < totalCells; i_1++) {
  275. cell = cells[i_1];
  276. if (previousCell.left + previousCell.width + cell.width > data.viewWidth) {
  277. // new row
  278. cell.row++;
  279. cell.top = (previousCell.top + previousCell.height);
  280. cell.left = 0;
  281. }
  282. else {
  283. // same row
  284. cell.row = previousCell.row;
  285. cell.top = previousCell.top;
  286. cell.left = (previousCell.left + previousCell.width);
  287. }
  288. // figure out which cells are viewable within the viewport
  289. if (cell.top + cell.height > data.scrollTop && i_1 < data.topViewCell) {
  290. data.topViewCell = i_1;
  291. }
  292. else if (cell.top < viewableBottom && i_1 > data.bottomViewCell) {
  293. data.bottomViewCell = i_1;
  294. }
  295. previousCell = cell;
  296. }
  297. }
  298. }
  299. export function updateNodeContext(nodes, cells, data) {
  300. // ensure each node has the correct bounds in its context
  301. var node;
  302. var cell;
  303. var bounds;
  304. for (var i = 0, ilen = nodes.length; i < ilen; i++) {
  305. node = nodes[i];
  306. cell = cells[node.cell];
  307. if (node && cell) {
  308. bounds = node.view.context.bounds;
  309. bounds.top = cell.top + data.viewTop;
  310. bounds.bottom = bounds.top + cell.height;
  311. bounds.left = cell.left + data.viewLeft;
  312. bounds.right = bounds.left + cell.width;
  313. bounds.width = cell.width;
  314. bounds.height = cell.height;
  315. }
  316. }
  317. }
  318. /**
  319. * DOM READ
  320. */
  321. function readElements(plt, cell, element) {
  322. // ******** DOM READ ****************
  323. var styles = plt.getElementComputedStyle(element);
  324. // ******** DOM READ ****************
  325. cell.left = (element.clientLeft - parseFloat(styles.marginLeft));
  326. // ******** DOM READ ****************
  327. cell.width = (element.offsetWidth + parseFloat(styles.marginLeft) + parseFloat(styles.marginRight));
  328. // ******** DOM READ ****************
  329. cell.height = (element.offsetHeight + parseFloat(styles.marginTop) + parseFloat(styles.marginBottom));
  330. }
  331. /**
  332. * DOM WRITE
  333. */
  334. export function writeToNodes(plt, nodes, cells, totalRecords) {
  335. var node;
  336. var element;
  337. var cell;
  338. var transform;
  339. var totalCells = Math.max(totalRecords, cells.length);
  340. for (var i = 0, ilen = nodes.length; i < ilen; i++) {
  341. node = nodes[i];
  342. cell = cells[node.cell];
  343. transform = "translate3d(" + cell.left + "px," + cell.top + "px,0px)";
  344. if (node.lastTransform !== transform) {
  345. element = getElement(node);
  346. if (element) {
  347. // ******** DOM WRITE ****************
  348. element.style[plt.Css.transform] = node.lastTransform = transform;
  349. // ******** DOM WRITE ****************
  350. element.classList.add('virtual-position');
  351. // https://www.w3.org/TR/wai-aria/states_and_properties#aria-posinset
  352. // ******** DOM WRITE ****************
  353. element.setAttribute('aria-posinset', node.cell + 1);
  354. // https://www.w3.org/TR/wai-aria/states_and_properties#aria-setsize
  355. // ******** DOM WRITE ****************
  356. element.setAttribute('aria-setsize', totalCells);
  357. }
  358. }
  359. }
  360. }
  361. /**
  362. * NO DOM
  363. */
  364. export function adjustRendered(cells, data) {
  365. var maxRenderHeight = (data.renderHeight - data.itmHeight);
  366. var totalCells = cells.length;
  367. var viewableRenderedPadding = (data.itmHeight < 90 ? VIEWABLE_RENDERED_PADDING : 0);
  368. if (data.scrollDiff > 0) {
  369. // scrolling down
  370. data.topCell = Math.max(data.topViewCell - viewableRenderedPadding, 0);
  371. data.bottomCell = data.topCell;
  372. var cellsRenderHeight = 0;
  373. for (var i = data.topCell; i < totalCells; i++) {
  374. cellsRenderHeight += cells[i].height;
  375. if (i > data.bottomCell)
  376. data.bottomCell = i;
  377. if (cellsRenderHeight >= maxRenderHeight)
  378. break;
  379. }
  380. if (cellsRenderHeight < maxRenderHeight) {
  381. // there are no more cells at the bottom, so move topCell to a smaller index
  382. for (var i = data.topCell - 1; i >= 0; i--) {
  383. cellsRenderHeight += cells[i].height;
  384. data.topCell = i;
  385. if (cellsRenderHeight >= maxRenderHeight)
  386. break;
  387. }
  388. }
  389. }
  390. else {
  391. // scroll up
  392. data.bottomCell = Math.min(data.bottomViewCell + viewableRenderedPadding, totalCells - 1);
  393. data.topCell = data.bottomCell;
  394. var cellsRenderHeight = 0;
  395. (void 0) /* assert */;
  396. for (var i = data.bottomCell; i >= 0; i--) {
  397. cellsRenderHeight += cells[i].height;
  398. if (i < data.topCell)
  399. data.topCell = i;
  400. if (cellsRenderHeight >= maxRenderHeight)
  401. break;
  402. }
  403. if (cellsRenderHeight < maxRenderHeight) {
  404. // there are no more cells at the top, so move bottomCell to a higher index
  405. for (var i = data.bottomCell; i < totalCells; i++) {
  406. cellsRenderHeight += cells[i].height;
  407. data.bottomCell = i;
  408. if (cellsRenderHeight >= maxRenderHeight)
  409. break;
  410. }
  411. }
  412. }
  413. }
  414. /**
  415. * NO DOM
  416. */
  417. export function getVirtualHeight(totalRecords, lastCell) {
  418. if (lastCell.record >= totalRecords - 1) {
  419. return (lastCell.top + lastCell.height);
  420. }
  421. var unknownRecords = (totalRecords - lastCell.record - 1);
  422. var knownHeight = (lastCell.top + lastCell.height);
  423. return Math.ceil(knownHeight + ((knownHeight / (totalRecords - unknownRecords)) * unknownRecords));
  424. }
  425. /**
  426. * NO DOM
  427. */
  428. export function estimateHeight(totalRecords, lastCell, existingHeight, difference) {
  429. if (!totalRecords || !lastCell) {
  430. return 0;
  431. }
  432. var newHeight = getVirtualHeight(totalRecords, lastCell);
  433. var percentToBottom = (lastCell.record / (totalRecords - 1));
  434. var diff = Math.abs(existingHeight - newHeight);
  435. if ((diff > (newHeight * difference)) ||
  436. (percentToBottom > .995)) {
  437. return newHeight;
  438. }
  439. return existingHeight;
  440. }
  441. /**
  442. * DOM READ
  443. */
  444. export function calcDimensions(data, virtualScrollElement, approxItemWidth, approxItemHeight, appoxHeaderWidth, approxHeaderHeight, approxFooterWidth, approxFooterHeight, bufferRatio) {
  445. // get the parent container's viewport bounds
  446. var viewportElement = virtualScrollElement.parentElement;
  447. // ******** DOM READ ****************
  448. data.viewWidth = viewportElement.offsetWidth;
  449. // ******** DOM READ ****************
  450. data.viewHeight = viewportElement.offsetHeight;
  451. // get the virtual scroll element's offset data
  452. // ******** DOM READ ****************
  453. data.viewTop = virtualScrollElement.offsetTop;
  454. // ******** DOM READ ****************
  455. data.viewLeft = virtualScrollElement.offsetLeft;
  456. // the height we'd like to render, which is larger than viewable
  457. data.renderHeight = (data.viewHeight * bufferRatio);
  458. if (data.viewWidth > 0 && data.viewHeight > 0) {
  459. data.itmWidth = calcWidth(data.viewWidth, approxItemWidth);
  460. data.itmHeight = calcHeight(data.viewHeight, approxItemHeight);
  461. data.hdrWidth = calcWidth(data.viewWidth, appoxHeaderWidth);
  462. data.hdrHeight = calcHeight(data.viewHeight, approxHeaderHeight);
  463. data.ftrWidth = calcWidth(data.viewWidth, approxFooterWidth);
  464. data.ftrHeight = calcHeight(data.viewHeight, approxFooterHeight);
  465. data.valid = true;
  466. }
  467. }
  468. /**
  469. * NO DOM
  470. */
  471. function calcWidth(viewportWidth, approxWidth) {
  472. if (approxWidth.indexOf('%') > 0) {
  473. return (viewportWidth * (parseFloat(approxWidth) / 100));
  474. }
  475. else if (approxWidth.indexOf('px') > 0) {
  476. return parseFloat(approxWidth);
  477. }
  478. throw new Error('virtual scroll width can only use "%" or "px" units');
  479. }
  480. /**
  481. * NO DOM
  482. */
  483. function calcHeight(_viewportHeight, approxHeight) {
  484. if (approxHeight.indexOf('px') > 0) {
  485. return parseFloat(approxHeight);
  486. }
  487. throw new Error('virtual scroll height must use "px" units');
  488. }
  489. /**
  490. * NO DOM
  491. */
  492. function getElement(node) {
  493. var rootNodes = node.view.rootNodes;
  494. for (var i = 0; i < rootNodes.length; i++) {
  495. if (rootNodes[i].nodeType === 1) {
  496. return rootNodes[i];
  497. }
  498. }
  499. return null;
  500. }
  501. var VirtualContext = (function () {
  502. function VirtualContext($implicit, index, count) {
  503. this.$implicit = $implicit;
  504. this.index = index;
  505. this.count = count;
  506. this.bounds = {};
  507. }
  508. Object.defineProperty(VirtualContext.prototype, "first", {
  509. get: function () { return this.index === 0; },
  510. enumerable: true,
  511. configurable: true
  512. });
  513. Object.defineProperty(VirtualContext.prototype, "last", {
  514. get: function () { return this.index === this.count - 1; },
  515. enumerable: true,
  516. configurable: true
  517. });
  518. Object.defineProperty(VirtualContext.prototype, "even", {
  519. get: function () { return this.index % 2 === 0; },
  520. enumerable: true,
  521. configurable: true
  522. });
  523. Object.defineProperty(VirtualContext.prototype, "odd", {
  524. get: function () { return !this.even; },
  525. enumerable: true,
  526. configurable: true
  527. });
  528. return VirtualContext;
  529. }());
  530. export { VirtualContext };
  531. var VIEWABLE_RENDERED_PADDING = 3;
  532. var REQUIRED_DOM_READS = 2;
  533. //# sourceMappingURL=virtual-util.js.map