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 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 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", 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/dist/ui-table-view.js b/dist/ui-table-view.js index cfc74df..b6db58e 100644 --- a/dist/ui-table-view.js +++ b/dist/ui-table-view.js @@ -408,8 +408,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 } } @@ -804,7 +804,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' }); @@ -817,7 +820,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 4c2b63f..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=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