@refrakt-md/editor 0.8.1 → 0.8.3
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/app/dist/assets/{index-D3TQo8gu.js → index-3MvwKRVQ.js} +1 -1
- package/app/dist/assets/{index-CeU_s7BB.js → index-B7e694w6.js} +1 -1
- package/app/dist/assets/{index-DzHt8ZRh.js → index-BBljOYQu.js} +1 -1
- package/app/dist/assets/{index-C72UC2ga.js → index-BEGy_i8o.js} +1 -1
- package/app/dist/assets/{index-CqHjo2YT.js → index-BGy7ixjW.js} +1 -1
- package/app/dist/assets/{index-DVM3uoxc.js → index-BaLgiiKk.js} +1 -1
- package/app/dist/assets/{index-CW02bulk.js → index-BjlNcvOf.js} +1 -1
- package/app/dist/assets/{index-DmY6uqAw.js → index-CKfKYVw7.js} +1 -1
- package/app/dist/assets/{index-BLuaHLN3.js → index-COFbngzR.js} +1 -1
- package/app/dist/assets/{index-BBinZAiy.js → index-CPEo_rvd.js} +1 -1
- package/app/dist/assets/{index-D_Y6J00B.js → index-CQDCT-XT.js} +1 -1
- package/app/dist/assets/{index-COIPZ34u.js → index-CUmEjEeR.js} +1 -1
- package/app/dist/assets/{index-BgCNqcSo.js → index-CeV-Af4N.js} +1 -1
- package/app/dist/assets/{index-DW2zI-Ss.js → index-ChbH55h5.js} +1 -1
- package/app/dist/assets/index-CzvG5PZT.css +1 -0
- package/app/dist/assets/{index-ZLvRNfLb.js → index-D9-aYc3I.js} +1 -1
- package/app/dist/assets/{index-BwFn9q4x.js → index-DezxtfNV.js} +1 -1
- package/app/dist/assets/{index-CXFMPmtf.js → index-DrI4IfXE.js} +1 -1
- package/app/dist/assets/{index-DgIg-QAA.js → index-DwfxgjnU.js} +2 -2
- package/app/dist/assets/index-ogrpJNou.js +555 -0
- package/app/dist/index.html +2 -2
- package/app/src/lib/api/client.ts +32 -0
- package/app/src/lib/components/ActionEditPopover.svelte +41 -19
- package/app/src/lib/components/BlockCard.svelte +74 -17
- package/app/src/lib/components/BlockEditPanel.svelte +142 -9
- package/app/src/lib/components/BlockEditor.svelte +534 -48
- package/app/src/lib/components/CodeEditPopover.svelte +281 -63
- package/app/src/lib/components/ContentModelTree.svelte +340 -67
- package/app/src/lib/components/IconPickerPopover.svelte +389 -0
- package/app/src/lib/components/ImageEditPopover.svelte +519 -0
- package/app/src/lib/components/InlineEditPopover.svelte +79 -56
- package/app/src/lib/components/InlineEditor.svelte +15 -5
- package/app/src/lib/components/ProseBlockCard.svelte +446 -0
- package/app/src/lib/components/ProseEditPanel.svelte +470 -0
- package/app/src/lib/components/RuneAttributes.svelte +51 -0
- package/app/src/lib/editor/block-parser.ts +211 -9
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +129 -1
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/app/dist/assets/index-BD2EBUrQ.css +0 -1
- package/app/dist/assets/index-BlAOhWAQ.js +0 -453
|
@@ -9,20 +9,32 @@
|
|
|
9
9
|
blockLabel,
|
|
10
10
|
extractRuneInner,
|
|
11
11
|
rebuildRuneSource,
|
|
12
|
+
rebuildHeadingSource,
|
|
13
|
+
rebuildFenceSource,
|
|
14
|
+
groupIntoEditorBlocks,
|
|
12
15
|
type ParsedBlock,
|
|
13
16
|
type RuneBlock,
|
|
17
|
+
type HeadingBlock,
|
|
18
|
+
type FenceBlock,
|
|
19
|
+
type EditorBlock,
|
|
20
|
+
type ProseBlock,
|
|
14
21
|
} from '../editor/block-parser.js';
|
|
15
|
-
import { findSectionMapping, applySectionEdit, findActionMapping, applyActionEdit, findCommandMapping, applyCommandEdit, type SectionMapping, type ActionMapping, type CommandMapping } from '../editor/section-mapper.js';
|
|
22
|
+
import { findSectionMapping, applySectionEdit, findActionMapping, applyActionEdit, findCommandMapping, applyCommandEdit, applyLanguageEdit, findImageMapping, applyImageEdit, findIconMapping, applyIconEdit, type SectionMapping, type ActionMapping, type CommandMapping, type ImageMapping, type IconMapping } from '../editor/section-mapper.js';
|
|
16
23
|
import { stripInlineMarkdown } from '../editor/inline-markdown.js';
|
|
17
24
|
import { editorState } from '../state/editor.svelte.js';
|
|
18
25
|
import BlockCard from './BlockCard.svelte';
|
|
19
26
|
import type { SectionClickInfo } from './BlockCard.svelte';
|
|
27
|
+
import ProseBlockCard from './ProseBlockCard.svelte';
|
|
28
|
+
import type { ProseElementClickInfo } from './ProseBlockCard.svelte';
|
|
20
29
|
import BlockEditPanel from './BlockEditPanel.svelte';
|
|
30
|
+
import ProseEditPanel from './ProseEditPanel.svelte';
|
|
21
31
|
import FrontmatterEditPanel from './FrontmatterEditPanel.svelte';
|
|
22
32
|
import InsertBlockDialog from './InsertBlockDialog.svelte';
|
|
23
33
|
import InlineEditPopover from './InlineEditPopover.svelte';
|
|
24
34
|
import ActionEditPopover from './ActionEditPopover.svelte';
|
|
25
35
|
import CodeEditPopover from './CodeEditPopover.svelte';
|
|
36
|
+
import ImageEditPopover from './ImageEditPopover.svelte';
|
|
37
|
+
import IconPickerPopover from './IconPickerPopover.svelte';
|
|
26
38
|
|
|
27
39
|
interface Props {
|
|
28
40
|
bodyContent: string;
|
|
@@ -112,6 +124,7 @@
|
|
|
112
124
|
editingFrontmatter = false;
|
|
113
125
|
anchorPoint = null;
|
|
114
126
|
inlineEdit = null;
|
|
127
|
+
proseInlineEdit = null;
|
|
115
128
|
}
|
|
116
129
|
});
|
|
117
130
|
|
|
@@ -122,6 +135,162 @@
|
|
|
122
135
|
onchange(newSource);
|
|
123
136
|
}
|
|
124
137
|
|
|
138
|
+
// ── Prose block grouping ─────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
let editorBlocks: EditorBlock[] = $derived(groupIntoEditorBlocks(blocks));
|
|
141
|
+
|
|
142
|
+
/** Map an editor block index to the range of flat block indices it covers */
|
|
143
|
+
function editorBlockToFlatRange(editorIndex: number): [number, number] {
|
|
144
|
+
const eb = editorBlocks[editorIndex];
|
|
145
|
+
if (!eb) return [0, 0];
|
|
146
|
+
if (eb.type === 'prose') {
|
|
147
|
+
const firstChild = eb.children[0];
|
|
148
|
+
const lastChild = eb.children[eb.children.length - 1];
|
|
149
|
+
const start = blocks.indexOf(firstChild);
|
|
150
|
+
const end = blocks.indexOf(lastChild);
|
|
151
|
+
return [start, end];
|
|
152
|
+
}
|
|
153
|
+
// Rune block — find its position in the flat array
|
|
154
|
+
const idx = blocks.indexOf(eb as ParsedBlock);
|
|
155
|
+
return [idx, idx];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Map a flat block index to the corresponding editor block index */
|
|
159
|
+
function flatToEditorIndex(flatIndex: number): number {
|
|
160
|
+
let offset = 0;
|
|
161
|
+
for (let i = 0; i < editorBlocks.length; i++) {
|
|
162
|
+
const eb = editorBlocks[i];
|
|
163
|
+
const count = eb.type === 'prose' ? eb.children.length : 1;
|
|
164
|
+
if (flatIndex < offset + count) return i;
|
|
165
|
+
offset += count;
|
|
166
|
+
}
|
|
167
|
+
return editorBlocks.length - 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Prose block operations ───────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/** Update a prose block from the ProseEditPanel (structure reorder, content edit) */
|
|
173
|
+
function handleUpdateProseBlock(editorIndex: number, updated: ProseBlock) {
|
|
174
|
+
const [startFlat, endFlat] = editorBlockToFlatRange(editorIndex);
|
|
175
|
+
const oldChildren = blocks.slice(startFlat, endFlat + 1);
|
|
176
|
+
reconcileIds(oldChildren, updated.children);
|
|
177
|
+
blocks = [
|
|
178
|
+
...blocks.slice(0, startFlat),
|
|
179
|
+
...updated.children,
|
|
180
|
+
...blocks.slice(endFlat + 1),
|
|
181
|
+
];
|
|
182
|
+
syncToSource();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Handle click on a prose element (heading, paragraph, etc.) for inline editing */
|
|
186
|
+
function handleProseSectionClick(editorIndex: number, info: ProseElementClickInfo) {
|
|
187
|
+
const eb = editorBlocks[editorIndex];
|
|
188
|
+
if (!eb || eb.type !== 'prose') return;
|
|
189
|
+
|
|
190
|
+
const child = eb.children[info.childIndex];
|
|
191
|
+
if (!child) return;
|
|
192
|
+
|
|
193
|
+
const [startFlat] = editorBlockToFlatRange(editorIndex);
|
|
194
|
+
const flatIndex = startFlat + info.childIndex;
|
|
195
|
+
|
|
196
|
+
if (child.type === 'fence') {
|
|
197
|
+
const fb = child as FenceBlock;
|
|
198
|
+
const code = fb.code;
|
|
199
|
+
const language = fb.language;
|
|
200
|
+
const opener = child.source.split('\n')[0];
|
|
201
|
+
const delimMatch = opener.match(/^(`{3,}|~{3,})/);
|
|
202
|
+
const delimiter = delimMatch ? delimMatch[1] : '```';
|
|
203
|
+
commandEdit = {
|
|
204
|
+
blockIndex: flatIndex,
|
|
205
|
+
rect: info.rect,
|
|
206
|
+
mapping: { source: child.source, code, language, opener, delimiter },
|
|
207
|
+
};
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// For headings and paragraphs: inline text editing
|
|
212
|
+
const source = child.source.trim();
|
|
213
|
+
let prefix = '';
|
|
214
|
+
let inlineContent = source;
|
|
215
|
+
|
|
216
|
+
if (child.type === 'heading') {
|
|
217
|
+
const match = source.match(/^(#{1,6}\s+)(.*)/);
|
|
218
|
+
if (match) {
|
|
219
|
+
prefix = match[1];
|
|
220
|
+
inlineContent = match[2];
|
|
221
|
+
}
|
|
222
|
+
} else if (child.type === 'quote') {
|
|
223
|
+
const match = source.match(/^(>\s*)(.*)/);
|
|
224
|
+
if (match) {
|
|
225
|
+
prefix = match[1];
|
|
226
|
+
inlineContent = match[2];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const mapping: SectionMapping = {
|
|
231
|
+
dataName: child.type,
|
|
232
|
+
text: stripInlineMarkdown(inlineContent),
|
|
233
|
+
source,
|
|
234
|
+
sourcePrefix: prefix,
|
|
235
|
+
inlineSource: inlineContent,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
proseInlineEdit = {
|
|
239
|
+
editorIndex,
|
|
240
|
+
childIndex: info.childIndex,
|
|
241
|
+
flatIndex,
|
|
242
|
+
inlineSource: inlineContent,
|
|
243
|
+
rect: info.rect,
|
|
244
|
+
mapping,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Prose inline edit state ──────────────────────────────────
|
|
249
|
+
|
|
250
|
+
let proseInlineEdit: {
|
|
251
|
+
editorIndex: number;
|
|
252
|
+
childIndex: number;
|
|
253
|
+
flatIndex: number;
|
|
254
|
+
inlineSource: string;
|
|
255
|
+
rect: DOMRect;
|
|
256
|
+
mapping: SectionMapping;
|
|
257
|
+
} | null = $state(null);
|
|
258
|
+
|
|
259
|
+
function handleProseInlineEditChange(newInlineSource: string) {
|
|
260
|
+
if (!proseInlineEdit) return;
|
|
261
|
+
const child = blocks[proseInlineEdit.flatIndex];
|
|
262
|
+
if (!child) return;
|
|
263
|
+
|
|
264
|
+
const newSource = proseInlineEdit.mapping.sourcePrefix + newInlineSource;
|
|
265
|
+
let updated: ParsedBlock;
|
|
266
|
+
|
|
267
|
+
if (child.type === 'heading') {
|
|
268
|
+
const hb = child as HeadingBlock;
|
|
269
|
+
updated = { ...hb, text: newInlineSource, source: '' } as HeadingBlock;
|
|
270
|
+
(updated as HeadingBlock).source = rebuildHeadingSource(updated as HeadingBlock);
|
|
271
|
+
} else {
|
|
272
|
+
updated = { ...child, source: newSource };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Update the mapping for subsequent edits
|
|
276
|
+
proseInlineEdit = {
|
|
277
|
+
...proseInlineEdit,
|
|
278
|
+
inlineSource: newInlineSource,
|
|
279
|
+
mapping: {
|
|
280
|
+
...proseInlineEdit.mapping,
|
|
281
|
+
text: stripInlineMarkdown(newInlineSource),
|
|
282
|
+
source: newSource,
|
|
283
|
+
inlineSource: newInlineSource,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
handleUpdateBlock(proseInlineEdit.flatIndex, updated);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function closeProseInlineEdit() {
|
|
291
|
+
proseInlineEdit = null;
|
|
292
|
+
}
|
|
293
|
+
|
|
125
294
|
// ── Frontmatter summary for visual mode header ──────────────
|
|
126
295
|
|
|
127
296
|
let editingFrontmatter = $state(false);
|
|
@@ -144,6 +313,33 @@
|
|
|
144
313
|
const POPOVER_WIDTH = 420;
|
|
145
314
|
const POPOVER_GAP = 12;
|
|
146
315
|
|
|
316
|
+
/** Drag the popover by its header */
|
|
317
|
+
function handlePopoverDragStart(e: MouseEvent) {
|
|
318
|
+
const header = (e.target as HTMLElement).closest('.edit-panel__header');
|
|
319
|
+
if (!header || (e.target as HTMLElement).closest('button')) return;
|
|
320
|
+
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
const popoverEl = e.currentTarget as HTMLElement;
|
|
323
|
+
const rect = popoverEl.getBoundingClientRect();
|
|
324
|
+
const startX = e.clientX;
|
|
325
|
+
const startY = e.clientY;
|
|
326
|
+
const origLeft = rect.left;
|
|
327
|
+
const origTop = rect.top;
|
|
328
|
+
|
|
329
|
+
function onMove(ev: MouseEvent) {
|
|
330
|
+
popoverEl.style.left = `${origLeft + (ev.clientX - startX)}px`;
|
|
331
|
+
popoverEl.style.top = `${origTop + (ev.clientY - startY)}px`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function onUp() {
|
|
335
|
+
window.removeEventListener('mousemove', onMove);
|
|
336
|
+
window.removeEventListener('mouseup', onUp);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
window.addEventListener('mousemove', onMove);
|
|
340
|
+
window.addEventListener('mouseup', onUp);
|
|
341
|
+
}
|
|
342
|
+
|
|
147
343
|
let popoverStyle = $derived.by(() => {
|
|
148
344
|
if (!anchorPoint) return '';
|
|
149
345
|
|
|
@@ -169,24 +365,27 @@
|
|
|
169
365
|
return `left: ${left}px; top: ${top}px; max-height: min(600px, ${maxH}px);`;
|
|
170
366
|
});
|
|
171
367
|
|
|
172
|
-
function toggleBlock(
|
|
368
|
+
function toggleBlock(editorIndex: number, x: number, y: number) {
|
|
173
369
|
editingFrontmatter = false;
|
|
174
|
-
|
|
370
|
+
const eb = editorBlocks[editorIndex];
|
|
371
|
+
if (!eb) return;
|
|
372
|
+
|
|
373
|
+
if (activeIndex === editorIndex) {
|
|
175
374
|
activeIndex = null;
|
|
176
375
|
anchorPoint = null;
|
|
177
376
|
pendingRuneIndex = null;
|
|
178
377
|
} else {
|
|
179
378
|
editSessionId++;
|
|
180
|
-
activeIndex =
|
|
379
|
+
activeIndex = editorIndex;
|
|
181
380
|
anchorPoint = { x, y };
|
|
182
381
|
pendingRuneIndex = null;
|
|
183
382
|
}
|
|
184
383
|
}
|
|
185
384
|
|
|
186
|
-
function handleRuneClick(
|
|
385
|
+
function handleRuneClick(editorIndex: number, x: number, y: number, nestedRuneIndex?: number) {
|
|
187
386
|
editingFrontmatter = false;
|
|
188
387
|
editSessionId++;
|
|
189
|
-
activeIndex =
|
|
388
|
+
activeIndex = editorIndex;
|
|
190
389
|
anchorPoint = { x, y };
|
|
191
390
|
pendingRuneIndex = nestedRuneIndex ?? null;
|
|
192
391
|
}
|
|
@@ -205,6 +404,10 @@
|
|
|
205
404
|
function handleKeydown(e: KeyboardEvent) {
|
|
206
405
|
if (readOnly) return;
|
|
207
406
|
if (e.key === 'Escape') {
|
|
407
|
+
if (proseInlineEdit) {
|
|
408
|
+
closeProseInlineEdit();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
208
411
|
if (activeIndex !== null || editingFrontmatter) {
|
|
209
412
|
activeIndex = null;
|
|
210
413
|
editingFrontmatter = false;
|
|
@@ -229,21 +432,24 @@
|
|
|
229
432
|
|
|
230
433
|
// ── Block operations ─────────────────────────────────────────
|
|
231
434
|
|
|
232
|
-
|
|
233
|
-
|
|
435
|
+
/** Update a block by its flat index in the blocks array */
|
|
436
|
+
function handleUpdateBlock(flatIndex: number, updated: ParsedBlock) {
|
|
437
|
+
blocks = blocks.map((b, i) => (i === flatIndex ? updated : b));
|
|
234
438
|
syncToSource();
|
|
235
439
|
}
|
|
236
440
|
|
|
237
|
-
|
|
441
|
+
/** Remove a block by its editor block index */
|
|
442
|
+
function handleRemoveEditorBlock(editorIndex: number) {
|
|
443
|
+
const [startFlat, endFlat] = editorBlockToFlatRange(editorIndex);
|
|
238
444
|
// Adjust activeIndex
|
|
239
445
|
if (activeIndex !== null) {
|
|
240
|
-
if (activeIndex ===
|
|
446
|
+
if (activeIndex === editorIndex) {
|
|
241
447
|
activeIndex = null;
|
|
242
|
-
} else if (activeIndex >
|
|
448
|
+
} else if (activeIndex > editorIndex) {
|
|
243
449
|
activeIndex--;
|
|
244
450
|
}
|
|
245
451
|
}
|
|
246
|
-
blocks = blocks.
|
|
452
|
+
blocks = [...blocks.slice(0, startFlat), ...blocks.slice(endFlat + 1)];
|
|
247
453
|
syncToSource();
|
|
248
454
|
}
|
|
249
455
|
|
|
@@ -274,17 +480,32 @@
|
|
|
274
480
|
function handleDrop(e: DragEvent, index: number) {
|
|
275
481
|
e.preventDefault();
|
|
276
482
|
if (dragIndex !== null && dragIndex !== index) {
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
483
|
+
// Get the flat block ranges for source and destination
|
|
484
|
+
const [srcStart, srcEnd] = editorBlockToFlatRange(dragIndex);
|
|
485
|
+
const movedBlocks = blocks.slice(srcStart, srcEnd + 1);
|
|
486
|
+
|
|
487
|
+
// Remove the source blocks
|
|
488
|
+
const without = [...blocks.slice(0, srcStart), ...blocks.slice(srcEnd + 1)];
|
|
489
|
+
|
|
490
|
+
// Find the insertion point in the filtered array
|
|
491
|
+
let insertAt: number;
|
|
492
|
+
if (index >= editorBlocks.length) {
|
|
493
|
+
insertAt = without.length;
|
|
494
|
+
} else {
|
|
495
|
+
// Recalculate target position in the reduced array
|
|
496
|
+
const [tgtStart] = editorBlockToFlatRange(index);
|
|
497
|
+
// Adjust for removed blocks if source was before target
|
|
498
|
+
insertAt = tgtStart > srcEnd ? tgtStart - movedBlocks.length : tgtStart;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
without.splice(insertAt, 0, ...movedBlocks);
|
|
502
|
+
blocks = without;
|
|
281
503
|
|
|
282
504
|
// Update activeIndex to follow the active block
|
|
283
505
|
if (activeIndex !== null) {
|
|
284
506
|
if (activeIndex === dragIndex) {
|
|
285
507
|
activeIndex = index > dragIndex ? index - 1 : index;
|
|
286
508
|
} else {
|
|
287
|
-
// Adjust if the move shifts the active block's position
|
|
288
509
|
let newActive = activeIndex;
|
|
289
510
|
if (dragIndex < activeIndex && index >= activeIndex) {
|
|
290
511
|
newActive--;
|
|
@@ -408,12 +629,29 @@
|
|
|
408
629
|
}
|
|
409
630
|
}
|
|
410
631
|
|
|
411
|
-
|
|
412
|
-
|
|
632
|
+
// Convert editor block index to flat block insertion position
|
|
633
|
+
const editorPos = insertAtIndex ?? editorBlocks.length;
|
|
634
|
+
let flatPos: number;
|
|
635
|
+
if (editorPos >= editorBlocks.length) {
|
|
636
|
+
flatPos = blocks.length;
|
|
637
|
+
} else {
|
|
638
|
+
const [start] = editorBlockToFlatRange(editorPos);
|
|
639
|
+
flatPos = start;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
blocks = [...blocks.slice(0, flatPos), newBlock, ...blocks.slice(flatPos)];
|
|
413
643
|
insertAtIndex = null;
|
|
414
644
|
showInsertMenu = false;
|
|
415
645
|
editingFrontmatter = false;
|
|
416
|
-
activeIndex
|
|
646
|
+
// Set activeIndex for rune blocks; for prose blocks the new element merges into a prose block
|
|
647
|
+
if (type === 'rune') {
|
|
648
|
+
// Find the new block's editor index after re-grouping
|
|
649
|
+
const newEditorBlocks = groupIntoEditorBlocks(blocks);
|
|
650
|
+
activeIndex = newEditorBlocks.findIndex(eb => eb.type === 'rune' && eb === blocks[flatPos]);
|
|
651
|
+
if (activeIndex === -1) activeIndex = null;
|
|
652
|
+
} else {
|
|
653
|
+
activeIndex = null;
|
|
654
|
+
}
|
|
417
655
|
syncToSource();
|
|
418
656
|
}
|
|
419
657
|
|
|
@@ -427,10 +665,15 @@
|
|
|
427
665
|
mapping: SectionMapping;
|
|
428
666
|
} | null = $state(null);
|
|
429
667
|
|
|
430
|
-
function handleSectionClick(
|
|
431
|
-
const
|
|
668
|
+
function handleSectionClick(editorIndex: number, info: SectionClickInfo) {
|
|
669
|
+
const eb = editorBlocks[editorIndex];
|
|
670
|
+
if (!eb || eb.type !== 'rune') return;
|
|
671
|
+
const [flatIndex] = editorBlockToFlatRange(editorIndex);
|
|
672
|
+
const block = blocks[flatIndex];
|
|
432
673
|
if (block.type !== 'rune') return;
|
|
433
674
|
const rb = block as RuneBlock;
|
|
675
|
+
// Use flatIndex for all block operations below
|
|
676
|
+
const index = flatIndex;
|
|
434
677
|
|
|
435
678
|
if (info.editType === 'link') {
|
|
436
679
|
const mapping = findActionMapping(rb.innerContent, info.text, info.href ?? '');
|
|
@@ -456,6 +699,30 @@
|
|
|
456
699
|
return;
|
|
457
700
|
}
|
|
458
701
|
|
|
702
|
+
if (info.editType === 'image') {
|
|
703
|
+
const imgSrc = info.href ?? '';
|
|
704
|
+
const mapping = findImageMapping(rb.innerContent, imgSrc);
|
|
705
|
+
if (!mapping) return;
|
|
706
|
+
|
|
707
|
+
imageEdit = {
|
|
708
|
+
blockIndex: index,
|
|
709
|
+
mapping,
|
|
710
|
+
};
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (info.editType === 'icon') {
|
|
715
|
+
const iconName = info.iconName ?? '';
|
|
716
|
+
const mapping = findIconMapping(rb.innerContent, iconName);
|
|
717
|
+
if (!mapping) return;
|
|
718
|
+
|
|
719
|
+
iconEdit = {
|
|
720
|
+
blockIndex: index,
|
|
721
|
+
mapping,
|
|
722
|
+
};
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
459
726
|
// Default: inline text editing
|
|
460
727
|
const mapping = findSectionMapping(rb.innerContent, info.dataName, info.text);
|
|
461
728
|
if (!mapping) return;
|
|
@@ -550,6 +817,16 @@
|
|
|
550
817
|
function handleCommandEditChange(newCode: string) {
|
|
551
818
|
if (!commandEdit) return;
|
|
552
819
|
const block = blocks[commandEdit.blockIndex];
|
|
820
|
+
|
|
821
|
+
if (block.type === 'fence') {
|
|
822
|
+
// Direct fence block (from prose section)
|
|
823
|
+
const fb = block as FenceBlock;
|
|
824
|
+
const updated: FenceBlock = { ...fb, code: newCode, source: '' };
|
|
825
|
+
updated.source = rebuildFenceSource(updated);
|
|
826
|
+
handleUpdateBlock(commandEdit.blockIndex, updated);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
553
830
|
if (block.type !== 'rune') return;
|
|
554
831
|
const rb = block as RuneBlock;
|
|
555
832
|
|
|
@@ -564,6 +841,15 @@
|
|
|
564
841
|
if (!commandEdit) return;
|
|
565
842
|
const blockIndex = commandEdit.blockIndex;
|
|
566
843
|
const block = blocks[blockIndex];
|
|
844
|
+
|
|
845
|
+
if (block.type === 'fence') {
|
|
846
|
+
// Direct fence block — remove entirely
|
|
847
|
+
commandEdit = null;
|
|
848
|
+
blocks = blocks.filter((_, i) => i !== blockIndex);
|
|
849
|
+
syncToSource();
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
567
853
|
if (block.type !== 'rune') return;
|
|
568
854
|
const rb = block as RuneBlock;
|
|
569
855
|
|
|
@@ -580,12 +866,127 @@
|
|
|
580
866
|
commandEdit = null;
|
|
581
867
|
}
|
|
582
868
|
|
|
869
|
+
function handleLanguageChange(newLanguage: string) {
|
|
870
|
+
if (!commandEdit) return;
|
|
871
|
+
const block = blocks[commandEdit.blockIndex];
|
|
872
|
+
|
|
873
|
+
if (block.type === 'fence') {
|
|
874
|
+
// Direct fence block (from prose section)
|
|
875
|
+
const fb = block as FenceBlock;
|
|
876
|
+
const updated: FenceBlock = { ...fb, language: newLanguage, source: '' };
|
|
877
|
+
updated.source = rebuildFenceSource(updated);
|
|
878
|
+
|
|
879
|
+
// Update the mapping
|
|
880
|
+
const afterDelimiter = commandEdit.mapping.opener.slice(commandEdit.mapping.delimiter.length);
|
|
881
|
+
const infoString = afterDelimiter.replace(/^\w*/, '').trim();
|
|
882
|
+
const newOpener = commandEdit.mapping.delimiter + newLanguage + (infoString ? ' ' + infoString : '');
|
|
883
|
+
const newSource = newOpener + '\n' + commandEdit.mapping.code + '\n' + commandEdit.mapping.delimiter;
|
|
884
|
+
commandEdit = {
|
|
885
|
+
...commandEdit,
|
|
886
|
+
mapping: { ...commandEdit.mapping, language: newLanguage, opener: newOpener, source: newSource },
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
handleUpdateBlock(commandEdit.blockIndex, updated);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (block.type !== 'rune') return;
|
|
894
|
+
const rb = block as RuneBlock;
|
|
895
|
+
|
|
896
|
+
const newInner = applyLanguageEdit(rb.innerContent, commandEdit.mapping, newLanguage);
|
|
897
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
898
|
+
updated.source = rebuildRuneSource(updated);
|
|
899
|
+
|
|
900
|
+
// Update the mapping to reflect the new language and opener
|
|
901
|
+
const afterDelimiter = commandEdit.mapping.opener.slice(commandEdit.mapping.delimiter.length);
|
|
902
|
+
const infoString = afterDelimiter.replace(/^\w*/, '').trim();
|
|
903
|
+
const newOpener = commandEdit.mapping.delimiter + newLanguage + (infoString ? ' ' + infoString : '');
|
|
904
|
+
const newSource = newOpener + '\n' + commandEdit.mapping.code + '\n' + commandEdit.mapping.delimiter;
|
|
905
|
+
|
|
906
|
+
commandEdit = {
|
|
907
|
+
...commandEdit,
|
|
908
|
+
mapping: {
|
|
909
|
+
...commandEdit.mapping,
|
|
910
|
+
language: newLanguage,
|
|
911
|
+
opener: newOpener,
|
|
912
|
+
source: newSource,
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
handleUpdateBlock(commandEdit.blockIndex, updated);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// ── Image editing ────────────────────────────────────────────
|
|
920
|
+
|
|
921
|
+
let imageEdit: {
|
|
922
|
+
blockIndex: number;
|
|
923
|
+
mapping: ImageMapping;
|
|
924
|
+
} | null = $state(null);
|
|
925
|
+
|
|
926
|
+
function handleImageEditChange(newSrc: string, newAlt: string) {
|
|
927
|
+
if (!imageEdit) return;
|
|
928
|
+
const block = blocks[imageEdit.blockIndex];
|
|
929
|
+
if (block.type !== 'rune') return;
|
|
930
|
+
const rb = block as RuneBlock;
|
|
931
|
+
|
|
932
|
+
const newInner = applyImageEdit(rb.innerContent, imageEdit.mapping, newAlt, newSrc);
|
|
933
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
934
|
+
updated.source = rebuildRuneSource(updated);
|
|
935
|
+
|
|
936
|
+
handleUpdateBlock(imageEdit.blockIndex, updated);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function handleImageRemove() {
|
|
940
|
+
if (!imageEdit) return;
|
|
941
|
+
const blockIndex = imageEdit.blockIndex;
|
|
942
|
+
const block = blocks[blockIndex];
|
|
943
|
+
if (block.type !== 'rune') return;
|
|
944
|
+
const rb = block as RuneBlock;
|
|
945
|
+
|
|
946
|
+
const newInner = rb.innerContent.replace(imageEdit.mapping.source + '\n', '').replace(imageEdit.mapping.source, '');
|
|
947
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
948
|
+
updated.source = rebuildRuneSource(updated);
|
|
949
|
+
|
|
950
|
+
imageEdit = null;
|
|
951
|
+
handleUpdateBlock(blockIndex, updated);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function closeImageEdit() {
|
|
955
|
+
imageEdit = null;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// ── Icon editing ────────────────────────────────────────────
|
|
959
|
+
|
|
960
|
+
let iconEdit: {
|
|
961
|
+
blockIndex: number;
|
|
962
|
+
mapping: IconMapping;
|
|
963
|
+
} | null = $state(null);
|
|
964
|
+
|
|
965
|
+
function handleIconEditChange(newIconName: string) {
|
|
966
|
+
if (!iconEdit) return;
|
|
967
|
+
const block = blocks[iconEdit.blockIndex];
|
|
968
|
+
if (block.type !== 'rune') return;
|
|
969
|
+
const rb = block as RuneBlock;
|
|
970
|
+
|
|
971
|
+
const newInner = applyIconEdit(rb.innerContent, iconEdit.mapping, newIconName);
|
|
972
|
+
const updated: RuneBlock = { ...rb, innerContent: newInner, source: '' };
|
|
973
|
+
updated.source = rebuildRuneSource(updated);
|
|
974
|
+
|
|
975
|
+
handleUpdateBlock(iconEdit.blockIndex, updated);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function closeIconEdit() {
|
|
979
|
+
iconEdit = null;
|
|
980
|
+
}
|
|
981
|
+
|
|
583
982
|
// ── Field edit from Structure tab ──────────────────────────────
|
|
584
983
|
|
|
585
984
|
function handleFieldEdit(dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) {
|
|
586
985
|
if (activeIndex === null) return;
|
|
986
|
+
const [flatIndex] = editorBlockToFlatRange(activeIndex);
|
|
987
|
+
commandEdit = null;
|
|
587
988
|
inlineEdit = {
|
|
588
|
-
blockIndex:
|
|
989
|
+
blockIndex: flatIndex,
|
|
589
990
|
dataName,
|
|
590
991
|
inlineSource,
|
|
591
992
|
rect,
|
|
@@ -593,6 +994,17 @@
|
|
|
593
994
|
};
|
|
594
995
|
}
|
|
595
996
|
|
|
997
|
+
function handleFieldCodeEdit(code: string, language: string, rect: DOMRect, mapping: CommandMapping) {
|
|
998
|
+
if (activeIndex === null) return;
|
|
999
|
+
const [flatIndex] = editorBlockToFlatRange(activeIndex);
|
|
1000
|
+
inlineEdit = null;
|
|
1001
|
+
commandEdit = {
|
|
1002
|
+
blockIndex: flatIndex,
|
|
1003
|
+
rect,
|
|
1004
|
+
mapping,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
596
1008
|
// Group runes by category for the insert menu
|
|
597
1009
|
let runesByCategory = $derived.by(() => {
|
|
598
1010
|
const map = new Map<string, RuneInfo[]>();
|
|
@@ -648,7 +1060,7 @@
|
|
|
648
1060
|
</div>
|
|
649
1061
|
{/if}
|
|
650
1062
|
|
|
651
|
-
{#each
|
|
1063
|
+
{#each editorBlocks as eb, i (eb.id)}
|
|
652
1064
|
<div
|
|
653
1065
|
class="block-editor__row"
|
|
654
1066
|
class:hovered={!readOnly && hoveredIndex === i}
|
|
@@ -659,23 +1071,43 @@
|
|
|
659
1071
|
onmouseleave={() => { if (!readOnly && hoveredIndex === i) hoveredIndex = null; }}
|
|
660
1072
|
>
|
|
661
1073
|
<div class="block-editor__block-cell">
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1074
|
+
{#if eb.type === 'prose'}
|
|
1075
|
+
<ProseBlockCard
|
|
1076
|
+
block={eb}
|
|
1077
|
+
{themeConfig}
|
|
1078
|
+
{themeCss}
|
|
1079
|
+
{highlightCss}
|
|
1080
|
+
{highlightTransform}
|
|
1081
|
+
{communityTags}
|
|
1082
|
+
{communityPostTransforms}
|
|
1083
|
+
{communityStyles}
|
|
1084
|
+
{aggregated}
|
|
1085
|
+
{readOnly}
|
|
1086
|
+
onsectionclick={readOnly ? undefined : (info) => handleProseSectionClick(i, info)}
|
|
1087
|
+
onblockclick={readOnly ? undefined : (info) => toggleBlock(i, info.x, info.y)}
|
|
1088
|
+
ondragstart={readOnly ? undefined : (e) => handleDragStart(e, i)}
|
|
1089
|
+
ondragover={readOnly ? undefined : (e) => handleDragOver(e, i)}
|
|
1090
|
+
ondrop={readOnly ? undefined : (e) => handleDrop(e, i)}
|
|
1091
|
+
/>
|
|
1092
|
+
{:else}
|
|
1093
|
+
<BlockCard
|
|
1094
|
+
block={eb}
|
|
1095
|
+
{themeConfig}
|
|
1096
|
+
{themeCss}
|
|
1097
|
+
{highlightCss}
|
|
1098
|
+
{highlightTransform}
|
|
1099
|
+
{communityTags}
|
|
1100
|
+
{communityPostTransforms}
|
|
1101
|
+
{communityStyles}
|
|
1102
|
+
{aggregated}
|
|
1103
|
+
{readOnly}
|
|
1104
|
+
onsectionclick={readOnly ? undefined : (info) => handleSectionClick(i, info)}
|
|
1105
|
+
onruneclick={readOnly ? undefined : (info) => handleRuneClick(i, info.x, info.y, info.nestedRuneIndex)}
|
|
1106
|
+
ondragstart={readOnly ? undefined : (e) => handleDragStart(e, i)}
|
|
1107
|
+
ondragover={readOnly ? undefined : (e) => handleDragOver(e, i)}
|
|
1108
|
+
ondrop={readOnly ? undefined : (e) => handleDrop(e, i)}
|
|
1109
|
+
/>
|
|
1110
|
+
{/if}
|
|
679
1111
|
</div>
|
|
680
1112
|
{#if !readOnly && showInsertMenuProp}
|
|
681
1113
|
<!-- Insert markers — top and bottom edges -->
|
|
@@ -707,7 +1139,7 @@
|
|
|
707
1139
|
onclick={(e) => toggleBlock(i, e.clientX, e.clientY)}
|
|
708
1140
|
aria-pressed={activeIndex === i}
|
|
709
1141
|
>
|
|
710
|
-
{blockLabel(
|
|
1142
|
+
{blockLabel(eb)}
|
|
711
1143
|
</button>
|
|
712
1144
|
{/if}
|
|
713
1145
|
</div>
|
|
@@ -724,23 +1156,37 @@
|
|
|
724
1156
|
class="block-editor__popover-backdrop"
|
|
725
1157
|
onmousedown={() => { activeIndex = null; editingFrontmatter = false; anchorPoint = null; pendingRuneIndex = null; }}
|
|
726
1158
|
></div>
|
|
727
|
-
<div class="block-editor__popover" style={popoverStyle}>
|
|
1159
|
+
<div class="block-editor__popover" style={popoverStyle} onmousedown={handlePopoverDragStart}>
|
|
728
1160
|
{#if editingFrontmatter}
|
|
729
1161
|
<FrontmatterEditPanel
|
|
730
1162
|
onclose={() => { editingFrontmatter = false; anchorPoint = null; }}
|
|
731
1163
|
/>
|
|
732
|
-
{:else if activeIndex !== null &&
|
|
1164
|
+
{:else if activeIndex !== null && editorBlocks[activeIndex]?.type === 'rune'}
|
|
1165
|
+
{@const activeBlock = blocks[editorBlockToFlatRange(activeIndex)[0]]}
|
|
733
1166
|
{#key editSessionId}
|
|
734
1167
|
<BlockEditPanel
|
|
735
|
-
block={
|
|
1168
|
+
block={activeBlock}
|
|
736
1169
|
{runeMap}
|
|
737
1170
|
runes={() => runes}
|
|
738
1171
|
{aggregated}
|
|
739
1172
|
initialRuneIndex={pendingRuneIndex}
|
|
740
|
-
onupdate={(updated) => handleUpdateBlock(activeIndex
|
|
741
|
-
onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; pendingRuneIndex = null;
|
|
1173
|
+
onupdate={(updated) => handleUpdateBlock(editorBlockToFlatRange(activeIndex!)[0], updated)}
|
|
1174
|
+
onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; pendingRuneIndex = null; handleRemoveEditorBlock(idx); }}
|
|
742
1175
|
onclose={() => { activeIndex = null; anchorPoint = null; pendingRuneIndex = null; }}
|
|
743
1176
|
oneditfield={handleFieldEdit}
|
|
1177
|
+
oneditcode={handleFieldCodeEdit}
|
|
1178
|
+
/>
|
|
1179
|
+
{/key}
|
|
1180
|
+
{:else if activeIndex !== null && editorBlocks[activeIndex]?.type === 'prose'}
|
|
1181
|
+
{@const proseBlock = editorBlocks[activeIndex] as ProseBlock}
|
|
1182
|
+
{#key editSessionId}
|
|
1183
|
+
<ProseEditPanel
|
|
1184
|
+
block={proseBlock}
|
|
1185
|
+
runes={() => runes}
|
|
1186
|
+
{aggregated}
|
|
1187
|
+
onupdate={(updated) => handleUpdateProseBlock(activeIndex!, updated)}
|
|
1188
|
+
onremove={() => { const idx = activeIndex!; activeIndex = null; anchorPoint = null; handleRemoveEditorBlock(idx); }}
|
|
1189
|
+
onclose={() => { activeIndex = null; anchorPoint = null; }}
|
|
744
1190
|
/>
|
|
745
1191
|
{/key}
|
|
746
1192
|
{/if}
|
|
@@ -783,10 +1229,40 @@
|
|
|
783
1229
|
code={commandEdit.mapping.code}
|
|
784
1230
|
language={commandEdit.mapping.language}
|
|
785
1231
|
onchange={handleCommandEditChange}
|
|
1232
|
+
onlanguagechange={handleLanguageChange}
|
|
786
1233
|
onremove={handleCommandRemove}
|
|
787
1234
|
onclose={closeCommandEdit}
|
|
788
1235
|
/>
|
|
789
1236
|
{/if}
|
|
1237
|
+
|
|
1238
|
+
{#if imageEdit}
|
|
1239
|
+
<ImageEditPopover
|
|
1240
|
+
currentSrc={imageEdit.mapping.src}
|
|
1241
|
+
currentAlt={imageEdit.mapping.alt}
|
|
1242
|
+
onchange={(src, alt) => { handleImageEditChange(src, alt); closeImageEdit(); }}
|
|
1243
|
+
onremove={handleImageRemove}
|
|
1244
|
+
onclose={closeImageEdit}
|
|
1245
|
+
/>
|
|
1246
|
+
{/if}
|
|
1247
|
+
|
|
1248
|
+
{#if iconEdit}
|
|
1249
|
+
<IconPickerPopover
|
|
1250
|
+
icons={themeConfig?.icons ?? {}}
|
|
1251
|
+
currentIcon={iconEdit.mapping.name}
|
|
1252
|
+
onchange={(name) => { handleIconEditChange(name); closeIconEdit(); }}
|
|
1253
|
+
onclose={closeIconEdit}
|
|
1254
|
+
/>
|
|
1255
|
+
{/if}
|
|
1256
|
+
|
|
1257
|
+
{#if proseInlineEdit}
|
|
1258
|
+
<InlineEditPopover
|
|
1259
|
+
anchorRect={proseInlineEdit.rect}
|
|
1260
|
+
dataName={proseInlineEdit.mapping.dataName}
|
|
1261
|
+
inlineSource={proseInlineEdit.inlineSource}
|
|
1262
|
+
onchange={handleProseInlineEditChange}
|
|
1263
|
+
onclose={closeProseInlineEdit}
|
|
1264
|
+
/>
|
|
1265
|
+
{/if}
|
|
790
1266
|
</div>
|
|
791
1267
|
|
|
792
1268
|
|
|
@@ -1005,7 +1481,9 @@
|
|
|
1005
1481
|
.block-editor__popover {
|
|
1006
1482
|
position: fixed;
|
|
1007
1483
|
width: 420px;
|
|
1008
|
-
overflow
|
|
1484
|
+
overflow: hidden;
|
|
1485
|
+
display: flex;
|
|
1486
|
+
flex-direction: column;
|
|
1009
1487
|
background: var(--ed-surface-0);
|
|
1010
1488
|
border-radius: var(--ed-radius-lg);
|
|
1011
1489
|
border: 1px solid var(--ed-border-default);
|
|
@@ -1015,6 +1493,14 @@
|
|
|
1015
1493
|
animation: popover-enter 0.15s ease-out;
|
|
1016
1494
|
}
|
|
1017
1495
|
|
|
1496
|
+
.block-editor__popover :global(.edit-panel__header) {
|
|
1497
|
+
cursor: grab;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.block-editor__popover :global(.edit-panel__header):active {
|
|
1501
|
+
cursor: grabbing;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1018
1504
|
@keyframes popover-enter {
|
|
1019
1505
|
from {
|
|
1020
1506
|
opacity: 0;
|