@swiftwc/ui 0.0.0-dev.67 → 0.0.0-dev.68
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/generated/client/index.d.ts +4 -1
- package/generated/client/index.js +50 -35
- package/generated/tpl/html.d.ts +5 -0
- package/generated/tpl/html.js +3 -0
- package/generated/tpl/index.d.ts +4 -0
- package/generated/tpl/index.js +4 -0
- package/generated/tpl/render.d.ts +2 -0
- package/generated/tpl/render.js +135 -0
- package/package.json +3 -2
|
@@ -11,7 +11,10 @@ export declare const alert: (title?: string, message?: string, actions?: {
|
|
|
11
11
|
role?: string;
|
|
12
12
|
}[], options?: {
|
|
13
13
|
titleVisibility?: boolean;
|
|
14
|
-
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}) => Promise<string | undefined> & {
|
|
16
|
+
dismiss: (reason?: unknown) => void;
|
|
17
|
+
};
|
|
15
18
|
export declare const confirmationDialog: (trigger: HTMLElement, title: string, message?: string, actions?: {
|
|
16
19
|
label?: string;
|
|
17
20
|
image?: string;
|
|
@@ -5,6 +5,7 @@ import { NavigationPath } from '../internal/class/navigation-path';
|
|
|
5
5
|
import { queryInsertPosition, startViewTransition } from '../internal/privateNamespace';
|
|
6
6
|
import { $, devFlags, kebabCase, onoff } from '../internal/utils';
|
|
7
7
|
import { Snapshot } from '../snapshot';
|
|
8
|
+
import { html, render } from '../tpl';
|
|
8
9
|
//#region polyfills
|
|
9
10
|
export const polyfills = new Map();
|
|
10
11
|
for (const [k, Ctor] of Object.entries(Components)) {
|
|
@@ -174,47 +175,61 @@ if (mediaQueryList.matches)
|
|
|
174
175
|
])
|
|
175
176
|
document.addEventListener(k, v, { passive: true });
|
|
176
177
|
//#region fns
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
178
|
+
// const a = alert('Saving…', 'Please wait')
|
|
179
|
+
// // later, e.g. save completed early
|
|
180
|
+
// a.dismiss() // silently resolves undefined, closes+removes dialog
|
|
181
|
+
// // or
|
|
182
|
+
// a.dismiss(new Error('cancelled')) // rejects instead
|
|
183
|
+
// const result = await a // still works normally if not dismissed
|
|
184
|
+
// const ac = new AbortController()
|
|
185
|
+
// const a = alert('Uploading…', 'Please wait', undefined, { signal: ac.signal })
|
|
186
|
+
// // elsewhere, e.g. upload finished before the user dismissed
|
|
187
|
+
// ac.abort() // dialog closes silently, a rejects with AbortError
|
|
188
|
+
// // or still works standalone
|
|
189
|
+
// a.dismiss() // same effect, no signal needed
|
|
190
|
+
export const alert = (title, message, actions, options) => {
|
|
191
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
192
|
+
let dialog;
|
|
193
|
+
let off;
|
|
194
|
+
const dismiss = (reason) => {
|
|
195
|
+
off?.();
|
|
196
|
+
dialog?.close();
|
|
197
|
+
dialog?.remove();
|
|
198
|
+
reason === undefined ? resolve(undefined) : reject(reason instanceof Error ? reason : new DOMException('Alert dismissed', 'AbortError'));
|
|
199
|
+
};
|
|
200
|
+
if (options?.signal)
|
|
201
|
+
if (options.signal.aborted)
|
|
202
|
+
dismiss(options.signal.reason); // already aborted before we even started — settle immediately, skip showing anything
|
|
203
|
+
else
|
|
204
|
+
options.signal.addEventListener('abort', () => dismiss(options.signal.reason), { once: true });
|
|
205
|
+
navigator.locks
|
|
206
|
+
.request('alert:', async () => {
|
|
207
|
+
if (options?.signal?.aborted)
|
|
208
|
+
return; // in case it was aborted while queued behind another lock holder
|
|
209
|
+
const mount = document.createElement('div');
|
|
210
|
+
render(html `<dialog is="alert-dialog">
|
|
211
|
+
<v-stack spacing="1" alignment="fill">
|
|
212
|
+
${title ? html `<label-view font="headline" title="${title}"></label-view>` : null}
|
|
213
|
+
${message ? html `<label-view foreground="secondary" font="callout" title="${message}"></label-view>` : null}
|
|
214
|
+
</v-stack>
|
|
215
|
+
${(actions ?? [{ role: 'close' }]).map((action, index) => html `
|
|
216
|
+
<button type="button" tabindex="0" is="bordered-button" value="${index}" role="${action.role ?? null}">
|
|
217
|
+
${action.label || action.image ? html `<label-view title="${action.label ?? null}" system-image="${action.image ?? null}"></label-view>` : null}
|
|
218
|
+
</button>
|
|
219
|
+
`)}
|
|
220
|
+
</dialog>`, mount);
|
|
221
|
+
dialog = mount.firstElementChild;
|
|
209
222
|
document.body.insertAdjacentElement('beforeend', dialog);
|
|
210
223
|
dialog.showModal();
|
|
211
|
-
|
|
224
|
+
off = onoff('alert:return', (evt) => {
|
|
212
225
|
const { detail } = evt;
|
|
213
|
-
off();
|
|
226
|
+
off?.();
|
|
214
227
|
resolve(detail.returnValue);
|
|
215
228
|
}, alertDialog, { once: true }).on();
|
|
216
229
|
return promise;
|
|
217
|
-
})
|
|
230
|
+
})
|
|
231
|
+
.catch(() => { });
|
|
232
|
+
return Object.assign(promise, { dismiss });
|
|
218
233
|
};
|
|
219
234
|
export const confirmationDialog = async (trigger, title, message, actions, options) => {
|
|
220
235
|
const newAnchorName = `--confirmation-dialog-${self.crypto.randomUUID()}`;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// --- Template preparation (runs ONCE per unique template literal) -----
|
|
2
|
+
const MARKER = '\uD83D\uDD25PART\uD83D\uDD25'; // unlikely-collision sentinel
|
|
3
|
+
const templateCache = new WeakMap();
|
|
4
|
+
function prepare(strings) {
|
|
5
|
+
let cached = templateCache.get(strings);
|
|
6
|
+
if (cached)
|
|
7
|
+
return cached;
|
|
8
|
+
const partDescriptors = [];
|
|
9
|
+
let htmlStr = '';
|
|
10
|
+
strings.forEach((chunk, i) => {
|
|
11
|
+
htmlStr += chunk;
|
|
12
|
+
if (i < strings.length - 1) {
|
|
13
|
+
// Is this interpolation inside an attribute value?
|
|
14
|
+
// e.g. `...aria-checked="${...}"` — chunk ends with `attr="` or `attr='` or `attr=`
|
|
15
|
+
const attrMatch = /([a-zA-Z0-9_-]+)=(["']?)$/.exec(chunk);
|
|
16
|
+
if (attrMatch) {
|
|
17
|
+
partDescriptors.push({ type: 'attr', name: attrMatch[1] });
|
|
18
|
+
htmlStr += MARKER; // becomes the attribute's value
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
partDescriptors.push({ type: 'child' });
|
|
22
|
+
// start/end comment pair — gives us a stable region to patch later,
|
|
23
|
+
// and room to grow into list/array rendering without a redesign
|
|
24
|
+
htmlStr += `<!--${MARKER}-->`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const element = document.createElement('template');
|
|
29
|
+
element.innerHTML = htmlStr;
|
|
30
|
+
cached = { element, partDescriptors };
|
|
31
|
+
templateCache.set(strings, cached);
|
|
32
|
+
return cached;
|
|
33
|
+
}
|
|
34
|
+
const instanceCache = new WeakMap();
|
|
35
|
+
function createInstance(prepared) {
|
|
36
|
+
const fragment = prepared.element.content.cloneNode(true);
|
|
37
|
+
const parts = [];
|
|
38
|
+
// Walk the clone once, in the same order `prepare()` emitted markers,
|
|
39
|
+
// and bind each descriptor to a real node.
|
|
40
|
+
const walker = document.createTreeWalker(fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
|
|
41
|
+
let descriptorIndex = 0;
|
|
42
|
+
const descriptors = prepared.partDescriptors;
|
|
43
|
+
let node;
|
|
44
|
+
while ((node = walker.nextNode())) {
|
|
45
|
+
if (descriptorIndex >= descriptors.length)
|
|
46
|
+
break;
|
|
47
|
+
if (node.nodeType === Node.COMMENT_NODE && node.data === MARKER) {
|
|
48
|
+
const start = node;
|
|
49
|
+
const end = document.createComment(MARKER);
|
|
50
|
+
start.after(end);
|
|
51
|
+
parts.push({ type: 'child', start, end, committed: undefined });
|
|
52
|
+
descriptorIndex++;
|
|
53
|
+
}
|
|
54
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
55
|
+
const el = node;
|
|
56
|
+
for (const attr of Array.from(el.attributes)) {
|
|
57
|
+
if (attr.value === MARKER) {
|
|
58
|
+
const desc = descriptors[descriptorIndex];
|
|
59
|
+
if (desc.type === 'attr') {
|
|
60
|
+
parts.push({ type: 'attr', el, name: desc.name, committed: undefined });
|
|
61
|
+
descriptorIndex++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { fragment, parts };
|
|
68
|
+
}
|
|
69
|
+
// --- Committing values into parts (the only thing that runs per update) -
|
|
70
|
+
function isTemplateResult(v) {
|
|
71
|
+
return !!v && typeof v === 'object' && 'strings' in v && 'values' in v;
|
|
72
|
+
}
|
|
73
|
+
function toNode(value) {
|
|
74
|
+
if (value instanceof Node)
|
|
75
|
+
return value;
|
|
76
|
+
if (isTemplateResult(value)) {
|
|
77
|
+
const { fragment, parts } = createInstance(prepare(value.strings));
|
|
78
|
+
parts.forEach((part, i) => {
|
|
79
|
+
const v = value.values[i];
|
|
80
|
+
part.type === 'child' ? commitChild(part, v) : commitAttr(part, v);
|
|
81
|
+
});
|
|
82
|
+
return fragment;
|
|
83
|
+
}
|
|
84
|
+
return document.createTextNode(String(value));
|
|
85
|
+
}
|
|
86
|
+
function commitChild(part, value) {
|
|
87
|
+
if (value === part.committed)
|
|
88
|
+
return; // dirty-check, skip untouched parts
|
|
89
|
+
// clear everything between start/end
|
|
90
|
+
let n = part.start.nextSibling;
|
|
91
|
+
while (n && n !== part.end) {
|
|
92
|
+
const next = n.nextSibling;
|
|
93
|
+
n.remove();
|
|
94
|
+
n = next;
|
|
95
|
+
}
|
|
96
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
97
|
+
if (item === null || item === undefined || item === false)
|
|
98
|
+
continue;
|
|
99
|
+
part.end.before(toNode(item));
|
|
100
|
+
}
|
|
101
|
+
part.committed = value;
|
|
102
|
+
}
|
|
103
|
+
function commitAttr(part, value) {
|
|
104
|
+
if (value === part.committed)
|
|
105
|
+
return;
|
|
106
|
+
if (value === null || value === undefined || value === false) {
|
|
107
|
+
part.el.removeAttribute(part.name);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
part.el.setAttribute(part.name, String(value));
|
|
111
|
+
}
|
|
112
|
+
part.committed = value;
|
|
113
|
+
}
|
|
114
|
+
// --- Public render() ----------------------------------------------------
|
|
115
|
+
export default function (result, container) {
|
|
116
|
+
const prepared = prepare(result.strings);
|
|
117
|
+
let instance = instanceCache.get(container);
|
|
118
|
+
if (!instance || instance.strings !== result.strings) {
|
|
119
|
+
// first render, OR the template shape changed entirely — full (re)build
|
|
120
|
+
container.textContent = '';
|
|
121
|
+
const { fragment, parts } = createInstance(prepared);
|
|
122
|
+
container.append(fragment);
|
|
123
|
+
instance = { strings: result.strings, fragmentRoot: fragment, parts };
|
|
124
|
+
instanceCache.set(container, instance);
|
|
125
|
+
}
|
|
126
|
+
// same template shape as last time → patch existing DOM nodes only,
|
|
127
|
+
// no innerHTML, no re-clone, no focus/scroll loss
|
|
128
|
+
instance.parts.forEach((part, i) => {
|
|
129
|
+
const value = result.values[i];
|
|
130
|
+
if (part.type === 'child')
|
|
131
|
+
commitChild(part, value);
|
|
132
|
+
else
|
|
133
|
+
commitAttr(part, value);
|
|
134
|
+
});
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftwc/ui",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.68",
|
|
4
4
|
"description": "Elegant SwiftUI-inspired web components for standalone web apps and web extensions.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -63,11 +63,12 @@
|
|
|
63
63
|
"build:ts": "tsc",
|
|
64
64
|
"build:scss": "sass --style=expanded --no-source-map scss/index.scss generated/css/index.css",
|
|
65
65
|
"build:manifest": "tsx scripts/make-manifest.ts",
|
|
66
|
-
"build:barrels": "npm run build:barrels:components && npm run build:barrels:events && npm run build:barrels:buses && npm run build:barrels:i18n",
|
|
66
|
+
"build:barrels": "npm run build:barrels:components && npm run build:barrels:events && npm run build:barrels:buses && npm run build:barrels:tpl && npm run build:barrels:i18n",
|
|
67
67
|
"build:barrels:components": "barrelsby --noHeader --delete --singleQuotes --noSemicolon --directory ./js/components --exclude \".\\.test\\.ts\"",
|
|
68
68
|
"build:barrels:events": "barrelsby --noHeader --delete --singleQuotes --noSemicolon --directory ./js/events --exclude \".\\.test\\.ts\"",
|
|
69
69
|
"build:barrels:buses": "barrelsby --noHeader --delete --singleQuotes --noSemicolon --exportDefault --directory ./js/buses",
|
|
70
70
|
"build:barrels:i18n": "barrelsby --noHeader --delete --singleQuotes --noSemicolon --exportDefault --fullPathname --directory ./js/i18n/locales",
|
|
71
|
+
"build:barrels:tpl": "barrelsby --noHeader --delete --singleQuotes --noSemicolon --exportDefault --directory ./js/tpl --exclude \".\\.test\\.ts\"",
|
|
71
72
|
"build:barrels:decorators": "barrelsby --noHeader --delete --singleQuotes --noSemicolon --exportDefault --directory ./js/internal/decorators",
|
|
72
73
|
"format:check": "prettier . --check",
|
|
73
74
|
"format:write": "prettier . --write",
|