├── .babelrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── bower.json ├── demo ├── adapter │ ├── adapter.html │ └── adapter.js ├── adapterSync │ ├── adapterSync.html │ ├── adapterSync.js │ └── server.js ├── animation │ ├── animation.html │ └── animation.js ├── append │ ├── append.html │ ├── append.js │ └── server.js ├── bottomVisible │ ├── bottomVisibleAdapter.html │ └── bottomVisibleAdapter.js ├── bufferItems │ ├── bufferItems.html │ └── bufferItems.js ├── cache │ ├── cache.html │ └── cache.js ├── chat │ ├── chat.html │ └── chat.js ├── controllerAs │ ├── controllerAs.html │ └── controllerAs.js ├── css │ ├── bootstrap.css │ ├── style.css │ └── style.less ├── differentItemHeights │ ├── differentItemHeights.html │ └── differentItemHeights.js ├── disabled │ ├── disabled.html │ └── disabled.js ├── grid-dnd-sort-2 │ ├── angular-dnd.js │ ├── grid-dnd-sort.html │ ├── grid-dnd-sort.js │ └── grid.css ├── grid-dnd-sort │ ├── grid-dnd-sort.html │ ├── grid-dnd-sort.js │ └── grid.css ├── grid-dnd-widths │ ├── grid-dnd-widths.html │ ├── grid-dnd-widths.js │ └── grid.css ├── grid-layout-apply │ ├── grid-layout-apply.html │ ├── grid-layout-apply.js │ └── grid.css ├── grid-layout-manipulations │ ├── grid-layout-manipulations.html │ ├── grid-layout-manipulations.js │ └── grid.css ├── grid-scopes-wrapping │ ├── grid-scopes-wrapping.html │ ├── grid-scopes-wrapping.js │ └── grid.css ├── index.html ├── insideComponent │ ├── insideComponent.html │ └── insideComponent.js ├── insideDirective │ ├── insideDirective.html │ └── insideDirective.js ├── isLoading │ ├── isLoading.html │ ├── isLoading.js │ ├── isLoadingAdapter.html │ └── isLoadingAdapter.js ├── jquery │ ├── jquery.html │ └── jquery.js ├── listScroller │ ├── listScroller.html │ └── listScroller.js ├── multipleLists │ ├── multipleLists.html │ └── multipleLists.js ├── multipleReloadTest │ ├── multipleReload.html │ └── multipleReload.js ├── outOfBuffer │ ├── outOfBuffer.html │ └── outOfBuffer.js ├── persistentScroll │ ├── persistentScroll.html │ └── persistentScroll.js ├── positionedList │ ├── positionedList.html │ └── positionedList.js ├── rebuilding │ ├── rebuilding.html │ └── rebuilding.js ├── reload100 │ ├── reload100.html │ └── reload100.js ├── remote │ ├── remote.html │ └── remote.js ├── scopeDatasource │ ├── scopeDatasource.html │ └── scopeDatasource.js ├── scrollBubblingPrevent │ ├── scrollBubblingPrevent.html │ └── scrollBubblingPrevent.js ├── serviceDatasource │ ├── serviceDatasource.html │ └── serviceDatasource.js ├── tableScroller │ ├── tableScroller.html │ └── tableScroller.js ├── topVisible │ ├── topVisible.html │ ├── topVisible.js │ ├── topVisibleAdapter.html │ └── topVisibleAdapter.js ├── ui-scroll-demo.gif ├── userIndexes │ ├── userIndexes.html │ └── userIndexes.js ├── visibility │ ├── visibility.html │ └── visibility.js ├── windowViewport │ ├── windowViewport-iframe.html │ ├── windowViewport.html │ └── windowViewport.js └── windowviewportInline │ ├── windowviewportInline.html │ └── windowviewportInline.js ├── dist ├── ui-scroll-grid.js ├── ui-scroll-grid.js.map ├── ui-scroll-grid.min.js ├── ui-scroll-grid.min.js.map ├── ui-scroll-jqlite.js ├── ui-scroll-jqlite.min.js ├── ui-scroll.js ├── ui-scroll.js.map ├── ui-scroll.min.js └── ui-scroll.min.js.map ├── package-lock.json ├── package.json ├── src ├── modules │ ├── adapter.js │ ├── buffer.js │ ├── elementRoutines.js │ ├── jqLiteExtras.js │ ├── padding.js │ ├── utils.js │ └── viewport.js ├── ui-scroll-grid.js ├── ui-scroll-jqlite.js └── ui-scroll.js ├── test ├── .jshintrc ├── AdapterTestsSpec.js ├── AssigningSpec.js ├── BasicSetupSpec.js ├── BasicTestsSpec.js ├── BufferCleanupSpec.js ├── GridTestsSpec.js ├── PaddingsSpec.js ├── UserIndicesSpec.js ├── VisibilitySwitchingSpec.js ├── config │ ├── karma.conf.files.js │ └── karma.conf.js ├── jqliteExtrasSpec.js └── misc │ ├── datasources.js │ ├── helpers.js │ ├── scaffolding.js │ ├── scaffoldingGrid.js │ └── test.css └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions", 9 | "IE >= 9" 10 | ] 11 | } 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: ui-scroll build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | workflow_dispatch: 8 | inputs: 9 | cause: 10 | description: 'Reason' 11 | required: true 12 | default: 'Manual triggering' 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 5 18 | steps: 19 | - name: Dispatched? 20 | if: ${{ github.event_name == 'workflow_dispatch' }} 21 | run: | 22 | echo "This is dispatched" 23 | echo "Build reason: ${{ github.event.inputs.cause }}" 24 | 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - name: Use Node.js 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: 18 32 | 33 | - name: Install dependencies 34 | run: npm ci 35 | 36 | - name: Run tests 37 | env: 38 | CI: true 39 | BROWSER: headless 40 | run: npm test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | temp 3 | *.log 4 | .idea -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "browser": true, 4 | "eqnull": true, 5 | "expr": true, 6 | "esversion": 6, 7 | "immed": true, 8 | "laxbreak": true, 9 | "loopfunc": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "noempty": true, 13 | "nonew": true, 14 | "quotmark": true, 15 | "smarttabs": true, 16 | "strict": false, 17 | "sub": true, 18 | "trailing": true, 19 | "undef": true, 20 | "unused": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /node_modules/ 3 | /src 4 | /temp 5 | /test 6 | /.github 7 | .babelrc 8 | .gitignore 9 | .jshintrc 10 | .npmignore 11 | webpack.config.js 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2023 Hill30 INC, https://github.com/Hill30 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-scroll", 3 | "description": "AngularJS infinite scrolling module", 4 | "version": "1.9.1", 5 | "main": "./dist/ui-scroll.js", 6 | "homepage": "https://github.com/angular-ui/ui-scroll.git", 7 | "license": "MIT", 8 | "keywords": [ 9 | "angular", 10 | "angularjs", 11 | "angular.ui", 12 | "angular-ui", 13 | "ui.scroll", 14 | "ui-scroll", 15 | "angular-ui-scroll", 16 | "virtual", 17 | "unlimited", 18 | "infinite", 19 | "live", 20 | "perpetual", 21 | "scroll", 22 | "scroller", 23 | "scrolling" 24 | ], 25 | "ignore": [ 26 | "node_modules", 27 | "src", 28 | "temp", 29 | "test", 30 | ".github", 31 | ".gitignore", 32 | ".jshintrc", 33 | ".npmignore", 34 | "Gruntfile.js", 35 | "package.json", 36 | "webpack.config.js" 37 | ] 38 | } -------------------------------------------------------------------------------- /demo/adapter/adapter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scroller Demo (adapter) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Adapter (updatable scroller)

18 | 19 |
20 | 26 |
27 | 28 |
29 |

1st list

30 | 31 |
32 | ...data loading... 33 | 1st list is loaded 34 |
35 | 36 |
37 | 38 | 39 | 40 |
41 | 42 |
43 |
{{item.content}}
47 |
48 |
49 | 50 |
51 | 52 |
53 |

2st list

54 | 55 |
56 | ...data loading... 57 | 2nd list is loaded 58 |
59 | 60 |
61 | 62 | 63 | 64 |
65 | 66 |
67 |
{{item.content}}
70 |
71 | 72 |
73 | 74 |
75 | 76 | -------------------------------------------------------------------------------- /demo/adapter/adapter.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | 3 | .controller('mainController', [ 4 | '$scope', '$timeout', 5 | function ($scope, $timeout) { 6 | 7 | $scope.title = 'Main Controller'; 8 | 9 | var datasource = {}; 10 | 11 | datasource.get = function (index, count, success) { 12 | $timeout(function () { 13 | var result = []; 14 | for (var i = index; i <= index + count - 1; i++) { 15 | result.push({ 16 | id: i, 17 | content: "item #" + i 18 | }); 19 | } 20 | success(result); 21 | }, 100); 22 | }; 23 | 24 | $scope.datasource = datasource; 25 | } 26 | ]) 27 | 28 | .controller('firstController', ['$scope', function ($scope) { 29 | $scope.title = 'First Controller'; 30 | 31 | $scope.firstListAdapter = { 32 | remain: true 33 | }; 34 | 35 | $scope.updateList1 = function () { 36 | return $scope.firstListAdapter.applyUpdates(function (item, scope) { 37 | return item.content += ' *'; 38 | }); 39 | }; 40 | 41 | $scope.removeFromList1 = function () { 42 | return $scope.firstListAdapter.applyUpdates(function (item, scope) { 43 | if (scope.$index % 2 === 0) { 44 | return []; 45 | } 46 | }); 47 | }; 48 | 49 | var idList1 = 1000; 50 | $scope.addToList1 = function () { 51 | return $scope.firstListAdapter.applyUpdates(function (item, scope) { 52 | var newItem; 53 | newItem = void 0; 54 | if (scope.$index === 2) { 55 | newItem = { 56 | id: idList1, 57 | content: 'a new one #' + idList1 58 | }; 59 | idList1++; 60 | return [item, newItem]; 61 | } 62 | }); 63 | }; 64 | }]) 65 | 66 | .controller('secondController', ['$scope', function ($scope) { 67 | $scope.title = 'Second Controller'; 68 | 69 | $scope.updateList2 = function () { 70 | return $scope.second.list.adapter.applyUpdates(function (item, scope) { 71 | return item.content += ' *'; 72 | }); 73 | }; 74 | 75 | $scope.removeFromList2 = function () { 76 | return $scope.second.list.adapter.applyUpdates(function (item, scope) { 77 | if (scope.$index % 2 !== 0) { 78 | return []; 79 | } 80 | }); 81 | }; 82 | 83 | var idList2 = 2000; 84 | $scope.addToList2 = function () { 85 | return $scope.second.list.adapter.applyUpdates(function (item, scope) { 86 | var newItem; 87 | newItem = void 0; 88 | if (scope.$index === 4) { 89 | newItem = { 90 | id: idList2, 91 | content: 'a new one #' + idList2 92 | }; 93 | idList2++; 94 | return [item, newItem]; 95 | } 96 | }); 97 | }; 98 | }]); 99 | -------------------------------------------------------------------------------- /demo/adapterSync/adapterSync.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Adapter sync 6 | 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 33 |
34 | 35 | browse other examples 36 | 37 |

Adapter: append, prepend and remove sync

38 | 39 |
40 |

41 | This demo had been created to show Adapter 'append', 'prepend' and 'applyUpdates' methods in action. 42 | The main point here is that all changes made on the front end have to be synced with the back end. 43 | For this purpose a special Server module was introduced to emulate the remote server. 44 | The following public methods are implemented by this Server factory:
45 |

68 | The initial data set consists of 40 items and can be extended/reduced unlimitedly. 69 |

70 |

71 | The implementation of the Server factory is not trivial, it is based on indices variations. 72 | Also you may see that new items would not be appended (via 'Append one item' or 73 | 'Insert some after index' buttons) to the viewport immediately if the EOF (end of file) is not reached. 74 | The same is true for prepend operations ('Prepend one item'): BOF (begin of file) must be reached, 75 | otherwise your new items will be rendered only after scrolling to the very top... 76 |

77 |
78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 | 88 |
89 |
90 | 91 |
92 | 93 | 101 | 102 |
103 | 104 | -------------------------------------------------------------------------------- /demo/adapterSync/adapterSync.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('application', ['ui.scroll', 'server']); 2 | 3 | app.controller('mainController', [ 4 | '$scope', 'Server', function ($scope, Server) { 5 | 6 | var ctrl = this; 7 | 8 | ctrl.datasource = { 9 | get: function (index, count, success) { 10 | console.log('request by index = ' + index + ', count = ' + count); 11 | Server.request(index, count).then(function (result) { 12 | if (result.items) { 13 | console.log('resolved ' + result.items.length + ' items'); 14 | } 15 | success(result.items); 16 | }); 17 | } 18 | }; 19 | 20 | $scope.$watch('adapter', (prev, next) => { 21 | console.log('The adapter has been initialized'); 22 | }); 23 | 24 | ctrl.prepend = function () { 25 | Server.prependItem(' ***').then(function (newItem) { 26 | if (ctrl.adapter.isBOF()) { 27 | ctrl.adapter.prepend([newItem]); 28 | } 29 | }); 30 | }; 31 | 32 | ctrl.append = function () { 33 | Server.appendItem(' ***').then(function (newItem) { 34 | if (ctrl.adapter.isEOF()) { 35 | ctrl.adapter.append([newItem]); 36 | } 37 | }); 38 | }; 39 | 40 | // todo dhilt : need to implement it properly 41 | ctrl.removeAll = function () { 42 | ctrl.adapter.applyUpdates(function (item) { 43 | if (item.id) { 44 | Server.removeItemById(item.id); 45 | return []; 46 | } 47 | }); 48 | }; 49 | 50 | ctrl.remove = function (itemRemove) { 51 | Server.removeItemById(itemRemove.id).then(function (result) { 52 | if (result !== false) { 53 | ctrl.adapter.applyUpdates(function (item) { 54 | if (item.id === itemRemove.id) { 55 | return []; 56 | } 57 | }); 58 | } 59 | }); 60 | }; 61 | 62 | ctrl.removeFirst = function () { 63 | Server.removeFirst().then(function (indexRemoved) { 64 | if (indexRemoved !== false) { 65 | ctrl.adapter.applyUpdates(indexRemoved, []); 66 | } 67 | }); 68 | }; 69 | 70 | ctrl.removeLast = function () { 71 | Server.removeLast().then(function (indexRemoved) { 72 | if (indexRemoved !== false) { 73 | ctrl.adapter.applyUpdates(indexRemoved, []); 74 | } 75 | }); 76 | }; 77 | 78 | ctrl.insertSome = function (indexToInsert) { 79 | indexToInsert = parseInt(indexToInsert, 10); 80 | var promises = [ 81 | Server.insertAfterIndex(indexToInsert, ' *** (1)'), 82 | Server.insertAfterIndex(indexToInsert + 1, ' *** (2)'), 83 | Server.insertAfterIndex(indexToInsert + 2, ' *** (3)') 84 | ]; 85 | Promise.all(promises).then(function (result) { 86 | if (result && result.length) { 87 | // need to protect from null 88 | var _result = []; 89 | for(var i = 0; i < result.length; i++) { 90 | if(result[i]) { 91 | _result.push(result[i]); 92 | } 93 | } 94 | if(_result.length) { 95 | var item = getItemByIndex(indexToInsert); 96 | if(item) { 97 | _result.unshift(item); 98 | } 99 | ctrl.adapter.applyUpdates(indexToInsert, _result); 100 | } 101 | } 102 | }); 103 | }; 104 | 105 | function getItemByIndex(index) { 106 | var foundItem = null; 107 | // use Adapter.applyUpdates to get indexed item from Buffer 108 | ctrl.adapter.applyUpdates(function (item) { 109 | if (item.index === index) { 110 | foundItem = item; 111 | } 112 | }); 113 | return foundItem; 114 | } 115 | 116 | ctrl.datasource.minIndex = Server.firstIndex; 117 | ctrl.datasource.maxIndex = Server.lastIndex; 118 | } 119 | ]); 120 | -------------------------------------------------------------------------------- /demo/adapterSync/server.js: -------------------------------------------------------------------------------- 1 | angular.module('server', []).factory('Server', 2 | ['$timeout', '$q', function ($timeout, $q) { 3 | 4 | var ServerFactory = { 5 | 6 | firstIndex: 1, 7 | 8 | lastIndex: 40, 9 | 10 | delay: 100, 11 | 12 | data: [], 13 | 14 | absIndex: 1, 15 | 16 | generateId: function () { 17 | var d = '-'; 18 | function S4() { 19 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 20 | } 21 | return (S4() + S4() + d + S4() + d + S4() + d + S4() + d + S4() + S4() + S4()); 22 | }, 23 | 24 | generateItem: function (index) { 25 | return { 26 | index: index, 27 | id: this.generateId(), 28 | content: 'Item #' + this.absIndex++ 29 | } 30 | }, 31 | 32 | init: function () { 33 | for (var i = this.firstIndex; i <= this.lastIndex; i++) { 34 | this.data.push(this.generateItem(i)); 35 | } 36 | }, 37 | 38 | getItem: function (index) { 39 | for (var i = this.data.length - 1; i >= 0; i--) { 40 | if (this.data[i].index === index) { 41 | return this.data[i]; 42 | } 43 | } 44 | }, 45 | 46 | returnDeferredResult: function (result) { 47 | var deferred = $q.defer(); 48 | $timeout(function () { 49 | deferred.resolve(result); 50 | }, this.delay); 51 | return deferred.promise; 52 | }, 53 | 54 | request: function (index, count) { 55 | var start = index; 56 | var end = index + count - 1; 57 | var item, result = { 58 | items: [] 59 | }; 60 | if (start <= end) { 61 | for (var i = start; i <= end; i++) { 62 | if (item = this.getItem(i)) { 63 | result.items.push(item); 64 | } 65 | } 66 | } 67 | return this.returnDeferredResult(result); 68 | }, 69 | 70 | prependItem: function (params) { 71 | var newItem = this.generateItem(--this.firstIndex); 72 | newItem.content += params; 73 | this.data.unshift(newItem); 74 | return this.returnDeferredResult(newItem); 75 | }, 76 | 77 | appendItem: function (params) { 78 | var newItem = this.generateItem(++this.lastIndex); 79 | newItem.content += params; 80 | this.data.push(newItem); 81 | return this.returnDeferredResult(newItem); 82 | }, 83 | 84 | removeFirst: function () { 85 | var firstItem = this.data.find(i => i.index === this.firstIndex); 86 | if(!firstItem) { 87 | return $q.reject(); 88 | } 89 | return this.removeItemById(firstItem.id); 90 | }, 91 | 92 | removeLast: function () { 93 | var lastItem = this.data.find(i => i.index === this.lastIndex); 94 | if(!lastItem) { 95 | return $q.reject(); 96 | } 97 | return this.removeItemById(lastItem.id); 98 | }, 99 | 100 | removeItemById: function (itemId) { 101 | var length = this.data.length; 102 | for (var i = 0; i < length; i++) { 103 | if (this.data[i].id === itemId) { 104 | var indexRemoved = this.data[i].index; 105 | if(indexRemoved > this.firstIndex) { 106 | for (var j = i; j < length; j++) { 107 | this.data[j].index--; 108 | } 109 | } 110 | this.data.splice(i, 1); 111 | this.setIndices(); 112 | return this.returnDeferredResult(indexRemoved); 113 | } 114 | } 115 | return this.returnDeferredResult(false); 116 | }, 117 | 118 | insertAfterIndex: function (index, params) { 119 | if(index < this.firstIndex || index > this.lastIndex) { 120 | return this.returnDeferredResult(null); 121 | } 122 | var length = this.data.length, item; 123 | for (var i = 0; i < length; i++) { 124 | if (this.data[i].index === index) { 125 | for (var j = i + 1; j < length; j++) { 126 | this.data[j].index++; 127 | } 128 | item = this.generateItem(index + 1); 129 | item.content += params; 130 | this.data.splice(i + 1, 0, item); 131 | this.setIndices(); 132 | return this.returnDeferredResult(item); 133 | } 134 | } 135 | return this.returnDeferredResult(null); 136 | }, 137 | 138 | setIndices: function () { 139 | if(!this.data.length) { 140 | this.firstIndex = 1; 141 | this.lastIndex = 1; 142 | return; 143 | } 144 | this.firstIndex = this.data[0].index; 145 | this.lastIndex = this.data[0].index; 146 | for (var i = this.data.length - 1; i >= 0; i--) { 147 | if(this.data[i].index > this.lastIndex) { 148 | this.lastIndex = this.data[i].index; 149 | } 150 | if(this.data[i].index < this.firstIndex) { 151 | this.firstIndex = this.data[i].index; 152 | } 153 | } 154 | } 155 | }; 156 | 157 | ServerFactory.init(); 158 | 159 | return ServerFactory; 160 | } 161 | ]); 162 | -------------------------------------------------------------------------------- /demo/animation/animation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scroller Demo (animation) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | browse other examples 15 |

Animation demo

16 | 17 |
18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
29 | {{$index}}) {{item.content}} 30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /demo/animation/animation.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll', 'ngAnimate']).controller('mainController', [ 2 | '$scope', '$log', '$timeout', function($scope, console, $timeout) { 3 | var datasource, idList; 4 | datasource = {}; 5 | datasource.get = function(index, count, success) { 6 | return $timeout(function() { 7 | var i, item, j, ref, ref1, result; 8 | result = []; 9 | for (i = j = ref = index, ref1 = index + count - 1; ref <= ref1 ? j <= ref1 : j >= ref1; i = ref <= ref1 ? ++j : --j) { 10 | if (i <= 0 || i > 14) { 11 | continue; 12 | } 13 | item = {}; 14 | item.id = i; 15 | item.content = "item #" + i; 16 | result.push(item); 17 | } 18 | return success(result); 19 | }, 100); 20 | }; 21 | $scope.datasource = datasource; 22 | $scope.adapterContainer = { 23 | adapter: { 24 | remain: true 25 | } 26 | }; 27 | $scope.updateList = function() { 28 | return $scope.adapterContainer.adapter.applyUpdates(function(item, scope) { 29 | return item.content += ' *'; 30 | }); 31 | }; 32 | $scope.removeFromList = function() { 33 | return $scope.adapterContainer.adapter.applyUpdates(function(item, scope) { 34 | if (scope.$index % 2 === 0) { 35 | return []; 36 | } 37 | }); 38 | }; 39 | $scope.refresh = function() { 40 | return $scope.adapterContainer.adapter.reload(); 41 | }; 42 | idList = 1000; 43 | return $scope.addToList = function() { 44 | return $scope.adapterContainer.adapter.applyUpdates(function(item, scope) { 45 | var newItem; 46 | newItem = void 0; 47 | if (scope.$index === 2) { 48 | newItem = { 49 | id: idList, 50 | content: 'a new one #' + idList 51 | }; 52 | idList++; 53 | return [item, newItem]; 54 | } 55 | }); 56 | }; 57 | } 58 | ]); 59 | 60 | 61 | 62 | 63 | /* 64 | //# sourceURL=src/animation.js 65 | */ 66 | 67 | // --- 68 | // generated by coffee-script 1.9.2 -------------------------------------------------------------------------------- /demo/append/append.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Append and prepend 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | browse other examples 17 | 18 |

Adapter: append and prepend sync

19 | 20 |
21 |

22 | This sample demonstrates an ability to append and prepend new items to the initial data set via adapter. 23 | New appended and prepended items have to be synced with the real data source. 24 | For this purpose a special Server module was implemented to emulate the remote. 25 | The Server hides all indices magic, so the front-end controller logic (ui-scroll datasource implementation) looks very simple. 26 |

27 |

28 | Two methods are implemented on the controller's scope: prepend and append. 29 | They work with two Server methods: prependItem and appendItem. 30 | The additional point is that new items would not be appended to the viewport if the EOF (end of file) is not reached. 31 | Also new items would not be prepended to the viewport if the BOF (begin of file) is not reached. 32 |

33 |

34 | For more complex and powerful sample you may follow this demo. 35 |

36 |
37 | 38 |
39 | 40 | 41 |
42 | 43 |
44 | 45 | 50 | 51 |
52 | 53 | -------------------------------------------------------------------------------- /demo/append/append.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('application', ['ui.scroll', 'server']); 2 | 3 | app.controller('mainController', [ 4 | '$scope', 'Server', 5 | function($scope, Server) { 6 | 7 | $scope.datasource = { 8 | get: function(index, count, success) { 9 | console.log('request by index = ' + index + ', count = ' + count); 10 | Server.request(index, count).then(function(result) { 11 | if (result.items.length) { 12 | console.log('resolved ' + result.items.length + ' items'); 13 | } 14 | success(result.items); 15 | }); 16 | } 17 | }; 18 | 19 | $scope.prepend = function() { 20 | var newItem = Server.prependItem(' (new)*'); 21 | if ($scope.adapter.isBOF()) { 22 | $scope.adapter.prepend([newItem]); 23 | } 24 | }; 25 | 26 | $scope.append = function() { 27 | var newItem = Server.appendItem(' (new)*'); 28 | if ($scope.adapter.isEOF()) { 29 | $scope.adapter.append([newItem]); 30 | } 31 | }; 32 | 33 | } 34 | ]); -------------------------------------------------------------------------------- /demo/append/server.js: -------------------------------------------------------------------------------- 1 | angular.module('server', []).factory('Server', 2 | ['$timeout', '$q', function($timeout, $q) { 3 | 4 | var ServerFactory = { 5 | 6 | max: 50, 7 | 8 | first: 1, 9 | 10 | delay: 100, 11 | 12 | data: [], 13 | 14 | prependedData: [], 15 | 16 | appendedData: [], 17 | 18 | generateItem: function(number) { 19 | return { 20 | number: number, 21 | content: 'Item #' + number 22 | } 23 | }, 24 | 25 | init: function() { 26 | for (var i = this.first - 1; i <= this.max; i++) { 27 | this.data.push(this.generateItem(i)); 28 | } 29 | }, 30 | 31 | getItem: function(index) { 32 | if (index < this.first) { 33 | return this.prependedData[(-1) * index]; 34 | } else if (index > this.max) { 35 | return this.appendedData[index - this.max - 1]; 36 | } else { 37 | return this.data[index]; 38 | } 39 | }, 40 | 41 | request: function(index, count) { 42 | var self = this; 43 | var deferred = $q.defer(); 44 | 45 | var start = index; 46 | var end = index + count - 1; 47 | 48 | $timeout(function() { 49 | var item, result = { 50 | items: [] 51 | }; 52 | if (start <= end) { 53 | for (var i = start; i <= end; i++) { 54 | if (item = self.getItem(i)) { 55 | result.items.push(item); 56 | } 57 | } 58 | } 59 | deferred.resolve(result); 60 | }, self.delay); 61 | 62 | return deferred.promise; 63 | }, 64 | 65 | prependItem: function(params) { 66 | var prependedDataIndex = this.first - this.prependedData.length - 1; 67 | var newItem = this.generateItem(prependedDataIndex); 68 | newItem.content += params; 69 | this.prependedData.push(newItem); 70 | return newItem; 71 | }, 72 | 73 | appendItem: function(params) { 74 | var appendedDataIndex = this.max + this.appendedData.length + 1; 75 | var newItem = this.generateItem(appendedDataIndex); 76 | newItem.content += params; 77 | this.appendedData.push(newItem); 78 | return newItem; 79 | } 80 | }; 81 | 82 | ServerFactory.init(); 83 | 84 | return ServerFactory; 85 | } 86 | ]); 87 | -------------------------------------------------------------------------------- /demo/bottomVisible/bottomVisibleAdapter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bottom visible (Adapter) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Bottom visible (Adapter)

18 | 19 |
20 | The Adapter implements bottomVisible property which is a reference to the item currently in the very bottom visible position. 21 | 22 |
23 |
<li ui-scroll="item in datasource" adapter="adapter">*{{item}}*</li>
24 |
25 | 26 |
27 |
bottom visible: {{adapter.bottomVisible}}
28 |
29 |
30 | 31 |
32 |
bottom visible: {{adapter.bottomVisible}}
33 |
34 | 35 |
36 |
*{{item}}*
37 |
38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /demo/bottomVisible/bottomVisibleAdapter.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | 5 | $scope.adapter = {}; 6 | 7 | $scope.datasource = {}; 8 | 9 | $scope.datasource.get = function (index, count, success) { 10 | $timeout(function () { 11 | var result = []; 12 | for (var i = index; i <= index + count - 1; i++) { 13 | result.push("item #" + i); 14 | } 15 | success(result); 16 | }, 100); 17 | }; 18 | 19 | } 20 | ]); 21 | -------------------------------------------------------------------------------- /demo/bufferItems/bufferItems.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Buffer first, last, length 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | browse other examples 18 | 19 |

Buffer first, last, length

20 | 21 |
22 | The ui-scroll Adapter has 3 read-only properties which provide information of current ui-scroll Buffer state. 23 | The buffer contains some visible items and some items that are out of visible part of the viewport. 24 | So with these properties we can get the topmost and the bottommost items that the ui-scroll is dealing with at the moment. 25 | At the template's layer it may look like 26 | 27 |
28 |
{{adapter.bufferFirst}}
29 | {{adapter.bufferLast}}
30 | {{adapter.bufferLength}}
31 | 
32 | <li ui-scroll="item in datasource" adapter="adapter">{{item}}</li>
33 |
34 |
35 | 36 |
37 |
First buffer {{adapter.bufferFirst}}
38 |
Last buffer {{adapter.bufferLast}}
39 |
Buffer length: {{adapter.bufferLength}}
40 |
41 | 42 |
43 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /demo/bufferItems/bufferItems.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | 5 | $scope.adapter = {}; 6 | 7 | $scope.datasource = {}; 8 | 9 | $scope.datasource.get = function (index, count, success) { 10 | $timeout(function () { 11 | var result = []; 12 | for (var i = index; i <= index + count - 1; i++) { 13 | result.push("item #" + i); 14 | } 15 | success(result); 16 | }, 0); 17 | }; 18 | 19 | } 20 | ]); 21 | -------------------------------------------------------------------------------- /demo/cache/cache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scroller Demo (cache option) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Cache within datasource implementation

18 | 19 |
20 |

21 | The cache that is implemented here is a facade over standard datasource. 22 | So all you need here is to initialize Cache: 23 |

24 |
datasource.cache.initialize();
25 |
26 | Base data retrieving method datasource.get() isn't changed. 27 | It may has any implementation like no any cache is presented. 28 |

29 |

30 | You may disable/enable Cache programmatically: 31 |

32 |
datasource.cache.isEnabled = true;
33 |
34 | Toggle-link demonstrates this ability. You can see the difference between cache and no-cache modes because of $timeout back-end delay emulation on datasource.get. 35 |

36 |
37 | 38 |
39 | Cache is {{datasource.cache.isEnabled ? 'enabled' : 'disabled'}} 40 | [toggle] 41 |
42 | 43 |
44 |
*{{item.content}}*
45 |
46 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/cache/cache.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']).controller('mainController', [ 2 | '$scope', '$log', '$timeout', function($scope, console, $timeout) { 3 | 4 | var datasource = {}; 5 | 6 | datasource.cache = { 7 | initialize: function() { 8 | this.isEnabled = true; 9 | this.items = {}; 10 | this.getPure = datasource.get; 11 | return datasource.get = this.getCached; 12 | }, 13 | 14 | getCached: function(index, count, successCallback) { 15 | var self; 16 | self = datasource.cache; 17 | if (self.isEnabled) { 18 | if (self.getItems(index, count, successCallback)) { 19 | return; 20 | } 21 | return self.getPure(index, count, function(result) { 22 | self.saveItems(index, count, result); 23 | return successCallback(result); 24 | }); 25 | } 26 | return self.getPure(index, count, successCallback); 27 | }, 28 | 29 | toggle: function() { 30 | this.isEnabled = !this.isEnabled; 31 | return this.items = {}; 32 | }, 33 | 34 | saveItems: function(index, count, resultItems) { 35 | var i, item, j, len, results; 36 | results = []; 37 | for (i = j = 0, len = resultItems.length; j < len; i = ++j) { 38 | item = resultItems[i]; 39 | if (!this.items.hasOwnProperty(index + i)) { 40 | results.push(this.items[index + i] = item); 41 | } else { 42 | results.push(void 0); 43 | } 44 | } 45 | return results; 46 | }, 47 | 48 | getItems: function(index, count, successCallback) { 49 | var i, isCached, j, ref, ref1, result; 50 | result = []; 51 | isCached = true; 52 | for (i = j = ref = index, ref1 = index + count - 1; j <= ref1; i = j += 1) { 53 | if (!this.items.hasOwnProperty(i)) { 54 | isCached = false; 55 | return; 56 | } 57 | result.push(this.items[i]); 58 | } 59 | successCallback(result); 60 | return true; 61 | } 62 | }; 63 | 64 | datasource.get = function(index, count, success) { 65 | return $timeout(function() { 66 | var i, item, j, ref, ref1, result; 67 | result = []; 68 | for (i = j = ref = index, ref1 = index + count - 1; ref <= ref1 ? j <= ref1 : j >= ref1; i = ref <= ref1 ? ++j : --j) { 69 | item = {}; 70 | item.content = "item #" + i; 71 | item.data = { 72 | some: false 73 | }; 74 | result.push(item); 75 | } 76 | return success(result); 77 | }, 100); 78 | }; 79 | 80 | $scope.datasource = datasource; 81 | 82 | datasource.cache.initialize(); 83 | } 84 | ]); -------------------------------------------------------------------------------- /demo/chat/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chat demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Chat demo

18 | 19 |
20 | Chat demo provides: 21 |
- initial position at the bottom of the dataset
22 |
- scroll up to retrieve new items with positive indexes
23 |
- special Server service to emulate the remote
24 |
25 | 26 |
27 |
{{adapter.topVisible.title}} -- top visible item
28 |
{{adapter.bottomVisible.title}} -- bottom visible item
29 |
30 | 31 | 38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /demo/chat/chat.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('application', ['ui.scroll']); 2 | 3 | app.factory('Server', [ 4 | '$timeout', '$q', function ($timeout, $q) { 5 | 6 | return { 7 | 8 | max: 99, 9 | 10 | first: 1, 11 | 12 | delay: 100, 13 | 14 | data: [], 15 | 16 | init: function () { 17 | for (var i = this.first; i <= this.max; i++) { 18 | this.data.push({ 19 | number: i, 20 | title: 'Message #' + i, 21 | text: Math.random().toString(36).substring(7) 22 | }); 23 | } 24 | }, 25 | 26 | request: function (start, end) { 27 | var self = this; 28 | var deferred = $q.defer(); 29 | 30 | $timeout(function () { 31 | var result = []; 32 | if (start <= end) { 33 | for (var i = start; i <= end; i++) { 34 | var serverDataIndex = (-1) * i + self.first; 35 | var item = self.data[serverDataIndex]; 36 | if (item) { 37 | result.push(item); 38 | } 39 | } 40 | } 41 | deferred.resolve(result); 42 | }, self.delay); 43 | 44 | return deferred.promise; 45 | } 46 | }; 47 | 48 | } 49 | ]); 50 | 51 | 52 | app.controller('mainController', [ 53 | '$scope', 'Server', function ($scope, Server) { 54 | var datasource = {}; 55 | 56 | Server.init(); 57 | 58 | datasource.get = function (index, count, success) { 59 | console.log('index = ' + index + '; count = ' + count); 60 | 61 | var start = index; 62 | var end = Math.min(index + count - 1, Server.first); 63 | 64 | Server.request(start, end).then(success); 65 | }; 66 | 67 | $scope.datasource = datasource; 68 | 69 | } 70 | ]); 71 | -------------------------------------------------------------------------------- /demo/controllerAs/controllerAs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scroller Demo (controller as) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | browse other examples 15 |

Controller as syntax for the Datasource and the Adapter

16 |
17 | If you have defined your controller in such way 18 |
19 |
<div ng-controller="mainController as mainCtrl">
20 |
21 | then you just shouldn't miss it when you define your datasource or adapter: 22 |
23 |
<div ui-scroll="item in mainCtrl.datasource" adapter="mainCtrl.adapter">{{item}}</div>
24 |
25 | "Hide/Show" button provides a nested scope between mainController and the Viewport via ng-if directive. 26 |
27 | 28 |
29 | 30 | 31 |
32 | 33 |
34 | data has been loaded 35 | data loading... 36 |
37 | 38 |
39 |
40 |
{{item.content}} 43 |
44 |
45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /demo/controllerAs/controllerAs.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']).controller('mainController', [ 2 | '$timeout', function($timeout) { 3 | 4 | var datasource = {}; 5 | 6 | datasource.get = function(index, count, success) { 7 | return $timeout(function() { 8 | var i, item, j, ref, ref1, result; 9 | result = []; 10 | for (i = j = ref = index, ref1 = index + count - 1; ref <= ref1 ? j <= ref1 : j >= ref1; i = ref <= ref1 ? ++j : --j) { 11 | item = {}; 12 | item.id = i; 13 | item.content = "item #" + i; 14 | result.push(item); 15 | } 16 | return success(result); 17 | }, 100); 18 | }; 19 | 20 | this.datasource = datasource; 21 | 22 | this.updateList = function() { 23 | this.adapter.applyUpdates(function(item, scope) { 24 | item.content += ' *'; 25 | }); 26 | }; 27 | 28 | } 29 | ]); 30 | -------------------------------------------------------------------------------- /demo/differentItemHeights/differentItemHeights.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Different Item Heights 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Different Item Heights (Interpolation)

18 | 19 |
20 |
21 |
22 |
23 | {{"item #" + item.index + ' (h = ' + item.height + ')'}} 24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /demo/differentItemHeights/differentItemHeights.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | 3 | .run(function($rootScope) { 4 | $rootScope.doReload = function () { 5 | $rootScope.$broadcast('DO_RELOAD'); 6 | }; 7 | }) 8 | 9 | .controller('MainCtrl', function($scope) { 10 | $scope.hello = 'Hello Main Controller!'; 11 | 12 | var reloadListener = $scope.$on('DO_RELOAD', function() { 13 | if ($scope.adapter) { 14 | $scope.adapter.reload(); 15 | } 16 | }); 17 | 18 | $scope.$on("$destroy", function() { 19 | reloadListener(); 20 | }); 21 | 22 | var min = -50, max = 100, delay = 0; 23 | 24 | $scope.datasource = { 25 | get: function(index, count, success) { 26 | console.log('Getting ' + count + ' items started from ' + index + '...'); 27 | setTimeout(function() { 28 | var result = []; 29 | var start = Math.max(min, index); 30 | var end = Math.min(index + count - 1, max); 31 | if (start <= end) { 32 | for (var i = start; i <= end; i++) { 33 | height = 50 + (i + 1); 34 | result.push({ index: i, height: height }); 35 | } 36 | } 37 | console.log('Got ' + result.length + ' items [' + start + '..' + end + ']'); 38 | success(result); 39 | }, delay); 40 | } 41 | }; 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /demo/disabled/disabled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disabling 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Disabled attribute

18 | 19 |
20 | There is an property "disabled" on the ui-scroll adapter which allows to ignore user scroll/resize events: 21 | 22 |
23 |
<div ui-scroll="item in datasource" adapter="myAdapter">{{item}}</div>
24 |
25 | Here we have two-way binding, so just set the property in appropriate time: 26 |
27 |
$scope.myAdapter.disabled = true;
28 |
29 | Since "disabled" is true no user scroll is being processed by ui-scroll engine. 30 | 31 |
32 | 33 |
34 | User scroll processing is {{myAdapter.disabled ? "disabled" : "enabled"}} now
35 | 36 |
37 | 38 |
39 |
{{item}}
40 |
41 | 42 |
43 | 44 | -------------------------------------------------------------------------------- /demo/disabled/disabled.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push("item #" + i); 11 | } 12 | success(result); 13 | }, 100); 14 | }; 15 | 16 | $scope.datasource = datasource; 17 | } 18 | ]); 19 | -------------------------------------------------------------------------------- /demo/grid-dnd-sort-2/angular-dnd.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-ui/ui-scroll/3339d3b4ccdb7fe0de0de279047205a4fa45cd02/demo/grid-dnd-sort-2/angular-dnd.js -------------------------------------------------------------------------------- /demo/grid-dnd-sort-2/grid-dnd-sort.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Grid dnd sort (applyLayout) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | browse other examples 18 | 19 |

Grid drag and drop sort, applyLayout

20 | 21 |
22 | Here is the implementation sample of drag and drop grid columns sorting through the applyLayout method call.
23 | Also this demo demonstrates an option of an integration with some external components.
24 | And here is being used angular-dnd component to provide 25 | columns drag and drop functionality. 26 |
27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
37 | {{item.name}} 38 |
{{item.col1}}{{item.col2}}{{item.col3}}
49 | 50 |
51 | 52 | -------------------------------------------------------------------------------- /demo/grid-dnd-sort-2/grid-dnd-sort.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll', 'ui.scroll.grid', 'dnd']) 2 | .controller('gridController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push({ 11 | col1: i, 12 | col2: 'item #' + i, 13 | col3: (Math.random() < 0.5) 14 | }); 15 | } 16 | success(result); 17 | }, 100); 18 | }; 19 | 20 | $scope.datasource = datasource; 21 | 22 | $scope.headers = [{ 23 | index: 0, 24 | name: 'col1', 25 | sortable: true 26 | }, { 27 | index: 1, 28 | name: 'col2', 29 | sortable: true 30 | }, { 31 | index: 2, 32 | name: 'col3', 33 | sortable: true 34 | }]; 35 | 36 | $scope.onSortEnd = function () { 37 | var layout = $scope.adapter.gridAdapter.getLayout(); 38 | for (var i = 0; i < $scope.headers.length; i++) { 39 | layout[$scope.headers[i].index].mapTo = i; 40 | } 41 | $scope.adapter.gridAdapter.applyLayout(layout) 42 | }; 43 | 44 | } 45 | ]); -------------------------------------------------------------------------------- /demo/grid-dnd-sort-2/grid.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | height: 300px; 3 | } 4 | 5 | .col1 { 6 | width: 80px; 7 | font-style: italic; 8 | } 9 | 10 | .col2 { 11 | width: 200px; 12 | } 13 | 14 | .col3 { 15 | width: 80px; 16 | } 17 | 18 | .grid th { 19 | cursor: pointer; 20 | } 21 | 22 | .angular-dnd-placeholder { 23 | background: #ddd; 24 | } 25 | 26 | .active { 27 | border: 1px dashed red; 28 | } -------------------------------------------------------------------------------- /demo/grid-dnd-sort/grid-dnd-sort.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Grid dnd sort 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | browse other examples 17 | 18 |

Grid drag and drop sort, moveBefore

19 | 20 |
21 |
22 | Here two methods are being demonstrated. columnFromPoint() which allows you to get the appropriate ColumnAdapter object by coordinates: 23 |
24 | target = $scope.adapter.gridAdapter.columnFromPoint(evt.clientX, evt.clientY)
25 | Another method is moveBefore() which provides an ability to move one column in front of another: 26 |
27 | column.moveBefore(target)
28 |
29 |
30 | 31 | 32 | 33 | 34 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
41 | {{item.name}} 42 |
{{item.col1}}{{item.col2}}{{item.col3}}
53 | 54 |
55 | 56 | -------------------------------------------------------------------------------- /demo/grid-dnd-sort/grid-dnd-sort.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll', 'ui.scroll.grid']) 2 | .controller('gridController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push({ 11 | col1: i, 12 | col2: 'item #' + i, 13 | col3: (Math.random() < 0.5) 14 | }); 15 | } 16 | success(result); 17 | }, 100); 18 | }; 19 | 20 | $scope.datasource = datasource; 21 | 22 | $scope.headers = [{ 23 | index: 0, 24 | name: 'col1', 25 | sortable: true 26 | }, { 27 | index: 1, 28 | name: 'col2', 29 | sortable: true 30 | }, { 31 | index: 2, 32 | name: 'col3', 33 | sortable: true 34 | }]; 35 | 36 | $scope.dragStart = function (evt) { 37 | var column = $scope.adapter.gridAdapter.columnFromPoint(evt.clientX, evt.clientY); 38 | evt.dataTransfer.setData('application/x-data', 39 | $scope.adapter.gridAdapter.columns.findIndex((c) => c.columnId === column.columnId) 40 | ); 41 | } 42 | 43 | $scope.dragOver = function (evt) { 44 | evt.preventDefault(); 45 | return false; 46 | } 47 | 48 | $scope.dragDrop = function (evt) { 49 | var target = $scope.adapter.gridAdapter.columnFromPoint(evt.clientX, evt.clientY); 50 | var column = $scope.adapter.gridAdapter.columns[evt.dataTransfer.getData('application/x-data')]; 51 | column.moveBefore(target); 52 | console.log(evt.dataTransfer); 53 | } 54 | 55 | } 56 | ]); 57 | -------------------------------------------------------------------------------- /demo/grid-dnd-sort/grid.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | height: 300px; 3 | } 4 | 5 | .col1 { 6 | width: 80px; 7 | font-style: italic; 8 | } 9 | 10 | .col2 { 11 | width: 200px; 12 | } 13 | 14 | .col3 { 15 | width: 80px; 16 | } 17 | 18 | .grid th { 19 | cursor: pointer; 20 | } 21 | 22 | .angular-dnd-placeholder { 23 | background: #ddd; 24 | } 25 | 26 | .active { 27 | border: 1px dashed red; 28 | } -------------------------------------------------------------------------------- /demo/grid-dnd-widths/grid-dnd-widths.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Grid dnd widths 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | browse other examples 17 | 18 |

Grid drag and drop widths

19 | 20 |
21 | To manipulate grid columns widths .css method on gridAdapter column is available: 22 |
23 |
24 | $scope.adapter.gridAdapter.columns[0].css('width', '200px');
25 |
26 | You also can set any css property this way. 27 |
28 | 29 | 30 | 31 | 34 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
35 | num 36 | 41 | text
{{item.col1}}{{item.col2}}
52 | 53 |
54 | 55 | -------------------------------------------------------------------------------- /demo/grid-dnd-widths/grid-dnd-widths.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll', 'ui.scroll.grid']) 2 | .controller('gridController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push({ 11 | col1: i, 12 | col2: 'item #' + i 13 | }); 14 | } 15 | success(result); 16 | }, 100); 17 | }; 18 | 19 | $scope.datasource = datasource; 20 | 21 | var splitter = angular.element(document.getElementById('splitter')); 22 | var startX = 0; 23 | var right = 0; 24 | var startDrag = false; 25 | 26 | $scope.dragStart = function (evt) { 27 | if(startDrag) { 28 | return false; 29 | } 30 | splitter.addClass('active'); 31 | startDrag = true; 32 | startX = right + evt.clientX; 33 | }; 34 | 35 | $scope.dragOver = function (evt) { 36 | if(!startDrag) { 37 | return false; 38 | } 39 | 40 | right = startX - evt.clientX; 41 | $scope.adapter.gridAdapter.columns[1].css('width', (200 + right) + 'px'); 42 | $scope.adapter.gridAdapter.columns[0].css('width', (80 - right) + 'px'); 43 | splitter.css('right', '0'); 44 | 45 | return false; 46 | }; 47 | 48 | $scope.dragDrop = function (evt) { 49 | startDrag = false; 50 | splitter.removeClass('active'); 51 | } 52 | 53 | 54 | } 55 | ]); 56 | 57 | 58 | -------------------------------------------------------------------------------- /demo/grid-dnd-widths/grid.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | height: 300px; 3 | } 4 | 5 | .grid tbody { 6 | width: 300px; 7 | } 8 | 9 | .col1 { 10 | width: 80px; 11 | } 12 | 13 | td.col1 { 14 | font-style: italic; 15 | } 16 | 17 | .col2 { 18 | width: 200px; 19 | } 20 | 21 | .splitter{ 22 | cursor: e-resize; 23 | float: right; 24 | border: 2px solid black; 25 | height: 20px; 26 | position: relative; 27 | right: 0; 28 | } 29 | 30 | td, th { 31 | border: 0 dashed #999; 32 | border-left-width: 1px; 33 | } 34 | th { 35 | border-bottom-width: 1px; 36 | } 37 | 38 | .active { 39 | border: 2px solid red; 40 | } -------------------------------------------------------------------------------- /demo/grid-layout-apply/grid-layout-apply.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Grid layout apply 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | browse other examples 17 | 18 |

Grid layout apply

19 | 20 |
21 | - should apply bg-colors and a new columns order 22 |
23 | - just should clear 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
numbercontentbool
{{item.col1}}{{item.col2}}{{item.col3}}
43 | 44 |
45 | 46 | -------------------------------------------------------------------------------- /demo/grid-layout-apply/grid-layout-apply.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll', 'ui.scroll.grid']) 2 | .controller('gridController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push({ 11 | col1: i, 12 | col2: 'item #' + i, 13 | col3: (Math.random() < 0.5) 14 | }); 15 | } 16 | success(result); 17 | }, 100); 18 | }; 19 | 20 | $scope.datasource = datasource; 21 | 22 | var clearLayout = [ 23 | {index: 0, mapTo: 0, css: {backgroundColor: ''}}, 24 | {index: 1, mapTo: 1, css: {backgroundColor: ''}}, 25 | {index: 2, mapTo: 2, css: {backgroundColor: ''}} 26 | ]; 27 | 28 | var someLayout = [ 29 | {index: 0, mapTo: 2, css: {backgroundColor: '#ccc'}}, 30 | {index: 1, mapTo: 1, css: {backgroundColor: '#ddd'}}, 31 | {index: 2, mapTo: 0, css: {backgroundColor: '#eee'}} 32 | ]; 33 | 34 | $scope.applyLayout = function () { 35 | $scope.adapter.gridAdapter.applyLayout(someLayout); 36 | }; 37 | 38 | $scope.clearLayout = function () { 39 | $scope.adapter.gridAdapter.applyLayout(clearLayout); 40 | }; 41 | 42 | } 43 | ]); 44 | -------------------------------------------------------------------------------- /demo/grid-layout-apply/grid.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | height: 300px; 3 | } 4 | 5 | .grid tbody { 6 | width: 380px; 7 | } 8 | 9 | hr { 10 | margin: 5px; 11 | } 12 | 13 | .col1 { 14 | width: 80px; 15 | } 16 | 17 | td.col1 { 18 | font-style: italic; 19 | } 20 | 21 | .col2 { 22 | width: 200px; 23 | } 24 | 25 | .col3 { 26 | width: 80px; 27 | } 28 | 29 | input { 30 | margin-bottom: 5px; 31 | } 32 | -------------------------------------------------------------------------------- /demo/grid-layout-manipulations/grid-layout-manipulations.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Grid layout manipulations 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | browse other examples 17 | 18 |

Grid layout manipulations

19 | 20 |
21 |

22 | Here we have a demo on grid layout manipulations. 23 | The layout can be updated out of the components core, saved and applied. 24 | To apply some layout you may call applyLayout method on gridAdapter: 25 |

26 |
27 | $scope.adapter.gridAdapter.applyLayout(layout)
28 | The "layout" variable must have an appropriate format which exactly matches the format of getLayout() method call result. 29 |
30 |
31 | layout = $scope.adapter.gridAdapter.getLayout()
32 | Note that saving via cookie requires web-server. 33 |

34 |
35 | 36 |
37 | 1 column bg-color
38 | 2 column bg-color
39 | 3 column bg-color
40 | 41 | 42 |
43 | 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
numbercontentbool
{{item.col1}}{{item.col2}}{{item.col3}}
63 | 64 |
65 | 66 | -------------------------------------------------------------------------------- /demo/grid-layout-manipulations/grid-layout-manipulations.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll', 'ui.scroll.grid']) 2 | .controller('gridController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push({ 11 | col1: i, 12 | col2: 'item #' + i, 13 | col3: (Math.random() < 0.5) 14 | }); 15 | } 16 | success(result); 17 | }, 100); 18 | }; 19 | 20 | $scope.datasource = datasource; 21 | 22 | var cookieName = 'ui-scroll-grid-layout'; 23 | 24 | var clearLayout = [ 25 | {index: 0, mapTo: 0, css: {backgroundColor: ''}}, 26 | {index: 1, mapTo: 1, css: {backgroundColor: ''}}, 27 | {index: 2, mapTo: 2, css: {backgroundColor: ''}} 28 | ]; 29 | 30 | $scope.layout = [ 31 | {index: 0, mapTo: 0, css: {backgroundColor: '#eee'}}, 32 | {index: 1, mapTo: 1, css: {backgroundColor: '#ddd'}}, 33 | {index: 2, mapTo: 2, css: {backgroundColor: '#ccc'}} 34 | ]; 35 | 36 | $scope.applyLayout = function () { 37 | $scope.adapter.gridAdapter.applyLayout($scope.layout); 38 | }; 39 | 40 | $scope.clearLayout = function () { 41 | $scope.adapter.gridAdapter.applyLayout(clearLayout); 42 | }; 43 | 44 | $scope.saveLayout = function () { 45 | var layout = $scope.adapter.gridAdapter.getLayout(); 46 | 47 | var date = new Date(); 48 | date.setTime(date.getTime() + 30 * 24 * 3600 * 1000); // 30 days 49 | document.cookie = cookieName + "=" + JSON.stringify(layout) + "; path=/;expires = " + date.toGMTString(); 50 | }; 51 | 52 | $scope.restoreLayout = function () { 53 | var value = "; " + document.cookie; 54 | var parts = value.split("; " + cookieName + "="); 55 | var result; 56 | if (parts.length != 2 || !(result = parts.pop().split(";").shift())) { 57 | alert('Nothing to apply'); 58 | return; 59 | } 60 | $scope.layout = JSON.parse(result); 61 | $scope.applyLayout(); 62 | }; 63 | 64 | } 65 | ]); 66 | -------------------------------------------------------------------------------- /demo/grid-layout-manipulations/grid.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | height: 300px; 3 | } 4 | 5 | .grid tbody { 6 | width: 380px; 7 | } 8 | 9 | hr { 10 | margin: 5px; 11 | } 12 | 13 | .col1 { 14 | width: 80px; 15 | } 16 | 17 | td.col1 { 18 | font-style: italic; 19 | } 20 | 21 | .col2 { 22 | width: 200px; 23 | } 24 | 25 | .col3 { 26 | width: 80px; 27 | } 28 | 29 | input { 30 | margin-bottom: 5px; 31 | } 32 | -------------------------------------------------------------------------------- /demo/grid-scopes-wrapping/grid-scopes-wrapping.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Grid scopes wrapping 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | browse other examples 17 | 18 |

Grid scopes wrapping

19 | 20 |
21 |

Here we have ng-repeat wrapping ui-scroll-td directive!

22 |

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
{{col}}
{{item[col]}}
36 | 37 |
38 | 39 | -------------------------------------------------------------------------------- /demo/grid-scopes-wrapping/grid-scopes-wrapping.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll', 'ui.scroll.grid']) 2 | .controller('gridController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | $scope.columns = ['col1','col2','col3']; 7 | 8 | datasource.get = function (index, count, success) { 9 | $timeout(function () { 10 | var result = []; 11 | for (var i = index; i <= index + count - 1; i++) { 12 | result.push({ 13 | col1: i, 14 | col2: 'item #' + i, 15 | col3: (Math.random() < 0.5) 16 | }); 17 | } 18 | success(result); 19 | }, 100); 20 | }; 21 | 22 | $scope.datasource = datasource; 23 | 24 | var clearLayout = [ 25 | {index: 0, mapTo: 0, css: {backgroundColor: ''}}, 26 | {index: 1, mapTo: 1, css: {backgroundColor: ''}}, 27 | {index: 2, mapTo: 2, css: {backgroundColor: ''}} 28 | ]; 29 | 30 | var someLayout = [ 31 | {index: 0, mapTo: 2, css: {backgroundColor: '#ccc'}}, 32 | {index: 1, mapTo: 1, css: {backgroundColor: '#ddd'}}, 33 | {index: 2, mapTo: 0, css: {backgroundColor: '#eee'}} 34 | ]; 35 | 36 | $scope.applyLayout = function () { 37 | $scope.adapter.gridAdapter.applyLayout(someLayout); 38 | }; 39 | 40 | $scope.clearLayout = function () { 41 | $scope.adapter.gridAdapter.applyLayout(clearLayout); 42 | }; 43 | 44 | } 45 | ]); 46 | -------------------------------------------------------------------------------- /demo/grid-scopes-wrapping/grid.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | height: 300px; 3 | } 4 | 5 | .grid tbody { 6 | width: 380px; 7 | } 8 | 9 | hr { 10 | margin: 5px; 11 | } 12 | 13 | .col1 { 14 | width: 80px; 15 | } 16 | 17 | td.col1 { 18 | font-style: italic; 19 | } 20 | 21 | .col2 { 22 | width: 200px; 23 | } 24 | 25 | .col3 { 26 | width: 80px; 27 | } 28 | 29 | input { 30 | margin-bottom: 5px; 31 | } 32 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scroller Demo 6 | 7 | 8 | 9 |
10 | 11 |

Scroller Examples

12 | 13 | 158 | 159 |
160 | 161 | 188 | 189 |
190 | read more... 191 |
192 | 193 |
194 | 195 | 196 | -------------------------------------------------------------------------------- /demo/insideComponent/insideComponent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inside Angular 1.5+ component 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Scroller inside Angular 1.5+ Component

18 | 19 |
20 | This sample demonstrates encapsulation of the ui-scroll directive inside some custom component (Angular 1.5+). 21 | 22 | To demonstrate the work of the Adapter, click on any row. The text of the item should change after click. 23 | 24 | The controller of this Component is implemented as ES6 class. Note that this demo might not work in old browsers which don't support ES6 classes. 25 |
26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demo/insideComponent/insideComponent.js: -------------------------------------------------------------------------------- 1 | (function (angular) { 2 | 3 | class Ctrl { 4 | constructor($timeout, $scope) { 5 | this.timeout = $timeout; 6 | this.show = true; 7 | this.$scope = $scope; 8 | } 9 | 10 | get(index, count, success) { 11 | this.timeout(function () { 12 | var result = []; 13 | for (var i = index; i <= index + count - 1; i++) { 14 | result.push({ 15 | id: i, 16 | name: "item #" + i 17 | }); 18 | } 19 | success(result); 20 | }, 100); 21 | } 22 | 23 | update(id) { 24 | return this.scrollAdapter.applyUpdates(function (item) { 25 | if (item.id === id) { 26 | item.name += " *"; 27 | } 28 | }); 29 | } 30 | } 31 | 32 | angular 33 | .module('application', ['ui.scroll']) 34 | .component('myComponent', { 35 | controllerAs: 'ctrl', 36 | template: 37 | '
' + 38 | '
' + 39 | '
{{item.name}}
' + 40 | '
' + 41 | '
', 42 | controller: Ctrl 43 | }); 44 | 45 | })(angular); 46 | -------------------------------------------------------------------------------- /demo/insideDirective/insideDirective.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inside directive 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Scroller inside the directive

18 | 19 |
20 | This sample demonstrates encapsulation of the ui-scroll directive inside another custom directive wich has it's own controller and wich uses "Controller As" syntax in it's template. 21 | To demonstrate the work of the Adapter, click on any row. The text of the item should change after click. 22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /demo/insideDirective/insideDirective.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', ['$scope', function($scope) { 3 | $scope.show = true; 4 | }]) 5 | .directive('myDir', function() { 6 | return { 7 | restrict: 'E', 8 | controllerAs: 'ctrl', 9 | template: 10 | '
' + 11 | '
' + 12 | '
{{item.name}}
' + 13 | '
' + 14 | '
', 15 | controller: function ($timeout) { 16 | var ctrl = this; 17 | ctrl.show = true; 18 | ctrl.get = function(index, count, success) { 19 | $timeout(function () { 20 | var result = []; 21 | for (var i = index; i <= index + count - 1; i++) { 22 | result.push({ 23 | id: i, 24 | name: "item #" + i 25 | }); 26 | } 27 | success(result); 28 | }, 100); 29 | } 30 | ctrl.update = function(id) { 31 | return ctrl.scrollAdapter.applyUpdates(function(item) { 32 | if (item.id === id) { 33 | item.name += " *"; 34 | } 35 | }); 36 | } 37 | } 38 | } 39 | } 40 | ); 41 | -------------------------------------------------------------------------------- /demo/isLoading/isLoading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Is loading 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Is loading

18 | 19 |
20 | There is an optional parameter of ui-scroll directive which allows us to know whether there are any pending load requests. 21 | 22 |
23 |
<li ui-scroll="item in datasource" is-loading="loading">*{{item}}*</li>
24 |
25 | 26 | We recommend to use isLoading property on adapter. 27 | Please follow this demo. 28 |
29 | 30 |
31 |
is loading: {{loading}}
32 |
33 | 34 |
35 |
    36 |
  • *{{item}}*
  • 37 |
38 |
39 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /demo/isLoading/isLoading.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', ['$log', '$timeout', 3 | function (console, $timeout) { 4 | 5 | var get = function (index, count, success) { 6 | $timeout(function () { 7 | var result = []; 8 | for (var i = index; i <= index + count - 1; i++) { 9 | result.push("item #" + i); 10 | } 11 | success(result); 12 | }, 100); 13 | }; 14 | 15 | return { 16 | get: get 17 | }; 18 | } 19 | ]); -------------------------------------------------------------------------------- /demo/isLoading/isLoadingAdapter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Is loading (Adapter) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Is loading (Adapter)

18 | 19 |
20 | As it follows from documentation Adapter implements isLoading property to make us know whether there are any pending load requests. 21 | 22 |
23 |
<li ui-scroll="item in datasource" adapter="adapter">*{{item}}*</li>
24 |
25 | 26 |
27 |
is loading: {{adapter.isLoading}}
28 |
29 |
30 | 31 |
32 |
is loading: {{adapter.isLoading}}
33 |
34 | 35 |
36 |
    37 |
  • *{{item}}*
  • 38 |
39 |
40 | 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /demo/isLoading/isLoadingAdapter.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | 5 | $scope.adapter = {}; 6 | 7 | $scope.datasource = {}; 8 | 9 | $scope.datasource.get = function (index, count, success) { 10 | $timeout(function () { 11 | var result = []; 12 | for (var i = index; i <= index + count - 1; i++) { 13 | result.push("item #" + i); 14 | } 15 | success(result); 16 | }, 100); 17 | }; 18 | 19 | } 20 | ]); 21 | -------------------------------------------------------------------------------- /demo/jquery/jquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | browse other examples 17 | 18 |

jQuery

19 | 20 |
21 | In case the Angular App works with jQuery, the ui-scroll should use jQuery. 22 | Otherwise, the ui-scroll should use jqLiteExtras. 23 |
24 | 25 |
26 | jQuery version is {{jqueryVersion}} 27 | jQuery is not loaded 28 |
29 | 30 |
31 |
{{item}}
32 |
33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/jquery/jquery.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push("item #" + i); 11 | } 12 | success(result); 13 | }, 100); 14 | }; 15 | 16 | $scope.datasource = datasource; 17 | 18 | $scope.jqueryVersion = window.jQuery && angular.element.fn && angular.element.fn.jquery; 19 | 20 | } 21 | ]); 22 | -------------------------------------------------------------------------------- /demo/listScroller/listScroller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | li based scrollable list 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

li based scrollable list

18 | 19 |
20 | Since html container with ui-scroll attribute is repeatable there are some ways to build a template. 21 | This sample demonstrates list based (<li>) template usage. 22 | 23 |
24 |
25 | <ul ui-scroll-viewport>
26 | 	<li ui-scroll="item in datasource">*{{item}}*</li>
27 | </ul>
28 |
29 |
30 | 31 |
32 |
    33 |
  • *{{item}}*
  • 34 |
35 |
36 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /demo/listScroller/listScroller.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', ['$log', '$timeout', 3 | function (console, $timeout) { 4 | 5 | var get = function (index, count, success) { 6 | $timeout(function () { 7 | var result = []; 8 | for (var i = index; i <= index + count - 1; i++) { 9 | result.push("item #" + i); 10 | } 11 | success(result); 12 | }, 100); 13 | }; 14 | 15 | return { 16 | get: get 17 | }; 18 | } 19 | ]); -------------------------------------------------------------------------------- /demo/multipleLists/multipleLists.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Independent scrollable lists 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Independent scrollable lists

18 | 19 |
20 | Here we have two different viewports but single datasource. 21 |
22 | 23 |
24 |
is loading (1): {{adapter.isLoading}}
25 |
is loading (2): {{adapter2.isLoading}}
26 |
27 | 28 |
29 |

The List

30 |
31 |
*one {{item}}*
32 |
33 |
34 | 35 |
36 |

One more list

37 |
38 |
39 | *two {{item}}* 40 |
    41 |
  • {{line}}
  • 42 |
43 |
44 |
45 | 46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /demo/multipleLists/multipleLists.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push("item #" + i); 11 | } 12 | success(result); 13 | }, 100); 14 | }; 15 | 16 | $scope.datasource = datasource; 17 | $scope.adapter = {}; 18 | } 19 | ]); 20 | -------------------------------------------------------------------------------- /demo/multipleReloadTest/multipleReload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Multiple reload test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Multiple reload test

18 | 19 |
20 | 21 | 22 | Loading: {{adapter.isLoading}} 23 |
24 | 25 |
26 |
{{item}}
27 |
28 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /demo/multipleReloadTest/multipleReload.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push("item #" + i); 11 | } 12 | success(result); 13 | }, 100); 14 | }; 15 | 16 | $scope.datasource = datasource; 17 | 18 | $scope.doSingleReload = function () { 19 | $scope.adapter.reload(); 20 | }; 21 | 22 | $scope.doMultipleReload = function () { 23 | $scope.adapter.reload(); 24 | $timeout(function () { 25 | $scope.adapter.reload(); 26 | }); 27 | $timeout(function () { 28 | $scope.adapter.reload(); 29 | }); 30 | }; 31 | 32 | } 33 | ]); 34 | -------------------------------------------------------------------------------- /demo/outOfBuffer/outOfBuffer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Adapter sync 6 | 7 | 8 | 9 | 10 | 23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 |
    39 |
  • 40 |
    41 | {{$index}}: {{item.content}} ({{item.id}}) 42 | [x] 43 |
    44 |
  • 45 |
46 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /demo/outOfBuffer/outOfBuffer.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('application', ['ui.scroll']); 2 | 3 | app.factory('Server', [ 4 | '$timeout', '$q', function ($timeout, $q) { 5 | 6 | var ServerFactory = { 7 | 8 | firstIndex: 1, 9 | 10 | lastIndex: 40, 11 | 12 | delay: 100, 13 | 14 | data: [], 15 | 16 | absIndex: 1, 17 | 18 | generateId: function () { 19 | var d = '-'; 20 | function S4() { 21 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 22 | } 23 | return (S4() + S4() + d + S4() + d + S4() + d + S4() + d + S4() + S4() + S4()); 24 | }, 25 | 26 | generateItem: function (index) { 27 | return { 28 | index: index, 29 | id: this.generateId(), 30 | content: 'Item #' + this.absIndex++ 31 | } 32 | }, 33 | 34 | init: function () { 35 | for (var i = this.firstIndex; i <= this.lastIndex; i++) { 36 | this.data.push(this.generateItem(i)); 37 | } 38 | }, 39 | 40 | getItem: function (index) { 41 | for (var i = this.data.length - 1; i >= 0; i--) { 42 | if (this.data[i].index === index) { 43 | return this.data[i]; 44 | } 45 | } 46 | }, 47 | 48 | returnDeferredResult: function (result) { 49 | var deferred = $q.defer(); 50 | $timeout(function () { 51 | deferred.resolve(result); 52 | }, this.delay); 53 | return deferred.promise; 54 | }, 55 | 56 | request: function (index, count) { 57 | var start = index; 58 | var end = index + count - 1; 59 | var item, result = { 60 | items: [] 61 | }; 62 | if (start <= end) { 63 | for (var i = start; i <= end; i++) { 64 | if (item = this.getItem(i)) { 65 | result.items.push(item); 66 | } 67 | } 68 | } 69 | return this.returnDeferredResult(result); 70 | }, 71 | 72 | prependItem: function (params) { 73 | var newItem = this.generateItem(--this.firstIndex); 74 | newItem.content += params; 75 | this.data.unshift(newItem); 76 | return this.returnDeferredResult(newItem); 77 | }, 78 | 79 | appendItem: function (params) { 80 | var newItem = this.generateItem(++this.lastIndex); 81 | newItem.content += params; 82 | this.data.push(newItem); 83 | return this.returnDeferredResult(newItem); 84 | }, 85 | 86 | removeFirst: function () { 87 | var firstItem = this.data.find(i => i.index === this.firstIndex); 88 | if(!firstItem) { 89 | return; 90 | } 91 | return this.removeItemById(firstItem.id); 92 | }, 93 | 94 | removeLast: function () { 95 | var lastItem = this.data.find(i => i.index === this.lastIndex); 96 | if(!lastItem) { 97 | return; 98 | } 99 | return this.removeItemById(lastItem.id); 100 | }, 101 | 102 | removeItemById: function (itemId) { 103 | var length = this.data.length; 104 | for (var i = 0; i < length; i++) { 105 | if (this.data[i].id === itemId) { 106 | var indexRemoved = this.data[i].index; 107 | this.data.splice(i, 1); 108 | this.setIndices(); 109 | return this.returnDeferredResult(indexRemoved); 110 | } 111 | } 112 | return this.returnDeferredResult(false); 113 | }, 114 | 115 | setIndices: function() { 116 | if(!this.data.length) { 117 | this.firstIndex = 1; 118 | this.lastIndex = 1; 119 | return; 120 | } 121 | this.firstIndex = this.data[0].index; 122 | this.lastIndex = this.data[0].index; 123 | for (var i = this.data.length - 1; i >= 0; i--) { 124 | if(this.data[i].index > this.lastIndex) { 125 | this.lastIndex = this.data[i].index; 126 | } 127 | if(this.data[i].index < this.firstIndex) { 128 | this.firstIndex = this.data[i].index; 129 | } 130 | } 131 | } 132 | }; 133 | 134 | ServerFactory.init(); 135 | 136 | return ServerFactory; 137 | 138 | } 139 | ]); 140 | 141 | 142 | app.controller('mainController', [ 143 | '$scope', 'Server', function ($scope, Server) { 144 | 145 | var ctrl = this; 146 | 147 | ctrl.datasource = { 148 | get: function (index, count, success) { 149 | console.log('request by index = ' + index + ', count = ' + count); 150 | Server.request(index, count).then(function (result) { 151 | if (result.items) { 152 | console.log('resolved ' + result.items.length + ' items'); 153 | } 154 | success(result.items); 155 | }); 156 | } 157 | }; 158 | 159 | $scope.$watch('adapter', (prev, next) => { 160 | console.log('The adapter has been initialized'); 161 | }); 162 | 163 | ctrl.prepend = function () { 164 | Server.prependItem(' ***').then(function (newItem) { 165 | if (ctrl.adapter.isBOF()) { 166 | ctrl.adapter.prepend([newItem]); 167 | } 168 | }); 169 | }; 170 | 171 | ctrl.append = function () { 172 | Server.appendItem(' ***').then(function (newItem) { 173 | if (ctrl.adapter.isEOF()) { 174 | ctrl.adapter.append([newItem]); 175 | } 176 | }); 177 | }; 178 | 179 | // todo dhilt : need to implement it properly 180 | ctrl.removeAll = function () { 181 | ctrl.adapter.applyUpdates(function (item) { 182 | if (item.id) { 183 | Server.removeItemById(item.id); 184 | return []; 185 | } 186 | }); 187 | }; 188 | 189 | ctrl.remove = function (itemRemove) { 190 | Server.removeItemById(itemRemove.id).then(function (result) { 191 | if (result !== false) { 192 | ctrl.adapter.applyUpdates(function (item) { 193 | if (item.id === itemRemove.id) { 194 | return []; 195 | } 196 | }); 197 | } 198 | }); 199 | }; 200 | 201 | ctrl.removeFirst = function () { 202 | Server.removeFirst().then(function (indexRemoved) { 203 | if (indexRemoved !== false) { 204 | ctrl.adapter.applyUpdates(indexRemoved, []); 205 | } 206 | }); 207 | }; 208 | 209 | ctrl.removeLast = function () { 210 | Server.removeLast().then(function (indexRemoved) { 211 | if (indexRemoved !== false) { 212 | ctrl.adapter.applyUpdates(indexRemoved, []); 213 | } 214 | }); 215 | }; 216 | } 217 | ]); 218 | -------------------------------------------------------------------------------- /demo/persistentScroll/persistentScroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Preserves the scroll position on refresh 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Preserves the scroll position on refresh

18 | 19 |
20 | This sample demonstrates a functionality of top item position persistence. 21 | This persistence is based on topVisible element's index (offset) which is stored in URL ($location). 22 | So when you do some scrolls you see that 'offset' param is persisted via URL hash. 23 | Then you refresh the page and after that the $index value (and leftmost column in the example) changes, 24 | but the item which used to be on top stays on top. 25 |
26 | 27 |
28 |
top visible: {{topVisible.$index}}
29 |
30 | 31 |
32 |
{{$index}}: {{item}}
33 |
34 | 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /demo/persistentScroll/persistentScroll.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', [ '$log', '$timeout', '$rootScope', '$location', 3 | function (console, $timeout, $rootScope, $location) { 4 | 5 | var offset = parseInt($location.search().offset || '0', 10); 6 | 7 | var get = function (index, count, success) { 8 | $timeout(function () { 9 | var actualIndex = index + offset; 10 | var result = []; 11 | var start = Math.max(-40, actualIndex); 12 | var end = Math.min(actualIndex + count - 1, 100); 13 | if (start <= end) { 14 | for (var i = start; i <= end; i++) { 15 | result.push("item " + i); 16 | } 17 | } 18 | success(result); 19 | }, 100); 20 | }; 21 | 22 | $rootScope.$watch((function () { 23 | return $rootScope.topVisible; 24 | }), function () { 25 | if ($rootScope.topVisible) { 26 | $location.search('offset', $rootScope.topVisible.$index + offset-1); 27 | $location.replace(); 28 | } 29 | }); 30 | 31 | return { 32 | get: get 33 | }; 34 | } 35 | ]); -------------------------------------------------------------------------------- /demo/positionedList/positionedList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scroller Demo (list based) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Positions the list based on element data

18 | 19 |
20 | This sample demonstrates a functionality of list positioning based on element data. 21 | There is no persistence, just enter key value in textinput (from 'aa' to 'zz') and get the key-offset positioned list. 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 |
{{$index}}: {{item}}
34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /demo/positionedList/positionedList.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', ['$log', '$timeout', '$rootScope', 3 | function (console, $timeout, $rootScope) { 4 | 5 | $rootScope.key = ""; 6 | var position = 0; 7 | var data = []; 8 | var ref1 = 'abcdefghijklmnopqrstuvwxyz'; 9 | var ref2 = 'abcdefghijklmnopqrstuvwxyz'; 10 | 11 | for (var j = 0; j < ref1.length; j++) 12 | for (var k = 0, letter1 = ref1[j]; k < ref2.length; k++) 13 | for (var i = 0, letter2 = ref2[k]; i <= 9; i++) 14 | data.push("" + letter1 + letter2 + ": 0" + i); 15 | 16 | var get = function (index, count, success) { 17 | return $timeout(function () { 18 | var actualIndex = index + position; 19 | var start = Math.max(0 - position, actualIndex); 20 | var end = Math.min(actualIndex + count - 1, data.length); 21 | 22 | if (start > end) { 23 | success([]); 24 | } else { 25 | success(data.slice(start, end + 1)); 26 | } 27 | }, 100); 28 | }; 29 | 30 | $rootScope.$watch((function () { 31 | return $rootScope.key; 32 | }), function () { 33 | position = 0; 34 | for (var m = 0; m < data.length; m++) { 35 | if ($rootScope.key > data[m]) { 36 | position++; 37 | } 38 | } 39 | if ($rootScope.key) 40 | $rootScope.adapter.reload(); 41 | }); 42 | 43 | return { 44 | get: get, 45 | }; 46 | } 47 | ]); 48 | -------------------------------------------------------------------------------- /demo/rebuilding/rebuilding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rebuilding 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Multiple component rebuild

18 | 19 |
20 | This demo allows to trace memory leaks during destroy and rebuild ui-scroll component. 21 |
22 | 23 |
24 |
25 | 28 |
29 |
30 | 31 |
32 |
33 |
{{item}}
34 |
35 |
36 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /demo/rebuilding/rebuilding.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push("item #" + i); 11 | } 12 | success(result); 13 | }, 100); 14 | }; 15 | 16 | $scope.datasource = datasource; 17 | 18 | $scope.switch = false; 19 | } 20 | ]); 21 | -------------------------------------------------------------------------------- /demo/reload100/reload100.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reload 100 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Reload with parameter

18 | 19 |
20 | Here we provide an ability to reload the datasource to some specified position. First you need an adapter defined on your scope: 21 |
22 |
<div ui-scroll="item in datasource" adapter="myAdapter">{{item}}</div>
23 |
24 | Then just call the reload method on the adapter with index parameter: 25 |
26 |
$scope.myAdapter.reload(100);
27 |
28 |
29 | 30 |
31 | - index to reload
32 | 33 |
34 | 35 | 36 |
37 |
{{item}}
38 |
39 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /demo/reload100/reload100.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push("item #" + i); 11 | } 12 | success(result); 13 | }, 100); 14 | }; 15 | 16 | $scope.datasource = datasource; 17 | 18 | $scope.doReload = function () { 19 | if (angular.isFunction($scope.adapter.reload)) { 20 | var reloadIndex = parseInt($scope.reloadIndex, 10); 21 | reloadIndex = isNaN(reloadIndex) ? 1 : reloadIndex; 22 | $scope.adapter.reload(reloadIndex); 23 | } 24 | }; 25 | /* 26 | $scope.delay = false; 27 | $timeout(function() { 28 | $scope.delay = true; 29 | }, 500); 30 | */ 31 | } 32 | ]); 33 | -------------------------------------------------------------------------------- /demo/remote/remote.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Remote server emulation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | browse other examples 16 |

Remote server emulation

17 |
18 | This demo implements Remote server emulation factory, so the datasource get method looks like 19 |
20 |
21 | datasource.get = function(index, count, success) {
22 |   Server.request(index, count).then(success);
23 | };
24 |
25 |
26 |
27 |
28 | {{item.content}} 29 |
30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /demo/remote/remote.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | 3 | .factory('Server', function($timeout, $q) { 4 | return { 5 | 6 | default: { 7 | first: 0, 8 | max: 99, 9 | delay: 100 10 | }, 11 | 12 | data: [], 13 | 14 | init: function(settings = {}) { 15 | this.first = settings.hasOwnProperty('first') ? settings.first : this.default.first; 16 | this.max = settings.hasOwnProperty('max') ? settings.max : this.default.max; 17 | this.delay = settings.hasOwnProperty('delay') ? settings.delay : this.default.delay; 18 | for (var i = this.first; i <= this.max; i++) { 19 | this.data[i] = { 20 | index: i, 21 | content: 'Item #' + i 22 | }; 23 | } 24 | }, 25 | 26 | request: function(index, count) { 27 | var self = this; 28 | var deferred = $q.defer(); 29 | 30 | var start = index; 31 | var end = index + count - 1; 32 | 33 | $timeout(function() { 34 | var item, result = []; 35 | if (start <= end) { 36 | for (var i = start; i <= end; i++) { 37 | if (item = self.data[i]) { 38 | result.push(item); 39 | } 40 | } 41 | } 42 | deferred.resolve(result); 43 | }, self.delay); 44 | 45 | return deferred.promise; 46 | } 47 | }; 48 | }) 49 | 50 | .controller('mainController', function($scope, Server) { 51 | 52 | $scope.firstIndex = 1; 53 | 54 | Server.init({ 55 | first: $scope.firstIndex, 56 | max: 100, 57 | delay: 40 58 | }); 59 | 60 | $scope.datasource = { 61 | get: function(index, count, success) { 62 | console.log('requested index = ' + index + ', count = ' + count); 63 | Server.request(index, count).then(function(result) { 64 | console.log('resolved ' + result.length + ' items'); 65 | success(result); 66 | }); 67 | } 68 | }; 69 | }); -------------------------------------------------------------------------------- /demo/scopeDatasource/scopeDatasource.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Datasource on scope (not as service) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Datasource on scope (not as service)

18 | 19 |
20 | Per documentation the datasource object can be defined in two different ways. 21 | 22 |
23 |
<li ui-scroll="item in datasource">{{item}}</li>
24 |
25 | 26 | And the directive will first look for a property with the given name (datasource) on its $scope. 27 | So to use scope-approach you need to declare datasource object on scope of your controller and define method get on it. 28 | 29 |
30 |
31 | angular.module('application', ['ui.scroll'])
32 |   .controller('mainController', ...
33 | 
34 |     var get = function(index, count, success) { ... };
35 | 
36 |     $scope.datasource = { get: get };
37 | 
38 |   );
39 |
40 |
41 | 42 |
43 |
{{item}}
44 |
45 | 46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /demo/scopeDatasource/scopeDatasource.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push("item #" + i); 11 | } 12 | success(result); 13 | }, 100); 14 | }; 15 | 16 | $scope.datasource = datasource; 17 | } 18 | ]); 19 | -------------------------------------------------------------------------------- /demo/scrollBubblingPrevent/scrollBubblingPrevent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scroller Demo (scroll bubbles up only after eof/bof) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Scroll bubbles up only after eof/bof

18 | 19 |
20 | There are 100 elements in datasource (from -50 to +50). 21 | Scrolling of the document begins only after bof/eof is reached. 22 |
23 | 24 |
25 |
26 |
{{item}}
27 |
28 |
29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /demo/scrollBubblingPrevent/scrollBubblingPrevent.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']).factory('datasource', [ 2 | '$log', '$timeout', function(console, $timeout) { 3 | var get, max, min; 4 | min = -50; 5 | max = 50; 6 | 7 | get = function(index, count, success) { 8 | $timeout(function() { 9 | var result = []; 10 | var start = Math.max(min, index); 11 | var end = Math.min(index + count - 1, max); 12 | if (start <= end) { 13 | for (var i = start; i <= end; i++) { 14 | result.push("item #" + i); 15 | } 16 | } 17 | success(result); 18 | }, 50); 19 | }; 20 | 21 | return { 22 | get: get 23 | }; 24 | } 25 | ]); -------------------------------------------------------------------------------- /demo/serviceDatasource/serviceDatasource.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Datasource as service 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Datasource as service

18 | 19 |
20 | Per documentation the datasource object can be defined in two different ways. 21 | 22 |
23 |
<li ui-scroll="item in datasource">{{item}}</li>
24 |
25 | 26 | In this sample we use service-approach. Here you need to define angular service and declare method get on it. 27 | 28 |
29 |
30 | angular.module('application', ['ui.scroll'])
31 |   .factory('datasource', function() { ...
32 | 
33 |     var get = function(index, count, success) { ... };
34 | 
35 |     return { get: get };
36 |   });
37 |
38 | 39 |
40 | 41 |
42 |
{{item}}
43 |
44 | 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/serviceDatasource/serviceDatasource.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', ['$log', '$timeout', 3 | function (console, $timeout) { 4 | 5 | var get = function (index, count, success) { 6 | $timeout(function () { 7 | var result = []; 8 | for (var i = index; i <= index + count - 1; i++) { 9 | result.push("item #" + i); 10 | } 11 | success(result); 12 | }, 100); 13 | }; 14 | 15 | return { 16 | get: get 17 | }; 18 | } 19 | ]); 20 | -------------------------------------------------------------------------------- /demo/tableScroller/tableScroller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Table based scrollable list 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Table based scrollable list

18 | 19 |
20 | Since html container with ui-scroll attribute is repeatable there are some ways to build a template. 21 | This sample demonstrates table based (<tr>) template usage. 22 | 23 |
24 |
25 | <table ui-scroll-viewport>
26 | 	<tr ui-scroll="item in datasource">
27 | 		<td>{{$index}}</td>
28 | 		<td>{{item}}</td>
29 | 	</tr>
30 | </table>
31 |
32 | 33 | And it needed to be said that we have special logic within ui-scroll sources to support table markup. 34 |
35 | 36 |
37 | 38 | 39 | 40 | 43 | 46 | 47 | 48 |
41 | {{$index}} 42 | 44 | {{item}} 45 |
49 |
50 | 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /demo/tableScroller/tableScroller.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', ['$log', '$timeout', 3 | function (console, $timeout) { 4 | 5 | var get = function (index, count, success) { 6 | $timeout(function () { 7 | var result = []; 8 | for (var i = index; i <= index + count - 1; i++) { 9 | result.push("item #" + i); 10 | } 11 | success(result); 12 | }, 100); 13 | }; 14 | 15 | return { 16 | get: get 17 | }; 18 | } 19 | ]); 20 | -------------------------------------------------------------------------------- /demo/topVisible/topVisible.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Top visible 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Top visible

18 | 19 |
20 | There is an optional parameter of ui-scroll directive which gives us a reference to the item currently in the topmost visible position. 21 | 22 |
23 |
<li ui-scroll="item in datasource" top-visible="topItem">*{{item}}*</li>
24 |
25 | 26 | We recommend to use topVisible property on adapter. 27 | Please follow this demo. 28 |
29 | 30 |
31 |
top visible: {{topVisible}}
32 |
33 | 34 |
35 |
{{$index}}: {{item}}
36 |
37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /demo/topVisible/topVisible.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', ['$log', '$timeout', 3 | function (console, $timeout) { 4 | 5 | var get = function (index, count, success) { 6 | $timeout(function () { 7 | var result = []; 8 | for (var i = index; i <= index + count - 1; i++) { 9 | result.push("item #" + i); 10 | } 11 | success(result); 12 | }, 100); 13 | }; 14 | 15 | return { 16 | get: get 17 | }; 18 | } 19 | ]); -------------------------------------------------------------------------------- /demo/topVisible/topVisibleAdapter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Top visible (Adapter) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Top visible (Adapter)

18 | 19 |
20 | As it follows from documentation Adapter implements topVisible property which is a reference to the item currently in the topmost visible position. 21 | 22 |
23 |
<li ui-scroll="item in datasource" adapter="adapter">*{{item}}*</li>
24 |
25 | 26 |
27 |
top visible: {{adapter.topVisible}}
28 |
29 |
30 | 31 |
32 |
top visible: {{adapter.topVisible}}
33 |
34 | 35 |
36 |
*{{item}}*
37 |
38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /demo/topVisible/topVisibleAdapter.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | 5 | $scope.adapter = {}; 6 | 7 | $scope.datasource = {}; 8 | 9 | $scope.datasource.get = function (index, count, success) { 10 | $timeout(function () { 11 | var result = []; 12 | for (var i = index; i <= index + count - 1; i++) { 13 | result.push("item #" + i); 14 | } 15 | success(result); 16 | }, 100); 17 | }; 18 | 19 | } 20 | ]); 21 | -------------------------------------------------------------------------------- /demo/ui-scroll-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-ui/ui-scroll/3339d3b4ccdb7fe0de0de279047205a4fa45cd02/demo/ui-scroll-demo.gif -------------------------------------------------------------------------------- /demo/userIndexes/userIndexes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | User min and max indexes 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 |
21 | 22 | browse other examples 23 | 24 |

Set user min and max indexes

25 | 26 |
27 | Here we provide an ability of external min and max indexes setting to let the viewport know how many items do we have. 28 |
29 |
<li ui-scroll="item in datasource">{{item}}</li>
30 |
31 | Then you can play with minIndex and maxIndex properties which are being accessible on your datasource after the ui-scroll directive is linked: 32 |
33 |
34 | datasource.minIndex = -100;
35 | datasource.maxIndex = 100;
36 |
37 | Such setting does not not lead to data fetching but the scroll bar params do match datasource size defined this way. 38 |
39 | 40 |
41 | 42 | minIndex value to set 43 |
44 | 45 | maxIndex value to set 46 |
47 | 48 |
49 | 50 |
51 |
{{item}}
52 |
53 | 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /demo/userIndexes/userIndexes.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .controller('mainController', [ 3 | '$scope', '$log', '$timeout', function ($scope, console, $timeout) { 4 | var datasource = {}; 5 | 6 | datasource.get = function (index, count, success) { 7 | $timeout(function () { 8 | var result = []; 9 | for (var i = index; i <= index + count - 1; i++) { 10 | result.push("item #" + i); 11 | } 12 | success(result); 13 | }, 100); 14 | }; 15 | 16 | $scope.datasource = datasource; 17 | $scope.adapter = {}; 18 | 19 | $scope.setUserMinIndex = function () { 20 | var userMinIndex = parseInt($scope.userMinIndex, 10); 21 | if(!isNaN(userMinIndex)) 22 | $scope.datasource.minIndex = userMinIndex; 23 | }; 24 | 25 | $scope.setUserMaxIndex = function () { 26 | var userMaxIndex = parseInt($scope.userMaxIndex, 10); 27 | if(!isNaN(userMaxIndex)) 28 | $scope.datasource.maxIndex = userMaxIndex; 29 | }; 30 | 31 | $scope.setUserIndexes = function () { 32 | $scope.setUserMinIndex(); 33 | $scope.setUserMaxIndex(); 34 | }; 35 | 36 | $scope.delay = false; 37 | $timeout(function() { 38 | $scope.delay = true; 39 | }, 500); 40 | 41 | } 42 | ]); -------------------------------------------------------------------------------- /demo/visibility/visibility.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Visibility 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Visibility

18 | 19 |
20 | We have an internal mechanism to start data fetching after invisible scroller becomes visible. 21 | So generally you don't need to execute reload method when you play with scroller visibility. 22 |
23 | 24 |
25 |
26 | 29 |
30 |
31 | 32 |
33 |
    34 |
  • *{{item}}*
  • 35 |
36 |
37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /demo/visibility/visibility.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', ['$log', '$timeout', 3 | function (console, $timeout) { 4 | 5 | var get = function (index, count, success) { 6 | $timeout(function () { 7 | var result = []; 8 | for (var i = index; i <= index + count - 1; i++) { 9 | result.push("item #" + i); 10 | } 11 | success(result); 12 | }, 100); 13 | }; 14 | 15 | return { 16 | get: get 17 | }; 18 | } 19 | ]); -------------------------------------------------------------------------------- /demo/windowViewport/windowViewport-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scroller Demo (entire window) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
is loading: {{loading}}
17 |
top visible: {{topItem}}
18 |
19 |
20 |
21 | 22 |
23 |
24 |
28 | {{item}} 29 |
30 |
31 |
32 | 33 | 34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /demo/windowViewport/windowViewport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Entire window scrollable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | browse other examples 16 | 17 |

Entire window scrollable

18 | 19 |
20 | This sample demonstrates the scroller without viewport defining. 21 | As it follows from documentation: 22 | unless specified explicitly with the uiScrollViewport directive, browser window will be used as viewport. 23 | And this is the reason why this demo is placed in an iframe. You also can open it in a new tab. 24 |
25 | 26 |
27 |
28 |

Sample in iframe

29 | Open example in new tab 30 |
31 | 32 |
33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/windowViewport/windowViewport.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']) 2 | .factory('datasource', ['$log', '$timeout', 3 | function (console, $timeout) { 4 | 5 | var get = function (index, count, success) { 6 | $timeout(function () { 7 | var result = []; 8 | for (var i = index; i <= index + count - 1; i++) { 9 | result.push("item #" + i); 10 | } 11 | success(result); 12 | }, 100); 13 | }; 14 | 15 | return { 16 | get: get 17 | }; 18 | } 19 | ]); 20 | -------------------------------------------------------------------------------- /demo/windowviewportInline/windowviewportInline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inline blocks demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 73 | 74 |
75 | browse other examples 76 |

Inline blocks demo

77 | 78 |
79 |
is loading: {{loading}}
80 |
top visible: {{topItem}}
81 |
82 | 83 |
84 |
85 |
86 | *{{item.content}}* 87 |
88 |
89 |
90 | 91 |
92 | 93 | 94 | -------------------------------------------------------------------------------- /demo/windowviewportInline/windowviewportInline.js: -------------------------------------------------------------------------------- 1 | angular.module('application', ['ui.scroll']).factory('datasource', [ 2 | '$log', '$timeout', function(console, $timeout) { 3 | var get; 4 | get = function(index, count, success) { 5 | return $timeout(function() { 6 | var i, item, j, ref, ref1, result; 7 | result = []; 8 | for (i = j = ref = index, ref1 = index + count - 1; ref <= ref1 ? j <= ref1 : j >= ref1; i = ref <= ref1 ? ++j : --j) { 9 | item = {}; 10 | if (inlineDemo) { 11 | item.width = inlineDemo.getWidth(i); 12 | item.height = inlineDemo.getHeight(i); 13 | item.color = inlineDemo.getColor(i); 14 | } 15 | item.content = "item #" + i; 16 | result.push(item); 17 | } 18 | return success(result); 19 | }, 100); 20 | }; 21 | return { 22 | get: get 23 | }; 24 | } 25 | ]); 26 | 27 | 28 | 29 | 30 | /* 31 | //# sourceURL=src/windowviewportInline.js 32 | */ 33 | 34 | // --- 35 | // generated by coffee-script 1.9.2 -------------------------------------------------------------------------------- /dist/ui-scroll-grid.min.js: -------------------------------------------------------------------------------- 1 | angular.module("ui.scroll.grid",[]).directive("uiScrollTh",["$log","$timeout",function(t,n){function r(t){this.getLayout=function(){return t.getLayout()},this.applyLayout=function(n){return t.applyLayout(n)},this.columnFromPoint=function(n,r){return t.columnFromPoint(n,r)},Object.defineProperty(this,"columns",{get:function(){return t.getColumns()}})}function o(t,n){this.css=function(){var r=arguments[0],o=arguments[1];if(1==arguments.length)return n.header.css(r);2==arguments.length&&(n.header.css(r,o),t.forEachRow((function(t){return t[n.id].css(r,o)})),n.css[r]=o)},this.moveBefore=function(r){return t.moveBefore(n,r)},this.exchangeWith=function(r){return t.exchangeWith(n,r)},Object.defineProperty(this,"columnId",{get:function(){return n.id}})}function e(t,n,r){function o(t,n,r){var o=t.offset();return!(n=u.length||(r.push(n),0))},this.unregisterCell=function(t,n){var r=c.get(t),o=r.indexOf(n);r.splice(o,1),r.length||c.delete(t)},this.forEachRow=function(t){c.forEach(t)},this.getColumns=function(){var t=this,n=[];return u.slice().sort((function(t,n){return t.mapTo-n.mapTo})).forEach((function(r){return n.push(new o(t,r))})),n},this.getLayout=function(){var t=[];return u.forEach((function(n,r){return t.push({index:r,css:angular.extend({},n.css),mapTo:n.mapTo})})),t},this.applyLayout=function(t){if(!t||t.length!=u.length)throw new Error("Failed to apply layout - number of layouts should match number of columns");t.forEach((function(t,n){return u[n].applyLayout(t)})),a(u.map((function(t){return t.header}))),c.forEach((function(t){return a(t)}))},this.moveBefore=function(t,n){var r=n;if(n%1!=0&&(r=n?u[n.columnId].mapTo:u.length),!(r<0||r>u.length)){var o=t.mapTo,e=null;r-=oo?1:0,t.mapTo+=t.mapTo>=r?1:0,e=t.mapTo===r+1?t:e})),t.mapTo=r,t.moveBefore(e)}},this.exchangeWith=function(t,n){n<0||n>=u.length||(u.find((function(t){return t.mapTo===n})).mapTo=t.mapTo,t.mapTo=n)},this.columnFromPoint=function(t,n){var r=u.find((function(r){return r.columnFromPoint(t,n)}));return r?new o(this,r):void 0}}return{require:["^^uiScrollViewport"],restrict:"A",link:function(t,n,r,o){o[0].gridController=o[0].gridController||new i(o[0]),o[0].gridController.registerColumn(n)}}}]).directive("uiScrollTd",(function(){return{require:["?^^uiScrollViewport"],restrict:"A",link:function(t,n,r,o){if(o[0]){var e=t,i=t.uiScrollTdInitializer;i||(i=t.uiScrollTdInitializer={linking:!0}),i.linking||(e=i.scope);var u=o[0].gridController;u.registerCell(e,n)&&t.$on("$destroy",(function(){return u.unregisterCell(e,n)})),i.linking||i.onLink()}}}})); 2 | //# sourceMappingURL=ui-scroll-grid.min.js.map -------------------------------------------------------------------------------- /dist/ui-scroll-jqlite.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-ui-scroll 3 | * https://github.com/angular-ui/ui-scroll.git 4 | * This module is deprecated since 1.6.0 and will be removed in future versions! 5 | * License: MIT 6 | */ 7 | (function () { 8 | 'use strict'; 9 | 10 | angular.module('ui.scroll.jqlite', []); 11 | 12 | })(); -------------------------------------------------------------------------------- /dist/ui-scroll-jqlite.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";angular.module("ui.scroll.jqlite",[])}(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-scroll", 3 | "description": "AngularJS virtual scrolling module", 4 | "version": "1.9.1", 5 | "src": "./src/", 6 | "public": "./dist/", 7 | "main": "./dist/ui-scroll.js", 8 | "homepage": "https://github.com/angular-ui/ui-scroll", 9 | "author": { 10 | "name": "Michael Feingold", 11 | "email": "mfeingold@hill30.com" 12 | }, 13 | "maintainers": [ 14 | { 15 | "name": "Denis Hilt", 16 | "web": "https://github.com/dhilt" 17 | } 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/angular-ui/ui-scroll.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/angular-ui/ui-scroll/issues" 25 | }, 26 | "license": "MIT", 27 | "scripts": { 28 | "prod-build": "webpack --color", 29 | "prod-test": "karma start test/config/karma.conf.js", 30 | "dev-server": "webpack-dev-server --color", 31 | "dev-test": "karma start test/config/karma.conf.js", 32 | "hint-test": "jshint --verbose test", 33 | "hint-src": "jshint --verbose src", 34 | "build": "npm run hint-src && npm run prod-build && npm run hint-test && npm run prod-test", 35 | "dev": "npm-run-all --parallel dev-server dev-test", 36 | "test": "npm run dev-test", 37 | "start": "npm run dev-server" 38 | }, 39 | "peerDependencies": { 40 | "angular": ">=1.2.0" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.21.4", 44 | "@babel/preset-env": "^7.21.4", 45 | "babel-loader": "^9.1.2", 46 | "clean-webpack-plugin": "^4.0.0", 47 | "copy-webpack-plugin": "^11.0.0", 48 | "jasmine-core": "^4.6.0", 49 | "jshint": "^2.13.6", 50 | "karma": "^6.4.1", 51 | "karma-chrome-launcher": "^3.1.1", 52 | "karma-firefox-launcher": "^2.1.2", 53 | "karma-jasmine": "^5.1.0", 54 | "karma-sourcemap-loader": "^0.4.0", 55 | "karma-webpack": "^5.0.0", 56 | "npm-run-all": "^4.1.5", 57 | "puppeteer": "^19.9.1", 58 | "terser-webpack-plugin": "^5.3.7", 59 | "webpack": "^5.79.0", 60 | "webpack-cli": "^5.0.1", 61 | "webpack-dev-server": "^4.13.3" 62 | }, 63 | "keywords": [ 64 | "angular", 65 | "angularjs", 66 | "angular.ui", 67 | "angular-ui", 68 | "ui.scroll", 69 | "ui-scroll", 70 | "angular-ui-scroll", 71 | "virtual", 72 | "unlimited", 73 | "infinite", 74 | "live", 75 | "perpetual", 76 | "scroll", 77 | "scroller", 78 | "scrolling" 79 | ] 80 | } -------------------------------------------------------------------------------- /src/modules/buffer.js: -------------------------------------------------------------------------------- 1 | import { OPERATIONS } from './utils'; 2 | 3 | export default function ScrollBuffer(elementRoutines, bufferSize, startIndex) { 4 | const buffer = Object.create(Array.prototype); 5 | 6 | angular.extend(buffer, { 7 | size: bufferSize, 8 | 9 | reset(startIndex) { 10 | buffer.remove(0, buffer.length); 11 | buffer.eof = false; 12 | buffer.bof = false; 13 | buffer.first = startIndex; 14 | buffer.next = startIndex; 15 | buffer.minIndex = startIndex; 16 | buffer.maxIndex = startIndex; 17 | buffer.minIndexUser = null; 18 | buffer.maxIndexUser = null; 19 | }, 20 | 21 | append(items) { 22 | items.forEach((item) => { 23 | ++buffer.next; 24 | buffer.insert(OPERATIONS.APPEND, item); 25 | }); 26 | buffer.maxIndex = buffer.eof ? buffer.next - 1 : Math.max(buffer.next - 1, buffer.maxIndex); 27 | }, 28 | 29 | prepend(items, immutableTop) { 30 | items.reverse().forEach((item) => { 31 | if (immutableTop) { 32 | ++buffer.next; 33 | } 34 | else { 35 | --buffer.first; 36 | } 37 | buffer.insert(OPERATIONS.PREPEND, item); 38 | }); 39 | buffer.minIndex = buffer.bof ? buffer.minIndex = buffer.first : Math.min(buffer.first, buffer.minIndex); 40 | }, 41 | 42 | /** 43 | * inserts wrapped element in the buffer 44 | * the first argument is either operation keyword (see below) or a number for operation 'insert' 45 | * for insert the number is the index for the buffer element the new one have to be inserted after 46 | * operations: 'append', 'prepend', 'insert', 'remove', 'none' 47 | */ 48 | insert(operation, item, shiftTop) { 49 | const wrapper = { 50 | item: item 51 | }; 52 | 53 | if (operation % 1 === 0) { // it is an insert 54 | wrapper.op = OPERATIONS.INSERT; 55 | buffer.splice(operation, 0, wrapper); 56 | if (shiftTop) { 57 | buffer.first--; 58 | } 59 | else { 60 | buffer.next++; 61 | } 62 | } else { 63 | wrapper.op = operation; 64 | switch (operation) { 65 | case OPERATIONS.APPEND: 66 | buffer.push(wrapper); 67 | break; 68 | case OPERATIONS.PREPEND: 69 | buffer.unshift(wrapper); 70 | break; 71 | } 72 | } 73 | }, 74 | 75 | // removes elements from buffer 76 | remove(arg1, arg2) { 77 | if (angular.isNumber(arg1)) { 78 | // removes items from arg1 (including) through arg2 (excluding) 79 | for (let i = arg1; i < arg2; i++) { 80 | elementRoutines.removeElement(buffer[i]); 81 | } 82 | return buffer.splice(arg1, arg2 - arg1); 83 | } 84 | // removes single item (wrapper) from the buffer 85 | buffer.splice(buffer.indexOf(arg1), 1); 86 | if (arg1.shiftTop && buffer.first === this.getAbsMinIndex()) { 87 | this.incrementMinIndex(); 88 | } 89 | else { 90 | this.decrementMaxIndex(); 91 | } 92 | if (arg1.shiftTop) { 93 | buffer.first++; 94 | } 95 | else { 96 | buffer.next--; 97 | } 98 | if (!buffer.length) { 99 | buffer.minIndex = Math.min(buffer.maxIndex, buffer.minIndex); 100 | } 101 | 102 | return elementRoutines.removeElementAnimated(arg1); 103 | }, 104 | 105 | incrementMinIndex() { 106 | if (buffer.minIndexUser !== null) { 107 | if (buffer.minIndex > buffer.minIndexUser) { 108 | buffer.minIndexUser++; 109 | return; 110 | } 111 | if (buffer.minIndex === buffer.minIndexUser) { 112 | buffer.minIndexUser++; 113 | } 114 | } 115 | buffer.minIndex++; 116 | }, 117 | 118 | decrementMaxIndex() { 119 | if (buffer.maxIndexUser !== null && buffer.maxIndex <= buffer.maxIndexUser) { 120 | buffer.maxIndexUser--; 121 | } 122 | buffer.maxIndex--; 123 | }, 124 | 125 | getAbsMinIndex() { 126 | if (buffer.minIndexUser !== null) { 127 | return Math.min(buffer.minIndexUser, buffer.minIndex); 128 | } 129 | return buffer.minIndex; 130 | }, 131 | 132 | getAbsMaxIndex() { 133 | if (buffer.maxIndexUser !== null) { 134 | return Math.max(buffer.maxIndexUser, buffer.maxIndex); 135 | } 136 | return buffer.maxIndex; 137 | }, 138 | 139 | effectiveHeight(elements) { 140 | if (!elements.length) { 141 | return 0; 142 | } 143 | let top = Number.MAX_VALUE; 144 | let bottom = Number.NEGATIVE_INFINITY; 145 | elements.forEach((wrapper) => { 146 | if (wrapper.element[0].offsetParent) { 147 | // element style is not display:none 148 | top = Math.min(top, wrapper.element.offset().top); 149 | bottom = Math.max(bottom, wrapper.element.offset().top + wrapper.element.outerHeight(true)); 150 | } 151 | }); 152 | return Math.max(0, bottom - top); 153 | }, 154 | 155 | getItems() { 156 | return buffer.filter(item => item.op === OPERATIONS.NONE); 157 | }, 158 | 159 | getFirstItem() { 160 | const list = buffer.getItems(); 161 | if (!list.length) { 162 | return null; 163 | } 164 | return list[0].item; 165 | }, 166 | 167 | getLastItem() { 168 | const list = buffer.getItems(); 169 | if (!list.length) { 170 | return null; 171 | } 172 | return list[list.length - 1].item; 173 | } 174 | 175 | }); 176 | 177 | buffer.reset(startIndex); 178 | 179 | return buffer; 180 | } 181 | -------------------------------------------------------------------------------- /src/modules/elementRoutines.js: -------------------------------------------------------------------------------- 1 | const hideClassToken = 'ng-ui-scroll-hide'; 2 | 3 | export default class ElementRoutines { 4 | 5 | static addCSSRules() { 6 | const selector = '.' + hideClassToken; 7 | const rules = 'display: none'; 8 | const sheet = document.styleSheets[0]; 9 | let index; 10 | try { 11 | index = sheet.cssRules.length; 12 | } catch (err) { 13 | index = 0; 14 | } 15 | if('insertRule' in sheet) { 16 | sheet.insertRule(selector + '{' + rules + '}', index); 17 | } 18 | else if('addRule' in sheet) { 19 | sheet.addRule(selector, rules, index); 20 | } 21 | } 22 | 23 | constructor($injector, $q) { 24 | this.$animate = ($injector.has && $injector.has('$animate')) ? $injector.get('$animate') : null; 25 | this.isAngularVersionLessThen1_3 = angular.version.major === 1 && angular.version.minor < 3; 26 | this.$q = $q; 27 | } 28 | 29 | hideElement(wrapper) { 30 | wrapper.element.addClass(hideClassToken); 31 | } 32 | 33 | showElement(wrapper) { 34 | wrapper.element.removeClass(hideClassToken); 35 | } 36 | 37 | insertElement(newElement, previousElement) { 38 | previousElement.after(newElement); 39 | return []; 40 | } 41 | 42 | removeElement(wrapper) { 43 | wrapper.element.remove(); 44 | wrapper.scope.$destroy(); 45 | return []; 46 | } 47 | 48 | insertElementAnimated(newElement, previousElement) { 49 | if (!this.$animate) { 50 | return this.insertElement(newElement, previousElement); 51 | } 52 | 53 | if (this.isAngularVersionLessThen1_3) { 54 | const deferred = this.$q.defer(); 55 | // no need for parent - previous element is never null 56 | this.$animate.enter(newElement, null, previousElement, () => deferred.resolve()); 57 | 58 | return [deferred.promise]; 59 | } 60 | 61 | // no need for parent - previous element is never null 62 | return [this.$animate.enter(newElement, null, previousElement)]; 63 | } 64 | 65 | removeElementAnimated(wrapper) { 66 | if (!this.$animate) { 67 | return this.removeElement(wrapper); 68 | } 69 | 70 | if (this.isAngularVersionLessThen1_3) { 71 | const deferred = this.$q.defer(); 72 | this.$animate.leave(wrapper.element, () => { 73 | wrapper.scope.$destroy(); 74 | return deferred.resolve(); 75 | }); 76 | 77 | return [deferred.promise]; 78 | } 79 | 80 | return [(this.$animate.leave(wrapper.element)).then(() => wrapper.scope.$destroy())]; 81 | } 82 | } -------------------------------------------------------------------------------- /src/modules/padding.js: -------------------------------------------------------------------------------- 1 | // Can't just extend the Array, due to Babel does not support built-in classes extending 2 | // This solution was taken from https://stackoverflow.com/questions/46897414/es6-class-extends-array-workaround-for-es5-babel-transpile 3 | class CacheProto { 4 | add(item) { 5 | for (let i = this.length - 1; i >= 0; i--) { 6 | if (this[i].index === item.scope.$index) { 7 | this[i].height = item.element.outerHeight(); 8 | return; 9 | } 10 | } 11 | this.push({ 12 | index: item.scope.$index, 13 | height: item.element.outerHeight() 14 | }); 15 | this.sort((a, b) => ((a.index < b.index) ? -1 : ((a.index > b.index) ? 1 : 0))); 16 | } 17 | 18 | remove(argument, _shiftTop) { 19 | const index = argument % 1 === 0 ? argument : argument.scope.$index; 20 | const shiftTop = argument % 1 === 0 ? _shiftTop : argument.shiftTop; 21 | for (let i = this.length - 1; i >= 0; i--) { 22 | if (this[i].index === index) { 23 | this.splice(i, 1); 24 | break; 25 | } 26 | } 27 | if (!shiftTop) { 28 | for (let i = this.length - 1; i >= 0; i--) { 29 | if (this[i].index > index) { 30 | this[i].index--; 31 | } 32 | } 33 | } 34 | } 35 | 36 | clear() { 37 | this.length = 0; 38 | } 39 | } 40 | 41 | function Cache() { 42 | const instance = []; 43 | instance.push.apply(instance, arguments); 44 | Object.setPrototypeOf(instance, Cache.prototype); 45 | return instance; 46 | } 47 | Cache.prototype = Object.create(Array.prototype); 48 | Object.getOwnPropertyNames(CacheProto.prototype).forEach(methodName => 49 | Cache.prototype[methodName] = CacheProto.prototype[methodName] 50 | ); 51 | 52 | function generateElement(template) { 53 | if (template.nodeType !== Node.ELEMENT_NODE) { 54 | throw new Error('ui-scroll directive requires an Element node for templating the view'); 55 | } 56 | let element; 57 | switch (template.tagName.toLowerCase()) { 58 | case 'dl': 59 | throw new Error(`ui-scroll directive does not support <${template.tagName}> as a repeating tag: ${template.outerHTML}`); 60 | case 'tr': 61 | let table = angular.element('
'); 62 | element = table.find('tr'); 63 | break; 64 | case 'li': 65 | element = angular.element('
  • '); 66 | break; 67 | default: 68 | element = angular.element('
    '); 69 | } 70 | return element; 71 | } 72 | 73 | class Padding { 74 | constructor(template) { 75 | this.element = generateElement(template); 76 | this.cache = new Cache(); 77 | } 78 | 79 | height() { 80 | return this.element.height.apply(this.element, arguments); 81 | } 82 | } 83 | 84 | export default Padding; -------------------------------------------------------------------------------- /src/modules/utils.js: -------------------------------------------------------------------------------- 1 | export const OPERATIONS = { 2 | PREPEND: 'prepend', 3 | APPEND: 'append', 4 | INSERT: 'insert', 5 | REMOVE: 'remove', 6 | NONE: 'none' 7 | }; 8 | -------------------------------------------------------------------------------- /src/ui-scroll-jqlite.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-ui-scroll 3 | * https://github.com/angular-ui/ui-scroll.git 4 | * This module is deprecated since 1.6.0 and will be removed in future versions! 5 | * License: MIT 6 | */ 7 | (function () { 8 | 'use strict'; 9 | 10 | angular.module('ui.scroll.jqlite', []); 11 | 12 | })(); -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "browser": true, 5 | "eqeqeq": true, 6 | "expr": true, 7 | "esversion": 9, 8 | "forin": true, 9 | "freeze": true, 10 | "futurehostile": true, 11 | "iterator": true, 12 | "jasmine": true, 13 | "jquery": true, 14 | "latedef": true, 15 | "noarg": true, 16 | "nocomma": true, 17 | "node": true, 18 | "nonbsp": true, 19 | "nonew": true, 20 | "smarttabs": true, 21 | "strict": true, 22 | "sub": true, 23 | "trailing": true, 24 | "undef": true, 25 | "unused": true, 26 | "globals": { 27 | "angular": false, 28 | "inject": false, 29 | "runTest": true, 30 | "runGridTest": false, 31 | "Helper": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/BasicSetupSpec.js: -------------------------------------------------------------------------------- 1 | describe('uiScroll', function() { 2 | 'use strict'; 3 | 4 | beforeEach(module('ui.scroll')); 5 | beforeEach(module('ui.scroll.test.datasources')); 6 | 7 | describe('basic setup', function() { 8 | 9 | it('should throw an error if the template\'s wrong', function() { 10 | runTest({ datasource: 'myEmptyDatasource', extra: 'ng-if="false"' }, null, { 11 | catch: function(error) { 12 | expect(error.message).toBe('ui-scroll directive requires an Element node for templating the view'); 13 | } 14 | } 15 | ); 16 | }); 17 | 18 | var scrollSettings = { datasource: 'myEmptyDatasource' }; 19 | 20 | it('should bind to window scroll and resize events and unbind them after the scope is destroyed', function() { 21 | spyOn($.fn, 'on').and.callThrough(); 22 | spyOn($.fn, 'off').and.callThrough(); 23 | runTest(scrollSettings, 24 | function(viewport) { 25 | expect($.fn.on.calls.all().length).toBe(4); 26 | expect($.fn.on.calls.all()[0].args[0]).toBe('visibilitychange'); 27 | expect($.fn.on.calls.all()[1].args[0]).toBe('mousewheel'); 28 | expect($.fn.on.calls.all()[1].object[0]).toBe(viewport[0]); 29 | expect($.fn.on.calls.all()[2].args[0]).toBe('resize'); 30 | expect($.fn.on.calls.all()[2].object[0]).toBe(viewport[0]); 31 | expect($.fn.on.calls.all()[3].args[0]).toBe('scroll'); 32 | expect($.fn.on.calls.all()[3].object[0]).toBe(viewport[0]); 33 | }, { 34 | cleanupTest: function(viewport, scope, $timeout) { 35 | $timeout(function() { 36 | expect($.fn.off.calls.all().length).toBe(4); 37 | expect($.fn.off.calls.all()[0].args[0]).toBe('visibilitychange'); 38 | expect($.fn.off.calls.all()[1].args[0]).toBe('resize'); 39 | expect($.fn.off.calls.all()[1].object[0]).toBe(viewport[0]); 40 | expect($.fn.off.calls.all()[2].args[0]).toBe('scroll'); 41 | expect($.fn.off.calls.all()[2].object[0]).toBe(viewport[0]); 42 | expect($.fn.off.calls.all()[3].args[0]).toBe('mousewheel'); 43 | expect($.fn.off.calls.all()[3].object[0]).toBe(viewport[0]); 44 | }); 45 | } 46 | } 47 | ); 48 | }); 49 | 50 | it('should create 2 divs of 0 height', function() { 51 | runTest(scrollSettings, 52 | function(viewport) { 53 | expect(viewport.children().length).toBe(2); 54 | 55 | var topPadding = viewport.children()[0]; 56 | expect(topPadding.tagName.toLowerCase()).toBe('div'); 57 | expect(Helper.getTopPadding(viewport)).toBe(0); 58 | 59 | var bottomPadding = viewport.children()[1]; 60 | expect(bottomPadding.tagName.toLowerCase()).toBe('div'); 61 | expect(Helper.getBottomPadding(viewport)).toBe(0); 62 | } 63 | ); 64 | }); 65 | 66 | it('should call get on the datasource 2 times ', function() { 67 | var spy; 68 | inject(function(myEmptyDatasource) { 69 | spy = spyOn(myEmptyDatasource, 'get').and.callThrough(); 70 | }); 71 | runTest(scrollSettings, 72 | function() { 73 | expect(spy.calls.all().length).toBe(2); 74 | expect(spy.calls.all()[0].args.length).toBe(3); 75 | expect(spy.calls.all()[0].args[0]).toBe(1); 76 | expect(spy.calls.all()[0].args[1]).toBe(10); 77 | expect(spy.calls.all()[1].args.length).toBe(3); 78 | expect(spy.calls.all()[1].args[0]).toBe(-9); 79 | expect(spy.calls.all()[1].args[1]).toBe(10); 80 | } 81 | ); 82 | }); 83 | }); 84 | 85 | describe('basic setup (new datasource get signature)', function() { 86 | var scrollSettings = { datasource: 'myDescriptoEmptyDatasource' }; 87 | 88 | it('should call get on the datasource 2 times ', function() { 89 | var spy; 90 | inject(function(myDescriptoEmptyDatasource) { 91 | spy = spyOn(myDescriptoEmptyDatasource, 'get').and.callThrough(); 92 | }); 93 | runTest(scrollSettings, 94 | function() { 95 | expect(spy.calls.all().length).toBe(2); 96 | expect(spy.calls.all()[0].args.length).toBe(2); 97 | expect(spy.calls.all()[0].args[0].index).toBe(1); 98 | expect(spy.calls.all()[0].args[0].count).toBe(10); 99 | expect('append' in spy.calls.all()[0].args[0]).toBe(true); 100 | expect(spy.calls.all()[0].args[0].append).toBeUndefined(); 101 | expect('prepend' in spy.calls.all()[0].args[0]).toBe(false); 102 | expect(spy.calls.all()[1].args.length).toBe(2); 103 | expect(spy.calls.all()[1].args[0].index).toBe(-9); 104 | expect(spy.calls.all()[1].args[0].count).toBe(10); 105 | expect('append' in spy.calls.all()[1].args[0]).toBe(false); 106 | expect('prepend' in spy.calls.all()[1].args[0]).toBe(true); 107 | expect(spy.calls.all()[1].args[0].prepend).toBeUndefined(); 108 | } 109 | ); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/BufferCleanupSpec.js: -------------------------------------------------------------------------------- 1 | describe('uiScroll', () => { 2 | 'use strict'; 3 | 4 | let datasource; 5 | beforeEach(module('ui.scroll')); 6 | beforeEach(module('ui.scroll.test.datasources')); 7 | 8 | const injectDatasource = datasourceToken => 9 | beforeEach( 10 | inject([datasourceToken, _datasource => 11 | datasource = _datasource 12 | ]) 13 | ); 14 | 15 | describe('buffer cleanup', () => { 16 | const settings = { 17 | datasource: 'myEdgeDatasource', // items range is [-5..6] 18 | adapter: 'adapter', 19 | viewportHeight: 60, 20 | itemHeight: 20, 21 | padding: 0.3, 22 | startIndex: 3, 23 | bufferSize: 3 24 | }; 25 | 26 | injectDatasource('myEdgeDatasource'); 27 | 28 | const cleanBuffer = (scope, applyUpdateOptions) => { 29 | const get = datasource.get; 30 | const removedItems = []; 31 | // sync the datasource 32 | datasource.get = (index, count, success) => { 33 | if (removedItems.indexOf('item' + index) !== -1) { 34 | index += removedItems.length; 35 | } 36 | get(index, count, success); 37 | }; 38 | // clean up the buffer 39 | scope.adapter.applyUpdates(item => { 40 | removedItems.push(item); 41 | return []; 42 | }, applyUpdateOptions); 43 | }; 44 | 45 | const shouldWorkWhenEOF = (viewport, scope, options) => { 46 | expect(scope.adapter.isBOF()).toBe(false); 47 | expect(scope.adapter.isEOF()).toBe(true); 48 | expect(scope.adapter.bufferFirst).toBe('item0'); 49 | expect(scope.adapter.bufferLast).toBe('item6'); 50 | 51 | // remove items 0..6 items form -5..6 datasource 52 | cleanBuffer(scope, options); 53 | 54 | // result [-5..-1] 55 | expect(scope.adapter.isBOF()).toBe(true); 56 | expect(scope.adapter.isEOF()).toBe(true); 57 | expect(Helper.getRow(viewport, 1)).toBe('-5: item-5'); 58 | expect(Helper.getRow(viewport, 2)).toBe('-4: item-4'); 59 | expect(Helper.getRow(viewport, 3)).toBe('-3: item-3'); 60 | expect(Helper.getRow(viewport, 4)).toBe('-2: item-2'); 61 | expect(Helper.getRow(viewport, 5)).toBe('-1: item-1'); 62 | expect(scope.adapter.bufferLength).toBe(5); 63 | }; 64 | 65 | it('should be consistent on forward direction when eof with immutabeTop', () => 66 | runTest(settings, (viewport, scope) => 67 | shouldWorkWhenEOF(viewport, scope, { immutableTop: true }) 68 | ) 69 | ); 70 | 71 | it('should be consistent on forward direction when eof without immutabeTop', () => 72 | runTest(settings, (viewport, scope) => 73 | shouldWorkWhenEOF(viewport, scope) 74 | ) 75 | ); 76 | 77 | const shouldWorkWhenNotEOF = (viewport, scope, options) => { 78 | expect(scope.adapter.isBOF()).toBe(false); 79 | expect(scope.adapter.isEOF()).toBe(false); 80 | expect(scope.adapter.bufferFirst).toBe('item-4'); 81 | expect(scope.adapter.bufferLast).toBe('item1'); 82 | 83 | // remove items -4..1 items form -5..6 datasource 84 | cleanBuffer(scope, options); 85 | 86 | // result [-5, 2, 3, 4] 87 | expect(scope.adapter.isBOF()).toBe(true); 88 | expect(scope.adapter.isEOF()).toBe(false); 89 | expect(Helper.getRow(viewport, 1)).toBe('-5: item-5'); 90 | expect(Helper.getRow(viewport, 2)).toBe('-4: item2'); 91 | expect(Helper.getRow(viewport, 3)).toBe('-3: item3'); 92 | expect(Helper.getRow(viewport, 4)).toBe('-2: item4'); 93 | expect(scope.adapter.bufferLength).toBe(4); 94 | }; 95 | 96 | it('should be consistent on forward direction when not eof with immutabeTop', () => 97 | runTest({ 98 | ...settings, 99 | startIndex: -1, 100 | viewportHeight: 40 101 | }, (viewport, scope) => 102 | shouldWorkWhenNotEOF(viewport, scope, { immutableTop: true }) 103 | ) 104 | ); 105 | 106 | it('should be consistent on forward direction when not eof without immutabeTop', () => 107 | runTest({ 108 | ...settings, 109 | startIndex: -1, 110 | viewportHeight: 40 111 | }, (viewport, scope) => 112 | shouldWorkWhenNotEOF(viewport, scope) 113 | ) 114 | ); 115 | 116 | it('should be consistent on backward direction when bof with immutableTop', () => 117 | runTest({ 118 | ...settings, 119 | startIndex: -3, 120 | padding: 0.5 121 | }, (viewport, scope) => { 122 | expect(scope.adapter.isBOF()).toBe(true); 123 | expect(scope.adapter.isEOF()).toBe(false); 124 | expect(scope.adapter.bufferFirst).toBe('item-5'); 125 | expect(scope.adapter.bufferLast).toBe('item1'); 126 | 127 | // remove items -5..1 items form -5..6 datasource 128 | cleanBuffer(scope, { immutableTop: true }); 129 | 130 | // result [2..6] 131 | expect(scope.adapter.isBOF()).toBe(true); 132 | expect(scope.adapter.isEOF()).toBe(true); 133 | expect(Helper.getRow(viewport, 1)).toBe('-5: item2'); 134 | expect(Helper.getRow(viewport, 2)).toBe('-4: item3'); 135 | expect(Helper.getRow(viewport, 3)).toBe('-3: item4'); 136 | expect(Helper.getRow(viewport, 4)).toBe('-2: item5'); 137 | expect(Helper.getRow(viewport, 5)).toBe('-1: item6'); 138 | expect(scope.adapter.bufferLength).toBe(5); 139 | }) 140 | ); 141 | 142 | it('should be consistent on backward direction when bof without immutableTop', () => 143 | runTest({ 144 | ...settings, 145 | startIndex: -3, 146 | padding: 0.5 147 | }, (viewport, scope) => { 148 | expect(scope.adapter.isBOF()).toBe(true); 149 | expect(scope.adapter.isEOF()).toBe(false); 150 | expect(scope.adapter.bufferFirst).toBe('item-5'); 151 | expect(scope.adapter.bufferLast).toBe('item1'); 152 | 153 | // remove items -5..1 items form -5..6 datasource 154 | cleanBuffer(scope); 155 | 156 | // result [2..6] 157 | expect(scope.adapter.isBOF()).toBe(true); 158 | expect(scope.adapter.isEOF()).toBe(true); 159 | expect(Helper.getRow(viewport, 1)).toBe('2: item2'); 160 | expect(Helper.getRow(viewport, 2)).toBe('3: item3'); 161 | expect(Helper.getRow(viewport, 3)).toBe('4: item4'); 162 | expect(Helper.getRow(viewport, 4)).toBe('5: item5'); 163 | expect(Helper.getRow(viewport, 5)).toBe('6: item6'); 164 | expect(scope.adapter.bufferLength).toBe(5); 165 | }) 166 | ); 167 | 168 | const shouldWorkWhenNotBOF = (viewport, scope, options) => { 169 | expect(scope.adapter.isBOF()).toBe(false); 170 | expect(scope.adapter.isEOF()).toBe(false); 171 | expect(scope.adapter.bufferFirst).toBe('item-4'); 172 | expect(scope.adapter.bufferLast).toBe('item2'); 173 | 174 | // remove items -4..2 items form -5..6 datasource 175 | cleanBuffer(scope, options); 176 | 177 | // result [-5, 3, 4, 5, 6] 178 | expect(scope.adapter.isBOF()).toBe(true); 179 | expect(scope.adapter.isEOF()).toBe(true); 180 | expect(Helper.getRow(viewport, 1)).toBe('-5: item-5'); 181 | expect(Helper.getRow(viewport, 2)).toBe('-4: item3'); 182 | expect(Helper.getRow(viewport, 3)).toBe('-3: item4'); 183 | expect(Helper.getRow(viewport, 4)).toBe('-2: item5'); 184 | expect(Helper.getRow(viewport, 5)).toBe('-1: item6'); 185 | expect(scope.adapter.bufferLength).toBe(5); 186 | }; 187 | 188 | it('should be consistent on backward direction when not bof with immutableTop', () => 189 | runTest({ 190 | ...settings, 191 | startIndex: -1, 192 | padding: 0.3 193 | }, (viewport, scope) => 194 | shouldWorkWhenNotBOF(viewport, scope, { immutableTop: true }) 195 | ) 196 | ); 197 | 198 | it('should be consistent on backward direction when not bof without immutableTop', () => 199 | runTest({ 200 | ...settings, 201 | startIndex: -1, 202 | padding: 0.3 203 | }, (viewport, scope) => 204 | shouldWorkWhenNotBOF(viewport, scope) 205 | ) 206 | ); 207 | }); 208 | 209 | }); 210 | -------------------------------------------------------------------------------- /test/UserIndicesSpec.js: -------------------------------------------------------------------------------- 1 | /*global describe, beforeEach, module, inject, it, expect, runTest, Helper */ 2 | describe('uiScroll main/max indices', function() { 3 | 'use strict'; 4 | 5 | let datasource; 6 | beforeEach(module('ui.scroll')); 7 | beforeEach(module('ui.scroll.test.datasources')); 8 | 9 | const injectDatasource = (datasourceToken) => 10 | beforeEach( 11 | inject([datasourceToken, function(_datasource) { 12 | datasource = _datasource; 13 | }]) 14 | ); 15 | 16 | const viewportHeight = 120; 17 | const itemHeight = 20; 18 | const bufferSize = 3; 19 | const userMinIndex = -99; // for 100 items 20 | const userMaxIndex = 100; 21 | 22 | const scrollSettings = { 23 | datasource: 'myInfiniteDatasource', 24 | viewportHeight: viewportHeight, 25 | itemHeight: itemHeight, 26 | bufferSize: bufferSize, 27 | adapter: 'adapter' 28 | }; 29 | 30 | describe('Setting\n', () => { 31 | injectDatasource('myInfiniteDatasource'); 32 | 33 | it('should set up bottom padding element\'s height after user max index is set', () => 34 | runTest(scrollSettings, 35 | (viewport) => { 36 | expect(viewport.scrollTop()).toBe(itemHeight * bufferSize); 37 | 38 | datasource.maxIndex = userMaxIndex; 39 | 40 | const virtualItemsAmount = userMaxIndex - (viewportHeight / itemHeight) - bufferSize; 41 | expect(Helper.getBottomPadding(viewport)).toBe(itemHeight * virtualItemsAmount); 42 | expect(viewport.scrollTop()).toBe(itemHeight * bufferSize); 43 | } 44 | ) 45 | ); 46 | 47 | it('should set up top padding element\'s height after user min index is set', () => 48 | runTest(scrollSettings, 49 | (viewport) => { 50 | expect(viewport.scrollTop()).toBe(itemHeight * bufferSize); 51 | 52 | datasource.minIndex = userMinIndex; 53 | 54 | const virtualItemsAmount = (-1) * userMinIndex - bufferSize + 1; 55 | expect(Helper.getTopPadding(viewport)).toBe(itemHeight * virtualItemsAmount); 56 | expect(viewport.scrollTop()).toBe(itemHeight * ((-1) * userMinIndex + 1)); 57 | } 58 | ) 59 | ); 60 | 61 | }); 62 | 63 | describe('Pre-setting\n', () => { 64 | injectDatasource('myInfiniteDatasource'); 65 | 66 | it('should work with maxIndex pre-set on datasource', () => { 67 | datasource.maxIndex = userMaxIndex; 68 | runTest(scrollSettings, 69 | (viewport) => { 70 | const virtualItemsAmount = userMaxIndex - (viewportHeight / itemHeight) - bufferSize; 71 | expect(Helper.getBottomPadding(viewport)).toBe(itemHeight * virtualItemsAmount); 72 | expect(viewport.scrollTop()).toBe(itemHeight * bufferSize); 73 | } 74 | ); 75 | }); 76 | 77 | it('should work with minIndex pre-set on datasource', () => { 78 | datasource.minIndex = userMinIndex; 79 | runTest(scrollSettings, 80 | (viewport) => { 81 | const virtualItemsAmount = (-1) * userMinIndex - bufferSize + 1; 82 | expect(Helper.getTopPadding(viewport)).toBe(itemHeight * virtualItemsAmount); 83 | expect(viewport.scrollTop()).toBe(itemHeight * ((-1) * userMinIndex + 1)); 84 | } 85 | ); 86 | }); 87 | 88 | it('should work when the viewport is big enough to include more than 1 pack of item (last)', () => { 89 | const viewportHeight = 450; 90 | const _topItemsCount = Math.round(viewportHeight * 0.5 / itemHeight); 91 | const _topPackCount = Math.ceil(_topItemsCount / bufferSize); 92 | const _minIndex = (-1) * _topPackCount * bufferSize + 1; // one additinal adjustment should occur 93 | datasource.minIndex = _minIndex; 94 | datasource.maxIndex = userMaxIndex; 95 | runTest(Object.assign({}, scrollSettings, { viewportHeight }), 96 | (viewport) => { 97 | expect(Helper.getTopPadding(viewport)).toBe(0); 98 | expect(viewport.scrollTop()).toBe(_topPackCount * bufferSize * itemHeight); 99 | } 100 | ); 101 | }); 102 | 103 | it('should work when the viewport is big enough to include more than 1 pack of item (first)', () => { 104 | const viewportHeight = 450; 105 | const _topItemsCount = Math.round(viewportHeight * 0.5 / itemHeight); 106 | const _topPackCount = Math.ceil(_topItemsCount / bufferSize); 107 | let _minIndex = -1; // ~9 additinal adjustments should occur 108 | datasource.minIndex = _minIndex; 109 | datasource.maxIndex = userMaxIndex; 110 | runTest(Object.assign({}, scrollSettings, { viewportHeight }), 111 | (viewport) => { 112 | expect(Helper.getTopPadding(viewport)).toBe(0); 113 | expect(viewport.scrollTop()).toBe(_topPackCount * bufferSize * itemHeight); 114 | } 115 | ); 116 | }); 117 | 118 | }); 119 | 120 | describe('Reload\n', () => { 121 | injectDatasource('myResponsiveDatasource'); 122 | beforeEach(() => { 123 | datasource.min = userMinIndex; 124 | datasource.max = userMaxIndex; 125 | datasource.init(); 126 | }); 127 | 128 | it('should persist user maxIndex after reload', () => { 129 | datasource.maxIndex = userMaxIndex; 130 | runTest(Object.assign({}, scrollSettings, { datasource: 'myResponsiveDatasource' }), 131 | (viewport, scope) => { 132 | scope.adapter.reload(); 133 | const virtualItemsAmount = userMaxIndex - (viewportHeight / itemHeight) - bufferSize; 134 | expect(Helper.getBottomPadding(viewport)).toBe(itemHeight * virtualItemsAmount); 135 | expect(viewport.scrollTop()).toBe(itemHeight * bufferSize); 136 | } 137 | ); 138 | }); 139 | 140 | it('should persist user minIndex after reload', () => { 141 | datasource.minIndex = userMinIndex; 142 | runTest(Object.assign({}, scrollSettings, { datasource: 'myResponsiveDatasource' }), 143 | (viewport, scope) => { 144 | scope.adapter.reload(); 145 | const virtualItemsAmount = (-1) * userMinIndex - bufferSize + 1; 146 | expect(Helper.getTopPadding(viewport)).toBe(itemHeight * virtualItemsAmount); 147 | expect(viewport.scrollTop()).toBe(itemHeight * ((-1) * userMinIndex + 1)); 148 | } 149 | ); 150 | }); 151 | 152 | it('should apply new user minIndex and maxIndex after reload', () => { 153 | const startIndex = 10; 154 | const add = 50; 155 | const minIndexNew = userMinIndex - add; 156 | const maxIndexNew = userMaxIndex + add; 157 | datasource.minIndex = userMinIndex; 158 | datasource.maxIndex = userMaxIndex; 159 | runTest(Object.assign({}, scrollSettings, { datasource: 'myResponsiveDatasource', startIndex }), 160 | (viewport, scope) => { 161 | const _scrollTop = viewport.scrollTop(); 162 | 163 | scope.adapter.reload(startIndex); 164 | datasource.min = minIndexNew; 165 | datasource.max = maxIndexNew; 166 | datasource.minIndex = minIndexNew; 167 | datasource.maxIndex = maxIndexNew; 168 | 169 | expect(Helper.getTopPadding(viewport)).toBe(itemHeight * ((-1) * minIndexNew + startIndex - bufferSize)); 170 | expect(Helper.getBottomPadding(viewport)).toBe(itemHeight * (maxIndexNew - startIndex + 1 - (viewportHeight / itemHeight) - bufferSize)); 171 | expect(viewport.scrollTop()).toBe(_scrollTop + itemHeight * add); 172 | } 173 | ); 174 | }); 175 | 176 | }); 177 | 178 | }); 179 | -------------------------------------------------------------------------------- /test/VisibilitySwitchingSpec.js: -------------------------------------------------------------------------------- 1 | describe('uiScroll visibility.', () => { 2 | 'use strict'; 3 | 4 | beforeEach(module('ui.scroll')); 5 | beforeEach(module('ui.scroll.test.datasources')); 6 | 7 | const scrollSettings = { 8 | datasource: 'myMultipageDatasource', 9 | viewportHeight: 200, 10 | itemHeight: 40, 11 | bufferSize: 3, 12 | adapter: 'adapter' 13 | }; 14 | 15 | const checkContent = (rows, count) => { 16 | expect(rows.length).toBe(count); 17 | for (var i = 1; i < count - 1; i++) { 18 | expect(rows[i].innerHTML).toBe(i + ': item' + i); 19 | } 20 | }; 21 | 22 | const onePackItemsCount = 3 * 1 + 2; 23 | const twoPacksItemsCount = 3 * 2 + 2; 24 | const threePacksItemsCount = 3 * 3 + 2; 25 | 26 | describe('Viewport visibility changing\n', () => { 27 | 28 | it('should create 9 divs with data (+ 2 padding divs)', () => 29 | runTest(scrollSettings, 30 | (viewport) => { 31 | expect(viewport.scrollTop()).toBe(0); 32 | checkContent(viewport.children(), threePacksItemsCount); 33 | } 34 | ) 35 | ); 36 | 37 | it('should preserve elements after visibility switched off (display:none)', () => 38 | runTest(scrollSettings, 39 | (viewport, scope) => { 40 | viewport.css('display', 'none'); 41 | scope.$apply(); 42 | 43 | expect(viewport.scrollTop()).toBe(0); 44 | checkContent(viewport.children(), threePacksItemsCount); 45 | } 46 | ) 47 | ); 48 | 49 | it('should only load one batch with visibility switched off (display:none)', () => 50 | runTest(scrollSettings, 51 | (viewport, scope) => { 52 | viewport.css('display', 'none'); 53 | scope.adapter.reload(); 54 | 55 | expect(viewport.scrollTop()).toBe(0); 56 | checkContent(viewport.children(), onePackItemsCount); 57 | } 58 | ) 59 | ); 60 | 61 | it('should load full set after css-visibility switched back on', () => 62 | runTest(scrollSettings, 63 | (viewport, scope, $timeout) => { 64 | viewport.css('display', 'none'); 65 | scope.adapter.reload(); 66 | 67 | viewport.css('display', 'block'); 68 | scope.$apply(); 69 | $timeout.flush(); 70 | 71 | expect(viewport.scrollTop()).toBe(0); 72 | checkContent(viewport.children(), threePacksItemsCount); 73 | expect(scope.adapter.topVisible).toBe('item1'); 74 | } 75 | ) 76 | ); 77 | 78 | it('should load full set after scope-visibility switched back on', () => 79 | runTest(Object.assign({}, scrollSettings, { 80 | wrapper: { 81 | start: '
    ', 82 | end: '
    ' 83 | } 84 | }), (viewport, scope) => { 85 | scope.show = false; 86 | scope.$apply(); 87 | expect(viewport.children().length).toBe(0); 88 | 89 | scope.show = true; 90 | scope.$apply(); 91 | expect(viewport.scrollTop()).toBe(0); 92 | checkContent(viewport.children().children(), threePacksItemsCount); 93 | }, { 94 | scope: { 95 | show: true 96 | } 97 | } 98 | ) 99 | ); 100 | }); 101 | 102 | describe('Items visibility changing\n', () => { 103 | 104 | it('should load only one batch with items height = 0', () => 105 | runTest(Object.assign({}, scrollSettings, { itemHeight: '0' }), 106 | (viewport) => { 107 | expect(viewport.children().length).toBe(onePackItemsCount); 108 | expect(viewport.scrollTop()).toBe(0); 109 | checkContent(viewport.children(), onePackItemsCount); 110 | } 111 | ) 112 | ); 113 | 114 | it('should load one more batch after the height of some item is set to a positive value', () => 115 | runTest(Object.assign({}, scrollSettings, { itemHeight: '0' }), 116 | (viewport, scope, $timeout) => { 117 | angular.element(viewport.children()[onePackItemsCount - 2]).css('height', 40); 118 | expect(angular.element(viewport.children()[onePackItemsCount - 2]).css('height')).toBe('40px'); 119 | scope.$apply(); 120 | $timeout.flush(); 121 | 122 | expect(viewport.scrollTop()).toBe(0); 123 | checkContent(viewport.children(), twoPacksItemsCount); 124 | } 125 | ) 126 | ); 127 | }); 128 | 129 | }); 130 | -------------------------------------------------------------------------------- /test/config/karma.conf.files.js: -------------------------------------------------------------------------------- 1 | const files = [ 2 | 'https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js', 3 | 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.5/angular.js', 4 | 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.5/angular-mocks.js', 5 | '../misc/test.css', 6 | '../misc/datasources.js', 7 | '../misc/scaffolding*.js', 8 | '../misc/helpers.js', 9 | { 10 | pattern: '../*Spec.js', 11 | watched: false, 12 | served: true, 13 | included: true 14 | } 15 | ]; 16 | 17 | module.exports.development = [ 18 | ...files, 19 | '../../src/ui-scroll.js', 20 | '../../src/ui-scroll-grid.js' 21 | ]; 22 | 23 | module.exports.production = [ 24 | ...files, 25 | '../../dist/ui-scroll.min.js', 26 | '../../dist/ui-scroll-grid.min.js', 27 | { 28 | pattern: '../../dist/*.js.map', 29 | included: false 30 | } 31 | ]; 32 | -------------------------------------------------------------------------------- /test/config/karma.conf.js: -------------------------------------------------------------------------------- 1 | // BROWSER env variable could be "headless", "firefox" or "chrome" (default) 2 | 3 | const browsers = []; 4 | if (process.env.BROWSER === 'headless') { 5 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 6 | browsers.push('ChromeHeadlessSized'); 7 | } else if (process.env.BROWSERS === "firefox") { 8 | browsers.push('FirefoxSized'); 9 | } else { 10 | browsers.push(process.platform === 'linux' ? ['ChromiumSized'] : ['ChromeSized']); 11 | } 12 | 13 | const flags = ['--window-size=1024,768']; 14 | const customLaunchers = { 15 | 'ChromeHeadlessSized': { base: 'ChromeHeadless', flags }, 16 | 'ChromiumSized': { base: 'Chromium', flags }, 17 | 'ChromeSized': { base: 'Chrome', flags }, 18 | 'FirefoxSized': { base: 'Firefox', flags } 19 | }; 20 | 21 | const ENV = (!process.env.CI && process.env.npm_lifecycle_event.indexOf('dev') === 0) ? 22 | 'development' : 23 | 'production'; 24 | 25 | const webpackSettings = ENV === 'development' ? { 26 | preprocessors: { 27 | '../../src/ui-scroll*.js': ['webpack', 'sourcemap'] 28 | }, 29 | webpack: require('../../webpack.config.js') 30 | } : {}; 31 | 32 | module.exports = function (config) { 33 | 'use strict'; 34 | 35 | config.set(Object.assign({ 36 | 37 | basePath: '', 38 | 39 | frameworks: ['jasmine'], 40 | 41 | files: [ 42 | ...require('./karma.conf.files.js')[ENV] 43 | ], 44 | 45 | exclude: [], 46 | 47 | reporters: ['dots'], 48 | 49 | port: ENV === 'development' ? 9100 : 8082, 50 | 51 | colors: true, 52 | 53 | logLevel: config.LOG_INFO, 54 | 55 | autoWatch: ENV === 'development', 56 | 57 | keepalive: ENV === 'development', 58 | 59 | browsers, 60 | customLaunchers, 61 | 62 | captureTimeout: 60000, 63 | 64 | singleRun: ENV !== 'development' 65 | 66 | }, webpackSettings)); 67 | }; 68 | -------------------------------------------------------------------------------- /test/jqliteExtrasSpec.js: -------------------------------------------------------------------------------- 1 | describe('\njqLite: testing against jQuery\n', function () { 2 | 'use strict'; 3 | 4 | var sandbox = angular.element('
    '); 5 | var extras; 6 | 7 | beforeEach(module('ui.scroll')); 8 | beforeEach(function(){ 9 | angular.element(document).find('body').append(sandbox = angular.element('
    ')); 10 | inject(function(JQLiteExtras) { 11 | extras = function(){}; 12 | (new JQLiteExtras()).registerFor(extras); 13 | }); 14 | }); 15 | 16 | afterEach(function() {sandbox.remove();}); 17 | 18 | describe('height() getter for window\n', function() { 19 | it('should work for window element', function() { 20 | var element = angular.element(window); 21 | expect(extras.prototype.height.call(element)).toBe(element.height()); 22 | }); 23 | }); 24 | 25 | describe('getters height() and outerHeight()\n', function () { 26 | 27 | function createElement(element) { 28 | var result = angular.element(element); 29 | sandbox.append(result); 30 | return result; 31 | } 32 | 33 | angular.forEach( 34 | [ 35 | '
    some text
    ', 36 | '
    some text (height in em)
    ', 37 | '
    some text height in px
    ', 38 | '
    some text w border
    ', 39 | '
    some text w border
    ', 40 | '
    some text w padding
    ', 41 | '
    some text w padding
    ', 42 | '
    some text w margin
    ', 43 | '
    some text w margin
    ' 44 | ], function(element) { 45 | 46 | it('should be the same as jQuery height() for ' + element, function() { 47 | (function(element) { 48 | expect(extras.prototype.height.call(element)).toBe(element.height()); 49 | })(createElement(element)); 50 | } 51 | ); 52 | 53 | it ('should be the same as jQuery outerHeight() for ' + element, function() { 54 | (function(element) { 55 | expect(extras.prototype.outerHeight.call(element)).toBe(element.outerHeight()); 56 | })(createElement(element)); 57 | } 58 | ); 59 | 60 | it ('should be the same as jQuery outerHeight(true) for ' + element, function() { 61 | (function(element) { 62 | expect(extras.prototype.outerHeight.call(element, true)).toBe(element.outerHeight(true)); 63 | })(createElement(element)); 64 | } 65 | ); 66 | 67 | } 68 | 69 | ); 70 | }); 71 | 72 | describe('height(value) setter\n', function () { 73 | 74 | function createElement(element) { 75 | var result = angular.element(element); 76 | sandbox.append(result); 77 | return result; 78 | } 79 | 80 | angular.forEach( 81 | [ 82 | '
    some text
    ', 83 | '
    some text (height in em)
    ', 84 | '
    some text height in px
    ', 85 | '
    some text w border
    ', 86 | '
    some text w border
    ', 87 | '
    some text w padding
    ', 88 | '
    some text w padding
    ', 89 | '
    some text w margin
    ', 90 | '
    some text w margin
    ', 91 | '
    some text w margin
    ' 92 | ], function(element) { 93 | 94 | // Since jQuery v3 the .hegth() results don't being rounded (https://github.com/jquery/jquery/pull/2454). 95 | // So the element '
    some text w line height
    ' will cause the error -- 96 | // Expected 18 to be 17.6 97 | 98 | it('height(value) for ' + element, function() { 99 | (function (element) { 100 | expect(extras.prototype.height.call(element)).toBe(element.height()); 101 | var h = element.height(); 102 | extras.prototype.height.call(element, h*2); 103 | expect(extras.prototype.height.call(element)).toBe(h*2); 104 | })(createElement(element)); 105 | } 106 | ); 107 | 108 | } 109 | 110 | ); 111 | }); 112 | 113 | describe('offset() getter\n', function () { 114 | 115 | function createElement(element) { 116 | var result = angular.element(element); 117 | sandbox.append(result); 118 | return result; 119 | } 120 | 121 | angular.forEach( 122 | [ 123 | '
    some text
    ', 124 | '
    some text (height in em)
    ', 125 | // '
    some text height in px
    ', 126 | // '
    some text w border
    ', 127 | // '
    some text w border
    ', 128 | // '
    some text w padding
    ', 129 | // '
    some text w padding
    ', 130 | // '
    some text w margin
    ', 131 | '

    some text w margin

    ' 132 | ], function(element) { 133 | 134 | it('should be the same as jQuery offset() for ' + element, function() { 135 | (function (element) { 136 | var target = jQuery(element.contents()[0]); 137 | expect(extras.prototype.offset.call(target)).toEqual(element.offset()); 138 | })(createElement(element)); 139 | } 140 | ); 141 | 142 | } 143 | 144 | ); 145 | }); 146 | 147 | describe('scrollTop()\n', function() { 148 | 149 | function createElement(element) { 150 | var result = angular.element(element); 151 | sandbox.append(result); 152 | return result; 153 | } 154 | 155 | it('should be the same as jQuery scrollTop() for window', function() { 156 | 157 | createElement('
    '); 158 | var element = jQuery(window); 159 | expect(extras.prototype.scrollTop.call(element)).toBe(element.scrollTop()); 160 | element.scrollTop(100); 161 | expect(extras.prototype.scrollTop.call(element)).toBe(element.scrollTop()); 162 | extras.prototype.scrollTop.call(element, 200); 163 | expect(extras.prototype.scrollTop.call(element)).toBe(element.scrollTop()); 164 | } 165 | ); 166 | 167 | it('should be the same as jQuery scrollTop() for window', function() { 168 | 169 | var element = createElement('
    '); 170 | expect(extras.prototype.scrollTop.call(element)).toBe(element.scrollTop()); 171 | element.scrollTop(100); 172 | expect(extras.prototype.scrollTop.call(element)).toBe(element.scrollTop()); 173 | extras.prototype.scrollTop.call(element, 200); 174 | expect(extras.prototype.scrollTop.call(element)).toBe(element.scrollTop()); 175 | } 176 | ); 177 | 178 | }); 179 | 180 | }); -------------------------------------------------------------------------------- /test/misc/datasources.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('ui.scroll.test.datasources', []) 5 | 6 | .factory('myEmptyDatasource', [ 7 | '$log', '$timeout', '$rootScope', 8 | function() { 9 | return { 10 | get: function(index, count, success) { 11 | success([]); 12 | } 13 | }; 14 | } 15 | ]) 16 | 17 | .factory('myDescriptoEmptyDatasource', [ 18 | '$log', '$timeout', '$rootScope', 19 | function() { 20 | return { 21 | get: function(descriptor, success) { 22 | success([]); 23 | } 24 | }; 25 | } 26 | ]) 27 | 28 | .factory('myOnePageDatasource', [ 29 | '$log', '$timeout', '$rootScope', 30 | function() { 31 | return { 32 | get: function(index, count, success) { 33 | if (index === 1) { 34 | success(['one', 'two', 'three']); 35 | } else { 36 | success([]); 37 | } 38 | } 39 | }; 40 | } 41 | ]) 42 | 43 | .factory('myOneBigPageDatasource', [ 44 | '$log', '$timeout', '$rootScope', 45 | function() { 46 | return { 47 | get: function(index, count, success) { 48 | if (index === 1) { 49 | var resultList = []; 50 | for (var i = 1; i < 100; i++) { 51 | resultList.push('item' + i); 52 | } 53 | success(resultList); 54 | } else { 55 | success([]); 56 | } 57 | } 58 | }; 59 | } 60 | ]) 61 | 62 | .factory('myDescriptorOnePageDatasource', [ 63 | '$log', '$timeout', '$rootScope', 64 | function() { 65 | return { 66 | get: function(descriptor, success) { 67 | if (descriptor.index === 1) { 68 | success(['one', 'two', 'three']); 69 | } else { 70 | success([]); 71 | } 72 | } 73 | }; 74 | } 75 | ]) 76 | 77 | .factory('myObjectDatasource', [ 78 | '$log', '$timeout', '$rootScope', 79 | function() { 80 | return { 81 | get: function(index, count, success) { 82 | if (index === 1) { 83 | success([{ text: 'one' }, { text: 'two' }, { text: 'three' }]); 84 | } else { 85 | success([]); 86 | } 87 | } 88 | }; 89 | } 90 | ]) 91 | 92 | .factory('myMultipageDatasource', [ 93 | '$log', '$timeout', '$rootScope', 94 | function() { 95 | return { 96 | get: function(index, count, success) { 97 | var result = []; 98 | for (var i = index; i < index + count; i++) { 99 | if (i > 0 && i <= 20) { 100 | result.push('item' + i); 101 | } 102 | } 103 | success(result); 104 | } 105 | }; 106 | } 107 | ]) 108 | 109 | .factory('anotherDatasource', [ 110 | '$log', '$timeout', '$rootScope', 111 | function() { 112 | return { 113 | get: function(index, count, success) { 114 | var result = []; 115 | for (var i = index; i < index + count; i++) { 116 | if (i > -3 && i < 1) { 117 | result.push('item' + i); 118 | } 119 | } 120 | success(result); 121 | } 122 | }; 123 | } 124 | ]) 125 | 126 | .factory('myEdgeDatasource', [ 127 | '$log', '$timeout', '$rootScope', 128 | function() { 129 | return { 130 | get: function(index, count, success) { 131 | var result = []; 132 | for (var i = index; i < index + count; i++) { 133 | if (i > -6 && i <= 6) { 134 | result.push('item' + i); 135 | } 136 | } 137 | success(result); 138 | } 139 | }; 140 | } 141 | ]) 142 | 143 | .factory('myDatasourceToPreventScrollBubbling', [ 144 | '$log', '$timeout', '$rootScope', 145 | function() { 146 | return { 147 | get: function(index, count, success) { 148 | var result = []; 149 | for (var i = index; i < index + count; i++) { 150 | if (i < -6 || i > 20) { 151 | break; 152 | } 153 | result.push('item' + i); 154 | } 155 | success(result); 156 | } 157 | }; 158 | } 159 | ]) 160 | 161 | .factory('myInfiniteDatasource', [ 162 | '$log', '$timeout', '$rootScope', 163 | function() { 164 | return { 165 | get: function(index, count, success) { 166 | var result = []; 167 | for (var i = index; i < index + count; i++) { 168 | result.push('item' + i); 169 | } 170 | success(result); 171 | } 172 | }; 173 | } 174 | ]) 175 | 176 | .factory('myGridDatasource', [ 177 | '$log', '$timeout', '$rootScope', 178 | function() { 179 | return { 180 | get: function(index, count, success) { 181 | var result = []; 182 | for (var i = index; i < index + count; i++) { 183 | result.push({ 184 | col0: 'col0', 185 | col1: 'col1', 186 | col2: 'col2', 187 | col3: 'col3' 188 | }); 189 | } 190 | success(result); 191 | } 192 | }; 193 | } 194 | ]) 195 | 196 | 197 | .factory('myResponsiveDatasource', function() { 198 | var datasource = { 199 | data: [], 200 | min: 1, 201 | max: 30, 202 | init: function() { 203 | this.data = []; 204 | for (var i = this.min; i <= this.max; i++) { 205 | this.data.push('item' + i); 206 | } 207 | }, 208 | getItem: function(index) { 209 | return this.data[index - this.min]; 210 | }, 211 | get: function(index, count, success) { 212 | var result = []; 213 | var start = Math.max(this.min, index); 214 | var end = Math.min(index + count - 1, this.max); 215 | if (start <= end) { 216 | for (var i = start; i <= end; i++) { 217 | result.push(this.getItem(i)); 218 | } 219 | } 220 | success(result); 221 | } 222 | }; 223 | datasource.init(); 224 | return datasource; 225 | } 226 | ); 227 | 228 | })(); -------------------------------------------------------------------------------- /test/misc/helpers.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | let Helper; 4 | 5 | (function() { 6 | 'use strict'; 7 | 8 | Helper = { 9 | 10 | getTopPadding: (viewport) => { 11 | const viewportChildren = viewport.children(); 12 | const topPadding = viewportChildren[0]; 13 | return parseInt(topPadding.style.height, 10); 14 | }, 15 | 16 | getBottomPadding: (viewport) => { 17 | const viewportChildren = viewport.children(); 18 | const bottomPadding = viewportChildren[viewportChildren.length - 1]; 19 | return parseInt(bottomPadding.style.height, 10); 20 | }, 21 | 22 | getRow: (viewport, number) => { // number is index + 1 23 | const viewportChildren = viewport.children(); 24 | if (viewportChildren.length < 2 + number) { 25 | return; 26 | } 27 | return viewportChildren[number].innerHTML; 28 | }, 29 | 30 | getFirstRow: (viewport) => Helper.getRow(viewport, 1), 31 | 32 | getLastRow: (viewport) => { 33 | const viewportChildren = viewport.children(); 34 | if (viewportChildren.length < 3) { 35 | return; 36 | } 37 | return viewportChildren[viewportChildren.length - 2].innerHTML; 38 | } 39 | 40 | }; 41 | 42 | })(); 43 | -------------------------------------------------------------------------------- /test/misc/scaffolding.js: -------------------------------------------------------------------------------- 1 | /* exported runTest */ 2 | 3 | function createHtml(settings) { 4 | 'use strict'; 5 | var viewportStyle = ' style="height:' + (settings.viewportHeight || 200) + 'px"'; 6 | var itemStyle = settings.itemHeight ? ' style="height:' + settings.itemHeight + 'px"' : ''; 7 | var bufferSize = settings.bufferSize ? ' buffer-size="' + settings.bufferSize + '"' : ''; 8 | var padding = settings.padding ? ' padding="' + settings.padding + '"' : ''; 9 | var isLoading = settings.isLoading ? ' is-loading="' + settings.isLoading + '"' : ''; 10 | var topVisible = settings.topVisible ? ' top-visible="' + settings.topVisible + '"' : ''; 11 | var disabled = settings.disabled ? ' disabled="' + settings.disabled + '"' : ''; 12 | var adapter = settings.adapter ? ' adapter="' + settings.adapter + '"' : ''; 13 | var template = settings.template ? settings.template : '{{$index}}: {{item}}'; 14 | var startIndex = settings.startIndex ? ' start-index="' + settings.startIndex + '"' : ''; 15 | var inertia = ' handle-inertia="false"'; 16 | var extra = settings.extra || ''; 17 | return '
    ' + 18 | (settings.wrapper ? settings.wrapper.start : '') + 19 | '
    ' + 22 | template + 23 | '
    ' + 24 | (settings.wrapper ? settings.wrapper.end : '') + 25 | '
    '; 26 | } 27 | 28 | function finalize(scroller, options, scope, $timeout) { 29 | 'use strict'; 30 | options = options || {}; 31 | scroller.remove(); 32 | 33 | if (typeof options.cleanupTest === 'function') { 34 | options.cleanupTest(scroller, scope, $timeout); 35 | } 36 | } 37 | 38 | function augmentScroller(scroller) { 39 | 'use strict'; 40 | var scrollTop = scroller.scrollTop; 41 | scroller.scrollTop = function () { 42 | var result = scrollTop.apply(scroller, arguments); 43 | if (arguments.length) { 44 | scroller.trigger('scroll'); 45 | } 46 | return result; 47 | }; 48 | } 49 | 50 | function runTest(scrollSettings, run, options) { 51 | 'use strict'; 52 | options = options || {}; 53 | inject(function ($rootScope, $compile, $window, $timeout) { 54 | var scroller = angular.element(createHtml(scrollSettings)); 55 | var scope = $rootScope.$new(); 56 | augmentScroller(scroller); 57 | 58 | angular.element(document).find('body').append(scroller); 59 | 60 | if (options.scope) { 61 | angular.extend(scope, options.scope); 62 | } 63 | 64 | var compile = function() { 65 | $compile(scroller)(scope); 66 | scope.$apply(); 67 | }; 68 | 69 | if (typeof options.catch === 'function') { 70 | try { 71 | compile(); 72 | } catch (error) { 73 | options.catch(error); 74 | } 75 | } else { 76 | compile(); 77 | } 78 | 79 | if(typeof run === 'function') { 80 | try { 81 | run(scroller, scope, $timeout); 82 | } finally { 83 | finalize(scroller, options, scope, $timeout); 84 | } 85 | } 86 | }); 87 | } -------------------------------------------------------------------------------- /test/misc/scaffoldingGrid.js: -------------------------------------------------------------------------------- 1 | /* exported runGridTest */ 2 | 3 | function createGridHtml (settings) { 4 | 'use strict'; 5 | var viewportStyle = ' style="height:' + (settings.viewportHeight || 200) + 'px"'; 6 | var columns = ['col0', 'col1', 'col2', 'col3']; 7 | 8 | var html = 9 | '' + 10 | '' + 11 | ''; 12 | columns.forEach(col => { html += 13 | ''; 14 | }); html += 15 | '' + 16 | '' + 17 | '' + 18 | ''; 19 | if(settings.rowTemplate) { 20 | html += settings.rowTemplate; 21 | } else { 22 | columns.forEach(col => { html += 23 | ''; 24 | }); 25 | } html += 26 | '' + 27 | '' + 28 | '
    ' + col + '
    {{item.' + col + '}}
    '; 29 | return html; 30 | } 31 | 32 | function finalize(scroller, options, scope, $timeout) { 33 | 'use strict'; 34 | options = options || {}; 35 | scroller.remove(); 36 | 37 | if (typeof options.cleanupTest === 'function') { 38 | options.cleanupTest(scroller, scope, $timeout); 39 | } 40 | } 41 | 42 | function runGridTest(scrollSettings, run, options) { 43 | 'use strict'; 44 | options = options || {}; 45 | inject(function($rootScope, $compile, $window, $timeout) { 46 | var scroller = angular.element(createGridHtml(scrollSettings)); 47 | var scope = $rootScope.$new(); 48 | 49 | angular.element(document).find('body').append(scroller); 50 | var head = angular.element(scroller.children()[0]); 51 | var body = angular.element(scroller.children()[1]); 52 | 53 | if (options.scope) { 54 | angular.extend(scope, options.scope); 55 | } 56 | 57 | $compile(scroller)(scope); 58 | 59 | scope.$apply(); 60 | $timeout.flush(); 61 | 62 | try { 63 | run(head, body, scope, $timeout); 64 | } finally { 65 | finalize(scroller, options, scope, $timeout); 66 | } 67 | }); 68 | } -------------------------------------------------------------------------------- /test/misc/test.css: -------------------------------------------------------------------------------- 1 | .grid { width: 250px; } 2 | .col0 { width: 40px; } 3 | .col1 { width: 80px; } 4 | .col2 { width: 40px; } 5 | .col3 { width: 50px; } 6 | .item:nth-child(odd) { background-color: #e6e6e6; } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const glob = require('glob'); 5 | const webpack = require('webpack'); 6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 7 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 8 | const TerserPlugin = require('terser-webpack-plugin'); 9 | 10 | const packageJSON = require('./package.json'); 11 | 12 | const getBanner = () => 13 | packageJSON.name + '\n' + 14 | packageJSON.homepage + '\n' + 15 | 'Version: ' + packageJSON.version + ' -- ' + (new Date()).toISOString() + '\n' + 16 | 'License: ' + packageJSON.license; 17 | 18 | const scriptName = process.env.npm_lifecycle_event; 19 | const ENV = scriptName.indexOf('dev') === 0 ? 'development' : 'production'; 20 | const isTest = scriptName.indexOf('test') >= 0; 21 | 22 | console.log('***** webpack runs in ' + ENV + (isTest ? ' (test)' : '') + ' environment\n'); 23 | 24 | const devServerPort = 5005; 25 | const devServerHost = 'localhost'; 26 | let configEnv; 27 | 28 | if (ENV === 'development') { 29 | configEnv = { 30 | entry: isTest ? ({ 31 | 'test': glob.sync(path.resolve(__dirname, 'test/*.js')) 32 | }) : ({}), 33 | 34 | output: { 35 | filename: '[name].js', 36 | publicPath: '/' 37 | }, 38 | 39 | devtool: 'inline-source-map', 40 | 41 | plugins: [], 42 | 43 | optimization: {}, 44 | 45 | devServer: !isTest ? { 46 | proxy: { 47 | '/dist': { 48 | target: 'http://' + devServerHost + ':' + devServerPort, 49 | pathRewrite: { '^/dist': '' } 50 | } 51 | }, 52 | 53 | port: devServerPort, 54 | host: devServerHost, 55 | static: "demo", 56 | devMiddleware: { 57 | stats: { 58 | modules: false, 59 | errors: true, 60 | warnings: true 61 | }, 62 | publicPath: '/' 63 | }, 64 | } : {}, 65 | 66 | watch: true 67 | } 68 | } 69 | 70 | if (ENV === 'production') { 71 | configEnv = { 72 | entry: { 73 | 'ui-scroll.min': path.resolve(__dirname, 'src/ui-scroll.js'), 74 | 'ui-scroll-grid.min': path.resolve(__dirname, 'src/ui-scroll-grid.js') 75 | }, 76 | 77 | output: { 78 | path: path.resolve(__dirname, 'dist'), 79 | filename: '[name].js' 80 | }, 81 | 82 | devtool: 'source-map', 83 | 84 | optimization: { 85 | minimize: true, 86 | minimizer: [ 87 | new TerserPlugin({ 88 | parallel: true, 89 | extractComments: false, 90 | terserOptions: { 91 | sourceMap: true, 92 | warnings: true, 93 | compress: { 94 | warnings: true, 95 | }, 96 | output: { 97 | comments: false, 98 | }, 99 | }, 100 | include: /\.min\.js$/ 101 | }) 102 | ], 103 | }, 104 | 105 | plugins: [ 106 | new CleanWebpackPlugin({ 107 | cleanOnceBeforeBuildPatterns: [path.join(__dirname, 'dist/**/*')] 108 | }), 109 | new CopyWebpackPlugin({ 110 | patterns: [ 111 | { from: 'src/ui-scroll-jqlite.js', to: 'ui-scroll-jqlite.min.js' }, 112 | { from: 'src/ui-scroll-jqlite.js', to: 'ui-scroll-jqlite.js' } 113 | ] 114 | }), 115 | new webpack.BannerPlugin(getBanner()) 116 | ], 117 | 118 | devServer: {}, 119 | 120 | watch: false 121 | } 122 | } 123 | 124 | module.exports = { 125 | entry: Object.assign({ 126 | 'ui-scroll': path.resolve(__dirname, 'src/ui-scroll.js'), 127 | 'ui-scroll-grid': path.resolve(__dirname, 'src/ui-scroll-grid.js') 128 | }, configEnv.entry), 129 | 130 | output: configEnv.output, 131 | 132 | cache: false, 133 | 134 | devtool: configEnv.devtool, 135 | 136 | mode: ENV, 137 | 138 | target: ['web', 'es5'], 139 | 140 | optimization: configEnv.optimization, 141 | 142 | module: { 143 | rules: [ 144 | { 145 | test: /\.js$/, 146 | exclude: /node_modules/, 147 | loader: 'babel-loader' 148 | } 149 | ] 150 | }, 151 | 152 | plugins: configEnv.plugins, 153 | 154 | devServer: configEnv.devServer, 155 | 156 | watch: configEnv.watch 157 | }; 158 | --------------------------------------------------------------------------------