@fusion.dev/helix 0.171.0 → 0.173.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/custom-elements.json +2531 -95
- package/dist/chat/composer/chat-composer.css.js +1 -1
- package/dist/copy-button/root/copy-button-root.css.js +1 -1
- package/dist/element-events.generated.js +9 -1
- package/dist/graph/root/graph-root.css.js +2 -1
- package/dist/orb/root/orb-root.css.js +1 -1
- package/dist/orb/root/orb-root.d.ts +24 -5
- package/dist/orb/root/orb-root.js +24 -6
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/index.js +1 -0
- package/dist/sandbox/preview/sandbox-preview.d.ts +39 -2
- package/dist/sandbox/preview/sandbox-preview.js +37 -5
- package/dist/sandbox/root/sandbox-root.css.js +1 -1
- package/dist/sandbox/root/sandbox-root.d.ts +44 -4
- package/dist/sandbox/root/sandbox-root.js +92 -17
- package/dist/sandbox/source/editing.d.ts +111 -0
- package/dist/sandbox/source/editing.js +185 -0
- package/dist/sandbox/source/sandbox-source.css.js +2 -1
- package/dist/sandbox/source/sandbox-source.d.ts +131 -6
- package/dist/sandbox/source/sandbox-source.js +301 -17
- package/dist/timeline/root/timeline-root.css.js +1 -1
- package/dist/virtual-list/reorder/index.d.ts +1 -0
- package/dist/virtual-list/reorder/index.js +5 -0
- package/dist/virtual-list/reorder/virtual-list-reorder.css.d.ts +1 -0
- package/dist/virtual-list/reorder/virtual-list-reorder.css.js +12 -0
- package/dist/virtual-list/root/virtual-list-root.d.ts +9 -0
- package/lint.json +1 -1
- package/package.json +6 -6
- package/types/jsx.d.ts +996 -38
- package/types/vue.d.ts +236 -6
- package/types/words.d.ts +27 -0
|
@@ -28,8 +28,37 @@ import { type SandboxTokenizer } from '../tokens.js';
|
|
|
28
28
|
* that wraps keeps one number. The numbers are drawn, and hidden from screen
|
|
29
29
|
* readers, which read the field.
|
|
30
30
|
*
|
|
31
|
-
* Enter keeps the indentation of the line it breaks,
|
|
32
|
-
*
|
|
31
|
+
* Enter keeps the indentation of the line it breaks, one level deeper after
|
|
32
|
+
* an opening bracket, and puts the closing bracket of an empty pair on a line
|
|
33
|
+
* of its own. The new line is undone with the next undo as typing is.
|
|
34
|
+
*
|
|
35
|
+
* **The editing keys are CodeMirror's.** Tab with text selected indents every
|
|
36
|
+
* line the selection touches. `Mod+]` indents the lines and `Mod+[` outdents
|
|
37
|
+
* them, and `Mod+/` comments them out or back in: `//` in a `js` pane, and one
|
|
38
|
+
* block comment in an `html` or `css` pane. `Mod` is Command on a Mac and
|
|
39
|
+
* Control elsewhere. Each is one step of undo.
|
|
40
|
+
*
|
|
41
|
+
* **`close-brackets` closes brackets and quotes** as you type them: `(`, `[`,
|
|
42
|
+
* `{` and the three quotes. A closing character typed before the same one
|
|
43
|
+
* steps over it, Backspace between an empty pair deletes both, and a bracket
|
|
44
|
+
* typed with text selected goes around the text. **`match-brackets` marks
|
|
45
|
+
* the bracket beside the caret and its pair** while the field has focus, each
|
|
46
|
+
* a `bracket` part, and a bracket with no pair is also `bracket-unmatched`.
|
|
47
|
+
* A bracket in a string or a comment is not matched, where `tokenize` says
|
|
48
|
+
* which runs those are.
|
|
49
|
+
*
|
|
50
|
+
* **`complete` offers completions as you type**: the words of the source and
|
|
51
|
+
* of `completions` that start with the word before the caret. They open in a
|
|
52
|
+
* list under the word. The arrow keys move through it, Enter or Tab takes the
|
|
53
|
+
* word, and Escape closes it. `Ctrl+Space` opens it where it is closed. The
|
|
54
|
+
* field is `aria-autocomplete="list"` and points at the word chosen with
|
|
55
|
+
* `aria-activedescendant`, so a screen reader reads each one. Give
|
|
56
|
+
* `completions` the names a page uses, such as the tag and attribute names of
|
|
57
|
+
* a custom elements manifest through `manifestWords`.
|
|
58
|
+
*
|
|
59
|
+
* **The pane is `:state(modified)` while its source differs from the source it
|
|
60
|
+
* was given**, the first `value` or the last one set from outside. `reset()`
|
|
61
|
+
* puts that source back, as an edit the sandbox passes on to its preview.
|
|
33
62
|
*
|
|
34
63
|
* **The source can be written inside the pane** where `value` is not set: its
|
|
35
64
|
* text, or the markup of a `<template>` it holds, without the indentation the
|
|
@@ -45,15 +74,23 @@ import { type SandboxTokenizer } from '../tokens.js';
|
|
|
45
74
|
* @event {HelixCaretEvent} helix-caret - Fired when the cursor moves.
|
|
46
75
|
* @event {HelixEditEvent} helix-edit - Fired after the source was edited, with the new source.
|
|
47
76
|
*
|
|
48
|
-
* @csspart
|
|
49
|
-
* @csspart
|
|
77
|
+
* @csspart bracket - The bracket beside the caret and its pair, under `match-brackets`.
|
|
78
|
+
* @csspart bracket-unmatched - A bracket beside the caret with no pair, under `match-brackets`.
|
|
79
|
+
* @csspart completion - One word in the list of completions, under `complete`.
|
|
80
|
+
* @csspart completion-active - The word chosen in that list, which Enter or Tab takes.
|
|
81
|
+
* @csspart completions - The list of completions under the word being typed, under `complete`.
|
|
82
|
+
* @csspart field - The box that scrolls the field and its colors together, under `tokenize`, `line-numbers`, `match-brackets` or `complete`.
|
|
83
|
+
* @csspart highlight - The colored copy of the source drawn under the field, under `tokenize`, `line-numbers`, `match-brackets` or `complete`.
|
|
50
84
|
* @csspart line - One line of that copy, under `line-numbers`, numbered in the gutter by its `::before`.
|
|
51
85
|
* @csspart source - The editable region.
|
|
52
86
|
* @csspart token - One run of the source that has a kind, under `tokenize`.
|
|
53
87
|
*
|
|
54
88
|
* @cssproperty --sandbox-source-background-color - Fill behind the source.
|
|
89
|
+
* @cssproperty --sandbox-source-bracket-color - Fill behind a matched bracket, under `match-brackets`. Defaults to `--helix-color-accent-subtle`.
|
|
55
90
|
* @cssproperty --sandbox-source-callable-color - Color of a `callable` run, under `tokenize`. Defaults to `--helix-color-syntax-callable`.
|
|
56
91
|
* @cssproperty --sandbox-source-comment-color - Color of a `comment` run, under `tokenize`. Defaults to `--helix-color-syntax-comment`.
|
|
92
|
+
* @cssproperty --sandbox-source-completions-border-width - Width of the edge of the list of completions, under `complete`. Defaults to `--helix-border-floating`.
|
|
93
|
+
* @cssproperty --sandbox-source-completions-shadow - Shadow under the list of completions, under `complete`. Defaults to `--helix-shadow-floating`.
|
|
57
94
|
* @cssproperty --sandbox-source-constant-color - Color of a `constant` run, under `tokenize`. Defaults to `--helix-color-syntax-constant`.
|
|
58
95
|
* @cssproperty --sandbox-source-invalid-color - Color of an `invalid` run, under `tokenize`. Defaults to `--helix-color-syntax-invalid`.
|
|
59
96
|
* @cssproperty --sandbox-source-keyword-color - Color of a `keyword` run, under `tokenize`. Defaults to `--helix-color-syntax-keyword`.
|
|
@@ -62,6 +99,8 @@ import { type SandboxTokenizer } from '../tokens.js';
|
|
|
62
99
|
* @cssproperty --sandbox-source-name-color - Color of a `name` run, under `tokenize`. Defaults to `--helix-color-syntax-name`.
|
|
63
100
|
* @cssproperty --sandbox-source-string-color - Color of a `string` run, under `tokenize`. Defaults to `--helix-color-syntax-string`.
|
|
64
101
|
* @cssproperty --sandbox-source-tag-color - Color of a `tag` run, under `tokenize`. Defaults to `--helix-color-syntax-tag`.
|
|
102
|
+
*
|
|
103
|
+
* @cssstate modified - While the source differs from the source the pane was given, until `reset()` or a new `value`.
|
|
65
104
|
*/
|
|
66
105
|
export declare class HelixSandboxSource extends HelixElement {
|
|
67
106
|
/**
|
|
@@ -69,6 +108,26 @@ export declare class HelixSandboxSource extends HelixElement {
|
|
|
69
108
|
* `registerWords('de', { 'helix-sandbox-source': { fieldLabel: 'Quelltext' } })`.
|
|
70
109
|
*/
|
|
71
110
|
static words: string;
|
|
111
|
+
/**
|
|
112
|
+
* Whether typing an opening bracket or quote types its closing one too, and
|
|
113
|
+
* a bracket typed with text selected goes around it.
|
|
114
|
+
*/
|
|
115
|
+
closeBrackets: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Whether the pane offers completions for the word before the caret, from
|
|
118
|
+
* the words of the source and of `completions`.
|
|
119
|
+
*/
|
|
120
|
+
complete: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Words the pane offers under `complete` beside the words of the source,
|
|
123
|
+
* such as tag and attribute names. `manifestWords` reads them from a custom
|
|
124
|
+
* elements manifest. Set it as JSON in the attribute or as an array.
|
|
125
|
+
*/
|
|
126
|
+
completions: readonly string[];
|
|
127
|
+
/**
|
|
128
|
+
* The name of the list of completions, read by screen readers.
|
|
129
|
+
*/
|
|
130
|
+
completionsLabel: string;
|
|
72
131
|
/**
|
|
73
132
|
* The editable region's name, read by screen readers.
|
|
74
133
|
*/
|
|
@@ -92,6 +151,11 @@ export declare class HelixSandboxSource extends HelixElement {
|
|
|
92
151
|
* Whether each line of the source is numbered, in a gutter beside it.
|
|
93
152
|
*/
|
|
94
153
|
lineNumbers: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Whether the bracket beside the caret and its pair are marked while the
|
|
156
|
+
* field has focus.
|
|
157
|
+
*/
|
|
158
|
+
matchBrackets: boolean;
|
|
95
159
|
/**
|
|
96
160
|
* Which source this pane holds. Every `helix-before-edit` event includes it.
|
|
97
161
|
*
|
|
@@ -101,6 +165,12 @@ export declare class HelixSandboxSource extends HelixElement {
|
|
|
101
165
|
* Not reflected: nothing selects on it, so it keeps a real default instead.
|
|
102
166
|
*/
|
|
103
167
|
name: string;
|
|
168
|
+
/**
|
|
169
|
+
* Whether the source is read only: it can be selected and copied, and not
|
|
170
|
+
* edited, as Sandpack's `readOnly` file is. Tab moves focus out of a field
|
|
171
|
+
* that is read only.
|
|
172
|
+
*/
|
|
173
|
+
readOnly: boolean;
|
|
104
174
|
/**
|
|
105
175
|
* Whether Tab indents the source. When it is off, Tab moves focus.
|
|
106
176
|
*/
|
|
@@ -125,6 +195,29 @@ export declare class HelixSandboxSource extends HelixElement {
|
|
|
125
195
|
value: string;
|
|
126
196
|
/** Set by Escape, so the next Tab leaves the field. */
|
|
127
197
|
private escaped;
|
|
198
|
+
/** The source the pane was given, which `reset()` puts back. */
|
|
199
|
+
private baseline;
|
|
200
|
+
/** The source the last edit left, to tell an edit from a `value` set from outside. */
|
|
201
|
+
private edited;
|
|
202
|
+
/** Where the caret is, while nothing is selected, for `match-brackets`. */
|
|
203
|
+
private caret;
|
|
204
|
+
/** The completions open under the word being typed, under `complete`. */
|
|
205
|
+
private offered;
|
|
206
|
+
/** Which of the completions Enter or Tab takes. */
|
|
207
|
+
private chosen;
|
|
208
|
+
/** Set while a completion is written, so writing it does not offer more. */
|
|
209
|
+
private accepting;
|
|
210
|
+
/**
|
|
211
|
+
* Whether the source differs from the source the pane was given: its first
|
|
212
|
+
* `value`, or the last one set from outside.
|
|
213
|
+
*/
|
|
214
|
+
get modified(): boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Puts back the source the pane was given, as an edit: `helix-before-edit`
|
|
217
|
+
* is sent first and can refuse it, and the sandbox passes it to its preview.
|
|
218
|
+
*/
|
|
219
|
+
reset(): void;
|
|
220
|
+
private get field();
|
|
128
221
|
/** Whether the source written inside the pane has been read. */
|
|
129
222
|
private read;
|
|
130
223
|
/**
|
|
@@ -141,6 +234,8 @@ export declare class HelixSandboxSource extends HelixElement {
|
|
|
141
234
|
private readWritten;
|
|
142
235
|
connectedCallback(): void;
|
|
143
236
|
protected willUpdate(changed: PropertyValues): void;
|
|
237
|
+
/** Places the completions under the word they complete, and keeps the chosen one in view. */
|
|
238
|
+
protected updated(changed: PropertyValues): void;
|
|
144
239
|
/**
|
|
145
240
|
* Reports where the cursor is, for a status bar to read.
|
|
146
241
|
*
|
|
@@ -172,6 +267,14 @@ export declare class HelixSandboxSource extends HelixElement {
|
|
|
172
267
|
* without sending the caret to the end. Exactly what `Input` does with the
|
|
173
268
|
* same helper for the same reason. */
|
|
174
269
|
private readonly handleInput;
|
|
270
|
+
/** Opens the completions for the word before the caret, or closes them where there are none. */
|
|
271
|
+
private offer;
|
|
272
|
+
/** Writes the chosen completion over the word it completes. */
|
|
273
|
+
private accept;
|
|
274
|
+
/**
|
|
275
|
+
* The keys of the open completions. Returns whether the key was theirs.
|
|
276
|
+
*/
|
|
277
|
+
private completionKey;
|
|
175
278
|
/**
|
|
176
279
|
* The one place an edit is taken, so a refusal cannot be forgotten at one of
|
|
177
280
|
* them. Two paths reach it, a keystroke and the tab that indents.
|
|
@@ -179,8 +282,21 @@ export declare class HelixSandboxSource extends HelixElement {
|
|
|
179
282
|
private edit;
|
|
180
283
|
private readonly handleKeydown;
|
|
181
284
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
285
|
+
* The editing commands: the `Mod` keys for indenting and comments, and, where
|
|
286
|
+
* they are on, closing brackets and opening completions. Returns whether the
|
|
287
|
+
* key was one, so it does nothing else.
|
|
288
|
+
*/
|
|
289
|
+
private command;
|
|
290
|
+
/**
|
|
291
|
+
* Makes a change as typing would, through `insert`, and puts the selection
|
|
292
|
+
* where the change says. A change that refuses nothing and replaces nothing
|
|
293
|
+
* only moves the caret.
|
|
294
|
+
*/
|
|
295
|
+
private apply;
|
|
296
|
+
/**
|
|
297
|
+
* Breaks the line with the indentation it has, one level deeper after an
|
|
298
|
+
* opening bracket. A line at the margin with no bracket is left to the
|
|
299
|
+
* platform's own Enter.
|
|
184
300
|
*/
|
|
185
301
|
private breakLine;
|
|
186
302
|
/**
|
|
@@ -197,6 +313,15 @@ export declare class HelixSandboxSource extends HelixElement {
|
|
|
197
313
|
*/
|
|
198
314
|
private insert;
|
|
199
315
|
protected render(): TemplateResult;
|
|
316
|
+
/**
|
|
317
|
+
* The runs with the brackets beside the caret and the completions' anchor
|
|
318
|
+
* marked, where either is on.
|
|
319
|
+
*/
|
|
320
|
+
private mark;
|
|
321
|
+
/** The list of completions, while it is open. */
|
|
322
|
+
private renderCompletions;
|
|
323
|
+
/** Closes the completions, as the field loses focus. */
|
|
324
|
+
private readonly close;
|
|
200
325
|
/** The field itself and its description, which is the whole pane without `tokenize`. */
|
|
201
326
|
private renderField;
|
|
202
327
|
static styles: CSSResultGroup;
|
|
@@ -6,8 +6,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import { customElement, formatWords, HelixBeforeEditEvent, HelixCaretEvent, HelixEditEvent, HelixElement, onUnlessFalse, restoreValue, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
|
-
import { property } from 'lit/decorators.js';
|
|
9
|
+
import { property, state } from 'lit/decorators.js';
|
|
10
10
|
import { drawnRuns } from '../tokens.js';
|
|
11
|
+
import { breakLine, closeBracket, deletePair, indentLines, matchBracket, outdentLines, suggest, syntaxOf, toggleComment, } from './editing.js';
|
|
11
12
|
import { styles } from './sandbox-source.css.js';
|
|
12
13
|
const MODIFIERS = new Set(['Alt', 'AltGraph', 'Control', 'Meta', 'Shift']);
|
|
13
14
|
const dedent = (text) => {
|
|
@@ -20,37 +21,90 @@ const dedent = (text) => {
|
|
|
20
21
|
.map((line) => /^[ \t]*/.exec(line)?.[0].length ?? 0));
|
|
21
22
|
return Number.isFinite(shared) ? lines.map((line) => line.slice(shared)).join('\n') : '';
|
|
22
23
|
};
|
|
24
|
+
const cut = (runs, marks) => {
|
|
25
|
+
if (marks.size === 0) {
|
|
26
|
+
return [...runs];
|
|
27
|
+
}
|
|
28
|
+
const points = [...marks.keys()].sort((a, b) => a - b);
|
|
29
|
+
const pieces = [];
|
|
30
|
+
let at = 0;
|
|
31
|
+
let next = 0;
|
|
32
|
+
for (const { kind, text } of runs) {
|
|
33
|
+
let from = 0;
|
|
34
|
+
for (let point = points[next]; point !== undefined && point < at + text.length; point = points[++next]) {
|
|
35
|
+
const mark = marks.get(point);
|
|
36
|
+
const index = point - at;
|
|
37
|
+
if (index > from) {
|
|
38
|
+
pieces.push({ kind, text: text.slice(from, index) });
|
|
39
|
+
}
|
|
40
|
+
from = mark === 'anchor' ? index : index + 1;
|
|
41
|
+
pieces.push({ kind, mark, text: text.slice(index, from) });
|
|
42
|
+
}
|
|
43
|
+
if (from < text.length) {
|
|
44
|
+
pieces.push({ kind, text: text.slice(from) });
|
|
45
|
+
}
|
|
46
|
+
at += text.length;
|
|
47
|
+
}
|
|
48
|
+
for (const point of points.slice(next)) {
|
|
49
|
+
pieces.push({ mark: marks.get(point), text: '' });
|
|
50
|
+
}
|
|
51
|
+
return pieces;
|
|
52
|
+
};
|
|
23
53
|
const linesOf = (runs) => {
|
|
24
54
|
const lines = [[]];
|
|
25
|
-
for (const { kind, text } of runs) {
|
|
55
|
+
for (const { kind, mark, text } of runs) {
|
|
26
56
|
text.split('\n').forEach((piece, at) => {
|
|
27
57
|
if (at > 0) {
|
|
28
58
|
lines.push([]);
|
|
29
59
|
}
|
|
30
|
-
if (piece !== '') {
|
|
31
|
-
lines.at(-1)?.push(
|
|
60
|
+
if (piece !== '' || mark === 'anchor') {
|
|
61
|
+
lines.at(-1)?.push({ kind, mark, text: piece });
|
|
32
62
|
}
|
|
33
63
|
});
|
|
34
64
|
}
|
|
35
65
|
return lines;
|
|
36
66
|
};
|
|
67
|
+
const quietOf = (runs, length) => {
|
|
68
|
+
const quiet = new Uint8Array(length);
|
|
69
|
+
let at = 0;
|
|
70
|
+
for (const { kind, text } of runs) {
|
|
71
|
+
if (kind === 'string' || kind === 'comment') {
|
|
72
|
+
quiet.fill(1, at, at + text.length);
|
|
73
|
+
}
|
|
74
|
+
at += text.length;
|
|
75
|
+
}
|
|
76
|
+
return quiet;
|
|
77
|
+
};
|
|
37
78
|
let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
38
79
|
constructor() {
|
|
39
80
|
super(...arguments);
|
|
81
|
+
this.closeBrackets = false;
|
|
82
|
+
this.complete = false;
|
|
83
|
+
this.completions = [];
|
|
84
|
+
this.completionsLabel = 'Completions';
|
|
40
85
|
this.fieldLabel = 'Example source';
|
|
41
86
|
this.fieldNameLabel = '{field}, {name}';
|
|
42
87
|
this.keysDescription = 'Tab inserts two spaces. To leave, press Escape and then Tab, or press Shift+Tab to go back.';
|
|
43
88
|
this.label = '';
|
|
44
89
|
this.lineNumbers = false;
|
|
90
|
+
this.matchBrackets = false;
|
|
45
91
|
this.name = 'html';
|
|
92
|
+
this.readOnly = false;
|
|
46
93
|
this.tabIndents = true;
|
|
47
94
|
this.value = '';
|
|
48
95
|
this.escaped = false;
|
|
96
|
+
this.baseline = '';
|
|
97
|
+
this.caret = -1;
|
|
98
|
+
this.chosen = 0;
|
|
99
|
+
this.accepting = false;
|
|
49
100
|
this.read = false;
|
|
50
101
|
this.reportCaret = (event) => {
|
|
51
102
|
const field = event.target;
|
|
52
103
|
const at = field.selectionStart ?? 0;
|
|
53
104
|
const before = field.value.slice(0, at);
|
|
105
|
+
if (this.matchBrackets) {
|
|
106
|
+
this.caret = at === field.selectionEnd ? at : -1;
|
|
107
|
+
}
|
|
54
108
|
this.dispatchEvent(new HelixCaretEvent({
|
|
55
109
|
column: at - (before.lastIndexOf('\n') + 1) + 1,
|
|
56
110
|
line: before.split('\n').length,
|
|
@@ -60,8 +114,24 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
60
114
|
const field = event.target;
|
|
61
115
|
this.edit(field, field.value);
|
|
62
116
|
this.reportCaret(event);
|
|
117
|
+
const typed = event.inputType ?? '';
|
|
118
|
+
if (this.complete &&
|
|
119
|
+
!this.accepting &&
|
|
120
|
+
(this.offered || /^insert(?:Text|Replacement)/.test(typed))) {
|
|
121
|
+
this.offer(field);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.offered = undefined;
|
|
125
|
+
}
|
|
63
126
|
};
|
|
64
127
|
this.handleKeydown = (event) => {
|
|
128
|
+
const field = event.target;
|
|
129
|
+
if (this.readOnly) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (this.completionKey(event, field)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
65
135
|
if (event.key === 'Escape') {
|
|
66
136
|
this.escaped = true;
|
|
67
137
|
return;
|
|
@@ -69,6 +139,11 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
69
139
|
if (MODIFIERS.has(event.key)) {
|
|
70
140
|
return;
|
|
71
141
|
}
|
|
142
|
+
if (this.command(event, field)) {
|
|
143
|
+
this.escaped = false;
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
72
147
|
if (event.key === 'Enter' &&
|
|
73
148
|
!event.isComposing &&
|
|
74
149
|
!(event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)) {
|
|
@@ -81,10 +156,30 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
81
156
|
return;
|
|
82
157
|
}
|
|
83
158
|
event.preventDefault();
|
|
84
|
-
|
|
159
|
+
if (field.selectionStart === field.selectionEnd) {
|
|
160
|
+
this.insert(field, ' ');
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.apply(field, indentLines(field.value, field.selectionStart, field.selectionEnd));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
this.close = () => {
|
|
167
|
+
this.offered = undefined;
|
|
85
168
|
};
|
|
86
169
|
}
|
|
87
170
|
static { this.words = 'helix-sandbox-source'; }
|
|
171
|
+
get modified() {
|
|
172
|
+
return this.value === this.edited && this.value !== this.baseline;
|
|
173
|
+
}
|
|
174
|
+
reset() {
|
|
175
|
+
const field = this.field;
|
|
176
|
+
if (field && this.modified) {
|
|
177
|
+
this.edit(field, this.baseline);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
get field() {
|
|
181
|
+
return this.renderRoot.querySelector('textarea');
|
|
182
|
+
}
|
|
88
183
|
readWritten() {
|
|
89
184
|
if (this.read ||
|
|
90
185
|
this.children === undefined ||
|
|
@@ -108,11 +203,77 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
108
203
|
if (!this.hasUpdated) {
|
|
109
204
|
this.readWritten();
|
|
110
205
|
}
|
|
206
|
+
if (this.value !== this.edited) {
|
|
207
|
+
this.baseline = this.value;
|
|
208
|
+
this.edited = undefined;
|
|
209
|
+
}
|
|
210
|
+
this.toggleState('modified', this.modified);
|
|
211
|
+
}
|
|
212
|
+
updated(changed) {
|
|
213
|
+
super.updated(changed);
|
|
214
|
+
const list = this.renderRoot.querySelector('[part="completions"]');
|
|
215
|
+
const anchor = this.renderRoot.querySelector('#anchor');
|
|
216
|
+
const box = this.renderRoot.querySelector('[part="field"]');
|
|
217
|
+
if (!list || !anchor || !box) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const at = anchor.getBoundingClientRect();
|
|
221
|
+
const frame = box.getBoundingClientRect();
|
|
222
|
+
list.style.top = `${at.bottom - frame.top + box.scrollTop}px`;
|
|
223
|
+
list.style.left = `${at.left - frame.left + box.scrollLeft}px`;
|
|
224
|
+
list.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: 'nearest' });
|
|
225
|
+
}
|
|
226
|
+
offer(field) {
|
|
227
|
+
const at = field.selectionStart;
|
|
228
|
+
this.offered =
|
|
229
|
+
at === field.selectionEnd ? suggest(field.value, at, this.completions) : undefined;
|
|
230
|
+
this.chosen = 0;
|
|
231
|
+
}
|
|
232
|
+
accept(field, index) {
|
|
233
|
+
const offered = this.offered;
|
|
234
|
+
const word = offered?.words[index];
|
|
235
|
+
this.offered = undefined;
|
|
236
|
+
if (!offered || word === undefined) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.accepting = true;
|
|
240
|
+
this.apply(field, {
|
|
241
|
+
from: offered.from,
|
|
242
|
+
selection: [offered.from + word.length, offered.from + word.length],
|
|
243
|
+
text: word,
|
|
244
|
+
to: field.selectionEnd,
|
|
245
|
+
});
|
|
246
|
+
this.accepting = false;
|
|
247
|
+
}
|
|
248
|
+
completionKey(event, field) {
|
|
249
|
+
const offered = this.offered;
|
|
250
|
+
if (!offered) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const count = offered.words.length;
|
|
254
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
255
|
+
this.chosen = (this.chosen + (event.key === 'ArrowDown' ? 1 : count - 1)) % count;
|
|
256
|
+
}
|
|
257
|
+
else if (event.key === 'Enter' || (event.key === 'Tab' && !event.shiftKey)) {
|
|
258
|
+
this.accept(field, this.chosen);
|
|
259
|
+
}
|
|
260
|
+
else if (event.key === 'Escape') {
|
|
261
|
+
this.offered = undefined;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
if (/^(?:Arrow|Home|End|Page)/.test(event.key)) {
|
|
265
|
+
this.offered = undefined;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
return true;
|
|
111
271
|
}
|
|
112
272
|
edit(field, next) {
|
|
113
273
|
const request = new HelixBeforeEditEvent({ value: next });
|
|
114
274
|
const accepted = this.dispatchEvent(request);
|
|
115
275
|
if (accepted) {
|
|
276
|
+
this.edited = next;
|
|
116
277
|
this.value = next;
|
|
117
278
|
}
|
|
118
279
|
restoreValue(field, this.value);
|
|
@@ -120,14 +281,56 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
120
281
|
this.dispatchEvent(new HelixEditEvent({ value: next }));
|
|
121
282
|
}
|
|
122
283
|
}
|
|
284
|
+
command(event, field) {
|
|
285
|
+
const { selectionEnd: to, selectionStart: from, value } = field;
|
|
286
|
+
const mod = (event.ctrlKey || event.metaKey) && !event.altKey;
|
|
287
|
+
if (event.isComposing) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (mod && (event.key === ']' || event.key === '[')) {
|
|
291
|
+
this.apply(field, (event.key === ']' ? indentLines : outdentLines)(value, from, to));
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
const syntax = syntaxOf(this.name);
|
|
295
|
+
if (mod && event.key === '/' && syntax) {
|
|
296
|
+
this.apply(field, toggleComment(value, from, to, syntax));
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
if (this.complete && event.ctrlKey && event.key === ' ') {
|
|
300
|
+
this.offer(field);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (!this.closeBrackets || mod || event.altKey) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const change = event.key === 'Backspace'
|
|
307
|
+
? deletePair(value, from, to)
|
|
308
|
+
: event.key.length === 1
|
|
309
|
+
? closeBracket(value, from, to, event.key)
|
|
310
|
+
: undefined;
|
|
311
|
+
this.apply(field, change);
|
|
312
|
+
return change !== undefined;
|
|
313
|
+
}
|
|
314
|
+
apply(field, change) {
|
|
315
|
+
if (!change) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const { from, selection, text, to } = change;
|
|
319
|
+
const next = `${field.value.slice(0, from)}${text}${field.value.slice(to)}`;
|
|
320
|
+
if (text !== '' || from !== to) {
|
|
321
|
+
field.setSelectionRange(from, to);
|
|
322
|
+
this.insert(field, text);
|
|
323
|
+
}
|
|
324
|
+
if (field.value === next) {
|
|
325
|
+
field.setSelectionRange(...selection);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
123
328
|
breakLine(event) {
|
|
124
329
|
const field = event.target;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
const indent = /^[ \t]*/.exec(field.value.slice(start, at))?.[0] ?? '';
|
|
128
|
-
if (indent !== '') {
|
|
330
|
+
const change = breakLine(field.value, field.selectionStart, field.selectionEnd);
|
|
331
|
+
if (change) {
|
|
129
332
|
event.preventDefault();
|
|
130
|
-
this.
|
|
333
|
+
this.apply(field, change);
|
|
131
334
|
}
|
|
132
335
|
}
|
|
133
336
|
insert(field, text) {
|
|
@@ -135,7 +338,9 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
135
338
|
const focused = field.getRootNode().activeElement === field;
|
|
136
339
|
if (focused &&
|
|
137
340
|
typeof ownerDocument.execCommand === 'function' &&
|
|
138
|
-
|
|
341
|
+
(text === ''
|
|
342
|
+
? ownerDocument.execCommand('delete')
|
|
343
|
+
: ownerDocument.execCommand('insertText', false, text))) {
|
|
139
344
|
return;
|
|
140
345
|
}
|
|
141
346
|
field.setRangeText(text, field.selectionStart, field.selectionEnd, 'end');
|
|
@@ -143,7 +348,7 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
143
348
|
}
|
|
144
349
|
render() {
|
|
145
350
|
const field = this.renderField();
|
|
146
|
-
if (!this.tokenize && !this.lineNumbers) {
|
|
351
|
+
if (!this.tokenize && !this.lineNumbers && !this.matchBrackets && !this.complete) {
|
|
147
352
|
return field;
|
|
148
353
|
}
|
|
149
354
|
let given = [];
|
|
@@ -153,18 +358,63 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
153
358
|
catch (error) {
|
|
154
359
|
globalThis.reportError?.(error);
|
|
155
360
|
}
|
|
156
|
-
const runs = drawnRuns(this.value, given);
|
|
157
|
-
const draw = (some) => some.map(({ kind, text }) =>
|
|
361
|
+
const runs = this.mark(drawnRuns(this.value, given));
|
|
362
|
+
const draw = (some) => some.map(({ kind, mark, text }) => {
|
|
363
|
+
const lone = mark === 'unmatched';
|
|
364
|
+
const shown = mark === 'anchor'
|
|
365
|
+
? html `<span id="anchor"></span>`
|
|
366
|
+
: mark
|
|
367
|
+
? html `<span part="bracket${lone ? ' bracket-unmatched' : ''}">${text}</span>`
|
|
368
|
+
: text;
|
|
369
|
+
return kind ? html `<span part="token" data-kind=${kind}>${shown}</span>` : shown;
|
|
370
|
+
});
|
|
158
371
|
if (this.lineNumbers) {
|
|
159
372
|
const lines = linesOf(runs);
|
|
160
373
|
return html `<div part="field" style="--_sandbox-source-gutter: ${String(lines.length).length}ch">
|
|
161
374
|
<pre part="highlight" aria-hidden="true">${lines.map((line) => html `<span part="line">${draw(line)}</span>`)}</pre>
|
|
162
|
-
${field}
|
|
375
|
+
${field}${this.renderCompletions()}
|
|
163
376
|
</div>`;
|
|
164
377
|
}
|
|
165
378
|
return html `<div part="field">
|
|
166
379
|
<pre part="highlight" aria-hidden="true">${draw(runs)}${'\n'}</pre>
|
|
167
|
-
${field}
|
|
380
|
+
${field}${this.renderCompletions()}
|
|
381
|
+
</div>`;
|
|
382
|
+
}
|
|
383
|
+
mark(runs) {
|
|
384
|
+
const marks = new Map();
|
|
385
|
+
if (this.matchBrackets && this.caret >= 0) {
|
|
386
|
+
const quiet = quietOf(runs, this.value.length);
|
|
387
|
+
const found = matchBracket(this.value, this.caret, (index) => quiet[index] === 1);
|
|
388
|
+
if (found) {
|
|
389
|
+
marks.set(found.at, found.match === undefined ? 'unmatched' : 'bracket');
|
|
390
|
+
if (found.match !== undefined) {
|
|
391
|
+
marks.set(found.match, 'bracket');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (this.offered) {
|
|
396
|
+
marks.set(this.offered.from, 'anchor');
|
|
397
|
+
}
|
|
398
|
+
return cut(runs, marks);
|
|
399
|
+
}
|
|
400
|
+
renderCompletions() {
|
|
401
|
+
const offered = this.offered;
|
|
402
|
+
if (!offered) {
|
|
403
|
+
return nothing;
|
|
404
|
+
}
|
|
405
|
+
return html `<div part="completions" id="completions" role="listbox" aria-label=${this.completionsLabel}>
|
|
406
|
+
${offered.words.map((word, at) => {
|
|
407
|
+
const chosen = at === this.chosen;
|
|
408
|
+
return html `<div part="completion${chosen ? ' completion-active' : ''}" id=${`completion-${at}`}
|
|
409
|
+
role="option" aria-selected=${chosen ? 'true' : 'false'}
|
|
410
|
+
@pointerdown=${(event) => {
|
|
411
|
+
event.preventDefault();
|
|
412
|
+
const field = this.field;
|
|
413
|
+
if (field) {
|
|
414
|
+
this.accept(field, at);
|
|
415
|
+
}
|
|
416
|
+
}}>${word}</div>`;
|
|
417
|
+
})}
|
|
168
418
|
</div>`;
|
|
169
419
|
}
|
|
170
420
|
renderField() {
|
|
@@ -173,10 +423,16 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
173
423
|
aria-label=${this.label
|
|
174
424
|
? formatWords(this, 'fieldNameLabel', { field: this.fieldLabel, name: this.label })
|
|
175
425
|
: this.fieldLabel}
|
|
176
|
-
aria-describedby=${this.tabIndents ? 'keys' : nothing}
|
|
426
|
+
aria-describedby=${this.tabIndents && !this.readOnly ? 'keys' : nothing}
|
|
427
|
+
aria-autocomplete=${this.complete ? 'list' : nothing}
|
|
428
|
+
aria-controls=${this.offered ? 'completions' : nothing}
|
|
429
|
+
aria-activedescendant=${this.offered ? `completion-${this.chosen}` : nothing}
|
|
177
430
|
spellcheck="false"
|
|
431
|
+
?readonly=${this.readOnly}
|
|
178
432
|
.value=${this.value}
|
|
433
|
+
@blur=${this.close}
|
|
179
434
|
@focus=${this.reportCaret}
|
|
435
|
+
@pointerdown=${this.close}
|
|
180
436
|
@input=${this.handleInput}
|
|
181
437
|
@keydown=${this.handleKeydown}
|
|
182
438
|
@keyup=${this.reportCaret}
|
|
@@ -186,6 +442,19 @@ let HelixSandboxSource = class HelixSandboxSource extends HelixElement {
|
|
|
186
442
|
}
|
|
187
443
|
static { this.styles = styles; }
|
|
188
444
|
};
|
|
445
|
+
__decorate([
|
|
446
|
+
property({ attribute: 'close-brackets', type: Boolean })
|
|
447
|
+
], HelixSandboxSource.prototype, "closeBrackets", void 0);
|
|
448
|
+
__decorate([
|
|
449
|
+
property({ type: Boolean })
|
|
450
|
+
], HelixSandboxSource.prototype, "complete", void 0);
|
|
451
|
+
__decorate([
|
|
452
|
+
property({ type: Array })
|
|
453
|
+
], HelixSandboxSource.prototype, "completions", void 0);
|
|
454
|
+
__decorate([
|
|
455
|
+
words,
|
|
456
|
+
property({ attribute: 'completions-label' })
|
|
457
|
+
], HelixSandboxSource.prototype, "completionsLabel", void 0);
|
|
189
458
|
__decorate([
|
|
190
459
|
words,
|
|
191
460
|
property({ attribute: 'field-label' })
|
|
@@ -204,9 +473,15 @@ __decorate([
|
|
|
204
473
|
__decorate([
|
|
205
474
|
property({ attribute: 'line-numbers', reflect: true, type: Boolean })
|
|
206
475
|
], HelixSandboxSource.prototype, "lineNumbers", void 0);
|
|
476
|
+
__decorate([
|
|
477
|
+
property({ attribute: 'match-brackets', type: Boolean })
|
|
478
|
+
], HelixSandboxSource.prototype, "matchBrackets", void 0);
|
|
207
479
|
__decorate([
|
|
208
480
|
property()
|
|
209
481
|
], HelixSandboxSource.prototype, "name", void 0);
|
|
482
|
+
__decorate([
|
|
483
|
+
property({ attribute: 'readonly', reflect: true, type: Boolean })
|
|
484
|
+
], HelixSandboxSource.prototype, "readOnly", void 0);
|
|
210
485
|
__decorate([
|
|
211
486
|
property({ attribute: 'tab-indents', converter: onUnlessFalse })
|
|
212
487
|
], HelixSandboxSource.prototype, "tabIndents", void 0);
|
|
@@ -216,6 +491,15 @@ __decorate([
|
|
|
216
491
|
__decorate([
|
|
217
492
|
property()
|
|
218
493
|
], HelixSandboxSource.prototype, "value", void 0);
|
|
494
|
+
__decorate([
|
|
495
|
+
state()
|
|
496
|
+
], HelixSandboxSource.prototype, "caret", void 0);
|
|
497
|
+
__decorate([
|
|
498
|
+
state()
|
|
499
|
+
], HelixSandboxSource.prototype, "offered", void 0);
|
|
500
|
+
__decorate([
|
|
501
|
+
state()
|
|
502
|
+
], HelixSandboxSource.prototype, "chosen", void 0);
|
|
219
503
|
HelixSandboxSource = __decorate([
|
|
220
504
|
customElement('helix-sandbox-source')
|
|
221
505
|
], HelixSandboxSource);
|