@refrakt-md/editor 0.8.2 → 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-Bn8ajfVl.js → index-3MvwKRVQ.js} +1 -1
- package/app/dist/assets/{index-DNtuldOx.js → index-B7e694w6.js} +1 -1
- package/app/dist/assets/{index-xo7v6nRB.js → index-BBljOYQu.js} +1 -1
- package/app/dist/assets/{index-DQUOY-pF.js → index-BEGy_i8o.js} +1 -1
- package/app/dist/assets/{index-DNJBunzP.js → index-BGy7ixjW.js} +1 -1
- package/app/dist/assets/{index-dGztG-54.js → index-BaLgiiKk.js} +1 -1
- package/app/dist/assets/{index-D5ucdUTo.js → index-BjlNcvOf.js} +1 -1
- package/app/dist/assets/{index-Cgbvx23V.js → index-CKfKYVw7.js} +1 -1
- package/app/dist/assets/{index-BDj1XPol.js → index-COFbngzR.js} +1 -1
- package/app/dist/assets/{index-aPeHMqUX.js → index-CPEo_rvd.js} +1 -1
- package/app/dist/assets/{index-BXe1fKaT.js → index-CQDCT-XT.js} +1 -1
- package/app/dist/assets/{index-CXeK-dZx.js → index-CUmEjEeR.js} +1 -1
- package/app/dist/assets/{index-80NtMar1.js → index-CeV-Af4N.js} +1 -1
- package/app/dist/assets/{index-CaRBCHaX.js → index-ChbH55h5.js} +1 -1
- package/app/dist/assets/index-CzvG5PZT.css +1 -0
- package/app/dist/assets/{index-BfxTGrHB.js → index-D9-aYc3I.js} +1 -1
- package/app/dist/assets/{index-DGYxLhpR.js → index-DezxtfNV.js} +1 -1
- package/app/dist/assets/{index-DskvyNKT.js → index-DrI4IfXE.js} +1 -1
- package/app/dist/assets/{index-CCkzIGTi.js → index-DwfxgjnU.js} +1 -1
- package/app/dist/assets/index-ogrpJNou.js +555 -0
- package/app/dist/index.html +2 -2
- package/app/src/lib/components/BlockEditor.svelte +381 -47
- 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/editor/block-parser.ts +59 -2
- package/package.json +6 -6
- package/app/dist/assets/index-B6H6LF1M.css +0 -1
- package/app/dist/assets/index-Cd12jZId.js +0 -479
package/app/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>refrakt editor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-ogrpJNou.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CzvG5PZT.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app"></div>
|
|
@@ -9,15 +9,25 @@
|
|
|
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
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';
|
|
@@ -114,6 +124,7 @@
|
|
|
114
124
|
editingFrontmatter = false;
|
|
115
125
|
anchorPoint = null;
|
|
116
126
|
inlineEdit = null;
|
|
127
|
+
proseInlineEdit = null;
|
|
117
128
|
}
|
|
118
129
|
});
|
|
119
130
|
|
|
@@ -124,6 +135,162 @@
|
|
|
124
135
|
onchange(newSource);
|
|
125
136
|
}
|
|
126
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
|
+
|
|
127
294
|
// ── Frontmatter summary for visual mode header ──────────────
|
|
128
295
|
|
|
129
296
|
let editingFrontmatter = $state(false);
|
|
@@ -146,6 +313,33 @@
|
|
|
146
313
|
const POPOVER_WIDTH = 420;
|
|
147
314
|
const POPOVER_GAP = 12;
|
|
148
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
|
+
|
|
149
343
|
let popoverStyle = $derived.by(() => {
|
|
150
344
|
if (!anchorPoint) return '';
|
|
151
345
|
|
|
@@ -171,24 +365,27 @@
|
|
|
171
365
|
return `left: ${left}px; top: ${top}px; max-height: min(600px, ${maxH}px);`;
|
|
172
366
|
});
|
|
173
367
|
|
|
174
|
-
function toggleBlock(
|
|
368
|
+
function toggleBlock(editorIndex: number, x: number, y: number) {
|
|
175
369
|
editingFrontmatter = false;
|
|
176
|
-
|
|
370
|
+
const eb = editorBlocks[editorIndex];
|
|
371
|
+
if (!eb) return;
|
|
372
|
+
|
|
373
|
+
if (activeIndex === editorIndex) {
|
|
177
374
|
activeIndex = null;
|
|
178
375
|
anchorPoint = null;
|
|
179
376
|
pendingRuneIndex = null;
|
|
180
377
|
} else {
|
|
181
378
|
editSessionId++;
|
|
182
|
-
activeIndex =
|
|
379
|
+
activeIndex = editorIndex;
|
|
183
380
|
anchorPoint = { x, y };
|
|
184
381
|
pendingRuneIndex = null;
|
|
185
382
|
}
|
|
186
383
|
}
|
|
187
384
|
|
|
188
|
-
function handleRuneClick(
|
|
385
|
+
function handleRuneClick(editorIndex: number, x: number, y: number, nestedRuneIndex?: number) {
|
|
189
386
|
editingFrontmatter = false;
|
|
190
387
|
editSessionId++;
|
|
191
|
-
activeIndex =
|
|
388
|
+
activeIndex = editorIndex;
|
|
192
389
|
anchorPoint = { x, y };
|
|
193
390
|
pendingRuneIndex = nestedRuneIndex ?? null;
|
|
194
391
|
}
|
|
@@ -207,6 +404,10 @@
|
|
|
207
404
|
function handleKeydown(e: KeyboardEvent) {
|
|
208
405
|
if (readOnly) return;
|
|
209
406
|
if (e.key === 'Escape') {
|
|
407
|
+
if (proseInlineEdit) {
|
|
408
|
+
closeProseInlineEdit();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
210
411
|
if (activeIndex !== null || editingFrontmatter) {
|
|
211
412
|
activeIndex = null;
|
|
212
413
|
editingFrontmatter = false;
|
|
@@ -231,21 +432,24 @@
|
|
|
231
432
|
|
|
232
433
|
// ── Block operations ─────────────────────────────────────────
|
|
233
434
|
|
|
234
|
-
|
|
235
|
-
|
|
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));
|
|
236
438
|
syncToSource();
|
|
237
439
|
}
|
|
238
440
|
|
|
239
|
-
|
|
441
|
+
/** Remove a block by its editor block index */
|
|
442
|
+
function handleRemoveEditorBlock(editorIndex: number) {
|
|
443
|
+
const [startFlat, endFlat] = editorBlockToFlatRange(editorIndex);
|
|
240
444
|
// Adjust activeIndex
|
|
241
445
|
if (activeIndex !== null) {
|
|
242
|
-
if (activeIndex ===
|
|
446
|
+
if (activeIndex === editorIndex) {
|
|
243
447
|
activeIndex = null;
|
|
244
|
-
} else if (activeIndex >
|
|
448
|
+
} else if (activeIndex > editorIndex) {
|
|
245
449
|
activeIndex--;
|
|
246
450
|
}
|
|
247
451
|
}
|
|
248
|
-
blocks = blocks.
|
|
452
|
+
blocks = [...blocks.slice(0, startFlat), ...blocks.slice(endFlat + 1)];
|
|
249
453
|
syncToSource();
|
|
250
454
|
}
|
|
251
455
|
|
|
@@ -276,17 +480,32 @@
|
|
|
276
480
|
function handleDrop(e: DragEvent, index: number) {
|
|
277
481
|
e.preventDefault();
|
|
278
482
|
if (dragIndex !== null && dragIndex !== index) {
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
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;
|
|
283
503
|
|
|
284
504
|
// Update activeIndex to follow the active block
|
|
285
505
|
if (activeIndex !== null) {
|
|
286
506
|
if (activeIndex === dragIndex) {
|
|
287
507
|
activeIndex = index > dragIndex ? index - 1 : index;
|
|
288
508
|
} else {
|
|
289
|
-
// Adjust if the move shifts the active block's position
|
|
290
509
|
let newActive = activeIndex;
|
|
291
510
|
if (dragIndex < activeIndex && index >= activeIndex) {
|
|
292
511
|
newActive--;
|
|
@@ -410,12 +629,29 @@
|
|
|
410
629
|
}
|
|
411
630
|
}
|
|
412
631
|
|
|
413
|
-
|
|
414
|
-
|
|
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)];
|
|
415
643
|
insertAtIndex = null;
|
|
416
644
|
showInsertMenu = false;
|
|
417
645
|
editingFrontmatter = false;
|
|
418
|
-
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
|
+
}
|
|
419
655
|
syncToSource();
|
|
420
656
|
}
|
|
421
657
|
|
|
@@ -429,10 +665,15 @@
|
|
|
429
665
|
mapping: SectionMapping;
|
|
430
666
|
} | null = $state(null);
|
|
431
667
|
|
|
432
|
-
function handleSectionClick(
|
|
433
|
-
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];
|
|
434
673
|
if (block.type !== 'rune') return;
|
|
435
674
|
const rb = block as RuneBlock;
|
|
675
|
+
// Use flatIndex for all block operations below
|
|
676
|
+
const index = flatIndex;
|
|
436
677
|
|
|
437
678
|
if (info.editType === 'link') {
|
|
438
679
|
const mapping = findActionMapping(rb.innerContent, info.text, info.href ?? '');
|
|
@@ -576,6 +817,16 @@
|
|
|
576
817
|
function handleCommandEditChange(newCode: string) {
|
|
577
818
|
if (!commandEdit) return;
|
|
578
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
|
+
|
|
579
830
|
if (block.type !== 'rune') return;
|
|
580
831
|
const rb = block as RuneBlock;
|
|
581
832
|
|
|
@@ -590,6 +841,15 @@
|
|
|
590
841
|
if (!commandEdit) return;
|
|
591
842
|
const blockIndex = commandEdit.blockIndex;
|
|
592
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
|
+
|
|
593
853
|
if (block.type !== 'rune') return;
|
|
594
854
|
const rb = block as RuneBlock;
|
|
595
855
|
|
|
@@ -609,6 +869,27 @@
|
|
|
609
869
|
function handleLanguageChange(newLanguage: string) {
|
|
610
870
|
if (!commandEdit) return;
|
|
611
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
|
+
|
|
612
893
|
if (block.type !== 'rune') return;
|
|
613
894
|
const rb = block as RuneBlock;
|
|
614
895
|
|
|
@@ -702,9 +983,10 @@
|
|
|
702
983
|
|
|
703
984
|
function handleFieldEdit(dataName: string, inlineSource: string, rect: DOMRect, mapping: SectionMapping) {
|
|
704
985
|
if (activeIndex === null) return;
|
|
986
|
+
const [flatIndex] = editorBlockToFlatRange(activeIndex);
|
|
705
987
|
commandEdit = null;
|
|
706
988
|
inlineEdit = {
|
|
707
|
-
blockIndex:
|
|
989
|
+
blockIndex: flatIndex,
|
|
708
990
|
dataName,
|
|
709
991
|
inlineSource,
|
|
710
992
|
rect,
|
|
@@ -714,9 +996,10 @@
|
|
|
714
996
|
|
|
715
997
|
function handleFieldCodeEdit(code: string, language: string, rect: DOMRect, mapping: CommandMapping) {
|
|
716
998
|
if (activeIndex === null) return;
|
|
999
|
+
const [flatIndex] = editorBlockToFlatRange(activeIndex);
|
|
717
1000
|
inlineEdit = null;
|
|
718
1001
|
commandEdit = {
|
|
719
|
-
blockIndex:
|
|
1002
|
+
blockIndex: flatIndex,
|
|
720
1003
|
rect,
|
|
721
1004
|
mapping,
|
|
722
1005
|
};
|
|
@@ -777,7 +1060,7 @@
|
|
|
777
1060
|
</div>
|
|
778
1061
|
{/if}
|
|
779
1062
|
|
|
780
|
-
{#each
|
|
1063
|
+
{#each editorBlocks as eb, i (eb.id)}
|
|
781
1064
|
<div
|
|
782
1065
|
class="block-editor__row"
|
|
783
1066
|
class:hovered={!readOnly && hoveredIndex === i}
|
|
@@ -788,23 +1071,43 @@
|
|
|
788
1071
|
onmouseleave={() => { if (!readOnly && hoveredIndex === i) hoveredIndex = null; }}
|
|
789
1072
|
>
|
|
790
1073
|
<div class="block-editor__block-cell">
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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}
|
|
808
1111
|
</div>
|
|
809
1112
|
{#if !readOnly && showInsertMenuProp}
|
|
810
1113
|
<!-- Insert markers — top and bottom edges -->
|
|
@@ -836,7 +1139,7 @@
|
|
|
836
1139
|
onclick={(e) => toggleBlock(i, e.clientX, e.clientY)}
|
|
837
1140
|
aria-pressed={activeIndex === i}
|
|
838
1141
|
>
|
|
839
|
-
{blockLabel(
|
|
1142
|
+
{blockLabel(eb)}
|
|
840
1143
|
</button>
|
|
841
1144
|
{/if}
|
|
842
1145
|
</div>
|
|
@@ -853,26 +1156,39 @@
|
|
|
853
1156
|
class="block-editor__popover-backdrop"
|
|
854
1157
|
onmousedown={() => { activeIndex = null; editingFrontmatter = false; anchorPoint = null; pendingRuneIndex = null; }}
|
|
855
1158
|
></div>
|
|
856
|
-
<div class="block-editor__popover" style={popoverStyle}>
|
|
1159
|
+
<div class="block-editor__popover" style={popoverStyle} onmousedown={handlePopoverDragStart}>
|
|
857
1160
|
{#if editingFrontmatter}
|
|
858
1161
|
<FrontmatterEditPanel
|
|
859
1162
|
onclose={() => { editingFrontmatter = false; anchorPoint = null; }}
|
|
860
1163
|
/>
|
|
861
|
-
{:else if activeIndex !== null &&
|
|
1164
|
+
{:else if activeIndex !== null && editorBlocks[activeIndex]?.type === 'rune'}
|
|
1165
|
+
{@const activeBlock = blocks[editorBlockToFlatRange(activeIndex)[0]]}
|
|
862
1166
|
{#key editSessionId}
|
|
863
1167
|
<BlockEditPanel
|
|
864
|
-
block={
|
|
1168
|
+
block={activeBlock}
|
|
865
1169
|
{runeMap}
|
|
866
1170
|
runes={() => runes}
|
|
867
1171
|
{aggregated}
|
|
868
1172
|
initialRuneIndex={pendingRuneIndex}
|
|
869
|
-
onupdate={(updated) => handleUpdateBlock(activeIndex
|
|
870
|
-
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); }}
|
|
871
1175
|
onclose={() => { activeIndex = null; anchorPoint = null; pendingRuneIndex = null; }}
|
|
872
1176
|
oneditfield={handleFieldEdit}
|
|
873
1177
|
oneditcode={handleFieldCodeEdit}
|
|
874
1178
|
/>
|
|
875
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; }}
|
|
1190
|
+
/>
|
|
1191
|
+
{/key}
|
|
876
1192
|
{/if}
|
|
877
1193
|
</div>
|
|
878
1194
|
{/if}
|
|
@@ -937,6 +1253,16 @@
|
|
|
937
1253
|
onclose={closeIconEdit}
|
|
938
1254
|
/>
|
|
939
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}
|
|
940
1266
|
</div>
|
|
941
1267
|
|
|
942
1268
|
|
|
@@ -1167,6 +1493,14 @@
|
|
|
1167
1493
|
animation: popover-enter 0.15s ease-out;
|
|
1168
1494
|
}
|
|
1169
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
|
+
|
|
1170
1504
|
@keyframes popover-enter {
|
|
1171
1505
|
from {
|
|
1172
1506
|
opacity: 0;
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
let container: HTMLElement;
|
|
28
28
|
let view = $state<EditorView | undefined>(undefined);
|
|
29
|
+
let lastEmitted = '';
|
|
29
30
|
|
|
30
31
|
const langCompartment = new Compartment();
|
|
31
32
|
const markdocCompartment = new Compartment();
|
|
@@ -145,7 +146,8 @@
|
|
|
145
146
|
]),
|
|
146
147
|
EditorView.updateListener.of((update) => {
|
|
147
148
|
if (update.docChanged) {
|
|
148
|
-
|
|
149
|
+
lastEmitted = update.state.doc.toString();
|
|
150
|
+
onchange(lastEmitted);
|
|
149
151
|
}
|
|
150
152
|
}),
|
|
151
153
|
EditorView.lineWrapping,
|
|
@@ -214,11 +216,19 @@
|
|
|
214
216
|
if (!editor) return;
|
|
215
217
|
|
|
216
218
|
const cmContent = editor.state.doc.toString();
|
|
217
|
-
if (current
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
if (current === cmContent) return;
|
|
220
|
+
|
|
221
|
+
// If CM still has what we last emitted, the incoming content
|
|
222
|
+
// is our own edit coming back (possibly normalized) — skip sync
|
|
223
|
+
if (lastEmitted && cmContent === lastEmitted) {
|
|
224
|
+
lastEmitted = '';
|
|
225
|
+
return;
|
|
221
226
|
}
|
|
227
|
+
|
|
228
|
+
lastEmitted = '';
|
|
229
|
+
editor.dispatch({
|
|
230
|
+
changes: { from: 0, to: editor.state.doc.length, insert: current },
|
|
231
|
+
});
|
|
222
232
|
});
|
|
223
233
|
</script>
|
|
224
234
|
|