/* global performance */ (function(exports) { 'use strict'; var debug = false; function log(str) { if (!debug) { return; } console.log('🎶 ', str); } // The naive mode just executes all blocks right away // (useful for performance comparison). var naive = false; var naiveExec = function(block) { block(); return Promise.resolve(); }; var directProtectionWindow = 360; var Scheduler = function() { this._directTracking = new Map(); this._ongoingTransitions = 0; this._queuedTransitions = []; this._flushing = false; this._pendingMutations = []; }; Scheduler.prototype = { // *Direct* blocks should be used for direct manipulation use cases // (touchevents, scrollevents...). // They're exectuted in a requestAnimationFrame block and are protected // from mutations. Request might be cancelled by subsequent direct blocks if // the event loop gets too busy. attachDirect: function(elm, evtType, block) { var tracking = this._directTracking; if (!tracking.get(elm)) { tracking.set(elm, {}); } var tElm = tracking.get(elm); if (!(evtType in tElm)) { tElm[evtType] = { rafID: null, protectionTimeout: null, blocks: [] }; } var tEvt = tElm[evtType]; if (!tEvt.blocks.length) { elm.addEventListener(evtType, this); } tEvt.blocks.push(block); }, detachDirect: function(elm, evtType, block) { var tEvt = this._trackingFor(elm, evtType); if (!tEvt) { return; } if (tEvt.rafID) { window.cancelAnimationFrame(tEvt.rafID); } if (tEvt.protectionTimeout) { clearTimeout(tEvt.protectionTimeout); tEvt.protectionTimeout = null; this._stopProtectingDirect(); } var toRemove = tEvt.blocks.indexOf(block); if (toRemove === -1) { log('wrong detach block'); return; } tEvt.blocks.splice(toRemove, 1); if (!tEvt.blocks.length) { elm.removeEventListener(evtType, this); var tElm = this._directTracking.get(elm); delete tElm[evtType]; if (Object.keys(tElm).length === 0) { this._directTracking.delete(elm); } } }, _trackingFor: function(elm, evtType) { var tracking = this._directTracking; if (!tracking.get(elm)) { log('wrong detach element'); return; } var tElm = tracking.get(elm); if (!(evtType in tElm)) { log('wrong detach event'); return; } return tElm[evtType]; }, _shouldProtectDirect: function() { var tracking = this._directTracking; for (var elm of tracking.keys()) { var tElm = tracking.get(elm); for (var evtType in tElm) { var tEvt = tElm[evtType]; if (tEvt.protectionTimeout !== null) { return true; } } } return false; }, _stopProtectingDirect: function() { this._dequeueTransitions(); this._flushMutations(); if (debug) { console.timeEnd('protecting'); } }, handleEvent: function(evt) { var tEvt = this._trackingFor(evt.currentTarget, evt.type); if (!tEvt) { return; } if (naive) { tEvt.blocks.forEach(function(block) { block(evt); }); return; } if (tEvt.protectionTimeout) { clearTimeout(tEvt.protectionTimeout); } else { if (debug) { console.time('protecting'); } } tEvt.protectionTimeout = setTimeout((function() { tEvt.protectionTimeout = null; this._stopProtectingDirect(); }).bind(this), directProtectionWindow); if (tEvt.rafID) { window.cancelAnimationFrame(tEvt.rafID); } tEvt.rafID = window.requestAnimationFrame(function() { tEvt.blocks.forEach(function(block) { var startDate; if (debug) { startDate = performance.now(); } block(evt); if (debug) { var blockDuration = performance.now() - startDate; if (blockDuration > 16) { log('Direct block took more than a frame (' + blockDuration.toString() + 'ms)'); } } }); }); }, // *Feedbacks* blocks have a built in 'transitionend' wait mechanism. // They're protected from mutation and will be delayed during a mutation // flush. // They have the same priority as `direct` blocks. // // -> Returns a promise fullfilled at the end of the transition for chaining feedback: function(block, elm, evt, timeout) { return this._transition(block, true, elm, evt, timeout); }, // *Transitions* blocks have a built in 'transitionend' wait mechanism. // They're protected from mutation and will be delayed during a mutation // flush. // They will also be delayed by `direct` blocks. // // -> Returns a promise fullfilled at the end of the transition for chaining transition: function(block, elm, evt, timeout) { return this._transition(block, false, elm, evt, timeout); }, _transition: function(block, feedback, elm, evt, timeout) { if (naive) { return naiveExec(block); } timeout = timeout || 500; return new Promise((function(resolve, reject) { var content = (function() { this._ongoingTransitions++; block(); if (!elm || !evt) { this._ongoingTransitions--; resolve(); return; } if (debug) { console.time('animating'); } var finishTimeout; var done = (function() { clearTimeout(finishTimeout); elm.removeEventListener(evt, done); this._ongoingTransitions--; if (debug) { console.timeEnd('animating'); } if (this._ongoingTransitions === 0) { setTimeout(this._flushMutations.bind(this)); } resolve(); }).bind(this); elm.addEventListener(evt, done); finishTimeout = setTimeout(function() { done(); log('Transition block saved by a timeout of ' + timeout + ' ~ ' + elm.style.transition); }, timeout); }).bind(this); if (this._flushing || (this._shouldProtectDirect() && !feedback)) { this._queuedTransitions.push(content); } else { content(); } }).bind(this)); }, _dequeueTransitions: function() { if (this._queuedTransitions.length === 0) { return; } if (this._flushing || this._shouldProtectDirect()) { return; } var transitions = this._queuedTransitions; transitions.forEach(function(transition) { transition(); }); this._queuedTransitions = []; }, // *Mutations* blocks should be used to write to the DOM or perform // actions requiring a reflow that are not direct manipulations. // We shoud always aim for the document to be almost visually identical // _before_ and _after_ a mutation block. // Any big change in layout/size will cause a flash/jump. // // -> Returns a promise fullfilled after the reflow for chaining mutation: function(block) { if (naive) { return naiveExec(block); } return new Promise((function(resolve, reject) { if (this._shouldProtectDirect() || this._ongoingTransitions > 0) { this._pendingMutations.push({ block: block, resolve: resolve }); } else { Promise.resolve() .then(block) .then(resolve); } }).bind(this)); }, _flushMutations: function() { if (this._pendingMutations.length === 0) { return; } if (this._shouldProtectDirect() || this._ongoingTransitions > 0) { return; } this._flushing = true; var fulfilments = this._pendingMutations .map(function(obj) { return obj.resolve; }); var mutations = this._pendingMutations; mutations.forEach(function(mutation) { mutation.block(); }); this._pendingMutations = []; this._flushing = false; fulfilments.forEach(function(resolve) { resolve(); }); this._dequeueTransitions(); } }; exports.DomScheduler = Scheduler; // We only ever want there to be // one instance of the scheduler if (!exports.scheduler || typeof exports.scheduler.mutation !== 'function') Object.defineProperty(exports, 'scheduler', {value:new Scheduler(),configurable:true,writable:true}); })(window);