From 5bba6669a14d85bc9fe1773df81cd802fb875916 Mon Sep 17 00:00:00 2001 From: Traxmaxx Date: Wed, 26 Mar 2014 17:02:10 +0100 Subject: [PATCH 01/10] Enable scrollbars on Android --- dist/ui-table-view.css | 18 ++++++++++++++++++ less/ui-table-view.less | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/dist/ui-table-view.css b/dist/ui-table-view.css index 7b11227..37f7d11 100644 --- a/dist/ui-table-view.css +++ b/dist/ui-table-view.css @@ -7,3 +7,21 @@ mlz-ui-table-view { mlz-ui-table-view .mlz-ui-table-view-wrapper { position: relative; } +/* Enable scrollbars on Android (optional) */ +::-webkit-scrollbar { + border-left: 1px solid #e5e5e5; + height: 0; + overflow: visible; + width: 5px; +} +::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + background-clip: padding-box; + min-height: 28px; + border-width: 1px 1px 1px 6px; + width: 5px; +} +::-webkit-scrollbar-corner { + background: transparent; + overflow: visible; +} diff --git a/less/ui-table-view.less b/less/ui-table-view.less index 323b2d1..e0d34a3 100644 --- a/less/ui-table-view.less +++ b/less/ui-table-view.less @@ -10,3 +10,21 @@ mlz-ui-table-view { } } +/* Enable scrollbars on Android (optional) */ +::-webkit-scrollbar { + border-left: 1px solid rgb(229, 229, 229); + height: 0; + overflow: visible; + width: 5px; +} +::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, .2); + background-clip: padding-box; + min-height: 28px; + border-width: 1px 1px 1px 6px; + width: 5px; +} +::-webkit-scrollbar-corner { + background: transparent; + overflow: visible; +} From 9a64605a71bf409453e5d18640869caf1f1cfbf0 Mon Sep 17 00:00:00 2001 From: Jamie Sutherland Date: Thu, 27 Mar 2014 16:25:03 +0000 Subject: [PATCH 02/10] [Fixes #21] Check for elements to update before attempting to update them --- dist/ui-table-view.js | 4 ++-- dist/ui-table-view.min.js | 2 +- src/ui-table-view.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dist/ui-table-view.js b/dist/ui-table-view.js index 48a9243..625b193 100644 --- a/dist/ui-table-view.js +++ b/dist/ui-table-view.js @@ -407,8 +407,8 @@ if (buffer.elements[p]) { // Scan the buffer for this item. If it exists we should move that item into this // position and send this block to the bottom to be reused. - for(var k = i; k < buffer.size; k++) { - if (found) { + for(var k = p; k < buffer.size; k++) { + if (buffer.elements[k] && found) { // Update positions of everything else in the buffer buffer.elements[k].scope.$coords = { x:x, y:y } } diff --git a/dist/ui-table-view.min.js b/dist/ui-table-view.min.js index 4c2b63f..378f6ea 100644 --- a/dist/ui-table-view.min.js +++ b/dist/ui-table-view.min.js @@ -1 +1 @@ -!function(window,angular,undefined){"use strict";function getBlockElements(nodes){var startNode=nodes[0],endNode=nodes[nodes.length-1];if(startNode===endNode)return angular.element(startNode);var element=startNode,elements=[element];do{if(element=element.nextSibling,!element)break;elements.push(element)}while(element!==endNode);return angular.element(elements)}function getItemElement(nodes){var startNode=nodes[0],endNode=nodes[nodes.length-1];if(startNode===endNode)return angular.element(startNode);var element=startNode;do{if(element.classList&&element.classList.contains("mlz-ui-table-view-item"))return angular.element(element);element=element.nextSibling}while(element!==endNode);return!1}var move=function(arr,old_index,new_index){if(new_index>=arr.length)for(var k=new_index-arr.length;k--+1;)arr.push(undefined);return arr.splice(new_index,0,arr.splice(old_index,1)[0]),arr};window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(callback){window.setTimeout(callback,1e3/60)}}(),angular.module("mallzee.ui-table-view",["ngAnimate"]).directive("mlzUiTableView",["$window","$timeout","$log","$animate",function($window,$timeout,$log,$animate){return{restrict:"E",transclude:!0,terminal:!0,priority:1e4,$$tlb:!0,replace:!1,template:'
',link:function(scope,element,attributes,ctrl,$transclude){function update(){setScrollPosition(y),updating=!1}function initialise(scope,attributes){element.css({display:"block",overflow:"auto"}),wrapper.el.css({position:"relative"}),_scroll=angular.copy(scroll),_view=angular.copy(view),_buffer=angular.copy(buffer),attributes.itemName&&(itemName=attributes.itemName),attributes.viewParams&&scope.$watch(attributes.viewParams,function(view){row.height=view.rowHeight||ROW_HEIGHT,columns=view.columns||COLUMNS,buffer.rows=view.rows||BUFFER_ROWS,buffer.size=buffer.rows*columns,refresh()},!0),attributes.triggerTop&&(triggerTop=function(){scope.$eval(attributes.triggerTop)}),attributes.triggerBottom&&(triggerBottom=function(){scope.$eval(attributes.triggerBottom)}),$window.addEventListener("statusTap",function(){scrollToTop()}),calculateContainer()}function refresh(){updateBufferModel(),generateBufferedItems(),calculateDimensions(),updateViewModel(),triggerEdge()}function cloneElement(clone){clone.addClass("mlz-ui-table-view-item"),clone[clone.length++]=document.createComment(" end mlzTableViewItem: "+attributes.list+" "),$animate.enter(clone,wrapper.el)}function updateItem(elIndex,item,coords,index){buffer.elements[elIndex].scope[itemName]=item,buffer.elements[elIndex].scope.$coords=coords,buffer.elements[elIndex].scope.$index=index}function destroyItem(index){var elementsToRemove=getBlockElements(buffer.elements[index].clone);$animate.leave(elementsToRemove),buffer.elements[index].scope.$destroy(),buffer.elements.splice(index,1)}function generateBufferedItems(){if(angular.copy(list.slice(itemIndexFromRow(buffer.top),itemIndexFromRow(buffer.bottom)),items),buffer.elements.length>buffer.size)for(var elementsLength=buffer.elements.length,i=elementsLength-1;i>=buffer.size;i--)destroyItem(i);for(var p,x,y,e,found,r=buffer.top-1,i=0;i=items.length)buffer.elements[i]&&destroyItem(i);else{if(found=!1,e=itemIndexFromRow(buffer.top)+i,p=getRelativeBufferPosition(e),p%columns===0?r++:null,x=p%columns*(container.width/columns),y=r*row.height,buffer.elements[p]&&angular.equals(list[e],buffer.elements[p].scope[itemName])){buffer.elements[p].scope.$coords={x:x,y:y},repositionElement(buffer.elements[p]);continue}if(buffer.elements[p]){for(var k=i;k=list.length/columns&&(buffer.bottom=list.length/columns,buffer.top=buffer.bottom-buffer.size>0?buffer.bottom-buffer.size:0),buffer.yTop=buffer.top*row.height,buffer.yBottom=buffer.bottom*row.height,buffer.atEdge=buffer.top<=0?EDGE_TOP:buffer.bottom>=wrapper.rows?EDGE_BOTTOM:!1}function isRenderRequired(){return scroll.direction===SCROLL_UP&&buffer.atEdge!==EDGE_TOP&&view.deadZone===!1&&(scroll.directionChange||view.ytChange)||scroll.direction===SCROLL_DOWN&&buffer.atEdge!==EDGE_BOTTOM&&view.deadZone===!1&&(scroll.directionChange||view.ytChange)||!(view.deadZone!==!1&&view.deadZoneChange===!1)}function isTriggerRequired(){return view.triggerZone!==!1&&view.triggerZoneChange}function updateScrollModel(y){scroll.y=y,scroll.yDelta=y-_scroll.y,scroll.row=Math.floor(y/row.height),scroll.row<0&&(scroll.row=0),scroll.row>=wrapper.rows&&(scroll.row=wrapper.rows-1),scroll.yDistance=Math.abs(scroll.row-_scroll.row),scroll.yChange=scroll.yDistance>0,scroll.direction=scroll.yDelta>=0?SCROLL_DOWN:SCROLL_UP,scroll.directionChange=scroll.direction!==_scroll.direction,buffer.reset=scroll.yDistance>buffer.rows}function updateViewModel(){view.yTop=scroll.y,view.yBottom=scroll.y+container.height,view.top=scroll.row,view.bottom=Math.floor(view.yBottom/row.height),view.atEdge=!(view.top>0&&view.bottom(list.length/columns-trigger.distance-1)*row.height?EDGE_BOTTOM:view.yTop(list.length-1)*row.height?EDGE_BOTTOM:!1,view.deadZoneChange=view.deadZone!==_view.deadZone,view.ytChange=view.top!==_view.top,view.ybChange=view.bottom!==_view.bottom}function setBufferToIndex(index){buffer.top=index,buffer.bottom=buffer.top+buffer.rows,validateBuffer()}function updateBufferModel(){var index=scroll.row,direction=scroll.direction,distance=buffer.distance;switch(direction){case SCROLL_UP:buffer.top=index-distance,buffer.bottom=index-distance+buffer.rows;break;case SCROLL_DOWN:buffer.top=index,buffer.bottom=index+buffer.rows;break;default:$log.warn("We only know how to deal with scrolling on the y axis for now")}validateBuffer()}function scrollingUp(start,distance){for(var p,x,y,itemsToMerge=list.slice(start*columns,start*columns+distance),r=start+distance/columns-1,updates=[],i=itemsToMerge.length-1;i>=0;i--){p=getRelativeBufferPosition(start*columns+i),x=p%columns*(container.width/columns),y=r*row.height;var coords={x:x,y:y},index=start*columns+i;updates.push(p),updateItem(p,itemsToMerge[i],coords,index),p%columns===0?r--:null}requestAnimFrame(function(){scope.$apply(function(){for(var i=0;i0?buffer.rows-view.rows:0}function clearElements(){for(var i=0;i=arr.length)for(var k=new_index-arr.length;k--+1;)arr.push(undefined);return arr.splice(new_index,0,arr.splice(old_index,1)[0]),arr};window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(callback){window.setTimeout(callback,1e3/60)}}(),angular.module("mallzee.ui-table-view",["ngAnimate"]).directive("mlzUiTableView",["$window","$timeout","$log","$animate",function($window,$timeout,$log,$animate){return{restrict:"E",transclude:!0,terminal:!0,priority:1e4,$$tlb:!0,replace:!1,template:'
',link:function(scope,element,attributes,ctrl,$transclude){function update(){setScrollPosition(y),updating=!1}function initialise(scope,attributes){element.css({display:"block",overflow:"auto"}),wrapper.el.css({position:"relative"}),_scroll=angular.copy(scroll),_view=angular.copy(view),_buffer=angular.copy(buffer),attributes.itemName&&(itemName=attributes.itemName),attributes.viewParams&&scope.$watch(attributes.viewParams,function(view){row.height=view.rowHeight||ROW_HEIGHT,columns=view.columns||COLUMNS,buffer.rows=view.rows||BUFFER_ROWS,buffer.size=buffer.rows*columns,refresh()},!0),attributes.triggerTop&&(triggerTop=function(){scope.$eval(attributes.triggerTop)}),attributes.triggerBottom&&(triggerBottom=function(){scope.$eval(attributes.triggerBottom)}),$window.addEventListener("statusTap",function(){scrollToTop()}),calculateContainer()}function refresh(){updateBufferModel(),generateBufferedItems(),calculateDimensions(),updateViewModel(),triggerEdge()}function cloneElement(clone){clone.addClass("mlz-ui-table-view-item"),clone[clone.length++]=document.createComment(" end mlzTableViewItem: "+attributes.list+" "),$animate.enter(clone,wrapper.el)}function updateItem(elIndex,item,coords,index){buffer.elements[elIndex].scope[itemName]=item,buffer.elements[elIndex].scope.$coords=coords,buffer.elements[elIndex].scope.$index=index}function destroyItem(index){var elementsToRemove=getBlockElements(buffer.elements[index].clone);$animate.leave(elementsToRemove),buffer.elements[index].scope.$destroy(),buffer.elements.splice(index,1)}function generateBufferedItems(){if(angular.copy(list.slice(itemIndexFromRow(buffer.top),itemIndexFromRow(buffer.bottom)),items),buffer.elements.length>buffer.size)for(var elementsLength=buffer.elements.length,i=elementsLength-1;i>=buffer.size;i--)destroyItem(i);for(var p,x,y,e,found,r=buffer.top-1,i=0;i=items.length)buffer.elements[i]&&destroyItem(i);else{if(found=!1,e=itemIndexFromRow(buffer.top)+i,p=getRelativeBufferPosition(e),p%columns===0?r++:null,x=p%columns*(container.width/columns),y=r*row.height,buffer.elements[p]&&angular.equals(list[e],buffer.elements[p].scope[itemName])){buffer.elements[p].scope.$coords={x:x,y:y},repositionElement(buffer.elements[p]);continue}if(buffer.elements[p]){for(var k=p;k=list.length/columns&&(buffer.bottom=list.length/columns,buffer.top=buffer.bottom-buffer.size>0?buffer.bottom-buffer.size:0),buffer.yTop=buffer.top*row.height,buffer.yBottom=buffer.bottom*row.height,buffer.atEdge=buffer.top<=0?EDGE_TOP:buffer.bottom>=wrapper.rows?EDGE_BOTTOM:!1}function isRenderRequired(){return scroll.direction===SCROLL_UP&&buffer.atEdge!==EDGE_TOP&&view.deadZone===!1&&(scroll.directionChange||view.ytChange)||scroll.direction===SCROLL_DOWN&&buffer.atEdge!==EDGE_BOTTOM&&view.deadZone===!1&&(scroll.directionChange||view.ytChange)||!(view.deadZone!==!1&&view.deadZoneChange===!1)}function isTriggerRequired(){return view.triggerZone!==!1&&view.triggerZoneChange}function updateScrollModel(y){scroll.y=y,scroll.yDelta=y-_scroll.y,scroll.row=Math.floor(y/row.height),scroll.row<0&&(scroll.row=0),scroll.row>=wrapper.rows&&(scroll.row=wrapper.rows-1),scroll.yDistance=Math.abs(scroll.row-_scroll.row),scroll.yChange=scroll.yDistance>0,scroll.direction=scroll.yDelta>=0?SCROLL_DOWN:SCROLL_UP,scroll.directionChange=scroll.direction!==_scroll.direction,buffer.reset=scroll.yDistance>buffer.rows}function updateViewModel(){view.yTop=scroll.y,view.yBottom=scroll.y+container.height,view.top=scroll.row,view.bottom=Math.floor(view.yBottom/row.height),view.atEdge=!(view.top>0&&view.bottom(list.length/columns-trigger.distance-1)*row.height?EDGE_BOTTOM:view.yTop(list.length-1)*row.height?EDGE_BOTTOM:!1,view.deadZoneChange=view.deadZone!==_view.deadZone,view.ytChange=view.top!==_view.top,view.ybChange=view.bottom!==_view.bottom}function setBufferToIndex(index){buffer.top=index,buffer.bottom=buffer.top+buffer.rows,validateBuffer()}function updateBufferModel(){var index=scroll.row,direction=scroll.direction,distance=buffer.distance;switch(direction){case SCROLL_UP:buffer.top=index-distance,buffer.bottom=index-distance+buffer.rows;break;case SCROLL_DOWN:buffer.top=index,buffer.bottom=index+buffer.rows;break;default:$log.warn("We only know how to deal with scrolling on the y axis for now")}validateBuffer()}function scrollingUp(start,distance){for(var p,x,y,itemsToMerge=list.slice(start*columns,start*columns+distance),r=start+distance/columns-1,updates=[],i=itemsToMerge.length-1;i>=0;i--){p=getRelativeBufferPosition(start*columns+i),x=p%columns*(container.width/columns),y=r*row.height;var coords={x:x,y:y},index=start*columns+i;updates.push(p),updateItem(p,itemsToMerge[i],coords,index),p%columns===0?r--:null}requestAnimFrame(function(){scope.$apply(function(){for(var i=0;i0?buffer.rows-view.rows:0}function clearElements(){for(var i=0;i Date: Mon, 31 Mar 2014 11:51:35 +0100 Subject: [PATCH 03/10] [Re #22] Updating documentation to reflect the removal of iScroll --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e90c836..f212a1a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ### An AngularJS Directive to mimic iOS UITableView to give a fast unlimited length list if items on mobile using ng-repeat -Scrolling on mobile is a pain. Infinite scrolling and large lists are a massive pain! Which is why there is no perfect solution out there, especially in the Angular world. So we developed our own. The core value of the project is to be as simple to utilise as possible while turning long lists of data into seamless, jank free, scrolling lists on mobile. Which in turn means they run shit hot on the desktop. +Scrolling on mobile is a pain. Infinite scrolling and large lists are a massive pain! Which is why there is no perfect solution out there, especially in the Angular world. So we developed our own. The core value of the project is to be as simple to utilise as possible while turning long lists of data into seamless, jank free, scrolling lists on mobile. Which in turn means they run shit hot on the desktop too! ## Demo http://angular-ui-table-view.mallzee.com @@ -20,7 +20,6 @@ Add the required files to your projects `index.html` file ```HTML - ``` @@ -42,13 +41,11 @@ Here's some sample markup to turn your list into a super list # How does it work? -The mlz-ui-table-view directive watches over your big list of items. It creates a subset of the items based the viewport size (You can override the calculated values with a view object). It then injects the correct data into the correct DOM elements, and moves them into position to create the illusion of a stream of items, without killing the performance of your device and or crashing it all together. +The mlz-ui-table-view directive watches over your big list of items. It creates a subset of the items based the viewport size. You can override the calculated values with a view object. It then injects the correct data from your full list into the correct DOM elements and moves them into position to create the illusion of a stream of items. This is required when displaying large lists to avoid killing the performance of your app, or crashing it all together. # Why is this different -Currently, this is based off iScroll 5, for now. This has the ability to inject the missing scroll events on mobile devices on momentum scroll. We then use this information to juggle DOM elements rather than deleting and recreating like some other solutions. - -DOM elements are limited to the buffer size and are only ever destroyed or created after initialisation when the list becomes smaller than the buffer size, or grows towards the buffer limit. This is what makes the list highly performant. They are moved into the correct place in the list ,using 3d transforms, based on the item index and scroll position and are injected with the correct information from the larger array. +A lot of solutions out there rely on keeping DOM elements to a minimum, but create and destroy them as is necessary, which is expensive. Here, DOM elements are limited to the buffer size and are only ever destroyed or created after initialisation when the list becomes smaller than the buffer size, or grows towards the buffer limit. This is what makes the list highly performant. They are moved into the correct place in the list using 3d transforms based on the item index and elements scope is injected with the correct information from the larger array. # Attributes From 1c1f5bd4c6af5c987489769941ffc45580351a3f Mon Sep 17 00:00:00 2001 From: Jamie Sutherland Date: Mon, 7 Apr 2014 16:29:33 +0100 Subject: [PATCH 04/10] Adding licence file --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..62ea36d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 - Mallzee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file From f5d9ee2176c6ef2e9abb9bc35e5e62229caccdb9 Mon Sep 17 00:00:00 2001 From: Traxmaxx Date: Tue, 8 Apr 2014 14:57:43 +0200 Subject: [PATCH 05/10] Look for a distinct listId in viewParams --- src/ui-table-view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui-table-view.js b/src/ui-table-view.js index a8a256d..5feff67 100644 --- a/src/ui-table-view.js +++ b/src/ui-table-view.js @@ -279,6 +279,7 @@ if (attributes.viewParams) { scope.$watch(attributes.viewParams, function (view) { + id = view.listId || 1; row.height = view.rowHeight || ROW_HEIGHT; columns = view.columns || COLUMNS; buffer.rows = view.rows || BUFFER_ROWS; From a192bf057b49c2e414dc62bbd27f99aaeb5adb94 Mon Sep 17 00:00:00 2001 From: Traxmaxx Date: Tue, 8 Apr 2014 16:20:56 +0200 Subject: [PATCH 06/10] Expose a function to be able to reset the scroll position for the current list --- src/ui-table-view.js | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/ui-table-view.js b/src/ui-table-view.js index 5feff67..3759e4d 100644 --- a/src/ui-table-view.js +++ b/src/ui-table-view.js @@ -288,6 +288,14 @@ }, true); } + /** + * Expose resetPosition function to the controller scope + * TODO: should accept an id parameter to be able to reset the position for different lists + */ + scope.resetPosition = function () { + resetPosition(); + } + // Setup trigger functions for the directive if (attributes.triggerTop) { triggerTop = function () { @@ -900,15 +908,13 @@ } function restorePosition() { - if ($window.localStorage.getItem('mlzUITableView.' + id + '.scroll')) { - scroll = JSON.parse($window.localStorage.getItem('mlzUITableView.' + id + '.scroll')); - } - if ($window.localStorage.getItem('mlzUITableView.' + id + '.view')) { - view = JSON.parse($window.localStorage.getItem('mlzUITableView.' + id + '.view')); - } - if ($window.localStorage.getItem('mlzUITableView.' + id + '.buffer')) { - buffer = JSON.parse($window.localStorage.getItem('mlzUITableView.' + id + '.buffer')); + if ($window.localStorage.getItem('mlzUITableView.' + id)) { + var cache = JSON.parse($window.localStorage.getItem('mlzUITableView.' + id)); + scroll = cache.scroll; + view = cache.view; + buffer = cache.buffer; } + console.log('Restoring', scroll.y); setupNextTick(); //setScrollPosition(scroll.y); @@ -916,9 +922,17 @@ } function savePosition () { - $window.localStorage.setItem('mlzUITableView.' + id + '.scroll', JSON.stringify(scroll)); - $window.localStorage.setItem('mlzUITableView.' + id + '.view', JSON.stringify(view)); - $window.localStorage.setItem('mlzUITableView.' + id + '.buffer', JSON.stringify(buffer)); + $window.localStorage.setItem('mlzUITableView.' + id, JSON.stringify({ + scroll: scroll, + view: view, + buffer: buffer + })); + } + + function resetPosition () { + if ($window.localStorage.getItem('mlzUITableView.' + id)) { + $window.localStorage.removeItem('mlzUITableView.' + id); + } } function cleanup () { From e543e95e0021269a93a2fbada4e0802a4f9e0578 Mon Sep 17 00:00:00 2001 From: Traxmaxx Date: Tue, 8 Apr 2014 16:57:14 +0200 Subject: [PATCH 07/10] Update tests and reset localStorage during cleanup --- test/ui-table-view.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/ui-table-view.js b/test/ui-table-view.js index 0cec994..f4228b1 100644 --- a/test/ui-table-view.js +++ b/test/ui-table-view.js @@ -12,7 +12,8 @@ describe('UITableView', function () { var numberOfItems = 1000, rowHeight = 100, - bufferSize = 10; + bufferSize = 10, + listId = 1337; var html = ' Date: Sat, 12 Apr 2014 13:02:08 +0200 Subject: [PATCH 08/10] Add main definition Adds the main files definition (useful when using tools such as bower-install) --- bower.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bower.json b/bower.json index a49cf68..551d1e1 100644 --- a/bower.json +++ b/bower.json @@ -14,6 +14,10 @@ "test", "tests" ], + "main": [ + "dist/ui-table-view.js", + "dist/ui-table-view.css" + ], "dependencies": { "angular": "~1.2.0", "angular-animate": "~1.2.0", From 1c973f09415843a268f983a6a90e92f7617d1193 Mon Sep 17 00:00:00 2001 From: Jamie Sutherland Date: Wed, 23 Apr 2014 14:55:20 +0100 Subject: [PATCH 09/10] [Fixes #26] Added support for common browsers --- dist/ui-table-view.js | 12 ++++++++++-- dist/ui-table-view.min.js | 2 +- src/ui-table-view.js | 12 ++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/dist/ui-table-view.js b/dist/ui-table-view.js index 625b193..6562b65 100644 --- a/dist/ui-table-view.js +++ b/dist/ui-table-view.js @@ -802,7 +802,10 @@ function setupElement (element) { var el = getItemElement(element.clone); el.css({ - 'webkitTransform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', + '-webkit-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', + '-moz-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', + '-ms-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', + transform: 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', position: 'absolute', height: element.scope.$height + 'px' }); @@ -815,7 +818,12 @@ */ function repositionElement (element) { var el = getItemElement(element.clone); - el.css('-webkit-transform', 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)') + el.css({ + '-webkit-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', + '-moz-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', + '-ms-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', + 'transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)' + }); } /** diff --git a/dist/ui-table-view.min.js b/dist/ui-table-view.min.js index 378f6ea..d2d9265 100644 --- a/dist/ui-table-view.min.js +++ b/dist/ui-table-view.min.js @@ -1 +1 @@ -!function(window,angular,undefined){"use strict";function getBlockElements(nodes){var startNode=nodes[0],endNode=nodes[nodes.length-1];if(startNode===endNode)return angular.element(startNode);var element=startNode,elements=[element];do{if(element=element.nextSibling,!element)break;elements.push(element)}while(element!==endNode);return angular.element(elements)}function getItemElement(nodes){var startNode=nodes[0],endNode=nodes[nodes.length-1];if(startNode===endNode)return angular.element(startNode);var element=startNode;do{if(element.classList&&element.classList.contains("mlz-ui-table-view-item"))return angular.element(element);element=element.nextSibling}while(element!==endNode);return!1}var move=function(arr,old_index,new_index){if(new_index>=arr.length)for(var k=new_index-arr.length;k--+1;)arr.push(undefined);return arr.splice(new_index,0,arr.splice(old_index,1)[0]),arr};window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(callback){window.setTimeout(callback,1e3/60)}}(),angular.module("mallzee.ui-table-view",["ngAnimate"]).directive("mlzUiTableView",["$window","$timeout","$log","$animate",function($window,$timeout,$log,$animate){return{restrict:"E",transclude:!0,terminal:!0,priority:1e4,$$tlb:!0,replace:!1,template:'
',link:function(scope,element,attributes,ctrl,$transclude){function update(){setScrollPosition(y),updating=!1}function initialise(scope,attributes){element.css({display:"block",overflow:"auto"}),wrapper.el.css({position:"relative"}),_scroll=angular.copy(scroll),_view=angular.copy(view),_buffer=angular.copy(buffer),attributes.itemName&&(itemName=attributes.itemName),attributes.viewParams&&scope.$watch(attributes.viewParams,function(view){row.height=view.rowHeight||ROW_HEIGHT,columns=view.columns||COLUMNS,buffer.rows=view.rows||BUFFER_ROWS,buffer.size=buffer.rows*columns,refresh()},!0),attributes.triggerTop&&(triggerTop=function(){scope.$eval(attributes.triggerTop)}),attributes.triggerBottom&&(triggerBottom=function(){scope.$eval(attributes.triggerBottom)}),$window.addEventListener("statusTap",function(){scrollToTop()}),calculateContainer()}function refresh(){updateBufferModel(),generateBufferedItems(),calculateDimensions(),updateViewModel(),triggerEdge()}function cloneElement(clone){clone.addClass("mlz-ui-table-view-item"),clone[clone.length++]=document.createComment(" end mlzTableViewItem: "+attributes.list+" "),$animate.enter(clone,wrapper.el)}function updateItem(elIndex,item,coords,index){buffer.elements[elIndex].scope[itemName]=item,buffer.elements[elIndex].scope.$coords=coords,buffer.elements[elIndex].scope.$index=index}function destroyItem(index){var elementsToRemove=getBlockElements(buffer.elements[index].clone);$animate.leave(elementsToRemove),buffer.elements[index].scope.$destroy(),buffer.elements.splice(index,1)}function generateBufferedItems(){if(angular.copy(list.slice(itemIndexFromRow(buffer.top),itemIndexFromRow(buffer.bottom)),items),buffer.elements.length>buffer.size)for(var elementsLength=buffer.elements.length,i=elementsLength-1;i>=buffer.size;i--)destroyItem(i);for(var p,x,y,e,found,r=buffer.top-1,i=0;i=items.length)buffer.elements[i]&&destroyItem(i);else{if(found=!1,e=itemIndexFromRow(buffer.top)+i,p=getRelativeBufferPosition(e),p%columns===0?r++:null,x=p%columns*(container.width/columns),y=r*row.height,buffer.elements[p]&&angular.equals(list[e],buffer.elements[p].scope[itemName])){buffer.elements[p].scope.$coords={x:x,y:y},repositionElement(buffer.elements[p]);continue}if(buffer.elements[p]){for(var k=p;k=list.length/columns&&(buffer.bottom=list.length/columns,buffer.top=buffer.bottom-buffer.size>0?buffer.bottom-buffer.size:0),buffer.yTop=buffer.top*row.height,buffer.yBottom=buffer.bottom*row.height,buffer.atEdge=buffer.top<=0?EDGE_TOP:buffer.bottom>=wrapper.rows?EDGE_BOTTOM:!1}function isRenderRequired(){return scroll.direction===SCROLL_UP&&buffer.atEdge!==EDGE_TOP&&view.deadZone===!1&&(scroll.directionChange||view.ytChange)||scroll.direction===SCROLL_DOWN&&buffer.atEdge!==EDGE_BOTTOM&&view.deadZone===!1&&(scroll.directionChange||view.ytChange)||!(view.deadZone!==!1&&view.deadZoneChange===!1)}function isTriggerRequired(){return view.triggerZone!==!1&&view.triggerZoneChange}function updateScrollModel(y){scroll.y=y,scroll.yDelta=y-_scroll.y,scroll.row=Math.floor(y/row.height),scroll.row<0&&(scroll.row=0),scroll.row>=wrapper.rows&&(scroll.row=wrapper.rows-1),scroll.yDistance=Math.abs(scroll.row-_scroll.row),scroll.yChange=scroll.yDistance>0,scroll.direction=scroll.yDelta>=0?SCROLL_DOWN:SCROLL_UP,scroll.directionChange=scroll.direction!==_scroll.direction,buffer.reset=scroll.yDistance>buffer.rows}function updateViewModel(){view.yTop=scroll.y,view.yBottom=scroll.y+container.height,view.top=scroll.row,view.bottom=Math.floor(view.yBottom/row.height),view.atEdge=!(view.top>0&&view.bottom(list.length/columns-trigger.distance-1)*row.height?EDGE_BOTTOM:view.yTop(list.length-1)*row.height?EDGE_BOTTOM:!1,view.deadZoneChange=view.deadZone!==_view.deadZone,view.ytChange=view.top!==_view.top,view.ybChange=view.bottom!==_view.bottom}function setBufferToIndex(index){buffer.top=index,buffer.bottom=buffer.top+buffer.rows,validateBuffer()}function updateBufferModel(){var index=scroll.row,direction=scroll.direction,distance=buffer.distance;switch(direction){case SCROLL_UP:buffer.top=index-distance,buffer.bottom=index-distance+buffer.rows;break;case SCROLL_DOWN:buffer.top=index,buffer.bottom=index+buffer.rows;break;default:$log.warn("We only know how to deal with scrolling on the y axis for now")}validateBuffer()}function scrollingUp(start,distance){for(var p,x,y,itemsToMerge=list.slice(start*columns,start*columns+distance),r=start+distance/columns-1,updates=[],i=itemsToMerge.length-1;i>=0;i--){p=getRelativeBufferPosition(start*columns+i),x=p%columns*(container.width/columns),y=r*row.height;var coords={x:x,y:y},index=start*columns+i;updates.push(p),updateItem(p,itemsToMerge[i],coords,index),p%columns===0?r--:null}requestAnimFrame(function(){scope.$apply(function(){for(var i=0;i0?buffer.rows-view.rows:0}function clearElements(){for(var i=0;i=arr.length)for(var k=new_index-arr.length;k--+1;)arr.push(undefined);return arr.splice(new_index,0,arr.splice(old_index,1)[0]),arr};window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(callback){window.setTimeout(callback,1e3/60)}}(),angular.module("mallzee.ui-table-view",["ngAnimate"]).directive("mlzUiTableView",["$window","$timeout","$log","$animate",function($window,$timeout,$log,$animate){return{restrict:"E",transclude:!0,terminal:!0,priority:1e4,$$tlb:!0,replace:!1,template:'
',link:function(scope,element,attributes,ctrl,$transclude){function update(){setScrollPosition(y),updating=!1}function initialise(scope,attributes){element.css({display:"block",overflow:"auto"}),wrapper.el.css({position:"relative"}),_scroll=angular.copy(scroll),_view=angular.copy(view),_buffer=angular.copy(buffer),attributes.itemName&&(itemName=attributes.itemName),attributes.viewParams&&scope.$watch(attributes.viewParams,function(view){row.height=view.rowHeight||ROW_HEIGHT,columns=view.columns||COLUMNS,buffer.rows=view.rows||BUFFER_ROWS,buffer.size=buffer.rows*columns,refresh()},!0),attributes.triggerTop&&(triggerTop=function(){scope.$eval(attributes.triggerTop)}),attributes.triggerBottom&&(triggerBottom=function(){scope.$eval(attributes.triggerBottom)}),$window.addEventListener("statusTap",function(){scrollToTop()}),calculateContainer()}function refresh(){updateBufferModel(),generateBufferedItems(),calculateDimensions(),updateViewModel(),triggerEdge()}function cloneElement(clone){clone.addClass("mlz-ui-table-view-item"),clone[clone.length++]=document.createComment(" end mlzTableViewItem: "+attributes.list+" "),$animate.enter(clone,wrapper.el)}function updateItem(elIndex,item,coords,index){buffer.elements[elIndex].scope[itemName]=item,buffer.elements[elIndex].scope.$coords=coords,buffer.elements[elIndex].scope.$index=index}function destroyItem(index){var elementsToRemove=getBlockElements(buffer.elements[index].clone);$animate.leave(elementsToRemove),buffer.elements[index].scope.$destroy(),buffer.elements.splice(index,1)}function generateBufferedItems(){if(angular.copy(list.slice(itemIndexFromRow(buffer.top),itemIndexFromRow(buffer.bottom)),items),buffer.elements.length>buffer.size)for(var elementsLength=buffer.elements.length,i=elementsLength-1;i>=buffer.size;i--)destroyItem(i);for(var p,x,y,e,found,r=buffer.top-1,i=0;i=items.length)buffer.elements[i]&&destroyItem(i);else{if(found=!1,e=itemIndexFromRow(buffer.top)+i,p=getRelativeBufferPosition(e),p%columns===0?r++:null,x=p%columns*(container.width/columns),y=r*row.height,buffer.elements[p]&&angular.equals(list[e],buffer.elements[p].scope[itemName])){buffer.elements[p].scope.$coords={x:x,y:y},repositionElement(buffer.elements[p]);continue}if(buffer.elements[p]){for(var k=p;k=list.length/columns&&(buffer.bottom=list.length/columns,buffer.top=buffer.bottom-buffer.size>0?buffer.bottom-buffer.size:0),buffer.yTop=buffer.top*row.height,buffer.yBottom=buffer.bottom*row.height,buffer.atEdge=buffer.top<=0?EDGE_TOP:buffer.bottom>=wrapper.rows?EDGE_BOTTOM:!1}function isRenderRequired(){return scroll.direction===SCROLL_UP&&buffer.atEdge!==EDGE_TOP&&view.deadZone===!1&&(scroll.directionChange||view.ytChange)||scroll.direction===SCROLL_DOWN&&buffer.atEdge!==EDGE_BOTTOM&&view.deadZone===!1&&(scroll.directionChange||view.ytChange)||!(view.deadZone!==!1&&view.deadZoneChange===!1)}function isTriggerRequired(){return view.triggerZone!==!1&&view.triggerZoneChange}function updateScrollModel(y){scroll.y=y,scroll.yDelta=y-_scroll.y,scroll.row=Math.floor(y/row.height),scroll.row<0&&(scroll.row=0),scroll.row>=wrapper.rows&&(scroll.row=wrapper.rows-1),scroll.yDistance=Math.abs(scroll.row-_scroll.row),scroll.yChange=scroll.yDistance>0,scroll.direction=scroll.yDelta>=0?SCROLL_DOWN:SCROLL_UP,scroll.directionChange=scroll.direction!==_scroll.direction,buffer.reset=scroll.yDistance>buffer.rows}function updateViewModel(){view.yTop=scroll.y,view.yBottom=scroll.y+container.height,view.top=scroll.row,view.bottom=Math.floor(view.yBottom/row.height),view.atEdge=!(view.top>0&&view.bottom(list.length/columns-trigger.distance-1)*row.height?EDGE_BOTTOM:view.yTop(list.length-1)*row.height?EDGE_BOTTOM:!1,view.deadZoneChange=view.deadZone!==_view.deadZone,view.ytChange=view.top!==_view.top,view.ybChange=view.bottom!==_view.bottom}function setBufferToIndex(index){buffer.top=index,buffer.bottom=buffer.top+buffer.rows,validateBuffer()}function updateBufferModel(){var index=scroll.row,direction=scroll.direction,distance=buffer.distance;switch(direction){case SCROLL_UP:buffer.top=index-distance,buffer.bottom=index-distance+buffer.rows;break;case SCROLL_DOWN:buffer.top=index,buffer.bottom=index+buffer.rows;break;default:$log.warn("We only know how to deal with scrolling on the y axis for now")}validateBuffer()}function scrollingUp(start,distance){for(var p,x,y,itemsToMerge=list.slice(start*columns,start*columns+distance),r=start+distance/columns-1,updates=[],i=itemsToMerge.length-1;i>=0;i--){p=getRelativeBufferPosition(start*columns+i),x=p%columns*(container.width/columns),y=r*row.height;var coords={x:x,y:y},index=start*columns+i;updates.push(p),updateItem(p,itemsToMerge[i],coords,index),p%columns===0?r--:null}requestAnimFrame(function(){scope.$apply(function(){for(var i=0;i0?buffer.rows-view.rows:0}function clearElements(){for(var i=0;i Date: Fri, 2 May 2014 13:59:37 +0200 Subject: [PATCH 10/10] Fix gaps when restoring scroll-position and save to localStorage when leaving view only --- src/ui-table-view.js | 90 ++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/src/ui-table-view.js b/src/ui-table-view.js index 5708715..9ebf0d1 100644 --- a/src/ui-table-view.js +++ b/src/ui-table-view.js @@ -95,19 +95,16 @@ SCROLL_DOWN = 'down', TRIGGER_DISTANCE = 1; - var id = 1, list = [], items = [], itemName = 'item', + var id = 1, list = [], items = [], itemName = 'item', posObj = {}, - model = { - - }, - // Model the main container for the view table + // Model the main container for the view table container = { height: 0, width: 0, el: undefined }, - // Model the wrapper that will hold the buffer and be scrolled by the container + // Model the wrapper that will hold the buffer and be scrolled by the container wrapper = { height: 0, width: 0, @@ -117,7 +114,7 @@ elements, - // Model a row + // Model a row row = { height: ROW_HEIGHT, width: ROW_WIDTH @@ -129,8 +126,8 @@ distance: TRIGGER_DISTANCE }, - // Information about the scroll status - // _ indicates the value of that item on the previous tick + // Information about the scroll status + // _ indicates the value of that item on the previous tick scroll = { // X-Axis x: 0, //TODO: Support x axis @@ -165,16 +162,6 @@ }, _scroll, // Previous tick data - metadata = { - $$position: 0, - $$visible: true, - $$coords: { - x: 0, - y: 0 - }, - $$height: 0 - }, - view = { top: 0, bottom: 0, @@ -408,7 +395,7 @@ // If we have an element cached and it contains the same info, leave it as it is. if (elements[p] && angular.equals(list[e], elements[p].scope[itemName])) { - elements[p].scope.$coords = { x:x, y:y } + elements[p].scope.$coords = { x:x, y:y }; //$animate.move(elements[p].clone, wrapper.el); repositionElement(elements[p]); @@ -419,7 +406,7 @@ // Scan the buffer for this item. If it exists we should move that item into this // position and send this block to the bottom to be reused. for(var k = p; k < buffer.size; k++) { - if (buffer.elements[k] && found) { + if (elements[k] && found) { // Update positions of everything else in the buffer elements[k].scope.$coords = { x:x, y:y } } @@ -431,7 +418,7 @@ // and move them to the end. //elements.join(elements.slice(p, k - p)); // Move the found element into the correct place in the buffer elements array - move(buffer.elements, k, p); + move(elements, k, p); //$animate.move(buffer.elements[p].clone, wrapper.el); //repositionElement(buffer.elements[p]); //repositionElement(buffer.elements[k]); @@ -577,7 +564,7 @@ function isRenderRequired () { return( ((scroll.direction === SCROLL_UP && buffer.atEdge !== EDGE_TOP && view.deadZone === false) && (scroll.directionChange || view.ytChange)) || - ((scroll.direction === SCROLL_DOWN && buffer.atEdge !== EDGE_BOTTOM && view.deadZone === false) && (scroll.directionChange || view.ytChange)) || !(view.deadZone !== false && view.deadZoneChange === false) + ((scroll.direction === SCROLL_DOWN && buffer.atEdge !== EDGE_BOTTOM && view.deadZone === false) && (scroll.directionChange || view.ytChange)) || !(view.deadZone !== false && view.deadZoneChange === false) ); } @@ -690,7 +677,7 @@ /** * Perform the scrolling up action by updating the required elements * @param start - * @param end + * @param distance */ function scrollingUp (start, distance) { @@ -813,12 +800,14 @@ function setupElement (element) { - var el = getItemElement(element.clone); + var el = getItemElement(element.clone), + transFunc = 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)'; + el.css({ - '-webkit-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', - '-moz-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', - '-ms-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', - transform: 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', + '-webkit-transform': transFunc, + '-moz-transform': transFunc, + '-ms-transform': transFunc, + transform: transFunc, position: 'absolute', height: element.scope.$height + 'px' }); @@ -830,12 +819,14 @@ * @param y */ function repositionElement (element) { - var el = getItemElement(element.clone); + var el = getItemElement(element.clone), + transFunc = 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)'; + el.css({ - '-webkit-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', - '-moz-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', - '-ms-transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)', - 'transform': 'translate3d(' + element.scope.$coords.x + 'px, ' + element.scope.$coords.y + 'px, 0px)' + '-webkit-transform': transFunc, + '-moz-transform': transFunc, + '-ms-transform': transFunc, + 'transform': transFunc }); } @@ -915,26 +906,36 @@ } } - function restorePosition() { - if ($window.localStorage.getItem('mlzUITableView.' + id)) { - var cache = JSON.parse($window.localStorage.getItem('mlzUITableView.' + id)); - scroll = cache.scroll; - view = cache.view; - buffer = cache.buffer; + /** + * Check localstorage for a valid scroll-position to restore + */ + function restorePosition () { + var posItem = $window.localStorage.getItem('mlzUITableView.' + id); + + if (posItem) { + //TODO: Leaving the view without scrolling saves an empty object which should never be saved + if (posItem !== '{}') { + posObj = JSON.parse(posItem); + scroll = posObj.scroll; + view = posObj.view; + buffer = posObj.buffer; + } + + resetPosition(); } - console.log('Restoring', scroll.y); setupNextTick(); - //setScrollPosition(scroll.y); + + setScrollPosition(scroll.y); container.el.prop('scrollTop', scroll.y); } function savePosition () { - $window.localStorage.setItem('mlzUITableView.' + id, JSON.stringify({ + posObj = { scroll: scroll, view: view, buffer: buffer - })); + }; } function resetPosition () { @@ -955,6 +956,7 @@ } scope.$on('$destroy', function () { + $window.localStorage.setItem('mlzUITableView.' + id, JSON.stringify(posObj)); cleanup(); }); }