You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

972 lines
32 KiB

  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v1.1.1
  6. */
  7. goog.provide('ngmaterial.components.virtualRepeat');
  8. goog.require('ngmaterial.components.showHide');
  9. goog.require('ngmaterial.core');
  10. /**
  11. * @ngdoc module
  12. * @name material.components.virtualRepeat
  13. */
  14. VirtualRepeatContainerController.$inject = ["$$rAF", "$mdUtil", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"];
  15. VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF", "$mdUtil"];
  16. VirtualRepeatDirective.$inject = ["$parse"];
  17. angular.module('material.components.virtualRepeat', [
  18. 'material.core',
  19. 'material.components.showHide'
  20. ])
  21. .directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective)
  22. .directive('mdVirtualRepeat', VirtualRepeatDirective);
  23. /**
  24. * @ngdoc directive
  25. * @name mdVirtualRepeatContainer
  26. * @module material.components.virtualRepeat
  27. * @restrict E
  28. * @description
  29. * `md-virtual-repeat-container` provides the scroll container for md-virtual-repeat.
  30. *
  31. * VirtualRepeat is a limited substitute for ng-repeat that renders only
  32. * enough DOM nodes to fill the container and recycling them as the user scrolls.
  33. *
  34. * Once an element is not visible anymore, the VirtualRepeat recycles it and will reuse it for
  35. * another visible item by replacing the previous dataset with the new one.
  36. *
  37. * ### Common Issues
  38. *
  39. * > When having one-time bindings inside of the view template, the VirtualRepeat will not properly
  40. * > update the bindings for new items, since the view will be recycled.
  41. *
  42. * ### Notes
  43. *
  44. * > The VirtualRepeat is a similar implementation to the Android
  45. * [RecyclerView](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html)
  46. *
  47. * <!-- This comment forces a break between blockquotes //-->
  48. *
  49. * > Please also review the <a ng-href="api/directive/mdVirtualRepeat">VirtualRepeat</a>
  50. * documentation for more information.
  51. *
  52. *
  53. * @usage
  54. * <hljs lang="html">
  55. *
  56. * <md-virtual-repeat-container md-top-index="topIndex">
  57. * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
  58. * </md-virtual-repeat-container>
  59. * </hljs>
  60. *
  61. * @param {number=} md-top-index Binds the index of the item that is at the top of the scroll
  62. * container to $scope. It can both read and set the scroll position.
  63. * @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally
  64. * (defaults to orientation and scrolling vertically).
  65. * @param {boolean=} md-auto-shrink When present, the container will shrink to fit
  66. * the number of items when that number is less than its original size.
  67. * @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink
  68. * will shrink to (default: 0).
  69. */
  70. function VirtualRepeatContainerDirective() {
  71. return {
  72. controller: VirtualRepeatContainerController,
  73. template: virtualRepeatContainerTemplate,
  74. compile: function virtualRepeatContainerCompile($element, $attrs) {
  75. $element
  76. .addClass('md-virtual-repeat-container')
  77. .addClass($attrs.hasOwnProperty('mdOrientHorizontal')
  78. ? 'md-orient-horizontal'
  79. : 'md-orient-vertical');
  80. }
  81. };
  82. }
  83. function virtualRepeatContainerTemplate($element) {
  84. return '<div class="md-virtual-repeat-scroller">' +
  85. '<div class="md-virtual-repeat-sizer"></div>' +
  86. '<div class="md-virtual-repeat-offsetter">' +
  87. $element[0].innerHTML +
  88. '</div></div>';
  89. }
  90. /**
  91. * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies
  92. * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could
  93. * *compute* this value, but Firefox always reports an element to have a size of zero if it
  94. * goes over the max, meaning that we'd have to binary search for the value.
  95. * @const {number}
  96. */
  97. var MAX_ELEMENT_SIZE = 1533917;
  98. /**
  99. * Number of additional elements to render above and below the visible area inside
  100. * of the virtual repeat container. A higher number results in less flicker when scrolling
  101. * very quickly in Safari, but comes with a higher rendering and dirty-checking cost.
  102. * @const {number}
  103. */
  104. var NUM_EXTRA = 3;
  105. /** ngInject */
  106. function VirtualRepeatContainerController(
  107. $$rAF, $mdUtil, $parse, $rootScope, $window, $scope, $element, $attrs) {
  108. this.$rootScope = $rootScope;
  109. this.$scope = $scope;
  110. this.$element = $element;
  111. this.$attrs = $attrs;
  112. /** @type {number} The width or height of the container */
  113. this.size = 0;
  114. /** @type {number} The scroll width or height of the scroller */
  115. this.scrollSize = 0;
  116. /** @type {number} The scrollLeft or scrollTop of the scroller */
  117. this.scrollOffset = 0;
  118. /** @type {boolean} Whether the scroller is oriented horizontally */
  119. this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal');
  120. /** @type {!VirtualRepeatController} The repeater inside of this container */
  121. this.repeater = null;
  122. /** @type {boolean} Whether auto-shrink is enabled */
  123. this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink');
  124. /** @type {number} Minimum number of items to auto-shrink to */
  125. this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0;
  126. /** @type {?number} Original container size when shrank */
  127. this.originalSize = null;
  128. /** @type {number} Amount to offset the total scroll size by. */
  129. this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0;
  130. /** @type {?string} height or width element style on the container prior to auto-shrinking. */
  131. this.oldElementSize = null;
  132. if (this.$attrs.mdTopIndex) {
  133. /** @type {function(angular.Scope): number} Binds to topIndex on Angular scope */
  134. this.bindTopIndex = $parse(this.$attrs.mdTopIndex);
  135. /** @type {number} The index of the item that is at the top of the scroll container */
  136. this.topIndex = this.bindTopIndex(this.$scope);
  137. if (!angular.isDefined(this.topIndex)) {
  138. this.topIndex = 0;
  139. this.bindTopIndex.assign(this.$scope, 0);
  140. }
  141. this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) {
  142. if (newIndex !== this.topIndex) {
  143. this.scrollToIndex(newIndex);
  144. }
  145. }));
  146. } else {
  147. this.topIndex = 0;
  148. }
  149. this.scroller = $element[0].querySelector('.md-virtual-repeat-scroller');
  150. this.sizer = this.scroller.querySelector('.md-virtual-repeat-sizer');
  151. this.offsetter = this.scroller.querySelector('.md-virtual-repeat-offsetter');
  152. // After the dom stablizes, measure the initial size of the container and
  153. // make a best effort at re-measuring as it changes.
  154. var boundUpdateSize = angular.bind(this, this.updateSize);
  155. $$rAF(angular.bind(this, function() {
  156. boundUpdateSize();
  157. var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false);
  158. var jWindow = angular.element($window);
  159. // Make one more attempt to get the size if it is 0.
  160. // This is not by any means a perfect approach, but there's really no
  161. // silver bullet here.
  162. if (!this.size) {
  163. debouncedUpdateSize();
  164. }
  165. jWindow.on('resize', debouncedUpdateSize);
  166. $scope.$on('$destroy', function() {
  167. jWindow.off('resize', debouncedUpdateSize);
  168. });
  169. $scope.$emit('$md-resize-enable');
  170. $scope.$on('$md-resize', boundUpdateSize);
  171. }));
  172. }
  173. /** Called by the md-virtual-repeat inside of the container at startup. */
  174. VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) {
  175. this.repeater = repeaterCtrl;
  176. angular.element(this.scroller)
  177. .on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_));
  178. };
  179. /** @return {boolean} Whether the container is configured for horizontal scrolling. */
  180. VirtualRepeatContainerController.prototype.isHorizontal = function() {
  181. return this.horizontal;
  182. };
  183. /** @return {number} The size (width or height) of the container. */
  184. VirtualRepeatContainerController.prototype.getSize = function() {
  185. return this.size;
  186. };
  187. /**
  188. * Resizes the container.
  189. * @private
  190. * @param {number} size The new size to set.
  191. */
  192. VirtualRepeatContainerController.prototype.setSize_ = function(size) {
  193. var dimension = this.getDimensionName_();
  194. this.size = size;
  195. this.$element[0].style[dimension] = size + 'px';
  196. };
  197. VirtualRepeatContainerController.prototype.unsetSize_ = function() {
  198. this.$element[0].style[this.getDimensionName_()] = this.oldElementSize;
  199. this.oldElementSize = null;
  200. };
  201. /** Instructs the container to re-measure its size. */
  202. VirtualRepeatContainerController.prototype.updateSize = function() {
  203. // If the original size is already determined, we can skip the update.
  204. if (this.originalSize) return;
  205. this.size = this.isHorizontal()
  206. ? this.$element[0].clientWidth
  207. : this.$element[0].clientHeight;
  208. // Recheck the scroll position after updating the size. This resolves
  209. // problems that can result if the scroll position was measured while the
  210. // element was display: none or detached from the document.
  211. this.handleScroll_();
  212. this.repeater && this.repeater.containerUpdated();
  213. };
  214. /** @return {number} The container's scrollHeight or scrollWidth. */
  215. VirtualRepeatContainerController.prototype.getScrollSize = function() {
  216. return this.scrollSize;
  217. };
  218. VirtualRepeatContainerController.prototype.getDimensionName_ = function() {
  219. return this.isHorizontal() ? 'width' : 'height';
  220. };
  221. /**
  222. * Sets the scroller element to the specified size.
  223. * @private
  224. * @param {number} size The new size.
  225. */
  226. VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) {
  227. var dimension = this.getDimensionName_();
  228. var crossDimension = this.isHorizontal() ? 'height' : 'width';
  229. // Clear any existing dimensions.
  230. this.sizer.innerHTML = '';
  231. // If the size falls within the browser's maximum explicit size for a single element, we can
  232. // set the size and be done. Otherwise, we have to create children that add up the the desired
  233. // size.
  234. if (size < MAX_ELEMENT_SIZE) {
  235. this.sizer.style[dimension] = size + 'px';
  236. } else {
  237. this.sizer.style[dimension] = 'auto';
  238. this.sizer.style[crossDimension] = 'auto';
  239. // Divide the total size we have to render into N max-size pieces.
  240. var numChildren = Math.floor(size / MAX_ELEMENT_SIZE);
  241. // Element template to clone for each max-size piece.
  242. var sizerChild = document.createElement('div');
  243. sizerChild.style[dimension] = MAX_ELEMENT_SIZE + 'px';
  244. sizerChild.style[crossDimension] = '1px';
  245. for (var i = 0; i < numChildren; i++) {
  246. this.sizer.appendChild(sizerChild.cloneNode(false));
  247. }
  248. // Re-use the element template for the remainder.
  249. sizerChild.style[dimension] = (size - (numChildren * MAX_ELEMENT_SIZE)) + 'px';
  250. this.sizer.appendChild(sizerChild);
  251. }
  252. };
  253. /**
  254. * If auto-shrinking is enabled, shrinks or unshrinks as appropriate.
  255. * @private
  256. * @param {number} size The new size.
  257. */
  258. VirtualRepeatContainerController.prototype.autoShrink_ = function(size) {
  259. var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize());
  260. if (this.autoShrink && shrinkSize !== this.size) {
  261. if (this.oldElementSize === null) {
  262. this.oldElementSize = this.$element[0].style[this.getDimensionName_()];
  263. }
  264. var currentSize = this.originalSize || this.size;
  265. if (!currentSize || shrinkSize < currentSize) {
  266. if (!this.originalSize) {
  267. this.originalSize = this.size;
  268. }
  269. // Now we update the containers size, because shrinking is enabled.
  270. this.setSize_(shrinkSize);
  271. } else if (this.originalSize !== null) {
  272. // Set the size back to our initial size.
  273. this.unsetSize_();
  274. var _originalSize = this.originalSize;
  275. this.originalSize = null;
  276. // We determine the repeaters size again, if the original size was zero.
  277. // The originalSize needs to be null, to be able to determine the size.
  278. if (!_originalSize) this.updateSize();
  279. // Apply the original size or the determined size back to the container, because
  280. // it has been overwritten before, in the shrink block.
  281. this.setSize_(_originalSize || this.size);
  282. }
  283. this.repeater.containerUpdated();
  284. }
  285. };
  286. /**
  287. * Sets the scrollHeight or scrollWidth. Called by the repeater based on
  288. * its item count and item size.
  289. * @param {number} itemsSize The total size of the items.
  290. */
  291. VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) {
  292. var size = itemsSize + this.offsetSize;
  293. if (this.scrollSize === size) return;
  294. this.sizeScroller_(size);
  295. this.autoShrink_(size);
  296. this.scrollSize = size;
  297. };
  298. /** @return {number} The container's current scroll offset. */
  299. VirtualRepeatContainerController.prototype.getScrollOffset = function() {
  300. return this.scrollOffset;
  301. };
  302. /**
  303. * Scrolls to a given scrollTop position.
  304. * @param {number} position
  305. */
  306. VirtualRepeatContainerController.prototype.scrollTo = function(position) {
  307. this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position;
  308. this.handleScroll_();
  309. };
  310. /**
  311. * Scrolls the item with the given index to the top of the scroll container.
  312. * @param {number} index
  313. */
  314. VirtualRepeatContainerController.prototype.scrollToIndex = function(index) {
  315. var itemSize = this.repeater.getItemSize();
  316. var itemsLength = this.repeater.itemsLength;
  317. if(index > itemsLength) {
  318. index = itemsLength - 1;
  319. }
  320. this.scrollTo(itemSize * index);
  321. };
  322. VirtualRepeatContainerController.prototype.resetScroll = function() {
  323. this.scrollTo(0);
  324. };
  325. VirtualRepeatContainerController.prototype.handleScroll_ = function() {
  326. var doc = angular.element(document)[0];
  327. var ltr = doc.dir != 'rtl' && doc.body.dir != 'rtl';
  328. if(!ltr && !this.maxSize) {
  329. this.scroller.scrollLeft = this.scrollSize;
  330. this.maxSize = this.scroller.scrollLeft;
  331. }
  332. var offset = this.isHorizontal() ?
  333. (ltr?this.scroller.scrollLeft : this.maxSize - this.scroller.scrollLeft)
  334. : this.scroller.scrollTop;
  335. if (offset === this.scrollOffset || offset > this.scrollSize - this.size) return;
  336. var itemSize = this.repeater.getItemSize();
  337. if (!itemSize) return;
  338. var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA);
  339. var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') +
  340. (!this.isHorizontal() || ltr ? (numItems * itemSize) : - (numItems * itemSize)) + 'px)';
  341. this.scrollOffset = offset;
  342. this.offsetter.style.webkitTransform = transform;
  343. this.offsetter.style.transform = transform;
  344. if (this.bindTopIndex) {
  345. var topIndex = Math.floor(offset / itemSize);
  346. if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) {
  347. this.topIndex = topIndex;
  348. this.bindTopIndex.assign(this.$scope, topIndex);
  349. if (!this.$rootScope.$$phase) this.$scope.$digest();
  350. }
  351. }
  352. this.repeater.containerUpdated();
  353. };
  354. /**
  355. * @ngdoc directive
  356. * @name mdVirtualRepeat
  357. * @module material.components.virtualRepeat
  358. * @restrict A
  359. * @priority 1000
  360. * @description
  361. * `md-virtual-repeat` specifies an element to repeat using virtual scrolling.
  362. *
  363. * Virtual repeat is a limited substitute for ng-repeat that renders only
  364. * enough dom nodes to fill the container and recycling them as the user scrolls.
  365. * Arrays, but not objects are supported for iteration.
  366. * Track by, as alias, and (key, value) syntax are not supported.
  367. *
  368. * > <b>Note:</b> Please also review the
  369. * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation
  370. * for more information.
  371. *
  372. * @usage
  373. * <hljs lang="html">
  374. * <md-virtual-repeat-container>
  375. * <div md-virtual-repeat="i in items">Hello {{i}}!</div>
  376. * </md-virtual-repeat-container>
  377. *
  378. * <md-virtual-repeat-container md-orient-horizontal>
  379. * <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
  380. * </md-virtual-repeat-container>
  381. * </hljs>
  382. *
  383. * @param {number=} md-item-size The height or width of the repeated elements (which must be
  384. * identical for each element). Optional. Will attempt to read the size from the dom if missing,
  385. * but still assumes that all repeated nodes have same height or width.
  386. * @param {string=} md-extra-name Evaluates to an additional name to which the current iterated item
  387. * can be assigned on the repeated scope (needed for use in `md-autocomplete`).
  388. * @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument as an object
  389. * that can fetch rows rather than an array.
  390. *
  391. * **NOTE:** This object must implement the following interface with two (2) methods:
  392. *
  393. * - `getItemAtIndex: function(index) [object]` The item at that index or null if it is not yet
  394. * loaded (it should start downloading the item in that case).
  395. * - `getLength: function() [number]` The data length to which the repeater container
  396. * should be sized. Ideally, when the count is known, this method should return it.
  397. * Otherwise, return a higher number than the currently loaded items to produce an
  398. * infinite-scroll behavior.
  399. */
  400. function VirtualRepeatDirective($parse) {
  401. return {
  402. controller: VirtualRepeatController,
  403. priority: 1000,
  404. require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'],
  405. restrict: 'A',
  406. terminal: true,
  407. transclude: 'element',
  408. compile: function VirtualRepeatCompile($element, $attrs) {
  409. var expression = $attrs.mdVirtualRepeat;
  410. var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);
  411. var repeatName = match[1];
  412. var repeatListExpression = $parse(match[2]);
  413. var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName);
  414. return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) {
  415. ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName);
  416. };
  417. }
  418. };
  419. }
  420. /** ngInject */
  421. function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope,
  422. $$rAF, $mdUtil) {
  423. this.$scope = $scope;
  424. this.$element = $element;
  425. this.$attrs = $attrs;
  426. this.$browser = $browser;
  427. this.$document = $document;
  428. this.$rootScope = $rootScope;
  429. this.$$rAF = $$rAF;
  430. /** @type {boolean} Whether we are in on-demand mode. */
  431. this.onDemand = $mdUtil.parseAttributeBoolean($attrs.mdOnDemand);
  432. /** @type {!Function} Backup reference to $browser.$$checkUrlChange */
  433. this.browserCheckUrlChange = $browser.$$checkUrlChange;
  434. /** @type {number} Most recent starting repeat index (based on scroll offset) */
  435. this.newStartIndex = 0;
  436. /** @type {number} Most recent ending repeat index (based on scroll offset) */
  437. this.newEndIndex = 0;
  438. /** @type {number} Most recent end visible index (based on scroll offset) */
  439. this.newVisibleEnd = 0;
  440. /** @type {number} Previous starting repeat index (based on scroll offset) */
  441. this.startIndex = 0;
  442. /** @type {number} Previous ending repeat index (based on scroll offset) */
  443. this.endIndex = 0;
  444. // TODO: measure width/height of first element from dom if not provided.
  445. // getComputedStyle?
  446. /** @type {?number} Height/width of repeated elements. */
  447. this.itemSize = $scope.$eval($attrs.mdItemSize) || null;
  448. /** @type {boolean} Whether this is the first time that items are rendered. */
  449. this.isFirstRender = true;
  450. /**
  451. * @private {boolean} Whether the items in the list are already being updated. Used to prevent
  452. * nested calls to virtualRepeatUpdate_.
  453. */
  454. this.isVirtualRepeatUpdating_ = false;
  455. /** @type {number} Most recently seen length of items. */
  456. this.itemsLength = 0;
  457. /**
  458. * @type {!Function} Unwatch callback for item size (when md-items-size is
  459. * not specified), or angular.noop otherwise.
  460. */
  461. this.unwatchItemSize_ = angular.noop;
  462. /**
  463. * Presently rendered blocks by repeat index.
  464. * @type {Object<number, !VirtualRepeatController.Block}
  465. */
  466. this.blocks = {};
  467. /** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */
  468. this.pooledBlocks = [];
  469. $scope.$on('$destroy', angular.bind(this, this.cleanupBlocks_));
  470. }
  471. /**
  472. * An object representing a repeated item.
  473. * @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}}
  474. */
  475. VirtualRepeatController.Block;
  476. /**
  477. * Called at startup by the md-virtual-repeat postLink function.
  478. * @param {!VirtualRepeatContainerController} container The container's controller.
  479. * @param {!Function} transclude The repeated element's bound transclude function.
  480. * @param {string} repeatName The left hand side of the repeat expression, indicating
  481. * the name for each item in the array.
  482. * @param {!Function} repeatListExpression A compiled expression based on the right hand side
  483. * of the repeat expression. Points to the array to repeat over.
  484. * @param {string|undefined} extraName The optional extra repeatName.
  485. */
  486. VirtualRepeatController.prototype.link_ =
  487. function(container, transclude, repeatName, repeatListExpression, extraName) {
  488. this.container = container;
  489. this.transclude = transclude;
  490. this.repeatName = repeatName;
  491. this.rawRepeatListExpression = repeatListExpression;
  492. this.extraName = extraName;
  493. this.sized = false;
  494. this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
  495. this.container.register(this);
  496. };
  497. /** @private Cleans up unused blocks. */
  498. VirtualRepeatController.prototype.cleanupBlocks_ = function() {
  499. angular.forEach(this.pooledBlocks, function cleanupBlock(block) {
  500. block.element.remove();
  501. });
  502. };
  503. /** @private Attempts to set itemSize by measuring a repeated element in the dom */
  504. VirtualRepeatController.prototype.readItemSize_ = function() {
  505. if (this.itemSize) {
  506. // itemSize was successfully read in a different asynchronous call.
  507. return;
  508. }
  509. this.items = this.repeatListExpression(this.$scope);
  510. this.parentNode = this.$element[0].parentNode;
  511. var block = this.getBlock_(0);
  512. if (!block.element[0].parentNode) {
  513. this.parentNode.appendChild(block.element[0]);
  514. }
  515. this.itemSize = block.element[0][
  516. this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null;
  517. this.blocks[0] = block;
  518. this.poolBlock_(0);
  519. if (this.itemSize) {
  520. this.containerUpdated();
  521. }
  522. };
  523. /**
  524. * Returns the user-specified repeat list, transforming it into an array-like
  525. * object in the case of infinite scroll/dynamic load mode.
  526. * @param {!angular.Scope} The scope.
  527. * @return {!Array|!Object} An array or array-like object for iteration.
  528. */
  529. VirtualRepeatController.prototype.repeatListExpression_ = function(scope) {
  530. var repeatList = this.rawRepeatListExpression(scope);
  531. if (this.onDemand && repeatList) {
  532. var virtualList = new VirtualRepeatModelArrayLike(repeatList);
  533. virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
  534. return virtualList;
  535. } else {
  536. return repeatList;
  537. }
  538. };
  539. /**
  540. * Called by the container. Informs us that the containers scroll or size has
  541. * changed.
  542. */
  543. VirtualRepeatController.prototype.containerUpdated = function() {
  544. // If itemSize is unknown, attempt to measure it.
  545. if (!this.itemSize) {
  546. // Make sure to clean up watchers if we can (see #8178)
  547. if(this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop){
  548. this.unwatchItemSize_();
  549. }
  550. this.unwatchItemSize_ = this.$scope.$watchCollection(
  551. this.repeatListExpression,
  552. angular.bind(this, function(items) {
  553. if (items && items.length) {
  554. this.readItemSize_();
  555. }
  556. }));
  557. if (!this.$rootScope.$$phase) this.$scope.$digest();
  558. return;
  559. } else if (!this.sized) {
  560. this.items = this.repeatListExpression(this.$scope);
  561. }
  562. if (!this.sized) {
  563. this.unwatchItemSize_();
  564. this.sized = true;
  565. this.$scope.$watchCollection(this.repeatListExpression,
  566. angular.bind(this, function(items, oldItems) {
  567. if (!this.isVirtualRepeatUpdating_) {
  568. this.virtualRepeatUpdate_(items, oldItems);
  569. }
  570. }));
  571. }
  572. this.updateIndexes_();
  573. if (this.newStartIndex !== this.startIndex ||
  574. this.newEndIndex !== this.endIndex ||
  575. this.container.getScrollOffset() > this.container.getScrollSize()) {
  576. if (this.items instanceof VirtualRepeatModelArrayLike) {
  577. this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
  578. }
  579. this.virtualRepeatUpdate_(this.items, this.items);
  580. }
  581. };
  582. /**
  583. * Called by the container. Returns the size of a single repeated item.
  584. * @return {?number} Size of a repeated item.
  585. */
  586. VirtualRepeatController.prototype.getItemSize = function() {
  587. return this.itemSize;
  588. };
  589. /**
  590. * Called by the container. Returns the size of a single repeated item.
  591. * @return {?number} Size of a repeated item.
  592. */
  593. VirtualRepeatController.prototype.getItemCount = function() {
  594. return this.itemsLength;
  595. };
  596. /**
  597. * Updates the order and visible offset of repeated blocks in response to scrolling
  598. * or items updates.
  599. * @private
  600. */
  601. VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) {
  602. this.isVirtualRepeatUpdating_ = true;
  603. var itemsLength = items && items.length || 0;
  604. var lengthChanged = false;
  605. // If the number of items shrank, keep the scroll position.
  606. if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
  607. this.items = items;
  608. var previousScrollOffset = this.container.getScrollOffset();
  609. this.container.resetScroll();
  610. this.container.scrollTo(previousScrollOffset);
  611. }
  612. if (itemsLength !== this.itemsLength) {
  613. lengthChanged = true;
  614. this.itemsLength = itemsLength;
  615. }
  616. this.items = items;
  617. if (items !== oldItems || lengthChanged) {
  618. this.updateIndexes_();
  619. }
  620. this.parentNode = this.$element[0].parentNode;
  621. if (lengthChanged) {
  622. this.container.setScrollSize(itemsLength * this.itemSize);
  623. }
  624. if (this.isFirstRender) {
  625. this.isFirstRender = false;
  626. var startIndex = this.$attrs.mdStartIndex ?
  627. this.$scope.$eval(this.$attrs.mdStartIndex) :
  628. this.container.topIndex;
  629. this.container.scrollToIndex(startIndex);
  630. }
  631. // Detach and pool any blocks that are no longer in the viewport.
  632. Object.keys(this.blocks).forEach(function(blockIndex) {
  633. var index = parseInt(blockIndex, 10);
  634. if (index < this.newStartIndex || index >= this.newEndIndex) {
  635. this.poolBlock_(index);
  636. }
  637. }, this);
  638. // Add needed blocks.
  639. // For performance reasons, temporarily block browser url checks as we digest
  640. // the restored block scopes ($$checkUrlChange reads window.location to
  641. // check for changes and trigger route change, etc, which we don't need when
  642. // trying to scroll at 60fps).
  643. this.$browser.$$checkUrlChange = angular.noop;
  644. var i, block,
  645. newStartBlocks = [],
  646. newEndBlocks = [];
  647. // Collect blocks at the top.
  648. for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) {
  649. block = this.getBlock_(i);
  650. this.updateBlock_(block, i);
  651. newStartBlocks.push(block);
  652. }
  653. // Update blocks that are already rendered.
  654. for (; this.blocks[i] != null; i++) {
  655. this.updateBlock_(this.blocks[i], i);
  656. }
  657. var maxIndex = i - 1;
  658. // Collect blocks at the end.
  659. for (; i < this.newEndIndex; i++) {
  660. block = this.getBlock_(i);
  661. this.updateBlock_(block, i);
  662. newEndBlocks.push(block);
  663. }
  664. // Attach collected blocks to the document.
  665. if (newStartBlocks.length) {
  666. this.parentNode.insertBefore(
  667. this.domFragmentFromBlocks_(newStartBlocks),
  668. this.$element[0].nextSibling);
  669. }
  670. if (newEndBlocks.length) {
  671. this.parentNode.insertBefore(
  672. this.domFragmentFromBlocks_(newEndBlocks),
  673. this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling);
  674. }
  675. // Restore $$checkUrlChange.
  676. this.$browser.$$checkUrlChange = this.browserCheckUrlChange;
  677. this.startIndex = this.newStartIndex;
  678. this.endIndex = this.newEndIndex;
  679. this.isVirtualRepeatUpdating_ = false;
  680. };
  681. /**
  682. * @param {number} index Where the block is to be in the repeated list.
  683. * @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index.
  684. * @private
  685. */
  686. VirtualRepeatController.prototype.getBlock_ = function(index) {
  687. if (this.pooledBlocks.length) {
  688. return this.pooledBlocks.pop();
  689. }
  690. var block;
  691. this.transclude(angular.bind(this, function(clone, scope) {
  692. block = {
  693. element: clone,
  694. new: true,
  695. scope: scope
  696. };
  697. this.updateScope_(scope, index);
  698. this.parentNode.appendChild(clone[0]);
  699. }));
  700. return block;
  701. };
  702. /**
  703. * Updates and if not in a digest cycle, digests the specified block's scope to the data
  704. * at the specified index.
  705. * @param {!VirtualRepeatController.Block} block The block whose scope should be updated.
  706. * @param {number} index The index to set.
  707. * @private
  708. */
  709. VirtualRepeatController.prototype.updateBlock_ = function(block, index) {
  710. this.blocks[index] = block;
  711. if (!block.new &&
  712. (block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) {
  713. return;
  714. }
  715. block.new = false;
  716. // Update and digest the block's scope.
  717. this.updateScope_(block.scope, index);
  718. // Perform digest before reattaching the block.
  719. // Any resulting synchronous dom mutations should be much faster as a result.
  720. // This might break some directives, but I'm going to try it for now.
  721. if (!this.$rootScope.$$phase) {
  722. block.scope.$digest();
  723. }
  724. };
  725. /**
  726. * Updates scope to the data at the specified index.
  727. * @param {!angular.Scope} scope The scope which should be updated.
  728. * @param {number} index The index to set.
  729. * @private
  730. */
  731. VirtualRepeatController.prototype.updateScope_ = function(scope, index) {
  732. scope.$index = index;
  733. scope[this.repeatName] = this.items && this.items[index];
  734. if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index];
  735. };
  736. /**
  737. * Pools the block at the specified index (Pulls its element out of the dom and stores it).
  738. * @param {number} index The index at which the block to pool is stored.
  739. * @private
  740. */
  741. VirtualRepeatController.prototype.poolBlock_ = function(index) {
  742. this.pooledBlocks.push(this.blocks[index]);
  743. this.parentNode.removeChild(this.blocks[index].element[0]);
  744. delete this.blocks[index];
  745. };
  746. /**
  747. * Produces a dom fragment containing the elements from the list of blocks.
  748. * @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements
  749. * should be added to the document fragment.
  750. * @return {DocumentFragment}
  751. * @private
  752. */
  753. VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) {
  754. var fragment = this.$document[0].createDocumentFragment();
  755. blocks.forEach(function(block) {
  756. fragment.appendChild(block.element[0]);
  757. });
  758. return fragment;
  759. };
  760. /**
  761. * Updates start and end indexes based on length of repeated items and container size.
  762. * @private
  763. */
  764. VirtualRepeatController.prototype.updateIndexes_ = function() {
  765. var itemsLength = this.items ? this.items.length : 0;
  766. var containerLength = Math.ceil(this.container.getSize() / this.itemSize);
  767. this.newStartIndex = Math.max(0, Math.min(
  768. itemsLength - containerLength,
  769. Math.floor(this.container.getScrollOffset() / this.itemSize)));
  770. this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
  771. this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
  772. this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
  773. };
  774. /**
  775. * This VirtualRepeatModelArrayLike class enforces the interface requirements
  776. * for infinite scrolling within a mdVirtualRepeatContainer. An object with this
  777. * interface must implement the following interface with two (2) methods:
  778. *
  779. * getItemAtIndex: function(index) -> item at that index or null if it is not yet
  780. * loaded (It should start downloading the item in that case).
  781. *
  782. * getLength: function() -> number The data legnth to which the repeater container
  783. * should be sized. Ideally, when the count is known, this method should return it.
  784. * Otherwise, return a higher number than the currently loaded items to produce an
  785. * infinite-scroll behavior.
  786. *
  787. * @usage
  788. * <hljs lang="html">
  789. * <md-virtual-repeat-container md-orient-horizontal>
  790. * <div md-virtual-repeat="i in items" md-on-demand>
  791. * Hello {{i}}!
  792. * </div>
  793. * </md-virtual-repeat-container>
  794. * </hljs>
  795. *
  796. */
  797. function VirtualRepeatModelArrayLike(model) {
  798. if (!angular.isFunction(model.getItemAtIndex) ||
  799. !angular.isFunction(model.getLength)) {
  800. throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' +
  801. 'functions getItemAtIndex() and getLength() ');
  802. }
  803. this.model = model;
  804. }
  805. VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) {
  806. for (var i = start; i < end; i++) {
  807. if (!this.hasOwnProperty(i)) {
  808. this[i] = this.model.getItemAtIndex(i);
  809. }
  810. }
  811. this.length = this.model.getLength();
  812. };
  813. function abstractMethod() {
  814. throw Error('Non-overridden abstract method called.');
  815. }
  816. ngmaterial.components.virtualRepeat = angular.module("material.components.virtualRepeat");