@quario/editor 0.1.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/CHANGELOG.md +39 -0
- package/LICENSE +219 -0
- package/README.md +89 -0
- package/lib/chrome.js +431 -0
- package/lib/document.js +674 -0
- package/lib/history.js +57 -0
- package/lib/index.d.ts +119 -0
- package/lib/index.js +820 -0
- package/lib/rail.js +506 -0
- package/lib/register.d.ts +9 -0
- package/lib/register.js +10 -0
- package/lib/stage.js +654 -0
- package/lib/style.js +85 -0
- package/package.json +77 -0
package/lib/rail.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The right rail's three sections — Properties, Groups, Problems — as Lit
|
|
3
|
+
* templates over the document model. Two flat groups per node, Content and
|
|
4
|
+
* Style, no progressive disclosure: at most six own keys plus nine style
|
|
5
|
+
* names is not a surface that earns one. Only literal-or-expression fields
|
|
6
|
+
* get the fx toggle; a group's `by` and an image's `source` are bare text
|
|
7
|
+
* fields, because there is no literal form of them to offer.
|
|
8
|
+
*
|
|
9
|
+
* Commits are the element's: every control reports through `act.edit`, which
|
|
10
|
+
* is three verbs. `touch` moves the working document without taking a history
|
|
11
|
+
* entry, so the preview and the problem list answer to the keystroke.
|
|
12
|
+
* `settle` is a typed control's own gesture ending — blurred, Entered, or a
|
|
13
|
+
* picker released — which is what the `touch`es before it were building
|
|
14
|
+
* toward. `write` is a discrete gesture in one act: a checkbox, an alignment
|
|
15
|
+
* button, an fx toggle. The distinction between the last two is the whole
|
|
16
|
+
* reason this module has three verbs rather than two: an fx toggle carries
|
|
17
|
+
* the same `key` as the field beside it, so routing by key alone would let it
|
|
18
|
+
* swallow the typing it interrupted instead of letting that land first.
|
|
19
|
+
*/
|
|
20
|
+
import { html, nothing } from "lit";
|
|
21
|
+
import { fieldKind, nodeKindAt, resolve } from "./document.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The rail's whole seam to the element: how a control says what it did.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* touch: (key: string, value: any) => void,
|
|
28
|
+
* settle: (key: string, value: any) => void,
|
|
29
|
+
* write: (key: string, value: any) => void,
|
|
30
|
+
* }} Edit
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The closed style vocabulary an item offers, in one fixed panel order. */
|
|
34
|
+
let STYLES = [
|
|
35
|
+
"family",
|
|
36
|
+
"size",
|
|
37
|
+
"bold",
|
|
38
|
+
"italic",
|
|
39
|
+
"underline",
|
|
40
|
+
"strikethrough",
|
|
41
|
+
"color",
|
|
42
|
+
"background",
|
|
43
|
+
"align",
|
|
44
|
+
];
|
|
45
|
+
// The vocabulary that differs from the full set: an image has two style
|
|
46
|
+
// declarations, and a group or the table itself has none.
|
|
47
|
+
/** @type {Record<string, string[]>} */
|
|
48
|
+
let STYLE_VOCABULARY = { image: ["align", "background"], group: [], table: [] };
|
|
49
|
+
|
|
50
|
+
let FAMILIES = ["", "sans", "serif", "mono"];
|
|
51
|
+
let ALIGNS = ["left", "center", "right"];
|
|
52
|
+
let FLAGS = new Set(["bold", "italic", "underline", "strikethrough"]);
|
|
53
|
+
|
|
54
|
+
/** @type {(value: any) => boolean} */
|
|
55
|
+
let isExpr = (value) => typeof value === "string" && value.startsWith("=");
|
|
56
|
+
|
|
57
|
+
/** What a plain text field writes: the string, as typed. */
|
|
58
|
+
/** @type {(value: string) => any} */
|
|
59
|
+
let asIs = (value) => value;
|
|
60
|
+
|
|
61
|
+
/** Which of a node's keys a problem belongs to: "" for the node itself,
|
|
62
|
+
* `style.color` for one inside it, null for a problem elsewhere. */
|
|
63
|
+
/** @type {(at: string, path: string) => string | null} */
|
|
64
|
+
let subKey = (at, path) => {
|
|
65
|
+
if (at === path) return "";
|
|
66
|
+
return at.startsWith(path + ".") ? at.slice(path.length + 1) : null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The problems that sit at or under one node, mapped to the sub-key they
|
|
71
|
+
* belong to, with the exact character span when the diagnostic carries one.
|
|
72
|
+
*
|
|
73
|
+
* @type {(problems: readonly any[], path: string) => Map<string, any>}
|
|
74
|
+
*/
|
|
75
|
+
let inlineProblems = (problems, path) => {
|
|
76
|
+
let out = new Map();
|
|
77
|
+
for (let problem of problems) {
|
|
78
|
+
let key = subKey(problem.path, path);
|
|
79
|
+
if (key !== null && !out.has(key)) out.set(key, problem);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Whether a diagnostic carries offsets at all. */
|
|
85
|
+
/** @type {(d: any) => boolean} */
|
|
86
|
+
let hasSpan = (d) => !!d && d.start != null && d.end != null;
|
|
87
|
+
|
|
88
|
+
/** Whether those offsets land inside the source the panel is showing. */
|
|
89
|
+
/** @type {(problem: any) => boolean} */
|
|
90
|
+
let spanned = (problem) =>
|
|
91
|
+
hasSpan(problem.diagnostic) &&
|
|
92
|
+
problem.source != null &&
|
|
93
|
+
problem.diagnostic.end <= problem.source.length;
|
|
94
|
+
|
|
95
|
+
/** The message without its located prefix: the field it sits under already
|
|
96
|
+
* names the node, so repeating the path there is noise. Built from the
|
|
97
|
+
* problem's own `path` and `source` — the engine kept both as data precisely
|
|
98
|
+
* so no consumer has to parse a wording format back apart. */
|
|
99
|
+
/** @type {(problem: any) => string} */
|
|
100
|
+
let trimmed = ({ path, source, message }) => {
|
|
101
|
+
let prefix = (source === undefined ? path : path + " [" + source + "]") + ": ";
|
|
102
|
+
return message.startsWith(prefix) ? message.slice(prefix.length) : message;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** The message with its located span underlined, when the engine knows it. */
|
|
106
|
+
/** @type {(problem: any) => unknown} */
|
|
107
|
+
let said = (problem) => {
|
|
108
|
+
if (!spanned(problem)) return html`<p class="qe-inline-error">${problem.message}</p>`;
|
|
109
|
+
let { diagnostic: d, source: src } = problem;
|
|
110
|
+
let end = Math.max(d.end, d.start + 1);
|
|
111
|
+
return html`<p class="qe-inline-error">
|
|
112
|
+
${trimmed(problem)}<br />
|
|
113
|
+
<code>${src.slice(0, d.start)}<u>${src.slice(d.start, end)}</u>${src.slice(end)}</code>
|
|
114
|
+
</p>`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** @type {(faults: Map<string, any>, key: string) => unknown} */
|
|
118
|
+
let fault = (faults, key) => {
|
|
119
|
+
let problem = faults.get(key);
|
|
120
|
+
return problem ? said(problem) : nothing;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** A literal-or-expression style flag (`bold`, `italic`, ...) edits as a
|
|
124
|
+
* checkbox; everything else is a text field. */
|
|
125
|
+
/** @type {(kind: string, key: string) => boolean} */
|
|
126
|
+
let asFlag = (kind, key) => kind === "dual" && FLAGS.has(key.replace(/^style\./, ""));
|
|
127
|
+
|
|
128
|
+
// The closed sets a literal field offers, so an author picks rather than
|
|
129
|
+
// spells. `visible`'s literal form is a boolean — SCHEMA.md makes any other
|
|
130
|
+
// literal a definition error — which is why it is a checkbox until the fx
|
|
131
|
+
// toggle turns it into an expression.
|
|
132
|
+
/** @type {Record<string, string[]>} */
|
|
133
|
+
let CHOICES = { fit: ["natural", "width"], break: ["page"], reset: ["page"] };
|
|
134
|
+
/** The literal fields whose value is a number, never the string typed. */
|
|
135
|
+
let COUNTED = new Set(["width", "take", "columns"]);
|
|
136
|
+
|
|
137
|
+
/** A value already written as an expression edits as its source, whatever
|
|
138
|
+
* control its literal form would have had. */
|
|
139
|
+
/** @type {(kind: string, key: string, ctx: any) => unknown} */
|
|
140
|
+
let literalOrFlag = (kind, key, ctx) => {
|
|
141
|
+
if (key === "visible") return visibleControl(ctx);
|
|
142
|
+
return asFlag(kind, key) ? flagControl(ctx) : textControl(ctx);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** @type {(ctx: any) => unknown} */
|
|
146
|
+
let readonlyControl = ({ value }) => html`<input type="text" .value=${value ?? ""} disabled />`;
|
|
147
|
+
|
|
148
|
+
// Four kinds decide every control (quario-yre.7): literal-only, always a
|
|
149
|
+
// template, always an expression, and literal-or-expression. Only the fourth
|
|
150
|
+
// can already hold an expression, and then it edits as its source.
|
|
151
|
+
/** @type {Record<string, (key: string, value: any, ctx: any) => unknown>} */
|
|
152
|
+
let CONTROLS = {
|
|
153
|
+
readonly: (_key, _value, ctx) => readonlyControl(ctx),
|
|
154
|
+
literal: (key, _value, ctx) => literalControl(key, ctx),
|
|
155
|
+
template: (_key, _value, ctx) => textControl(ctx),
|
|
156
|
+
expression: (_key, _value, ctx) => textControl(ctx),
|
|
157
|
+
dual: (key, value, ctx) => (isExpr(value) ? textControl(ctx) : literalOrFlag("dual", key, ctx)),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** @type {(kind: string, key: string, value: any, ctx: any) => unknown} */
|
|
161
|
+
let fieldControl = (kind, key, value, ctx) => CONTROLS[kind](key, value, ctx);
|
|
162
|
+
|
|
163
|
+
/** The two handlers a typed control needs: `input` moves the working
|
|
164
|
+
* document, `change` ends the gesture. `read` is what the schema wants out of
|
|
165
|
+
* the DOM's string. */
|
|
166
|
+
/** @type {(edit: Edit, key: string, read: (value: string) => any) => { live: any, commit: any }} */
|
|
167
|
+
let handlers = (edit, key, read) => ({
|
|
168
|
+
live: (/** @type {any} */ event) => edit.touch(key, read(event.target.value)),
|
|
169
|
+
commit: (/** @type {any} */ event) => edit.settle(key, read(event.target.value)),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/** A count writes a number: `width: "30"` is a definition error, and the
|
|
173
|
+
* author never sees the difference between the two on screen. */
|
|
174
|
+
/** @type {(value: string) => any} */
|
|
175
|
+
let asCount = (value) => (value === "" ? "" : Number(value));
|
|
176
|
+
|
|
177
|
+
/** A number field's intermediate states — a lone `-`, a half-typed `1e` —
|
|
178
|
+
* reach the DOM as an empty value. Live-writing that would clear the key, and
|
|
179
|
+
* the re-render would then erase what was typed, so a count moves the working
|
|
180
|
+
* document only once it is one. Clearing the field still commits on blur. */
|
|
181
|
+
/** @type {(edit: Edit, key: string) => any} */
|
|
182
|
+
let liveCount = (edit, key) => (/** @type {any} */ event) => {
|
|
183
|
+
if (event.target.value === "") return;
|
|
184
|
+
edit.touch(key, Number(event.target.value));
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** The counted control both `width`-style literals and `style.size` use: the
|
|
188
|
+
* ordinary pair read as a count, with only the live half replaced. */
|
|
189
|
+
/** @type {(ctx: any, least: unknown) => unknown} */
|
|
190
|
+
let countedControl = ({ edit, key, value }, least) => html`<input
|
|
191
|
+
type="number"
|
|
192
|
+
min=${least ?? nothing}
|
|
193
|
+
.value=${value ?? ""}
|
|
194
|
+
@input=${liveCount(edit, key)}
|
|
195
|
+
@change=${handlers(edit, key, asCount).commit}
|
|
196
|
+
/>`;
|
|
197
|
+
|
|
198
|
+
/** @type {(key: string, ctx: any) => unknown} */
|
|
199
|
+
let literalControl = (key, ctx) => {
|
|
200
|
+
if (COUNTED.has(key)) return countedControl(ctx, undefined);
|
|
201
|
+
return CHOICES[key] ? choiceControl(CHOICES[key], ctx) : textControl(ctx);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* One field. `kind` narrows the control; `dual` fields carry the fx toggle
|
|
206
|
+
* that swaps the control for its expression source.
|
|
207
|
+
*/
|
|
208
|
+
/** @type {(label: string, key: string, value: any, kind: string, faults: Map<string, any>, edit: Edit) => unknown} */
|
|
209
|
+
let field = (label, key, value, kind, faults, edit) => {
|
|
210
|
+
let ctx = { key, value, edit, ...handlers(edit, key, asIs) };
|
|
211
|
+
let control = fieldControl(kind, key, value, ctx);
|
|
212
|
+
return html`<label class="qe-field">
|
|
213
|
+
<span>${label}</span>
|
|
214
|
+
<span class="qe-row">${control}${kind === "dual" ? fxButton(key, value, edit) : nothing}</span>
|
|
215
|
+
${fault(faults, key)}
|
|
216
|
+
</label>`;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// The fx toggle: only a literal-or-expression field has a literal form to
|
|
220
|
+
// swap back to, which is why a group's `by` and an image's `source` never
|
|
221
|
+
// carry one.
|
|
222
|
+
/** @type {(key: string, value: any, edit: Edit) => unknown} */
|
|
223
|
+
let fxButton = (key, value, edit) => html`<button
|
|
224
|
+
class="qe-button"
|
|
225
|
+
title="Switch between a value and an expression"
|
|
226
|
+
aria-pressed=${isExpr(value) ? "true" : "false"}
|
|
227
|
+
@click=${() => edit.write(key, isExpr(value) ? "" : "=")}
|
|
228
|
+
>
|
|
229
|
+
fx
|
|
230
|
+
</button>`;
|
|
231
|
+
|
|
232
|
+
/** @type {(ctx: any) => unknown} */
|
|
233
|
+
let flagControl = ({ value, key, edit }) => html`<input
|
|
234
|
+
type="checkbox"
|
|
235
|
+
.checked=${!!value}
|
|
236
|
+
@change=${(/** @type {any} */ event) => edit.write(key, event.target.checked ? true : "")}
|
|
237
|
+
/>`;
|
|
238
|
+
|
|
239
|
+
// `visible` is the mirror image of a style flag: an absent key means visible,
|
|
240
|
+
// so the box is checked by default and unchecking it writes the literal
|
|
241
|
+
// `false` the schema wants — clearing the key instead would make the item
|
|
242
|
+
// visible again, which is the one control that could turn it back on.
|
|
243
|
+
/** @type {(ctx: any) => unknown} */
|
|
244
|
+
let visibleControl = ({ value, key, edit }) => html`<input
|
|
245
|
+
type="checkbox"
|
|
246
|
+
.checked=${value !== false}
|
|
247
|
+
@change=${(/** @type {any} */ event) => edit.write(key, event.target.checked ? "" : false)}
|
|
248
|
+
/>`;
|
|
249
|
+
|
|
250
|
+
/** @type {(ctx: any) => unknown} */
|
|
251
|
+
let textControl = ({ value, live, commit }) =>
|
|
252
|
+
html`<input type="text" .value=${value ?? ""} @input=${live} @change=${commit} />`;
|
|
253
|
+
|
|
254
|
+
/** One of a closed set, plus the empty choice that clears the key. */
|
|
255
|
+
/** @type {(options: string[], ctx: any) => unknown} */
|
|
256
|
+
let choiceControl = (options, { value, commit }) => html`<select @change=${commit}>
|
|
257
|
+
${["", ...options].map(
|
|
258
|
+
(option) =>
|
|
259
|
+
html`<option value=${option} ?selected=${(value ?? "") === option}>
|
|
260
|
+
${option || "(none)"}
|
|
261
|
+
</option>`,
|
|
262
|
+
)}
|
|
263
|
+
</select>`;
|
|
264
|
+
|
|
265
|
+
/** @type {(ctx: any) => unknown} */
|
|
266
|
+
let colorControl = ({ value, key, edit, live, commit }) => html`<span class="qe-row">
|
|
267
|
+
<input
|
|
268
|
+
type="color"
|
|
269
|
+
.value=${typeof value === "string" && value.startsWith("#") ? value : "#000000"}
|
|
270
|
+
@input=${live}
|
|
271
|
+
@change=${commit}
|
|
272
|
+
/>
|
|
273
|
+
<button class="qe-button" title="Clear" @click=${() => edit.write(key, "")}>×</button>
|
|
274
|
+
</span>`;
|
|
275
|
+
|
|
276
|
+
// Real controls, one per style name — a colour picker, a bold toggle,
|
|
277
|
+
// alignment buttons — so nothing is a value the author has to remember.
|
|
278
|
+
// Anything unlisted falls back to a plain text field.
|
|
279
|
+
/** @type {Record<string, (ctx: any) => unknown>} */
|
|
280
|
+
let STYLE_CONTROLS = {
|
|
281
|
+
family: ({ value, commit }) => html`<select @change=${commit}>
|
|
282
|
+
${FAMILIES.map(
|
|
283
|
+
(option) =>
|
|
284
|
+
html`<option value=${option} ?selected=${(value ?? "") === option}>
|
|
285
|
+
${option || "(default)"}
|
|
286
|
+
</option>`,
|
|
287
|
+
)}
|
|
288
|
+
</select>`,
|
|
289
|
+
align: ({ value, key, edit }) => html`<span class="qe-row" role="group" aria-label="align">
|
|
290
|
+
${ALIGNS.map(
|
|
291
|
+
(option) => html`<button
|
|
292
|
+
class="qe-button"
|
|
293
|
+
aria-pressed=${value === option ? "true" : "false"}
|
|
294
|
+
@click=${() => edit.write(key, value === option ? "" : option)}
|
|
295
|
+
>
|
|
296
|
+
${option}
|
|
297
|
+
</button>`,
|
|
298
|
+
)}
|
|
299
|
+
</span>`,
|
|
300
|
+
size: (ctx) => countedControl(ctx, "1"),
|
|
301
|
+
color: colorControl,
|
|
302
|
+
background: colorControl,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/** @type {(ctx: any) => unknown} */
|
|
306
|
+
let styleControl = (ctx) => {
|
|
307
|
+
if (FLAGS.has(ctx.name)) return flagControl(ctx);
|
|
308
|
+
return (STYLE_CONTROLS[ctx.name] ?? textControl)(ctx);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** @type {(name: string, node: any, faults: Map<string, any>, edit: Edit) => unknown} */
|
|
312
|
+
let styleField = (name, node, faults, edit) => {
|
|
313
|
+
let value = node?.style?.[name];
|
|
314
|
+
let key = "style." + name;
|
|
315
|
+
// An expression-valued style edits as its source, through the same field a
|
|
316
|
+
// `visible` expression gets.
|
|
317
|
+
if (isExpr(value)) return field(name, key, value, "dual", faults, edit);
|
|
318
|
+
let ctx = { name, key, value, edit, ...handlers(edit, key, asIs) };
|
|
319
|
+
return html`<label class="qe-field">
|
|
320
|
+
<span>${name}</span>
|
|
321
|
+
<span class="qe-row">${styleControl(ctx)}${fxButton(key, value, edit)}</span>
|
|
322
|
+
${fault(faults, key)}
|
|
323
|
+
</label>`;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** Content fields per node kind — each an entry of [label, key, kind]. */
|
|
327
|
+
let CONTENT = /** @type {Record<string, [string, string][]>} */ ({
|
|
328
|
+
text: [
|
|
329
|
+
["value", "value"],
|
|
330
|
+
["visible", "visible"],
|
|
331
|
+
],
|
|
332
|
+
image: [
|
|
333
|
+
["source", "source"],
|
|
334
|
+
["alt", "alt"],
|
|
335
|
+
["fit", "fit"],
|
|
336
|
+
["visible", "visible"],
|
|
337
|
+
],
|
|
338
|
+
column: [
|
|
339
|
+
["header", "header"],
|
|
340
|
+
["value", "value"],
|
|
341
|
+
["visible", "visible"],
|
|
342
|
+
["width", "width"],
|
|
343
|
+
],
|
|
344
|
+
total: [
|
|
345
|
+
["value", "value"],
|
|
346
|
+
["visible", "visible"],
|
|
347
|
+
],
|
|
348
|
+
group: [
|
|
349
|
+
["name", "name"],
|
|
350
|
+
["by", "by"],
|
|
351
|
+
["break", "break"],
|
|
352
|
+
["reset", "reset"],
|
|
353
|
+
["take", "take"],
|
|
354
|
+
["columns", "columns"],
|
|
355
|
+
],
|
|
356
|
+
table: [],
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
/** Every rail section is a titled block; the title is the only chrome. */
|
|
360
|
+
/** @type {(title: string, body: unknown) => unknown} */
|
|
361
|
+
let section = (title, body) => html`<div class="qe-section">
|
|
362
|
+
<h3>${title}</h3>
|
|
363
|
+
${body}
|
|
364
|
+
</div>`;
|
|
365
|
+
|
|
366
|
+
/** A column's shorthand string header edits as its own template; the widened
|
|
367
|
+
* object form edits through `.value`. Widening is the element's write path. */
|
|
368
|
+
/** @type {(header: any) => any} */
|
|
369
|
+
let headerText = (header) => (typeof header === "string" ? header : header?.value);
|
|
370
|
+
|
|
371
|
+
/** @type {(node: any, key: string) => any} */
|
|
372
|
+
let readKey = (node, key) => (key === "header" ? headerText(node?.header) : node?.[key]);
|
|
373
|
+
|
|
374
|
+
/** The style vocabulary a node kind actually has: an image shows its two
|
|
375
|
+
* declarations rather than nine controls with seven disabled. */
|
|
376
|
+
/** @type {(kind: string) => string[]} */
|
|
377
|
+
let stylesFor = (kind) => STYLE_VOCABULARY[kind] ?? STYLES;
|
|
378
|
+
|
|
379
|
+
/** @type {(kind: string, node: any, faults: Map<string, any>, edit: Edit) => unknown} */
|
|
380
|
+
let styleFields = (kind, node, faults, edit) => {
|
|
381
|
+
let names = stylesFor(kind);
|
|
382
|
+
if (!names.length) return nothing;
|
|
383
|
+
return html`<h3>Style</h3>
|
|
384
|
+
${names.map((name) => styleField(name, node, faults, edit))}`;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/** The table verbs, on the table and on any column of it. */
|
|
388
|
+
/** @type {(kind: string, act: any) => unknown} */
|
|
389
|
+
let columnVerbs = (kind, act) => {
|
|
390
|
+
if (kind !== "column" && kind !== "table") return nothing;
|
|
391
|
+
return html`<div class="qe-row">
|
|
392
|
+
<button class="qe-button" @click=${act.insertColumn}>Add column</button>
|
|
393
|
+
${
|
|
394
|
+
kind === "column"
|
|
395
|
+
? html`<button class="qe-button" @click=${act.duplicateColumn}>Duplicate</button>
|
|
396
|
+
<button class="qe-button" @click=${act.removeColumn}>Remove</button>`
|
|
397
|
+
: nothing
|
|
398
|
+
}
|
|
399
|
+
</div>`;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The Properties section for the selected node, or its empty prompting form.
|
|
404
|
+
*
|
|
405
|
+
* @param {any} schema
|
|
406
|
+
* @param {string | null} selected
|
|
407
|
+
* @param {readonly any[]} problems
|
|
408
|
+
* @param {{ edit: Edit,
|
|
409
|
+
* insertColumn: () => void, removeColumn: () => void, duplicateColumn: () => void }} act
|
|
410
|
+
*/
|
|
411
|
+
export let propertiesSection = (schema, selected, problems, act) => {
|
|
412
|
+
let kind = selected && nodeKindAt(schema, selected);
|
|
413
|
+
if (!selected || !kind)
|
|
414
|
+
return section("Properties", html`<p>Select something in the preview.</p>`);
|
|
415
|
+
let node = resolve(schema, selected);
|
|
416
|
+
let faults = inlineProblems(problems, selected);
|
|
417
|
+
return section(
|
|
418
|
+
"Properties",
|
|
419
|
+
html`<p><code class="qe-crumb">${selected}</code></p>
|
|
420
|
+
${columnVerbs(kind, act)}
|
|
421
|
+
${CONTENT[kind].map(([label, key]) =>
|
|
422
|
+
field(label, key, readKey(node, key), fieldKind(key), faults, act.edit),
|
|
423
|
+
)}
|
|
424
|
+
${styleFields(kind, node, faults, act.edit)}`,
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/** One toolbar button in a group's row. */
|
|
429
|
+
/** @type {(title: string, glyph: string, click: () => void, opts?: any) => unknown} */
|
|
430
|
+
let rowButton = (title, glyph, click, opts = {}) => html`<button
|
|
431
|
+
class="qe-button"
|
|
432
|
+
title=${title}
|
|
433
|
+
?disabled=${!!opts.disabled}
|
|
434
|
+
aria-pressed=${opts.pressed ?? nothing}
|
|
435
|
+
@click=${click}
|
|
436
|
+
>
|
|
437
|
+
${glyph}
|
|
438
|
+
</button>`;
|
|
439
|
+
|
|
440
|
+
/** @type {Record<string, string>} */
|
|
441
|
+
let GLYPH = { header: "H", footer: "F" };
|
|
442
|
+
|
|
443
|
+
/** A group's header or footer band: one verb that adds it, one that removes
|
|
444
|
+
* it whole. There is no such thing as a hidden band, so the editor presents
|
|
445
|
+
* none — an off-then-on toggle would discard the band's contents. */
|
|
446
|
+
/** @type {(group: any, i: number, which: "header" | "footer", act: any) => unknown} */
|
|
447
|
+
let bandButton = (group, i, which, act) => {
|
|
448
|
+
let band = group?.[which];
|
|
449
|
+
let title = (band ? "Remove the " : "Add a ") + which + " band";
|
|
450
|
+
let pressed = band ? "true" : "false";
|
|
451
|
+
return rowButton(title, GLYPH[which], () => act.band(i, which, band == null), { pressed });
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/** @type {(group: any, i: number, count: number, act: any) => unknown} */
|
|
455
|
+
let groupRow = (group, i, count, act) => html`<div class="qe-group-row">
|
|
456
|
+
<button class="qe-button qe-name" @click=${() => act.select("groups[" + i + "]")}>
|
|
457
|
+
${group?.name}
|
|
458
|
+
</button>
|
|
459
|
+
${rowButton("Move up", "↑", () => act.move(i, i - 1), { disabled: i === 0 })}
|
|
460
|
+
${rowButton("Move down", "↓", () => act.move(i, i + 2), { disabled: i === count - 1 })}
|
|
461
|
+
${bandButton(group, i, "header", act)} ${bandButton(group, i, "footer", act)}
|
|
462
|
+
${rowButton("Remove the group, keeping its items", "⇱", () => act.remove(i, "splice"))}
|
|
463
|
+
${rowButton("Remove the group and its bands", "×", () => act.remove(i, "discard"))}
|
|
464
|
+
</div>`;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The Groups section: the docked list no single part of the page shows. A
|
|
468
|
+
* group spans its whole instance, so its controls cannot sit on any one part
|
|
469
|
+
* of the page — this is their one home.
|
|
470
|
+
*
|
|
471
|
+
* @param {any} schema
|
|
472
|
+
* @param {{ select: (path: string) => void, move: (at: number, to: number) => void,
|
|
473
|
+
* remove: (at: number, mode: "discard" | "splice") => void,
|
|
474
|
+
* band: (at: number, which: "header" | "footer", add: boolean) => void }} act
|
|
475
|
+
*/
|
|
476
|
+
export let groupsSection = (schema, act) => {
|
|
477
|
+
let groups = Array.isArray(schema?.groups) ? schema.groups : [];
|
|
478
|
+
return section(
|
|
479
|
+
"Groups",
|
|
480
|
+
html`${groups.length ? nothing : html`<p>No groups.</p>`}
|
|
481
|
+
${groups.map((/** @type {any} */ group, /** @type {number} */ i) =>
|
|
482
|
+
groupRow(group, i, groups.length, act),
|
|
483
|
+
)}`,
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* The Problems section: every problem in the document; clicking one selects
|
|
489
|
+
* the node it belongs to.
|
|
490
|
+
*
|
|
491
|
+
* @param {readonly any[]} problems
|
|
492
|
+
* @param {{ select: (path: string) => void }} act
|
|
493
|
+
*/
|
|
494
|
+
export let problemsSection = (problems, act) =>
|
|
495
|
+
section(
|
|
496
|
+
"Problems",
|
|
497
|
+
html`${problems.length ? nothing : html`<p>None.</p>`}
|
|
498
|
+
${problems.map(
|
|
499
|
+
(/** @type {any} */ problem) => html`<button
|
|
500
|
+
class="qe-problem"
|
|
501
|
+
@click=${() => act.select(problem.path)}
|
|
502
|
+
>
|
|
503
|
+
${problem.message}
|
|
504
|
+
</button>`,
|
|
505
|
+
)}`,
|
|
506
|
+
);
|
package/lib/register.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-line define, as its own entry so the main module stays
|
|
3
|
+
* side-effect-free: importing `@quario/editor` gives you the class and
|
|
4
|
+
* defines nothing, importing `@quario/editor/register` gives the tag to
|
|
5
|
+
* hosts that want the platform default. A host that wants its own tag calls
|
|
6
|
+
* `customElements.define` itself (docs/adr/0005-the-surfaces-are-custom-elements.md).
|
|
7
|
+
*/
|
|
8
|
+
import { QuarioEditor, TAG } from "./index.js";
|
|
9
|
+
|
|
10
|
+
customElements.define(TAG, QuarioEditor);
|