@jxsuite/studio 0.23.2 → 0.24.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/dist/studio.js +2230 -714
- package/dist/studio.js.map +121 -104
- package/package.json +8 -7
- package/src/browse/{browse-modal.js → browse-modal.ts} +9 -10
- package/src/browse/{browse.js → browse.ts} +109 -84
- package/src/canvas/{canvas-diff.js → canvas-diff.ts} +42 -35
- package/src/canvas/{canvas-helpers.js → canvas-helpers.ts} +25 -19
- package/src/canvas/{canvas-live-render.js → canvas-live-render.ts} +53 -63
- package/src/canvas/{canvas-render.js → canvas-render.ts} +110 -121
- package/src/canvas/{canvas-utils.js → canvas-utils.ts} +48 -44
- package/src/canvas/{nested-site-style.js → nested-site-style.ts} +3 -3
- package/src/editor/{component-inline-edit.js → component-inline-edit.ts} +45 -33
- package/src/editor/{content-inline-edit.js → content-inline-edit.ts} +27 -45
- package/src/editor/{context-menu.js → context-menu.ts} +38 -36
- package/src/editor/{convert-targets.js → convert-targets.ts} +15 -12
- package/src/editor/{convert-to-component.js → convert-to-component.ts} +25 -26
- package/src/editor/{convert-to-repeater.js → convert-to-repeater.ts} +60 -40
- package/src/editor/{inline-edit.js → inline-edit.ts} +91 -55
- package/src/editor/{inline-format.js → inline-format.ts} +46 -51
- package/src/editor/{insertion-helper.js → insertion-helper.ts} +47 -63
- package/src/editor/{shortcuts.js → shortcuts.ts} +45 -31
- package/src/editor/{slash-menu.js → slash-menu.ts} +34 -35
- package/src/files/{components.js → components.ts} +12 -15
- package/src/files/{file-ops.js → file-ops.ts} +36 -29
- package/src/files/{files.js → files.ts} +102 -84
- package/src/github/{github-auth.js → github-auth.ts} +3 -3
- package/src/github/{github-publish.js → github-publish.ts} +20 -18
- package/src/markdown/{md-allowlist.js → md-allowlist.ts} +5 -2
- package/src/markdown/{md-convert.js → md-convert.ts} +146 -169
- package/src/new-project/{new-project-modal.js → new-project-modal.ts} +16 -18
- package/src/panels/{activity-bar.js → activity-bar.ts} +26 -34
- package/src/panels/{ai-panel.js → ai-panel.ts} +30 -30
- package/src/panels/{block-action-bar.js → block-action-bar.ts} +64 -73
- package/src/panels/{canvas-dnd.js → canvas-dnd.ts} +44 -49
- package/src/panels/{data-explorer.js → data-explorer.ts} +21 -15
- package/src/panels/dnd.ts +417 -0
- package/src/panels/{editors.js → editors.ts} +43 -50
- package/src/panels/{elements-panel.js → elements-panel.ts} +30 -27
- package/src/panels/{events-panel.js → events-panel.ts} +33 -25
- package/src/panels/{git-panel.js → git-panel.ts} +97 -83
- package/src/panels/{head-panel.js → head-panel.ts} +117 -139
- package/src/panels/{imports-panel.js → imports-panel.ts} +69 -89
- package/src/panels/{layers-panel.js → layers-panel.ts} +61 -57
- package/src/panels/{left-panel.js → left-panel.ts} +56 -63
- package/src/panels/{overlays.js → overlays.ts} +28 -36
- package/src/panels/{panel-events.js → panel-events.ts} +53 -51
- package/src/panels/{preview-render.js → preview-render.ts} +20 -15
- package/src/panels/{properties-panel.js → properties-panel.ts} +212 -246
- package/src/panels/{pseudo-preview.js → pseudo-preview.ts} +9 -8
- package/src/panels/{quick-search.js → quick-search.ts} +16 -17
- package/src/panels/{right-panel.js → right-panel.ts} +41 -59
- package/src/panels/{shared.js → shared.ts} +7 -5
- package/src/panels/{signals-panel.js → signals-panel.ts} +322 -362
- package/src/panels/{statusbar.js → statusbar.ts} +14 -14
- package/src/panels/{style-inputs.js → style-inputs.ts} +66 -48
- package/src/panels/{style-panel.js → style-panel.ts} +235 -290
- package/src/panels/{style-utils.js → style-utils.ts} +32 -31
- package/src/panels/{stylebook-layers-panel.js → stylebook-layers-panel.ts} +22 -14
- package/src/panels/{stylebook-panel.js → stylebook-panel.ts} +127 -140
- package/src/panels/{tab-strip.js → tab-strip.ts} +12 -14
- package/src/panels/{toolbar.js → toolbar.ts} +68 -70
- package/src/panels/{welcome-screen.js → welcome-screen.ts} +14 -16
- package/src/{platform.js → platform.ts} +5 -3
- package/src/platforms/{devserver.js → devserver.ts} +53 -44
- package/src/{recent-projects.js → recent-projects.ts} +17 -12
- package/src/{resize-edges.js → resize-edges.ts} +18 -13
- package/src/services/{cem-export.js → cem-export.ts} +14 -7
- package/src/services/{code-services.js → code-services.ts} +29 -20
- package/src/services/{monaco-setup.js → monaco-setup.ts} +2 -3
- package/src/settings/{content-types-editor.js → content-types-editor.ts} +88 -67
- package/src/settings/{css-vars-editor.js → css-vars-editor.ts} +78 -51
- package/src/settings/{defs-editor.js → defs-editor.ts} +70 -56
- package/src/settings/{general-settings.js → general-settings.ts} +29 -28
- package/src/settings/{head-editor.js → head-editor.ts} +37 -35
- package/src/settings/{schema-field-ui.js → schema-field-ui.ts} +86 -76
- package/src/settings/{settings-modal.js → settings-modal.ts} +14 -16
- package/src/{site-context.js → site-context.ts} +35 -32
- package/src/{state.js → state.ts} +95 -78
- package/src/{store.js → store.ts} +60 -63
- package/src/{studio.js → studio.ts} +117 -119
- package/src/tabs/tab.ts +196 -0
- package/src/tabs/{transact.js → transact.ts} +107 -60
- package/src/types.ts +185 -0
- package/src/ui/{button-group.js → button-group.ts} +21 -21
- package/src/ui/{color-selector.js → color-selector.ts} +44 -37
- package/src/ui/{expression-editor.js → expression-editor.ts} +65 -40
- package/src/ui/{field-row.js → field-row.ts} +19 -2
- package/src/ui/{icons.js → icons.ts} +2 -2
- package/src/ui/{layers.js → layers.ts} +33 -24
- package/src/ui/{media-picker.js → media-picker.ts} +13 -9
- package/src/ui/{panel-resize.js → panel-resize.ts} +9 -4
- package/src/ui/{spectrum.js → spectrum.ts} +4 -4
- package/src/ui/{unit-selector.js → unit-selector.ts} +16 -19
- package/src/ui/{value-selector.js → value-selector.ts} +28 -28
- package/src/ui/{widgets.js → widgets.ts} +62 -15
- package/src/utils/{canvas-media.js → canvas-media.ts} +29 -14
- package/src/utils/{edit-display.js → edit-display.ts} +23 -20
- package/src/utils/{google-fonts.js → google-fonts.ts} +14 -13
- package/src/utils/{inherited-style.js → inherited-style.ts} +15 -10
- package/src/utils/{studio-utils.js → studio-utils.ts} +35 -23
- package/src/{view.js → view.ts} +53 -55
- package/src/workspace/{workspace.js → workspace.ts} +44 -33
- package/src/panels/dnd.js +0 -426
- package/src/tabs/tab.js +0 -192
- /package/src/{reactivity.js → reactivity.ts} +0 -0
|
@@ -1,20 +1,22 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
1
2
|
// ─── Convert to Repeater ──────────────────────────────────────────────────────
|
|
2
3
|
import { html, render as litRender, nothing } from "lit-html";
|
|
3
4
|
import { ref } from "lit-html/directives/ref.js";
|
|
4
|
-
import { getNodeAtPath, parentElementPath, childIndex } from "../store
|
|
5
|
-
import { activeTab } from "../workspace/workspace
|
|
6
|
-
import { transactDoc } from "../tabs/transact
|
|
7
|
-
import { showDialog } from "../ui/layers
|
|
8
|
-
import { defCategory } from "../panels/signals-panel
|
|
5
|
+
import { getNodeAtPath, parentElementPath, childIndex } from "../store";
|
|
6
|
+
import { activeTab } from "../workspace/workspace";
|
|
7
|
+
import { transactDoc } from "../tabs/transact";
|
|
8
|
+
import { showDialog } from "../ui/layers";
|
|
9
|
+
import { defCategory } from "../panels/signals-panel";
|
|
10
|
+
import { fetchPluginSchema } from "../services/code-services";
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
13
|
+
|
|
14
|
+
interface RepeaterConfig {
|
|
15
|
+
items: { $ref: string } | unknown[];
|
|
16
|
+
filter?: { $ref: string };
|
|
17
|
+
sort?: { $ref: string };
|
|
18
|
+
newDef?: { name: string };
|
|
19
|
+
}
|
|
18
20
|
|
|
19
21
|
/** Convert the currently selected element into a repeater template. */
|
|
20
22
|
export async function convertToRepeater() {
|
|
@@ -37,13 +39,12 @@ export async function convertToRepeater() {
|
|
|
37
39
|
}
|
|
38
40
|
const pp = parentElementPath(path);
|
|
39
41
|
if (!pp) return;
|
|
40
|
-
const idx =
|
|
42
|
+
const idx = childIndex(path) as number;
|
|
41
43
|
const parent = getNodeAtPath(doc, pp);
|
|
42
44
|
if (!parent?.children) return;
|
|
43
45
|
const element = parent.children[idx];
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
const repeater = {
|
|
47
|
+
const repeater: Record<string, unknown> = {
|
|
47
48
|
$prototype: "Array",
|
|
48
49
|
items: config.items,
|
|
49
50
|
map: element,
|
|
@@ -51,9 +52,9 @@ export async function convertToRepeater() {
|
|
|
51
52
|
if (config.filter) repeater.filter = config.filter;
|
|
52
53
|
if (config.sort) repeater.sort = config.sort;
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
(parent.children as (string | JxMutableNode)[])[idx] = {
|
|
55
56
|
tagName: "div",
|
|
56
|
-
children:
|
|
57
|
+
children: repeater as unknown as (string | JxMutableNode)[],
|
|
57
58
|
};
|
|
58
59
|
});
|
|
59
60
|
}
|
|
@@ -62,13 +63,32 @@ export async function convertToRepeater() {
|
|
|
62
63
|
* @param {Record<string, unknown>} defs
|
|
63
64
|
* @returns {Promise<RepeaterConfig | null>}
|
|
64
65
|
*/
|
|
65
|
-
function promptRepeaterConfig(defs) {
|
|
66
|
+
async function promptRepeaterConfig(defs: Record<string, unknown>) {
|
|
66
67
|
const arrayDefs = Object.entries(defs).filter(
|
|
67
68
|
([, d]) =>
|
|
68
|
-
|
|
69
|
-
Array.isArray(
|
|
70
|
-
|
|
69
|
+
(d as Record<string, unknown> | null)?.type === "array" ||
|
|
70
|
+
Array.isArray((d as Record<string, unknown> | null)?.default) ||
|
|
71
|
+
(d as Record<string, unknown> | null)?.$prototype === "Array",
|
|
71
72
|
);
|
|
73
|
+
|
|
74
|
+
const tab = activeTab.value;
|
|
75
|
+
const docPath = tab?.documentPath;
|
|
76
|
+
for (const [name, d] of Object.entries(defs)) {
|
|
77
|
+
const def = d as Record<string, unknown> | null;
|
|
78
|
+
if (!def?.$prototype || def.$prototype === "Function" || def.$prototype === "Array") continue;
|
|
79
|
+
if (arrayDefs.some(([n]) => n === name)) continue;
|
|
80
|
+
const schema = await fetchPluginSchema(
|
|
81
|
+
{
|
|
82
|
+
...(def.$src != null && { $src: def.$src as string }),
|
|
83
|
+
$prototype: def.$prototype as string,
|
|
84
|
+
},
|
|
85
|
+
{ ...(docPath != null && { documentPath: docPath }) },
|
|
86
|
+
);
|
|
87
|
+
if (schema?.returns?.type === "array") {
|
|
88
|
+
arrayDefs.push([name, d]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
const fnDefs = Object.entries(defs).filter(([, d]) => defCategory(d) === "function");
|
|
73
93
|
|
|
74
94
|
let source = arrayDefs.length > 0 ? arrayDefs[0][0] : "__new__";
|
|
@@ -77,7 +97,7 @@ function promptRepeaterConfig(defs) {
|
|
|
77
97
|
let sortDef = "";
|
|
78
98
|
let error = "";
|
|
79
99
|
|
|
80
|
-
return showDialog((done) => {
|
|
100
|
+
return showDialog<RepeaterConfig | null>((done) => {
|
|
81
101
|
function confirm() {
|
|
82
102
|
if (source === "__new__") {
|
|
83
103
|
const name = newDefName.trim();
|
|
@@ -97,16 +117,16 @@ function promptRepeaterConfig(defs) {
|
|
|
97
117
|
return;
|
|
98
118
|
}
|
|
99
119
|
done({
|
|
100
|
-
items: { $ref:
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
items: { $ref: `#/state/${name}` },
|
|
121
|
+
...(filterDef && { filter: { $ref: `#/state/${filterDef}` } }),
|
|
122
|
+
...(sortDef && { sort: { $ref: `#/state/${sortDef}` } }),
|
|
103
123
|
newDef: { name },
|
|
104
124
|
});
|
|
105
125
|
} else {
|
|
106
126
|
done({
|
|
107
|
-
items: { $ref:
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
items: { $ref: `#/state/${source}` },
|
|
128
|
+
...(filterDef && { filter: { $ref: `#/state/${filterDef}` } }),
|
|
129
|
+
...(sortDef && { sort: { $ref: `#/state/${sortDef}` } }),
|
|
110
130
|
});
|
|
111
131
|
}
|
|
112
132
|
}
|
|
@@ -114,7 +134,7 @@ function promptRepeaterConfig(defs) {
|
|
|
114
134
|
function rerender() {
|
|
115
135
|
const layer = document.getElementById("layer-dialog");
|
|
116
136
|
const slot = layer?.lastElementChild;
|
|
117
|
-
if (slot) litRender(buildTpl(),
|
|
137
|
+
if (slot) litRender(buildTpl(), slot as HTMLElement);
|
|
118
138
|
}
|
|
119
139
|
|
|
120
140
|
function buildTpl() {
|
|
@@ -137,8 +157,8 @@ function promptRepeaterConfig(defs) {
|
|
|
137
157
|
label="Items source"
|
|
138
158
|
size="s"
|
|
139
159
|
.value=${source}
|
|
140
|
-
@change=${(
|
|
141
|
-
source =
|
|
160
|
+
@change=${(e: Event) => {
|
|
161
|
+
source = (e.target as HTMLInputElement).value;
|
|
142
162
|
error = "";
|
|
143
163
|
rerender();
|
|
144
164
|
}}
|
|
@@ -160,17 +180,17 @@ function promptRepeaterConfig(defs) {
|
|
|
160
180
|
placeholder="myItems"
|
|
161
181
|
.value=${newDefName}
|
|
162
182
|
?negative=${!!error}
|
|
163
|
-
@input=${(
|
|
164
|
-
newDefName =
|
|
183
|
+
@input=${(e: Event) => {
|
|
184
|
+
newDefName = (e.target as HTMLInputElement).value || "";
|
|
165
185
|
error = "";
|
|
166
186
|
rerender();
|
|
167
187
|
}}
|
|
168
|
-
@keydown=${(
|
|
188
|
+
@keydown=${(e: KeyboardEvent) => {
|
|
169
189
|
if (e.key === "Enter") confirm();
|
|
170
190
|
}}
|
|
171
191
|
${ref((el) => {
|
|
172
192
|
if (el && source === "__new__")
|
|
173
|
-
requestAnimationFrame(() =>
|
|
193
|
+
requestAnimationFrame(() => (el as HTMLElement).focus());
|
|
174
194
|
})}
|
|
175
195
|
>
|
|
176
196
|
<sp-help-text slot="negative-help-text">${error}</sp-help-text>
|
|
@@ -186,8 +206,8 @@ function promptRepeaterConfig(defs) {
|
|
|
186
206
|
label="Filter"
|
|
187
207
|
size="s"
|
|
188
208
|
.value=${filterDef}
|
|
189
|
-
@change=${(
|
|
190
|
-
filterDef =
|
|
209
|
+
@change=${(e: Event) => {
|
|
210
|
+
filterDef = (e.target as HTMLInputElement).value;
|
|
191
211
|
rerender();
|
|
192
212
|
}}
|
|
193
213
|
>
|
|
@@ -203,8 +223,8 @@ function promptRepeaterConfig(defs) {
|
|
|
203
223
|
label="Sort"
|
|
204
224
|
size="s"
|
|
205
225
|
.value=${sortDef}
|
|
206
|
-
@change=${(
|
|
207
|
-
sortDef =
|
|
226
|
+
@change=${(e: Event) => {
|
|
227
|
+
sortDef = (e.target as HTMLInputElement).value;
|
|
208
228
|
rerender();
|
|
209
229
|
}}
|
|
210
230
|
>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/// <reference lib="dom.iterable" />
|
|
1
3
|
/**
|
|
2
4
|
* Inline-edit.js — Contenteditable inline editing for content mode
|
|
3
5
|
*
|
|
@@ -5,23 +7,39 @@
|
|
|
5
7
|
* text formatting, Enter for new paragraphs, and slash commands for inserting elements.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {{ tag: string; label: string; icon?: string; command?: string; shortcut?: string }} InlineAction
|
|
10
|
-
*
|
|
11
|
-
* @typedef {{ $inlineChildren?: string[]; $inlineActions?: InlineAction[] | string }} ElementDef
|
|
12
|
-
*
|
|
13
|
-
* @typedef {{ label: string; tag: string; description: string }} SlashCommand
|
|
14
|
-
*
|
|
15
|
-
* @typedef {{ textContent?: string | null; children?: (JxMutableNode | string)[] }} JxContentResult
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
10
|
import elementsMeta from "../../data/elements-meta.json";
|
|
19
|
-
import { toggleInlineFormat, normalizeInlineContent } from "./inline-format
|
|
11
|
+
import { toggleInlineFormat, normalizeInlineContent } from "./inline-format";
|
|
12
|
+
import type { JxPath } from "../state";
|
|
13
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
14
|
+
export interface InlineAction {
|
|
15
|
+
tag: string;
|
|
16
|
+
label: string;
|
|
17
|
+
icon?: string;
|
|
18
|
+
command?: string;
|
|
19
|
+
shortcut?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ElementDef {
|
|
23
|
+
$inlineChildren?: string[];
|
|
24
|
+
$inlineActions?: InlineAction[] | string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SlashCommand {
|
|
28
|
+
label: string;
|
|
29
|
+
tag: string;
|
|
30
|
+
description: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface JxContentResult {
|
|
34
|
+
textContent?: string | null;
|
|
35
|
+
children?: (JxMutableNode | string)[];
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
import {
|
|
21
39
|
showSlashMenu as sharedShowSlashMenu,
|
|
22
40
|
dismissSlashMenu as sharedDismissSlashMenu,
|
|
23
41
|
isSlashMenuOpen,
|
|
24
|
-
} from "./slash-menu
|
|
42
|
+
} from "./slash-menu";
|
|
25
43
|
|
|
26
44
|
// ─── Inline tag set (tags that represent rich text formatting) ─────────────
|
|
27
45
|
|
|
@@ -71,9 +89,9 @@ const EDITABLE_BLOCKS = new Set([
|
|
|
71
89
|
* @param {string} parentTag
|
|
72
90
|
* @returns {boolean}
|
|
73
91
|
*/
|
|
74
|
-
export function isInlineInContext(childTag, parentTag) {
|
|
92
|
+
export function isInlineInContext(childTag: string, parentTag: string) {
|
|
75
93
|
if (!parentTag) return INLINE_TAGS.has(childTag);
|
|
76
|
-
const parentDef =
|
|
94
|
+
const parentDef = (elementsMeta.$defs as Record<string, ElementDef>)[parentTag];
|
|
77
95
|
if (!parentDef || !parentDef.$inlineChildren) return false;
|
|
78
96
|
return parentDef.$inlineChildren.includes(childTag);
|
|
79
97
|
}
|
|
@@ -85,12 +103,12 @@ export function isInlineInContext(childTag, parentTag) {
|
|
|
85
103
|
* @param {string} tag
|
|
86
104
|
* @returns {InlineAction[] | null}
|
|
87
105
|
*/
|
|
88
|
-
export function getInlineActions(tag) {
|
|
89
|
-
const def =
|
|
106
|
+
export function getInlineActions(tag: string) {
|
|
107
|
+
const def = (elementsMeta.$defs as Record<string, ElementDef>)[tag];
|
|
90
108
|
if (!def) return null;
|
|
91
109
|
let actions = def.$inlineActions;
|
|
92
110
|
if (typeof actions === "string") {
|
|
93
|
-
const refDef =
|
|
111
|
+
const refDef = (elementsMeta.$defs as Record<string, ElementDef>)[actions];
|
|
94
112
|
actions = refDef?.$inlineActions ?? undefined;
|
|
95
113
|
}
|
|
96
114
|
if (!Array.isArray(actions)) return null;
|
|
@@ -99,10 +117,8 @@ export function getInlineActions(tag) {
|
|
|
99
117
|
|
|
100
118
|
// ─── Editing state ─────────────────────────────────────────────────────────
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
let
|
|
104
|
-
/** @type {JxPath | null} */
|
|
105
|
-
let activePath = null; // JSON path to the active element
|
|
120
|
+
let activeEl: HTMLElement | null = null; // currently contenteditable element
|
|
121
|
+
let activePath: JxPath | null = null; // JSON path to the active element
|
|
106
122
|
/**
|
|
107
123
|
* @type {((
|
|
108
124
|
* path: JxPath,
|
|
@@ -111,19 +127,28 @@ let activePath = null; // JSON path to the active element
|
|
|
111
127
|
* ) => void)
|
|
112
128
|
* | null}
|
|
113
129
|
*/
|
|
114
|
-
let commitFn
|
|
130
|
+
let commitFn:
|
|
131
|
+
| ((
|
|
132
|
+
path: JxPath,
|
|
133
|
+
children: (JxMutableNode | string)[] | null,
|
|
134
|
+
textContent: string | null,
|
|
135
|
+
) => void)
|
|
136
|
+
| null = null; // function(path, newChildren, newTextContent) to commit changes
|
|
115
137
|
/**
|
|
116
138
|
* @type {((path: JxPath, beforeChildren: JxContentResult, afterChildren: JxContentResult) => void)
|
|
117
139
|
* | null}
|
|
118
140
|
*/
|
|
119
|
-
let splitFn
|
|
141
|
+
let splitFn:
|
|
142
|
+
| ((path: JxPath, beforeChildren: JxContentResult, afterChildren: JxContentResult) => void)
|
|
143
|
+
| null = null; // function(path, beforeChildren, afterChildren) to split paragraph
|
|
120
144
|
/**
|
|
121
145
|
* @type {((path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void)
|
|
122
146
|
* | null}
|
|
123
147
|
*/
|
|
124
|
-
let insertFn
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
let insertFn:
|
|
149
|
+
| ((path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void)
|
|
150
|
+
| null = null; // function(path, elementDef, commitData?) to insert after current block
|
|
151
|
+
let endFn: (() => void) | null = null; // function() called when editing stops
|
|
127
152
|
|
|
128
153
|
/**
|
|
129
154
|
* Check if an element is a text-bearing editable block.
|
|
@@ -131,7 +156,7 @@ let endFn = null; // function() called when editing stops
|
|
|
131
156
|
* @param {HTMLElement} el
|
|
132
157
|
* @returns {boolean}
|
|
133
158
|
*/
|
|
134
|
-
export function isEditableBlock(el) {
|
|
159
|
+
export function isEditableBlock(el: HTMLElement) {
|
|
135
160
|
return EDITABLE_BLOCKS.has(el.tagName.toLowerCase());
|
|
136
161
|
}
|
|
137
162
|
|
|
@@ -143,7 +168,7 @@ export function isEditableBlock(el) {
|
|
|
143
168
|
* @param {JxMutableNode} [parentNode]
|
|
144
169
|
* @returns {boolean}
|
|
145
170
|
*/
|
|
146
|
-
export function isInlineElement(node, parentNode) {
|
|
171
|
+
export function isInlineElement(node: JxMutableNode, parentNode?: JxMutableNode) {
|
|
147
172
|
if (!node || typeof node !== "object") return false;
|
|
148
173
|
const childTag = (node.tagName ?? "div").toLowerCase();
|
|
149
174
|
if (parentNode) {
|
|
@@ -173,7 +198,24 @@ export function isInlineElement(node, parentNode) {
|
|
|
173
198
|
* onEnd: () => void;
|
|
174
199
|
* }} callbacks
|
|
175
200
|
*/
|
|
176
|
-
export function startEditing(
|
|
201
|
+
export function startEditing(
|
|
202
|
+
el: HTMLElement,
|
|
203
|
+
path: JxPath,
|
|
204
|
+
callbacks: {
|
|
205
|
+
onCommit: (
|
|
206
|
+
path: JxPath,
|
|
207
|
+
children: (JxMutableNode | string)[] | null,
|
|
208
|
+
textContent: string | null,
|
|
209
|
+
) => void;
|
|
210
|
+
onSplit: (
|
|
211
|
+
path: JxPath,
|
|
212
|
+
beforeChildren: JxContentResult,
|
|
213
|
+
afterChildren: JxContentResult,
|
|
214
|
+
) => void;
|
|
215
|
+
onInsert: (path: JxPath, elementDef: SlashCommand, commitData?: JxContentResult) => void;
|
|
216
|
+
onEnd: () => void;
|
|
217
|
+
},
|
|
218
|
+
) {
|
|
177
219
|
if (activeEl) stopEditing();
|
|
178
220
|
|
|
179
221
|
activeEl = el;
|
|
@@ -259,7 +301,7 @@ export function getActiveElement() {
|
|
|
259
301
|
// ─── Event handlers ────────────────────────────────────────────────────────
|
|
260
302
|
|
|
261
303
|
/** @param {KeyboardEvent} e */
|
|
262
|
-
function handleKeydown(e) {
|
|
304
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
263
305
|
if (e.key === "Escape") {
|
|
264
306
|
e.preventDefault();
|
|
265
307
|
e.stopPropagation();
|
|
@@ -325,7 +367,7 @@ function handleInput() {
|
|
|
325
367
|
}
|
|
326
368
|
|
|
327
369
|
/** @param {FocusEvent} _e */
|
|
328
|
-
function handleBlur(_e) {
|
|
370
|
+
function handleBlur(_e: FocusEvent) {
|
|
329
371
|
// Don't close if focus moved to slash menu
|
|
330
372
|
if (isSlashMenuOpen()) return;
|
|
331
373
|
|
|
@@ -338,7 +380,7 @@ function handleBlur(_e) {
|
|
|
338
380
|
}
|
|
339
381
|
|
|
340
382
|
/** @param {ClipboardEvent} e */
|
|
341
|
-
function handlePaste(e) {
|
|
383
|
+
function handlePaste(e: ClipboardEvent) {
|
|
342
384
|
e.preventDefault();
|
|
343
385
|
// Paste as plain text to avoid foreign HTML
|
|
344
386
|
const text = e.clipboardData?.getData("text/plain") ?? "";
|
|
@@ -412,7 +454,7 @@ function commitChanges() {
|
|
|
412
454
|
* @param {{ children?: (JxMutableNode | string)[] }} node
|
|
413
455
|
* @returns {JxContentResult}
|
|
414
456
|
*/
|
|
415
|
-
export function normalizeChildren(node) {
|
|
457
|
+
export function normalizeChildren(node: { children?: (JxMutableNode | string)[] }) {
|
|
416
458
|
if (!Array.isArray(node.children) || node.children.length === 0) return { textContent: "" };
|
|
417
459
|
|
|
418
460
|
// Step 1: Merge adjacent text nodes
|
|
@@ -430,7 +472,7 @@ export function normalizeChildren(node) {
|
|
|
430
472
|
}
|
|
431
473
|
|
|
432
474
|
// Step 2: If all children are text, fold into textContent
|
|
433
|
-
if (merged.every((
|
|
475
|
+
if (merged.every((c: JxMutableNode | string) => typeof c === "string")) {
|
|
434
476
|
return { textContent: merged.join("") };
|
|
435
477
|
}
|
|
436
478
|
|
|
@@ -444,7 +486,7 @@ export function normalizeChildren(node) {
|
|
|
444
486
|
* @param {HTMLElement} el
|
|
445
487
|
* @returns {JxContentResult}
|
|
446
488
|
*/
|
|
447
|
-
function elementToJx(el) {
|
|
489
|
+
function elementToJx(el: HTMLElement): JxContentResult {
|
|
448
490
|
const nodes = el.childNodes;
|
|
449
491
|
|
|
450
492
|
// If just a single text node, use textContent
|
|
@@ -454,8 +496,7 @@ function elementToJx(el) {
|
|
|
454
496
|
}
|
|
455
497
|
|
|
456
498
|
// Mixed content → children array
|
|
457
|
-
|
|
458
|
-
const children = [];
|
|
499
|
+
const children: (JxMutableNode | string)[] = [];
|
|
459
500
|
for (const child of nodes) {
|
|
460
501
|
const jsx = domNodeToJx(child);
|
|
461
502
|
if (jsx !== null && jsx !== undefined) children.push(jsx);
|
|
@@ -471,7 +512,7 @@ function elementToJx(el) {
|
|
|
471
512
|
* @param {Node} node
|
|
472
513
|
* @returns {JxMutableNode | string | null}
|
|
473
514
|
*/
|
|
474
|
-
function domNodeToJx(node) {
|
|
515
|
+
function domNodeToJx(node: Node) {
|
|
475
516
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
476
517
|
const text = node.textContent;
|
|
477
518
|
if (!text) return null;
|
|
@@ -480,21 +521,18 @@ function domNodeToJx(node) {
|
|
|
480
521
|
|
|
481
522
|
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
482
523
|
|
|
483
|
-
const el =
|
|
524
|
+
const el = node as HTMLElement;
|
|
484
525
|
const tag = el.tagName.toLowerCase();
|
|
485
|
-
|
|
486
|
-
const result = { tagName: tag };
|
|
526
|
+
const result: JxMutableNode = { tagName: tag };
|
|
487
527
|
|
|
488
528
|
// Map browser execCommand output to our tag conventions
|
|
489
|
-
|
|
490
|
-
const tagMap = { b: "strong", i: "em", s: "del", strike: "del" };
|
|
529
|
+
const tagMap: Record<string, string> = { b: "strong", i: "em", s: "del", strike: "del" };
|
|
491
530
|
if (tagMap[tag]) result.tagName = tagMap[tag];
|
|
492
531
|
|
|
493
532
|
// Attributes
|
|
494
|
-
if (tag === "a" &&
|
|
533
|
+
if (tag === "a" && (el as HTMLAnchorElement).href) {
|
|
495
534
|
result.attributes = { href: el.getAttribute("href") };
|
|
496
|
-
if (
|
|
497
|
-
result.attributes.title = /** @type {HTMLAnchorElement} */ (el).title;
|
|
535
|
+
if ((el as HTMLAnchorElement).title) result.attributes.title = (el as HTMLAnchorElement).title;
|
|
498
536
|
}
|
|
499
537
|
if (tag === "code") {
|
|
500
538
|
result.textContent = el.textContent;
|
|
@@ -524,15 +562,14 @@ function domNodeToJx(node) {
|
|
|
524
562
|
* @param {DocumentFragment} frag
|
|
525
563
|
* @returns {JxContentResult}
|
|
526
564
|
*/
|
|
527
|
-
function fragmentToJx(frag) {
|
|
565
|
+
function fragmentToJx(frag: DocumentFragment) {
|
|
528
566
|
const nodes = frag.childNodes;
|
|
529
567
|
if (nodes.length === 0) return { textContent: "" };
|
|
530
568
|
if (nodes.length === 1 && nodes[0].nodeType === Node.TEXT_NODE) {
|
|
531
569
|
return { textContent: nodes[0].textContent };
|
|
532
570
|
}
|
|
533
571
|
|
|
534
|
-
|
|
535
|
-
const children = [];
|
|
572
|
+
const children: (JxMutableNode | string)[] = [];
|
|
536
573
|
for (const child of nodes) {
|
|
537
574
|
const jsx = domNodeToJx(child);
|
|
538
575
|
if (jsx) children.push(jsx);
|
|
@@ -556,9 +593,9 @@ function fragmentToJx(frag) {
|
|
|
556
593
|
* @param {Range} range
|
|
557
594
|
* @returns {string}
|
|
558
595
|
*/
|
|
559
|
-
function getTextBeforeCursor(range) {
|
|
596
|
+
function getTextBeforeCursor(range: Range) {
|
|
560
597
|
const preRange = document.createRange();
|
|
561
|
-
preRange.setStart(
|
|
598
|
+
preRange.setStart(activeEl!, 0);
|
|
562
599
|
preRange.setEnd(range.startContainer, range.startOffset);
|
|
563
600
|
return preRange.toString();
|
|
564
601
|
}
|
|
@@ -602,7 +639,7 @@ function updateSlashMenu() {
|
|
|
602
639
|
}
|
|
603
640
|
|
|
604
641
|
/** @param {SlashCommand} cmd */
|
|
605
|
-
function handleSlashSelect(cmd) {
|
|
642
|
+
function handleSlashSelect(cmd: SlashCommand) {
|
|
606
643
|
if (!activeEl || !insertFn || !activePath) return;
|
|
607
644
|
|
|
608
645
|
// Remove the /command text from the element
|
|
@@ -614,11 +651,10 @@ function handleSlashSelect(cmd) {
|
|
|
614
651
|
if (slashIdx >= 0) {
|
|
615
652
|
const walker = document.createTreeWalker(activeEl, NodeFilter.SHOW_TEXT);
|
|
616
653
|
let charCount = 0;
|
|
617
|
-
|
|
618
|
-
let slashNode = null;
|
|
654
|
+
let slashNode: Text | null = null;
|
|
619
655
|
let slashOffset = 0;
|
|
620
656
|
while (walker.nextNode()) {
|
|
621
|
-
const node =
|
|
657
|
+
const node = walker.currentNode as Text;
|
|
622
658
|
if (charCount + node.length > slashIdx) {
|
|
623
659
|
slashNode = node;
|
|
624
660
|
slashOffset = slashIdx - charCount;
|