@the-w4/responsive-video-source 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The W4
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # @the-w4/responsive-video-source
2
+
3
+ Switches a `<video>`'s `<source>` children based on a `data-resolution`
4
+ media-query attribute, so the right mp4/webm pair loads for the current
5
+ viewport instead of relying on the browser's (resolution-blind) native
6
+ `<source>` matching.
7
+
8
+ ## The problem this solves
9
+
10
+ `<video>` lets you list multiple `<source>` elements, but the browser only
11
+ uses `type` to pick one — it has no concept of viewport size. If you want a
12
+ different clip (or just a different encode) on mobile vs. desktop, you need
13
+ JS. This package does that by reading a `data-resolution` attribute you put
14
+ on each `<source>`:
15
+
16
+ ```html
17
+ <video autoplay muted loop playsinline>
18
+ <source src="/desktop.mp4" type="video/mp4" data-resolution="min-width: 1024px">
19
+ <source src="/desktop.webm" type="video/webm" data-resolution="min-width: 1024px">
20
+
21
+ <source src="/mobile.mp4" type="video/mp4" data-resolution="max-width: 1023px">
22
+ <source src="/mobile.webm" type="video/webm" data-resolution="max-width: 1023px">
23
+ </video>
24
+ ```
25
+
26
+ Sources are grouped by their `data-resolution` value. When a group's query
27
+ matches, the video is given only that group's `<source>` elements (so the
28
+ browser still gets to pick mp4 vs. webm based on support within the matched
29
+ group) and reloaded. Listeners are added per unique breakpoint via
30
+ `matchMedia`, so switching happens automatically on resize/orientation
31
+ change — no manual resize polling.
32
+
33
+ **Videos with a partial data set are skipped.** If a `<video>` has some
34
+ `<source>` children with `data-resolution` and some without, the package
35
+ leaves it completely untouched (native behavior applies). Only videos where
36
+ *every* `<source>` carries the attribute are managed. A video with no
37
+ `data-resolution` attributes at all is also left alone.
38
+
39
+ The attribute value can be a bare media feature (`"min-width: 1024px"`,
40
+ wrapped in parens automatically) or a full query you write yourself
41
+ (`"(min-width: 1024px) and (orientation: landscape)"`, used as-is). Keep
42
+ breakpoints mutually exclusive (e.g. `min-width` / `max-width` pairs) —
43
+ if more than one group matches at once, the first one declared in the
44
+ markup wins.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ npm install @the-w4/responsive-video-source
50
+ ```
51
+
52
+ ## Usage with a bundler (e.g. @wordpress/scripts, webpack, Vite)
53
+
54
+ ```js
55
+ import { init } from '@the-w4/responsive-video-source';
56
+
57
+ const controller = init();
58
+
59
+ // later, if needed:
60
+ controller.refresh(); // re-check all managed videos now
61
+ controller.destroy(); // remove all matchMedia listeners
62
+ ```
63
+
64
+ `init(options)` accepts:
65
+
66
+ | option | default | description |
67
+ |-------------|----------------------|-----------------------------------------------|
68
+ | `root` | `document` | scope to search within |
69
+ | `selector` | `'video'` | selector for video elements |
70
+ | `attribute` | `'data-resolution'` | attribute holding the media-query value |
71
+
72
+ ## Usage directly in WordPress (no build step)
73
+
74
+ Copy `dist/index.global.min.js` into your theme and enqueue it — it
75
+ auto-runs on `DOMContentLoaded` with the defaults shown above:
76
+
77
+ ```php
78
+ wp_enqueue_script(
79
+ 'responsive-video-source',
80
+ get_stylesheet_directory_uri() . '/js/responsive-video-source.min.js',
81
+ array(),
82
+ '0.1.0',
83
+ true
84
+ );
85
+ ```
86
+
87
+ To opt out of auto-init (e.g. you want a custom `root`/`selector`, or to
88
+ call `init()` yourself), add `data-no-autoinit` to the script tag. WordPress
89
+ doesn't expose a clean way to add arbitrary attributes to `wp_enqueue_script`
90
+ output, so either filter the `script_loader_tag` or just `init()` is also
91
+ fully exposed on `window.ResponsiveVideoSource` for manual control:
92
+
93
+ ```js
94
+ window.ResponsiveVideoSource.init({ root: document.querySelector('.hero') });
95
+ ```
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ npm install
101
+ npm run build # writes dist/index.mjs, .cjs, .global.js, .global.min.js
102
+ npm test # jsdom-based unit tests
103
+ ```
104
+
105
+ ## Publishing
106
+
107
+ ```bash
108
+ npm login # if not already authenticated
109
+ npm publish # prepublishOnly runs build + test automatically
110
+ ```
111
+
112
+ This is a scoped package (`@the-w4/...`); `publishConfig.access` is already
113
+ set to `public` so it publishes for free under the `the-w4` npm scope.
package/dist/index.cjs ADDED
@@ -0,0 +1,133 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.js
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ autoInit: () => autoInit,
23
+ init: () => init
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var DEFAULT_ATTRIBUTE = "data-resolution";
27
+ var DEFAULT_SELECTOR = "video";
28
+ function toMediaQuery(value) {
29
+ const trimmed = value.trim();
30
+ return trimmed.startsWith("(") ? trimmed : `(${trimmed})`;
31
+ }
32
+ function getDirectSourceChildren(video) {
33
+ return Array.from(video.children).filter(
34
+ (el) => el.tagName === "SOURCE"
35
+ );
36
+ }
37
+ function readSourceGroups(video, attribute) {
38
+ const sources = getDirectSourceChildren(video);
39
+ if (sources.length === 0) return null;
40
+ const withAttr = sources.filter((s) => s.hasAttribute(attribute));
41
+ if (withAttr.length !== sources.length) return null;
42
+ const order = [];
43
+ const groups = /* @__PURE__ */ new Map();
44
+ sources.forEach((s) => {
45
+ const key = s.getAttribute(attribute).trim();
46
+ if (!groups.has(key)) {
47
+ groups.set(key, []);
48
+ order.push(key);
49
+ }
50
+ groups.get(key).push({
51
+ src: s.getAttribute("src"),
52
+ type: s.getAttribute("type") || ""
53
+ });
54
+ });
55
+ return { order, groups };
56
+ }
57
+ function applySources(video, items) {
58
+ getDirectSourceChildren(video).forEach((s) => s.remove());
59
+ items.forEach(({ src, type }) => {
60
+ const source = document.createElement("source");
61
+ source.setAttribute("src", src);
62
+ if (type) source.setAttribute("type", type);
63
+ video.appendChild(source);
64
+ });
65
+ try {
66
+ video.load();
67
+ } catch (e) {
68
+ }
69
+ if (video.autoplay) {
70
+ try {
71
+ const playResult = video.play();
72
+ if (playResult && typeof playResult.catch === "function") {
73
+ playResult.catch(() => {
74
+ });
75
+ }
76
+ } catch (e) {
77
+ }
78
+ }
79
+ }
80
+ function setupVideo(video, attribute) {
81
+ const data = readSourceGroups(video, attribute);
82
+ if (!data) return null;
83
+ const { order, groups } = data;
84
+ const queries = order.map((key) => ({
85
+ key,
86
+ mql: window.matchMedia(toMediaQuery(key))
87
+ }));
88
+ let currentKey = null;
89
+ function pickKey() {
90
+ const match = queries.find((q) => q.mql.matches);
91
+ return match ? match.key : null;
92
+ }
93
+ function evaluate() {
94
+ const key = pickKey();
95
+ if (key === null || key === currentKey) return;
96
+ currentKey = key;
97
+ applySources(video, groups.get(key));
98
+ }
99
+ const handler = () => evaluate();
100
+ queries.forEach((q) => q.mql.addEventListener("change", handler));
101
+ evaluate();
102
+ return {
103
+ evaluate,
104
+ destroy() {
105
+ queries.forEach((q) => q.mql.removeEventListener("change", handler));
106
+ }
107
+ };
108
+ }
109
+ function init(options = {}) {
110
+ const {
111
+ root = document,
112
+ selector = DEFAULT_SELECTOR,
113
+ attribute = DEFAULT_ATTRIBUTE
114
+ } = options;
115
+ const controllers = Array.from(root.querySelectorAll(selector)).map((video) => setupVideo(video, attribute)).filter(Boolean);
116
+ return {
117
+ refresh() {
118
+ controllers.forEach((c) => c.evaluate());
119
+ },
120
+ destroy() {
121
+ controllers.forEach((c) => c.destroy());
122
+ }
123
+ };
124
+ }
125
+ function autoInit(options) {
126
+ const run = () => init(options);
127
+ if (document.readyState === "loading") {
128
+ document.addEventListener("DOMContentLoaded", run, { once: true });
129
+ } else {
130
+ run();
131
+ }
132
+ }
133
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.js"],
4
+ "sourcesContent": ["const DEFAULT_ATTRIBUTE = 'data-resolution';\nconst DEFAULT_SELECTOR = 'video';\n\n/**\n * Wraps a bare media feature (\"min-width: 1024px\") in parens so it can be\n * passed to matchMedia(). Values that are already a full query (start with\n * \"(\") are left untouched so compound queries keep working.\n */\nfunction toMediaQuery(value) {\n const trimmed = value.trim();\n return trimmed.startsWith('(') ? trimmed : `(${trimmed})`;\n}\n\nfunction getDirectSourceChildren(video) {\n return Array.from(video.children).filter(\n (el) => el.tagName === 'SOURCE'\n );\n}\n\n/**\n * Groups a video's <source> elements by their data-resolution value.\n * Returns null if the video has no sources, or only some of them carry the\n * attribute (a \"partial\" data set) -- those videos are left untouched so\n * native browser behavior still applies.\n */\nfunction readSourceGroups(video, attribute) {\n const sources = getDirectSourceChildren(video);\n if (sources.length === 0) return null;\n\n const withAttr = sources.filter((s) => s.hasAttribute(attribute));\n if (withAttr.length !== sources.length) return null;\n\n const order = [];\n const groups = new Map();\n\n sources.forEach((s) => {\n const key = s.getAttribute(attribute).trim();\n if (!groups.has(key)) {\n groups.set(key, []);\n order.push(key);\n }\n groups.get(key).push({\n src: s.getAttribute('src'),\n type: s.getAttribute('type') || '',\n });\n });\n\n return { order, groups };\n}\n\nfunction applySources(video, items) {\n getDirectSourceChildren(video).forEach((s) => s.remove());\n\n items.forEach(({ src, type }) => {\n const source = document.createElement('source');\n source.setAttribute('src', src);\n if (type) source.setAttribute('type', type);\n video.appendChild(source);\n });\n\n try {\n video.load();\n } catch (e) {\n /* media APIs can be unimplemented in some test/SSR environments */\n }\n\n if (video.autoplay) {\n try {\n const playResult = video.play();\n if (playResult && typeof playResult.catch === 'function') {\n playResult.catch(() => {});\n }\n } catch (e) {\n /* autoplay can be rejected/unsupported; not our concern here */\n }\n }\n}\n\nfunction setupVideo(video, attribute) {\n const data = readSourceGroups(video, attribute);\n if (!data) return null;\n\n const { order, groups } = data;\n\n // One matchMedia listener per unique breakpoint, not per <source>.\n const queries = order.map((key) => ({\n key,\n mql: window.matchMedia(toMediaQuery(key)),\n }));\n\n let currentKey = null;\n\n function pickKey() {\n const match = queries.find((q) => q.mql.matches);\n return match ? match.key : null;\n }\n\n // If nothing matches (a gap between breakpoints), leave things as they\n // are rather than blanking the video out.\n function evaluate() {\n const key = pickKey();\n if (key === null || key === currentKey) return;\n currentKey = key;\n applySources(video, groups.get(key));\n }\n\n const handler = () => evaluate();\n queries.forEach((q) => q.mql.addEventListener('change', handler));\n\n evaluate();\n\n return {\n evaluate,\n destroy() {\n queries.forEach((q) => q.mql.removeEventListener('change', handler));\n },\n };\n}\n\n/**\n * Scans for <video> elements and switches their <source> children based on\n * a data-resolution media-query attribute. Videos where only some <source>\n * elements carry the attribute are skipped entirely.\n *\n * @param {Object} [options]\n * @param {Document|Element} [options.root=document] - scope to search within\n * @param {string} [options.selector='video'] - selector for video elements\n * @param {string} [options.attribute='data-resolution'] - attribute to read\n * @returns {{ refresh: () => void, destroy: () => void }}\n */\nexport function init(options = {}) {\n const {\n root = document,\n selector = DEFAULT_SELECTOR,\n attribute = DEFAULT_ATTRIBUTE,\n } = options;\n\n const controllers = Array.from(root.querySelectorAll(selector))\n .map((video) => setupVideo(video, attribute))\n .filter(Boolean);\n\n return {\n refresh() {\n controllers.forEach((c) => c.evaluate());\n },\n destroy() {\n controllers.forEach((c) => c.destroy());\n },\n };\n}\n\n/**\n * Convenience helper for the standalone <script> build: runs init() once\n * the DOM is ready, using the given options (or defaults).\n */\nexport function autoInit(options) {\n const run = () => init(options);\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', run, { once: true });\n } else {\n run();\n }\n}"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,18 @@
1
+ export interface InitOptions {
2
+ /** Scope to search within. Defaults to `document`. */
3
+ root?: Document | Element;
4
+ /** Selector for video elements. Defaults to `'video'`. */
5
+ selector?: string;
6
+ /** Attribute holding the media-query value. Defaults to `'data-resolution'`. */
7
+ attribute?: string;
8
+ }
9
+
10
+ export interface Controller {
11
+ /** Re-runs the breakpoint check for every managed video. */
12
+ refresh(): void;
13
+ /** Removes all matchMedia listeners added by init(). */
14
+ destroy(): void;
15
+ }
16
+
17
+ export function init(options?: InitOptions): Controller;
18
+ export function autoInit(options?: InitOptions): void;
@@ -0,0 +1,143 @@
1
+ var ResponsiveVideoSource = (() => {
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.js
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ autoInit: () => autoInit,
24
+ init: () => init
25
+ });
26
+ var DEFAULT_ATTRIBUTE = "data-resolution";
27
+ var DEFAULT_SELECTOR = "video";
28
+ function toMediaQuery(value) {
29
+ const trimmed = value.trim();
30
+ return trimmed.startsWith("(") ? trimmed : `(${trimmed})`;
31
+ }
32
+ function getDirectSourceChildren(video) {
33
+ return Array.from(video.children).filter(
34
+ (el) => el.tagName === "SOURCE"
35
+ );
36
+ }
37
+ function readSourceGroups(video, attribute) {
38
+ const sources = getDirectSourceChildren(video);
39
+ if (sources.length === 0) return null;
40
+ const withAttr = sources.filter((s) => s.hasAttribute(attribute));
41
+ if (withAttr.length !== sources.length) return null;
42
+ const order = [];
43
+ const groups = /* @__PURE__ */ new Map();
44
+ sources.forEach((s) => {
45
+ const key = s.getAttribute(attribute).trim();
46
+ if (!groups.has(key)) {
47
+ groups.set(key, []);
48
+ order.push(key);
49
+ }
50
+ groups.get(key).push({
51
+ src: s.getAttribute("src"),
52
+ type: s.getAttribute("type") || ""
53
+ });
54
+ });
55
+ return { order, groups };
56
+ }
57
+ function applySources(video, items) {
58
+ getDirectSourceChildren(video).forEach((s) => s.remove());
59
+ items.forEach(({ src, type }) => {
60
+ const source = document.createElement("source");
61
+ source.setAttribute("src", src);
62
+ if (type) source.setAttribute("type", type);
63
+ video.appendChild(source);
64
+ });
65
+ try {
66
+ video.load();
67
+ } catch (e) {
68
+ }
69
+ if (video.autoplay) {
70
+ try {
71
+ const playResult = video.play();
72
+ if (playResult && typeof playResult.catch === "function") {
73
+ playResult.catch(() => {
74
+ });
75
+ }
76
+ } catch (e) {
77
+ }
78
+ }
79
+ }
80
+ function setupVideo(video, attribute) {
81
+ const data = readSourceGroups(video, attribute);
82
+ if (!data) return null;
83
+ const { order, groups } = data;
84
+ const queries = order.map((key) => ({
85
+ key,
86
+ mql: window.matchMedia(toMediaQuery(key))
87
+ }));
88
+ let currentKey = null;
89
+ function pickKey() {
90
+ const match = queries.find((q) => q.mql.matches);
91
+ return match ? match.key : null;
92
+ }
93
+ function evaluate() {
94
+ const key = pickKey();
95
+ if (key === null || key === currentKey) return;
96
+ currentKey = key;
97
+ applySources(video, groups.get(key));
98
+ }
99
+ const handler = () => evaluate();
100
+ queries.forEach((q) => q.mql.addEventListener("change", handler));
101
+ evaluate();
102
+ return {
103
+ evaluate,
104
+ destroy() {
105
+ queries.forEach((q) => q.mql.removeEventListener("change", handler));
106
+ }
107
+ };
108
+ }
109
+ function init(options = {}) {
110
+ const {
111
+ root = document,
112
+ selector = DEFAULT_SELECTOR,
113
+ attribute = DEFAULT_ATTRIBUTE
114
+ } = options;
115
+ const controllers = Array.from(root.querySelectorAll(selector)).map((video) => setupVideo(video, attribute)).filter(Boolean);
116
+ return {
117
+ refresh() {
118
+ controllers.forEach((c) => c.evaluate());
119
+ },
120
+ destroy() {
121
+ controllers.forEach((c) => c.destroy());
122
+ }
123
+ };
124
+ }
125
+ function autoInit(options) {
126
+ const run = () => init(options);
127
+ if (document.readyState === "loading") {
128
+ document.addEventListener("DOMContentLoaded", run, { once: true });
129
+ } else {
130
+ run();
131
+ }
132
+ }
133
+ return __toCommonJS(index_exports);
134
+ })();
135
+
136
+ (function () {
137
+ if (typeof document === 'undefined') return;
138
+ var script = document.currentScript;
139
+ if (script && script.hasAttribute('data-no-autoinit')) return;
140
+ ResponsiveVideoSource.autoInit();
141
+ })();
142
+
143
+ //# sourceMappingURL=index.global.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.js"],
4
+ "sourcesContent": ["const DEFAULT_ATTRIBUTE = 'data-resolution';\nconst DEFAULT_SELECTOR = 'video';\n\n/**\n * Wraps a bare media feature (\"min-width: 1024px\") in parens so it can be\n * passed to matchMedia(). Values that are already a full query (start with\n * \"(\") are left untouched so compound queries keep working.\n */\nfunction toMediaQuery(value) {\n const trimmed = value.trim();\n return trimmed.startsWith('(') ? trimmed : `(${trimmed})`;\n}\n\nfunction getDirectSourceChildren(video) {\n return Array.from(video.children).filter(\n (el) => el.tagName === 'SOURCE'\n );\n}\n\n/**\n * Groups a video's <source> elements by their data-resolution value.\n * Returns null if the video has no sources, or only some of them carry the\n * attribute (a \"partial\" data set) -- those videos are left untouched so\n * native browser behavior still applies.\n */\nfunction readSourceGroups(video, attribute) {\n const sources = getDirectSourceChildren(video);\n if (sources.length === 0) return null;\n\n const withAttr = sources.filter((s) => s.hasAttribute(attribute));\n if (withAttr.length !== sources.length) return null;\n\n const order = [];\n const groups = new Map();\n\n sources.forEach((s) => {\n const key = s.getAttribute(attribute).trim();\n if (!groups.has(key)) {\n groups.set(key, []);\n order.push(key);\n }\n groups.get(key).push({\n src: s.getAttribute('src'),\n type: s.getAttribute('type') || '',\n });\n });\n\n return { order, groups };\n}\n\nfunction applySources(video, items) {\n getDirectSourceChildren(video).forEach((s) => s.remove());\n\n items.forEach(({ src, type }) => {\n const source = document.createElement('source');\n source.setAttribute('src', src);\n if (type) source.setAttribute('type', type);\n video.appendChild(source);\n });\n\n try {\n video.load();\n } catch (e) {\n /* media APIs can be unimplemented in some test/SSR environments */\n }\n\n if (video.autoplay) {\n try {\n const playResult = video.play();\n if (playResult && typeof playResult.catch === 'function') {\n playResult.catch(() => {});\n }\n } catch (e) {\n /* autoplay can be rejected/unsupported; not our concern here */\n }\n }\n}\n\nfunction setupVideo(video, attribute) {\n const data = readSourceGroups(video, attribute);\n if (!data) return null;\n\n const { order, groups } = data;\n\n // One matchMedia listener per unique breakpoint, not per <source>.\n const queries = order.map((key) => ({\n key,\n mql: window.matchMedia(toMediaQuery(key)),\n }));\n\n let currentKey = null;\n\n function pickKey() {\n const match = queries.find((q) => q.mql.matches);\n return match ? match.key : null;\n }\n\n // If nothing matches (a gap between breakpoints), leave things as they\n // are rather than blanking the video out.\n function evaluate() {\n const key = pickKey();\n if (key === null || key === currentKey) return;\n currentKey = key;\n applySources(video, groups.get(key));\n }\n\n const handler = () => evaluate();\n queries.forEach((q) => q.mql.addEventListener('change', handler));\n\n evaluate();\n\n return {\n evaluate,\n destroy() {\n queries.forEach((q) => q.mql.removeEventListener('change', handler));\n },\n };\n}\n\n/**\n * Scans for <video> elements and switches their <source> children based on\n * a data-resolution media-query attribute. Videos where only some <source>\n * elements carry the attribute are skipped entirely.\n *\n * @param {Object} [options]\n * @param {Document|Element} [options.root=document] - scope to search within\n * @param {string} [options.selector='video'] - selector for video elements\n * @param {string} [options.attribute='data-resolution'] - attribute to read\n * @returns {{ refresh: () => void, destroy: () => void }}\n */\nexport function init(options = {}) {\n const {\n root = document,\n selector = DEFAULT_SELECTOR,\n attribute = DEFAULT_ATTRIBUTE,\n } = options;\n\n const controllers = Array.from(root.querySelectorAll(selector))\n .map((video) => setupVideo(video, attribute))\n .filter(Boolean);\n\n return {\n refresh() {\n controllers.forEach((c) => c.evaluate());\n },\n destroy() {\n controllers.forEach((c) => c.destroy());\n },\n };\n}\n\n/**\n * Convenience helper for the standalone <script> build: runs init() once\n * the DOM is ready, using the given options (or defaults).\n */\nexport function autoInit(options) {\n const run = () => init(options);\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', run, { once: true });\n } else {\n run();\n }\n}"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,10 @@
1
+ var ResponsiveVideoSource=(()=>{var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var g=(e,t)=>{for(var r in t)l(e,r,{get:t[r],enumerable:!0})},A=(e,t,r,u)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of y(t))!E.call(e,n)&&n!==r&&l(e,n,{get:()=>t[n],enumerable:!(u=p(t,n))||u.enumerable});return e};var L=e=>A(l({},"__esModule",{value:!0}),e);var M={};g(M,{autoInit:()=>D,init:()=>d});var S="data-resolution",b="video";function q(e){let t=e.trim();return t.startsWith("(")?t:`(${t})`}function h(e){return Array.from(e.children).filter(t=>t.tagName==="SOURCE")}function T(e,t){let r=h(e);if(r.length===0||r.filter(a=>a.hasAttribute(t)).length!==r.length)return null;let n=[],o=new Map;return r.forEach(a=>{let s=a.getAttribute(t).trim();o.has(s)||(o.set(s,[]),n.push(s)),o.get(s).push({src:a.getAttribute("src"),type:a.getAttribute("type")||""})}),{order:n,groups:o}}function w(e,t){h(e).forEach(r=>r.remove()),t.forEach(({src:r,type:u})=>{let n=document.createElement("source");n.setAttribute("src",r),u&&n.setAttribute("type",u),e.appendChild(n)});try{e.load()}catch{}if(e.autoplay)try{let r=e.play();r&&typeof r.catch=="function"&&r.catch(()=>{})}catch{}}function C(e,t){let r=T(e,t);if(!r)return null;let{order:u,groups:n}=r,o=u.map(c=>({key:c,mql:window.matchMedia(q(c))})),a=null;function s(){let c=o.find(m=>m.mql.matches);return c?c.key:null}function i(){let c=s();c===null||c===a||(a=c,w(e,n.get(c)))}let f=()=>i();return o.forEach(c=>c.mql.addEventListener("change",f)),i(),{evaluate:i,destroy(){o.forEach(c=>c.mql.removeEventListener("change",f))}}}function d(e={}){let{root:t=document,selector:r=b,attribute:u=S}=e,n=Array.from(t.querySelectorAll(r)).map(o=>C(o,u)).filter(Boolean);return{refresh(){n.forEach(o=>o.evaluate())},destroy(){n.forEach(o=>o.destroy())}}}function D(e){let t=()=>d(e);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()}return L(M);})();
2
+
3
+ (function () {
4
+ if (typeof document === 'undefined') return;
5
+ var script = document.currentScript;
6
+ if (script && script.hasAttribute('data-no-autoinit')) return;
7
+ ResponsiveVideoSource.autoInit();
8
+ })();
9
+
10
+ //# sourceMappingURL=index.global.min.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.js"],
4
+ "sourcesContent": ["const DEFAULT_ATTRIBUTE = 'data-resolution';\nconst DEFAULT_SELECTOR = 'video';\n\n/**\n * Wraps a bare media feature (\"min-width: 1024px\") in parens so it can be\n * passed to matchMedia(). Values that are already a full query (start with\n * \"(\") are left untouched so compound queries keep working.\n */\nfunction toMediaQuery(value) {\n const trimmed = value.trim();\n return trimmed.startsWith('(') ? trimmed : `(${trimmed})`;\n}\n\nfunction getDirectSourceChildren(video) {\n return Array.from(video.children).filter(\n (el) => el.tagName === 'SOURCE'\n );\n}\n\n/**\n * Groups a video's <source> elements by their data-resolution value.\n * Returns null if the video has no sources, or only some of them carry the\n * attribute (a \"partial\" data set) -- those videos are left untouched so\n * native browser behavior still applies.\n */\nfunction readSourceGroups(video, attribute) {\n const sources = getDirectSourceChildren(video);\n if (sources.length === 0) return null;\n\n const withAttr = sources.filter((s) => s.hasAttribute(attribute));\n if (withAttr.length !== sources.length) return null;\n\n const order = [];\n const groups = new Map();\n\n sources.forEach((s) => {\n const key = s.getAttribute(attribute).trim();\n if (!groups.has(key)) {\n groups.set(key, []);\n order.push(key);\n }\n groups.get(key).push({\n src: s.getAttribute('src'),\n type: s.getAttribute('type') || '',\n });\n });\n\n return { order, groups };\n}\n\nfunction applySources(video, items) {\n getDirectSourceChildren(video).forEach((s) => s.remove());\n\n items.forEach(({ src, type }) => {\n const source = document.createElement('source');\n source.setAttribute('src', src);\n if (type) source.setAttribute('type', type);\n video.appendChild(source);\n });\n\n try {\n video.load();\n } catch (e) {\n /* media APIs can be unimplemented in some test/SSR environments */\n }\n\n if (video.autoplay) {\n try {\n const playResult = video.play();\n if (playResult && typeof playResult.catch === 'function') {\n playResult.catch(() => {});\n }\n } catch (e) {\n /* autoplay can be rejected/unsupported; not our concern here */\n }\n }\n}\n\nfunction setupVideo(video, attribute) {\n const data = readSourceGroups(video, attribute);\n if (!data) return null;\n\n const { order, groups } = data;\n\n // One matchMedia listener per unique breakpoint, not per <source>.\n const queries = order.map((key) => ({\n key,\n mql: window.matchMedia(toMediaQuery(key)),\n }));\n\n let currentKey = null;\n\n function pickKey() {\n const match = queries.find((q) => q.mql.matches);\n return match ? match.key : null;\n }\n\n // If nothing matches (a gap between breakpoints), leave things as they\n // are rather than blanking the video out.\n function evaluate() {\n const key = pickKey();\n if (key === null || key === currentKey) return;\n currentKey = key;\n applySources(video, groups.get(key));\n }\n\n const handler = () => evaluate();\n queries.forEach((q) => q.mql.addEventListener('change', handler));\n\n evaluate();\n\n return {\n evaluate,\n destroy() {\n queries.forEach((q) => q.mql.removeEventListener('change', handler));\n },\n };\n}\n\n/**\n * Scans for <video> elements and switches their <source> children based on\n * a data-resolution media-query attribute. Videos where only some <source>\n * elements carry the attribute are skipped entirely.\n *\n * @param {Object} [options]\n * @param {Document|Element} [options.root=document] - scope to search within\n * @param {string} [options.selector='video'] - selector for video elements\n * @param {string} [options.attribute='data-resolution'] - attribute to read\n * @returns {{ refresh: () => void, destroy: () => void }}\n */\nexport function init(options = {}) {\n const {\n root = document,\n selector = DEFAULT_SELECTOR,\n attribute = DEFAULT_ATTRIBUTE,\n } = options;\n\n const controllers = Array.from(root.querySelectorAll(selector))\n .map((video) => setupVideo(video, attribute))\n .filter(Boolean);\n\n return {\n refresh() {\n controllers.forEach((c) => c.evaluate());\n },\n destroy() {\n controllers.forEach((c) => c.destroy());\n },\n };\n}\n\n/**\n * Convenience helper for the standalone <script> build: runs init() once\n * the DOM is ready, using the given options (or defaults).\n */\nexport function autoInit(options) {\n const run = () => init(options);\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', run, { once: true });\n } else {\n run();\n }\n}"],
5
+ "mappings": "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",
6
+ "names": ["index_exports", "__export", "autoInit", "init", "DEFAULT_ATTRIBUTE", "DEFAULT_SELECTOR", "toMediaQuery", "value", "trimmed", "getDirectSourceChildren", "video", "el", "readSourceGroups", "attribute", "sources", "s", "order", "groups", "key", "applySources", "items", "src", "type", "source", "playResult", "setupVideo", "data", "queries", "currentKey", "pickKey", "match", "q", "evaluate", "handler", "options", "root", "selector", "controllers", "c", "run"]
7
+ }
package/dist/index.mjs ADDED
@@ -0,0 +1,113 @@
1
+ // src/index.js
2
+ var DEFAULT_ATTRIBUTE = "data-resolution";
3
+ var DEFAULT_SELECTOR = "video";
4
+ function toMediaQuery(value) {
5
+ const trimmed = value.trim();
6
+ return trimmed.startsWith("(") ? trimmed : `(${trimmed})`;
7
+ }
8
+ function getDirectSourceChildren(video) {
9
+ return Array.from(video.children).filter(
10
+ (el) => el.tagName === "SOURCE"
11
+ );
12
+ }
13
+ function readSourceGroups(video, attribute) {
14
+ const sources = getDirectSourceChildren(video);
15
+ if (sources.length === 0) return null;
16
+ const withAttr = sources.filter((s) => s.hasAttribute(attribute));
17
+ if (withAttr.length !== sources.length) return null;
18
+ const order = [];
19
+ const groups = /* @__PURE__ */ new Map();
20
+ sources.forEach((s) => {
21
+ const key = s.getAttribute(attribute).trim();
22
+ if (!groups.has(key)) {
23
+ groups.set(key, []);
24
+ order.push(key);
25
+ }
26
+ groups.get(key).push({
27
+ src: s.getAttribute("src"),
28
+ type: s.getAttribute("type") || ""
29
+ });
30
+ });
31
+ return { order, groups };
32
+ }
33
+ function applySources(video, items) {
34
+ getDirectSourceChildren(video).forEach((s) => s.remove());
35
+ items.forEach(({ src, type }) => {
36
+ const source = document.createElement("source");
37
+ source.setAttribute("src", src);
38
+ if (type) source.setAttribute("type", type);
39
+ video.appendChild(source);
40
+ });
41
+ try {
42
+ video.load();
43
+ } catch (e) {
44
+ }
45
+ if (video.autoplay) {
46
+ try {
47
+ const playResult = video.play();
48
+ if (playResult && typeof playResult.catch === "function") {
49
+ playResult.catch(() => {
50
+ });
51
+ }
52
+ } catch (e) {
53
+ }
54
+ }
55
+ }
56
+ function setupVideo(video, attribute) {
57
+ const data = readSourceGroups(video, attribute);
58
+ if (!data) return null;
59
+ const { order, groups } = data;
60
+ const queries = order.map((key) => ({
61
+ key,
62
+ mql: window.matchMedia(toMediaQuery(key))
63
+ }));
64
+ let currentKey = null;
65
+ function pickKey() {
66
+ const match = queries.find((q) => q.mql.matches);
67
+ return match ? match.key : null;
68
+ }
69
+ function evaluate() {
70
+ const key = pickKey();
71
+ if (key === null || key === currentKey) return;
72
+ currentKey = key;
73
+ applySources(video, groups.get(key));
74
+ }
75
+ const handler = () => evaluate();
76
+ queries.forEach((q) => q.mql.addEventListener("change", handler));
77
+ evaluate();
78
+ return {
79
+ evaluate,
80
+ destroy() {
81
+ queries.forEach((q) => q.mql.removeEventListener("change", handler));
82
+ }
83
+ };
84
+ }
85
+ function init(options = {}) {
86
+ const {
87
+ root = document,
88
+ selector = DEFAULT_SELECTOR,
89
+ attribute = DEFAULT_ATTRIBUTE
90
+ } = options;
91
+ const controllers = Array.from(root.querySelectorAll(selector)).map((video) => setupVideo(video, attribute)).filter(Boolean);
92
+ return {
93
+ refresh() {
94
+ controllers.forEach((c) => c.evaluate());
95
+ },
96
+ destroy() {
97
+ controllers.forEach((c) => c.destroy());
98
+ }
99
+ };
100
+ }
101
+ function autoInit(options) {
102
+ const run = () => init(options);
103
+ if (document.readyState === "loading") {
104
+ document.addEventListener("DOMContentLoaded", run, { once: true });
105
+ } else {
106
+ run();
107
+ }
108
+ }
109
+ export {
110
+ autoInit,
111
+ init
112
+ };
113
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.js"],
4
+ "sourcesContent": ["const DEFAULT_ATTRIBUTE = 'data-resolution';\nconst DEFAULT_SELECTOR = 'video';\n\n/**\n * Wraps a bare media feature (\"min-width: 1024px\") in parens so it can be\n * passed to matchMedia(). Values that are already a full query (start with\n * \"(\") are left untouched so compound queries keep working.\n */\nfunction toMediaQuery(value) {\n const trimmed = value.trim();\n return trimmed.startsWith('(') ? trimmed : `(${trimmed})`;\n}\n\nfunction getDirectSourceChildren(video) {\n return Array.from(video.children).filter(\n (el) => el.tagName === 'SOURCE'\n );\n}\n\n/**\n * Groups a video's <source> elements by their data-resolution value.\n * Returns null if the video has no sources, or only some of them carry the\n * attribute (a \"partial\" data set) -- those videos are left untouched so\n * native browser behavior still applies.\n */\nfunction readSourceGroups(video, attribute) {\n const sources = getDirectSourceChildren(video);\n if (sources.length === 0) return null;\n\n const withAttr = sources.filter((s) => s.hasAttribute(attribute));\n if (withAttr.length !== sources.length) return null;\n\n const order = [];\n const groups = new Map();\n\n sources.forEach((s) => {\n const key = s.getAttribute(attribute).trim();\n if (!groups.has(key)) {\n groups.set(key, []);\n order.push(key);\n }\n groups.get(key).push({\n src: s.getAttribute('src'),\n type: s.getAttribute('type') || '',\n });\n });\n\n return { order, groups };\n}\n\nfunction applySources(video, items) {\n getDirectSourceChildren(video).forEach((s) => s.remove());\n\n items.forEach(({ src, type }) => {\n const source = document.createElement('source');\n source.setAttribute('src', src);\n if (type) source.setAttribute('type', type);\n video.appendChild(source);\n });\n\n try {\n video.load();\n } catch (e) {\n /* media APIs can be unimplemented in some test/SSR environments */\n }\n\n if (video.autoplay) {\n try {\n const playResult = video.play();\n if (playResult && typeof playResult.catch === 'function') {\n playResult.catch(() => {});\n }\n } catch (e) {\n /* autoplay can be rejected/unsupported; not our concern here */\n }\n }\n}\n\nfunction setupVideo(video, attribute) {\n const data = readSourceGroups(video, attribute);\n if (!data) return null;\n\n const { order, groups } = data;\n\n // One matchMedia listener per unique breakpoint, not per <source>.\n const queries = order.map((key) => ({\n key,\n mql: window.matchMedia(toMediaQuery(key)),\n }));\n\n let currentKey = null;\n\n function pickKey() {\n const match = queries.find((q) => q.mql.matches);\n return match ? match.key : null;\n }\n\n // If nothing matches (a gap between breakpoints), leave things as they\n // are rather than blanking the video out.\n function evaluate() {\n const key = pickKey();\n if (key === null || key === currentKey) return;\n currentKey = key;\n applySources(video, groups.get(key));\n }\n\n const handler = () => evaluate();\n queries.forEach((q) => q.mql.addEventListener('change', handler));\n\n evaluate();\n\n return {\n evaluate,\n destroy() {\n queries.forEach((q) => q.mql.removeEventListener('change', handler));\n },\n };\n}\n\n/**\n * Scans for <video> elements and switches their <source> children based on\n * a data-resolution media-query attribute. Videos where only some <source>\n * elements carry the attribute are skipped entirely.\n *\n * @param {Object} [options]\n * @param {Document|Element} [options.root=document] - scope to search within\n * @param {string} [options.selector='video'] - selector for video elements\n * @param {string} [options.attribute='data-resolution'] - attribute to read\n * @returns {{ refresh: () => void, destroy: () => void }}\n */\nexport function init(options = {}) {\n const {\n root = document,\n selector = DEFAULT_SELECTOR,\n attribute = DEFAULT_ATTRIBUTE,\n } = options;\n\n const controllers = Array.from(root.querySelectorAll(selector))\n .map((video) => setupVideo(video, attribute))\n .filter(Boolean);\n\n return {\n refresh() {\n controllers.forEach((c) => c.evaluate());\n },\n destroy() {\n controllers.forEach((c) => c.destroy());\n },\n };\n}\n\n/**\n * Convenience helper for the standalone <script> build: runs init() once\n * the DOM is ready, using the given options (or defaults).\n */\nexport function autoInit(options) {\n const run = () => init(options);\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', run, { once: true });\n } else {\n run();\n }\n}"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@the-w4/responsive-video-source",
3
+ "version": "0.1.0",
4
+ "description": "Switches <video> <source> elements based on a data-resolution media-query attribute, so the right mp4/webm loads per breakpoint.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "browser": "./dist/index.global.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.cjs",
15
+ "default": "./dist/index.mjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "build": "node scripts/build.js",
25
+ "test": "node --test",
26
+ "prepublishOnly": "npm run build && npm test"
27
+ },
28
+ "keywords": [
29
+ "video",
30
+ "responsive",
31
+ "source",
32
+ "media-query",
33
+ "matchmedia",
34
+ "resolution",
35
+ "mp4",
36
+ "webm"
37
+ ],
38
+ "author": "Assaf Katz <assaf@thew4.co> (https://thew4.co)",
39
+ "license": "MIT",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "devDependencies": {
44
+ "esbuild": "^0.24.0",
45
+ "jsdom": "^25.0.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ }
50
+ }