formatchange
Version:
Utility that monitors named CSS media-query breakpoints and triggers event callbacks when a media-format change occurs.
227 lines (195 loc) • 5.99 kB
JavaScript
/* FormatChange -- (c) 2012-2022 Hugsmiðjan ehf. @license MIT/GPL */
// ----------------------------------------------------------------------------------
// FormatChange -- https://github.com/maranomynet/formatchange
// ----------------------------------------------------------------------------------
// (c) 2012-2022 Hugsmiðjan ehf -- http://www.hugsmidjan.is
// written by:
// * Már Örlygsson -- http://mar.anomy.net
//
// Dual licensed under a MIT licence (http://en.wikipedia.org/wiki/MIT_License)
// and GPL 2.0 or above (http://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
// ----------------------------------------------------------------------------------
var _beget = Object.create || function (prototype) {
var F = function () {};
F.prototype = prototype;
return new F();
};
// FYI: `jsdom` doesn't have `window.getComputedStyle`...
var isBrowser = (window) => !!(window && window.getComputedStyle)
var FormatChange = function (groups, config) {
var self = this;
if (!(this instanceof FormatChange)) {
// tolerate cases when new is missing.
return new FormatChange(groups, config);
}
config = config || {};
self.win = config.win || self.win;
self.elm = config.elm;
if (config.elmTagName) {
self.elmTagName = config.elmTagName;
}
if (config.elmId) {
self.elmId = config.elmId;
}
if ('manual' in config) {
self.manual = config.manual;
}
if ('defer' in config) {
self.defer = config.defer;
}
self.formatGroups = groups || _beget(self.formatGroups);
self.media = {};
self._callbacks = [];
// for (var i=0, callback; (callback = (config.callbacks||[])[i]); i++)
// {
// self.subscribe(callback);
// }
// a self-bound handler-function for window.onresize events.
self._check = function () { self.check(); };
!self.defer && self.start();
};
FormatChange.prototype = {
// Default options and format groups.
elmTagName: 'del',
elmId: 'mediaformat',
manual: false,
defer: false,
win: typeof window !== 'undefined' ? window : undefined,
formatGroups: {},
oldFormat: null,
_failures: true,
isRunning: function () {
return this._on;
},
start: function (afresh) {
// Only define the Format Info object if needed
// Also: Don't start if window is undefined
if (this._on || !isBrowser(this.win)) {
return;
}
// Ensure elm is defined
if (!this.elm) {
var doc = this.win.document;
var id = this.elmId || 'mediaformat';
var elm = this.elm = doc.getElementById(id);
if (!elm) {
// build and inject the hidden monitoring element
elm = this.elm = doc.createElement(this.elmTagName||'del');
var elm_style = elm.style;
elm_style.position = 'absolute';
elm_style.visibility =
elm_style.overflow = 'hidden';
elm_style.border =
elm_style.padding =
elm_style.margin =
elm_style.width =
elm_style.height = 0;
elm.id = id;
elm._isMine = true;
doc.body.appendChild(elm);
}
}
this._on = true;
if (!this.manual) {
this.win.addEventListener('resize', this._check);
}
this.refresh(afresh);
},
stop: function () {
var elm = this.elm;
if (!this._on) {
return;
}
if (!this.manual) {
this.win.removeEventListener('resize', this._check);
}
if (elm._isMine) {
elm.parentNode.removeChild(elm);
delete this.elm;
}
this._on = false;
},
refresh: function (hardRefresh) {
if (hardRefresh) {
this.oldFormat = null;
}
if (!this._on) {
return false;
}
const changeOccurred = this.check();
if (changeOccurred === false) {
// Update group flags, even though check() returned false (indicating no change)
// in case Group data has changed or something
this._updateGroupFlags();
}
return this._on;
},
subscribe: function (callback, runImmediately) {
if (!callback) {
return;
}
this.unsubscribe(callback);
this._callbacks.push(callback);
// run callbacks immediately if .start()
if (runImmediately !== false && this._on && !this._triggering) {
callback(this.media);
}
},
unsubscribe: function (callback) {
if (!callback) {
return;
}
for (var i=0, cb; (cb = this._callbacks[i]); i++) {
if (cb === callback) {
this._callbacks.splice(i,1);
break;
}
}
},
_on: false,
// update the static group-related flags.
_updateGroupFlags: function () {
var self = this;
var media = self.media;
var formatGroups = self.formatGroups;
for (var grpName in formatGroups) {
var grp = formatGroups[grpName];
var is = !!(grp&&grp[media.is]);
var was = !!(grp&&grp[media.was]);
media['is'+grpName] = is;
media['was'+grpName] = was;
media['became'+grpName] = is && !was;
media['left'+grpName] = !is && was;
!grp && (delete formatGroups[grpName]); // delete when we've made sure all flags are set to false (cleanup)
}
},
check: function () {
if (!this._on) {
return;
}
var media = this.media;
var oldFormat = this.oldFormat;
var newFormat = this.win.getComputedStyle(this.elm, '::after').content;
if (newFormat === 'none' && this._failures < 10) {
this._failures++;
setTimeout(() => this.check(), 67);
return;
}
newFormat = newFormat.replace(/['"]/g, '');
var changeOccurred = newFormat !== oldFormat;
if (changeOccurred) {
media.is = newFormat;
media.was = oldFormat;
this.oldFormat = newFormat;
this._updateGroupFlags();
// issue Notification
this._triggering = true;
for (var i=0, callback; (callback = this._callbacks[i]); i++) {
callback(media);
}
this._triggering = false;
}
return changeOccurred;
},
};
exports.FormatChange = FormatChange