@gjsify/adwaita-web 0.64.12 → 0.64.13
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/dist/adwaita-web.css +31 -0
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/elements/adw-bin.d.ts +13 -0
- package/lib/types/elements/adw-expander-row.d.ts +3 -0
- package/lib/types/elements/gtk-label.d.ts +64 -0
- package/lib/types/label-markup.d.ts +30 -0
- package/lib/types/pango-markup.d.ts +17 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/widget-element.d.ts +8 -2
- package/package.json +11 -11
- package/scss/_labels.scss +19 -0
- package/scss/_link.scss +73 -7
- package/src/adw-action-rows.spec.ts +19 -0
- package/src/adw-bin.spec.ts +20 -0
- package/src/adw-row-state.spec.ts +80 -0
- package/src/elements/adw-bin.ts +20 -1
- package/src/elements/adw-expander-row.ts +46 -16
- package/src/elements/gtk-label.ts +206 -16
- package/src/gtk-label.spec.ts +431 -11
- package/src/gtk-switch.spec.ts +8 -0
- package/src/label-markup.ts +193 -0
- package/src/pango-markup.spec.ts +11 -0
- package/src/pango-markup.ts +14 -5
- package/src/split-button.spec.ts +7 -4
- package/src/styles.generated.ts +1 -1
- package/src/widget-element.ts +28 -7
- package/src/widget-members.spec.ts +42 -3
|
@@ -8,11 +8,21 @@
|
|
|
8
8
|
// mounted through `mountSharedTree` arrives as a TAG with a `label` attribute, and without
|
|
9
9
|
// an element that reads it the text never reached the screen.
|
|
10
10
|
//
|
|
11
|
-
// THE TEXT IS WRITTEN AS
|
|
12
|
-
//
|
|
13
|
-
// label shares),
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// THE TEXT IS WRITTEN AS NODES, NEVER AS MARKUP. `use-markup` is DRAWN: the string is
|
|
12
|
+
// parsed headlessly (`labelMarkupRender` in `@gjsify/adwaita-core`, the derivation the
|
|
13
|
+
// NativeScript label shares), and its styled runs become elements built with
|
|
14
|
+
// `createElement` — `../label-markup.js`. Nothing is ever handed to `innerHTML`, which
|
|
15
|
+
// would run whatever the string carries; markup that does not PARSE falls back to the raw
|
|
16
|
+
// string as text, which is the C's own fallback. `use-underline` strips the mnemonic
|
|
17
|
+
// marker and binds no key.
|
|
18
|
+
//
|
|
19
|
+
// `<a href>` IS A LINK HERE, with `::activate-link` behind it. The anchors carry GTK's
|
|
20
|
+
// `link` CSS class and their markup's `title` as a tooltip, the widget gains the `link`
|
|
21
|
+
// class and `has-tooltip` the way `gtk_label_set_markup` and
|
|
22
|
+
// `gtk_label_ensure_has_tooltip` set them, and a click emits the cancelable
|
|
23
|
+
// `activate-link` before anything is opened — `<gtk-link-button>`'s pattern, because the
|
|
24
|
+
// two must not disagree about what following a link does. `visited` is carried across a
|
|
25
|
+
// re-render by URI, as the C does.
|
|
16
26
|
//
|
|
17
27
|
// `xalign` IS WHERE THE TEXT SITS IN THE LABEL'S BOX, `justify` how its LINES align with
|
|
18
28
|
// each other — two properties, as in GTK. `xalign` is a continuum and CSS alignment has
|
|
@@ -59,9 +69,14 @@
|
|
|
59
69
|
// Copyright (c) The GTK Team, GNOME contributors. LGPLv2.1+.
|
|
60
70
|
|
|
61
71
|
import {
|
|
72
|
+
labelActivateLink,
|
|
62
73
|
labelDisplayText,
|
|
63
74
|
labelEffectiveLines,
|
|
64
75
|
labelEllipsizeOverflowValue,
|
|
76
|
+
labelHasLinks,
|
|
77
|
+
labelLinksHaveTooltip,
|
|
78
|
+
labelLinkVisited,
|
|
79
|
+
labelMarkupRender,
|
|
65
80
|
labelWidthCharsExtent,
|
|
66
81
|
labelYalignAlignItems,
|
|
67
82
|
normalizeLabelEllipsize,
|
|
@@ -74,10 +89,14 @@ import {
|
|
|
74
89
|
normalizeLabelYalign,
|
|
75
90
|
type LabelEllipsizeMode,
|
|
76
91
|
type LabelJustification,
|
|
92
|
+
type LabelLink,
|
|
93
|
+
type LabelMarkupRender,
|
|
77
94
|
type LabelWrapMode,
|
|
78
95
|
} from '@gjsify/adwaita-core';
|
|
79
96
|
|
|
80
97
|
import { JUSTIFY_TEXT_ALIGN } from './justification.js';
|
|
98
|
+
import { LABEL_LINK_ATTRIBUTE, labelMarkupAnchorIndex, labelMarkupAnchors, labelMarkupNodes } from '../label-markup.js';
|
|
99
|
+
import type { DispatchedSignals } from '../signals.js';
|
|
81
100
|
import { WidgetElement } from '../widget-element.js';
|
|
82
101
|
|
|
83
102
|
/** The attributes that carry a property — also the `notify::` roster. */
|
|
@@ -108,12 +127,51 @@ export class GtkLabel extends WidgetElement {
|
|
|
108
127
|
* gets one text node").
|
|
109
128
|
*/
|
|
110
129
|
private _wrapSpan: HTMLSpanElement | null = null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The box the markup's runs are drawn in while the label is a SINGLE LINE — the case
|
|
133
|
+
* {@link _wrapSpan} does not cover, and the runs still may not go without a box.
|
|
134
|
+
*
|
|
135
|
+
* THE RUNS MAY NOT BE THE LABEL'S OWN CHILDREN. `gtk-label` is a flex container (the
|
|
136
|
+
* `xalign` spacers, `_labels.scss`), so each node directly under it is a FLEX ITEM —
|
|
137
|
+
* and a flex item is not text: `vertical-align` does not apply to one, so `<sup>` and
|
|
138
|
+
* `<sub>` stop being raised and lowered, and `align-items` centres each run on its own
|
|
139
|
+
* rather than the line sharing one baseline. A plain label never showed this because a
|
|
140
|
+
* bare text node is ONE anonymous item; a run list is as many items as it has runs. So
|
|
141
|
+
* the runs get a box and the box is the item. `null` while plain text is drawn, so a
|
|
142
|
+
* label without markup keeps the single text node `childElementCount` asserts on.
|
|
143
|
+
*/
|
|
144
|
+
private _runSpan: HTMLSpanElement | null = null;
|
|
111
145
|
private _resizes: ResizeObserver | null = null;
|
|
112
146
|
|
|
113
147
|
/** Whether `_sync` last wrote `min-width` / `max-width` itself, and so may clear them. */
|
|
114
148
|
private _wroteMinWidth = false;
|
|
115
149
|
private _wroteMaxWidth = false;
|
|
116
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The `<a href>` elements the label currently draws, in reading order.
|
|
153
|
+
*
|
|
154
|
+
* HELD ACROSS RENDERS because `visited` is: `gtk_label_set_markup` carries it over by
|
|
155
|
+
* URI (`labelLinksInheritVisited`), so a followed link stays followed when the text
|
|
156
|
+
* around it is rewritten. Empty whenever the markup has no links or does not parse.
|
|
157
|
+
*/
|
|
158
|
+
private _links: readonly LabelLink[] = [];
|
|
159
|
+
private _wired = false;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The inputs of the run list currently in the DOM, or `null` when plain text is.
|
|
163
|
+
*
|
|
164
|
+
* `_render` runs on EVERY observed attribute — `xalign` included — and rebuilding the
|
|
165
|
+
* run elements each time would drop a selection and the focus inside a link for a
|
|
166
|
+
* change that never touched the text. The plain-text path compares `textContent`; a
|
|
167
|
+
* list of elements has no such single string to compare against, so the two inputs
|
|
168
|
+
* that produced it stand in for it.
|
|
169
|
+
*/
|
|
170
|
+
private _drawn: string | null = null;
|
|
171
|
+
|
|
172
|
+
/** The GTK signals this element dispatches, each with the DOM event it arrives as (ADR 0093). */
|
|
173
|
+
static readonly signals: DispatchedSignals = { 'activate-link': 'activate-link' };
|
|
174
|
+
|
|
117
175
|
static get observedAttributes() {
|
|
118
176
|
return [...PROPERTY_ATTRIBUTES];
|
|
119
177
|
}
|
|
@@ -279,6 +337,7 @@ export class GtkLabel extends WidgetElement {
|
|
|
279
337
|
}
|
|
280
338
|
|
|
281
339
|
connectedCallback() {
|
|
340
|
+
this._wire();
|
|
282
341
|
this._render();
|
|
283
342
|
this._resizes = new ResizeObserver(() => this._remeasure());
|
|
284
343
|
this._resizes.observe(this);
|
|
@@ -318,15 +377,28 @@ export class GtkLabel extends WidgetElement {
|
|
|
318
377
|
}
|
|
319
378
|
|
|
320
379
|
private _render(): void {
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
380
|
+
// THE MARKUP IS DRAWN WHEN IT PARSES, and `null` here is both cases where it is
|
|
381
|
+
// not: `use-markup` unset, and a string pango itself rejects — for which GTK
|
|
382
|
+
// shows the raw text (`labelDisplayText`, the C fallback). Either way no string
|
|
383
|
+
// becomes markup the browser parses: the run path builds every node with
|
|
384
|
+
// `createElement` (`../label-markup.js`) and the text path is `textContent`.
|
|
385
|
+
const render = this.useMarkup ? labelMarkupRender(this.label, this._links) : null;
|
|
386
|
+
this._links = render?.links ?? [];
|
|
387
|
+
const drawn = render === null ? null : `${this.useUnderline ? '1' : '0'}\u0000${this.label}`;
|
|
388
|
+
|
|
324
389
|
const text = this.getText();
|
|
325
390
|
const wrap = this.wrap;
|
|
326
391
|
const ellipsize = this.ellipsize;
|
|
327
392
|
const clamped = ellipsize !== 'none';
|
|
328
|
-
if (wrap || clamped) this._renderSpan(text, clamped);
|
|
329
|
-
else this._renderSingleLine(text);
|
|
393
|
+
if (wrap || clamped) this._renderSpan(text, render, drawn, clamped);
|
|
394
|
+
else this._renderSingleLine(text, render, drawn);
|
|
395
|
+
|
|
396
|
+
// `gtk_label_set_markup`'s two side effects on the WIDGET (gtklabel.c:3896,
|
|
397
|
+
// gtk_label_ensure_has_tooltip): the class libadwaita's stylesheet selects on, and
|
|
398
|
+
// the flag that says a tooltip may be asked for. The tooltip TEXT is per link and
|
|
399
|
+
// sits on the anchor itself, which is where a browser reads one from.
|
|
400
|
+
this.classList.toggle('link', labelHasLinks(this._links));
|
|
401
|
+
this.toggleAttribute('has-tooltip', labelLinksHaveTooltip(this._links));
|
|
330
402
|
|
|
331
403
|
this.style.setProperty('--gtk-label-xalign', String(this.xalign));
|
|
332
404
|
this.style.textAlign = JUSTIFY_TEXT_ALIGN[this.justify];
|
|
@@ -373,10 +445,118 @@ export class GtkLabel extends WidgetElement {
|
|
|
373
445
|
this._wroteMaxWidth = extent.maxCh !== null;
|
|
374
446
|
}
|
|
375
447
|
|
|
376
|
-
private _renderSingleLine(text: string): void {
|
|
377
|
-
this._wrapSpan
|
|
378
|
-
|
|
379
|
-
|
|
448
|
+
private _renderSingleLine(text: string, render: LabelMarkupRender | null, drawn: string | null): void {
|
|
449
|
+
if (this._wrapSpan) {
|
|
450
|
+
this._wrapSpan.remove();
|
|
451
|
+
this._wrapSpan = null;
|
|
452
|
+
// The box the runs were in is gone, so what `_drawn` describes is gone with it.
|
|
453
|
+
this._drawn = null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// PLAIN TEXT GOES STRAIGHT IN, runs may not — see {@link _runSpan}. The text node
|
|
457
|
+
// is one anonymous flex item whichever way the label is aligned; a run list under
|
|
458
|
+
// the same parent would be one flex item PER RUN.
|
|
459
|
+
if (render === null) {
|
|
460
|
+
if (this._runSpan) {
|
|
461
|
+
this._runSpan.remove();
|
|
462
|
+
this._runSpan = null;
|
|
463
|
+
this._drawn = null;
|
|
464
|
+
}
|
|
465
|
+
this._draw(this, text, render, drawn);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!this._runSpan) {
|
|
470
|
+
this.textContent = '';
|
|
471
|
+
this._runSpan = document.createElement('span');
|
|
472
|
+
this._runSpan.className = 'adw-label-runs';
|
|
473
|
+
this.appendChild(this._runSpan);
|
|
474
|
+
// A NEW, EMPTY box: whatever `_drawn` described was in the old one.
|
|
475
|
+
this._drawn = null;
|
|
476
|
+
}
|
|
477
|
+
this._draw(this._runSpan, text, render, drawn);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* `render`'s runs — or `text` when there are none — into `target`.
|
|
482
|
+
*
|
|
483
|
+
* A PLAIN LABEL KEEPS ITS SINGLE TEXT NODE: `textContent` is still the whole of the
|
|
484
|
+
* write whenever there is no markup to draw, so the shape `gtk-label.spec.ts` asserts
|
|
485
|
+
* for the XSS answer (no elements at all) is unchanged for every label that does not
|
|
486
|
+
* ask for markup.
|
|
487
|
+
*/
|
|
488
|
+
private _draw(target: HTMLElement, text: string, render: LabelMarkupRender | null, drawn: string | null): void {
|
|
489
|
+
if (render === null) {
|
|
490
|
+
this._drawn = null;
|
|
491
|
+
if (target.textContent !== text) target.textContent = text;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (this._drawn === drawn) return;
|
|
495
|
+
this._drawn = drawn;
|
|
496
|
+
target.replaceChildren(...labelMarkupNodes(render, target.ownerDocument, this.useUnderline));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* ONE delegated `click` listener, added once and never removed: the anchors are rebuilt
|
|
501
|
+
* on every render, so a listener per anchor would have to be re-added with them, and
|
|
502
|
+
* the element is the node they are always under.
|
|
503
|
+
*/
|
|
504
|
+
private _wire(): void {
|
|
505
|
+
if (this._wired) return;
|
|
506
|
+
this._wired = true;
|
|
507
|
+
this.addEventListener('click', (event) => this._click(event));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private _click(event: MouseEvent): void {
|
|
511
|
+
const target = event.target;
|
|
512
|
+
const anchor = target instanceof Element ? target.closest(`a[${LABEL_LINK_ATTRIBUTE}]`) : null;
|
|
513
|
+
if (anchor === null) return;
|
|
514
|
+
const index = labelMarkupAnchorIndex(anchor);
|
|
515
|
+
const link = index === null ? undefined : this._links[index];
|
|
516
|
+
if (index === null || link === undefined) return;
|
|
517
|
+
|
|
518
|
+
// THE DEFAULT IS CANCELLED, every time. The anchor carries a real `href` — it has
|
|
519
|
+
// to, or it is not a link to the keyboard, to the accessibility tree or to "copy
|
|
520
|
+
// link address" — and a browser would therefore NAVIGATE this window. GTK does
|
|
521
|
+
// not: `gtk_label_activate_link` hands the URI to the desktop and leaves the
|
|
522
|
+
// window standing. So the signal runs in the navigation's place.
|
|
523
|
+
event.preventDefault();
|
|
524
|
+
const visited = labelActivateLink(link.href, {
|
|
525
|
+
// `GTK_IS_WINDOW (gtk_widget_get_root (widget))` — a label with no window
|
|
526
|
+
// above it launches nothing. A document with no `defaultView` (one built by
|
|
527
|
+
// `DOMParser`, a template's content) is this port's version of that: there is
|
|
528
|
+
// no window to open anything from.
|
|
529
|
+
rooted: this.isConnected && this.ownerDocument.defaultView !== null,
|
|
530
|
+
emit: () =>
|
|
531
|
+
!this.dispatchEvent(
|
|
532
|
+
new CustomEvent('activate-link', { bubbles: true, cancelable: true, detail: { uri: link.href } }),
|
|
533
|
+
),
|
|
534
|
+
launch: (uri) => this.launch(uri),
|
|
535
|
+
});
|
|
536
|
+
if (!visited) return;
|
|
537
|
+
this._links = labelLinkVisited(this._links, index);
|
|
538
|
+
this._paintVisited();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The launcher call, on its own method so a caller (or a spec) can stand in for the
|
|
543
|
+
* window the desktop would have opened — `<gtk-link-button>`'s `launch`, and the same
|
|
544
|
+
* door, because the two must not disagree about what following a link does.
|
|
545
|
+
*/
|
|
546
|
+
protected launch(uri: string): void {
|
|
547
|
+
window.open(uri, '_blank', 'noopener');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* `visited` onto the anchors already in the DOM, rather than through a re-render: the
|
|
552
|
+
* link that was just clicked holds the focus, and rebuilding the runs under it would
|
|
553
|
+
* take that away for a change of one class.
|
|
554
|
+
*/
|
|
555
|
+
private _paintVisited(): void {
|
|
556
|
+
for (const anchor of labelMarkupAnchors(this)) {
|
|
557
|
+
const index = labelMarkupAnchorIndex(anchor);
|
|
558
|
+
if (index !== null) anchor.classList.toggle('visited', this._links[index]?.visited === true);
|
|
559
|
+
}
|
|
380
560
|
}
|
|
381
561
|
|
|
382
562
|
/**
|
|
@@ -415,15 +595,25 @@ export class GtkLabel extends WidgetElement {
|
|
|
415
595
|
* min-content/max-content gap, the exact shrink-to-fit collapse the wrapped case above
|
|
416
596
|
* was already written for. One code path now, for both.
|
|
417
597
|
*/
|
|
418
|
-
private _renderSpan(text: string, clamped: boolean): void {
|
|
598
|
+
private _renderSpan(text: string, render: LabelMarkupRender | null, drawn: string | null, clamped: boolean): void {
|
|
599
|
+
// THIS BOX SUPERSEDES THE RUN BOX. `.adw-label-text` is `display: block`, so the
|
|
600
|
+
// runs inside it are inline boxes and the flex-item problem {@link _runSpan}
|
|
601
|
+
// answers cannot arise — one box, not two nested ones.
|
|
602
|
+
if (this._runSpan) {
|
|
603
|
+
this._runSpan.remove();
|
|
604
|
+
this._runSpan = null;
|
|
605
|
+
this._drawn = null;
|
|
606
|
+
}
|
|
419
607
|
if (!this._wrapSpan) {
|
|
420
608
|
this.textContent = '';
|
|
421
609
|
this._wrapSpan = document.createElement('span');
|
|
422
610
|
this._wrapSpan.className = 'adw-label-text';
|
|
423
611
|
this.appendChild(this._wrapSpan);
|
|
612
|
+
// A NEW, EMPTY box: whatever `_drawn` described was in the old one.
|
|
613
|
+
this._drawn = null;
|
|
424
614
|
}
|
|
425
615
|
this._wrapSpan.classList.toggle('adw-label-clamp', clamped);
|
|
426
|
-
|
|
616
|
+
this._draw(this._wrapSpan, text, render, drawn);
|
|
427
617
|
this._remeasure();
|
|
428
618
|
}
|
|
429
619
|
}
|