@gjsify/adwaita-web 0.64.1 → 0.64.2-next.252
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 +12 -8
- package/dist/adwaita-web.css +39 -9
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/application.d.ts +1 -1
- package/lib/types/child-slot.spec.d.ts +1 -0
- package/lib/types/container-fill.spec.d.ts +1 -0
- package/lib/types/elements/adw-alert-dialog.d.ts +77 -17
- package/lib/types/elements/adw-toast-overlay.d.ts +8 -29
- package/lib/types/elements/adw-window-features.d.ts +8 -0
- package/lib/types/elements/gtk-application-window.d.ts +1 -4
- package/lib/types/elements/gtk-emoji-chooser.d.ts +2 -0
- package/lib/types/elements/gtk-popover-menu.d.ts +2 -0
- package/lib/types/elements/gtk-popover.d.ts +5 -0
- package/lib/types/elements/gtk-stack-page.d.ts +28 -0
- package/lib/types/elements/gtk-stack.d.ts +11 -0
- package/lib/types/elements/gtk-window.d.ts +16 -1
- package/lib/types/gobject-elements.d.ts +9 -0
- package/lib/types/index.d.ts +2 -2
- package/lib/types/learn6502-window.spec.d.ts +1 -0
- package/lib/types/namespace/adw.d.ts +1 -1
- package/lib/types/namespace/gtk.d.ts +2 -0
- package/lib/types/shared-tree-builder.d.ts +31 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-visible.spec.d.ts +1 -0
- package/lib/types/window-maximize.spec.d.ts +1 -0
- package/package.json +13 -12
- package/scss/_bin.scss +6 -3
- package/scss/_overlay.scss +11 -1
- package/scss/_reset.scss +1 -1
- package/scss/_stack.scss +8 -2
- package/scss/_toast.scss +13 -3
- package/scss/_window.scss +16 -0
- package/src/adw-alert-dialog.spec.ts +71 -5
- package/src/adw-bin.spec.ts +4 -3
- package/src/adw-header-bar.spec.ts +18 -0
- package/src/adw-toast-overlay.spec.ts +68 -24
- package/src/application.spec.ts +44 -0
- package/src/application.ts +13 -18
- package/src/capabilities.mjs +1 -3
- package/src/child-slot.spec.ts +71 -0
- package/src/construct-vectors.spec.ts +51 -6
- package/src/container-fill.spec.ts +70 -0
- package/src/elements/adw-alert-dialog.ts +174 -44
- package/src/elements/adw-application-window.ts +0 -3
- package/src/elements/adw-breakpoint-bin.ts +3 -0
- package/src/elements/adw-dialog.ts +3 -1
- package/src/elements/adw-header-bar.ts +4 -0
- package/src/elements/adw-toast-overlay.ts +35 -69
- package/src/elements/adw-window-features.ts +14 -2
- package/src/elements/gtk-application-window.ts +3 -8
- package/src/elements/gtk-drag-icon.ts +4 -0
- package/src/elements/gtk-emoji-chooser.ts +5 -0
- package/src/elements/gtk-flow-box-child.ts +3 -1
- package/src/elements/gtk-graphics-offload.ts +3 -1
- package/src/elements/gtk-list-box-row.ts +3 -1
- package/src/elements/gtk-overlay.ts +5 -4
- package/src/elements/gtk-paned.ts +7 -1
- package/src/elements/gtk-popover-bin.ts +3 -0
- package/src/elements/gtk-popover-menu.ts +5 -0
- package/src/elements/gtk-popover.ts +12 -0
- package/src/elements/gtk-search-bar.ts +5 -5
- package/src/elements/gtk-stack-page.ts +111 -0
- package/src/elements/gtk-stack.ts +50 -5
- package/src/elements/gtk-tree-expander.ts +3 -0
- package/src/elements/gtk-viewport.ts +4 -0
- package/src/elements/gtk-window-handle.ts +4 -0
- package/src/elements/gtk-window.ts +55 -6
- package/src/gobject-elements.ts +28 -0
- package/src/gtk-stack.spec.ts +170 -0
- package/src/index.ts +2 -2
- package/src/keyboard-operable.spec.ts +3 -3
- package/src/learn6502-window.spec.ts +113 -0
- package/src/namespace/adw.ts +1 -1
- package/src/namespace/gtk.ts +2 -0
- package/src/shared-tree-builder.ts +105 -10
- package/src/shared-trees.spec.ts +15 -1
- package/src/slotted-children.spec.ts +24 -0
- package/src/slotted-children.ts +18 -2
- package/src/styles.generated.ts +1 -1
- package/src/test.browser.mts +10 -0
- package/src/value-lists.spec.ts +3 -3
- package/src/widget-visible.spec.ts +104 -0
- package/src/window-maximize.spec.ts +93 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Learn6502's `main.window.blp`, reduced to the shapes that rendered short through
|
|
2
|
+
// `buildWithSiblings`: header-bar buttons after a single `[start]` (GtkBuilder packs an untyped
|
|
3
|
+
// child at start too), and `Overlay { child: Adw.ToastOverlay { Stack } }`. Every
|
|
4
|
+
// widget the file authors has to be in the document once the built window is attached — the
|
|
5
|
+
// failure was widgets that were built and then silently dropped on the way.
|
|
6
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
7
|
+
import { parseBlueprint, projectToSharedNode } from '@gjsify/blueprint';
|
|
8
|
+
|
|
9
|
+
import { buildWithSiblings } from './shared-tree-builder.js';
|
|
10
|
+
|
|
11
|
+
const BLUEPRINT = `using Gtk 4.0;
|
|
12
|
+
using Adw 1;
|
|
13
|
+
|
|
14
|
+
Adw.ApplicationWindow mainWindow {
|
|
15
|
+
content: Adw.ToolbarView toolbarView {
|
|
16
|
+
[top]
|
|
17
|
+
Adw.HeaderBar headerBar {
|
|
18
|
+
title-widget: Label titleLabel {
|
|
19
|
+
label: "Learn 6502";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
[start]
|
|
23
|
+
ToggleButton sidebarToggleButton {
|
|
24
|
+
icon-name: "sidebar-show-symbolic";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Button learnBackButton {
|
|
28
|
+
icon-name: "go-previous-symbolic";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Button unsavedChangesIndicator {
|
|
32
|
+
icon-name: "dot-symbolic";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[end]
|
|
36
|
+
MenuButton menuButton {
|
|
37
|
+
icon-name: "open-menu-symbolic";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
content: Overlay mainOverlay {
|
|
42
|
+
child: Adw.ToastOverlay toastOverlay {
|
|
43
|
+
Stack layoutHost {
|
|
44
|
+
StackPage singlePage {
|
|
45
|
+
name: "single";
|
|
46
|
+
child: Adw.Bin singleContainer {
|
|
47
|
+
child: Label singleLabel {
|
|
48
|
+
label: "single";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
StackPage threePage {
|
|
54
|
+
name: "three";
|
|
55
|
+
child: Adw.Bin threeColumnContainer {
|
|
56
|
+
child: Box threeColumns {
|
|
57
|
+
Label threeLabel {
|
|
58
|
+
label: "three";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const AUTHORED = [
|
|
71
|
+
'toolbarView',
|
|
72
|
+
'headerBar',
|
|
73
|
+
'titleLabel',
|
|
74
|
+
'sidebarToggleButton',
|
|
75
|
+
'learnBackButton',
|
|
76
|
+
'unsavedChangesIndicator',
|
|
77
|
+
'menuButton',
|
|
78
|
+
'mainOverlay',
|
|
79
|
+
'toastOverlay',
|
|
80
|
+
'layoutHost',
|
|
81
|
+
'singleContainer',
|
|
82
|
+
'singleLabel',
|
|
83
|
+
'threeColumnContainer',
|
|
84
|
+
'threeColumns',
|
|
85
|
+
'threeLabel',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
export const AdwLearn6502WindowTest = async () => {
|
|
89
|
+
await describe("Learn6502's main window through buildWithSiblings", async () => {
|
|
90
|
+
const { node, lost } = projectToSharedNode(parseBlueprint(BLUEPRINT, 'main.window.blp'));
|
|
91
|
+
|
|
92
|
+
await it('projects without losing anything', () => {
|
|
93
|
+
expect(lost.map((loss) => `${loss.kind}@${loss.line}`).join()).toBe('');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await it('puts every authored widget in the document, the start buttons at start', () => {
|
|
97
|
+
const { root } = buildWithSiblings(node);
|
|
98
|
+
document.body.append(root);
|
|
99
|
+
const missing = AUTHORED.filter((id) => !root.querySelector(`#${id}`)?.isConnected);
|
|
100
|
+
const start = ['sidebarToggleButton', 'learnBackButton', 'unsavedChangesIndicator'].filter(
|
|
101
|
+
(id) => root.querySelector(`#${id}`)?.parentElement?.className !== 'adw-header-bar-start',
|
|
102
|
+
);
|
|
103
|
+
const toast = root.querySelector('#toastOverlay');
|
|
104
|
+
const overlaid =
|
|
105
|
+
toast?.closest('.adw-overlay-child') === root.querySelector('#mainOverlay > .adw-overlay-child');
|
|
106
|
+
root.remove();
|
|
107
|
+
|
|
108
|
+
expect(missing.join()).toBe('');
|
|
109
|
+
expect(start.join()).toBe('');
|
|
110
|
+
expect(overlaid).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
};
|
package/src/namespace/adw.ts
CHANGED
|
@@ -111,4 +111,4 @@ export { AdwWindowTitle as WindowTitle } from '../elements/adw-window-title.js';
|
|
|
111
111
|
export { AdwWrapBox as WrapBox } from '../elements/adw-wrap-box.js';
|
|
112
112
|
export { AdwApplication as Application } from '../application.js';
|
|
113
113
|
export { AdwStyleManager as StyleManager } from '../style-manager.js';
|
|
114
|
-
export { ColorScheme } from '@gjsify/adwaita-core';
|
|
114
|
+
export { ColorScheme, Toast, ToastPriority } from '@gjsify/adwaita-core';
|
package/src/namespace/gtk.ts
CHANGED
|
@@ -86,6 +86,7 @@ export { GtkSeparator as Separator } from '../elements/gtk-separator.js';
|
|
|
86
86
|
export { GtkSpinButton as SpinButton } from '../elements/gtk-spin-button.js';
|
|
87
87
|
export { GtkSpinner as Spinner } from '../elements/gtk-spinner.js';
|
|
88
88
|
export { GtkStack as Stack } from '../elements/gtk-stack.js';
|
|
89
|
+
export { GtkStackPage as StackPage } from '../elements/gtk-stack-page.js';
|
|
89
90
|
export { GtkStackSidebar as StackSidebar } from '../elements/gtk-stack-sidebar.js';
|
|
90
91
|
export { GtkStackSwitcher as StackSwitcher } from '../elements/gtk-stack-switcher.js';
|
|
91
92
|
export { GtkStringList as StringList } from '@gjsify/adwaita-core';
|
|
@@ -101,6 +102,7 @@ export type { WindowCommon } from '@gjsify/adwaita-core';
|
|
|
101
102
|
export { GtkWindowControls as WindowControls } from '../elements/gtk-window-controls.js';
|
|
102
103
|
export { GtkViewport as Viewport } from '../elements/gtk-viewport.js';
|
|
103
104
|
export { GtkWindowHandle as WindowHandle } from '../elements/gtk-window-handle.js';
|
|
105
|
+
export { ApplicationInhibitFlags } from '@gjsify/adwaita-core';
|
|
104
106
|
export { GtkApplication as Application } from '../application.js';
|
|
105
107
|
export {
|
|
106
108
|
STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
@@ -55,7 +55,14 @@ import {
|
|
|
55
55
|
|
|
56
56
|
import { observeAdaptiveSize } from './breakpoints.js';
|
|
57
57
|
import { capabilities } from './capabilities.mjs';
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
endpointOf,
|
|
60
|
+
hasElementApi,
|
|
61
|
+
hasInstalledVisible,
|
|
62
|
+
installElementApi,
|
|
63
|
+
installSensitive,
|
|
64
|
+
installVisible,
|
|
65
|
+
} from './gobject-elements.js';
|
|
59
66
|
import { dispatchedSignalsOf } from './signals.js';
|
|
60
67
|
import { slottedChildrenOf } from './slotted-children.js';
|
|
61
68
|
import { isTemplateTag, templateTagFor } from './template-classes.js';
|
|
@@ -154,14 +161,93 @@ function isWritable(el: object, member: string): boolean {
|
|
|
154
161
|
* upgraded has declared no slots yet, so the refusal below belongs to the mount.
|
|
155
162
|
*/
|
|
156
163
|
export function buildSharedTree(node: SharedTreeNode, record: BuildRecord = newRecord()): HTMLElement {
|
|
164
|
+
refuseSiblings(node, 'buildSharedTree');
|
|
165
|
+
return buildTree(node, record).root;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What {@link buildWithSiblings} hands back: the root element and the objects built beside it. */
|
|
169
|
+
export interface BuiltTree {
|
|
170
|
+
/** The element the tree roots at, as {@link buildSharedTree} returns it. */
|
|
171
|
+
root: HTMLElement;
|
|
172
|
+
/**
|
|
173
|
+
* The sibling objects by id — `Adw.AlertDialog unsavedChangesDialog { }` or `$Learn learn { }`
|
|
174
|
+
* written after the template. A sibling is built in the root's id scope (a setter or a bind
|
|
175
|
+
* may name it) but is no child of the root: the code beside the file decides where, or
|
|
176
|
+
* whether, it is placed.
|
|
177
|
+
*/
|
|
178
|
+
siblings: ReadonlyMap<string, HTMLElement>;
|
|
179
|
+
/** Every element the tree names by id, siblings included: what `GtkBuilder.get_object` answers for. */
|
|
180
|
+
objects: ReadonlyMap<string, HTMLElement>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** What {@link buildWithSiblings} takes beside the tree. */
|
|
184
|
+
export interface BuildOptions {
|
|
185
|
+
/** The object a handler NAME is looked up on (ADR 0093 § 3). */
|
|
186
|
+
scope?: Readonly<Record<string, unknown>>;
|
|
187
|
+
/** The size source a `breakpoints` node is driven from. */
|
|
188
|
+
observeSize?: SizeSource;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A tree with sibling roots (ADR 0093): the root element, and each sibling by id, all DETACHED
|
|
193
|
+
* like {@link buildSharedTree}'s result.
|
|
194
|
+
*
|
|
195
|
+
* {@link buildSharedTree}, {@link mountSharedTree} and {@link buildTemplateTree} refuse such a
|
|
196
|
+
* tree by name, because they return one element and the siblings would be built and then thrown
|
|
197
|
+
* away. A sibling with no id is refused too: nothing could ever ask for it, which is how a
|
|
198
|
+
* dropped object looks finished.
|
|
199
|
+
*/
|
|
200
|
+
export function buildWithSiblings(node: SharedTreeNode, options: BuildOptions = {}): BuiltTree {
|
|
201
|
+
const record = newRecord(options.scope, options.observeSize);
|
|
202
|
+
const built = buildTree(node, record);
|
|
203
|
+
// An element declares its slots when it is connected, so a detached tree cannot be checked
|
|
204
|
+
// as it stands. It is connected for the length of the check and handed back detached, as
|
|
205
|
+
// documented: a placement the renderer could not honour is refused here, like
|
|
206
|
+
// `mountSharedTree` refuses it, instead of surfacing as a widget missing from the page.
|
|
207
|
+
const probe = document.createElement('div');
|
|
208
|
+
probe.hidden = true;
|
|
209
|
+
probe.append(built.root, ...built.siblings.values());
|
|
210
|
+
document.body.append(probe);
|
|
211
|
+
try {
|
|
212
|
+
refuseUnknownSlots(record.placed);
|
|
213
|
+
refuseUnheldExtensions(record.extended);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
for (const dispose of record.disposers) dispose();
|
|
216
|
+
throw error;
|
|
217
|
+
} finally {
|
|
218
|
+
probe.remove();
|
|
219
|
+
for (const el of [built.root, ...built.siblings.values()]) el.remove();
|
|
220
|
+
}
|
|
221
|
+
return built;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function refuseSiblings(node: SharedTreeNode, door: string): void {
|
|
225
|
+
if ((node.siblings?.length ?? 0) === 0) return;
|
|
226
|
+
throw new Error(
|
|
227
|
+
`\`${node.tag}\` carries ${node.siblings!.length} sibling object(s), and \`${door}\` returns one object ` +
|
|
228
|
+
'so they would be built and lost. Use `buildWithSiblings`, which hands them back by id.',
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Every node, its siblings in the same id scope, then the binds and breakpoints that name them. */
|
|
233
|
+
function buildTree(node: SharedTreeNode, record: BuildRecord): BuiltTree {
|
|
157
234
|
// ADR 0093 § 2: the whole tree is checked against the capability table before an element is
|
|
158
235
|
// created. A refused `layout` is named with its node and the reason the table gives.
|
|
159
236
|
assertTreeConstructs('adwaita-web', capabilities, node);
|
|
160
237
|
if (node.menus !== undefined) record.menus = node.menus;
|
|
161
238
|
const root = buildNode(node, record);
|
|
239
|
+
const siblings = new Map<string, HTMLElement>();
|
|
240
|
+
for (const sibling of node.siblings ?? []) {
|
|
241
|
+
if (sibling.id === undefined) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`the sibling \`${sibling.tag}\` has no id, so nothing could ever ask for it: give it one in the .blp.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
siblings.set(sibling.id, buildNode(sibling, record));
|
|
247
|
+
}
|
|
162
248
|
for (const bind of record.binds) bindProperty(bind, record);
|
|
163
249
|
for (const host of record.breakpointHosts) bindBreakpoints(host.el, host.node, record);
|
|
164
|
-
return root;
|
|
250
|
+
return { root, siblings, objects: new Map(record.ids) };
|
|
165
251
|
}
|
|
166
252
|
|
|
167
253
|
/** A fresh record, with the scope when the caller has one. */
|
|
@@ -344,7 +430,9 @@ export function writeProp(el: HTMLElement, prop: string, value: string | number
|
|
|
344
430
|
const member = propertyOf(prop);
|
|
345
431
|
const expand = GTK_WIDGET_EXPAND.find((name) => name === attr);
|
|
346
432
|
if (expand !== undefined && typeof value === 'boolean') el.setAttribute(expand, value ? '' : 'false');
|
|
347
|
-
else if (
|
|
433
|
+
else if (attr === 'visible' && typeof value === 'boolean' && hasInstalledVisible(el)) {
|
|
434
|
+
(el as unknown as Record<string, unknown>)[member] = value;
|
|
435
|
+
} else if (isTemplateTag(el.localName) && member in el) {
|
|
348
436
|
// An application class has no attributeChangedCallback: its properties are GObject properties,
|
|
349
437
|
// set the way `Gtk.Builder` sets them, after the constructor.
|
|
350
438
|
(el as unknown as Record<string, unknown>)[member] = value;
|
|
@@ -372,9 +460,14 @@ function buildNode(node: SharedTreeNode, record: BuildRecord): HTMLElement {
|
|
|
372
460
|
const el = document.createElement(node.extern === true ? templateTagFor(node.tag) : hostTagOf(node.tag));
|
|
373
461
|
// A template's children talk GObject to the template: `connect`, `emit`, `notify` (ADR 0096).
|
|
374
462
|
if (record.template !== undefined) installElementApi(el);
|
|
463
|
+
// `Gtk.Widget:visible`, which no element spells: a setter's target and an authored `false` need it.
|
|
464
|
+
installVisible(el);
|
|
375
465
|
// The id is how the TypeScript beside a `.blp` reaches this element
|
|
376
466
|
// (`root.querySelector('#…')`), the counterpart of `InternalChildren` on GTK.
|
|
377
467
|
if (node.id !== undefined) {
|
|
468
|
+
if (record.ids.has(node.id)) {
|
|
469
|
+
throw new Error(`Two nodes of this tree carry the id '${node.id}'; GtkBuilder refuses a duplicate id.`);
|
|
470
|
+
}
|
|
378
471
|
el.id = node.id;
|
|
379
472
|
record.ids.set(node.id, el);
|
|
380
473
|
}
|
|
@@ -492,6 +585,7 @@ export interface BuiltTemplateTree {
|
|
|
492
585
|
* belong to the first `connectedCallback`.
|
|
493
586
|
*/
|
|
494
587
|
export function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, host: HTMLElement): BuiltTemplateTree {
|
|
588
|
+
refuseSiblings(tree, 'buildTemplateTree');
|
|
495
589
|
assertTreeConstructs('adwaita-web', capabilities, tree);
|
|
496
590
|
const record = newRecord(undefined, undefined, scope);
|
|
497
591
|
record.menus = tree.menus;
|
|
@@ -591,9 +685,9 @@ function writeExtensions(el: HTMLElement, node: SharedTreeNode): void {
|
|
|
591
685
|
|
|
592
686
|
/** The response API an element answers to — `<adw-alert-dialog>`'s, named after libadwaita's. */
|
|
593
687
|
interface ResponseReader {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
688
|
+
get_response_label(id: string): string | null;
|
|
689
|
+
get_response_appearance(id: string): string | null;
|
|
690
|
+
get_response_enabled(id: string): boolean;
|
|
597
691
|
}
|
|
598
692
|
|
|
599
693
|
/**
|
|
@@ -617,10 +711,10 @@ function refuseUnheldExtensions(extended: readonly ExtendedNode[]): void {
|
|
|
617
711
|
const reader = el as unknown as Partial<ResponseReader>;
|
|
618
712
|
for (const response of node.extensions?.responses ?? []) {
|
|
619
713
|
const held =
|
|
620
|
-
typeof reader.
|
|
621
|
-
reader.
|
|
622
|
-
reader.
|
|
623
|
-
reader.
|
|
714
|
+
typeof reader.get_response_label === 'function' &&
|
|
715
|
+
reader.get_response_label(response.id) === response.label &&
|
|
716
|
+
reader.get_response_appearance?.(response.id) === (response.appearance ?? 'default') &&
|
|
717
|
+
reader.get_response_enabled?.(response.id) === (response.enabled ?? true);
|
|
624
718
|
if (!held) {
|
|
625
719
|
throw new Error(
|
|
626
720
|
`<${el.localName}> did not register the response "${response.id}" as authored, so the ` +
|
|
@@ -696,6 +790,7 @@ export interface MountOptions {
|
|
|
696
790
|
* `buildSharedTree` is for a caller that already has somewhere of its own to attach it.
|
|
697
791
|
*/
|
|
698
792
|
export function mountSharedTree(node: SharedTreeNode, options: MountOptions = {}): MountedSharedTree {
|
|
793
|
+
refuseSiblings(node, 'mountSharedTree');
|
|
699
794
|
const { into } = options;
|
|
700
795
|
const host = into ?? document.createElement('div');
|
|
701
796
|
const record = newRecord(options.scope, options.observeSize);
|
package/src/shared-trees.spec.ts
CHANGED
|
@@ -54,7 +54,7 @@ import { ADWAITA_GALLERY_SHARED_TREES } from '../../../../scripts/adwaita-galler
|
|
|
54
54
|
|
|
55
55
|
import '@gjsify/adwaita-web';
|
|
56
56
|
|
|
57
|
-
import { mountSharedTree } from './shared-tree-builder.js';
|
|
57
|
+
import { buildWithSiblings, mountSharedTree } from './shared-tree-builder.js';
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Connect the authored root — these elements build on connect — run, and take it down again.
|
|
@@ -364,6 +364,20 @@ export const AdwSharedTreesTest = async () => {
|
|
|
364
364
|
).toThrow('has no slot "middle"');
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
+
// The detached door refuses it too: a root built and handed back is mounted by the
|
|
368
|
+
// caller, after the builder could still say anything.
|
|
369
|
+
await it('`buildWithSiblings` refuses the same placement, and hands a good tree back detached', () => {
|
|
370
|
+
const bad = { tag: 'AdwToolbarView', children: [{ tag: 'AdwBanner', slot: 'middle' }] };
|
|
371
|
+
expect(() => buildWithSiblings(bad)).toThrow('has no slot "middle"');
|
|
372
|
+
const { root, siblings } = buildWithSiblings({
|
|
373
|
+
tag: 'AdwToolbarView',
|
|
374
|
+
children: [{ tag: 'AdwBanner', slot: 'top' }],
|
|
375
|
+
siblings: [{ tag: 'GtkLabel', id: 'beside' }],
|
|
376
|
+
});
|
|
377
|
+
expect(root.isConnected || siblings.get('beside')!.isConnected).toBe(false);
|
|
378
|
+
expect(root.querySelector('adw-banner') !== null).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
367
381
|
// ADR 0092's `layout { }`: a grid child's cell. GTK counts from 0, CSS Grid from 1.
|
|
368
382
|
await it('`layout { }` places a grid child in its cell, spans defaulting to one', () => {
|
|
369
383
|
mounted(
|
|
@@ -113,6 +113,30 @@ export const AdwSlottedChildrenTest = async () => {
|
|
|
113
113
|
expect(report).toBe('[1]');
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
// The DECLARED twin of the case above. `install()` replaces the host's children with
|
|
117
|
+
// its structure, and a child no slot took used to go with them — gone without a word.
|
|
118
|
+
await it('keeps a declared child no slot takes, and warns that it was not placed', () => {
|
|
119
|
+
const host = document.createElement('div');
|
|
120
|
+
const typo = probe('sied', 'a');
|
|
121
|
+
host.append(document.createTextNode('\n '), typo);
|
|
122
|
+
mounted(host);
|
|
123
|
+
const side = document.createElement('div');
|
|
124
|
+
side.className = 'side';
|
|
125
|
+
|
|
126
|
+
const warned: string[] = [];
|
|
127
|
+
const warn = console.warn;
|
|
128
|
+
console.warn = (...args: unknown[]) => void warned.push(args.join(' '));
|
|
129
|
+
try {
|
|
130
|
+
bindSlottedChildren(host, [{ name: 'side', into: side }]).install(side);
|
|
131
|
+
} finally {
|
|
132
|
+
console.warn = warn;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const report = `${placement(host, typo)} | ${warned.join()}`;
|
|
136
|
+
host.remove();
|
|
137
|
+
expect(report).toBe("[1] | 'sied' is not a valid child type of 'div'");
|
|
138
|
+
});
|
|
139
|
+
|
|
116
140
|
await it('re-adopts a child that was appended, removed and appended again', async () => {
|
|
117
141
|
const host = mounted(document.createElement('div'));
|
|
118
142
|
const side = document.createElement('div');
|
package/src/slotted-children.ts
CHANGED
|
@@ -113,6 +113,12 @@ export interface AdwSlottedChildren {
|
|
|
113
113
|
/** Where a bound host's slots can be read back from — see {@link slottedChildrenOf}. */
|
|
114
114
|
const BOUND = new WeakMap<Element, AdwSlottedChildren>();
|
|
115
115
|
|
|
116
|
+
/** An element or a text node with something in it — markup's indentation and comments are not. */
|
|
117
|
+
function isContent(node: Node): boolean {
|
|
118
|
+
if (node instanceof Element) return true;
|
|
119
|
+
return node.nodeType === Node.TEXT_NODE && (node.textContent ?? '').trim() !== '';
|
|
120
|
+
}
|
|
121
|
+
|
|
116
122
|
/**
|
|
117
123
|
* The slot binding `host` installed, if it has one.
|
|
118
124
|
*
|
|
@@ -184,9 +190,19 @@ export function bindSlottedChildren(
|
|
|
184
190
|
slots,
|
|
185
191
|
slotFor,
|
|
186
192
|
install(...nodes: readonly Node[]) {
|
|
187
|
-
|
|
193
|
+
const unplaced: Node[] = [];
|
|
194
|
+
for (const node of Array.from(host.childNodes)) {
|
|
195
|
+
if (route(node) !== null || node.parentNode !== host || !isContent(node)) continue;
|
|
196
|
+
// `replaceChildren` below would delete it, and a child the author wrote that
|
|
197
|
+
// vanishes without a word is the silent drop this used to be. It stays beside
|
|
198
|
+
// the structure, as a late child with the same slot does, and says so the
|
|
199
|
+
// way GtkBuilder does (GTK_BUILDER_WARN_INVALID_CHILD_TYPE).
|
|
200
|
+
unplaced.push(node);
|
|
201
|
+
const type = node instanceof Element ? (node.getAttribute('slot') ?? '') : '';
|
|
202
|
+
console.warn(`'${type}' is not a valid child type of '${host.localName}'`);
|
|
203
|
+
}
|
|
188
204
|
structure = new Set(nodes);
|
|
189
|
-
host.replaceChildren(...nodes);
|
|
205
|
+
host.replaceChildren(...nodes, ...unplaced);
|
|
190
206
|
// Armed AFTER `replaceChildren`, so its own records are never delivered: an
|
|
191
207
|
// observer armed first would see the whole of `structure` as added nodes and
|
|
192
208
|
// spend a microtask rejecting them.
|