@jsswift/core 1.0.41

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.
@@ -0,0 +1,213 @@
1
+ JSswift.overlay = (() => {
2
+ let seq = 0;
3
+ const stack = new Map(); // id -> entry
4
+ let root = null;
5
+ const {
6
+ ensureRoot: ensureOverlayRoot,
7
+ focusFirst,
8
+ trapFocus,
9
+ applyAnchoredPosition
10
+ } = JSswift._overlayShared;
11
+ const ensureRoot = () => ensureOverlayRoot(() => root, (nextRoot) => {
12
+ root = nextRoot;
13
+ });
14
+
15
+ let scrollLockCount = 0;
16
+ const lockScroll = () => {
17
+ scrollLockCount++;
18
+ if (scrollLockCount === 1) {
19
+ document.documentElement.classList.add("cms-scroll-locked");
20
+ }
21
+ };
22
+ const unlockScroll = () => {
23
+ scrollLockCount = Math.max(0, scrollLockCount - 1);
24
+ if (scrollLockCount === 0) {
25
+ document.documentElement.classList.remove("cms-scroll-locked");
26
+ }
27
+ };
28
+
29
+ const getTop = () => {
30
+ let top = null;
31
+ for (const e of stack.values()) top = e; // insertion order
32
+ return top;
33
+ };
34
+
35
+ const isOverlayInteractivePortal = (target) => {
36
+ let node = target;
37
+ while (node && node !== document) {
38
+ if (node.nodeType === 1 && node.getAttribute?.("data-cms-overlay-portal") === "true") return true;
39
+ node = node.parentNode;
40
+ }
41
+ return false;
42
+ };
43
+
44
+ const open = (content, opts = {}) => {
45
+ const id = `ov_${++seq}`;
46
+
47
+ const entry = {
48
+ id,
49
+ opts,
50
+ isOpen: true,
51
+ anchorEl: opts.anchorEl || null,
52
+ placement: opts.placement || "bottom-start",
53
+ onClose: typeof opts.onClose === "function" ? opts.onClose : null,
54
+ overlay: null,
55
+ panel: null,
56
+ backdrop: null,
57
+ reposition: null,
58
+ _positionCleanup: null,
59
+ _positionFrame: null,
60
+ _panelResizeObserver: null,
61
+ _cleanup: null
62
+ };
63
+
64
+ const overlay = document.createElement("div");
65
+ overlay.className = ["cms-overlay", opts.type ? `type-${opts.type}` : ""].filter(Boolean).join(" ");
66
+ overlay.dataset.id = id;
67
+
68
+ const backdrop = document.createElement("div");
69
+ backdrop.className = "cms-overlay-backdrop";
70
+ if (!opts.backdrop) backdrop.style.display = "none";
71
+
72
+ const panel = document.createElement("div");
73
+ panel.className = ["cms-overlay-panel", opts.className].filter(Boolean).join(" ");
74
+ panel.tabIndex = -1;
75
+
76
+ // mount content
77
+ const node = (typeof content === "function") ? content({ close: () => close(id) }) : content;
78
+ const normalized = JSswift.ui.slot(node);
79
+ if (Array.isArray(normalized)) normalized.forEach(n => n && panel.appendChild(n));
80
+ else if (normalized) panel.appendChild(normalized);
81
+
82
+ overlay.appendChild(backdrop);
83
+ overlay.appendChild(panel);
84
+ const host = ensureRoot();
85
+ if (!host) return { id, close: () => close(id), panel, overlay };
86
+ host.appendChild(overlay);
87
+
88
+ // stack bookkeeping
89
+ entry.overlay = overlay;
90
+ entry.panel = panel;
91
+ entry.backdrop = backdrop;
92
+ stack.set(id, entry);
93
+
94
+ // scroll lock + focus
95
+ if (opts.lockScroll) lockScroll();
96
+ if (opts.autoFocus !== false) setTimeout(() => focusFirst(panel), 0);
97
+
98
+ // positioning (for menus/tooltips)
99
+ const position = (nextOpts = null) => {
100
+ if (nextOpts && typeof nextOpts === "object") Object.assign(entry.opts, nextOpts);
101
+ applyAnchoredPosition(panel, entry.opts);
102
+ };
103
+ const schedulePosition = () => {
104
+ if (!opts.anchorEl) return;
105
+ const raf = globalThis.requestAnimationFrame || ((fn) => setTimeout(() => fn(Date.now()), 0));
106
+ const caf = globalThis.cancelAnimationFrame || clearTimeout;
107
+ if (entry._positionFrame != null) caf(entry._positionFrame);
108
+ entry._positionFrame = raf(() => {
109
+ entry._positionFrame = null;
110
+ position();
111
+ });
112
+ };
113
+ entry.reposition = position;
114
+
115
+ if (opts.anchorEl) {
116
+ position();
117
+ schedulePosition();
118
+ const onResize = () => position();
119
+ window.addEventListener("resize", onResize);
120
+ window.addEventListener("scroll", onResize, true);
121
+ window.visualViewport?.addEventListener?.("resize", onResize);
122
+ window.visualViewport?.addEventListener?.("scroll", onResize);
123
+ if (typeof ResizeObserver === "function") {
124
+ const panelResizeObserver = new ResizeObserver(() => position());
125
+ panelResizeObserver.observe(panel);
126
+ entry._panelResizeObserver = panelResizeObserver;
127
+ }
128
+ entry._positionCleanup = () => {
129
+ const caf = globalThis.cancelAnimationFrame || clearTimeout;
130
+ if (entry._positionFrame != null) {
131
+ caf(entry._positionFrame);
132
+ entry._positionFrame = null;
133
+ }
134
+ entry._panelResizeObserver?.disconnect?.();
135
+ entry._panelResizeObserver = null;
136
+ window.removeEventListener("resize", onResize);
137
+ window.removeEventListener("scroll", onResize, true);
138
+ window.visualViewport?.removeEventListener?.("resize", onResize);
139
+ window.visualViewport?.removeEventListener?.("scroll", onResize);
140
+ };
141
+ }
142
+
143
+ // click outside
144
+ const onDocClick = (e) => {
145
+ const top = getTop();
146
+ if (!top || top.id !== id) return;
147
+ if (opts.closeOnOutside === false) return;
148
+ if (isOverlayInteractivePortal(e.target)) return;
149
+ if (!panel.contains(e.target)) close(id);
150
+ };
151
+ document.addEventListener("mousedown", onDocClick, true);
152
+
153
+ // ESC + focus trap
154
+ const onKeyDown = (e) => {
155
+ const top = getTop();
156
+ if (!top || top.id !== id) return;
157
+
158
+ if (opts.trapFocus) trapFocus(e, panel);
159
+ if (e.key === "Escape" && opts.closeOnEsc !== false) {
160
+ e.preventDefault();
161
+ close(id);
162
+ }
163
+ };
164
+ document.addEventListener("keydown", onKeyDown, true);
165
+
166
+ // backdrop click
167
+ backdrop.addEventListener("click", () => {
168
+ if (opts.closeOnBackdrop === false) return;
169
+ close(id);
170
+ });
171
+
172
+ // store cleanup
173
+ entry._cleanup = () => {
174
+ document.removeEventListener("mousedown", onDocClick, true);
175
+ document.removeEventListener("keydown", onKeyDown, true);
176
+ entry._positionCleanup?.();
177
+ };
178
+
179
+ // z-index stacking
180
+ const z = 1000 + stack.size * 10;
181
+ overlay.style.zIndex = String(z);
182
+
183
+ return {
184
+ id,
185
+ close: () => close(id),
186
+ panel,
187
+ overlay,
188
+ reposition: position
189
+ };
190
+ };
191
+
192
+ const close = (id) => {
193
+ const entry = stack.get(id);
194
+ if (!entry) return;
195
+ stack.delete(id);
196
+
197
+ entry._cleanup?.();
198
+
199
+ if (entry.opts.lockScroll) unlockScroll();
200
+
201
+ if (typeof cleanupNodeTree === "function") cleanupNodeTree(entry.overlay);
202
+ entry.overlay?.remove();
203
+
204
+ entry.onClose?.();
205
+ };
206
+
207
+ const closeTop = () => {
208
+ const top = getTop();
209
+ if (top) close(top.id);
210
+ };
211
+
212
+ return { open, close, closeTop, root, _stack: stack };
213
+ })();
@@ -0,0 +1,211 @@
1
+ /* ===============================
2
+ Shared DOM prop bridge
3
+ =============================== */
4
+
5
+ function createDomPropBridge(el, options = {}) {
6
+ const {
7
+ isSVG = false,
8
+ normalizeClass: normalizeClassValue = (value) => String(value ?? ""),
9
+ isContentProp: isContent = () => false,
10
+ autoValueTags = new Set(["INPUT", "TEXTAREA", "SELECT"])
11
+ } = options;
12
+
13
+ function isBooleanDomProp(name) {
14
+ if (isSVG || !(name in el)) return false;
15
+ try {
16
+ return typeof el[name] === "boolean";
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function isAttributeOnlyProp(name) {
23
+ return isSVG || name.startsWith("data-") || name.startsWith("aria-") || !(name in el);
24
+ }
25
+
26
+ function setAttributeValue(name, value) {
27
+ if (value == null) {
28
+ el.removeAttribute(name);
29
+ return;
30
+ }
31
+ if (value === false) {
32
+ if (name.startsWith("aria-")) {
33
+ el.setAttribute(name, "false");
34
+ return;
35
+ }
36
+ el.removeAttribute(name);
37
+ return;
38
+ }
39
+ if (value === true && !isSVG && !name.startsWith("aria-") && !name.startsWith("data-")) {
40
+ el.setAttribute(name, "");
41
+ return;
42
+ }
43
+ el.setAttribute(name, String(value));
44
+ }
45
+
46
+ function setStyleEntry(name, value) {
47
+ if (name == null) return;
48
+ const styleName = String(name);
49
+ const isCssProperty = styleName.startsWith("--") || styleName.includes("-");
50
+ if (value == null || value === false || value === "") {
51
+ if (isCssProperty) el.style.removeProperty(styleName);
52
+ else el.style[styleName] = "";
53
+ return;
54
+ }
55
+ if (isCssProperty) {
56
+ el.style.setProperty(styleName, String(value));
57
+ return;
58
+ }
59
+ el.style[styleName] = value;
60
+ }
61
+
62
+ function setClassValue(value) {
63
+ const normalized = normalizeClassValue(value);
64
+ if (normalized == null || normalized === false || normalized === "") {
65
+ el.removeAttribute("class");
66
+ return;
67
+ }
68
+ el.setAttribute("class", String(normalized));
69
+ }
70
+
71
+ function setBooleanProp(name, value) {
72
+ const next = !!value;
73
+ el[name] = next;
74
+ if (next) el.setAttribute(name, "");
75
+ else el.removeAttribute(name);
76
+ }
77
+
78
+ function removeProp(name) {
79
+ if (isContent(name)) {
80
+ el[name] = "";
81
+ return;
82
+ }
83
+ if (name === "class") {
84
+ el.removeAttribute("class");
85
+ return;
86
+ }
87
+ if (name === "style") {
88
+ el.removeAttribute("style");
89
+ return;
90
+ }
91
+ if (isBooleanDomProp(name)) {
92
+ try { el[name] = false; } catch { }
93
+ el.removeAttribute(name);
94
+ return;
95
+ }
96
+ el.removeAttribute(name);
97
+ if (!isAttributeOnlyProp(name)) {
98
+ try {
99
+ if (typeof el[name] === "string") el[name] = "";
100
+ } catch { }
101
+ }
102
+ }
103
+
104
+ function applyStyleObject(styleObj) {
105
+ if (!styleObj || typeof styleObj !== "object") return;
106
+ Object.entries(styleObj).forEach(([styleName, styleValue]) => {
107
+ setStyleEntry(styleName, styleValue);
108
+ });
109
+ }
110
+
111
+ function setProp(name, value) {
112
+ if (isContent(name)) {
113
+ el[name] = value ?? "";
114
+ return;
115
+ }
116
+ if (name === "class") {
117
+ setClassValue(value);
118
+ return;
119
+ }
120
+ if (name === "style") {
121
+ if (value == null || value === false) {
122
+ removeProp(name);
123
+ return;
124
+ }
125
+ if (typeof value === "object") {
126
+ applyStyleObject(value);
127
+ return;
128
+ }
129
+ }
130
+ if (isBooleanDomProp(name)) {
131
+ setBooleanProp(name, value);
132
+ return;
133
+ }
134
+ if (value == null || value === false) {
135
+ removeProp(name);
136
+ return;
137
+ }
138
+ if (isAttributeOnlyProp(name)) {
139
+ setAttributeValue(name, value);
140
+ return;
141
+ }
142
+ el[name] = value;
143
+ }
144
+
145
+ function setPathValue(path, value) {
146
+ const parts = String(path).split(".");
147
+ let obj = el;
148
+ for (let i = 0; i < parts.length - 1; i++) {
149
+ obj = obj?.[parts[i]];
150
+ if (!obj) return;
151
+ }
152
+ obj[parts[parts.length - 1]] = value;
153
+ }
154
+
155
+ function isBindingValueKey(name) {
156
+ if (!name) return false;
157
+ return name === "auto"
158
+ || name.startsWith("attr:")
159
+ || name.startsWith("@")
160
+ || name.startsWith("style.")
161
+ || name.includes(".");
162
+ }
163
+
164
+ function applyBindingValue(name, value) {
165
+ if (!name || name === "auto") {
166
+ if (autoValueTags.has(el.tagName)) {
167
+ setProp("value", value);
168
+ } else {
169
+ el.textContent = value ?? "";
170
+ }
171
+ return;
172
+ }
173
+
174
+ if (name.startsWith("attr:")) {
175
+ setAttributeValue(name.slice(5), value);
176
+ return;
177
+ }
178
+
179
+ if (name.startsWith("@")) {
180
+ setAttributeValue(name.slice(1), value);
181
+ return;
182
+ }
183
+
184
+ if (name.startsWith("style.")) {
185
+ setStyleEntry(name.slice(6), value);
186
+ return;
187
+ }
188
+
189
+ if (name.includes(".")) {
190
+ setPathValue(name, value);
191
+ return;
192
+ }
193
+
194
+ setProp(name, value);
195
+ }
196
+
197
+ return {
198
+ isBooleanDomProp,
199
+ isAttributeOnlyProp,
200
+ isBindingValueKey,
201
+ setAttributeValue,
202
+ setStyleEntry,
203
+ setClassValue,
204
+ setBooleanProp,
205
+ removeProp,
206
+ applyStyleObject,
207
+ setProp,
208
+ setPathValue,
209
+ applyBindingValue
210
+ };
211
+ }