UNPKG

mock-match-media

Version:

mock window.matchMedia for tests or node

226 lines (185 loc) 5.71 kB
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