diff --git a/ionic/animations/animation.ts b/ionic/animations/animation.ts index bd134050ac..e894f17dde 100644 --- a/ionic/animations/animation.ts +++ b/ionic/animations/animation.ts @@ -1,6 +1,5 @@ -import {CSS} from '../util/dom'; +import {CSS, rafFrames} from '../util/dom'; import {extend} from '../util/util'; -import {FastDom} from '../util/fastdom'; /** @@ -25,9 +24,8 @@ import {FastDom} from '../util/fastdom'; export class Animation { - constructor(ele, opts={}, fastdom=null) { + constructor(ele, opts={}) { this.reset(); - this._fastdom = fastdom; this._opts = extend({ renderDelay: 16 @@ -259,12 +257,7 @@ export class Animation { 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 - if (this._fastdom) { - this._fastdom.write(kickoff); - - } else { - setTimeout(kickoff, this._opts.renderDelay); - } + rafFrames(this._opts.renderDelay / 16, kickoff); } else { // no need to render everything in there place before animating in diff --git a/ionic/components/app/app.ts b/ionic/components/app/app.ts index 95b73b3579..c7a9dd4bf4 100644 --- a/ionic/components/app/app.ts +++ b/ionic/components/app/app.ts @@ -1,5 +1,6 @@ import {Title} from 'angular2/angular2'; +import {rafFrames} from '../../util/dom'; import {ClickBlock} from '../../util/click-block'; import {ScrollTo} from '../../animations/scroll-to'; @@ -10,9 +11,8 @@ import {ScrollTo} from '../../animations/scroll-to'; */ export class IonicApp { - constructor(config, fastdom) { + constructor(config) { this._config = config; - this._fastdom = fastdom; this._titleSrv = new Title(); this._title = ''; this._disTime = 0; @@ -29,7 +29,7 @@ export class IonicApp { setTitle(val) { if (val !== this._title) { this._title = val; - this._fastdom.defer(4, () => { + rafFrames(4, () => { this._titleSrv.setTitle(this._title); }); } diff --git a/ionic/components/tap-click/activator.ts b/ionic/components/tap-click/activator.ts index 48eff4f949..f9cdfada86 100644 --- a/ionic/components/tap-click/activator.ts +++ b/ionic/components/tap-click/activator.ts @@ -1,11 +1,10 @@ -import {raf} from '../../util/dom'; +import {raf, rafFrames} from '../../util/dom'; export class Activator { - constructor(app, config, fastdom) { + constructor(app, config) { this.app = app; - this.fastdom = fastdom; this.queue = []; this.active = []; this.clearStateDefers = 5; @@ -27,7 +26,7 @@ export class Activator { // queue to have this element activated this.queue.push(activatableEle); - this.fastdom.write(() => { + rafFrames(2, () => { let activatableEle; for (let i = 0; i < this.queue.length; i++) { activatableEle = this.queue[i]; @@ -44,7 +43,7 @@ export class Activator { upAction() { // the user was pressing down, then just let up - this.fastdom.defer(this.clearStateDefers, () => { + rafFrames(this.clearStateDefers, () => { this.clearState(); }); } @@ -52,17 +51,17 @@ export class Activator { clearState() { // all states should return to normal - if ((!this.app.isEnabled() || this.app.isTransitioning()) && this.clearAttempt < 100) { + if ((!this.app.isEnabled() || this.app.isTransitioning())) { // the app is actively disabled, so don't bother deactivating anything. // this makes it easier on the GPU so it doesn't have to redraw any // buttons during a transition. This will retry in XX milliseconds. - ++this.clearAttempt; - this.upAction(); + setTimeout(() => { + this.clearState(); + }, 600); } else { // not actively transitioning, good to deactivate any elements this.deactivate(); - this.clearAttempt = 0; } } @@ -70,7 +69,7 @@ export class Activator { // remove the active class from all active elements this.queue = []; - this.fastdom.write(() => { + rafFrames(2, () => { for (let i = 0; i < this.active.length; i++) { this.active[i].classList.remove(this.activatedClass); } diff --git a/ionic/components/tap-click/ripple.ts b/ionic/components/tap-click/ripple.ts index c4a4942a56..684aeb02f0 100644 --- a/ionic/components/tap-click/ripple.ts +++ b/ionic/components/tap-click/ripple.ts @@ -1,12 +1,12 @@ import {Activator} from './activator'; import {Animation} from '../../animations/animation'; -import {raf} from '../../util/dom'; +import {raf, rafFrames} from '../../util/dom'; export class RippleActivator extends Activator { - constructor(app, config, fastdom) { - super(app, config, fastdom); + constructor(app, config) { + super(app, config); this.expands = {}; this.fades = {}; @@ -18,16 +18,12 @@ export class RippleActivator extends Activator { // create a new ripple element this.expandSpeed = EXPAND_DOWN_PLAYBACK_RATE; - this.fastdom.defer(2, () => { + rafFrames(2, () => { + let clientRect = activatableEle.getBoundingClientRect(); - this.fastdom.read(() => { - let clientRect = activatableEle.getBoundingClientRect(); - - this.fastdom.write(() => { - this.createRipple(activatableEle, pointerX, pointerY, clientRect); - }); + raf(() => { + this.createRipple(activatableEle, pointerX, pointerY, clientRect); }); - }); } } @@ -60,7 +56,7 @@ export class RippleActivator extends Activator { .duration(FADE_OUT_DURATION) .playbackRate(1) .onFinish(() => { - this.fastdom.write(() => { + raf(() => { this.fades[rippleId].dispose(true); delete this.fades[rippleId]; }); @@ -87,7 +83,7 @@ export class RippleActivator extends Activator { this.expandSpeed = 1; - this.fastdom.defer(4, () => { + rafFrames(4, () => { this.next(); }); } diff --git a/ionic/components/tap-click/tap-click.ts b/ionic/components/tap-click/tap-click.ts index 9045487b35..0b813e3045 100644 --- a/ionic/components/tap-click/tap-click.ts +++ b/ionic/components/tap-click/tap-click.ts @@ -19,16 +19,16 @@ let doc = null; /** * @private */ -export function initTapClick(windowInstance, documentInstance, appInstance, config, fastdom) { +export function initTapClick(windowInstance, documentInstance, appInstance, config) { win = windowInstance; doc = documentInstance; app = appInstance; if (config.get('activator') == 'ripple') { - activator = new RippleActivator(app, config, fastdom); + activator = new RippleActivator(app, config); } else if (config.get('activator') == 'highlight') { - activator = new Activator(app, config, fastdom); + activator = new Activator(app, config); } isTapPolyfill = (config.get('tapPolyfill') === true); diff --git a/ionic/config/bootstrap.ts b/ionic/config/bootstrap.ts index 598d4a6ee7..244722880d 100644 --- a/ionic/config/bootstrap.ts +++ b/ionic/config/bootstrap.ts @@ -6,7 +6,6 @@ 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'; @@ -22,8 +21,6 @@ import * as dom from '../util/dom'; export function ionicProviders(args={}) { - let fastdom = new FastDom(); - let platform = new Platform(); let navRegistry = new NavRegistry(args.pages); @@ -39,10 +36,10 @@ export function ionicProviders(args={}) { platform.load(); config.setPlatform(platform); - let app = new IonicApp(config, fastdom); + let app = new IonicApp(config); let events = new Events(); - initTapClick(window, document, app, config, fastdom); + initTapClick(window, document, app, config); let featureDetect = new FeatureDetect(); setupDom(window, document, config, platform, featureDetect); @@ -52,7 +49,6 @@ 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/util/dom.ts b/ionic/util/dom.ts index e5d3bc35dd..ed8736d5b3 100644 --- a/ionic/util/dom.ts +++ b/ionic/util/dom.ts @@ -7,15 +7,15 @@ const nativeCancelRaf = window.cancelAnimationFrame || window.webkitCancelRequestAnimationFrame; export const raf = nativeRaf || function(callback) { - let timeCurrent = (new Date()).getTime(), - timeDelta; + let timeCurrent = (new Date()).getTime(), + timeDelta; - /* Dynamically set delay on a per-tick basis to match 60fps. */ - /* Technique by Erik Moller. MIT license: https://gist.github.com/paulirish/1579671 */ - timeDelta = Math.max(0, 16 - (timeCurrent - timeLast)); - timeLast = timeCurrent + timeDelta; + /* Dynamically set delay on a per-tick basis to match 60fps. */ + /* Technique by Erik Moller. MIT license: https://gist.github.com/paulirish/1579671 */ + timeDelta = Math.max(0, 16 - (timeCurrent - timeLast)); + timeLast = timeCurrent + timeDelta; - return setTimeout(function() { callback(timeCurrent + timeDelta); }, timeDelta); + return setTimeout(function() { callback(timeCurrent + timeDelta); }, timeDelta); } export const rafCancel = nativeRaf ? nativeCancelRaf : function(id) { @@ -26,6 +26,19 @@ export function rafPromise() { return new Promise(resolve => raf(resolve)); } +export function rafFrames(framesToWait, callback) { + framesToWait = Math.ceil(framesToWait); + + if (framesToWait < 2) { + raf(callback); + + } else { + setTimeout(() => { + raf(callback); + }, (framesToWait - 1) * 17); + } +} + export let CSS = {}; (function() { // transform diff --git a/ionic/util/fastdom.ts b/ionic/util/fastdom.ts deleted file mode 100644 index 4038132d95..0000000000 --- a/ionic/util/fastdom.ts +++ /dev/null @@ -1,389 +0,0 @@ -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; -};