diff --git a/core/misc/matchmedia.js b/core/misc/matchmedia.js new file mode 100644 index 0000000..bd18fe8 --- /dev/null +++ b/core/misc/matchmedia.js @@ -0,0 +1,156 @@ +/** + * Polyfill the behavior of window.matchMedia. + * + * @see http://dev.w3.org/csswg/cssom-view/#widl-Window-matchMedia-MediaQueryList-DOMString-query + * + * Test whether a CSS media type or media query applies. Register listeners + * to MediaQueryList objects. + * + * Adapted from https://github.com/paulirish/matchMedia.js with the addition + * of addListener and removeListener. The polyfill referenced above uses + * polling to trigger registered listeners on matchMedia tests. + * This polyfill triggers tests on window resize and orientationchange. + */ + +window.matchMedia = window.matchMedia || (function (doc, window) { + + "use strict"; + + var docElem = doc.documentElement; + var refNode = docElem.firstElementChild || docElem.firstChild; + // fakeBody required for . + var fakeBody = doc.createElement("body"); + var div = doc.createElement("div"); + + div.id = "mq-test-1"; + div.style.cssText = "position:absolute;top:-100em"; + fakeBody.style.background = "none"; + fakeBody.appendChild(div); + + /** + * A replacement for the native MediaQueryList object. + * + * @param {String} q + * A media query e.g. "screen" or "screen and (min-width: 28em)". + */ + function MediaQueryList (q) { + this.media = q; + this.matches = false; + this.check.call(this); + } + + /** + * Polyfill the addListener and removeListener methods. + */ + MediaQueryList.prototype = { + listeners: [], + + /** + * Perform the media query application check. + */ + check: function () { + var isApplied; + div.innerHTML = "­"; + docElem.insertBefore(fakeBody, refNode); + isApplied = div.offsetWidth === 42; + docElem.removeChild(fakeBody); + this.matches = isApplied; + }, + + /** + * Polyfill the addListener method of the MediaQueryList object. + * + * @param {Function} callback + * The callback to be invoked when the media query is applicable. + * + * @return {Object MediaQueryList} + * A MediaQueryList object that indicates whether the registered media + * query applies. The matches property is true when the media query + * applies and false when not. The original media query is referenced in + * the media property. + */ + addListener: function (callback) { + var handler = (function (mql, debounced) { + return function () { + mql.check(); + debounced.call(mql, mql); + }; + }(this, debounce(callback, 250))); + this.listeners.push({ + 'callback': callback, + 'handler': handler + }); + + // Associate the handler to the resize and orientationchange events. + if ('addEventListener' in window) { + window.addEventListener('resize', handler); + window.addEventListener('orientationchange', handler); + } + else if ('attachEvent' in window) { + window.attachEvent('onresize', handler); + window.attachEvent('onorientationchange', handler); + } + }, + + /** + * Polyfill the removeListener method of the MediaQueryList object. + * + * @param {Function} callback + * The callback to be removed from the set of listeners. + */ + removeListener: function (callback) { + for (var i = 0, listeners = this.listeners; i < listeners.length; i++) { + if (listeners[i].callback === callback) { + // Disassociate the handler to the resize and orientationchange events. + if ('removeEventListener' in window) { + window.removeEventListener('resize', listeners[i].handler); + window.removeEventListener('orientationchange', listeners[i].handler); + } + else if ('detachEvent' in window) { + window.detachEvent('onresize', listeners[i].handler); + window.detachEvent('onorientationchange', listeners[i].handler); + } + listeners.splice(i, 1); + } + } + } + }; + + /** + * Limits the invocations of a function in a given time frame. + * + * @param {Function} callback + * The function to be invoked. + * + * @param {Number} wait + * The time period within which the callback function should only be + * invoked once. For example if the wait period is 250ms, then the callback + * will only be called at most 4 times per second. + */ + function debounce (callback, wait) { + var timeout, result; + return function () { + var context = this; + var args = arguments; + var later = function () { + timeout = null; + result = callback.apply(context, args); + }; + window.clearTimeout(timeout); + timeout = window.setTimeout(later, wait); + return result; + }; + } + + /** + * Return a MediaQueryList. + * + * @param {String} q + * A media query e.g. "screen" or "screen and (min-width: 28em)". The media + * query is checked for applicability before the object is returned. + */ + return function (q) { + // Build a new MediaQueryList object with the result of the check. + return new MediaQueryList(q); + }; +}(document, window)); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index a9d951b..38f734b 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1380,6 +1380,16 @@ function system_library_info() { ), ); + // matchMedia polyfill. + $libraries['matchmedia'] = array( + 'title' => 'window.matchMedia polyfill', + 'website' => 'http://drupal.org/node/1815602', + 'version' => VERSION, + 'js' => array( + 'core/misc/matchmedia.js' => array(), + ), + ); + // Farbtastic. $libraries['jquery.farbtastic'] = array( 'title' => 'Farbtastic',