@raylang/elements 0.8.0 → 0.9.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/components/code/code.js +8 -5
- package/components/command-palette/command-palette.js +16 -1
- package/components/doc-tabs/doc-tabs.js +255 -0
- package/components/icon-button/icon-button.js +4 -0
- package/components/input/input.js +13 -1
- package/components/menu/menu.js +42 -7
- package/components/mode-input/mode-input.js +221 -0
- package/components/sidebar/sidebar.js +140 -5
- package/components/split/split.js +13 -1
- package/components/tabs/tabs.js +44 -6
- package/components/textarea/textarea.js +13 -1
- package/core/complete.js +252 -0
- package/core/scroll.js +85 -1
- package/core/textlayer.js +214 -0
- package/custom-elements.json +1414 -88
- package/index.js +2 -0
- package/llms-full.txt +184 -12
- package/llms.txt +7 -3
- package/package.json +9 -1
- package/ray-elements.d.ts +158 -6
- package/vscode.html-custom-data.json +397 -4
package/components/code/code.js
CHANGED
|
@@ -400,6 +400,7 @@ export class RayCode extends RayElement {
|
|
|
400
400
|
lineNumbers: { type: Boolean, reflect: true },
|
|
401
401
|
highlight: { type: String },
|
|
402
402
|
wrap: { type: Boolean, reflect: true },
|
|
403
|
+
verbatim: { type: Boolean, reflect: true },
|
|
403
404
|
maxHeight: { type: String },
|
|
404
405
|
expanded: { type: Boolean, reflect: true },
|
|
405
406
|
hideCopy: { type: Boolean, reflect: true },
|
|
@@ -450,20 +451,22 @@ export class RayCode extends RayElement {
|
|
|
450
451
|
this.#observer.disconnect();
|
|
451
452
|
}
|
|
452
453
|
|
|
453
|
-
/** The code shown (dedented), whatever its source. */
|
|
454
|
+
/** The code shown (dedented unless verbatim), whatever its source. */
|
|
454
455
|
get source() {
|
|
455
456
|
return this.#source;
|
|
456
457
|
}
|
|
457
458
|
|
|
458
459
|
#readSource() {
|
|
459
|
-
|
|
460
|
+
// verbatim: data (a response body, a log) is shown and copied as it came.
|
|
461
|
+
const shape = this.verbatim ? String : dedent;
|
|
462
|
+
if (typeof this.code === "string") return shape(this.code);
|
|
460
463
|
// A <template> is parsed as HTML: perfect for markup; its serialization
|
|
461
464
|
// escapes &, < and > in text, undone here.
|
|
462
465
|
const tpl = this.querySelector(":scope > template");
|
|
463
|
-
if (tpl) return
|
|
466
|
+
if (tpl) return shape(tpl.innerHTML.replace(/</g, "<").replace(/>/g, ">").replace(/ /g, "\u00a0").replace(/&/g, "&"));
|
|
464
467
|
const script = this.querySelector(':scope > script[type="text/plain"]');
|
|
465
|
-
if (script) return
|
|
466
|
-
return
|
|
468
|
+
if (script) return shape(script.textContent);
|
|
469
|
+
return shape(this.textContent);
|
|
467
470
|
}
|
|
468
471
|
|
|
469
472
|
update() {
|
|
@@ -86,6 +86,7 @@ const styles = css`
|
|
|
86
86
|
.icon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 1.25rem; height: 1.25rem; color: var(--ray-muted-foreground); }
|
|
87
87
|
.option[aria-selected="true"] .icon { color: inherit; }
|
|
88
88
|
.icon svg { width: 1rem; height: 1rem; }
|
|
89
|
+
.prefix { flex: none; display: inline-flex; align-items: center; font-size: var(--ray-font-size-xs); color: var(--ray-muted-foreground); }
|
|
89
90
|
.text { display: flex; flex: 1; flex-direction: column; min-width: 0; }
|
|
90
91
|
.label, .description { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
91
92
|
.label { font-size: var(--ray-font-size-sm); }
|
|
@@ -187,6 +188,9 @@ export class RayCommandPalette extends RayElement {
|
|
|
187
188
|
emptyLabel: { type: String, default: "Nothing found" },
|
|
188
189
|
label: { type: String, default: "Command palette" },
|
|
189
190
|
hideHint: { type: Boolean, reflect: true },
|
|
191
|
+
navigateLabel: { type: String, default: "navigate" },
|
|
192
|
+
runLabel: { type: String, default: "run" },
|
|
193
|
+
closeLabel: { type: String, default: "close" },
|
|
190
194
|
overlayBlur: { type: String },
|
|
191
195
|
nativeMenus: { type: Boolean, attribute: "native-menus", reflect: true },
|
|
192
196
|
};
|
|
@@ -196,7 +200,7 @@ export class RayCommandPalette extends RayElement {
|
|
|
196
200
|
<div class="search" part="search">${SEARCH_ICON}<input id="input" part="input" role="combobox" aria-expanded="true" aria-controls="list" aria-autocomplete="list" autocomplete="off" spellcheck="false"></div>
|
|
197
201
|
<div class="list" id="list" role="listbox" part="list"></div>
|
|
198
202
|
<div class="empty" part="empty" hidden></div>
|
|
199
|
-
<div class="hint" part="hint"><span><b>↑↓</b> navigate
|
|
203
|
+
<div class="hint" part="hint"><span><b>↑↓</b> <span id="hint-navigate"></span></span><span><b>↵</b> <span id="hint-run"></span></span><span><b>esc</b> <span id="hint-close"></span></span></div>
|
|
200
204
|
<slot name="overlay"></slot>
|
|
201
205
|
</dialog>`;
|
|
202
206
|
|
|
@@ -281,6 +285,9 @@ export class RayCommandPalette extends RayElement {
|
|
|
281
285
|
this.dialog.setAttribute("aria-label", this.label ?? "Command palette");
|
|
282
286
|
if (this.input.value !== (this.query ?? "")) this.input.value = this.query ?? "";
|
|
283
287
|
this.$(".hint").hidden = Boolean(this.hideHint);
|
|
288
|
+
this.$("#hint-navigate").textContent = this.navigateLabel ?? "";
|
|
289
|
+
this.$("#hint-run").textContent = this.runLabel ?? "";
|
|
290
|
+
this.$("#hint-close").textContent = this.closeLabel ?? "";
|
|
284
291
|
this._render();
|
|
285
292
|
if (changed.has("open") || this._first === undefined) {
|
|
286
293
|
this._first = false;
|
|
@@ -399,6 +406,14 @@ export class RayCommandPalette extends RayElement {
|
|
|
399
406
|
else icon.textContent = String(a.icon);
|
|
400
407
|
opt.append(icon);
|
|
401
408
|
}
|
|
409
|
+
// Content of its own before the text, any width: a colored method tag.
|
|
410
|
+
if (a.prefix !== undefined && a.prefix !== null && a.prefix !== "") {
|
|
411
|
+
const prefix = document.createElement("span");
|
|
412
|
+
prefix.className = "prefix";
|
|
413
|
+
if (a.prefix instanceof Node) prefix.append(a.prefix);
|
|
414
|
+
else prefix.textContent = String(a.prefix);
|
|
415
|
+
opt.append(prefix);
|
|
416
|
+
}
|
|
402
417
|
const text = document.createElement("span");
|
|
403
418
|
text.className = "text";
|
|
404
419
|
const label = document.createElement("span");
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// <ray-doc-tabs> + <ray-doc-tab>: the tabs of open documents (an editor's,
|
|
2
|
+
// a browser's), each closable and with an unsaved mark, all over one shared
|
|
3
|
+
// panel (WAI-ARIA tabs: `panel` is that element's id).
|
|
4
|
+
//
|
|
5
|
+
// Closing is the app's: the × (for the pointer only: a tab holds no
|
|
6
|
+
// focusable control), a middle click, or Delete/Backspace on the focused tab
|
|
7
|
+
// fire `ray-request-close` ({ value, tab, source }); the app asks if it must
|
|
8
|
+
// and removes the tab. When the selected tab goes, the one that takes its
|
|
9
|
+
// place is selected (`change`), and focused if the closed tab had the focus.
|
|
10
|
+
//
|
|
11
|
+
// Too many tabs for the width: the bar scrolls sideways like ray-tabs.
|
|
12
|
+
|
|
13
|
+
import { RayElement, css, define, uid } from "../../core/core.js";
|
|
14
|
+
import { inlineKey } from "../../core/direction.js";
|
|
15
|
+
import { SideScroller, sideScrollerStyles } from "../../core/scroll.js";
|
|
16
|
+
|
|
17
|
+
const barStyles = css`
|
|
18
|
+
:host { display: flex; align-items: center; gap: var(--ray-space-1); min-width: 0; }
|
|
19
|
+
.list { display: flex; flex: 0 1 auto; align-items: center; gap: 2px; min-width: 0; overflow-x: auto; scrollbar-width: none; }
|
|
20
|
+
.list::-webkit-scrollbar { display: none; }
|
|
21
|
+
.list ::slotted(ray-doc-tab) { flex: none; }
|
|
22
|
+
.end { display: inline-flex; flex: none; align-items: center; }
|
|
23
|
+
.end:not([data-filled]) { display: none; }
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export class RayDocTabs extends RayElement {
|
|
27
|
+
static properties = {
|
|
28
|
+
value: { type: String },
|
|
29
|
+
panel: { type: String },
|
|
30
|
+
label: { type: String },
|
|
31
|
+
closeLabel: { type: String, default: "Close" },
|
|
32
|
+
dirtyLabel: { type: String, default: "Unsaved changes" },
|
|
33
|
+
};
|
|
34
|
+
static styles = [sideScrollerStyles, barStyles];
|
|
35
|
+
static template = `<div class="list" part="list" role="tablist"><slot></slot></div><span class="end" part="end"><slot name="end"></slot></span>`;
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
this._order = [];
|
|
40
|
+
this._focused = null;
|
|
41
|
+
this.addEventListener("click", (e) => {
|
|
42
|
+
const tab = this._tabOf(e);
|
|
43
|
+
if (!tab) return;
|
|
44
|
+
if (e.composedPath().some((n) => n.part?.contains?.("close"))) this._requestClose(tab, "button");
|
|
45
|
+
else this._select(tab, true);
|
|
46
|
+
});
|
|
47
|
+
// The middle button closes, without the browser's autoscroll.
|
|
48
|
+
this.addEventListener("mousedown", (e) => {
|
|
49
|
+
if (e.button === 1 && this._tabOf(e)) e.preventDefault();
|
|
50
|
+
});
|
|
51
|
+
this.addEventListener("auxclick", (e) => {
|
|
52
|
+
const tab = this._tabOf(e);
|
|
53
|
+
if (tab && e.button === 1) this._requestClose(tab, "middle");
|
|
54
|
+
});
|
|
55
|
+
this.addEventListener("keydown", (e) => this._key(e));
|
|
56
|
+
// Which tab has the focus, kept while it is removed (no focusout then).
|
|
57
|
+
this.addEventListener("focusin", (e) => {
|
|
58
|
+
this._focused = e.target.closest?.("ray-doc-tab") ?? null;
|
|
59
|
+
});
|
|
60
|
+
this.addEventListener("focusout", (e) => {
|
|
61
|
+
if (e.relatedTarget) this._focused = null;
|
|
62
|
+
});
|
|
63
|
+
const end = this.$('slot[name="end"]');
|
|
64
|
+
end.addEventListener("slotchange", () => end.parentElement.toggleAttribute("data-filled", end.assignedNodes().length > 0));
|
|
65
|
+
this.$(".list slot").addEventListener("slotchange", () => this.requestUpdate());
|
|
66
|
+
this._scroller = new SideScroller(this, this.$(".list"), { onResize: () => this.requestUpdate() });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
connectedCallback() {
|
|
70
|
+
super.connectedCallback();
|
|
71
|
+
this._scroller.connect();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
disconnectedCallback() {
|
|
75
|
+
super.disconnectedCallback();
|
|
76
|
+
this._scroller.disconnect();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get tabs() {
|
|
80
|
+
return [...this.querySelectorAll("ray-doc-tab")].filter((t) => t.closest("ray-doc-tabs") === this);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The shared panel (`panel` is its id), or null. */
|
|
84
|
+
get panelElement() {
|
|
85
|
+
return this.panel ? this.getRootNode().getElementById?.(this.panel) ?? null : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
update() {
|
|
89
|
+
const tabs = this.tabs;
|
|
90
|
+
if (!tabs.some((t) => t.value === this.value)) this._replaceSelected(tabs);
|
|
91
|
+
const list = this.$(".list");
|
|
92
|
+
if (this.label) list.setAttribute("aria-label", this.label);
|
|
93
|
+
else list.removeAttribute("aria-label");
|
|
94
|
+
const panel = this.panelElement;
|
|
95
|
+
for (const tab of tabs) {
|
|
96
|
+
const active = tab.value === this.value;
|
|
97
|
+
tab.id ||= uid("doc-tab");
|
|
98
|
+
tab.setAttribute("role", "tab");
|
|
99
|
+
tab.setAttribute("aria-selected", String(active));
|
|
100
|
+
tab.tabIndex = active ? 0 : -1;
|
|
101
|
+
tab.toggleAttribute("data-active", active);
|
|
102
|
+
if (panel) tab.setAttribute("aria-controls", panel.id);
|
|
103
|
+
else tab.removeAttribute("aria-controls");
|
|
104
|
+
tab.requestUpdate();
|
|
105
|
+
}
|
|
106
|
+
if (panel) {
|
|
107
|
+
panel.setAttribute("role", "tabpanel");
|
|
108
|
+
const active = tabs.find((t) => t.value === this.value);
|
|
109
|
+
if (active) panel.setAttribute("aria-labelledby", active.id);
|
|
110
|
+
else panel.removeAttribute("aria-labelledby");
|
|
111
|
+
}
|
|
112
|
+
this._order = tabs;
|
|
113
|
+
if (this.value !== this._revealed && this._scroller.reveal(tabs.find((t) => t.value === this.value))) {
|
|
114
|
+
this._revealed = this.value;
|
|
115
|
+
}
|
|
116
|
+
this._scroller.update();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The selected tab is gone (or none was chosen yet): the one at its place
|
|
120
|
+
// takes over, else the one before it; the first when nothing was selected.
|
|
121
|
+
_replaceSelected(tabs) {
|
|
122
|
+
const before = this._order.findIndex((t) => t.value === this.value);
|
|
123
|
+
const next = before < 0 ? tabs[0] : tabs[Math.min(before, tabs.length - 1)];
|
|
124
|
+
const closed = before >= 0 ? this._order[before] : null;
|
|
125
|
+
if (!next) {
|
|
126
|
+
if (this.value !== undefined && before >= 0) {
|
|
127
|
+
this.commitProperty("value", undefined);
|
|
128
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (this.value === undefined || before < 0) {
|
|
133
|
+
this.commitProperty("value", next.value);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.commitProperty("value", next.value);
|
|
137
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
138
|
+
if (closed && closed === this._focused) {
|
|
139
|
+
this._focused = next;
|
|
140
|
+
queueMicrotask(() => next.focus());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_tabOf(e) {
|
|
145
|
+
const tab = e.target.closest?.("ray-doc-tab");
|
|
146
|
+
return tab && tab.closest("ray-doc-tabs") === this ? tab : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_select(tab, user) {
|
|
150
|
+
const changed = tab.value !== this.value;
|
|
151
|
+
this.value = tab.value;
|
|
152
|
+
if (user && changed) this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_requestClose(tab, source) {
|
|
156
|
+
if (tab.hideClose) return;
|
|
157
|
+
this.emit("ray-request-close", { value: tab.value, tab, source });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_key(e) {
|
|
161
|
+
const tab = this._tabOf(e);
|
|
162
|
+
if (!tab || e.target !== tab) return;
|
|
163
|
+
const tabs = this.tabs;
|
|
164
|
+
const i = tabs.indexOf(tab);
|
|
165
|
+
// Right to left, ArrowLeft is the next tab.
|
|
166
|
+
const key = inlineKey(e.key, this);
|
|
167
|
+
let target = null;
|
|
168
|
+
if (key === "ArrowRight") target = tabs[(i + 1) % tabs.length];
|
|
169
|
+
else if (key === "ArrowLeft") target = tabs[(i - 1 + tabs.length) % tabs.length];
|
|
170
|
+
else if (e.key === "Home") target = tabs[0];
|
|
171
|
+
else if (e.key === "End") target = tabs[tabs.length - 1];
|
|
172
|
+
else if (e.key === "Delete" || e.key === "Backspace") {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
this._requestClose(tab, "key");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!target) return;
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
target.focus();
|
|
180
|
+
this._select(target, true);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const tabStyles = css`
|
|
185
|
+
:host {
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: var(--ray-space-1);
|
|
189
|
+
box-sizing: border-box;
|
|
190
|
+
max-width: 14rem;
|
|
191
|
+
height: var(--ray-control-height-xs);
|
|
192
|
+
padding-inline: var(--ray-space-2) var(--ray-space-1);
|
|
193
|
+
font-size: var(--ray-font-size-sm);
|
|
194
|
+
color: var(--ray-muted-foreground);
|
|
195
|
+
white-space: nowrap;
|
|
196
|
+
border: 1px solid transparent;
|
|
197
|
+
border-radius: var(--ray-radius-sm);
|
|
198
|
+
cursor: default;
|
|
199
|
+
user-select: none;
|
|
200
|
+
outline: none;
|
|
201
|
+
transition: color var(--ray-duration-fast) var(--ray-easing-standard), background-color var(--ray-duration-fast) var(--ray-easing-standard);
|
|
202
|
+
}
|
|
203
|
+
:host(:hover) { color: var(--ray-foreground); background: var(--ray-muted); }
|
|
204
|
+
:host([data-active]) { color: var(--ray-foreground); background: var(--ray-card); border-color: var(--ray-border); }
|
|
205
|
+
:host(:focus-visible) { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: -2px; }
|
|
206
|
+
.section { display: inline-flex; flex: none; align-items: center; }
|
|
207
|
+
.section:not([data-filled]) { display: none; }
|
|
208
|
+
::slotted(svg) { width: 1rem; height: 1rem; }
|
|
209
|
+
.label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
210
|
+
|
|
211
|
+
/* The end of the tab: the × (on hover and on the selected tab), or the
|
|
212
|
+
unsaved dot, which turns into the × under the pointer. */
|
|
213
|
+
.mark { position: relative; display: inline-flex; flex: none; align-items: center; justify-content: center; width: 1.125rem; height: 1.125rem; }
|
|
214
|
+
.close { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: var(--ray-muted-foreground); border-radius: var(--ray-radius-xs); visibility: hidden; }
|
|
215
|
+
.close svg { width: 0.75rem; height: 0.75rem; }
|
|
216
|
+
.close:hover { color: var(--ray-foreground); background: var(--ray-accent); }
|
|
217
|
+
:host(:hover) .close, :host([data-active]) .close { visibility: visible; }
|
|
218
|
+
.dot { position: absolute; display: none; width: 0.4375rem; height: 0.4375rem; background: var(--ray-primary); border-radius: 50%; }
|
|
219
|
+
:host([dirty]) .dot { display: block; }
|
|
220
|
+
:host([dirty]) .close { visibility: hidden; }
|
|
221
|
+
:host([dirty]) .mark:hover .close { visibility: visible; }
|
|
222
|
+
:host([dirty]) .mark:hover .dot { display: none; }
|
|
223
|
+
:host([hide-close]) .close { display: none; }
|
|
224
|
+
:host([hide-close]:not([dirty])) .mark { display: none; }
|
|
225
|
+
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
export class RayDocTab extends RayElement {
|
|
229
|
+
static properties = {
|
|
230
|
+
value: { type: String, reflect: true },
|
|
231
|
+
dirty: { type: Boolean, reflect: true },
|
|
232
|
+
hideClose: { type: Boolean, reflect: true },
|
|
233
|
+
};
|
|
234
|
+
static styles = [tabStyles];
|
|
235
|
+
static template = `<span class="section" part="left-section"><slot name="left"></slot></span><span class="label" part="label"><slot></slot></span><span class="mark" aria-hidden="true"><span class="dot" part="dot"></span><span class="close" part="close" data-ray-icon="ui.close"></span></span><span class="sr"></span>`;
|
|
236
|
+
|
|
237
|
+
constructor() {
|
|
238
|
+
super();
|
|
239
|
+
const slot = this.$('slot[name="left"]');
|
|
240
|
+
slot.addEventListener("slotchange", () => slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
update(changed) {
|
|
244
|
+
const bar = this.closest("ray-doc-tabs");
|
|
245
|
+
this.$(".close").title = bar?.closeLabel ?? "Close";
|
|
246
|
+
// The unsaved mark is part of the tab's name for assistive technology.
|
|
247
|
+
this.$(".sr").textContent = this.dirty ? `, ${bar?.dirtyLabel ?? "Unsaved changes"}` : "";
|
|
248
|
+
if (this.hideClose) this.removeAttribute("aria-keyshortcuts");
|
|
249
|
+
else this.setAttribute("aria-keyshortcuts", "Delete");
|
|
250
|
+
if (changed.has("value")) bar?.requestUpdate();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
define("ray-doc-tabs", RayDocTabs);
|
|
255
|
+
define("ray-doc-tab", RayDocTab);
|
|
@@ -5,6 +5,10 @@ import { css, define } from "../../core/core.js";
|
|
|
5
5
|
import { RayButton } from "../button/button.js";
|
|
6
6
|
|
|
7
7
|
const styles = css`
|
|
8
|
+
/* The square on the host too: the inner button's flex: 1 (a 0 basis) gives
|
|
9
|
+
it no width of its own to an auto grid track or a shrink-to-fit box. A
|
|
10
|
+
width set on the element from outside still wins. */
|
|
11
|
+
:host { width: var(--_h); }
|
|
8
12
|
.button { width: var(--_h); padding: 0; }
|
|
9
13
|
.label { font-size: calc(var(--_fs) * 1.15); }
|
|
10
14
|
::slotted(svg) { width: 1.15em; height: 1.15em; }
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { css, define } from "../../core/core.js";
|
|
5
5
|
import { sizeStyles, radiusStyles } from "../../core/styles.js";
|
|
6
6
|
import { FieldControl, fieldStyles, boxStyles, fieldTemplate } from "../../core/form.js";
|
|
7
|
+
import { syncHighlight, textLayerStyles } from "../../core/textlayer.js";
|
|
8
|
+
import { syncComplete, completeStyles } from "../../core/complete.js";
|
|
7
9
|
|
|
8
10
|
const styles = css`
|
|
9
11
|
.control[type="search"]::-webkit-search-cancel-button { cursor: pointer; }
|
|
@@ -23,8 +25,11 @@ export class RayInput extends FieldControl {
|
|
|
23
25
|
placeholder: { type: String },
|
|
24
26
|
readonly: { type: Boolean, reflect: true },
|
|
25
27
|
autofocus: { type: Boolean },
|
|
28
|
+
highlight: { attribute: false },
|
|
29
|
+
complete: { attribute: false },
|
|
30
|
+
suggestionsLabel: { type: String, default: "Suggestions" },
|
|
26
31
|
};
|
|
27
|
-
static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
|
|
32
|
+
static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, textLayerStyles, completeStyles, styles];
|
|
28
33
|
static template = fieldTemplate(`<input id="control" class="control" part="input">`);
|
|
29
34
|
|
|
30
35
|
static get observedAttributes() {
|
|
@@ -64,6 +69,13 @@ export class RayInput extends FieldControl {
|
|
|
64
69
|
c.readOnly = Boolean(this.readonly);
|
|
65
70
|
this.syncField();
|
|
66
71
|
this._commit();
|
|
72
|
+
syncHighlight(this);
|
|
73
|
+
syncComplete(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Runs `highlight` again (what it marks changed, not the text). */
|
|
77
|
+
refreshHighlight() {
|
|
78
|
+
syncHighlight(this);
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
_commit() {
|
package/components/menu/menu.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
// context menu opened at the pointer over the trigger area.
|
|
4
4
|
//
|
|
5
5
|
// Choosing an item fires `ray-select` ({ value, item }) on the menu and
|
|
6
|
-
// closes it (unless keep-open).
|
|
6
|
+
// closes it (unless keep-open). An item of type="checkbox" toggles its
|
|
7
|
+
// `checked` first; one of type="radio" checks itself and unchecks the radio
|
|
8
|
+
// items of the menu with the same `name`. Before its trigger opens it, the
|
|
9
|
+
// menu fires a cancelable `ray-request-open` ({ event }): the place to fill
|
|
10
|
+
// items that depend on what was clicked, read only after it.
|
|
7
11
|
//
|
|
8
12
|
// With `native` and a native host that offers "popup_menu" (core/host.js; a
|
|
9
13
|
// std/ui window with core/raylang.js), it opens as the system's context menu
|
|
@@ -64,6 +68,7 @@ export class RayMenu extends Floating {
|
|
|
64
68
|
if (this.context) {
|
|
65
69
|
t.addEventListener("contextmenu", (e) => {
|
|
66
70
|
e.preventDefault();
|
|
71
|
+
if (!this._requestOpen(e)) return;
|
|
67
72
|
if (this._native()) {
|
|
68
73
|
this.popupNative();
|
|
69
74
|
return;
|
|
@@ -84,12 +89,12 @@ export class RayMenu extends Floating {
|
|
|
84
89
|
});
|
|
85
90
|
return;
|
|
86
91
|
}
|
|
87
|
-
t.addEventListener("click", () => {
|
|
92
|
+
t.addEventListener("click", (e) => {
|
|
88
93
|
if (this._native()) {
|
|
89
|
-
this.popupNative();
|
|
94
|
+
if (this._requestOpen(e)) this.popupNative();
|
|
90
95
|
return;
|
|
91
96
|
}
|
|
92
|
-
if (!this.open && this.justClosed()) return;
|
|
97
|
+
if (!this.open && (this.justClosed() || !this._requestOpen(e))) return;
|
|
93
98
|
this._focusOnOpen = "first";
|
|
94
99
|
this.toggle();
|
|
95
100
|
});
|
|
@@ -98,11 +103,20 @@ export class RayMenu extends Floating {
|
|
|
98
103
|
e.preventDefault();
|
|
99
104
|
this._focusOnOpen = e.key === "ArrowDown" ? "first" : "last";
|
|
100
105
|
if (this.open) this._focus(this._focusOnOpen);
|
|
101
|
-
else this.open = true;
|
|
106
|
+
else if (this._requestOpen(e)) this.open = true;
|
|
102
107
|
}
|
|
103
108
|
});
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Fires ray-request-open before the trigger opens the menu; false when a
|
|
113
|
+
* listener cancelled it. It comes before the items are read: an app's own
|
|
114
|
+
* listener on the trigger may run after the menu's.
|
|
115
|
+
*/
|
|
116
|
+
_requestOpen(event) {
|
|
117
|
+
return this.emit("ray-request-open", { event }, { cancelable: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
_native() {
|
|
107
121
|
return Boolean(this.native) && hostCan("popup_menu");
|
|
108
122
|
}
|
|
@@ -116,7 +130,7 @@ export class RayMenu extends Floating {
|
|
|
116
130
|
const entries = [...this.querySelectorAll("ray-menu-item, ray-menu-divider")].filter((el) => el.closest("ray-menu") === this);
|
|
117
131
|
const items = entries.map((el) => (el.localName === "ray-menu-divider"
|
|
118
132
|
? { separator: true }
|
|
119
|
-
: { title: itemLabel(el), enabled: !el.disabled }));
|
|
133
|
+
: { title: itemLabel(el), enabled: !el.disabled, ...(el.checkable ? { checked: Boolean(el.checked) } : {}) }));
|
|
120
134
|
const index = await getHost().popupMenu(items);
|
|
121
135
|
const item = index === null || index === undefined ? null : entries[index];
|
|
122
136
|
if (!item || item.localName !== "ray-menu-item") return null;
|
|
@@ -193,6 +207,13 @@ export class RayMenu extends Floating {
|
|
|
193
207
|
|
|
194
208
|
_choose(item) {
|
|
195
209
|
if (item.disabled) return;
|
|
210
|
+
if (item.type === "checkbox") item.checked = !item.checked;
|
|
211
|
+
else if (item.type === "radio") {
|
|
212
|
+
for (const other of this.items) {
|
|
213
|
+
if (other !== item && other.type === "radio" && (other.name ?? "") === (item.name ?? "")) other.checked = false;
|
|
214
|
+
}
|
|
215
|
+
item.checked = true;
|
|
216
|
+
}
|
|
196
217
|
this.emit("ray-select", { value: item.value ?? item.textContent.trim(), item });
|
|
197
218
|
if (!this.keepOpen) this.open = false;
|
|
198
219
|
}
|
|
@@ -227,6 +248,10 @@ const itemStyles = css`
|
|
|
227
248
|
:host([color]:hover), :host([color]:focus) { background: var(--_light); color: var(--_light-color); }
|
|
228
249
|
:host([disabled]) { color: var(--ray-muted-foreground); background: transparent; cursor: not-allowed; opacity: 0.6; }
|
|
229
250
|
.label { flex: 1; white-space: nowrap; }
|
|
251
|
+
/* A checkbox or radio item keeps a column for its mark, checked or not. */
|
|
252
|
+
.mark { display: none; flex: none; width: 1rem; height: 1rem; visibility: hidden; }
|
|
253
|
+
:host([type="checkbox"]) .mark, :host([type="radio"]) .mark { display: inline-flex; }
|
|
254
|
+
:host([checked]) .mark { visibility: visible; }
|
|
230
255
|
.section { display: inline-flex; align-items: center; color: var(--ray-muted-foreground); }
|
|
231
256
|
:host([color]) .section { color: inherit; }
|
|
232
257
|
.section:not([data-filled]) { display: none; }
|
|
@@ -239,9 +264,12 @@ export class RayMenuItem extends RayElement {
|
|
|
239
264
|
value: { type: String, reflect: true },
|
|
240
265
|
disabled: { type: Boolean, reflect: true },
|
|
241
266
|
color: { type: String, reflect: true },
|
|
267
|
+
type: { type: String, reflect: true, default: "item" },
|
|
268
|
+
checked: { type: Boolean, reflect: true },
|
|
269
|
+
name: { type: String, reflect: true },
|
|
242
270
|
};
|
|
243
271
|
static styles = [itemStyles];
|
|
244
|
-
static template = `<span class="section left" part="left-section"><slot name="left"></slot></span><span class="label" part="label"><slot></slot></span><span class="section right" part="right-section"><slot name="right"></slot></span>`;
|
|
272
|
+
static template = `<span class="mark" part="mark" aria-hidden="true" data-ray-icon="ui.checked"></span><span class="section left" part="left-section"><slot name="left"></slot></span><span class="label" part="label"><slot></slot></span><span class="section right" part="right-section"><slot name="right"></slot></span>`;
|
|
245
273
|
|
|
246
274
|
constructor() {
|
|
247
275
|
super();
|
|
@@ -252,8 +280,15 @@ export class RayMenuItem extends RayElement {
|
|
|
252
280
|
}
|
|
253
281
|
}
|
|
254
282
|
|
|
283
|
+
/** Whether it is a checkbox or radio item. */
|
|
284
|
+
get checkable() {
|
|
285
|
+
return this.type === "checkbox" || this.type === "radio";
|
|
286
|
+
}
|
|
287
|
+
|
|
255
288
|
update() {
|
|
256
289
|
this.tabIndex = -1;
|
|
290
|
+
this.internals.role = this.type === "checkbox" ? "menuitemcheckbox" : this.type === "radio" ? "menuitemradio" : "menuitem";
|
|
291
|
+
this.internals.ariaChecked = this.checkable ? String(Boolean(this.checked)) : null;
|
|
257
292
|
this.internals.ariaDisabled = this.disabled ? "true" : "false";
|
|
258
293
|
if (this.color) applyColor(this, this.color);
|
|
259
294
|
}
|