@seamlet/astro 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -17
- package/components/RichLine.astro +37 -0
- package/components/RichText.astro +42 -0
- package/dist/bridge.d.ts +3 -3
- package/dist/bridge.js +263 -13
- package/dist/chunk-27XIZEVS.js +705 -0
- package/dist/{chunk-GLXC5OLC.js → chunk-ZUJEM3P7.js} +127 -0
- package/dist/{content-C_WpiWBm.d.ts → content-6k-E7NWf.d.ts} +48 -4
- package/dist/{messages-CBf5diaN.d.ts → messages-Dx0jNy3N.d.ts} +76 -4
- package/dist/{parse-content-9RIWhb6z.d.ts → parse-content-C9ldnmvj.d.ts} +1 -1
- package/dist/protocol.d.ts +16 -4
- package/dist/protocol.js +26 -4
- package/dist/schema.d.ts +4 -4
- package/dist/schema.js +13 -1
- package/dist/site.d.ts +1 -1
- package/dist/site.js +1 -1
- package/package.json +5 -3
- package/dist/chunk-XCBYYKWR.js +0 -160
package/README.md
CHANGED
|
@@ -78,22 +78,23 @@ The site's pages take their published content with `site.loadPublishedContent()`
|
|
|
78
78
|
|
|
79
79
|
## Fields
|
|
80
80
|
|
|
81
|
-
| Builder
|
|
82
|
-
|
|
|
83
|
-
| `text({ label, multiline?, maxLength? })`
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
81
|
+
| Builder | Holds | Empty |
|
|
82
|
+
| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------- |
|
|
83
|
+
| `text({ label, multiline?, maxLength? })` | a string | `""` |
|
|
84
|
+
| `richText({ label, inline?, maxLength? })` | paragraphs and lists of pieces, bold, italic or links (below) | `[]` |
|
|
85
|
+
| `number({ label, min?, max? })` | a number | `null` |
|
|
86
|
+
| `date({ label })` | `YYYY-MM-DD` | `null` |
|
|
87
|
+
| `time({ label })` | `HH:MM`, 00:00 to 23:59 | `null` |
|
|
88
|
+
| `image({ label })` | `{ src, alt }` | `null` |
|
|
89
|
+
| `link({ label })` | `{ text, href }`: a page of the site, another site, a phone or an email | `null` |
|
|
90
|
+
| `url({ label })` | a web address without a text: a booking widget, a map | `null` |
|
|
91
|
+
| `toggle({ label })` | `true` or `false` | `false` |
|
|
92
|
+
| `choice({ label, options })` | the `value` of one option | `null` |
|
|
93
|
+
| `choices({ label, options })` | the `value`s of any number of options | `[]` |
|
|
94
|
+
| `reference({ label, collection })` | the `id` of a record of a collection of the schema | `null` |
|
|
95
|
+
| `references({ label, collection })` | the `id`s of any number of such records | `[]` |
|
|
96
|
+
| `group({ label, fields })` | named fields shown together | each field empty |
|
|
97
|
+
| `collection({ label, item, itemTitle? })` | a list of records of the same fields | `[]` |
|
|
97
98
|
|
|
98
99
|
Options are `{ value, label }`: the site reads `value`, the editor shows `label`, so a label can change without touching the site. The content has their exact type:
|
|
99
100
|
|
|
@@ -141,6 +142,30 @@ export default defineSeamletSite({
|
|
|
141
142
|
});
|
|
142
143
|
```
|
|
143
144
|
|
|
145
|
+
A text with a link in a sentence, bold words or a list is one `richText` field, not a text cut around the link. Its value is data, never HTML: paragraphs (`{ kind: "paragraph", spans }`) and lists (`{ kind: "list", items }`) of pieces `{ text, bold?, italic?, href? }`, where `href` leads where a `link` may. `inline` keeps it one paragraph without lists, for a text inside an element that holds one line. A saved plain string is read as paragraphs, so a `text` field turned into `richText` keeps its words. Render it with the component of the package, which puts out paragraphs and lists (or, with `inline`, the words alone) and never raw HTML; style its elements from around it:
|
|
146
|
+
|
|
147
|
+
```astro
|
|
148
|
+
---
|
|
149
|
+
import RichText from "@seamlet/astro/components/RichText.astro";
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
<div class="about" {...edit("about")}>
|
|
153
|
+
<RichText value={content.about} />
|
|
154
|
+
</div>
|
|
155
|
+
<p {...edit("note")}>
|
|
156
|
+
<RichText value={content.note} inline />
|
|
157
|
+
</p>
|
|
158
|
+
<style>
|
|
159
|
+
.about :global(a) {
|
|
160
|
+
color: inherit;
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
When the marked element holds the component alone, the editor types the text right on the page: a person clicks a paragraph and types, marks the selected words bold or italic (⌘B, ⌘I or the toolbar beside the selection) and puts a link on them. The element is then rendered with the same elements the component puts out, so style them from around it, never by classes of your own on them.
|
|
166
|
+
|
|
167
|
+
`plainTextOf` gives its words without marks, for a page title or a description.
|
|
168
|
+
|
|
144
169
|
## Presets
|
|
145
170
|
|
|
146
171
|
Sets of fields almost every site has, edited the same way everywhere; they are plain groups and collections, rendered as the site likes:
|
|
@@ -193,7 +218,7 @@ What was opened stays open while the page is rendered anew after every edit.
|
|
|
193
218
|
## What the integration adds
|
|
194
219
|
|
|
195
220
|
- `/preview` and `/preview/<page>`: the preview the CMS's editor frames. `GET` shows the published content; `POST` with the content as JSON shows that content without saving it, which is how the editor shows every edit at once. Only the CMS may frame it (`Content-Security-Policy: frame-ancestors`), and it is never indexed or cached.
|
|
196
|
-
- The bridge on the preview: it tells the editor where the marked elements are and what a person does on the page, and types texts in place.
|
|
221
|
+
- The bridge on the preview: it tells the editor where the marked elements are and what a person does on the page, and types texts in place, formatted ones too.
|
|
197
222
|
- `/seamlet-photos/<address>`: the site's own photo at an address its content gives it, found by `photos` of `defineSeamletSite`, for the CMS's editor.
|
|
198
223
|
- `/media/<site>/<file>`: the photos uploaded in the CMS, taken from it on request; a static build copies every uploaded photo its built pages refer to next to them, so a static host serves them too.
|
|
199
224
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The pieces of one line of formatted text with their marks.
|
|
3
|
+
import { linkTargetOf, type RichLine } from "@seamlet/astro/schema";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
readonly spans: RichLine;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const LINE_BREAK = "\n";
|
|
10
|
+
|
|
11
|
+
const { spans } = Astro.props;
|
|
12
|
+
const pieces = spans.map((span) => ({
|
|
13
|
+
...span,
|
|
14
|
+
lines: span.text.split(LINE_BREAK),
|
|
15
|
+
// Content is checked when it is saved; a site never renders an address a link may not have.
|
|
16
|
+
safeHref:
|
|
17
|
+
span.href !== undefined && linkTargetOf(span.href) !== undefined
|
|
18
|
+
? span.href
|
|
19
|
+
: undefined,
|
|
20
|
+
}));
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
{pieces.map((piece) => {
|
|
24
|
+
const words = piece.lines.map((line, index) => (
|
|
25
|
+
<>
|
|
26
|
+
{index > 0 && <br />}
|
|
27
|
+
{line}
|
|
28
|
+
</>
|
|
29
|
+
));
|
|
30
|
+
const italic = piece.italic === true ? <em>{words}</em> : words;
|
|
31
|
+
const bold = piece.bold === true ? <strong>{italic}</strong> : italic;
|
|
32
|
+
return piece.safeHref === undefined ? (
|
|
33
|
+
bold
|
|
34
|
+
) : (
|
|
35
|
+
<a href={piece.safeHref}>{bold}</a>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Formatted text of the CMS as the site's own elements: paragraphs and lists, or, with `inline`,
|
|
3
|
+
// the words alone for an element that holds one line (`<p>`, `<h2>`). Nothing is rendered as raw
|
|
4
|
+
// HTML, and a link leads only where linkTargetOf allows. The site styles the elements from
|
|
5
|
+
// around it: `.about :global(a)`.
|
|
6
|
+
import type { RichText } from "@seamlet/astro/schema";
|
|
7
|
+
import RichLine from "./RichLine.astro";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
readonly value: RichText;
|
|
11
|
+
/** Only the words, without paragraphs and lists; its lines are joined with line breaks. */
|
|
12
|
+
readonly inline?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { value, inline = false } = Astro.props;
|
|
16
|
+
const lines = value.flatMap((block) =>
|
|
17
|
+
block.kind === "paragraph" ? [block.spans] : block.items,
|
|
18
|
+
);
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
{inline
|
|
22
|
+
? lines.map((line, index) => (
|
|
23
|
+
<>
|
|
24
|
+
{index > 0 && <br />}
|
|
25
|
+
<RichLine spans={line} />
|
|
26
|
+
</>
|
|
27
|
+
))
|
|
28
|
+
: value.map((block) =>
|
|
29
|
+
block.kind === "paragraph" ? (
|
|
30
|
+
<p>
|
|
31
|
+
<RichLine spans={block.spans} />
|
|
32
|
+
</p>
|
|
33
|
+
) : (
|
|
34
|
+
<ul>
|
|
35
|
+
{block.items.map((item) => (
|
|
36
|
+
<li>
|
|
37
|
+
<RichLine spans={item} />
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
),
|
|
42
|
+
)}
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { F as Fields } from './content-
|
|
2
|
-
import { P as PageMessage, E as EditorMessage } from './messages-
|
|
3
|
-
import './parse-content-
|
|
1
|
+
import { F as Fields } from './content-6k-E7NWf.js';
|
|
2
|
+
import { P as PageMessage, E as EditorMessage } from './messages-Dx0jNy3N.js';
|
|
3
|
+
import './parse-content-C9ldnmvj.js';
|
|
4
4
|
|
|
5
5
|
interface BridgeOptions {
|
|
6
6
|
/** The origin of the CMS whose editor may frame the page, such as `https://cms.example.com`. */
|
package/dist/bridge.js
CHANGED
|
@@ -2,16 +2,31 @@ import {
|
|
|
2
2
|
PROTOCOL_VERSION,
|
|
3
3
|
carriesFiles,
|
|
4
4
|
closestMarkedPath,
|
|
5
|
+
formatsAt,
|
|
5
6
|
imageFieldAt,
|
|
7
|
+
insertPlainText,
|
|
8
|
+
isCollapsed,
|
|
9
|
+
linkSelectionAt,
|
|
10
|
+
rangeOf,
|
|
6
11
|
readEditorMessage,
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
readRichTextFrom,
|
|
13
|
+
readSelectionFrom,
|
|
14
|
+
renderInlineRichText,
|
|
15
|
+
renderRichText,
|
|
16
|
+
selectIn,
|
|
17
|
+
setLink,
|
|
18
|
+
shortcutOf,
|
|
19
|
+
toggleList,
|
|
20
|
+
toggleMark
|
|
21
|
+
} from "./chunk-27XIZEVS.js";
|
|
9
22
|
import {
|
|
10
23
|
FIELD_PATH_ATTRIBUTE,
|
|
11
24
|
REVEALED_ATTRIBUTE,
|
|
12
25
|
REVEAL_ATTRIBUTE,
|
|
13
|
-
REVEAL_EVENT
|
|
14
|
-
|
|
26
|
+
REVEAL_EVENT,
|
|
27
|
+
asOneParagraph,
|
|
28
|
+
readRichText
|
|
29
|
+
} from "./chunk-ZUJEM3P7.js";
|
|
15
30
|
|
|
16
31
|
// src/bridge/page-markers.ts
|
|
17
32
|
var MARKER_SELECTOR = `[${FIELD_PATH_ATTRIBUTE}]`;
|
|
@@ -28,6 +43,10 @@ function linkAround(target) {
|
|
|
28
43
|
const isLink = isHtmlElement(element) && typeof href === "string";
|
|
29
44
|
return isLink ? { element, href } : void 0;
|
|
30
45
|
}
|
|
46
|
+
function isInTypedText(target) {
|
|
47
|
+
const editable = asElement(target)?.closest("[contenteditable]");
|
|
48
|
+
return editable !== null && editable !== void 0;
|
|
49
|
+
}
|
|
31
50
|
function findMarkedElement(document2, path) {
|
|
32
51
|
const element = document2.querySelector(
|
|
33
52
|
`[${FIELD_PATH_ATTRIBUTE}="${CSS.escape(path)}"]`
|
|
@@ -170,7 +189,7 @@ var DRAG_GONE_AFTER_MS = 150;
|
|
|
170
189
|
function listenToPage(document2, listeners) {
|
|
171
190
|
const view = document2.defaultView;
|
|
172
191
|
const hover = (event) => {
|
|
173
|
-
const link =
|
|
192
|
+
const link = linkToFollow(event.target);
|
|
174
193
|
listeners.onHover(
|
|
175
194
|
link === void 0 ? markedElementAround(event.target)?.path : void 0
|
|
176
195
|
);
|
|
@@ -182,7 +201,7 @@ function listenToPage(document2, listeners) {
|
|
|
182
201
|
};
|
|
183
202
|
const click = (event) => {
|
|
184
203
|
event.preventDefault();
|
|
185
|
-
const link =
|
|
204
|
+
const link = linkToFollow(event.target);
|
|
186
205
|
if (link !== void 0) {
|
|
187
206
|
listeners.onLinkClick(link);
|
|
188
207
|
return;
|
|
@@ -256,6 +275,9 @@ function listenToPage(document2, listeners) {
|
|
|
256
275
|
view?.removeEventListener("resize", listeners.onLayoutChange);
|
|
257
276
|
};
|
|
258
277
|
}
|
|
278
|
+
function linkToFollow(target) {
|
|
279
|
+
return isInTypedText(target) ? void 0 : nearerLink(target);
|
|
280
|
+
}
|
|
259
281
|
function nearerLink(target) {
|
|
260
282
|
const link = linkAround(target);
|
|
261
283
|
const marked = markedElementAround(target);
|
|
@@ -326,7 +348,7 @@ function createLayoutReports(document2, report) {
|
|
|
326
348
|
// src/bridge/page-rendering.ts
|
|
327
349
|
function createPageRenderer(options) {
|
|
328
350
|
let request;
|
|
329
|
-
const
|
|
351
|
+
const render2 = (content) => {
|
|
330
352
|
request?.abort();
|
|
331
353
|
const currentRequest = new AbortController();
|
|
332
354
|
request = currentRequest;
|
|
@@ -344,7 +366,7 @@ function createPageRenderer(options) {
|
|
|
344
366
|
});
|
|
345
367
|
};
|
|
346
368
|
return {
|
|
347
|
-
render,
|
|
369
|
+
render: render2,
|
|
348
370
|
stop: () => {
|
|
349
371
|
request?.abort();
|
|
350
372
|
}
|
|
@@ -501,12 +523,202 @@ function caretRangeAt(document2, point) {
|
|
|
501
523
|
return lookup.caretRangeFromPoint?.(point.x, point.y) ?? void 0;
|
|
502
524
|
}
|
|
503
525
|
|
|
526
|
+
// src/bridge/rich-text-editing.ts
|
|
527
|
+
var EDITABLE = "true";
|
|
528
|
+
var PASTED_LINE_BREAKS = /\s*\n\s*/gu;
|
|
529
|
+
var SPACE = " ";
|
|
530
|
+
function canEditRichTextInPlace(element, value, isInline) {
|
|
531
|
+
if (hasMarkedDescendants(element)) return false;
|
|
532
|
+
const normalized = readRichText(value) ?? [];
|
|
533
|
+
const expected = isInline ? asOneParagraph(normalized) : normalized;
|
|
534
|
+
const shown = readTextOf(element, isInline);
|
|
535
|
+
return JSON.stringify(shown) === JSON.stringify(expected);
|
|
536
|
+
}
|
|
537
|
+
function startRichTextEditing(element, options) {
|
|
538
|
+
const document2 = element.ownerDocument;
|
|
539
|
+
const view = document2.defaultView;
|
|
540
|
+
let lastSelection;
|
|
541
|
+
let reportFrame;
|
|
542
|
+
let blurTimer;
|
|
543
|
+
const readText = () => readTextOf(element, options.isInline);
|
|
544
|
+
const liveSelection = () => isSelectionIn(element) ? readSelectionFrom(element) : void 0;
|
|
545
|
+
const reportSelection = () => {
|
|
546
|
+
reportFrame = void 0;
|
|
547
|
+
options.onSelect(measureSelection(element, readText(), lastSelection));
|
|
548
|
+
};
|
|
549
|
+
const scheduleReport = () => {
|
|
550
|
+
if (reportFrame !== void 0) return;
|
|
551
|
+
reportFrame = view?.requestAnimationFrame(reportSelection);
|
|
552
|
+
};
|
|
553
|
+
const followSelection = () => {
|
|
554
|
+
const selection = document2.hasFocus() ? liveSelection() : void 0;
|
|
555
|
+
if (selection === void 0) return;
|
|
556
|
+
lastSelection = selection;
|
|
557
|
+
scheduleReport();
|
|
558
|
+
};
|
|
559
|
+
const reportInput = () => {
|
|
560
|
+
options.onInput(readText());
|
|
561
|
+
followSelection();
|
|
562
|
+
};
|
|
563
|
+
const change = (changed, selection) => {
|
|
564
|
+
render(element, changed, options.isInline);
|
|
565
|
+
lastSelection = selection;
|
|
566
|
+
if (document2.hasFocus()) selectIn(element, selection);
|
|
567
|
+
options.onInput(changed);
|
|
568
|
+
scheduleReport();
|
|
569
|
+
};
|
|
570
|
+
const format = (requested) => {
|
|
571
|
+
const selection = liveSelection() ?? lastSelection;
|
|
572
|
+
const isListInLine = requested === "list" && options.isInline;
|
|
573
|
+
if (selection === void 0 || isListInLine) return;
|
|
574
|
+
const current = readText();
|
|
575
|
+
const changed = requested === "list" ? toggleList(current, selection) : toggleMark(current, selection, requested);
|
|
576
|
+
change(changed, selection);
|
|
577
|
+
};
|
|
578
|
+
const linkTo = (href) => {
|
|
579
|
+
const selection = lastSelection;
|
|
580
|
+
if (selection === void 0) return;
|
|
581
|
+
const current = readText();
|
|
582
|
+
const target = isCollapsed(selection) ? linkSelectionAt(current, selection.start) : selection;
|
|
583
|
+
if (target === void 0) return;
|
|
584
|
+
change(setLink(current, target, href), selection);
|
|
585
|
+
};
|
|
586
|
+
const pressKey = (event) => {
|
|
587
|
+
const isEnd = event.key === "Enter" && options.isInline && !event.shiftKey;
|
|
588
|
+
const shortcut = formatShortcutOf(event);
|
|
589
|
+
if (event.key === "Escape") {
|
|
590
|
+
event.preventDefault();
|
|
591
|
+
end("reverted");
|
|
592
|
+
} else if (isEnd) {
|
|
593
|
+
event.preventDefault();
|
|
594
|
+
end("kept");
|
|
595
|
+
} else if (shortcut !== void 0) {
|
|
596
|
+
event.preventDefault();
|
|
597
|
+
if (shortcut !== "none") format(shortcut);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
const pastePlainText = (event) => {
|
|
601
|
+
event.preventDefault();
|
|
602
|
+
const pasted = event.clipboardData?.getData("text/plain") ?? "";
|
|
603
|
+
const words = options.isInline ? pasted.replace(PASTED_LINE_BREAKS, SPACE) : pasted;
|
|
604
|
+
insertPlainText(element, words);
|
|
605
|
+
reportInput();
|
|
606
|
+
};
|
|
607
|
+
const refuseDrop = (event) => {
|
|
608
|
+
event.preventDefault();
|
|
609
|
+
};
|
|
610
|
+
const endOnBlur = () => {
|
|
611
|
+
clearTimeout(blurTimer);
|
|
612
|
+
blurTimer = setTimeout(() => {
|
|
613
|
+
const isFocusOnPage = document2.hasFocus();
|
|
614
|
+
const isFocusInText = document2.activeElement === element;
|
|
615
|
+
if (isFocusOnPage && !isFocusInText) end("kept");
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
const stopListening = () => {
|
|
619
|
+
clearTimeout(blurTimer);
|
|
620
|
+
if (reportFrame !== void 0) view?.cancelAnimationFrame(reportFrame);
|
|
621
|
+
element.removeEventListener("input", reportInput);
|
|
622
|
+
element.removeEventListener("keydown", pressKey);
|
|
623
|
+
element.removeEventListener("paste", pastePlainText);
|
|
624
|
+
element.removeEventListener("drop", refuseDrop);
|
|
625
|
+
element.removeEventListener("blur", endOnBlur);
|
|
626
|
+
document2.removeEventListener("selectionchange", followSelection);
|
|
627
|
+
document2.removeEventListener("scroll", scheduleReport, true);
|
|
628
|
+
view?.removeEventListener("resize", scheduleReport);
|
|
629
|
+
element.removeAttribute("contenteditable");
|
|
630
|
+
};
|
|
631
|
+
let hasEnded = false;
|
|
632
|
+
const end = (outcome) => {
|
|
633
|
+
if (hasEnded) return;
|
|
634
|
+
hasEnded = true;
|
|
635
|
+
stopListening();
|
|
636
|
+
options.onSelect(void 0);
|
|
637
|
+
options.onEnd(outcome);
|
|
638
|
+
};
|
|
639
|
+
element.setAttribute("contenteditable", EDITABLE);
|
|
640
|
+
element.addEventListener("input", reportInput);
|
|
641
|
+
element.addEventListener("keydown", pressKey);
|
|
642
|
+
element.addEventListener("paste", pastePlainText);
|
|
643
|
+
element.addEventListener("drop", refuseDrop);
|
|
644
|
+
element.addEventListener("blur", endOnBlur);
|
|
645
|
+
document2.addEventListener("selectionchange", followSelection);
|
|
646
|
+
document2.addEventListener("scroll", scheduleReport, true);
|
|
647
|
+
view?.addEventListener("resize", scheduleReport);
|
|
648
|
+
element.focus();
|
|
649
|
+
return {
|
|
650
|
+
format,
|
|
651
|
+
setLink: linkTo,
|
|
652
|
+
stop: () => {
|
|
653
|
+
end("kept");
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function readTextOf(element, isInline) {
|
|
658
|
+
const read = readRichTextFrom(element);
|
|
659
|
+
return isInline ? asOneParagraph(read) : read;
|
|
660
|
+
}
|
|
661
|
+
function render(element, text, isInline) {
|
|
662
|
+
if (isInline) renderInlineRichText(element, text);
|
|
663
|
+
else renderRichText(element, text);
|
|
664
|
+
}
|
|
665
|
+
function measureSelection(element, text, selection) {
|
|
666
|
+
if (selection === void 0) return void 0;
|
|
667
|
+
const range = isSelectionIn(element) ? element.ownerDocument.getSelection()?.getRangeAt(0) : rangeOf(element, selection);
|
|
668
|
+
const box = (range === void 0 ? void 0 : boxOfRange(range)) ?? boxOf(element);
|
|
669
|
+
if (box === void 0) return void 0;
|
|
670
|
+
return {
|
|
671
|
+
box,
|
|
672
|
+
isCollapsed: isCollapsed(selection),
|
|
673
|
+
formats: formatsAt(text, selection)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function boxOfRange(range) {
|
|
677
|
+
const whole = range.getBoundingClientRect();
|
|
678
|
+
const rectangle = whole.width > 0 || whole.height > 0 ? whole : range.getClientRects()[0];
|
|
679
|
+
if (rectangle === void 0) return void 0;
|
|
680
|
+
const isShown = rectangle.width > 0 || rectangle.height > 0;
|
|
681
|
+
if (!isShown) return void 0;
|
|
682
|
+
return {
|
|
683
|
+
top: rectangle.top,
|
|
684
|
+
left: rectangle.left,
|
|
685
|
+
width: rectangle.width,
|
|
686
|
+
height: rectangle.height
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function isSelectionIn(element) {
|
|
690
|
+
const selection = element.ownerDocument.getSelection();
|
|
691
|
+
const anchor = selection?.anchorNode ?? null;
|
|
692
|
+
const hasRange = (selection?.rangeCount ?? 0) > 0;
|
|
693
|
+
return hasRange && anchor !== null && element.contains(anchor);
|
|
694
|
+
}
|
|
695
|
+
function formatShortcutOf(event) {
|
|
696
|
+
const hasCommandKey = event.metaKey || event.ctrlKey;
|
|
697
|
+
if (!hasCommandKey || event.altKey || event.shiftKey) return void 0;
|
|
698
|
+
switch (event.code) {
|
|
699
|
+
case "KeyB":
|
|
700
|
+
return "bold";
|
|
701
|
+
case "KeyI":
|
|
702
|
+
return "italic";
|
|
703
|
+
case "KeyU":
|
|
704
|
+
return "none";
|
|
705
|
+
default:
|
|
706
|
+
return void 0;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
504
710
|
// src/bridge/page-typing.ts
|
|
505
711
|
function createPageTyping(options) {
|
|
506
712
|
let stopEditing;
|
|
713
|
+
let richEditing;
|
|
507
714
|
const stop = () => {
|
|
508
715
|
stopEditing?.();
|
|
509
716
|
};
|
|
717
|
+
const ended = (path) => (outcome) => {
|
|
718
|
+
stopEditing = void 0;
|
|
719
|
+
richEditing = void 0;
|
|
720
|
+
options.send({ type: "typing-ended", path, outcome });
|
|
721
|
+
};
|
|
510
722
|
const start = (request) => {
|
|
511
723
|
stop();
|
|
512
724
|
const { path } = request;
|
|
@@ -523,14 +735,43 @@ function createPageTyping(options) {
|
|
|
523
735
|
options.send({ type: "typed", path, text });
|
|
524
736
|
options.onInput();
|
|
525
737
|
},
|
|
526
|
-
onEnd: (
|
|
527
|
-
stopEditing = void 0;
|
|
528
|
-
options.send({ type: "typing-ended", path, outcome });
|
|
529
|
-
}
|
|
738
|
+
onEnd: ended(path)
|
|
530
739
|
});
|
|
531
740
|
placeCaretAt(element, request.point);
|
|
532
741
|
};
|
|
533
|
-
|
|
742
|
+
const startRich = (request) => {
|
|
743
|
+
stop();
|
|
744
|
+
const { path, isInline } = request;
|
|
745
|
+
const element = findMarkedElement(options.document, path);
|
|
746
|
+
const canType = element !== void 0 && canEditRichTextInPlace(element, request.text, isInline);
|
|
747
|
+
if (!canType) {
|
|
748
|
+
options.send({ type: "typing-refused", path });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
options.send({ type: "typing-started", path });
|
|
752
|
+
const editing = startRichTextEditing(element, {
|
|
753
|
+
isInline,
|
|
754
|
+
onInput: (text) => {
|
|
755
|
+
options.send({ type: "rich-typed", path, text });
|
|
756
|
+
options.onInput();
|
|
757
|
+
},
|
|
758
|
+
onSelect: (selection) => {
|
|
759
|
+
options.send({ type: "typed-selection", path, selection });
|
|
760
|
+
},
|
|
761
|
+
onEnd: ended(path)
|
|
762
|
+
});
|
|
763
|
+
richEditing = editing;
|
|
764
|
+
stopEditing = editing.stop;
|
|
765
|
+
placeCaretAt(element, request.point);
|
|
766
|
+
};
|
|
767
|
+
return {
|
|
768
|
+
isTyping: () => stopEditing !== void 0,
|
|
769
|
+
start,
|
|
770
|
+
startRich,
|
|
771
|
+
format: (format) => richEditing?.format(format),
|
|
772
|
+
setLink: (href) => richEditing?.setLink(href),
|
|
773
|
+
stop
|
|
774
|
+
};
|
|
534
775
|
}
|
|
535
776
|
|
|
536
777
|
// src/bridge/connect-to-editor.ts
|
|
@@ -624,6 +865,15 @@ function connectPage(document2, schema, send) {
|
|
|
624
865
|
case "start-typing":
|
|
625
866
|
typing.start(message);
|
|
626
867
|
break;
|
|
868
|
+
case "start-rich-typing":
|
|
869
|
+
typing.startRich(message);
|
|
870
|
+
break;
|
|
871
|
+
case "format-typed":
|
|
872
|
+
typing.format(message.format);
|
|
873
|
+
break;
|
|
874
|
+
case "link-typed":
|
|
875
|
+
typing.setLink(message.href);
|
|
876
|
+
break;
|
|
627
877
|
case "stop-typing":
|
|
628
878
|
typing.stop();
|
|
629
879
|
break;
|