@commonpub/editor 0.7.1 → 0.7.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/package.json
CHANGED
|
@@ -140,7 +140,8 @@ const floatingToolbar = ref<{
|
|
|
140
140
|
top: number;
|
|
141
141
|
left: number;
|
|
142
142
|
blockId: string;
|
|
143
|
-
|
|
143
|
+
activeMarks: { bold: boolean; italic: boolean; strike: boolean; code: boolean; link: boolean };
|
|
144
|
+
}>({ visible: false, top: 0, left: 0, blockId: '', activeMarks: { bold: false, italic: false, strike: false, code: false, link: false } });
|
|
144
145
|
|
|
145
146
|
function onSelectionChange(block: EditorBlock, hasSelection: boolean, rect: DOMRect | null): void {
|
|
146
147
|
if (hasSelection && rect) {
|
|
@@ -148,14 +149,25 @@ function onSelectionChange(block: EditorBlock, hasSelection: boolean, rect: DOMR
|
|
|
148
149
|
const toolbarHeight = 44;
|
|
149
150
|
const rawTop = rect.top - toolbarHeight;
|
|
150
151
|
const rawLeft = rect.left + rect.width / 2;
|
|
152
|
+
// Query active marks from the TipTap editor
|
|
153
|
+
const editor = getActiveEditorForBlock(block.id);
|
|
154
|
+
const marks = {
|
|
155
|
+
bold: editor?.isActive('bold') ?? false,
|
|
156
|
+
italic: editor?.isActive('italic') ?? false,
|
|
157
|
+
strike: editor?.isActive('strike') ?? false,
|
|
158
|
+
code: editor?.isActive('code') ?? false,
|
|
159
|
+
link: editor?.isActive('link') ?? false,
|
|
160
|
+
};
|
|
151
161
|
floatingToolbar.value = {
|
|
152
162
|
visible: true,
|
|
153
163
|
top: Math.max(4, rawTop),
|
|
154
164
|
left: Math.max(toolbarWidth / 2 + 4, Math.min(rawLeft, window.innerWidth - toolbarWidth / 2 - 4)),
|
|
155
165
|
blockId: block.id,
|
|
166
|
+
activeMarks: marks,
|
|
156
167
|
};
|
|
157
168
|
} else {
|
|
158
|
-
floatingToolbar.value = { visible: false, top: 0, left: 0, blockId: '' };
|
|
169
|
+
floatingToolbar.value = { visible: false, top: 0, left: 0, blockId: '', activeMarks: { bold: false, italic: false, strike: false, code: false, link: false } };
|
|
170
|
+
linkPrompt.value = { visible: false, url: '' };
|
|
159
171
|
}
|
|
160
172
|
}
|
|
161
173
|
|
|
@@ -168,6 +180,11 @@ function setBlockRef(blockId: string, el: unknown): void {
|
|
|
168
180
|
}
|
|
169
181
|
}
|
|
170
182
|
|
|
183
|
+
function getActiveEditorForBlock(blockId: string): TipTapEditor | null {
|
|
184
|
+
const ref = blockRefs.value.get(blockId);
|
|
185
|
+
return (ref?.getEditor?.() as TipTapEditor) ?? null;
|
|
186
|
+
}
|
|
187
|
+
|
|
171
188
|
function getActiveEditor(): unknown {
|
|
172
189
|
const ref = blockRefs.value.get(floatingToolbar.value.blockId);
|
|
173
190
|
return ref?.getEditor?.() ?? null;
|
|
@@ -193,6 +210,9 @@ function toggleMark(mark: string): void {
|
|
|
193
210
|
editor.chain().focus().toggleMark(mark).run();
|
|
194
211
|
}
|
|
195
212
|
|
|
213
|
+
// --- Link URL inline prompt ---
|
|
214
|
+
const linkPrompt = ref<{ visible: boolean; url: string }>({ visible: false, url: '' });
|
|
215
|
+
|
|
196
216
|
function toggleLink(): void {
|
|
197
217
|
const editor = getActiveEditor() as TipTapEditor | null;
|
|
198
218
|
if (!editor) return;
|
|
@@ -200,10 +220,23 @@ function toggleLink(): void {
|
|
|
200
220
|
editor.chain().focus().unsetLink().run();
|
|
201
221
|
return;
|
|
202
222
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
223
|
+
// Open inline prompt instead of window.prompt
|
|
224
|
+
linkPrompt.value = { visible: true, url: '' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function applyLink(): void {
|
|
228
|
+
const url = linkPrompt.value.url.trim();
|
|
229
|
+
linkPrompt.value = { visible: false, url: '' };
|
|
230
|
+
if (!url) return;
|
|
231
|
+
const editor = getActiveEditor() as TipTapEditor | null;
|
|
232
|
+
if (!editor) return;
|
|
233
|
+
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function cancelLink(): void {
|
|
237
|
+
linkPrompt.value = { visible: false, url: '' };
|
|
238
|
+
const editor = getActiveEditor() as TipTapEditor | null;
|
|
239
|
+
editor?.chain().focus().run();
|
|
207
240
|
}
|
|
208
241
|
|
|
209
242
|
// --- Empty state ---
|
|
@@ -275,7 +308,8 @@ function onDrop(atIndex: number, event: DragEvent): void {
|
|
|
275
308
|
// --- Click outside to deselect ---
|
|
276
309
|
function onCanvasClick(): void {
|
|
277
310
|
props.blockEditor.selectBlock(null);
|
|
278
|
-
floatingToolbar.value = { visible: false, top: 0, left: 0, blockId: '' };
|
|
311
|
+
floatingToolbar.value = { visible: false, top: 0, left: 0, blockId: '', activeMarks: { bold: false, italic: false, strike: false, code: false, link: false } };
|
|
312
|
+
linkPrompt.value = { visible: false, url: '' };
|
|
279
313
|
}
|
|
280
314
|
|
|
281
315
|
// --- Resolve block component (injected overrides take priority) ---
|
|
@@ -310,24 +344,51 @@ function needsSearch(type: string): boolean {
|
|
|
310
344
|
return type === 'partsList';
|
|
311
345
|
}
|
|
312
346
|
|
|
313
|
-
// ---
|
|
347
|
+
// --- Keyboard shortcuts ---
|
|
314
348
|
function onKeydown(event: KeyboardEvent): void {
|
|
315
349
|
const mod = event.metaKey || event.ctrlKey;
|
|
316
|
-
if (!mod || event.key.toLowerCase() !== 'z') return;
|
|
317
|
-
|
|
318
|
-
// Don't intercept when an element with its own undo is focused:
|
|
319
|
-
// - ProseMirror (TipTap text blocks have their own undo)
|
|
320
|
-
// - textarea/input (native browser undo for code blocks, math, titles, etc.)
|
|
321
350
|
const el = document.activeElement;
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
351
|
+
const inProseMirror = !!el?.closest('.ProseMirror');
|
|
352
|
+
const inFormField = el?.tagName === 'TEXTAREA' || el?.tagName === 'INPUT' || el?.tagName === 'SELECT';
|
|
353
|
+
|
|
354
|
+
// Undo/Redo: Ctrl+Z / Ctrl+Shift+Z
|
|
355
|
+
if (mod && event.key.toLowerCase() === 'z') {
|
|
356
|
+
if (inProseMirror || inFormField) return;
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
if (event.shiftKey) { props.blockEditor.redo(); } else { props.blockEditor.undo(); }
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
325
361
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
362
|
+
// Block operations only when a block is selected and not editing text
|
|
363
|
+
const selectedId = props.blockEditor.selectedBlockId.value;
|
|
364
|
+
if (!selectedId || inProseMirror || inFormField) return;
|
|
365
|
+
|
|
366
|
+
// Ctrl+Shift+ArrowUp: move block up
|
|
367
|
+
if (mod && event.shiftKey && event.key === 'ArrowUp') {
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
props.blockEditor.moveBlockUp(selectedId);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Ctrl+Shift+ArrowDown: move block down
|
|
374
|
+
if (mod && event.shiftKey && event.key === 'ArrowDown') {
|
|
375
|
+
event.preventDefault();
|
|
376
|
+
props.blockEditor.moveBlockDown(selectedId);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Ctrl+D: duplicate block
|
|
381
|
+
if (mod && event.key.toLowerCase() === 'd') {
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
props.blockEditor.duplicateBlock(selectedId);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Delete / Backspace: remove selected block
|
|
388
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
389
|
+
event.preventDefault();
|
|
390
|
+
props.blockEditor.removeBlock(selectedId);
|
|
391
|
+
return;
|
|
331
392
|
}
|
|
332
393
|
}
|
|
333
394
|
|
|
@@ -409,23 +470,45 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeydown); });
|
|
|
409
470
|
class="cpub-floating-toolbar"
|
|
410
471
|
:style="{ top: floatingToolbar.top + 'px', left: floatingToolbar.left + 'px' }"
|
|
411
472
|
>
|
|
412
|
-
<button class="cpub-ft-btn" title="Bold" @mousedown.prevent="toggleMark('bold')">
|
|
473
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.bold }" title="Bold" @mousedown.prevent="toggleMark('bold')">
|
|
413
474
|
<i class="fa-solid fa-bold"></i>
|
|
414
475
|
</button>
|
|
415
|
-
<button class="cpub-ft-btn" title="Italic" @mousedown.prevent="toggleMark('italic')">
|
|
476
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.italic }" title="Italic" @mousedown.prevent="toggleMark('italic')">
|
|
416
477
|
<i class="fa-solid fa-italic"></i>
|
|
417
478
|
</button>
|
|
418
|
-
<button class="cpub-ft-btn" title="Strikethrough" @mousedown.prevent="toggleMark('strike')">
|
|
479
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.strike }" title="Strikethrough" @mousedown.prevent="toggleMark('strike')">
|
|
419
480
|
<i class="fa-solid fa-strikethrough"></i>
|
|
420
481
|
</button>
|
|
421
|
-
<button class="cpub-ft-btn" title="Inline code" @mousedown.prevent="toggleMark('code')">
|
|
482
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.code }" title="Inline code" @mousedown.prevent="toggleMark('code')">
|
|
422
483
|
<i class="fa-solid fa-code"></i>
|
|
423
484
|
</button>
|
|
424
485
|
<div class="cpub-ft-divider" />
|
|
425
|
-
<button class="cpub-ft-btn" title="Link" @mousedown.prevent="toggleLink">
|
|
486
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.link }" title="Link" @mousedown.prevent="toggleLink">
|
|
426
487
|
<i class="fa-solid fa-link"></i>
|
|
427
488
|
</button>
|
|
428
489
|
</div>
|
|
490
|
+
<!-- Inline link URL prompt -->
|
|
491
|
+
<div
|
|
492
|
+
v-if="linkPrompt.visible && floatingToolbar.visible"
|
|
493
|
+
class="cpub-link-prompt"
|
|
494
|
+
:style="{ top: (floatingToolbar.top + 38) + 'px', left: floatingToolbar.left + 'px' }"
|
|
495
|
+
>
|
|
496
|
+
<input
|
|
497
|
+
v-model="linkPrompt.url"
|
|
498
|
+
class="cpub-link-prompt-input"
|
|
499
|
+
type="url"
|
|
500
|
+
placeholder="https://..."
|
|
501
|
+
autofocus
|
|
502
|
+
@keydown.enter.prevent="applyLink"
|
|
503
|
+
@keydown.escape.prevent="cancelLink"
|
|
504
|
+
/>
|
|
505
|
+
<button class="cpub-link-prompt-btn" @mousedown.prevent="applyLink">
|
|
506
|
+
<i class="fa-solid fa-check"></i>
|
|
507
|
+
</button>
|
|
508
|
+
<button class="cpub-link-prompt-btn cpub-link-prompt-btn--cancel" @mousedown.prevent="cancelLink">
|
|
509
|
+
<i class="fa-solid fa-xmark"></i>
|
|
510
|
+
</button>
|
|
511
|
+
</div>
|
|
429
512
|
</Teleport>
|
|
430
513
|
</div>
|
|
431
514
|
</template>
|
|
@@ -536,10 +619,59 @@ onUnmounted(() => { document.removeEventListener('keydown', onKeydown); });
|
|
|
536
619
|
color: var(--surface, #fff);
|
|
537
620
|
}
|
|
538
621
|
|
|
622
|
+
.cpub-ft-btn--active {
|
|
623
|
+
background: var(--ft-surface);
|
|
624
|
+
color: var(--accent, #5b9cf6);
|
|
625
|
+
}
|
|
626
|
+
|
|
539
627
|
.cpub-ft-divider {
|
|
540
628
|
width: 2px;
|
|
541
629
|
height: 18px;
|
|
542
630
|
background: var(--ft-surface);
|
|
543
631
|
margin: 0 2px;
|
|
544
632
|
}
|
|
633
|
+
|
|
634
|
+
.cpub-link-prompt {
|
|
635
|
+
position: fixed;
|
|
636
|
+
z-index: 201;
|
|
637
|
+
display: flex;
|
|
638
|
+
align-items: center;
|
|
639
|
+
gap: 0;
|
|
640
|
+
background: var(--text, #1a1a1a);
|
|
641
|
+
border: var(--border-width-default, 2px) solid var(--border, #1a1a1a);
|
|
642
|
+
box-shadow: var(--shadow-md);
|
|
643
|
+
padding: 3px;
|
|
644
|
+
transform: translateX(-50%);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.cpub-link-prompt-input {
|
|
648
|
+
width: 200px;
|
|
649
|
+
padding: 4px 8px;
|
|
650
|
+
background: transparent;
|
|
651
|
+
border: none;
|
|
652
|
+
color: var(--surface3, #eaeae7);
|
|
653
|
+
font-family: var(--font-mono, monospace);
|
|
654
|
+
font-size: 11px;
|
|
655
|
+
outline: none;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.cpub-link-prompt-input::placeholder {
|
|
659
|
+
color: rgba(255, 255, 255, 0.3);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.cpub-link-prompt-btn {
|
|
663
|
+
width: 26px;
|
|
664
|
+
height: 26px;
|
|
665
|
+
display: flex;
|
|
666
|
+
align-items: center;
|
|
667
|
+
justify-content: center;
|
|
668
|
+
background: transparent;
|
|
669
|
+
border: none;
|
|
670
|
+
color: var(--surface3, #eaeae7);
|
|
671
|
+
cursor: pointer;
|
|
672
|
+
font-size: 10px;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.cpub-link-prompt-btn:hover { background: rgba(255, 255, 255, 0.15); color: #fff; }
|
|
676
|
+
.cpub-link-prompt-btn--cancel:hover { background: var(--red, #e04030); color: #fff; }
|
|
545
677
|
</style>
|
|
@@ -47,6 +47,23 @@ function onDragLeave(): void {
|
|
|
47
47
|
transition: height 0.15s;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/* Always-visible thin line */
|
|
51
|
+
.cpub-insert-zone::before {
|
|
52
|
+
content: '';
|
|
53
|
+
position: absolute;
|
|
54
|
+
left: 0;
|
|
55
|
+
right: 0;
|
|
56
|
+
top: 50%;
|
|
57
|
+
height: 1px;
|
|
58
|
+
background: var(--border2, rgba(0, 0, 0, 0.06));
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.cpub-insert-zone:hover::before,
|
|
63
|
+
.cpub-insert-zone--dragover::before {
|
|
64
|
+
background: transparent;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
.cpub-insert-zone:hover,
|
|
51
68
|
.cpub-insert-zone--dragover {
|
|
52
69
|
height: 36px;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Selected state (accent outline)
|
|
7
7
|
* - Click-to-select
|
|
8
8
|
*/
|
|
9
|
+
import { ref, watch } from 'vue';
|
|
9
10
|
import type { EditorBlock } from '../types.js';
|
|
10
11
|
|
|
11
12
|
const props = defineProps<{
|
|
@@ -23,6 +24,29 @@ const emit = defineEmits<{
|
|
|
23
24
|
'drag-end': [event: DragEvent];
|
|
24
25
|
}>();
|
|
25
26
|
|
|
27
|
+
// Two-step delete: first click arms, second click confirms
|
|
28
|
+
const deleteArmed = ref(false);
|
|
29
|
+
let deleteTimer: ReturnType<typeof setTimeout> | null = null;
|
|
30
|
+
|
|
31
|
+
function handleDelete(): void {
|
|
32
|
+
if (deleteArmed.value) {
|
|
33
|
+
emit('delete');
|
|
34
|
+
deleteArmed.value = false;
|
|
35
|
+
if (deleteTimer) { clearTimeout(deleteTimer); deleteTimer = null; }
|
|
36
|
+
} else {
|
|
37
|
+
deleteArmed.value = true;
|
|
38
|
+
deleteTimer = setTimeout(() => { deleteArmed.value = false; }, 2000);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Reset armed state when block loses selection
|
|
43
|
+
watch(() => props.selected, (sel) => {
|
|
44
|
+
if (!sel) {
|
|
45
|
+
deleteArmed.value = false;
|
|
46
|
+
if (deleteTimer) { clearTimeout(deleteTimer); deleteTimer = null; }
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
26
50
|
function onDragStart(event: DragEvent): void {
|
|
27
51
|
event.dataTransfer?.setData('text/plain', props.block.id);
|
|
28
52
|
event.dataTransfer!.effectAllowed = 'move';
|
|
@@ -53,6 +77,9 @@ function onDragEnd(event: DragEvent): void {
|
|
|
53
77
|
</button>
|
|
54
78
|
</div>
|
|
55
79
|
|
|
80
|
+
<!-- Block type badge (top-left, shown on hover) -->
|
|
81
|
+
<div class="cpub-block-type-badge">{{ block.type.replace(/_/g, ' ') }}</div>
|
|
82
|
+
|
|
56
83
|
<!-- Block controls (top-right, shown on hover) -->
|
|
57
84
|
<div class="cpub-block-controls">
|
|
58
85
|
<button class="cpub-block-ctrl" title="Move up" @click.stop="emit('move-up')">
|
|
@@ -64,8 +91,18 @@ function onDragEnd(event: DragEvent): void {
|
|
|
64
91
|
<button class="cpub-block-ctrl" title="Duplicate" @click.stop="emit('duplicate')">
|
|
65
92
|
<i class="fa-solid fa-copy"></i>
|
|
66
93
|
</button>
|
|
67
|
-
<button
|
|
68
|
-
|
|
94
|
+
<button
|
|
95
|
+
class="cpub-block-ctrl"
|
|
96
|
+
:class="deleteArmed ? 'cpub-block-ctrl--armed' : 'cpub-block-ctrl--danger'"
|
|
97
|
+
:title="deleteArmed ? 'Click again to confirm' : 'Delete'"
|
|
98
|
+
@click.stop="handleDelete"
|
|
99
|
+
>
|
|
100
|
+
<template v-if="deleteArmed">
|
|
101
|
+
<i class="fa-solid fa-check"></i>
|
|
102
|
+
</template>
|
|
103
|
+
<template v-else>
|
|
104
|
+
<i class="fa-solid fa-trash"></i>
|
|
105
|
+
</template>
|
|
69
106
|
</button>
|
|
70
107
|
</div>
|
|
71
108
|
|
|
@@ -133,6 +170,25 @@ function onDragEnd(event: DragEvent): void {
|
|
|
133
170
|
cursor: grabbing;
|
|
134
171
|
}
|
|
135
172
|
|
|
173
|
+
.cpub-block-type-badge {
|
|
174
|
+
position: absolute;
|
|
175
|
+
top: -22px;
|
|
176
|
+
left: 0;
|
|
177
|
+
font-family: var(--font-mono);
|
|
178
|
+
font-size: 9px;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
text-transform: capitalize;
|
|
181
|
+
letter-spacing: 0.04em;
|
|
182
|
+
color: var(--text-faint);
|
|
183
|
+
opacity: 0;
|
|
184
|
+
transition: opacity 0.12s;
|
|
185
|
+
pointer-events: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.cpub-block-wrap:hover .cpub-block-type-badge {
|
|
189
|
+
opacity: 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
136
192
|
.cpub-block-controls {
|
|
137
193
|
--ctrl-surface: rgba(255, 255, 255, 0.15);
|
|
138
194
|
position: absolute;
|
|
@@ -177,6 +233,17 @@ function onDragEnd(event: DragEvent): void {
|
|
|
177
233
|
color: var(--surface);
|
|
178
234
|
}
|
|
179
235
|
|
|
236
|
+
.cpub-block-ctrl--armed {
|
|
237
|
+
background: var(--red);
|
|
238
|
+
color: var(--surface);
|
|
239
|
+
animation: cpub-pulse 0.6s ease-in-out infinite alternate;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@keyframes cpub-pulse {
|
|
243
|
+
from { opacity: 1; }
|
|
244
|
+
to { opacity: 0.6; }
|
|
245
|
+
}
|
|
246
|
+
|
|
180
247
|
.cpub-block-inner {
|
|
181
248
|
min-height: 20px;
|
|
182
249
|
}
|