diff --git a/src/modules/padding.js b/src/modules/padding.js index 23e5df8c..ce66e22d 100644 --- a/src/modules/padding.js +++ b/src/modules/padding.js @@ -49,28 +49,36 @@ Object.getOwnPropertyNames(CacheProto.prototype).forEach(methodName => Cache.prototype[methodName] = CacheProto.prototype[methodName] ); -export default function Padding(template) { - let result; - +function generateElement(template) { if(template.nodeType !== Node.ELEMENT_NODE) { throw new Error('ui-scroll directive requires an Element node for templating the view'); } - + let element; switch (template.tagName.toLowerCase()) { case 'dl': throw new Error(`ui-scroll directive does not support <${template.tagName}> as a repeating tag: ${template.outerHTML}`); case 'tr': let table = angular.element('
'); - result = table.find('tr'); + element = table.find('tr'); break; case 'li': - result = angular.element('
  • '); + element = angular.element('
  • '); break; default: - result = angular.element('
    '); + element = angular.element('
    '); } + return element; +} - result.cache = new Cache(); +class Padding { + constructor(template) { + this.element = generateElement(template); + this.cache = new Cache(); + } + + height() { + return this.element.height.apply(this.element, arguments); + } +} - return result; -} \ No newline at end of file +export default Padding; \ No newline at end of file diff --git a/src/modules/viewport.js b/src/modules/viewport.js index fe8aaee1..3d384f79 100644 --- a/src/modules/viewport.js +++ b/src/modules/viewport.js @@ -25,14 +25,20 @@ export default function Viewport(elementRoutines, buffer, element, viewportContr createPaddingElements(template) { topPadding = new Padding(template); bottomPadding = new Padding(template); - element.before(topPadding); - element.after(bottomPadding); + element.before(topPadding.element); + element.after(bottomPadding.element); + topPadding.height(0); + bottomPadding.height(0); }, applyContainerStyle() { - if (container && container !== viewport) { + if (!container) { + return true; + } + if(container !== viewport) { viewport.css('height', window.getComputedStyle(container[0]).height); } + return viewport.height() > 0; }, bottomDataPos() { @@ -54,11 +60,11 @@ export default function Viewport(elementRoutines, buffer, element, viewportContr }, insertElement(e, sibling) { - return elementRoutines.insertElement(e, sibling || topPadding); + return elementRoutines.insertElement(e, sibling || topPadding.element); }, insertElementAnimated(e, sibling) { - return elementRoutines.insertElementAnimated(e, sibling || topPadding); + return elementRoutines.insertElementAnimated(e, sibling || topPadding.element); }, shouldLoadBottom() { diff --git a/src/ui-scroll.js b/src/ui-scroll.js index c3341376..1dc0ff03 100644 --- a/src/ui-scroll.js +++ b/src/ui-scroll.js @@ -39,9 +39,10 @@ angular.module('ui.scroll', []) '$injector', '$rootScope', '$timeout', + '$interval', '$q', '$parse', - function (console, $injector, $rootScope, $timeout, $q, $parse) { + function (console, $injector, $rootScope, $timeout, $interval, $q, $parse) { return { require: ['?^uiScrollViewport'], @@ -59,7 +60,7 @@ angular.module('ui.scroll', []) } function parseNumericAttr(value, defaultValue) { - let result = $parse(value)($scope); + const result = $parse(value)($scope); return isNaN(result) ? defaultValue : result; } @@ -67,6 +68,8 @@ angular.module('ui.scroll', []) const BUFFER_DEFAULT = 10; const PADDING_MIN = 0.3; const PADDING_DEFAULT = 0.5; + const MAX_VIEWPORT_DELAY = 500; + const VIEWPORT_POLLING_INTERVAL = 50; let datasource = null; const itemName = match[1]; @@ -78,16 +81,16 @@ angular.module('ui.scroll', []) let ridActual = 0;// current data revision id let pending = []; - let elementRoutines = new ElementRoutines($injector, $q); - let buffer = new ScrollBuffer(elementRoutines, bufferSize); - let viewport = new Viewport(elementRoutines, buffer, element, viewportController, $rootScope, padding); - let adapter = new Adapter(viewport, buffer, adjustBuffer, reload, $attr, $parse, $scope); + const elementRoutines = new ElementRoutines($injector, $q); + const buffer = new ScrollBuffer(elementRoutines, bufferSize); + const viewport = new Viewport(elementRoutines, buffer, element, viewportController, $rootScope, padding); + const adapter = new Adapter(viewport, buffer, adjustBuffer, reload, $attr, $parse, $scope); if (viewportController) { viewportController.adapter = adapter; } - let isDatasourceValid = () => angular.isObject(datasource) && angular.isFunction(datasource.get); + const isDatasourceValid = () => angular.isObject(datasource) && angular.isFunction(datasource.get); datasource = $parse(datasourceName)($scope); // try to get datasource on scope if (!isDatasourceValid()) { datasource = $injector.get(datasourceName); // try to inject datasource as service @@ -114,7 +117,7 @@ angular.module('ui.scroll', []) } function defineIndexProperty(datasource, propName, propUserName) { - let descriptor = Object.getOwnPropertyDescriptor(datasource, propName); + const descriptor = Object.getOwnPropertyDescriptor(datasource, propName); if (descriptor && (descriptor.set || descriptor.get)) { return; } @@ -124,7 +127,7 @@ angular.module('ui.scroll', []) set: (value) => { getter = value; buffer[propUserName] = value; - let topPaddingHeightOld = viewport.topDataPos(); + const topPaddingHeightOld = viewport.topDataPos(); viewport.adjustPaddings(); if (propName === 'minIndex') { viewport.onAfterMinIndexSet(topPaddingHeightOld); @@ -157,6 +160,26 @@ angular.module('ui.scroll', []) }, success); }; + const run = () => { + let tryCount = 0; + if(!viewport.applyContainerStyle()) { + const timer = $interval(() => { + tryCount++; + if(viewport.applyContainerStyle()) { + $interval.cancel(timer); + reload(); + } + if(tryCount * VIEWPORT_POLLING_INTERVAL >= MAX_VIEWPORT_DELAY) { + $interval.cancel(timer); + throw Error(`ui-scroll directive requires a viewport with non-zero height in ${MAX_VIEWPORT_DELAY}ms`); + } + }, VIEWPORT_POLLING_INTERVAL); + } + else { + reload(); + } + }; + /** * Build padding elements * @@ -180,10 +203,7 @@ angular.module('ui.scroll', []) viewport.bind('mousewheel', wheelHandler); - $timeout(() => { - viewport.applyContainerStyle(); - reload(); - }); + run(); /* Private function definitions */ @@ -239,7 +259,7 @@ angular.module('ui.scroll', []) function createElement(wrapper, insertAfter, insertElement) { let promises = null; - let sibling = (insertAfter > 0) ? buffer[insertAfter - 1].element : undefined; + const sibling = (insertAfter > 0) ? buffer[insertAfter - 1].element : undefined; linker((clone, scope) => { promises = insertElement(clone, sibling); wrapper.element = clone; @@ -248,7 +268,7 @@ angular.module('ui.scroll', []) }); // ui-scroll-grid apply if (adapter.transform) { - let tdInitializer = wrapper.scope.uiScrollTdInitializer; + const tdInitializer = wrapper.scope.uiScrollTdInitializer; if (tdInitializer && tdInitializer.linking) { adapter.transform(wrapper.scope, wrapper.element); } else { @@ -459,8 +479,8 @@ angular.module('ui.scroll', []) function wheelHandler(event) { if (!adapter.disabled) { - let scrollTop = viewport[0].scrollTop; - let yMax = viewport[0].scrollHeight - viewport[0].clientHeight; + const scrollTop = viewport[0].scrollTop; + const yMax = viewport[0].scrollHeight - viewport[0].clientHeight; if ((scrollTop === 0 && !buffer.bof) || (scrollTop === yMax && !buffer.eof)) { event.preventDefault(); diff --git a/test/AssigningSpec.js b/test/AssigningSpec.js index dd7edcf2..12c2edce 100644 --- a/test/AssigningSpec.js +++ b/test/AssigningSpec.js @@ -40,14 +40,13 @@ describe('uiScroll', function () { }; var executeTest = function(template, scopeSelector, scopeContainer) { - inject(function($rootScope, $compile, $timeout) { + inject(function($rootScope, $compile) { // build and render var templateElement = angular.element(template); var scope = $rootScope.$new(); angular.element(document).find('body').append(templateElement); $compile(templateElement)(scope); scope.$apply(); - $timeout.flush(); // find adapter element and scope container var adapterContainer; diff --git a/test/VisibilitySwitchingSpec.js b/test/VisibilitySwitchingSpec.js index 56bf2d6a..5cee1876 100644 --- a/test/VisibilitySwitchingSpec.js +++ b/test/VisibilitySwitchingSpec.js @@ -1,79 +1,67 @@ /*global describe, beforeEach, module, it, expect, runTest */ -describe('uiScroll visibility. ', function() { +describe('uiScroll visibility.', () => { 'use strict'; beforeEach(module('ui.scroll')); beforeEach(module('ui.scroll.test.datasources')); - var getScrollSettings = function() { - return { - datasource: 'myMultipageDatasource', - viewportHeight: 200, - itemHeight: 40, - bufferSize: 3, - adapter: 'adapter' - }; + const scrollSettings = { + datasource: 'myMultipageDatasource', + viewportHeight: 200, + itemHeight: 40, + bufferSize: 3, + adapter: 'adapter' }; - var checkContent = function(rows, count) { + const checkContent = (rows, count) => { + expect(rows.length).toBe(count); for (var i = 1; i < count - 1; i++) { - var row = rows[i]; - expect(row.tagName.toLowerCase()).toBe('div'); - expect(row.innerHTML).toBe(i + ': item' + i); + expect(rows[i].innerHTML).toBe(i + ': item' + i); } }; - describe('Viewport visibility changing. ', function() { - var onePackItemsCount = 3 + 2; - var twoPacksItemsCount = 3 * 3 + 2; + const onePackItemsCount = 3 * 1 + 2; + const twoPacksItemsCount = 3 * 2 + 2; + const threePacksItemsCount = 3 * 3 + 2; - it('Should create 9 divs with data (+ 2 padding divs).', function() { - runTest(getScrollSettings(), - function(viewport) { - expect(viewport.children().length).toBe(twoPacksItemsCount); + describe('Viewport visibility changing\n', () => { + + it('should create 9 divs with data (+ 2 padding divs)', () => + runTest(scrollSettings, + (viewport) => { expect(viewport.scrollTop()).toBe(0); - expect(viewport.children().css('height')).toBe('0px'); - expect(angular.element(viewport.children()[twoPacksItemsCount - 1]).css('height')).toBe('0px'); - checkContent(viewport.children(), twoPacksItemsCount); + checkContent(viewport.children(), threePacksItemsCount); } - ); - }); + ) + ); - it('Should preserve elements after visibility switched off (display:none).', function() { - runTest(getScrollSettings(), - function(viewport, scope) { + it('should preserve elements after visibility switched off (display:none)', () => + runTest(scrollSettings, + (viewport, scope) => { viewport.css('display', 'none'); scope.$apply(); - expect(viewport.children().length).toBe(twoPacksItemsCount); expect(viewport.scrollTop()).toBe(0); - expect(viewport.children().css('height')).toBe('0px'); - expect(angular.element(viewport.children()[twoPacksItemsCount - 1]).css('height')).toBe('0px'); - checkContent(viewport.children(), twoPacksItemsCount); + checkContent(viewport.children(), threePacksItemsCount); } - ); - }); - + ) + ); - it('Should only load one batch with visibility switched off (display:none).', function() { - runTest(getScrollSettings(), - function(viewport, scope) { + it('should only load one batch with visibility switched off (display:none)', () => + runTest(scrollSettings, + (viewport, scope) => { viewport.css('display', 'none'); scope.adapter.reload(); - expect(viewport.children().length).toBe(onePackItemsCount); expect(viewport.scrollTop()).toBe(0); - expect(viewport.children().css('height')).toBe('0px'); - expect(angular.element(viewport.children()[onePackItemsCount - 1]).css('height')).toBe('0px'); checkContent(viewport.children(), onePackItemsCount); } - ); - }); + ) + ); - it('Should load full set after css-visibility switched back on.', function() { - var scrollSettings = getScrollSettings(); + it('should load full set after css-visibility switched back on', () => runTest(scrollSettings, - function(viewport, scope, $timeout) { + (viewport, scope, $timeout) => { viewport.css('display', 'none'); scope.adapter.reload(); @@ -81,99 +69,62 @@ describe('uiScroll visibility. ', function() { scope.$apply(); $timeout.flush(); - let rows = viewport.children(); - expect(rows.length).toBe(twoPacksItemsCount); expect(viewport.scrollTop()).toBe(0); - expect(rows.css('height')).toBe('0px'); - expect(angular.element(rows[twoPacksItemsCount - 1]).css('height')).toBe('0px'); - checkContent(rows, onePackItemsCount); + checkContent(viewport.children(), threePacksItemsCount); + expect(scope.adapter.topVisible).toBe('item1'); } - ); - }); - - it('Should load full set after scope-visibility switched back on.', function() { - var scrollSettings = getScrollSettings(); - scrollSettings.wrapper = { - start: '
    ', - end: '
    ' - }; - runTest(scrollSettings, - function(viewport, scope, $timeout) { + ) + ); + + it('should load full set after scope-visibility switched back on', () => + runTest(Object.assign({}, scrollSettings, { + wrapper: { + start: '
    ', + end: '
    ' + } + }), (viewport, scope) => { scope.show = false; - scope.adapter.reload(); scope.$apply(); + expect(viewport.children().length).toBe(0); scope.show = true; scope.$apply(); - $timeout.flush(); - - let rows = viewport.children().children(); - expect(rows.length).toBe(twoPacksItemsCount); expect(viewport.scrollTop()).toBe(0); - expect(rows.css('height')).toBe('0px'); - expect(angular.element(rows[twoPacksItemsCount - 1]).css('height')).toBe('0px'); - checkContent(rows, onePackItemsCount); + checkContent(viewport.children().children(), threePacksItemsCount); }, { scope: { show: true } } - ); - }); - - it('Should stay on the 1st item after the visibility is on (infinite list).', function() { - var scrollSettings = getScrollSettings(); - scrollSettings.datasource = 'myInfiniteDatasource'; - scrollSettings.topVisible = 'topVisible'; - runTest(scrollSettings, - function(viewport, scope, $timeout) { - viewport.css('display', 'none'); - scope.adapter.reload(); - scope.$apply(); - - viewport.css('display', 'block'); - scope.$apply(); - $timeout.flush(); - - expect(scope.topVisible).toBe('item1'); - } - ); - }); + ) + ); }); - describe('Items visibility changing. ', function() { - var scrollSettings = getScrollSettings(); - scrollSettings.itemHeight = '0'; - var onePackItemsCount = 3 + 2; - var twoPacksItemsCount = 3 * 2 + 2; - - it('Should load only one batch with items height = 0', function() { - runTest(scrollSettings, - function(viewport) { + describe('Items visibility changing\n', () => { + it('should load only one batch with items height = 0', () => + runTest(Object.assign({}, scrollSettings, { itemHeight: '0' }), + (viewport) => { expect(viewport.children().length).toBe(onePackItemsCount); expect(viewport.scrollTop()).toBe(0); checkContent(viewport.children(), onePackItemsCount); } - ); - }); - - it('Should continue loading after the height of some item switched to non-zero.', function() { - runTest(scrollSettings, - function(viewport, scope, $timeout) { + ) + ); + it('should load one more batch after the height of some item is set to a positive value', () => + runTest(Object.assign({}, scrollSettings, { itemHeight: '0' }), + (viewport, scope, $timeout) => { angular.element(viewport.children()[onePackItemsCount - 2]).css('height', 40); expect(angular.element(viewport.children()[onePackItemsCount - 2]).css('height')).toBe('40px'); scope.$apply(); $timeout.flush(); - expect(viewport.children().length).toBe(twoPacksItemsCount); expect(viewport.scrollTop()).toBe(0); checkContent(viewport.children(), twoPacksItemsCount); } - ); - }); + ) + ); }); - }); diff --git a/test/config/karma.conf.files.js b/test/config/karma.conf.files.js index f8d44881..eb1a1549 100644 --- a/test/config/karma.conf.files.js +++ b/test/config/karma.conf.files.js @@ -6,7 +6,7 @@ var files = [ 'https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular-mocks.js', '../misc/test.css', '../misc/datasources.js', - '../misc/scaffolding.js', + '../misc/scaffolding*.js', '../*Spec.js', { pattern: scrollerPath + '*.js.map', diff --git a/test/misc/scaffolding.js b/test/misc/scaffolding.js index 91b06e5a..a63d00cf 100644 --- a/test/misc/scaffolding.js +++ b/test/misc/scaffolding.js @@ -21,34 +21,6 @@ function createHtml(settings) { ''; } -function createGridHtml (settings) { - var viewportStyle = ' style="height:' + (settings.viewportHeight || 200) + 'px"'; - var columns = ['col0', 'col1', 'col2', 'col3']; - - var html = - '' + - '' + - ''; - columns.forEach(col => { html += - ''; - }); html += - '' + - '' + - '' + - ''; - if(settings.rowTemplate) { - html += settings.rowTemplate; - } else { - columns.forEach(col => { html += - ''; - }); - } html += - '' + - '' + - '
    ' + col + '
    {{item.' + col + '}}
    '; - return html; -} - function finalize(scroller, options = {}, scope, $timeout) { scroller.remove(); @@ -71,7 +43,6 @@ function runTest(scrollSettings, run, options = {}) { var compile = function() { $compile(scroller)(scope); scope.$apply(); - $timeout.flush(); }; if (typeof options.catch === 'function') { @@ -92,31 +63,4 @@ function runTest(scrollSettings, run, options = {}) { } } }); -} - -function runGridTest(scrollSettings, run, options = {}) { - inject(function($rootScope, $compile, $window, $timeout) { - var scroller = angular.element(createGridHtml(scrollSettings)); - var scope = $rootScope.$new(); - - angular.element(document).find('body').append(scroller); - var head = angular.element(scroller.children()[0]); - var body = angular.element(scroller.children()[1]); - - if (options.scope) { - angular.extend(scope, options.scope); - } - - $compile(scroller)(scope); - - scope.$apply(); - $timeout.flush(); - - try { - run(head, body, scope, $timeout); - } finally { - finalize(scroller, options, scope, $timeout); - } - - }); } \ No newline at end of file diff --git a/test/misc/scaffoldingGrid.js b/test/misc/scaffoldingGrid.js new file mode 100644 index 00000000..fd32b7ee --- /dev/null +++ b/test/misc/scaffoldingGrid.js @@ -0,0 +1,61 @@ +function createGridHtml (settings) { + var viewportStyle = ' style="height:' + (settings.viewportHeight || 200) + 'px"'; + var columns = ['col0', 'col1', 'col2', 'col3']; + + var html = + '' + + '' + + ''; + columns.forEach(col => { html += + ''; + }); html += + '' + + '' + + '' + + ''; + if(settings.rowTemplate) { + html += settings.rowTemplate; + } else { + columns.forEach(col => { html += + ''; + }); + } html += + '' + + '' + + '
    ' + col + '
    {{item.' + col + '}}
    '; + return html; +} + +function finalize(scroller, options = {}, scope, $timeout) { + scroller.remove(); + + if (typeof options.cleanupTest === 'function') { + options.cleanupTest(scroller, scope, $timeout); + } +} + +function runGridTest(scrollSettings, run, options = {}) { + inject(function($rootScope, $compile, $window, $timeout) { + var scroller = angular.element(createGridHtml(scrollSettings)); + var scope = $rootScope.$new(); + + angular.element(document).find('body').append(scroller); + var head = angular.element(scroller.children()[0]); + var body = angular.element(scroller.children()[1]); + + if (options.scope) { + angular.extend(scope, options.scope); + } + + $compile(scroller)(scope); + + scope.$apply(); + $timeout.flush(); + + try { + run(head, body, scope, $timeout); + } finally { + finalize(scroller, options, scope, $timeout); + } + }); +} \ No newline at end of file