Skip to content

Instantly share code, notes, and snippets.

@james-jlo-long
Last active August 12, 2019 10:41
Show Gist options
  • Save james-jlo-long/fe69c667463d127063617a2b4d5a54c5 to your computer and use it in GitHub Desktop.
Save james-jlo-long/fe69c667463d127063617a2b4d5a54c5 to your computer and use it in GitHub Desktop.
Listening for break point changes without using the window resize event.
/**
* @file A class that listens for breakpoints. This is far more efficient
* and accurate than relying on the window resize event.
* @license MIT
* @author James "JLo" Long <jameslong@xigen.co.uk>
* @requires Observer
*/
var BreakPoints = (function () {
"use strict";
var EVENT_NAME_GLUE = "-";
/**
* @class BreakPoints
* @param {Object} [points]
* Optional initial break points. See
* {@link BreakPoints#addBreakPoint} for details.
*/
var Points = function (points) {
/**
* A collection of break point names and matchMedia instances.
* @type {Object}
*/
this.media = {};
if (points) {
Object.keys(points).forEach(function (name) {
this.addBreakPoint(name, points[name]);
}, this);
}
};
Points.prototype = Observer.extend(/** @lends BreakPoints.prototype */{
/**
* Adds a break point.
*
* @param {String} name
* The name of the break point.
* @param {Number|String} mediaQuery
* Either the minimum number of pixels (e.g. 100 is converted
* into "(min-width: 100px)") or a string with a media query
* (e.g. "(max-width: 30em)").
*/
addBreakPoint: function (name, mediaQuery) {
var that = this;
var mm = window.matchMedia(
typeof mediaQuery === "string"
? mediaQuery
: "(min-width:" + mediaQuery + "px)"
);
mm.addListener(function (media) {
var eventName = (
media.matches
? "enter"
: "leave"
);
/**
* The generic change event that fires whenever a break point
* change occurs.
* @event BreakPoints#change
*/
that.dispatchEvent("change", media);
/**
* The change event that fires whenever a specific break point
* changes.
* @event BreakPoints#change-(name)
*/
that.dispatchEvent("change" + EVENT_NAME_GLUE + name, media);
/**
* The event that fires when a break point becomes active.
* @event BreakPoints#enter
*/
/**
* The event that fires when a break point becomes inactive.
* @event BreakPoints#leave
*/
that.dispatchEvent(eventName, media);
/**
* The event that fires when a specific break point becomes
* active.
* @event BreakPoints#enter-(name)
*/
/**
* The event that fires when a specific break point becomes
* inactive.
* @event BreakPoints#leave-(name)
*/
that.dispatchEvent(
eventName + EVENT_NAME_GLUE + name,
media
);
});
that.media[name] = mm;
},
/**
* Adds an event listener to a breakpoint change.
*
* @param {String} event
* Name of the event to listen for.
* @param {String} [name]
* Optional name of the break point whose event should be
* listened for.
* @param {Function} handler
* Function to execute when the event occurs.
* @param {Boolean} [autoExecute=false]
* If true and if name is given, the handler will execute
* instantly if it would have matched.
*/
on: function (event, name, handler, autoExecute) {
var isNameGiven = typeof name === "string";
var func = (
isNameGiven
? handler
: name
);
var eventName = (
isNameGiven
? (event + EVENT_NAME_GLUE + name)
: event
);
var matches = false;
this.addEventListener(eventName, func);
if (isNameGiven && autoExecute) {
matches = this.matches(name);
if (
event === "change"
|| (event === "enter" && matches)
|| (event === "leave" && !matches)
) {
func.call(
this.getEventElement(),
this.createEvent(eventName, this.media[name])
);
}
}
},
/**
* Removes an event listener from a breakpoint change.
*
* @param {String} event
* Name of the event to stop listening for.
* @param {String} [name]
* Optional name of the break point whose event should no longer
* be listened for.
* @param {Function} handler
* Function to remove from the event.
*/
off: function (event, name, handler) {
var isNameGiven = typeof name === "string";
var func = (
isNameGiven
? handler
: name
);
var eventName = (
isNameGiven
? (event + EVENT_NAME_GLUE + name)
: event
);
this.removeEventListener(eventName, func);
},
/**
* Checks to see if the given break point name matches. This function
* will return false if the break point name isn't recognised.
*
* @param {String} name
* Name of the break point to check.
* @return {Boolean}
* True if the break point matches false if it doesn't (or if
* the break point name isn't recognised).
*/
matches: function (name) {
var media = this.media[name];
return Boolean(media) && media.matches;
}
});
return Points;
}());
/*! MIT lisence, author: James "JLo" Long <jameslong@xigen.co.uk> */
var Observer=function(){"use strict";var t=new WeakMap,e=function(){};return e.prototype={getEventElement:function(){var e=t.get(this);return e||(e=document.createElement("div"),t.set(this,e)),e},addEventListener:function(t,e){$(this.getEventElement()).on(t,e)},removeEventListener:function(t,e){$(this.getEventElement()).off(t,e)},createEvent:function(t,e){return $.Event(t,{detail:e})},dispatchEvent:function(t,e){$(this.getEventElement()).trigger("string"==typeof t?this.createEvent(t,e):t)}},e.extend=function(t){return $.extend(Object.create(e.prototype),t)},e}(),BreakPoints=function(){"use strict";var t=function(t){this.media={},t&&Object.keys(t).forEach(function(e){this.addBreakPoint(e,t[e])},this)};return t.prototype=Observer.extend({addBreakPoint:function(t,e){var n=this,i=window.matchMedia("string"==typeof e?e:"(min-width:"+e+"px)");i.addListener(function(e){var i=e.matches?"enter":"leave";n.dispatchEvent("change",e),n.dispatchEvent("change-"+t,e),n.dispatchEvent(i,e),n.dispatchEvent(i+"-"+t,e)}),n.media[t]=i},on:function(t,e,n,i){var r="string"==typeof e,a=r?n:e,s=r?t+"-"+e:t,c=!1;this.addEventListener(s,a),r&&i&&(c=this.matches(e),("change"===t||"enter"===t&&c||"leave"===t&&!c)&&a.call(this.getEventElement(),this.createEvent(s,this.media[e])))},off:function(t,e,n){var i="string"==typeof e,r=i?n:e,a=i?t+"-"+e:t;this.removeEventListener(a,r)},matches:function(t){var e=this.media[t];return Boolean(e)&&e.matches}}),t}();
/**
* @file A basic jQuery-based observer.
* @license MIT
* @author James "JLo" Long <jameslong@xigen.co.uk>
* @requires jQuery
*/
var Observer = (function () {
"use strict";
var dummies = new WeakMap();
/**
* @class Observer
*/
var Obs = function () {
};
Obs.prototype = /** @lends Observer.prototype */{
/**
* Gets the element on which events will be heard and dispatched. By
* default, this function will create a unique div element but this
* method can be overridden to work on elements in the existing DOM
* tree, allowing observable events to be delegated.
*
* @return {Element}
* Event element.
*/
getEventElement: function () {
var dummy = dummies.get(this);
if (!dummy) {
dummy = document.createElement("div");
dummies.set(this, dummy);
}
return dummy;
},
/**
* Adds an event listener to the event element (see
* {@link Observer#getEventElement}). Events always bubble.
*
* @param {String} eventName
* Name of the event to listen for.
* @param {Function} handler
* Function to execute on the event.
*/
addEventListener: function (eventName, handler) {
$(this.getEventElement()).on(eventName, handler);
},
/**
* Removes an event listener from the event element (see
* {@link Observer#getEventElement}). * @return {[type]} [description]
*
* @param {String} eventName
* Name of the event to stop listening for.
* @param {Function} handler
* Function to remove from the event.
*/
removeEventListener: function (eventName, handler) {
$(this.getEventElement()).off(eventName, handler);
},
/**
* Creates an event.
*
* @param {String} eventName
* Name of the event to create.
* @param {?} [detail]
* Optional detail for the event.
* @return {jQuery.Event}
* jQuery event.
*/
createEvent: function (eventName, detail) {
return $.Event(eventName, {
detail: detail
});
},
/**
* Dispatches an event on the event element (see
* {@link Observer#getEventElement}).
*
* @param {String|jQuery.Event} event
* Either the name of the event or the event itself.
* @param {?} [detail]
* Optional detail for the event. This is ignored if the event
* parameter is already a jQuery.Event instance.
*/
dispatchEvent: function (event, detail) {
$(this.getEventElement()).trigger(
typeof event === "string"
? this.createEvent(event, detail)
: event
);
}
};
/**
* Helper function for creating a prototype object that includes the
* {@link Observer} methods.
*
* @memberof Observer
* @param {Object} prototype
* Methods to add to the new prototype.
* @return {Object}
* Prototype with a live link to the prototype of
* {@link Observer}.
*/
Obs.extend = function (prototype) {
return $.extend(Object.create(Obs.prototype), prototype);
};
return Obs;
}());
// Listen for website break point changes using JavaScript but without relying
// on the window resize event. Not only does that fire a lot, but checking the
// window width isn't as accurate as you'd think.
// Creating an instance (based on Bootstrap 4)
var breakpoints = new BreakPoints({
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200
});
// You could also use a media query string either in this method or in the
// initial object.
breakpoints.addBreakPoint("custom", "(max-width: 30em)");
// Does the break point match?
breakpoints.matches("sm"); // -> true if the screen is at least 576 pixels wide.
// Listening for a change.
//
// There are 3 events:
// - "change" (break point has either been entered or left)
// - "enter" (break point has been entered)
// - "leave" (break point has been left)
// Listen for all break point enters:
breakpoints.on("enter", function (e) {
// e.detail is a window.matchMedia instance.
});
// Listen for the "md" break point leaving.
breakpoints.on("leave", "md", function (e) {
// e.detail is a window.matchMedia instance.
});
// Automatically execute the function if it would have matched.
breakpoints.on("change", "lg", function (e) {
// ...
}, true);
@james-jlo-long
Copy link
Author

james-jlo-long commented Aug 12, 2019

Note to self: the media passed to the event should have a name property so users can easily identify them from the generic change event.

Probably worth wrapping it:

class BreakPointQuery {

    constructor(/* String */ name, /* MediaQueryList */ matchMedia) {

        this.mediaQueryList = matchMedia;

        Object.defineProperties(this, {
            name: {
                configurable: false,
                enumerable: true,
                writable: false,
                value: name
            },
            matches: {
                get: function () {
                    return matchMedia.matches;
                }
            },
            media: {
                get: function () {
                    return matchMedia.media;
                }
            }
        });

    }

    addListener(/* Function */ func) {
        this.mediaQueryList.addListener(func);
    }

    removeListener(/* Function */ func) {
        this.mediaQueryList.removeListener(func);
    }
    
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment