From 6fad32dc227abe9251a5e1dc6c632bf131009be9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Wed, 17 Oct 2012 15:25:43 -0400
Subject: [PATCH] Issue #1815602 by jessebeach: Introduce a JavaScript utility
 that associates callback functions with media queries.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 core/misc/matchMedia/matchMedia.js |   40 ++++++
 core/misc/mediaquerygroup.js       |  278 ++++++++++++++++++++++++++++++++++++
 core/modules/system/system.module  |   24 ++++
 3 files changed, 342 insertions(+)
 create mode 100644 core/misc/matchMedia/matchMedia.js
 create mode 100644 core/misc/mediaquerygroup.js

diff --git a/core/misc/matchMedia/matchMedia.js b/core/misc/matchMedia/matchMedia.js
new file mode 100644
index 0000000..763e318
--- /dev/null
+++ b/core/misc/matchMedia/matchMedia.js
@@ -0,0 +1,40 @@
+/**
+ * @author Paul Irish
+ * @see https://github.com/paulirish/matchMedia.js
+ *
+ * Test whether a CSS media type or media query applies.
+ *
+ * Polyfill for window.matchMedia.
+ */
+window.matchMedia = window.matchMedia || (function( doc, undefined ) {
+
+  "use strict";
+
+  var bool,
+      docElem = doc.documentElement,
+      refNode = docElem.firstElementChild || docElem.firstChild,
+      // fakeBody required for <FF4 when executed in <head>
+      fakeBody = doc.createElement( "body" ),
+      div = doc.createElement( "div" );
+
+  div.id = "mq-test-1";
+  div.style.cssText = "position:absolute;top:-100em";
+  fakeBody.style.background = "none";
+  fakeBody.appendChild(div);
+
+  return function(q){
+
+    div.innerHTML = "&shy;<style media=\"" + q + "\"> #mq-test-1 { width: 42px; }</style>";
+
+    docElem.insertBefore( fakeBody, refNode );
+    bool = div.offsetWidth === 42;
+    docElem.removeChild( fakeBody );
+
+    return {
+      matches: bool,
+      media: q
+    };
+
+  };
+
+}( document ));
diff --git a/core/misc/mediaquerygroup.js b/core/misc/mediaquerygroup.js
new file mode 100644
index 0000000..5549827
--- /dev/null
+++ b/core/misc/mediaquerygroup.js
@@ -0,0 +1,278 @@
+/*global Drupal:true, window:true */
+
+(function (_, matchMedia) {
+
+"use strict";
+
+/**
+ * Attach the mediaquerygroup function to Drupal.behaviors.
+ */
+Drupal.behaviors.mediaquerygroup = {
+  attach: function (context, settings) {
+    _.extend(this.options, settings);
+    var addEventListener = window.addEventListener || window.attachEvent;
+    if (addEventListener) {
+      window.addEventListener('resize', _.debounce(_.bind(MediaQueryGroup.refresh, MediaQueryGroup), this.options.mediaquerygroup.debounce));
+    }
+  },
+  /**
+   * Option defaults.
+   */
+  options: {
+    mediaquerygroup: {
+      debounce: 350
+    }
+  }
+};
+
+/**
+ * A MediaQueryGroup instance invokes callbacks associated with media queries.
+ *
+ * Clients create a new instance of a MediaQueryGroup and add callbacks to
+ * the instance with an associated valid media query. A default callback can
+ * be added, to be invoked when no other media query in the instance is
+ * applicable.
+ *
+ *   funtion foo () {}
+ *   var mqg = new MediaQueryGroup('myModule');
+ *   mpg.add('default', function () {});
+ *   mpg.add('screen and (min-width: 28em)', foo);
+ *
+ * Callbacks can be removed by reference individually or removed as a group
+ * by removing the media query referenced by string.
+ *
+ *   mgp.remove(foo);
+ *   mpg.remove('screen and (min-width: 28em)');
+ *
+ * @param {String} namespace
+ *   A name to identify your MediaQueryGroup instance.
+ */
+function MediaQueryGroup (namespace) {
+  if (!namespace) {
+    throw new Error("MediaQueryGroup: a namespace must be provided to create a MediaQueryGroup instance.");
+  }
+  this.namespace = namespace;
+  Drupal.MediaQueryGroup.groups[namespace] = this;
+}
+
+/**
+ * Extend the MediaQueryGroup constructor.
+ *
+ * The constructor stores information about each instances queries. It manages
+ * the testing of media queries on window load and resize.
+ *
+ * Media queries from each instance group are conglomerated into a single list.
+ */
+_.extend(MediaQueryGroup, {
+  fallback: 'default',
+  groups: {},
+  queries: [],
+  /**
+   * @return {Array}
+   *   An array of the media queries managed by the MediaQueryGroup
+   */
+  listQueries: function () {
+    return this.queries;
+  },
+  /**
+   * @return {Object}
+   *   An object listing the MediaQueryGroup instances, keyed by their
+   *   namespace property.
+   */
+  listGroups: function () {
+    return this.groups;
+  },
+  /**
+   * Employ window.matchMedia to determine if a media query is applicable to
+   * the current user agent dimensions and attributes.
+   *
+   * @param {String} mq
+   *   A valid media query.
+   */
+  test: function (mq) {
+    return matchMedia(mq).matches;
+  },
+  /**
+   * Triggered when the window has resized.
+   *
+   * @param {Event} event
+   *   Standard DOM event.
+   */
+  refresh: function (event) {
+    var refreshed = [];
+    var query, group, i, callbacks, c;
+    // Run through each query in the master list.
+    for (i = 0; i <this.queries.length; i++) {
+      query = this.queries[i];
+      // Test the query with matchMedia.
+      if (this.test(query)) {
+        // The the media query applies, fire callbacks in the groups that
+        // track this query.
+        for (group in this.groups) {
+          if (this.groups.hasOwnProperty(group) && (query in this.groups[group].queries)) {
+            // Note the groups that have callbacks invoked. Any group without a
+            // callback invoked with have its default callbacks invoked below.
+            refreshed.push(group);
+            // If the group had callbacks for an active media query fired
+            // already, don't fire them again. This ensures callbacks are fired
+            // only once per media query application.
+            if (this.groups[group].lastFired !== query) {
+              callbacks = this.groups[group].queries[query];
+              // Cycle through the callbacks for a media query and invoke each
+              // one.
+              for (c = 0; c < callbacks.length; c++) {
+                callbacks[c].call(window, event);
+              }
+              // Store the media query that was last applied for this group.
+              this.groups[group].lastFired = query;
+            }
+          }
+        }
+      }
+    }
+    // Run the default callback on any group that did not have a callback
+    // invoked for any of its registered media queries.
+    var defaulters = _.omit(this.groups, refreshed);
+    for (group in defaulters) {
+      // Don't invoke callbacks if the default media query was the last one
+      // to be applied.
+      if (defaulters.hasOwnProperty(group) && (this.fallback in defaulters[group].queries) && defaulters[group].lastFired !== this.fallback) {
+        callbacks = defaulters[group].queries[this.fallback];
+        // Cycle through the callbacks for a media query and invoke each one.
+        for (c = 0; c < callbacks.length; c++) {
+          callbacks[c].call(window, event);
+        }
+        // Store the media query that was last applied for this group.
+        this.groups[group].lastFired = this.fallback;
+      }
+    }
+  },
+  /**
+   * Add unique media queries to this.queries.
+   *
+   * @param {String} mq
+   *   A valid media query.
+   */
+  queryAdd: function (mq) {
+    // Store neither the fallback media query nor duplicates.
+    if (mq !== this.fallback && this.queries.indexOf(mq) === -1) {
+      this.queries.push(mq);
+    }
+  },
+  /**
+   * Cycle through all this.groups and see if this was the last instance of
+   * the media query. If so, remove it from this.queries.
+   *
+   * @param {String} mq
+   *   A valid media query.
+   */
+  queryRemove: function (mq) {
+    if (mq !== this.fallback) {
+      var query, group, i, index;
+      for (group in this.groups) {
+        // If at least one group tracks the media query, return and do nothing.
+        if (this.groups.hasOwnProperty(group) && (mq in this.groups[group].queries)) {
+          return;
+        }
+      }
+      // If no groups have the media query in their list of queries, remove it
+      // from the master list.
+      index = this.queries.indexOf(mq);
+      this.queries.splice(index, 1);
+    }
+  }
+});
+
+/**
+ * Extend the MediaQueryGroup prototype.
+ *
+ * Instances of the MediaQueryGroup have these properties and methods.
+ */
+_.extend(MediaQueryGroup.prototype, {
+  namespace: '',
+  // An object keyed by the media query string. Each property contains an
+  // array of callback functions for that media query.
+  queries: {},
+  // The media query that last applied.
+  lastFired: '',
+  /**
+   * Add a callback to the group and associated it with a media query.
+   *
+   * @param {String} mq
+   *   A valid media query.
+   *
+   * @param {function} callback
+   *   A function to be invoked with the associated media query is
+   *   is applicable.
+   */
+  add: function (mq, callback) {
+    if (!(mq && this.queries[mq])) {
+      this.queries[mq] = [];
+      // Add the mq to the global list of queries.
+      Drupal.MediaQueryGroup.queryAdd(mq);
+    }
+    this.queries[mq].push(callback);
+    // If the media query applies when the callback is added, invoke it.
+    // If no media query has been applied and this query is the fallback, then
+    // invoke it.
+    if (Drupal.MediaQueryGroup.test(mq) || (mq === Drupal.MediaQueryGroup.fallback && this.lastFired.length === 0)) {
+      // The callback might be a bound function, so don't change the
+      // context. Just call it.
+      callback();
+      this.lastFired = mq;
+    }
+  },
+  /**
+   * Add a callback to the group and associated it with a media query.
+   *
+   * @param {String | function} property
+   *   If a string is provided, it is assumed to be a media query. If a function
+   *   is provided, it will be assumed to be a callback. Removing a media
+   *   query removes all callbacks associated with that query.
+   *
+   * @return {Boolean}
+   *   True if the property was removed. False is nothing was removed.
+   */
+  remove: function (property) {
+    var mq, callback, i, index;
+    // The property is a media query.
+    if (typeof property === 'string') {
+      if (property in this.queries) {
+        delete this.queries[property];
+        // Attempt to remove the media query from the master list.
+        Drupal.MediaQueryGroup.queryRemove(property);
+        return true;
+      }
+    }
+    // The property is a callback.
+    if (typeof property === 'function') {
+      callback = property;
+      // Loop through all the media queries in this instance.
+      for (mq in this.queries) {
+        if (this.queries.hasOwnProperty(mq)) {
+          // Determine if the supplied callback function is associated with
+          // this media query.
+          index = this.queries[mq].indexOf(callback);
+          if (index > -1) {
+            this.queries[mq].splice(index, 1);
+            // If the media query has no callbacks associated with it, remove
+            // the property from the list of queries.
+            if (this.queries[mq].length === 0) {
+              delete this.queries[mq];
+            }
+            // Attempt to remove the media query from the master list.
+            Drupal.MediaQueryGroup.queryRemove(mq);
+            return true;
+          }
+        }
+      }
+    }
+    // Return false if nothing was removed.
+    return false;
+  }
+});
+
+// Expose constructor in the public space.
+_.extend(Drupal, {'MediaQueryGroup': MediaQueryGroup});
+
+}(_, matchMedia));
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index ea3dadc..c298592 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1380,6 +1380,30 @@ function system_library_info() {
     ),
   );
 
+  // Media Query support.
+  $libraries['matchMedia'] = array(
+    'title' => 'matchMedia() polyfill',
+    'website' => 'https://github.com/paulirish/matchMedia.js',
+    'version' => '1.0',
+    'js' => array(
+      'core/misc/matchMedia/matchMedia.js' => array(),
+    ),
+  );
+
+  $libraries['drupal.mediaquerygroup'] = array(
+    'title' => 'Media Query support',
+    'website' => '',
+    'version' => '1.0',
+    'js' => array(
+      'core/misc/mediaquerygroup.js' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'underscore'),
+      array('system', 'drupal'),
+      array('system', 'matchMedia'),
+    ),
+  );
+
   // Farbtastic.
   $libraries['jquery.farbtastic'] = array(
     'title' => 'Farbtastic',
-- 
1.7.10.4

