UNPKG

ids-enterprise

Version:

Infor Design System (IDS) Enterprise Components for the web

403 lines (340 loc) • 11.6 kB
/** * Gets an accurate timestamp from * @private * @returns {number} a current timestamp */ function timestamp() { return window.performance && window.performance.now ? window.performance.now() : new Date().getTime(); } /** * RenderLoop Queue items * @param {object} opts options * @returns {this} RenderLoopItem */ function RenderLoopItem(opts) { // Either ID or a duration is required this.id = opts.id; this.duration = opts.duration || -1; if (this.duration < 1 && (typeof this.id !== 'string' || !this.id.length)) { throw new Error('cannot build a RenderLoopItem with no duration and no namespace'); } this.updateDuration = opts.updateDuration || 1; // functions this.setFuncs(opts); // internal state this.paused = false; this.elapsedTime = 0; this.startTime = timestamp(); return this; } RenderLoopItem.prototype = { /** * @private * @param {object} opts incoming settings */ setFuncs(opts) { if (typeof opts.updateCallback !== 'function' && typeof opts.timeoutCallback !== 'function') { throw new Error('cannot register callback to RenderLoop because callback is not a function'); } if (typeof opts.updateCallback === 'function') { this.updateCallback = opts.updateCallback; } if (typeof opts.timeoutCallback === 'function') { this.timeoutCallback = opts.timeoutCallback; } }, pause() { this.paused = true; }, resume() { this.paused = false; }, destroy() { this.doRemoveOnNextTick = true; } }; /** * Sets up a timed rendering loop that can be used for controlling animations * globally in an application that implements Soho. * @constructor */ function RenderLoop() { this.items = []; this.element = $('body'); return this; } RenderLoop.prototype = { /** * Start the entire render loop * @returns {void} */ start() { this.doLoop = true; this.startTime = timestamp(); const self = this; let last = timestamp(); let now; let deltaTime; function tick() { // Don't continue if the loop is stopped externally if (!self.doLoop) { return; } now = timestamp(); deltaTime = (now - last) / 1000; // Iterate through each item stored in the queue and "update" each one. // In some cases, items will be removed from the queue automatically. // In some cases, `update` events will be triggered on loop items, if they are // ready to be externally updated. self.items.forEach((loopItem) => { // Remove if we've set the `doRemoveOnNextTick` flag. if (loopItem.doRemoveOnNextTick) { self.remove(loopItem); return; } // Add to elapsedTime if (!loopItem.paused) { loopItem.elapsedTime++; } // Check duration if (typeof loopItem.duration === 'number' && loopItem.duration > -1) { if (!loopItem.startTime) { loopItem.startTime = now; } if (loopItem.elapsedTime >= loopItem.duration) { loopItem.destroy(); return; } } // Call the updateCallback, if applicable. let modifiedArgs; if (typeof loopItem.updateCallback === 'function') { // If this item doesn't update on each tick, simply count down. // Otherwise, call the update function if (loopItem.updateDuration && loopItem.updateDuration > 1) { if (isNaN(loopItem.timeUntilNextUpdate)) { loopItem.timeUntilNextUpdate = loopItem.updateDuration; } if (loopItem.timeUntilNextUpdate > 0) { --loopItem.timeUntilNextUpdate; return; } } // Arguments produced for the updateCallback contain: // [0] the current RenderLoopItem // [1] overall timing values for the RenderLoop modifiedArgs = [loopItem, { last, delta: deltaTime, now }]; loopItem.updateCallback.apply(null, modifiedArgs); } }); // Continue the loop last = now; requestAnimationFrame(tick); } tick(); }, /** * Stops the entire render loop * @returns {void} */ stop() { this.doLoop = false; }, /** * @returns {number} amount of time that has passed since the RenderLoop was started. */ totalDuration() { return timestamp() - this.startTime; }, /** * External method for getting the callback queue contents * @returns {array} list of internal RenderLoopItems */ queue() { return this.items; }, /** * @private * @param {function} updateCallback - (can also be the "updateCallback" function) * @param {function} [timeoutCallback] callback function that gets fired at * the end of this item's lifecycle * @param {number} [duration] the amount of time in frames that this item should exist * @param {string} [namespace] the namespace for this item * @returns {RenderLoopItem} the item that was registered */ buildRenderLoopItem(updateCallback, timeoutCallback, duration, namespace) { let noNamespace = typeof namespace !== 'string' || !namespace.length; // valid for a callback not to have a duration, as long as it's // namespaced for future manual removal if (typeof duration === 'string') { if (noNamespace) { namespace = duration; duration = -1; noNamespace = false; } else { const numberDuration = Number(duration); if (!isNaN(numberDuration)) { duration = numberDuration; } } } else if (typeof duration !== 'number') { duration = -1; } if (typeof namespace !== 'string' || !namespace.length) { namespace = ''; // TODO: make unique } const loopItem = new RenderLoopItem({ id: namespace, updateCallback, timeoutCallback, duration }); return loopItem; }, /** * @param {RenderLoopItem|function} loopItem - (can also be the "updateCallback" function) * @param {function} [timeoutCallback] callback function that gets fired at * the end of this item's lifecycle * @param {number} [duration] the amount of time in frames that this item should exist * @param {string} [namespace] the namespace for this item * @returns {RenderLoopItem} the item that was registered */ register(loopItem, timeoutCallback, duration, namespace) { // If we're not working with a RenderLoopItem off the bat, take arguments // and convert to a RenderLoopItem. Consider the first argument // to be the "updateCallback" function if (!(loopItem instanceof RenderLoopItem)) { loopItem = this.buildRenderLoopItem(loopItem, timeoutCallback, duration, namespace); } this.items.push(loopItem); return loopItem; }, /** * @param {function} callback callback function to be unregistered * @param {string} [namespace] namespace to be unregistered * @returns {RenderLoopItem} the item that was unregistered */ unregister(callback, namespace) { if (typeof callback !== 'function' && typeof callback !== 'string' && typeof namespace !== 'string') { throw new Error('must provide either a callback function or a namespace string to remove an entry from the RenderLoop queue.'); } // If callback is defined as a string, simply swap it for the namespace. if (typeof callback === 'string') { namespace = callback; callback = undefined; } return this.remove({ cb: callback, id: namespace }); }, /** * @private * Uses a callback function, or a defined namespace, to grab a RenderLoop item from the queue. * @param {function} updateCallback callback function to be retrieved * @param {string} [namespace] namespace to be retrieved * @returns {RenderLoopItem} the RenderLoopItem that represents the item that was paused. */ getFromQueue(updateCallback, namespace) { // If callback is defined as a string, simply swap it for the namespace. if (typeof callback === 'string') { namespace = updateCallback; updateCallback = undefined; } let retreivedItem; if (typeof callback === 'function') { // Remove by callback method this.items.forEach((item) => { if (`${item.updateCallback}` !== `${updateCallback}`) { return true; } retreivedItem = item; return false; }); } else if (typeof namespace === 'string') { // Remove by namespace this.items.forEach((item) => { if (item.id !== namespace) { return true; } retreivedItem = item; return false; }); } return retreivedItem; }, /** * @private * Actually does the removal of a registered callback from the queue * Pulled out into its own function because it can be automatically called by * the tick, or manually triggered from an external API call. * @param {renderLoopItem|Object} obj the renderLoopItem * @returns {RenderLoopItem} reference to the removed renderLoopItem */ remove(obj) { let removedItem; if (obj instanceof RenderLoopItem) { removedItem = obj; this.items = this.items.filter(item => item !== obj); } else if (typeof obj.updateCallback === 'function') { // Remove by callback method this.items = this.items.filter((item) => { if (`${item.updateCallback}` !== `${obj.updateCallback}`) { return true; } removedItem = item; return false; }); } else if (typeof obj.id === 'string') { // Remove by namespace this.items = this.items.filter((item) => { if (item.id !== obj.id) { return true; } removedItem = item; return false; }); } if (typeof removedItem.timeoutCallback === 'function') { removedItem.timeoutCallback.apply(null, removedItem); } this.element.triggerHandler('remove.renderLoop', [removedItem]); // If this is undefined, an item was NOT removed from the queue successfully. return removedItem; }, /** * @param {function} callback callback function to be paused * @param {string} [namespace] namespace to be paused * @returns {RenderLoopItem} the RenderLoopItem that represents the item that was paused. */ pause(callback, namespace) { if (typeof callback !== 'function' && typeof callback !== 'string' && typeof namespace !== 'string') { throw new Error('must provide either a callback function or a namespace string to pause an entry in the RenderLoop queue.'); } const pausedItem = this.getFromQueue(callback, namespace); pausedItem.pause(); return pausedItem; }, /** * @param {function} callback callback function to be resumed * @param {string} [namespace] namespace to be resumed * @returns {RenderLoopItem} the RenderLoopItem that represents the item that was resumed. */ resume(callback, namespace) { if (typeof callback !== 'function' && typeof callback !== 'string' && typeof namespace !== 'string') { throw new Error('must provide either a callback function or a namespace string to pause an entry in the RenderLoop queue.'); } const resumableItem = this.getFromQueue(callback, namespace); resumableItem.resume(); return resumableItem; }, }; // Setup a single instance of RenderLoop for export. const renderLoop = new RenderLoop(); export { RenderLoopItem, renderLoop };