@gemboss/gem-framework 0.5.2 → 0.9.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/README.md +63 -1
- package/dist/SOURCE.json +2 -2
- package/dist/gem-framework.css +206 -64
- package/dist/gem-runtime.js +158 -12
- package/dist/index.js +747 -112
- package/dist/plugins/before_after.d.ts +9 -0
- package/dist/plugins/before_after.js +37 -0
- package/dist/plugins/carousel.d.ts +9 -0
- package/dist/plugins/carousel.js +49 -0
- package/dist/plugins/cart-drawer.d.ts +9 -0
- package/dist/plugins/cart-drawer.js +69 -0
- package/dist/plugins/context-adapter.d.ts +9 -0
- package/dist/plugins/context-adapter.js +17 -0
- package/dist/plugins/countdown.d.ts +9 -0
- package/dist/plugins/countdown.js +164 -0
- package/dist/plugins/disclosure.d.ts +9 -0
- package/dist/plugins/disclosure.js +40 -0
- package/dist/plugins/hero-slideshow.d.ts +9 -0
- package/dist/plugins/hero-slideshow.js +57 -0
- package/dist/plugins/localization.d.ts +9 -0
- package/dist/plugins/localization.js +37 -0
- package/dist/plugins/media-gallery.d.ts +9 -0
- package/dist/plugins/media-gallery.js +57 -0
- package/dist/plugins/mobile-menu.d.ts +9 -0
- package/dist/plugins/mobile-menu.js +51 -0
- package/dist/plugins/offer-engine.d.ts +9 -0
- package/dist/plugins/offer-engine.js +57 -0
- package/dist/plugins/predictive-search.d.ts +9 -0
- package/dist/plugins/predictive-search.js +77 -0
- package/dist/plugins/promo-popup.d.ts +9 -0
- package/dist/plugins/promo-popup.js +117 -0
- package/dist/plugins/quantity-input.d.ts +9 -0
- package/dist/plugins/quantity-input.js +44 -0
- package/dist/plugins/quick-view.d.ts +9 -0
- package/dist/plugins/quick-view.js +69 -0
- package/dist/plugins/recently-viewed.d.ts +9 -0
- package/dist/plugins/recently-viewed.js +71 -0
- package/dist/plugins/reveal-on-scroll.d.ts +9 -0
- package/dist/plugins/reveal-on-scroll.js +37 -0
- package/dist/plugins/runtime-slot-loader.d.ts +9 -0
- package/dist/plugins/runtime-slot-loader.js +29 -0
- package/dist/plugins/seasonal-effects.d.ts +9 -0
- package/dist/plugins/seasonal-effects.js +17 -0
- package/dist/plugins/sticky-atc.d.ts +9 -0
- package/dist/plugins/sticky-atc.js +43 -0
- package/dist/plugins/tabs.d.ts +9 -0
- package/dist/plugins/tabs.js +50 -0
- package/dist/plugins/variant-selects.d.ts +9 -0
- package/dist/plugins/variant-selects.js +70 -0
- package/dist/popup-shell.css +91 -0
- package/dist/primitives.css +1430 -0
- package/dist/runtime-core.d.ts +51 -0
- package/dist/runtime-core.js +176 -0
- package/dist/section-gate.js +432 -21
- package/dist/sections.css +1116 -0
- package/dist/tokens.css +67 -0
- package/package.json +21 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Types for @gemboss/gem-framework/runtime-core (runtime-core.js, copied as is into dist/). Hand-written next to the
|
|
2
|
+
// file it describes; the package build test type-checks a consumer against it (package-build.test.ts).
|
|
3
|
+
|
|
4
|
+
/** A gem:* event callback. The detail's shape depends on the event, so it is left to the caller. */
|
|
5
|
+
export type GemEventCallback = (detail: any) => void;
|
|
6
|
+
|
|
7
|
+
/** The shape of window.GemTheme: on() returns an unsubscribe, emit() fires a document CustomEvent and the callbacks. */
|
|
8
|
+
export interface GemApi {
|
|
9
|
+
listeners: Record<string, GemEventCallback[]>;
|
|
10
|
+
on(event: string, callback: GemEventCallback): () => void;
|
|
11
|
+
emit(event: string, detail?: Record<string, unknown>): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MountOptions {
|
|
15
|
+
/** The event hub (pass window.GemTheme to share one with gem-runtime.js). Default: one hub shared by the modules. */
|
|
16
|
+
api?: GemApi;
|
|
17
|
+
/** The window used for timers, storage and scroll. Default: root.ownerDocument.defaultView. */
|
|
18
|
+
window?: Window | typeof globalThis;
|
|
19
|
+
[option: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Removes what one mount() added. Calling it twice does nothing the second time. */
|
|
23
|
+
export type Unmount = () => void;
|
|
24
|
+
|
|
25
|
+
/** An expando a plugin sets on its elements instead of a data-gem-bound-<key> mark (offer-engine: __gemOffer). */
|
|
26
|
+
export interface PluginGuard {
|
|
27
|
+
selector: string;
|
|
28
|
+
prop: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The names a plugin body is given, scoped to one root. */
|
|
32
|
+
export interface PluginInstallScope {
|
|
33
|
+
document: Document;
|
|
34
|
+
window: Window;
|
|
35
|
+
target: Window;
|
|
36
|
+
mark(element: Element | null | undefined, key: string): boolean;
|
|
37
|
+
canListen(element: unknown): boolean;
|
|
38
|
+
listen(element: EventTarget, type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
39
|
+
registerPlugin(key: string, setup: (context: { api: GemApi; config: unknown; reason: string }) => void): void;
|
|
40
|
+
registerNoopPlugin(key: string, readyEvent?: string): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export declare function createGemApi(doc?: Document): GemApi;
|
|
44
|
+
export declare function gemApi(): GemApi;
|
|
45
|
+
export declare function mountPlugin(
|
|
46
|
+
key: string,
|
|
47
|
+
install: (scope: PluginInstallScope) => void,
|
|
48
|
+
root?: ParentNode | null,
|
|
49
|
+
options?: MountOptions,
|
|
50
|
+
guard?: PluginGuard,
|
|
51
|
+
): Unmount;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @gemboss/gem-framework/runtime-core: what every dist/plugins/<key>.js shares (gem-framework 0.8.0).
|
|
2
|
+
//
|
|
3
|
+
// A plugin module carries the SAME plugin body gem-runtime.js carries (runtime.ts, one table), and this file gives
|
|
4
|
+
// that body the names it expects (`document`, `window`, `target`, `mark`, `canListen`, `registerPlugin`) scoped to
|
|
5
|
+
// one root, and keeps a ledger of what the body did so unmount() can undo it:
|
|
6
|
+
// - listeners (the module wraps every `x.addEventListener(` of the body as `__gemListen(x, `), timers,
|
|
7
|
+
// animation frames and IntersectionObservers made through `window` / `target`;
|
|
8
|
+
// - the `data-gem-bound-<key>` marks it set, so the same root can be mounted again after an unmount.
|
|
9
|
+
// Importing this file does nothing: no global is read or written until mount() is called.
|
|
10
|
+
|
|
11
|
+
const mounted = new WeakMap(); // root -> Map(key -> { count, teardown })
|
|
12
|
+
let sharedApi = null;
|
|
13
|
+
|
|
14
|
+
/** An event hub with the shape of window.GemTheme: on(), emit() (a document CustomEvent + the callbacks), listeners. */
|
|
15
|
+
export function createGemApi(doc) {
|
|
16
|
+
const listeners = {};
|
|
17
|
+
return {
|
|
18
|
+
listeners,
|
|
19
|
+
on(event, callback) {
|
|
20
|
+
(listeners[event] ||= []).push(callback);
|
|
21
|
+
return () => { listeners[event] = (listeners[event] || []).filter((item) => item !== callback); };
|
|
22
|
+
},
|
|
23
|
+
emit(event, detail = {}) {
|
|
24
|
+
const d = doc || globalThis.document;
|
|
25
|
+
const Event = (d && d.defaultView && d.defaultView.CustomEvent) || globalThis.CustomEvent;
|
|
26
|
+
if (d && typeof d.dispatchEvent === "function" && Event) d.dispatchEvent(new Event(event, { detail }));
|
|
27
|
+
for (const callback of listeners[event] || []) callback(detail);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The hub every plugin module uses unless mount() is given `options.api` (pass window.GemTheme to share one). */
|
|
33
|
+
export function gemApi() {
|
|
34
|
+
return (sharedApi ||= createGemApi());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const bindMethod = (owner, value, prop) => (typeof value === "function" && !/^[A-Z]/.test(String(prop)) ? value.bind(owner) : value);
|
|
38
|
+
|
|
39
|
+
// `document` for a plugin mounted on an element: queries see only that element and what is inside it (the element
|
|
40
|
+
// itself included, as gemsite's behaviors match their own root); everything else is the real document.
|
|
41
|
+
function scopedDocument(root, doc) {
|
|
42
|
+
const matches = (selector) => typeof root.matches === "function" && root.matches(selector);
|
|
43
|
+
const own = {
|
|
44
|
+
querySelectorAll: (selector) => {
|
|
45
|
+
const found = Array.from(root.querySelectorAll(selector));
|
|
46
|
+
return matches(selector) ? [root, ...found] : found;
|
|
47
|
+
},
|
|
48
|
+
querySelector: (selector) => (matches(selector) ? root : root.querySelector(selector)),
|
|
49
|
+
};
|
|
50
|
+
return new Proxy(doc, {
|
|
51
|
+
get: (real, prop) => (Object.prototype.hasOwnProperty.call(own, prop) ? own[prop] : bindMethod(real, Reflect.get(real, prop), prop)),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// `window` / `target` for a plugin: the real window, except that timers, frames and observers made through it are
|
|
56
|
+
// remembered so unmount() can stop them.
|
|
57
|
+
function trackedWindow(win, ledger) {
|
|
58
|
+
const own = {
|
|
59
|
+
setTimeout: (fn, ms, ...args) => {
|
|
60
|
+
const id = win.setTimeout(() => { ledger.timeouts.delete(id); fn(...args); }, ms);
|
|
61
|
+
ledger.timeouts.add(id);
|
|
62
|
+
return id;
|
|
63
|
+
},
|
|
64
|
+
clearTimeout: (id) => { ledger.timeouts.delete(id); win.clearTimeout(id); },
|
|
65
|
+
setInterval: (fn, ms, ...args) => {
|
|
66
|
+
const id = win.setInterval(fn, ms, ...args);
|
|
67
|
+
ledger.intervals.add(id);
|
|
68
|
+
return id;
|
|
69
|
+
},
|
|
70
|
+
clearInterval: (id) => { ledger.intervals.delete(id); win.clearInterval(id); },
|
|
71
|
+
requestAnimationFrame: typeof win.requestAnimationFrame === "function"
|
|
72
|
+
? (fn) => {
|
|
73
|
+
const id = win.requestAnimationFrame((time) => { ledger.frames.delete(id); fn(time); });
|
|
74
|
+
ledger.frames.add(id);
|
|
75
|
+
return id;
|
|
76
|
+
}
|
|
77
|
+
: undefined,
|
|
78
|
+
IntersectionObserver: typeof win.IntersectionObserver === "function"
|
|
79
|
+
? class extends win.IntersectionObserver {
|
|
80
|
+
constructor(callback, init) {
|
|
81
|
+
super(callback, init);
|
|
82
|
+
ledger.observers.add(this);
|
|
83
|
+
}
|
|
84
|
+
disconnect() {
|
|
85
|
+
ledger.observers.delete(this);
|
|
86
|
+
super.disconnect();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
90
|
+
};
|
|
91
|
+
return new Proxy(win, {
|
|
92
|
+
get: (real, prop) => (Object.prototype.hasOwnProperty.call(own, prop) ? own[prop] : bindMethod(real, Reflect.get(real, prop), prop)),
|
|
93
|
+
set: (real, prop, value) => Reflect.set(real, prop, value),
|
|
94
|
+
has: (real, prop) => (Object.prototype.hasOwnProperty.call(own, prop) ? own[prop] !== undefined : prop in real),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run one plugin's install() against `root` (default: the whole document) and return unmount().
|
|
100
|
+
* Mounting a key on a root it is already mounted on binds nothing more: it returns another handle on the same
|
|
101
|
+
* mount, and the plugin is torn down when the last handle is released.
|
|
102
|
+
* `guard` names an expando the plugin body sets on its elements instead of a data-gem-bound mark (offer-engine:
|
|
103
|
+
* `__gemOffer`); unmount clears it on the elements this mount bound so a later mount binds them again.
|
|
104
|
+
*/
|
|
105
|
+
export function mountPlugin(key, install, root, options = {}, guard) {
|
|
106
|
+
const scope = root || globalThis.document;
|
|
107
|
+
if (!scope || typeof scope.querySelectorAll !== "function") return () => {};
|
|
108
|
+
let byKey = mounted.get(scope);
|
|
109
|
+
if (!byKey) mounted.set(scope, (byKey = new Map()));
|
|
110
|
+
const existing = byKey.get(key);
|
|
111
|
+
if (existing) {
|
|
112
|
+
existing.count += 1;
|
|
113
|
+
return release(byKey, key, existing);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const doc = scope.nodeType === 9 ? scope : scope.ownerDocument || globalThis.document;
|
|
117
|
+
const win = options.window || (doc && doc.defaultView) || globalThis;
|
|
118
|
+
const api = options.api || (doc === globalThis.document ? gemApi() : createGemApi(doc));
|
|
119
|
+
const ledger = { listeners: [], marks: [], timeouts: new Set(), intervals: new Set(), frames: new Set(), observers: new Set() };
|
|
120
|
+
const window = trackedWindow(win, ledger);
|
|
121
|
+
const document = scope === doc ? doc : scopedDocument(scope, doc);
|
|
122
|
+
// `data-gem-bound-<key>` with ANY value means bound, including one the markup carries to claim an element for its own
|
|
123
|
+
// behavior (promo-popup.email-capture.v1). Teardown removes only the marks in this ledger, never a claim.
|
|
124
|
+
const mark = (element, markKey) => {
|
|
125
|
+
const attr = "data-gem-bound-" + markKey;
|
|
126
|
+
if (!element || element.hasAttribute(attr)) return false;
|
|
127
|
+
element.setAttribute(attr, "true");
|
|
128
|
+
ledger.marks.push([element, attr]);
|
|
129
|
+
return true;
|
|
130
|
+
};
|
|
131
|
+
const canListen = (element) => Boolean(element && typeof element.addEventListener === "function");
|
|
132
|
+
const listen = (element, type, callback, listenOptions) => {
|
|
133
|
+
element.addEventListener(type, callback, listenOptions);
|
|
134
|
+
ledger.listeners.push(() => element.removeEventListener && element.removeEventListener(type, callback, listenOptions));
|
|
135
|
+
};
|
|
136
|
+
let setup = null;
|
|
137
|
+
const registerPlugin = (registeredKey, fn) => { if (registeredKey === key) setup = fn; };
|
|
138
|
+
const registerNoopPlugin = (registeredKey, readyEvent) => {
|
|
139
|
+
registerPlugin(registeredKey, ({ api: hub }) => { if (readyEvent) hub.emit(readyEvent, { source: "gem-runtime" }); });
|
|
140
|
+
};
|
|
141
|
+
const unguarded = guard ? Array.from(document.querySelectorAll(guard.selector)).filter((element) => !element[guard.prop]) : [];
|
|
142
|
+
|
|
143
|
+
const teardown = () => {
|
|
144
|
+
ledger.timeouts.forEach((id) => win.clearTimeout(id));
|
|
145
|
+
ledger.intervals.forEach((id) => win.clearInterval(id));
|
|
146
|
+
ledger.frames.forEach((id) => win.cancelAnimationFrame && win.cancelAnimationFrame(id));
|
|
147
|
+
ledger.observers.forEach((observer) => observer.disconnect());
|
|
148
|
+
ledger.listeners.splice(0).reverse().forEach((off) => off());
|
|
149
|
+
ledger.marks.splice(0).forEach(([element, attr]) => element.removeAttribute(attr));
|
|
150
|
+
if (guard) unguarded.forEach((element) => { delete element[guard.prop]; });
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
install({ document, window, target: window, mark, canListen, listen, registerPlugin, registerNoopPlugin });
|
|
154
|
+
if (typeof setup === "function") {
|
|
155
|
+
setup({ api, config: { planId: "gem-plugin-module", plugins: [{ key, loadStrategy: "mount", dependsOn: [] }] }, reason: "mount" });
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
teardown(); // a plugin that throws half way leaves nothing bound behind it
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
const record = { count: 1, teardown };
|
|
162
|
+
byKey.set(key, record);
|
|
163
|
+
return release(byKey, key, record);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function release(byKey, key, record) {
|
|
167
|
+
let released = false;
|
|
168
|
+
return () => {
|
|
169
|
+
if (released) return;
|
|
170
|
+
released = true;
|
|
171
|
+
record.count -= 1;
|
|
172
|
+
if (record.count > 0) return;
|
|
173
|
+
if (byKey.get(key) === record) byKey.delete(key);
|
|
174
|
+
record.teardown();
|
|
175
|
+
};
|
|
176
|
+
}
|