@webqit/webflo 0.20.35 → 0.20.36
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/package.json
CHANGED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
class DeviceViewport {
|
|
2
|
+
|
|
3
|
+
#stack = [];
|
|
4
|
+
#ownedElements = new Set(); // Stores elements created by this class
|
|
5
|
+
#elements = {}; // Map of active elements/selectors
|
|
6
|
+
#timer = null; // For commit batching
|
|
7
|
+
|
|
8
|
+
#specials = {
|
|
9
|
+
themeColor: { name: 'theme-color', type: 'meta' },
|
|
10
|
+
appleStatusBarStyle: { name: 'apple-mobile-web-app-status-bar-style', type: 'meta' },
|
|
11
|
+
colorScheme: { name: 'color-scheme', type: 'meta' },
|
|
12
|
+
manifest: { name: 'manifest', type: 'link' }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
const initialState = { _isInitial: true };
|
|
17
|
+
|
|
18
|
+
// 1. Ingest Viewport
|
|
19
|
+
const vMeta = document.querySelector('meta[name="viewport"]');
|
|
20
|
+
if (vMeta) {
|
|
21
|
+
this.#elements.viewport = vMeta;
|
|
22
|
+
Object.assign(initialState, this.#parseViewport(vMeta.content));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Ingest Title & Specials
|
|
26
|
+
initialState.title = document.title;
|
|
27
|
+
Object.entries(this.#specials).forEach(([jsKey, config]) => {
|
|
28
|
+
const el = this.#findDom(config);
|
|
29
|
+
if (el) {
|
|
30
|
+
this.#elements[jsKey] = el;
|
|
31
|
+
initialState[jsKey] = config.type === 'link' ? el.getAttribute('href') : el.getAttribute('content');
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.#stack.push(initialState);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#findDom({ name, type }) {
|
|
39
|
+
return type === 'link'
|
|
40
|
+
? document.querySelector(`link[rel="${name}"]`)
|
|
41
|
+
: document.querySelector(`meta[name="${name}"]`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#getOrCreate(jsKey, media = null) {
|
|
45
|
+
const cacheKey = media ? `${jsKey}-${media}` : jsKey;
|
|
46
|
+
if (this.#elements[cacheKey]) return this.#elements[cacheKey];
|
|
47
|
+
|
|
48
|
+
const config = this.#specials[jsKey] || { name: 'viewport', type: 'meta' };
|
|
49
|
+
const el = document.createElement(config.type);
|
|
50
|
+
|
|
51
|
+
if (config.type === 'link') el.rel = config.name;
|
|
52
|
+
else el.name = config.name;
|
|
53
|
+
if (media) el.setAttribute('media', media);
|
|
54
|
+
|
|
55
|
+
document.head.appendChild(el);
|
|
56
|
+
this.#ownedElements.add(el);
|
|
57
|
+
this.#elements[cacheKey] = el;
|
|
58
|
+
return el;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#scheduleRender() {
|
|
62
|
+
if (this.#timer) return;
|
|
63
|
+
this.#timer = requestAnimationFrame(() => {
|
|
64
|
+
this.#render();
|
|
65
|
+
this.#timer = null;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#render() {
|
|
70
|
+
const state = this.peek();
|
|
71
|
+
const viewportDirectives = [];
|
|
72
|
+
const activeKeys = new Set(Object.keys(state).filter(k => !k.startsWith('_')));
|
|
73
|
+
|
|
74
|
+
// 1. Handle Title
|
|
75
|
+
if ('title' in state) {
|
|
76
|
+
document.title = state.title || '';
|
|
77
|
+
activeKeys.delete('title');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Handle Specials (with Media Query Support)
|
|
81
|
+
Object.keys(this.#specials).forEach(jsKey => {
|
|
82
|
+
const val = state[jsKey]; // Can be string or { default, dark, light, "(media)": color }
|
|
83
|
+
activeKeys.delete(jsKey);
|
|
84
|
+
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
|
|
87
|
+
if (val && typeof val === 'object' && jsKey === 'themeColor') {
|
|
88
|
+
// Media Query Logic
|
|
89
|
+
Object.entries(val).forEach(([query, color]) => {
|
|
90
|
+
const mediaStr = query === 'dark' ? '(prefers-color-scheme: dark)' :
|
|
91
|
+
query === 'light' ? '(prefers-color-scheme: light)' :
|
|
92
|
+
query === 'default' ? '' : query;
|
|
93
|
+
seen.add(mediaStr);
|
|
94
|
+
this.#setAttr(jsKey, color, mediaStr);
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
seen.add('');
|
|
98
|
+
this.#setAttr(jsKey, val);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// cleanup
|
|
102
|
+
Object.keys(this.#elements)
|
|
103
|
+
.filter(k => k === jsKey || k.startsWith(`${jsKey}-`))
|
|
104
|
+
.forEach(k => {
|
|
105
|
+
const keyId = k === jsKey ? '' : k.slice(jsKey.length + 1);
|
|
106
|
+
if (!seen.has(keyId)) {
|
|
107
|
+
this.#setAttr(jsKey, null, keyId);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 3. Handle Viewport
|
|
113
|
+
activeKeys.forEach(key => {
|
|
114
|
+
const val = state[key];
|
|
115
|
+
if (val === null) return;
|
|
116
|
+
const kebab = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
117
|
+
viewportDirectives.push(val === true ? kebab : `${kebab}=${val}`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const vContent = viewportDirectives.join(', ');
|
|
121
|
+
const vEl = this.#elements.viewport || (vContent ? this.#getOrCreate('viewport') : null);
|
|
122
|
+
if (vEl) {
|
|
123
|
+
vEl.setAttribute('content', vContent);
|
|
124
|
+
if (!vContent && this.#ownedElements.has(vEl)) {
|
|
125
|
+
vEl.remove();
|
|
126
|
+
delete this.#elements.viewport;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#setAttr(jsKey, val, media = null) {
|
|
132
|
+
const cacheKey = media ? `${jsKey}-${media}` : jsKey;
|
|
133
|
+
const config = this.#specials[jsKey];
|
|
134
|
+
|
|
135
|
+
if (val !== undefined && val !== null) {
|
|
136
|
+
const el = this.#getOrCreate(jsKey, media);
|
|
137
|
+
el.setAttribute(config.type === 'link' ? 'href' : 'content', val);
|
|
138
|
+
} else {
|
|
139
|
+
const el = this.#elements[cacheKey];
|
|
140
|
+
if (el) {
|
|
141
|
+
if (this.#ownedElements.has(el)) {
|
|
142
|
+
el.remove();
|
|
143
|
+
delete this.#elements[cacheKey];
|
|
144
|
+
} else {
|
|
145
|
+
el.setAttribute(config.type === 'link' ? 'href' : 'content', '');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#parseViewport = (c) => Object.fromEntries(c.split(',').filter(Boolean).map(s => {
|
|
152
|
+
const [k, v] = s.split('=').map(p => p.trim());
|
|
153
|
+
return [k.replace(/-([a-z])/g, g => g.toUpperCase()), v || true];
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
push(id, config) {
|
|
157
|
+
if (!id) throw new Error("push() requires a unique ID");
|
|
158
|
+
if (this.#stack.some(e => e.id === id)) return;
|
|
159
|
+
this.#stack.push({ ...this.peek(), ...config, id, _isInitial: false });
|
|
160
|
+
this.#scheduleRender();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pop(id) {
|
|
164
|
+
if (!id) throw new Error("pop() requires a target ID");
|
|
165
|
+
const idx = this.#stack.findIndex(e => e.id === id);
|
|
166
|
+
if (idx > 0) { // Never pop the initial state at index 0
|
|
167
|
+
this.#stack.splice(idx, 1);
|
|
168
|
+
this.#scheduleRender();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
peek() { return this.#stack[this.#stack.length - 1]; }
|
|
173
|
+
}
|
|
@@ -35,9 +35,6 @@ export class WebfloClient extends AppRuntime {
|
|
|
35
35
|
#background;
|
|
36
36
|
get background() { return this.#background; }
|
|
37
37
|
|
|
38
|
-
#viewport;
|
|
39
|
-
get viewport() { return this.#viewport; }
|
|
40
|
-
|
|
41
38
|
get isClientSide() { return true; }
|
|
42
39
|
|
|
43
40
|
constructor(bootstrap, host) {
|
|
@@ -59,68 +56,6 @@ export class WebfloClient extends AppRuntime {
|
|
|
59
56
|
phase: 0
|
|
60
57
|
};
|
|
61
58
|
this.#background = new StarPort({ handshake: 1, autoClose: false });
|
|
62
|
-
|
|
63
|
-
// ---------------------
|
|
64
|
-
// Dynamic viewport styling
|
|
65
|
-
|
|
66
|
-
const oskToken = 'interactive-widget=resizes-content';
|
|
67
|
-
const hasOsk = (content) => content?.includes(oskToken);
|
|
68
|
-
const removeOsk = (content) => {
|
|
69
|
-
if (content?.includes('interactive-widget')) {
|
|
70
|
-
return content
|
|
71
|
-
.split(',')
|
|
72
|
-
.filter((s) => !s.includes('interactive-widget'))
|
|
73
|
-
.map((s) => s.trim())
|
|
74
|
-
.join(', ');
|
|
75
|
-
}
|
|
76
|
-
return content;
|
|
77
|
-
};
|
|
78
|
-
const addOsk = (content) => {
|
|
79
|
-
if (content?.includes('interactive-widget')) {
|
|
80
|
-
return content
|
|
81
|
-
.split(',')
|
|
82
|
-
.map((s) => s.includes('interactive-widget') ? oskToken : s.trim())
|
|
83
|
-
.join(', ');
|
|
84
|
-
}
|
|
85
|
-
return content + ', ' + oskToken;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const viewportMeta = document.querySelector('meta[name="viewport"]');
|
|
89
|
-
const viewportMetaInitialContent = viewportMeta?.content;
|
|
90
|
-
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
|
91
|
-
const renderViewportMetas = (entry) => {
|
|
92
|
-
viewportMeta?.setAttribute('content', entry.osk ? addOsk(viewportMetaInitialContent) : removeOsk(viewportMetaInitialContent));
|
|
93
|
-
themeColorMeta?.setAttribute('content', entry.themeColor);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const initial = {
|
|
97
|
-
themeColor: themeColorMeta?.content,
|
|
98
|
-
osk: hasOsk(viewportMetaInitialContent),
|
|
99
|
-
};
|
|
100
|
-
const viewportStack = [initial];
|
|
101
|
-
|
|
102
|
-
this.#viewport = {
|
|
103
|
-
push(entryId, { themeColor = viewportStack[0].themeColor, osk = viewportStack[0].osk }) {
|
|
104
|
-
if (typeof entryId !== 'string' || !entryId?.trim()) {
|
|
105
|
-
throw new Error('entryId cannot be ommited');
|
|
106
|
-
}
|
|
107
|
-
if (viewportStack.find((e) => e.entryId === entryId)) return;
|
|
108
|
-
viewportStack.unshift({ entryId, themeColor, osk });
|
|
109
|
-
renderViewportMetas(viewportStack[0]);
|
|
110
|
-
},
|
|
111
|
-
pop(entryId) {
|
|
112
|
-
if (typeof entryId !== 'string' || !entryId?.trim()) {
|
|
113
|
-
throw new Error('entryId cannot be ommited');
|
|
114
|
-
}
|
|
115
|
-
const index = viewportStack.findIndex((e) => e.entryId === entryId);
|
|
116
|
-
if (index === -1) return;
|
|
117
|
-
viewportStack.splice(index, 1);
|
|
118
|
-
renderViewportMetas(viewportStack[0]);
|
|
119
|
-
},
|
|
120
|
-
current() {
|
|
121
|
-
return viewportStack[0];
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
59
|
}
|
|
125
60
|
|
|
126
61
|
async initialize() {
|
|
@@ -3,6 +3,7 @@ import { LiveResponse } from '@webqit/fetch-plus';
|
|
|
3
3
|
import { HttpEvent111 } from '../webflo-routing/HttpEvent111.js';
|
|
4
4
|
import { ClientSideWorkport } from './ClientSideWorkport.js';
|
|
5
5
|
import { DeviceCapabilities } from './DeviceCapabilities.js';
|
|
6
|
+
import { DeviceViewport } from './DeviceViewport.js';
|
|
6
7
|
import { WebfloClient } from './WebfloClient.js';
|
|
7
8
|
import { WebfloHMR } from './webflo-devmode.js';
|
|
8
9
|
|
|
@@ -12,6 +13,8 @@ export class WebfloRootClientA extends WebfloClient {
|
|
|
12
13
|
|
|
13
14
|
static get DeviceCapabilities() { return DeviceCapabilities; }
|
|
14
15
|
|
|
16
|
+
static get DeviceViewport() { return DeviceViewport; }
|
|
17
|
+
|
|
15
18
|
static create(bootstrap, host) {
|
|
16
19
|
return new this(bootstrap, host);
|
|
17
20
|
}
|
|
@@ -19,12 +22,15 @@ export class WebfloRootClientA extends WebfloClient {
|
|
|
19
22
|
#network;
|
|
20
23
|
get network() { return this.#network; }
|
|
21
24
|
|
|
22
|
-
#
|
|
23
|
-
get
|
|
25
|
+
#viewport;
|
|
26
|
+
get viewport() { return this.#viewport; }
|
|
24
27
|
|
|
25
28
|
#capabilities;
|
|
26
29
|
get capabilities() { return this.#capabilities; }
|
|
27
30
|
|
|
31
|
+
#workport;
|
|
32
|
+
get workport() { return this.#workport; }
|
|
33
|
+
|
|
28
34
|
#hmr;
|
|
29
35
|
|
|
30
36
|
get withViewTransitions() {
|
|
@@ -67,10 +73,14 @@ export class WebfloRootClientA extends WebfloClient {
|
|
|
67
73
|
|
|
68
74
|
async setupCapabilities() {
|
|
69
75
|
const instanceController = await super.setupCapabilities();
|
|
70
|
-
const cleanups = [];
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
const cleanups = [];
|
|
73
78
|
instanceController.signal.addEventListener('abort', () => cleanups.forEach((c) => c()), { once: true });
|
|
79
|
+
|
|
80
|
+
// DeviceViewport, DeviceCapabilities, & Service Worker
|
|
81
|
+
|
|
82
|
+
this.#viewport = new this.constructor.DeviceViewport();
|
|
83
|
+
|
|
74
84
|
this.#capabilities = await this.constructor.DeviceCapabilities.initialize(this, this.config.CLIENT.capabilities);
|
|
75
85
|
cleanups.push(() => this.#capabilities.close());
|
|
76
86
|
|
|
@@ -272,30 +272,31 @@ export class ToastElement extends BaseElement {
|
|
|
272
272
|
|
|
273
273
|
/* ----------- */
|
|
274
274
|
|
|
275
|
-
:host(:not([popover="manual"]):popover-open)::backdrop {
|
|
275
|
+
:host(:not([popover="manual"], ._manual-dismiss):popover-open)::backdrop {
|
|
276
276
|
animation: flash 0.3s ease-in;
|
|
277
277
|
animation-iteration-count: 3;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
:host([popover="manual"])::backdrop {
|
|
280
|
+
:host(:is([popover="manual"], ._manual-dismiss))::backdrop {
|
|
281
281
|
/* Transition */
|
|
282
282
|
transition:
|
|
283
283
|
display 0.2s allow-discrete,
|
|
284
284
|
overlay 0.2s allow-discrete,
|
|
285
|
-
backdrop-filter 0.2s
|
|
285
|
+
backdrop-filter 0.2s,
|
|
286
|
+
background 0.2s;
|
|
286
287
|
}
|
|
287
288
|
|
|
288
|
-
:host([popover="manual"]:popover-open)::backdrop {
|
|
289
|
+
:host(:is([popover="manual"], ._manual-dismiss):popover-open)::backdrop {
|
|
289
290
|
backdrop-filter: blur(3px);
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
@starting-style {
|
|
293
|
-
:host([popover="manual"]:popover-open)::backdrop {
|
|
294
|
+
:host(:is([popover="manual"], ._manual-dismiss):popover-open)::backdrop {
|
|
294
295
|
backdrop-filter: none;
|
|
295
296
|
}
|
|
296
297
|
}
|
|
297
298
|
|
|
298
|
-
:host([popover="manual"]:popover-open)::before {
|
|
299
|
+
:host(:is([popover="manual"], ._manual-dismiss):popover-open)::before {
|
|
299
300
|
position: fixed;
|
|
300
301
|
inset: 0;
|
|
301
302
|
display: block;
|
|
@@ -345,7 +346,7 @@ export class ToastElement extends BaseElement {
|
|
|
345
346
|
transform: translateX(0.1rem);
|
|
346
347
|
}
|
|
347
348
|
|
|
348
|
-
:host(:not([popover="manual"])) .close-button {
|
|
349
|
+
:host(:not([popover="manual"], ._manual-dismiss)) .close-button {
|
|
349
350
|
display: none;
|
|
350
351
|
}
|
|
351
352
|
|
|
@@ -579,6 +580,11 @@ export class ModalElement extends BaseElement {
|
|
|
579
580
|
if (e.newState === 'open') {
|
|
580
581
|
this.#bindDimensionsWorker();
|
|
581
582
|
this.bindMinmaxWorker();
|
|
583
|
+
|
|
584
|
+
if (!this.delegatesFocus
|
|
585
|
+
&& !this.querySelector('[autofocus]')) {
|
|
586
|
+
this.shadowRoot.querySelector('[autofocus]')?.focus();
|
|
587
|
+
}
|
|
582
588
|
} else if (e.newState === 'closed') {
|
|
583
589
|
this.#unbindDimensionsWorker?.();
|
|
584
590
|
this.#unbindDimensionsWorker = null;
|
|
@@ -1404,7 +1410,7 @@ export class ModalElement extends BaseElement {
|
|
|
1404
1410
|
backdrop-filter: blur(3px);
|
|
1405
1411
|
}
|
|
1406
1412
|
|
|
1407
|
-
:host(:not([popover="manual"]):popover-open)::backdrop {
|
|
1413
|
+
:host(:not([popover="manual"], ._manual-dismiss):popover-open)::backdrop {
|
|
1408
1414
|
backdrop-filter: blur(0px);
|
|
1409
1415
|
}
|
|
1410
1416
|
|
|
@@ -1472,12 +1478,8 @@ export class ModalElement extends BaseElement {
|
|
|
1472
1478
|
border: none;
|
|
1473
1479
|
background: none;
|
|
1474
1480
|
}
|
|
1475
|
-
|
|
1476
|
-
:host(:not([popover="manual"])) {
|
|
1477
|
-
pointer-events: none;
|
|
1478
|
-
}
|
|
1479
1481
|
|
|
1480
|
-
:host(:not([popover="manual"])) .close-button {
|
|
1482
|
+
:host(:not([popover="manual"], ._manual-dismiss)) .close-button {
|
|
1481
1483
|
display: none;
|
|
1482
1484
|
}
|
|
1483
1485
|
|