Merge branch 'master' into list-border-refactor

This commit is contained in:
Brandy Carney
2015-11-10 20:27:27 -05:00
12 changed files with 560 additions and 136 deletions

View File

@ -1,5 +1,6 @@
import {CSS} from '../util/dom'; import {CSS} from '../util/dom';
import {extend} from '../util/util'; import {extend} from '../util/util';
import {FastDom} from '../util/fastdom';
/** /**
@ -24,8 +25,10 @@ import {extend} from '../util/util';
export class Animation { export class Animation {
constructor(ele, opts={}) { constructor(ele, opts={}, fastdom=null) {
this.reset(); this.reset();
this._fastdom = fastdom;
this._opts = extend({ this._opts = extend({
renderDelay: 16 renderDelay: 16
}, opts); }, 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 // begin each animation when everything is rendered in their starting point
// give the browser some time to render everything in place before starting // 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 { } else {
// no need to render everything in there place before animating in // no need to render everything in there place before animating in
@ -503,14 +510,19 @@ export class Animation {
return copy(new Animation(), this); return copy(new Animation(), this);
} }
dispose() { dispose(removeElement) {
let i; let i;
for (i = 0; i < this._chld.length; 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++) { 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(); this.reset();
@ -607,7 +619,7 @@ class Animate {
self.ani = self.ani.onfinish = null; self.ani = self.ani.onfinish = null;
done && done(); done && done();
} }
}; };
} }

View File

@ -2,7 +2,6 @@ import {Title} from 'angular2/angular2';
import {ClickBlock} from '../../util/click-block'; import {ClickBlock} from '../../util/click-block';
import {ScrollTo} from '../../animations/scroll-to'; 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 { export class IonicApp {
/** constructor(fastdom) {
* TODO this._fastdom = fastdom;
*/ this._titleSrv = new Title();
constructor() { this._title = '';
this._title = new Title();
this._disTime = 0; this._disTime = 0;
this._trnsTime = 0; this._trnsTime = 0;
@ -28,11 +26,12 @@ export class IonicApp {
* @param {string} val Value to set the document title to. * @param {string} val Value to set the document title to.
*/ */
setTitle(val) { setTitle(val) {
this._title.setTitle(val); if (val !== this._title) {
} this._title = val;
this._fastdom.defer(4, () => {
getTitle() { this._titleSrv.setTitle(this._title);
return this._title.getTitle(val); });
}
} }
/** /**

View File

@ -436,7 +436,7 @@ export class NavController extends Ion {
} }
if (!opts.animation) { if (!opts.animation) {
opts.animation = this.config.get('viewTransition'); opts.animation = this.config.get('pageTransition');
} }
if (this.config.get('animate') === false) { if (this.config.get('animate') === false) {
opts.animate = false; opts.animate = false;
@ -468,6 +468,7 @@ export class NavController extends Ion {
leavingView.state = STAGED_LEAVING_STATE; leavingView.state = STAGED_LEAVING_STATE;
// init the transition animation // init the transition animation
opts.renderDelay = this.config.get('pageTransitionDelay');
let transAnimation = Transition.create(this, opts); let transAnimation = Transition.create(this, opts);
if (opts.animate === false) { if (opts.animate === false) {
// force it to not animate the elements, just apply the "to" styles // force it to not animate the elements, just apply the "to" styles

View File

@ -4,6 +4,7 @@ import {Ion} from '../ion';
import {IonicApp} from '../app/app'; import {IonicApp} from '../app/app';
import {Attr} from '../app/id'; import {Attr} from '../app/id';
import {Config} from '../../config/config'; import {Config} from '../../config/config';
import {Platform} from '../../platform/platform';
import {ViewController} from '../nav/view-controller'; import {ViewController} from '../nav/view-controller';
import {ConfigComponent} from '../../config/decorators'; import {ConfigComponent} from '../../config/decorators';
import {Icon} from '../icon/icon'; import {Icon} from '../icon/icon';
@ -107,7 +108,8 @@ export class Tabs extends Ion {
app: IonicApp, app: IonicApp,
config: Config, config: Config,
elementRef: ElementRef, elementRef: ElementRef,
@Optional() viewCtrl: ViewController @Optional() viewCtrl: ViewController,
private platform: Platform
) { ) {
super(elementRef, config); super(elementRef, config);
this.app = app; 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 * @private
*/ */
@ -305,7 +316,7 @@ class TabButton extends Ion {
}) })
class TabHighlight { class TabHighlight {
constructor(@Host() tabs: Tabs, config: Config, elementRef: ElementRef) { constructor(@Host() tabs: Tabs, config: Config, elementRef: ElementRef) {
if (config.get('mode') === 'md') { if (config.get('tabbarHighlight')) {
tabs.highlight = this; tabs.highlight = this;
this.elementRef = elementRef; this.elementRef = elementRef;
} }
@ -320,7 +331,7 @@ class TabHighlight {
this.init = true; this.init = true;
setTimeout(() => { setTimeout(() => {
ele.classList.add('animate'); ele.classList.add('animate');
}, 64) }, 64);
} }
} }

View File

@ -3,11 +3,12 @@ import {raf} from '../../util/dom';
export class Activator { export class Activator {
constructor(app, config) { constructor(app, config, fastdom) {
this.app = app; this.app = app;
this.fastdom = fastdom;
this.queue = []; this.queue = [];
this.active = []; this.active = [];
this.clearStateTimeout = 80; this.clearStateDefers = 5;
this.clearAttempt = 0; this.clearAttempt = 0;
this.activatedClass = config.get('activatedClass') || 'activated'; this.activatedClass = config.get('activatedClass') || 'activated';
this.x = 0; this.x = 0;
@ -17,7 +18,7 @@ export class Activator {
downAction(ev, activatableEle, pointerX, pointerY, callback) { downAction(ev, activatableEle, pointerX, pointerY, callback) {
// the user just pressed down // the user just pressed down
if (this.disableActivated(ev)) return; if (this.disableActivated(ev)) return false;
// remember where they pressed // remember where they pressed
this.x = pointerX; this.x = pointerX;
@ -26,7 +27,7 @@ export class Activator {
// queue to have this element activated // queue to have this element activated
this.queue.push(activatableEle); this.queue.push(activatableEle);
raf(() => { this.fastdom.write(() => {
let activatableEle; let activatableEle;
for (let i = 0; i < this.queue.length; i++) { for (let i = 0; i < this.queue.length; i++) {
activatableEle = this.queue[i]; activatableEle = this.queue[i];
@ -37,13 +38,15 @@ export class Activator {
} }
this.queue = []; this.queue = [];
}); });
return true;
} }
upAction() { upAction() {
// the user was pressing down, then just let up // the user was pressing down, then just let up
setTimeout(() => { this.fastdom.defer(this.clearStateDefers, () => {
this.clearState(); this.clearState();
}, this.clearStateTimeout); });
} }
clearState() { clearState() {
@ -65,11 +68,14 @@ export class Activator {
deactivate() { deactivate() {
// remove the active class from all active elements // 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.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) { disableActivated(ev) {

View File

@ -1,23 +1,38 @@
import {Activator} from './activator'; import {Activator} from './activator';
import {removeElement, raf} from '../../util/dom';
import {Animation} from '../../animations/animation'; import {Animation} from '../../animations/animation';
import {raf} from '../../util/dom';
export class RippleActivator extends Activator { export class RippleActivator extends Activator {
constructor(app, config) { constructor(app, config, fastdom) {
super(app, config); super(app, config, fastdom);
this.ripples = {};
this.expands = {};
this.fades = {};
this.expandSpeed = null;
} }
downAction(ev, activatableEle, pointerX, pointerY) { 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 this.fastdom.write(() => {
let clientRect = activatableEle.getBoundingClientRect(); this.createRipple(activatableEle, pointerX, pointerY, clientRect);
});
});
});
}
}
createRipple(activatableEle, pointerX, pointerY, clientRect) {
let clientPointerX = (pointerX - clientRect.left); let clientPointerX = (pointerX - clientRect.left);
let clientPointerY = (pointerY - clientRect.top); 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 duration = (1000 * Math.sqrt(radius / TOUCH_DOWN_ACCEL) + 0.5);
let rippleEle = document.createElement('md-ripple'); let rippleEle = document.createElement('md-ripple');
let rippleId = Date.now();
let eleStyle = rippleEle.style; let eleStyle = rippleEle.style;
eleStyle.width = eleStyle.height = diameter + 'px'; eleStyle.width = eleStyle.height = diameter + 'px';
eleStyle.marginTop = eleStyle.marginLeft = -(diameter / 2) + 'px'; eleStyle.marginTop = eleStyle.marginLeft = -(diameter / 2) + 'px';
@ -37,96 +53,74 @@ export class RippleActivator extends Activator {
activatableEle.appendChild(rippleEle); activatableEle.appendChild(rippleEle);
let ripple = this.ripples[Date.now()] = { // create the animation for the fade out, but don't start it yet
ele: rippleEle, this.fades[rippleId] = new Animation(rippleEle, {renderDelay: 0});
radius: radius, this.fades[rippleId]
duration: duration .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 // expand the circle from the users starting point
// start slow, and when they let up, then speed up the animation // start slow, and when they let up, then speed up the animation
ripple.expand = new Animation(rippleEle, {renderDelay: 0}); this.expands[rippleId] = new Animation(rippleEle, {renderDelay: 0});
ripple.expand this.expands[rippleId]
.fromTo('scale', '0.001', '1') .fromTo('scale', '0.001', '1')
.duration(duration) .duration(duration)
.playbackRate(EXPAND_DOWN_PLAYBACK_RATE) .playbackRate(this.expandSpeed)
.onFinish(()=> { .onFinish(()=> {
// finished expanding this.expands[rippleId].dispose();
ripple.expand && ripple.expand.dispose(); delete this.expands[rippleId];
ripple.expand = null;
ripple.expanded = true;
this.next(); this.next();
}) })
.play(); .play();
this.next();
} }
upAction(forceFadeOut) { upAction() {
this.deactivate(); this.deactivate();
let rippleId, ripple; this.expandSpeed = 1;
for (rippleId in this.ripples) {
ripple = this.ripples[rippleId];
if (!ripple.fade || forceFadeOut) { this.fastdom.defer(4, () => {
// ripple has not been let up yet this.next();
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();
}); 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) { } else if (!this.fades[rippleId].isPlaying) {
let rippleId, ripple; this.fades[rippleId].isPlaying = true;
for (rippleId in this.ripples) { this.fades[rippleId].play();
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();
});
} }
} }
} }
clearState() { clearState() {
this.deactivate(); this.deactivate();
this.next(true); this.next();
}
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];
}
}
} }
} }
@ -134,4 +128,4 @@ export class RippleActivator extends Activator {
const TOUCH_DOWN_ACCEL = 512; const TOUCH_DOWN_ACCEL = 512;
const EXPAND_DOWN_PLAYBACK_RATE = 0.35; const EXPAND_DOWN_PLAYBACK_RATE = 0.35;
const EXPAND_OUT_PLAYBACK_RATE = 3; const EXPAND_OUT_PLAYBACK_RATE = 3;
const OPACITY_OUT_DURATION = 750; const FADE_OUT_DURATION = 700;

View File

@ -12,27 +12,29 @@ let disableNativeClickAmount = 3000;
let activator = null; let activator = null;
let isTapPolyfill = false; let isTapPolyfill = false;
let app = null; let app = null;
let config = null;
let win = null; let win = null;
let doc = null; let doc = null;
export function initTapClick(windowInstance, documentInstance, appInstance, configInstance) { export function initTapClick(windowInstance, documentInstance, appInstance, config, fastdom) {
win = windowInstance; win = windowInstance;
doc = documentInstance; doc = documentInstance;
app = appInstance; 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); isTapPolyfill = (config.get('tapPolyfill') === true);
addListener('click', click, true); addListener('click', click, true);
if (isTapPolyfill) { addListener('touchstart', touchStart);
addListener('touchstart', touchStart); addListener('touchend', touchEnd);
addListener('touchend', touchEnd); addListener('touchcancel', touchCancel);
addListener('touchcancel', touchCancel);
}
addListener('mousedown', mouseDown, true); addListener('mousedown', mouseDown, true);
addListener('mouseup', mouseUp, true); addListener('mouseup', mouseUp, true);
@ -47,7 +49,7 @@ function touchStart(ev) {
function touchEnd(ev) { function touchEnd(ev) {
touchAction(); touchAction();
if (startCoord && app.isEnabled()) { if (isTapPolyfill && startCoord && app.isEnabled()) {
let endCoord = pointerCoord(ev); let endCoord = pointerCoord(ev);
if (!hasPointerMoved(pointerTolerance, startCoord, endCoord)) { if (!hasPointerMoved(pointerTolerance, startCoord, endCoord)) {
@ -100,8 +102,8 @@ function pointerStart(ev) {
startCoord = pointerCoord(ev); startCoord = pointerCoord(ev);
let now = Date.now(); let now = Date.now();
if (lastActivated + 100 < now) { if (lastActivated + 150 < now) {
activator.downAction(ev, activatableEle, startCoord.x, startCoord.y); activator && activator.downAction(ev, activatableEle, startCoord.x, startCoord.y);
lastActivated = now; lastActivated = now;
} }
@ -114,7 +116,7 @@ function pointerStart(ev) {
function pointerEnd(ev) { function pointerEnd(ev) {
moveListeners(false); moveListeners(false);
activator.upAction(); activator && activator.upAction();
} }
function pointerMove(ev) { function pointerMove(ev) {
@ -127,20 +129,22 @@ function pointerMove(ev) {
function pointerCancel(ev) { function pointerCancel(ev) {
console.debug('pointerCancel from', ev.type); console.debug('pointerCancel from', ev.type);
activator.clearState(); activator && activator.clearState();
moveListeners(false); moveListeners(false);
} }
function moveListeners(shouldAdd) { function moveListeners(shouldAdd) {
if (isTapPolyfill) {
removeListener('touchmove', pointerMove);
}
removeListener('mousemove', pointerMove);
if (shouldAdd) { if (shouldAdd) {
if (isTapPolyfill) { if (isTapPolyfill) {
addListener('touchmove', pointerMove); addListener('touchmove', pointerMove);
} }
addListener('mousemove', 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); console.debug('click prevent', preventReason);
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
} else {
activator.upAction();
} }
} }

View File

@ -6,6 +6,7 @@ import {IonicApp} from '../components/app/app';
import {Config} from './config'; import {Config} from './config';
import {Platform} from '../platform/platform'; import {Platform} from '../platform/platform';
import {OverlayController} from '../components/overlay/overlay-controller'; import {OverlayController} from '../components/overlay/overlay-controller';
import {FastDom} from '../util/fastdom';
import {Form} from '../util/form'; import {Form} from '../util/form';
import {Keyboard} from '../util/keyboard'; import {Keyboard} from '../util/keyboard';
import {ActionSheet} from '../components/action-sheet/action-sheet'; import {ActionSheet} from '../components/action-sheet/action-sheet';
@ -21,7 +22,9 @@ import * as dom from '../util/dom';
export function ionicProviders(args) { export function ionicProviders(args) {
let app = new IonicApp(); let fastdom = new FastDom();
let app = new IonicApp(fastdom);
let platform = new Platform(); let platform = new Platform();
let navRegistry = new NavRegistry(args.pages); let navRegistry = new NavRegistry(args.pages);
@ -38,7 +41,7 @@ export function ionicProviders(args) {
config.setPlatform(platform); config.setPlatform(platform);
let events = new Events(); let events = new Events();
initTapClick(window, document, app, config); initTapClick(window, document, app, config, fastdom);
let featureDetect = new FeatureDetect(); let featureDetect = new FeatureDetect();
setupDom(window, document, config, platform, featureDetect); setupDom(window, document, config, platform, featureDetect);
@ -48,6 +51,7 @@ export function ionicProviders(args) {
platform.prepareReady(config); platform.prepareReady(config);
return [ return [
provide(FastDom, {useValue: fastdom}),
provide(IonicApp, {useValue: app}), provide(IonicApp, {useValue: app}),
provide(Config, {useValue: config}), provide(Config, {useValue: config}),
provide(Platform, {useValue: platform}), provide(Platform, {useValue: platform}),

View File

@ -22,7 +22,7 @@ import {isObject, isDefined, isFunction, isArray, extend} from '../util/util';
* modalEnter: 'modal-slide-in', * modalEnter: 'modal-slide-in',
* modalLeave: 'modal-slide-out', * modalLeave: 'modal-slide-out',
* tabbarPlacement: 'bottom', * tabbarPlacement: 'bottom',
* viewTransition: 'ios', * pageTransition: 'ios',
* } * }
* }) * })
* ``` * ```

View File

@ -4,6 +4,7 @@ import {Config} from './config';
// iOS Mode Settings // iOS Mode Settings
Config.setModeConfig('ios', { Config.setModeConfig('ios', {
activator: 'highlight',
actionSheetEnter: 'action-sheet-slide-in', actionSheetEnter: 'action-sheet-slide-in',
actionSheetLeave: 'action-sheet-slide-out', actionSheetLeave: 'action-sheet-slide-out',
@ -18,16 +19,19 @@ Config.setModeConfig('ios', {
modalEnter: 'modal-slide-in', modalEnter: 'modal-slide-in',
modalLeave: 'modal-slide-out', modalLeave: 'modal-slide-out',
tabbarPlacement: 'bottom', pageTransition: 'ios',
viewTransition: 'ios', pageTransitionDelay: 16,
popupPopIn: 'popup-pop-in', popupPopIn: 'popup-pop-in',
popupPopOut: 'popup-pop-out', popupPopOut: 'popup-pop-out',
tabbarPlacement: 'bottom',
}); });
// Material Design Mode Settings // Material Design Mode Settings
Config.setModeConfig('md', { Config.setModeConfig('md', {
activator: 'ripple',
actionSheetEnter: 'action-sheet-md-slide-in', actionSheetEnter: 'action-sheet-md-slide-in',
actionSheetLeave: 'action-sheet-md-slide-out', actionSheetLeave: 'action-sheet-md-slide-out',
@ -39,16 +43,19 @@ Config.setModeConfig('md', {
iconMode: 'md', iconMode: 'md',
type: 'overlay',
modalEnter: 'modal-md-slide-in', modalEnter: 'modal-md-slide-in',
modalLeave: 'modal-md-slide-out', modalLeave: 'modal-md-slide-out',
tabbarPlacement: 'top', pageTransition: 'md',
viewTransition: 'md', pageTransitionDelay: 80,
popupPopIn: 'popup-md-pop-in', popupPopIn: 'popup-md-pop-in',
popupPopOut: 'popup-md-pop-out', popupPopOut: 'popup-md-pop-out',
tabbarHighlight: true,
tabbarPlacement: 'top',
tabSubPages: true, tabSubPages: true,
type: 'overlay',
mdRipple: true,
}); });

View File

@ -10,7 +10,6 @@ const SHOW_BACK_BTN_CSS = 'show-back-button';
class MDTransition extends Animation { class MDTransition extends Animation {
constructor(navCtrl, opts) { constructor(navCtrl, opts) {
//opts.renderDelay = 80;
super(null, opts); super(null, opts);
// what direction is the transition going // what direction is the transition going

389
ionic/util/fastdom.ts Normal file
View File

@ -0,0 +1,389 @@
import {raf} from './dom';
/**
* FastDom
*
* Eliminates layout thrashing
* by batching DOM read/write
* interactions.
*
* @author Wilson Page <wilsonpage@me.com>
*/
/**
* 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;
};