define([ 'dojo/has' ], function (has) { // summary: // This module defines miscellaneous utility methods for purposes of // adding styles, and throttling/debouncing function calls. // establish an extra stylesheet which addCssRule calls will use, // plus an array to track actual indices in stylesheet for removal var extraRules = [], extraSheet, removeMethod, rulesProperty, invalidCssChars = /([^A-Za-z0-9_\u00A0-\uFFFF-])/g, delayCallback = setTimeout, cancelDelay = clearTimeout; has.add('requestidlecallback', function (global) { return typeof global.requestIdleCallback === 'function'; }); // The presence of the 'requestIdleCallback' method indicates a browser that might // performance optimize code by delaying execution of the callback passed to // 'setTimeout', so use 'requestIdleCallback' to improve the likelihood of the // callback being executed in a timely manner. // This is not a perfect solution, but has worked well in testing. // requestIdleCallback is designed to be called successively, performing progressive chunks // of computation each time until the task is complete. // setTimeout executes its callback when delay has transpired, *or later* // requestIdleCallback executes its callback when delay has transpired, *or sooner* // Fixes https://github.com/SitePen/dgrid/issues/1351 // if (has('requestidlecallback')) { // delayCallback = function (callback, delay) { // return requestIdleCallback(callback, { timeout: delay }); // }; // cancelDelay = cancelIdleCallback; // } function removeRule(index) { // Function called by the remove method on objects returned by addCssRule. var realIndex = extraRules[index], i, l; if (realIndex === undefined) { return; // already removed } // remove rule indicated in internal array at index extraSheet[removeMethod](realIndex); // Clear internal array item representing rule that was just deleted. // NOTE: we do NOT splice, since the point of this array is specifically // to negotiate the splicing that occurs in the stylesheet itself! extraRules[index] = undefined; // Then update array items as necessary to downshift remaining rule indices. // Can start at index + 1, since array is sparse but strictly increasing. for (i = index + 1, l = extraRules.length; i < l; i++) { if (extraRules[i] > realIndex) { extraRules[i]--; } } } var util = { // Throttle/debounce functions defaultDelay: 15, throttle: function (cb, context, delay) { // summary: // Returns a function which calls the given callback at most once per // delay milliseconds. (Inspired by plugd) var ran = false; delay = delay || util.defaultDelay; return function () { if (ran) { return; } ran = true; cb.apply(context, arguments); delayCallback(function () { ran = false; }, delay); }; }, throttleDelayed: function (cb, context, delay) { // summary: // Like throttle, except that the callback runs after the delay, // rather than before it. var ran = false; delay = delay || util.defaultDelay; return function () { if (ran) { return; } ran = true; var a = arguments; delayCallback(function () { ran = false; cb.apply(context, a); }, delay); }; }, debounce: function (cb, context, delay) { // summary: // Returns a function which calls the given callback only after a // certain time has passed without successive calls. (Inspired by plugd) var timer; delay = delay || util.defaultDelay; return function () { if (timer) { cancelDelay(timer); timer = null; } var a = arguments; timer = delayCallback(function () { timer = null; cb.apply(context, a); }, delay); }; }, // Iterative functions each: function (arrayOrObject, callback, context) { // summary: // Given an array or object, iterates through its keys. // Does not use hasOwnProperty (since even Dojo does not // consistently use it), but will iterate using a for or for-in // loop as appropriate. var i, len; if (!arrayOrObject) { return; } if (typeof arrayOrObject.length === 'number') { for (i = 0, len = arrayOrObject.length; i < len; i++) { callback.call(context, arrayOrObject[i], i, arrayOrObject); } } else { for (i in arrayOrObject) { callback.call(context, arrayOrObject[i], i, arrayOrObject); } } }, // CSS-related functions addCssRule: function (selector, css) { // summary: // Dynamically adds a style rule to the document. Returns an object // with a remove method which can be called to later remove the rule. if (!extraSheet) { // First time, create an extra stylesheet for adding rules extraSheet = document.createElement('style'); document.getElementsByTagName('head')[0].appendChild(extraSheet); // Keep reference to actual StyleSheet object (`styleSheet` for IE < 9) extraSheet = extraSheet.sheet || extraSheet.styleSheet; // Store name of method used to remove rules (`removeRule` for IE < 9) removeMethod = extraSheet.deleteRule ? 'deleteRule' : 'removeRule'; // Store name of property used to access rules (`rules` for IE < 9) rulesProperty = extraSheet.cssRules ? 'cssRules' : 'rules'; } var index = extraRules.length; extraRules[index] = (extraSheet.cssRules || extraSheet.rules).length; extraSheet.addRule ? extraSheet.addRule(selector, css) : extraSheet.insertRule(selector + '{' + css + '}', extraRules[index]); return { get: function (prop) { return extraSheet[rulesProperty][extraRules[index]].style[prop]; }, set: function (prop, value) { if (typeof extraRules[index] !== 'undefined') { extraSheet[rulesProperty][extraRules[index]].style[prop] = value; } }, remove: function () { removeRule(index); } }; }, escapeCssIdentifier: function (id, replace) { // summary: // Escapes normally-invalid characters in a CSS identifier (such as . or :); // see http://www.w3.org/TR/CSS2/syndata.html#value-def-identifier // id: String // CSS identifier (e.g. tag name, class, or id) to be escaped // replace: String? // If specified, indicates that invalid characters should be // replaced by the given string rather than being escaped return typeof id === 'string' ? id.replace(invalidCssChars, replace || '\\$1') : id; } }; return util; });