diff --git a/ionic/animations/animation.ts b/ionic/animations/animation.ts index 680425f17d..7317f06f0e 100644 --- a/ionic/animations/animation.ts +++ b/ionic/animations/animation.ts @@ -1,5 +1,6 @@ import {CSS} from '../util/dom'; import {extend} from '../util/util'; +import {FastDom} from '../util/fastdom'; /** @@ -24,8 +25,10 @@ import {extend} from '../util/util'; export class Animation { - constructor(ele, opts={}) { + constructor(ele, opts={}, fastdom=null) { this.reset(); + this._fastdom = fastdom; + this._opts = extend({ renderDelay: 16 }, opts); @@ -251,10 +254,14 @@ export class Animation { }); } - if (self._duration > 16) { + if (self._duration > 16 && this._opts.renderDelay > 0) { // begin each animation when everything is rendered in their starting point // give the browser some time to render everything in place before starting - setTimeout(kickoff, this._opts.renderDelay); + if (this._fastdom) { + this._fastdom.write(kickoff); + } else { + setTimeout(kickoff, this._opts.renderDelay); + } } else { // no need to render everything in there place before animating in @@ -503,14 +510,19 @@ export class Animation { return copy(new Animation(), this); } - dispose() { + dispose(removeElement) { let i; for (i = 0; i < this._chld.length; i++) { - this._chld[i].dispose(); + this._chld[i].dispose(removeElement); } for (i = 0; i < this._ani.length; i++) { - this._ani[i].dispose(); + this._ani[i].dispose(removeElement); + } + if (removeElement) { + for (i = 0; i < this._el.length; i++) { + this._el[i].parentNode && this._el[i].parentNode.removeChild(this._el[i]); + } } this.reset(); @@ -607,7 +619,7 @@ class Animate { self.ani = self.ani.onfinish = null; - done && done(); + done && done(); } }; } diff --git a/ionic/components/app/app.ts b/ionic/components/app/app.ts index 0365f37ad1..2589e3a829 100644 --- a/ionic/components/app/app.ts +++ b/ionic/components/app/app.ts @@ -2,7 +2,6 @@ import {Title} from 'angular2/angular2'; import {ClickBlock} from '../../util/click-block'; import {ScrollTo} from '../../animations/scroll-to'; -import * as dom from '../../util/dom'; /** @@ -11,11 +10,10 @@ import * as dom from '../../util/dom'; */ export class IonicApp { - /** - * TODO - */ - constructor() { - this._title = new Title(); + constructor(fastdom) { + this._fastdom = fastdom; + this._titleSrv = new Title(); + this._title = ''; this._disTime = 0; this._trnsTime = 0; @@ -28,11 +26,12 @@ export class IonicApp { * @param {string} val Value to set the document title to. */ setTitle(val) { - this._title.setTitle(val); - } - - getTitle() { - return this._title.getTitle(val); + if (val !== this._title) { + this._title = val; + this._fastdom.defer(4, () => { + this._titleSrv.setTitle(this._title); + }); + } } /** diff --git a/ionic/components/nav/nav-controller.ts b/ionic/components/nav/nav-controller.ts index 75788e75bd..6bfe247320 100644 --- a/ionic/components/nav/nav-controller.ts +++ b/ionic/components/nav/nav-controller.ts @@ -436,7 +436,7 @@ export class NavController extends Ion { } if (!opts.animation) { - opts.animation = this.config.get('viewTransition'); + opts.animation = this.config.get('pageTransition'); } if (this.config.get('animate') === false) { opts.animate = false; @@ -468,6 +468,7 @@ export class NavController extends Ion { leavingView.state = STAGED_LEAVING_STATE; // init the transition animation + opts.renderDelay = this.config.get('pageTransitionDelay'); let transAnimation = Transition.create(this, opts); if (opts.animate === false) { // force it to not animate the elements, just apply the "to" styles diff --git a/ionic/components/tabs/tabs.ts b/ionic/components/tabs/tabs.ts index 4d7543c666..0130aa6092 100644 --- a/ionic/components/tabs/tabs.ts +++ b/ionic/components/tabs/tabs.ts @@ -4,6 +4,7 @@ import {Ion} from '../ion'; import {IonicApp} from '../app/app'; import {Attr} from '../app/id'; import {Config} from '../../config/config'; +import {Platform} from '../../platform/platform'; import {ViewController} from '../nav/view-controller'; import {ConfigComponent} from '../../config/decorators'; import {Icon} from '../icon/icon'; @@ -107,7 +108,8 @@ export class Tabs extends Ion { app: IonicApp, config: Config, elementRef: ElementRef, - @Optional() viewCtrl: ViewController + @Optional() viewCtrl: ViewController, + private platform: Platform ) { super(elementRef, config); this.app = app; @@ -132,6 +134,15 @@ export class Tabs extends Ion { } } + onInit() { + super.onInit(); + if (this.highlight) { + this.platform.onResize(() => { + this.highlight.select(this.getSelected()); + }); + } + } + /** * @private */ @@ -305,7 +316,7 @@ class TabButton extends Ion { }) class TabHighlight { constructor(@Host() tabs: Tabs, config: Config, elementRef: ElementRef) { - if (config.get('mode') === 'md') { + if (config.get('tabbarHighlight')) { tabs.highlight = this; this.elementRef = elementRef; } @@ -320,7 +331,7 @@ class TabHighlight { this.init = true; setTimeout(() => { ele.classList.add('animate'); - }, 64) + }, 64); } } diff --git a/ionic/components/tap-click/activator.ts b/ionic/components/tap-click/activator.ts index 3c6f2cf805..48eff4f949 100644 --- a/ionic/components/tap-click/activator.ts +++ b/ionic/components/tap-click/activator.ts @@ -3,11 +3,12 @@ import {raf} from '../../util/dom'; export class Activator { - constructor(app, config) { + constructor(app, config, fastdom) { this.app = app; + this.fastdom = fastdom; this.queue = []; this.active = []; - this.clearStateTimeout = 80; + this.clearStateDefers = 5; this.clearAttempt = 0; this.activatedClass = config.get('activatedClass') || 'activated'; this.x = 0; @@ -17,7 +18,7 @@ export class Activator { downAction(ev, activatableEle, pointerX, pointerY, callback) { // the user just pressed down - if (this.disableActivated(ev)) return; + if (this.disableActivated(ev)) return false; // remember where they pressed this.x = pointerX; @@ -26,7 +27,7 @@ export class Activator { // queue to have this element activated this.queue.push(activatableEle); - raf(() => { + this.fastdom.write(() => { let activatableEle; for (let i = 0; i < this.queue.length; i++) { activatableEle = this.queue[i]; @@ -37,13 +38,15 @@ export class Activator { } this.queue = []; }); + + return true; } upAction() { // the user was pressing down, then just let up - setTimeout(() => { + this.fastdom.defer(this.clearStateDefers, () => { this.clearState(); - }, this.clearStateTimeout); + }); } clearState() { @@ -65,11 +68,14 @@ export class Activator { deactivate() { // remove the active class from all active elements - for (let i = 0; i < this.active.length; i++) { - this.active[i].classList.remove(this.activatedClass); - } this.queue = []; - this.active = []; + + this.fastdom.write(() => { + for (let i = 0; i < this.active.length; i++) { + this.active[i].classList.remove(this.activatedClass); + } + this.active = []; + }); } disableActivated(ev) { diff --git a/ionic/components/tap-click/ripple.ts b/ionic/components/tap-click/ripple.ts index 72f9d261a1..c4a4942a56 100644 --- a/ionic/components/tap-click/ripple.ts +++ b/ionic/components/tap-click/ripple.ts @@ -1,23 +1,38 @@ import {Activator} from './activator'; -import {removeElement, raf} from '../../util/dom'; import {Animation} from '../../animations/animation'; +import {raf} from '../../util/dom'; export class RippleActivator extends Activator { - constructor(app, config) { - super(app, config); - this.ripples = {}; + constructor(app, config, fastdom) { + super(app, config, fastdom); + + this.expands = {}; + this.fades = {}; + this.expandSpeed = null; } downAction(ev, activatableEle, pointerX, pointerY) { + if (super.downAction(ev, activatableEle, pointerX, pointerY) ) { + // create a new ripple element + this.expandSpeed = EXPAND_DOWN_PLAYBACK_RATE; - if (this.disableActivated(ev)) return; + this.fastdom.defer(2, () => { - super.downAction(ev, activatableEle, pointerX, pointerY); + this.fastdom.read(() => { + let clientRect = activatableEle.getBoundingClientRect(); - // create a new ripple element - let clientRect = activatableEle.getBoundingClientRect(); + this.fastdom.write(() => { + this.createRipple(activatableEle, pointerX, pointerY, clientRect); + }); + }); + + }); + } + } + + createRipple(activatableEle, pointerX, pointerY, clientRect) { let clientPointerX = (pointerX - clientRect.left); let clientPointerY = (pointerY - clientRect.top); @@ -29,6 +44,7 @@ export class RippleActivator extends Activator { let duration = (1000 * Math.sqrt(radius / TOUCH_DOWN_ACCEL) + 0.5); let rippleEle = document.createElement('md-ripple'); + let rippleId = Date.now(); let eleStyle = rippleEle.style; eleStyle.width = eleStyle.height = diameter + 'px'; eleStyle.marginTop = eleStyle.marginLeft = -(diameter / 2) + 'px'; @@ -37,96 +53,74 @@ export class RippleActivator extends Activator { activatableEle.appendChild(rippleEle); - let ripple = this.ripples[Date.now()] = { - ele: rippleEle, - radius: radius, - duration: duration - }; + // create the animation for the fade out, but don't start it yet + this.fades[rippleId] = new Animation(rippleEle, {renderDelay: 0}); + this.fades[rippleId] + .fadeOut() + .duration(FADE_OUT_DURATION) + .playbackRate(1) + .onFinish(() => { + this.fastdom.write(() => { + this.fades[rippleId].dispose(true); + delete this.fades[rippleId]; + }); + }); // expand the circle from the users starting point // start slow, and when they let up, then speed up the animation - ripple.expand = new Animation(rippleEle, {renderDelay: 0}); - ripple.expand + this.expands[rippleId] = new Animation(rippleEle, {renderDelay: 0}); + this.expands[rippleId] .fromTo('scale', '0.001', '1') .duration(duration) - .playbackRate(EXPAND_DOWN_PLAYBACK_RATE) + .playbackRate(this.expandSpeed) .onFinish(()=> { - // finished expanding - ripple.expand && ripple.expand.dispose(); - ripple.expand = null; - ripple.expanded = true; + this.expands[rippleId].dispose(); + delete this.expands[rippleId]; + this.next(); }) .play(); - - this.next(); } - upAction(forceFadeOut) { + upAction() { this.deactivate(); - let rippleId, ripple; - for (rippleId in this.ripples) { - ripple = this.ripples[rippleId]; + this.expandSpeed = 1; - if (!ripple.fade || forceFadeOut) { - // ripple has not been let up yet - clearTimeout(ripple.fadeStart); - ripple.fadeStart = setTimeout(() => { - // speed up the rate if the animation is still going - ripple.expand && ripple.expand.playbackRate(EXPAND_OUT_PLAYBACK_RATE); - ripple.fade = new Animation(ripple.ele); - ripple.fade - .fadeOut() - .duration(OPACITY_OUT_DURATION) - .playbackRate(1) - .onFinish(() => { - ripple.fade && ripple.fade.dispose(); - ripple.fade = null; - ripple.faded = true; - this.next(); - }) - .play(); + this.fastdom.defer(4, () => { + this.next(); + }); + } - }); + next() { + const now = Date.now(); + + let rippleId; + for (rippleId in this.expands) { + if (parseInt(rippleId, 10) + 4000 < now) { + this.expands[rippleId].dispose(true); + delete this.expands[rippleId]; + + } else if (this.expands[rippleId].playbackRate() === EXPAND_DOWN_PLAYBACK_RATE) { + this.expands[rippleId].playbackRate(EXPAND_OUT_PLAYBACK_RATE); } } - this.next(); - } + for (rippleId in this.fades) { + if (parseInt(rippleId, 10) + 4000 < now) { + this.fades[rippleId].dispose(true); + delete this.fades[rippleId]; - next(forceComplete) { - let rippleId, ripple; - for (rippleId in this.ripples) { - ripple = this.ripples[rippleId]; - - if ((ripple.expanded && ripple.faded && ripple.ele) || forceComplete) { - // finished expanding and the user has lifted the pointer - ripple.remove = true; - raf(() => { - this.remove(); - }); + } else if (!this.fades[rippleId].isPlaying) { + this.fades[rippleId].isPlaying = true; + this.fades[rippleId].play(); } } } clearState() { this.deactivate(); - this.next(true); - } - - remove() { - let rippleId, ripple; - for (rippleId in this.ripples) { - ripple = this.ripples[rippleId]; - if (ripple.remove || parseInt(rippleId, 10) + 4000 < Date.now()) { - ripple.expand && ripple.expand.dispose(); - ripple.fade && ripple.fade.dispose(); - removeElement(ripple.ele); - ripple.ele = ripple.expand = ripple.fade = null; - delete this.ripples[rippleId]; - } - } + this.next(); } } @@ -134,4 +128,4 @@ export class RippleActivator extends Activator { const TOUCH_DOWN_ACCEL = 512; const EXPAND_DOWN_PLAYBACK_RATE = 0.35; const EXPAND_OUT_PLAYBACK_RATE = 3; -const OPACITY_OUT_DURATION = 750; +const FADE_OUT_DURATION = 700; diff --git a/ionic/components/tap-click/tap-click.ts b/ionic/components/tap-click/tap-click.ts index 9841fd7e0e..e57deb1b54 100644 --- a/ionic/components/tap-click/tap-click.ts +++ b/ionic/components/tap-click/tap-click.ts @@ -12,27 +12,29 @@ let disableNativeClickAmount = 3000; let activator = null; let isTapPolyfill = false; let app = null; -let config = null; let win = null; let doc = null; -export function initTapClick(windowInstance, documentInstance, appInstance, configInstance) { +export function initTapClick(windowInstance, documentInstance, appInstance, config, fastdom) { win = windowInstance; doc = documentInstance; app = appInstance; - config = configInstance; - activator = (config.get('mdRipple') ? new RippleActivator(app, config) : new Activator(app, config)); + if (config.get('activator') == 'ripple') { + activator = new RippleActivator(app, config, fastdom); + + } else if (config.get('activator') == 'highlight') { + activator = new Activator(app, config, fastdom)); + } + isTapPolyfill = (config.get('tapPolyfill') === true); addListener('click', click, true); - if (isTapPolyfill) { - addListener('touchstart', touchStart); - addListener('touchend', touchEnd); - addListener('touchcancel', touchCancel); - } + addListener('touchstart', touchStart); + addListener('touchend', touchEnd); + addListener('touchcancel', touchCancel); addListener('mousedown', mouseDown, true); addListener('mouseup', mouseUp, true); @@ -47,7 +49,7 @@ function touchStart(ev) { function touchEnd(ev) { touchAction(); - if (startCoord && app.isEnabled()) { + if (isTapPolyfill && startCoord && app.isEnabled()) { let endCoord = pointerCoord(ev); if (!hasPointerMoved(pointerTolerance, startCoord, endCoord)) { @@ -100,8 +102,8 @@ function pointerStart(ev) { startCoord = pointerCoord(ev); let now = Date.now(); - if (lastActivated + 100 < now) { - activator.downAction(ev, activatableEle, startCoord.x, startCoord.y); + if (lastActivated + 150 < now) { + activator && activator.downAction(ev, activatableEle, startCoord.x, startCoord.y); lastActivated = now; } @@ -114,7 +116,7 @@ function pointerStart(ev) { function pointerEnd(ev) { moveListeners(false); - activator.upAction(); + activator && activator.upAction(); } function pointerMove(ev) { @@ -127,20 +129,22 @@ function pointerMove(ev) { function pointerCancel(ev) { console.debug('pointerCancel from', ev.type); - activator.clearState(); + activator && activator.clearState(); moveListeners(false); } function moveListeners(shouldAdd) { - if (isTapPolyfill) { - removeListener('touchmove', pointerMove); - } - removeListener('mousemove', pointerMove); if (shouldAdd) { if (isTapPolyfill) { addListener('touchmove', pointerMove); } addListener('mousemove', pointerMove); + + } else { + if (isTapPolyfill) { + removeListener('touchmove', pointerMove); + } + removeListener('mousemove', pointerMove); } } @@ -168,8 +172,6 @@ function click(ev) { console.debug('click prevent', preventReason); ev.preventDefault(); ev.stopPropagation(); - } else { - activator.upAction(); } } diff --git a/ionic/config/bootstrap.ts b/ionic/config/bootstrap.ts index 75ba635cd3..ed3ee24618 100644 --- a/ionic/config/bootstrap.ts +++ b/ionic/config/bootstrap.ts @@ -6,6 +6,7 @@ import {IonicApp} from '../components/app/app'; import {Config} from './config'; import {Platform} from '../platform/platform'; import {OverlayController} from '../components/overlay/overlay-controller'; +import {FastDom} from '../util/fastdom'; import {Form} from '../util/form'; import {Keyboard} from '../util/keyboard'; import {ActionSheet} from '../components/action-sheet/action-sheet'; @@ -21,7 +22,9 @@ import * as dom from '../util/dom'; export function ionicProviders(args) { - let app = new IonicApp(); + let fastdom = new FastDom(); + + let app = new IonicApp(fastdom); let platform = new Platform(); let navRegistry = new NavRegistry(args.pages); @@ -38,7 +41,7 @@ export function ionicProviders(args) { config.setPlatform(platform); let events = new Events(); - initTapClick(window, document, app, config); + initTapClick(window, document, app, config, fastdom); let featureDetect = new FeatureDetect(); setupDom(window, document, config, platform, featureDetect); @@ -48,6 +51,7 @@ export function ionicProviders(args) { platform.prepareReady(config); return [ + provide(FastDom, {useValue: fastdom}), provide(IonicApp, {useValue: app}), provide(Config, {useValue: config}), provide(Platform, {useValue: platform}), diff --git a/ionic/config/config.ts b/ionic/config/config.ts index bc9d89f963..fd629f7723 100644 --- a/ionic/config/config.ts +++ b/ionic/config/config.ts @@ -22,7 +22,7 @@ import {isObject, isDefined, isFunction, isArray, extend} from '../util/util'; * modalEnter: 'modal-slide-in', * modalLeave: 'modal-slide-out', * tabbarPlacement: 'bottom', - * viewTransition: 'ios', + * pageTransition: 'ios', * } * }) * ``` diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts index ea60eee408..c0ba802ce0 100644 --- a/ionic/config/modes.ts +++ b/ionic/config/modes.ts @@ -4,6 +4,7 @@ import {Config} from './config'; // iOS Mode Settings Config.setModeConfig('ios', { + activator: 'highlight', actionSheetEnter: 'action-sheet-slide-in', actionSheetLeave: 'action-sheet-slide-out', @@ -18,16 +19,19 @@ Config.setModeConfig('ios', { modalEnter: 'modal-slide-in', modalLeave: 'modal-slide-out', - tabbarPlacement: 'bottom', - viewTransition: 'ios', + pageTransition: 'ios', + pageTransitionDelay: 16, popupPopIn: 'popup-pop-in', popupPopOut: 'popup-pop-out', + + tabbarPlacement: 'bottom', }); // Material Design Mode Settings Config.setModeConfig('md', { + activator: 'ripple', actionSheetEnter: 'action-sheet-md-slide-in', actionSheetLeave: 'action-sheet-md-slide-out', @@ -39,16 +43,19 @@ Config.setModeConfig('md', { iconMode: 'md', + type: 'overlay', + modalEnter: 'modal-md-slide-in', modalLeave: 'modal-md-slide-out', - tabbarPlacement: 'top', - viewTransition: 'md', + pageTransition: 'md', + pageTransitionDelay: 80, popupPopIn: 'popup-md-pop-in', popupPopOut: 'popup-md-pop-out', + tabbarHighlight: true, + tabbarPlacement: 'top', + tabSubPages: true, - type: 'overlay', - mdRipple: true, }); diff --git a/ionic/transitions/md-transition.ts b/ionic/transitions/md-transition.ts index d09d57ee67..f43cddc683 100644 --- a/ionic/transitions/md-transition.ts +++ b/ionic/transitions/md-transition.ts @@ -10,7 +10,6 @@ const SHOW_BACK_BTN_CSS = 'show-back-button'; class MDTransition extends Animation { constructor(navCtrl, opts) { - //opts.renderDelay = 80; super(null, opts); // what direction is the transition going diff --git a/ionic/util/fastdom.ts b/ionic/util/fastdom.ts new file mode 100644 index 0000000000..4038132d95 --- /dev/null +++ b/ionic/util/fastdom.ts @@ -0,0 +1,389 @@ +import {raf} from './dom'; + +/** + * FastDom + * + * Eliminates layout thrashing + * by batching DOM read/write + * interactions. + * + * @author Wilson Page + */ + +/** + * Creates a fresh + * FastDom instance. + * + * @constructor + */ +export function FastDom() { + this.frames = []; + this.lastId = 0; + + // Placing the rAF method + // on the instance allows + // us to replace it with + // a stub for testing. + this.raf = raf; + + this.batch = { + hash: {}, + read: [], + write: [], + mode: null + }; +} + +/** + * Adds a job to the + * read batch and schedules + * a new frame if need be. + * + * @param {Function} fn + * @public + */ +FastDom.prototype.read = function(fn, ctx) { + var job = this.add('read', fn, ctx); + var id = job.id; + + // Add this job to the read queue + this.batch.read.push(job.id); + + // We should *not* schedule a new frame if: + // 1. We're 'reading' + // 2. A frame is already scheduled + var doesntNeedFrame = this.batch.mode === 'reading' + || this.batch.scheduled; + + // If a frame isn't needed, return + if (doesntNeedFrame) return id; + + // Schedule a new + // frame, then return + this.scheduleBatch(); + return id; +}; + +/** + * Adds a job to the + * write batch and schedules + * a new frame if need be. + * + * @param {Function} fn + * @public + */ +FastDom.prototype.write = function(fn, ctx) { + var job = this.add('write', fn, ctx); + var mode = this.batch.mode; + var id = job.id; + + // Push the job id into the queue + this.batch.write.push(job.id); + + // We should *not* schedule a new frame if: + // 1. We are 'writing' + // 2. We are 'reading' + // 3. A frame is already scheduled. + var doesntNeedFrame = mode === 'writing' + || mode === 'reading' + || this.batch.scheduled; + + // If a frame isn't needed, return + if (doesntNeedFrame) return id; + + // Schedule a new + // frame, then return + this.scheduleBatch(); + return id; +}; + +/** + * Defers the given job + * by the number of frames + * specified. + * + * If no frames are given + * then the job is run in + * the next free frame. + * + * @param {Number} frame + * @param {Function} fn + * @public + */ +FastDom.prototype.defer = function(frame, fn, ctx) { + + // Accepts two arguments + if (typeof frame === 'function') { + ctx = fn; + fn = frame; + frame = 1; + } + + var self = this; + var index = frame - 1; + + return this.schedule(index, function() { + self.run({ + fn: fn, + ctx: ctx + }); + }); +}; + +/** + * Clears a scheduled 'read', + * 'write' or 'defer' job. + * + * @param {Number|String} id + * @public + */ +FastDom.prototype.clear = function(id) { + + // Defer jobs are cleared differently + if (typeof id === 'function') { + return this.clearFrame(id); + } + + // Allow ids to be passed as strings + id = Number(id); + + var job = this.batch.hash[id]; + if (!job) return; + + var list = this.batch[job.type]; + var index = list.indexOf(id); + + // Clear references + delete this.batch.hash[id]; + if (~index) list.splice(index, 1); +}; + +/** + * Clears a scheduled frame. + * + * @param {Function} frame + * @private + */ +FastDom.prototype.clearFrame = function(frame) { + var index = this.frames.indexOf(frame); + if (~index) this.frames.splice(index, 1); +}; + +/** + * Schedules a new read/write + * batch if one isn't pending. + * + * @private + */ +FastDom.prototype.scheduleBatch = function() { + var self = this; + + // Schedule batch for next frame + this.schedule(0, function() { + self.batch.scheduled = false; + self.runBatch(); + }); + + // Set flag to indicate + // a frame has been scheduled + this.batch.scheduled = true; +}; + +/** + * Generates a unique + * id for a job. + * + * @return {Number} + * @private + */ +FastDom.prototype.uniqueId = function() { + return ++this.lastId; +}; + +/** + * Calls each job in + * the list passed. + * + * If a context has been + * stored on the function + * then it is used, else the + * current `this` is used. + * + * @param {Array} list + * @private + */ +FastDom.prototype.flush = function(list) { + var id; + + while (id = list.shift()) { + this.run(this.batch.hash[id]); + } +}; + +/** + * Runs any 'read' jobs followed + * by any 'write' jobs. + * + * We run this inside a try catch + * so that if any jobs error, we + * are able to recover and continue + * to flush the batch until it's empty. + * + * @private + */ +FastDom.prototype.runBatch = function() { + try { + + // Set the mode to 'reading', + // then empty all read jobs + this.batch.mode = 'reading'; + this.flush(this.batch.read); + + // Set the mode to 'writing' + // then empty all write jobs + this.batch.mode = 'writing'; + this.flush(this.batch.write); + + this.batch.mode = null; + + } catch (e) { + this.runBatch(); + throw e; + } +}; + +/** + * Adds a new job to + * the given batch. + * + * @param {Array} list + * @param {Function} fn + * @param {Object} ctx + * @returns {Number} id + * @private + */ +FastDom.prototype.add = function(type, fn, ctx) { + var id = this.uniqueId(); + return this.batch.hash[id] = { + id: id, + fn: fn, + ctx: ctx, + type: type + }; +}; + +/** + * Runs a given job. + * + * Applications using FastDom + * have the options of setting + * `fastdom.onError`. + * + * This will catch any + * errors that may throw + * inside callbacks, which + * is useful as often DOM + * nodes have been removed + * since a job was scheduled. + * + * Example: + * + * fastdom.onError = function(e) { + * // Runs when jobs error + * }; + * + * @param {Object} job + * @private + */ +FastDom.prototype.run = function(job){ + var ctx = job.ctx || this; + var fn = job.fn; + + // Clear reference to the job + delete this.batch.hash[job.id]; + + // If no `onError` handler + // has been registered, just + // run the job normally. + if (!this.onError) { + return fn.call(ctx); + } + + // If an `onError` handler + // has been registered, catch + // errors that throw inside + // callbacks, and run the + // handler instead. + try { fn.call(ctx); } catch (e) { + this.onError(e); + } +}; + +/** + * Starts a rAF loop + * to empty the frame queue. + * + * @private + */ +FastDom.prototype.loop = function() { + var self = this; + var raf = this.raf; + + // Don't start more than one loop + if (this.looping) return; + + raf(function frame() { + var fn = self.frames.shift(); + + // If no more frames, + // stop looping + if (!self.frames.length) { + self.looping = false; + + // Otherwise, schedule the + // next frame + } else { + raf(frame); + } + + // Run the frame. Note that + // this may throw an error + // in user code, but all + // fastdom tasks are dealt + // with already so the code + // will continue to iterate + if (fn) fn(); + }); + + this.looping = true; +}; + +/** + * Adds a function to + * a specified index + * of the frame queue. + * + * @param {Number} index + * @param {Function} fn + * @return {Function} + * @private + */ +FastDom.prototype.schedule = function(index, fn) { + + // Make sure this slot + // hasn't already been + // taken. If it has, try + // re-scheduling for the next slot + if (this.frames[index]) { + return this.schedule(index + 1, fn); + } + + // Start the rAF + // loop to empty + // the frame queue + this.loop(); + + // Insert this function into + // the frames queue and return + return this.frames[index] = fn; +};