kahuna/public/js/components/gu-lazy-preview/gu-lazy-preview.js (90 lines of code) (raw):

import angular from 'angular'; import Rx from 'rx'; import 'rx-dom'; import template from './gu-lazy-preview.html'; import './gu-lazy-preview.css'; import '../../util/rx'; export var lazyPreview = angular.module('gu.lazyPreview', ['util.rx']); function asInt(string) { return parseInt(string, 10); } lazyPreview.controller('GuLazyPreviewCtrl', [function() { let ctrl = this; ctrl.init = function({items$, totalItems$, preloadedItems$, currentIndex$}) { const itemsCount$ = items$.map(items => items.length).distinctUntilChanged(); const totalItemsCount$ = totalItems$.map(totalItems => { return totalItems.length; }).distinctUntilChanged(); const buttonCommands$ = Rx.Observable.create(observer => { ctrl.prevItem = () => observer.onNext('prevItem'); ctrl.nextItem = () => observer.onNext('nextItem'); // Make sure we start at the beginning observer.onNext('previewStart'); }); const itemsOffset$ = buttonCommands$.combineLatest( itemsCount$, (command, itemsCount) => { return {command, itemsCount}; }).withLatestFrom( currentIndex$, ({command}, currentIndex) => { return { prevItem: -1, nextItem: +1, previewStart: currentIndex * -1 }[command] || 0; } ); const updatedIndex$ = itemsOffset$.withLatestFrom( currentIndex$, itemsCount$, totalItemsCount$, (itemsOffset, currentIndex, itemsCount, totalItemsCount) => { const updatedIndex = currentIndex + itemsOffset; // Update the index if it's in the range of items if (updatedIndex >= 0 && updatedIndex < totalItemsCount) { return updatedIndex; } else { return currentIndex; } }); const item$ = updatedIndex$.withLatestFrom( totalItemsCount$, items$, (updatedIndex, totalItemsCount, items) => { currentIndex$.onNext(updatedIndex); return items[updatedIndex]; }); const currentPage$ = currentIndex$.withLatestFrom( preloadedItems$, (currentIndex, preloadedItems) => { return Math.floor(currentIndex / preloadedItems); }); const rangeToLoad$ = currentPage$.withLatestFrom( preloadedItems$, (currentPage, preloadedItems) => { const start = currentPage * preloadedItems; const end = ((currentPage + 1) * preloadedItems) - 1; return {start, end}; }). // Debounce range loading, which also helps discard // erroneous large ranges while combining // loadedRangeStart$ and loadedRangeEnd$ changes (one after the other) debounce(10). // Ignore if either end isn't set (whole range already loaded) filter(({start, end}) => start !== -1 && end !== -1). // Ignore if $start after $end (incomplete combine$ state) filter(({start, end}) => start <= end). distinctUntilChanged(({start, end}) => `${start}-${end}`); return { item$, rangeToLoad$ }; }; }]); lazyPreview.directive('guLazyPreview', ['observe$', 'observeCollection$', 'subscribe$', 'inject$', function( observe$, observeCollection$, subscribe$, inject$) { return { restrict: 'E', controller: 'GuLazyPreviewCtrl', controllerAs: 'previewCtrl', transclude: true, template: template, link: function(scope, element, attrs, ctrl) { // Map attributes as Observable streams const { guLazyPreviewItems: itemsAttr, guLazyPreviewItemsTotal: totalItemsAttr, guLazyPreviewSelectionMode: selectionMode, guLazyPreviewLoadRange: loadRangeFn, guLazyPreviewPreloadedItems: preloadedItemsAttr } = attrs; const items$ = observeCollection$(scope, itemsAttr); const selectionMode$ = observe$(scope, selectionMode); const totalItems$ = observe$(scope, totalItemsAttr); const preloadedItems$ = observe$(scope, preloadedItemsAttr).map(asInt); const currentIndex$ = new Rx.BehaviorSubject(0); const {item$, rangeToLoad$} = ctrl.init( {items$, totalItems$, preloadedItems$, currentIndex$} ); subscribe$(scope, rangeToLoad$, range => { scope.$eval(loadRangeFn, range); }); inject$(scope, currentIndex$, ctrl, 'currentIndex'); inject$(scope, selectionMode$, ctrl, 'selectionMode'); inject$(scope, item$, ctrl, 'item'); } }; }]);