mock-match-media
Version:
mock window.matchMedia for tests or node
226 lines (185 loc) • 5.71 kB
JavaScript
var cssMediaquery = require('css-mediaquery');
let state = {};
const now = Date.now(); // Event was added in node 15, so until we drop the support for versions before it, we need to use this
class EventLegacy {
// See https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase
constructor(type) {
this.type = void 0;
this.timeStamp = void 0;
this.bubbles = false;
this.cancelBubble = false;
this.cancelable = false;
this.composed = false;
this.target = null;
this.currentTarget = null;
this.defaultPrevented = false;
this.eventPhase = 0;
this.isTrusted = false;
this.initEvent = () => {};
this.composedPath = () => [];
this.preventDefault = () => {};
this.stopImmediatePropagation = () => {};
this.stopPropagation = () => {};
this.returnValue = true;
this.srcElement = null;
this.NONE = 0;
this.CAPTURING_PHASE = 1;
this.AT_TARGET = 2;
this.BUBBLING_PHASE = 3;
this.type = type;
this.timeStamp = Date.now() - now; // See https://developer.mozilla.org/en-US/docs/Web/API/Event/timeStamp#value
}
} // @ts-expect-error
const EventCompat = typeof Event === "undefined" ? EventLegacy : Event;
const getFeaturesFromQuery = query => {
const parsedQuery = cssMediaquery.parse(query);
const features = new Set();
parsedQuery.forEach(subQuery => {
subQuery.expressions.forEach(expression => {
features.add(expression.feature);
});
});
return features;
};
const MQLs = new Map();
const matchMedia = query => {
let queryTyped = query;
let previousMatched;
try {
previousMatched = cssMediaquery.match(queryTyped, state);
} catch (e) {
queryTyped = "not all";
previousMatched = false;
}
const callbacks = new Set();
const onces = new WeakSet();
const clear = () => {
for (const callback of callbacks) {
onces.delete(callback);
}
callbacks.clear();
};
const removeListener = callback => {
callbacks.delete(callback);
onces.delete(callback);
};
const mql = {
get matches() {
return cssMediaquery.match(queryTyped, state);
},
media: query,
onchange: null,
addEventListener: (event, callback, options) => {
if (event === "change" && callback) {
const isAlreadyListed = callbacks.has(callback);
callbacks.add(callback);
const hasOnce = typeof options === "object" && (options == null ? void 0 : options.once); // If it doesn’t have `once: true`, but it was previously added with one, the `once` status should be lifted
if (!hasOnce) {
onces.delete(callback);
return;
} // If the callback is already listed in the list of callback to call, but not as a `once`,
// it means that it was added without the flag and thus shouldn’t be treated as such.
if (isAlreadyListed && !onces.has(callback)) {
return;
} // Otherwise, use the `once` flag
onces.add(callback);
}
},
removeEventListener: (event, callback) => {
if (event === "change") removeListener(callback);
},
dispatchEvent: event => {
if (!event) {
throw new TypeError(`Failed to execute 'dispatchEvent' on 'EventTarget': 1 argument required, but only 0 present.`);
}
if (!(event instanceof EventCompat)) {
throw new TypeError(`Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.`);
}
if (event.type !== "change") {
return true;
}
mql.onchange == null ? void 0 : mql.onchange(event);
callbacks.forEach(callback => {
callback(event);
if (onces.has(callback)) {
removeListener(callback);
}
}); // TODO: target and currentTarget
// Object.defineProperty(event, "target", { value: mql });
return true;
},
addListener: callback => {
if (!callback) return;
callbacks.add(callback);
},
removeListener: callback => {
if (!callback) return;
removeListener(callback);
}
};
MQLs.set(mql, {
previousMatched,
clear,
features: getFeaturesFromQuery(queryTyped)
});
return mql;
};
class MediaQueryListEvent extends EventCompat {
constructor(type, options = {}) {
super(type);
this.media = void 0;
this.matches = void 0;
this.media = options.media || "";
this.matches = options.matches || false;
}
} // Cannot use MediaState here as setMedia is exposed in the API
const setMedia = media => {
const changedFeatures = new Set();
Object.keys(media).forEach(feature => {
changedFeatures.add(feature);
state[feature] = media[feature];
});
for (const [MQL, cache] of MQLs) {
let found = false;
for (const feature of cache.features) {
if (changedFeatures.has(feature)) {
found = true;
break;
}
}
if (!found) {
continue;
}
const matches = cssMediaquery.match(MQL.media, state);
if (matches === cache.previousMatched) {
continue;
}
cache.previousMatched = matches;
MQL.dispatchEvent(new MediaQueryListEvent("change", {
matches,
media: MQL.media
}));
}
};
const cleanupListeners = () => {
for (const {
clear
} of MQLs.values()) {
clear();
}
MQLs.clear();
};
const cleanupMedia = () => {
state = {};
};
const cleanup = () => {
cleanupListeners();
cleanupMedia();
};
exports.MediaQueryListEvent = MediaQueryListEvent;
exports.cleanup = cleanup;
exports.cleanupListeners = cleanupListeners;
exports.cleanupMedia = cleanupMedia;
exports.matchMedia = matchMedia;
exports.setMedia = setMedia;
//# sourceMappingURL=index.js.map