@gjsify/adwaita-web 0.11.0 → 0.12.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 +5 -5
- package/dist/adwaita-web.css +2742 -263
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/elements/adw-about-dialog.d.ts +77 -0
- package/lib/types/elements/adw-action-row.d.ts +15 -0
- package/lib/types/elements/adw-alert-dialog.d.ts +80 -0
- package/lib/types/elements/adw-avatar.d.ts +9 -0
- package/lib/types/elements/adw-banner.d.ts +9 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +35 -0
- package/lib/types/elements/adw-button-content.d.ts +10 -0
- package/lib/types/elements/adw-button-row.d.ts +11 -0
- package/lib/types/elements/adw-button.d.ts +11 -0
- package/lib/types/elements/adw-carousel.d.ts +64 -0
- package/lib/types/elements/adw-clamp.d.ts +6 -0
- package/lib/types/elements/adw-combo-row.d.ts +3 -0
- package/lib/types/elements/adw-entry-row.d.ts +10 -0
- package/lib/types/elements/adw-entry.d.ts +11 -0
- package/lib/types/elements/adw-expander-row.d.ts +26 -0
- package/lib/types/elements/adw-header-bar.d.ts +4 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +30 -0
- package/lib/types/elements/adw-navigation-split-view.d.ts +15 -0
- package/lib/types/elements/adw-navigation-view.d.ts +66 -0
- package/lib/types/elements/adw-password-entry-row.d.ts +16 -0
- package/lib/types/elements/adw-preferences-dialog.d.ts +36 -0
- package/lib/types/elements/adw-preferences-group.d.ts +8 -0
- package/lib/types/elements/adw-sidebar.d.ts +29 -0
- package/lib/types/elements/adw-spin-row.d.ts +3 -0
- package/lib/types/elements/adw-spinner.d.ts +6 -0
- package/lib/types/elements/adw-split-button.d.ts +18 -0
- package/lib/types/elements/adw-status-page.d.ts +11 -0
- package/lib/types/elements/adw-switch-row.d.ts +3 -0
- package/lib/types/elements/adw-tab-view.d.ts +24 -0
- package/lib/types/elements/adw-toast-overlay.d.ts +19 -4
- package/lib/types/elements/adw-toggle-group.d.ts +19 -0
- package/lib/types/elements/adw-toolbar-view.d.ts +13 -0
- package/lib/types/elements/adw-view-switcher.d.ts +29 -0
- package/lib/types/elements/adw-window-title.d.ts +9 -0
- package/lib/types/elements/adw-wrap-box.d.ts +13 -0
- package/lib/types/index.d.ts +29 -0
- package/lib/types/styles.generated.d.ts +1 -0
- package/package.json +5 -5
- package/scss/_about_dialog.scss +257 -0
- package/scss/_action_row.scss +65 -0
- package/scss/_alert_dialog.scss +151 -0
- package/scss/_avatar.scss +31 -0
- package/scss/_banner.scss +46 -0
- package/scss/_bottom_sheet.scss +113 -0
- package/scss/_button.scss +168 -0
- package/scss/_button_content.scss +48 -0
- package/scss/_button_row.scss +100 -0
- package/scss/_carousel.scss +130 -0
- package/scss/_clamp.scss +11 -0
- package/scss/_entry.scss +49 -0
- package/scss/_entry_row.scss +54 -0
- package/scss/_expander_row.scss +150 -0
- package/scss/_headerbar.scss +1 -1
- package/scss/_inline_view_switcher.scss +174 -0
- package/scss/_navigation_split_view.scss +48 -0
- package/scss/_navigation_view.scss +80 -0
- package/scss/_overlay_split_view.scss +19 -0
- package/scss/_password_entry_row.scss +67 -0
- package/scss/_preferences.scss +26 -3
- package/scss/_preferences_dialog.scss +154 -0
- package/scss/_row.scss +31 -0
- package/scss/_sidebar.scss +213 -0
- package/scss/_spin_row.scss +0 -1
- package/scss/_spinner.scss +28 -0
- package/scss/_split_button.scss +222 -0
- package/scss/_status_page.scss +40 -0
- package/scss/_switch_row.scss +0 -1
- package/scss/_tab_view.scss +193 -0
- package/scss/_theme.scss +12 -0
- package/scss/_toast.scss +122 -12
- package/scss/_toggle_group.scss +124 -0
- package/scss/_toolbar_view.scss +31 -0
- package/scss/_variables.scss +19 -0
- package/scss/_view_switcher.scss +133 -0
- package/scss/_window_title.scss +33 -0
- package/scss/_wrap_box.scss +20 -0
- package/scss/adwaita-skin.scss +38 -0
- package/src/elements/adw-about-dialog.ts +629 -0
- package/src/elements/adw-action-row.ts +82 -0
- package/src/elements/adw-alert-dialog.ts +385 -0
- package/src/elements/adw-avatar.ts +108 -0
- package/src/elements/adw-banner.ts +57 -0
- package/src/elements/adw-bottom-sheet.ts +205 -0
- package/src/elements/adw-button-content.ts +69 -0
- package/src/elements/adw-button-row.ts +75 -0
- package/src/elements/adw-button.ts +81 -0
- package/src/elements/adw-carousel.ts +321 -0
- package/src/elements/adw-clamp.ts +28 -0
- package/src/elements/adw-combo-row.ts +32 -20
- package/src/elements/adw-entry-row.ts +69 -0
- package/src/elements/adw-entry.ts +61 -0
- package/src/elements/adw-expander-row.ts +167 -0
- package/src/elements/adw-header-bar.ts +21 -8
- package/src/elements/adw-inline-view-switcher.ts +222 -0
- package/src/elements/adw-navigation-split-view.ts +89 -0
- package/src/elements/adw-navigation-view.ts +344 -0
- package/src/elements/adw-password-entry-row.ts +121 -0
- package/src/elements/adw-preferences-dialog.ts +240 -0
- package/src/elements/adw-preferences-group.ts +79 -12
- package/src/elements/adw-sidebar.ts +238 -0
- package/src/elements/adw-spin-row.ts +28 -14
- package/src/elements/adw-spinner.ts +31 -0
- package/src/elements/adw-split-button.ts +206 -0
- package/src/elements/adw-status-page.ts +66 -0
- package/src/elements/adw-switch-row.ts +29 -23
- package/src/elements/adw-tab-view.ts +184 -0
- package/src/elements/adw-toast-overlay.ts +98 -19
- package/src/elements/adw-toggle-group.ts +126 -0
- package/src/elements/adw-toolbar-view.ts +60 -0
- package/src/elements/adw-view-switcher.ts +200 -0
- package/src/elements/adw-window-title.ts +48 -0
- package/src/elements/adw-wrap-box.ts +139 -0
- package/src/index.js +1 -1
- package/src/index.ts +46 -3
- package/src/styles.generated.js +2976 -0
- package/src/styles2.generated.js +2976 -0
- package/style.css.d.ts +17 -4
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// <adw-navigation-view> — A page-based navigation container (the web
|
|
2
|
+
// counterpart of Adw.NavigationView). It presents one page at a time and keeps
|
|
3
|
+
// a navigation stack controlled with push()/pop()/replace(); the visible page
|
|
4
|
+
// fills the view. Only <adw-navigation-page> children are managed.
|
|
5
|
+
//
|
|
6
|
+
// Pages are declared as <adw-navigation-page> children (each with a `title` and
|
|
7
|
+
// optional `tag` attribute). The view snapshots them at connect time. Like
|
|
8
|
+
// Adw.NavigationView, a statically-added page is kept around but not shown until
|
|
9
|
+
// pushed (the first added page is pushed automatically). push() accepts a page
|
|
10
|
+
// element OR an existing page's tag (push-by-tag); a dynamically-pushed page is
|
|
11
|
+
// removed from the DOM again once it is popped.
|
|
12
|
+
//
|
|
13
|
+
// Header Bar integration: mirroring libadwaita, when the visible page is above
|
|
14
|
+
// the root the view surfaces an automatic back button at the start of that
|
|
15
|
+
// page's header bar (the first <adw-header-bar> found in the page content),
|
|
16
|
+
// unless the page sets `no-back-button` (the equivalent of
|
|
17
|
+
// AdwHeaderBar:show-back-button = FALSE) or `can-pop` is "false".
|
|
18
|
+
//
|
|
19
|
+
// Attributes (on <adw-navigation-view>):
|
|
20
|
+
// animate-transitions (boolean, default present — slide animation on push/pop;
|
|
21
|
+
// mirrors Adw.NavigationView:animate-transitions)
|
|
22
|
+
// Attributes (on <adw-navigation-page>):
|
|
23
|
+
// title (the page title — shown in the header bar's window title)
|
|
24
|
+
// tag (a unique identifier for push-by-tag)
|
|
25
|
+
// can-pop ("false" disables the back button / popping; mirrors
|
|
26
|
+
// AdwNavigationPage:can-pop)
|
|
27
|
+
// no-back-button (boolean — suppresses the automatic back button only)
|
|
28
|
+
// Events (CustomEvent, bubbles):
|
|
29
|
+
// `pushed` (detail = { tag }) after a page is pushed — mirrors
|
|
30
|
+
// AdwNavigationView::pushed.
|
|
31
|
+
// `popped` (detail = { tag }) after a page is popped — mirrors
|
|
32
|
+
// AdwNavigationView::popped.
|
|
33
|
+
// `replaced` after the whole stack is replaced — mirrors
|
|
34
|
+
// AdwNavigationView::replaced.
|
|
35
|
+
// `notify::visible-page` (detail = { tag }) whenever the visible page changes
|
|
36
|
+
// — mirrors the Adw.NavigationView:visible-page property.
|
|
37
|
+
// Reference: refs/libadwaita/src/adw-navigation-view.c (AdwNavigationView behaviour)
|
|
38
|
+
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/navigationview.md
|
|
39
|
+
// Copyright (c) 2022-2023 Purism SPC / GNOME contributors (libadwaita). LGPLv2.1+.
|
|
40
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
41
|
+
|
|
42
|
+
/** A single navigation page. Children of <adw-navigation-view>. */
|
|
43
|
+
export class AdwNavigationPage extends HTMLElement {
|
|
44
|
+
static get observedAttributes() {
|
|
45
|
+
return ['title', 'tag', 'can-pop', 'no-back-button'];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get title(): string {
|
|
49
|
+
return this.getAttribute('title') ?? '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set title(value: string) {
|
|
53
|
+
this.setAttribute('title', value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get tag(): string | null {
|
|
57
|
+
return this.getAttribute('tag');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
set tag(value: string | null) {
|
|
61
|
+
if (value === null) this.removeAttribute('tag');
|
|
62
|
+
else this.setAttribute('tag', value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether the page can be popped (and thus shows a back button). */
|
|
66
|
+
get canPop(): boolean {
|
|
67
|
+
return this.getAttribute('can-pop') !== 'false';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
set canPop(value: boolean) {
|
|
71
|
+
this.setAttribute('can-pop', value ? 'true' : 'false');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class AdwNavigationView extends HTMLElement {
|
|
76
|
+
private _pagesEl!: HTMLDivElement;
|
|
77
|
+
// Every page known to the view (static + dynamically pushed), in DOM order.
|
|
78
|
+
private _pages: AdwNavigationPage[] = [];
|
|
79
|
+
// The navigation stack — references into _pages, bottom-first.
|
|
80
|
+
private _stack: AdwNavigationPage[] = [];
|
|
81
|
+
// Pages added dynamically via push() (destroyed when popped off).
|
|
82
|
+
private _dynamic = new Set<AdwNavigationPage>();
|
|
83
|
+
// The back button injected into each visible page (so we can remove it).
|
|
84
|
+
private _backButtons = new WeakMap<AdwNavigationPage, HTMLElement>();
|
|
85
|
+
private _initialized = false;
|
|
86
|
+
|
|
87
|
+
static get observedAttributes() {
|
|
88
|
+
return ['animate-transitions'];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Whether push/pop transitions are animated. */
|
|
92
|
+
get animateTransitions(): boolean {
|
|
93
|
+
// Absent attribute defaults to true (matches Adw.NavigationView).
|
|
94
|
+
return this.getAttribute('animate-transitions') !== 'false';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
set animateTransitions(value: boolean) {
|
|
98
|
+
this.setAttribute('animate-transitions', value ? 'true' : 'false');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The currently-visible page (top of the stack), or null when empty. */
|
|
102
|
+
get visiblePage(): AdwNavigationPage | null {
|
|
103
|
+
return this._stack.length > 0 ? this._stack[this._stack.length - 1] : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The tag of the visible page, or null. */
|
|
107
|
+
get visiblePageTag(): string | null {
|
|
108
|
+
return this.visiblePage?.tag ?? null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
connectedCallback() {
|
|
112
|
+
if (this._initialized) return;
|
|
113
|
+
this._initialized = true;
|
|
114
|
+
|
|
115
|
+
// Snapshot the declared <adw-navigation-page> children — they become the
|
|
116
|
+
// statically-added pages. The first one is pushed automatically.
|
|
117
|
+
const pages = Array.from(this.querySelectorAll(':scope > adw-navigation-page')) as AdwNavigationPage[];
|
|
118
|
+
|
|
119
|
+
this._pagesEl = document.createElement('div');
|
|
120
|
+
this._pagesEl.className = 'adw-navigation-view-pages';
|
|
121
|
+
|
|
122
|
+
this._pages = [];
|
|
123
|
+
this._stack = [];
|
|
124
|
+
|
|
125
|
+
for (const page of pages) {
|
|
126
|
+
page.classList.add('adw-navigation-page');
|
|
127
|
+
this._pagesEl.appendChild(page);
|
|
128
|
+
this._pages.push(page);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.replaceChildren(this._pagesEl);
|
|
132
|
+
|
|
133
|
+
// Auto-push the first added page (Adw.NavigationView behaviour).
|
|
134
|
+
if (this._pages.length > 0) {
|
|
135
|
+
this._stack.push(this._pages[0]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this._syncClasses();
|
|
139
|
+
this._render();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
attributeChangedCallback() {
|
|
143
|
+
if (!this._initialized) return;
|
|
144
|
+
// animate-transitions only affects future transitions; just keep the
|
|
145
|
+
// class in sync so CSS can gate the slide animation.
|
|
146
|
+
this._syncClasses();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Add a static page without pushing it (mirrors AdwNavigationView.add). The
|
|
151
|
+
* view keeps a reference; the page becomes reachable via push-by-tag. Named
|
|
152
|
+
* `addPage` rather than `add` to avoid clashing with any DOM method.
|
|
153
|
+
*/
|
|
154
|
+
addPage(page: AdwNavigationPage): void {
|
|
155
|
+
if (this._pages.includes(page)) return;
|
|
156
|
+
page.classList.add('adw-navigation-page');
|
|
157
|
+
this._pagesEl.appendChild(page);
|
|
158
|
+
this._pages.push(page);
|
|
159
|
+
this._render();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Remove a static page (mirrors AdwNavigationView.remove). Named `removePage`
|
|
164
|
+
* to avoid overriding HTMLElement.remove() (which takes no arguments).
|
|
165
|
+
*/
|
|
166
|
+
removePage(page: AdwNavigationPage): void {
|
|
167
|
+
if (this._stack.includes(page)) return; // can't remove a page on the stack
|
|
168
|
+
const index = this._pages.indexOf(page);
|
|
169
|
+
if (index === -1) return;
|
|
170
|
+
this._pages.splice(index, 1);
|
|
171
|
+
this._dynamic.delete(page);
|
|
172
|
+
if (page.parentNode === this._pagesEl) this._pagesEl.removeChild(page);
|
|
173
|
+
this._render();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Push a page onto the navigation stack (mirrors AdwNavigationView.push).
|
|
178
|
+
* Accepts a page element or, when given a string, the tag of a known page
|
|
179
|
+
* (push-by-tag, mirrors AdwNavigationView.push_by_tag). A page that is not
|
|
180
|
+
* already known is added dynamically and destroyed when it is popped.
|
|
181
|
+
*/
|
|
182
|
+
push(pageOrTag: AdwNavigationPage | string): void {
|
|
183
|
+
const page = this._resolvePage(pageOrTag);
|
|
184
|
+
if (!page) {
|
|
185
|
+
console.warn(`[adw-navigation-view] No page found for "${String(pageOrTag)}"`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (this._stack.includes(page)) {
|
|
189
|
+
console.warn('[adw-navigation-view] Page is already in the navigation stack');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!this._pages.includes(page)) {
|
|
193
|
+
// A brand-new dynamic page — keep a reference + mark for cleanup.
|
|
194
|
+
page.classList.add('adw-navigation-page');
|
|
195
|
+
this._pagesEl.appendChild(page);
|
|
196
|
+
this._pages.push(page);
|
|
197
|
+
this._dynamic.add(page);
|
|
198
|
+
}
|
|
199
|
+
this._stack.push(page);
|
|
200
|
+
this._render();
|
|
201
|
+
this.dispatchEvent(new CustomEvent('pushed', { bubbles: true, detail: { tag: page.tag } }));
|
|
202
|
+
this.dispatchEvent(new CustomEvent('notify::visible-page', { bubbles: true, detail: { tag: page.tag } }));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pop the visible page off the stack (mirrors AdwNavigationView.pop).
|
|
207
|
+
* Returns true when a page was popped. A dynamically-pushed page is removed
|
|
208
|
+
* from the DOM. The root page cannot be popped.
|
|
209
|
+
*/
|
|
210
|
+
pop(): boolean {
|
|
211
|
+
if (this._stack.length <= 1) return false;
|
|
212
|
+
const visible = this.visiblePage;
|
|
213
|
+
if (visible && !visible.canPop) return false;
|
|
214
|
+
|
|
215
|
+
const popped = this._stack.pop() as AdwNavigationPage;
|
|
216
|
+
// Destroy dynamically-pushed pages once they leave the stack.
|
|
217
|
+
if (this._dynamic.has(popped)) {
|
|
218
|
+
this._dynamic.delete(popped);
|
|
219
|
+
const index = this._pages.indexOf(popped);
|
|
220
|
+
if (index !== -1) this._pages.splice(index, 1);
|
|
221
|
+
if (popped.parentNode === this._pagesEl) this._pagesEl.removeChild(popped);
|
|
222
|
+
}
|
|
223
|
+
this._render();
|
|
224
|
+
this.dispatchEvent(new CustomEvent('popped', { bubbles: true, detail: { tag: popped.tag } }));
|
|
225
|
+
const next = this.visiblePage;
|
|
226
|
+
this.dispatchEvent(new CustomEvent('notify::visible-page', { bubbles: true, detail: { tag: next?.tag ?? null } }));
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Pop every page down to (and including pushing) the given tag's page. */
|
|
231
|
+
popToTag(tag: string): boolean {
|
|
232
|
+
const target = this._pages.find((p) => p.tag === tag);
|
|
233
|
+
if (!target) return false;
|
|
234
|
+
const index = this._stack.indexOf(target);
|
|
235
|
+
if (index === -1 || index === this._stack.length - 1) return false;
|
|
236
|
+
let popped = false;
|
|
237
|
+
while (this._stack.length - 1 > index) {
|
|
238
|
+
if (!this.pop()) break;
|
|
239
|
+
popped = true;
|
|
240
|
+
}
|
|
241
|
+
return popped;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Replace the whole navigation stack with the given pages (mirrors
|
|
246
|
+
* AdwNavigationView.replace). The last page becomes visible.
|
|
247
|
+
*/
|
|
248
|
+
replace(pages: Array<AdwNavigationPage | string>): void {
|
|
249
|
+
// Clear the current stack first (without firing popped events — replace
|
|
250
|
+
// is its own signal in libadwaita).
|
|
251
|
+
for (const page of this._stack) {
|
|
252
|
+
if (this._dynamic.has(page)) {
|
|
253
|
+
this._dynamic.delete(page);
|
|
254
|
+
const index = this._pages.indexOf(page);
|
|
255
|
+
if (index !== -1) this._pages.splice(index, 1);
|
|
256
|
+
if (page.parentNode === this._pagesEl) this._pagesEl.removeChild(page);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
this._stack = [];
|
|
260
|
+
|
|
261
|
+
for (const entry of pages) {
|
|
262
|
+
const page = this._resolvePage(entry);
|
|
263
|
+
if (!page) continue;
|
|
264
|
+
if (!this._pages.includes(page)) {
|
|
265
|
+
page.classList.add('adw-navigation-page');
|
|
266
|
+
this._pagesEl.appendChild(page);
|
|
267
|
+
this._pages.push(page);
|
|
268
|
+
this._dynamic.add(page);
|
|
269
|
+
}
|
|
270
|
+
this._stack.push(page);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this._render();
|
|
274
|
+
this.dispatchEvent(new CustomEvent('replaced', { bubbles: true }));
|
|
275
|
+
this.dispatchEvent(
|
|
276
|
+
new CustomEvent('notify::visible-page', { bubbles: true, detail: { tag: this.visiblePageTag } }),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private _resolvePage(pageOrTag: AdwNavigationPage | string): AdwNavigationPage | null {
|
|
281
|
+
if (typeof pageOrTag === 'string') {
|
|
282
|
+
return this._pages.find((p) => p.tag === pageOrTag) ?? null;
|
|
283
|
+
}
|
|
284
|
+
return pageOrTag;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private _syncClasses(): void {
|
|
288
|
+
this.classList.toggle('animated', this.animateTransitions);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private _render(): void {
|
|
292
|
+
const visible = this.visiblePage;
|
|
293
|
+
|
|
294
|
+
for (const page of this._pages) {
|
|
295
|
+
const isVisible = page === visible;
|
|
296
|
+
page.classList.toggle('visible-page', isVisible);
|
|
297
|
+
page.hidden = !isVisible;
|
|
298
|
+
// The back button only belongs on the visible page; clean up the
|
|
299
|
+
// others so a stale button never lingers when the stack shrinks.
|
|
300
|
+
if (!isVisible) this._removeBackButton(page);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (visible) this._syncBackButton(visible);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Inject (or remove) the automatic back button on the visible page. */
|
|
307
|
+
private _syncBackButton(page: AdwNavigationPage): void {
|
|
308
|
+
// Pop is possible only when there is a page below this one on the stack.
|
|
309
|
+
const poppable = this._stack.indexOf(page) > 0 && page.canPop && !page.hasAttribute('no-back-button');
|
|
310
|
+
if (!poppable) {
|
|
311
|
+
this._removeBackButton(page);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (this._backButtons.has(page)) return;
|
|
315
|
+
|
|
316
|
+
// Find the page's header bar's start section. We surface the button into
|
|
317
|
+
// the first <adw-header-bar> inside the page content, the way
|
|
318
|
+
// AdwHeaderBar grows a back button when placed inside an AdwNavigationView.
|
|
319
|
+
const headerBar = page.querySelector('adw-header-bar') as HTMLElement | null;
|
|
320
|
+
if (!headerBar) return;
|
|
321
|
+
const start = (headerBar as { startSection?: HTMLElement | null }).startSection ?? null;
|
|
322
|
+
if (!start) return;
|
|
323
|
+
|
|
324
|
+
const back = document.createElement('adw-button');
|
|
325
|
+
back.classList.add('adw-navigation-back-button');
|
|
326
|
+
back.setAttribute('icon', 'go-previous');
|
|
327
|
+
back.setAttribute('flat', '');
|
|
328
|
+
back.setAttribute('tooltip', 'Back');
|
|
329
|
+
back.addEventListener('click', () => this.pop());
|
|
330
|
+
// Place the back button at the very start of the header.
|
|
331
|
+
start.insertBefore(back, start.firstChild);
|
|
332
|
+
this._backButtons.set(page, back);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private _removeBackButton(page: AdwNavigationPage): void {
|
|
336
|
+
const back = this._backButtons.get(page);
|
|
337
|
+
if (!back) return;
|
|
338
|
+
if (back.parentNode) back.parentNode.removeChild(back);
|
|
339
|
+
this._backButtons.delete(page);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
customElements.define('adw-navigation-page', AdwNavigationPage);
|
|
344
|
+
customElements.define('adw-navigation-view', AdwNavigationView);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// <adw-password-entry-row> — A boxed-list entry row tailored for secrets: the
|
|
2
|
+
// title floats as a label, the editable text is masked, and a trailing peek
|
|
3
|
+
// button toggles between hiding and revealing the contents. A specialization of
|
|
4
|
+
// <adw-entry-row> (Adw.PasswordEntryRow extends Adw.EntryRow).
|
|
5
|
+
// Attributes: title, text, revealed (boolean — show the contents in clear text).
|
|
6
|
+
// Property: text (get/set, proxies the inner input); revealed (get/set).
|
|
7
|
+
// Events: `changed` (CustomEvent) on every edit — mirrors Adw.EntryRow's
|
|
8
|
+
// `changed` signal so it drives the same code path as the GTK renderer.
|
|
9
|
+
// `notify::revealed` (CustomEvent) when the peek toggle flips visibility.
|
|
10
|
+
// Reference: refs/adwaita-web/adwaita-web/docs/widgets/passwordentryrow.md
|
|
11
|
+
// Reference: refs/libadwaita/src/adw-password-entry-row.c (peek toggle: the
|
|
12
|
+
// view-reveal / view-conceal symbolic swap; CSS node `row` carries `.entry`
|
|
13
|
+
// and `.password` style classes).
|
|
14
|
+
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
15
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
16
|
+
|
|
17
|
+
export class AdwPasswordEntryRow extends HTMLElement {
|
|
18
|
+
private _input!: HTMLInputElement;
|
|
19
|
+
private _titleEl!: HTMLSpanElement;
|
|
20
|
+
private _toggle!: HTMLButtonElement;
|
|
21
|
+
private _toggleIcon!: HTMLSpanElement;
|
|
22
|
+
private _initialized = false;
|
|
23
|
+
|
|
24
|
+
static get observedAttributes() {
|
|
25
|
+
return ['title', 'text', 'revealed'];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get text(): string {
|
|
29
|
+
return this._input ? this._input.value : (this.getAttribute('text') ?? '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set text(value: string) {
|
|
33
|
+
if (this._input) this._input.value = value;
|
|
34
|
+
else this.setAttribute('text', value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether the contents are shown in clear text (peek toggle is engaged). */
|
|
38
|
+
get revealed(): boolean {
|
|
39
|
+
return this.hasAttribute('revealed');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
set revealed(value: boolean) {
|
|
43
|
+
if (value) this.setAttribute('revealed', '');
|
|
44
|
+
else this.removeAttribute('revealed');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
connectedCallback() {
|
|
48
|
+
if (this._initialized) return;
|
|
49
|
+
this._initialized = true;
|
|
50
|
+
|
|
51
|
+
this._titleEl = document.createElement('span');
|
|
52
|
+
this._titleEl.className = 'adw-row-title';
|
|
53
|
+
this._titleEl.textContent = this.getAttribute('title') ?? '';
|
|
54
|
+
|
|
55
|
+
this._input = document.createElement('input');
|
|
56
|
+
this._input.className = 'adw-password-entry-row-input';
|
|
57
|
+
// Masked by default — Adw.PasswordEntryRow sets GTK_INPUT_PURPOSE_PASSWORD
|
|
58
|
+
// and starts with visibility off.
|
|
59
|
+
this._input.type = this.hasAttribute('revealed') ? 'text' : 'password';
|
|
60
|
+
this._input.value = this.getAttribute('text') ?? '';
|
|
61
|
+
this._input.autocomplete = 'off';
|
|
62
|
+
this._input.addEventListener('input', () => {
|
|
63
|
+
this.dispatchEvent(new CustomEvent('changed', { bubbles: true, detail: { text: this._input.value } }));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Title floats as a small label above the masked value, both left-aligned
|
|
67
|
+
// in a column (the filled Adw.EntryRow look).
|
|
68
|
+
const text = document.createElement('div');
|
|
69
|
+
text.className = 'adw-password-entry-row-text';
|
|
70
|
+
text.append(this._titleEl, this._input);
|
|
71
|
+
|
|
72
|
+
// Trailing peek toggle — a flat icon button, vertically centred. Swaps
|
|
73
|
+
// between the reveal / conceal symbolics like the native widget.
|
|
74
|
+
this._toggle = document.createElement('button');
|
|
75
|
+
this._toggle.className = 'adw-button flat circular icon-only adw-password-entry-row-toggle';
|
|
76
|
+
this._toggle.type = 'button';
|
|
77
|
+
this._toggleIcon = document.createElement('span');
|
|
78
|
+
this._toggleIcon.setAttribute('aria-hidden', 'true');
|
|
79
|
+
this._toggle.appendChild(this._toggleIcon);
|
|
80
|
+
this._toggle.addEventListener('click', () => {
|
|
81
|
+
this.revealed = !this.revealed;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Trailing edit pencil (before the peek toggle), mirroring native.
|
|
85
|
+
const edit = document.createElement('span');
|
|
86
|
+
edit.className = 'adw-row-edit adw-icon adw-icon--document-edit';
|
|
87
|
+
edit.setAttribute('aria-hidden', 'true');
|
|
88
|
+
edit.addEventListener('click', () => this._input.focus());
|
|
89
|
+
|
|
90
|
+
this.replaceChildren(text, edit, this._toggle);
|
|
91
|
+
this._renderRevealed();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
95
|
+
if (!this._initialized) return;
|
|
96
|
+
if (name === 'title') {
|
|
97
|
+
this._titleEl.textContent = value ?? '';
|
|
98
|
+
} else if (name === 'text') {
|
|
99
|
+
if (this._input.value !== (value ?? '')) this._input.value = value ?? '';
|
|
100
|
+
} else if (name === 'revealed') {
|
|
101
|
+
this._renderRevealed();
|
|
102
|
+
this.dispatchEvent(
|
|
103
|
+
new CustomEvent('notify::revealed', { bubbles: true, detail: { revealed: this.revealed } }),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private _renderRevealed() {
|
|
109
|
+
const revealed = this.revealed;
|
|
110
|
+
this._input.type = revealed ? 'text' : 'password';
|
|
111
|
+
// view-conceal when revealed (offer to hide), view-reveal when masked.
|
|
112
|
+
const icon = revealed ? 'view-conceal' : 'view-reveal';
|
|
113
|
+
this._toggleIcon.className = `adw-icon adw-icon--${icon}`;
|
|
114
|
+
const labelText = revealed ? 'Hide Password' : 'Show Password';
|
|
115
|
+
this._toggle.title = labelText;
|
|
116
|
+
this._toggle.setAttribute('aria-label', labelText);
|
|
117
|
+
this._toggle.setAttribute('aria-pressed', String(revealed));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
customElements.define('adw-password-entry-row', AdwPasswordEntryRow);
|