@commonpub/editor 0.7.0 → 0.7.2
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/editor",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TipTap block editor with 18+ maker-focused extensions for CommonPub",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"remark-rehype": "^11.1.2",
|
|
51
51
|
"unified": "^11.0.5",
|
|
52
52
|
"zod": "^4.3.6",
|
|
53
|
-
"@commonpub/
|
|
54
|
-
"@commonpub/
|
|
53
|
+
"@commonpub/schema": "0.9.4",
|
|
54
|
+
"@commonpub/config": "0.9.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vue": "^3.4.0",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Drag-and-drop reordering via BlockWrapper
|
|
10
10
|
* - Floating text toolbar on selection (delegated to FloatingToolbar)
|
|
11
11
|
*/
|
|
12
|
-
import { ref, inject, type Component } from 'vue';
|
|
12
|
+
import { ref, inject, onMounted, onUnmounted, type Component } from 'vue';
|
|
13
13
|
import type { EditorBlock, BlockTypeGroup } from '../types.js';
|
|
14
14
|
import type { BlockEditor } from '../composables/useBlockEditor.js';
|
|
15
15
|
import { BLOCK_COMPONENTS_KEY, UPLOAD_HANDLER_KEY, SEARCH_PRODUCTS_KEY } from '../provide.js';
|
|
@@ -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,24 @@ 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 } };
|
|
159
170
|
}
|
|
160
171
|
}
|
|
161
172
|
|
|
@@ -168,6 +179,11 @@ function setBlockRef(blockId: string, el: unknown): void {
|
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
181
|
|
|
182
|
+
function getActiveEditorForBlock(blockId: string): TipTapEditor | null {
|
|
183
|
+
const ref = blockRefs.value.get(blockId);
|
|
184
|
+
return (ref?.getEditor?.() as TipTapEditor) ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
171
187
|
function getActiveEditor(): unknown {
|
|
172
188
|
const ref = blockRefs.value.get(floatingToolbar.value.blockId);
|
|
173
189
|
return ref?.getEditor?.() ?? null;
|
|
@@ -193,6 +209,9 @@ function toggleMark(mark: string): void {
|
|
|
193
209
|
editor.chain().focus().toggleMark(mark).run();
|
|
194
210
|
}
|
|
195
211
|
|
|
212
|
+
// --- Link URL inline prompt ---
|
|
213
|
+
const linkPrompt = ref<{ visible: boolean; url: string }>({ visible: false, url: '' });
|
|
214
|
+
|
|
196
215
|
function toggleLink(): void {
|
|
197
216
|
const editor = getActiveEditor() as TipTapEditor | null;
|
|
198
217
|
if (!editor) return;
|
|
@@ -200,10 +219,23 @@ function toggleLink(): void {
|
|
|
200
219
|
editor.chain().focus().unsetLink().run();
|
|
201
220
|
return;
|
|
202
221
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
222
|
+
// Open inline prompt instead of window.prompt
|
|
223
|
+
linkPrompt.value = { visible: true, url: '' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function applyLink(): void {
|
|
227
|
+
const url = linkPrompt.value.url.trim();
|
|
228
|
+
linkPrompt.value = { visible: false, url: '' };
|
|
229
|
+
if (!url) return;
|
|
230
|
+
const editor = getActiveEditor() as TipTapEditor | null;
|
|
231
|
+
if (!editor) return;
|
|
232
|
+
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cancelLink(): void {
|
|
236
|
+
linkPrompt.value = { visible: false, url: '' };
|
|
237
|
+
const editor = getActiveEditor() as TipTapEditor | null;
|
|
238
|
+
editor?.chain().focus().run();
|
|
207
239
|
}
|
|
208
240
|
|
|
209
241
|
// --- Empty state ---
|
|
@@ -275,7 +307,7 @@ function onDrop(atIndex: number, event: DragEvent): void {
|
|
|
275
307
|
// --- Click outside to deselect ---
|
|
276
308
|
function onCanvasClick(): void {
|
|
277
309
|
props.blockEditor.selectBlock(null);
|
|
278
|
-
floatingToolbar.value = { visible: false, top: 0, left: 0, blockId: '' };
|
|
310
|
+
floatingToolbar.value = { visible: false, top: 0, left: 0, blockId: '', activeMarks: { bold: false, italic: false, strike: false, code: false, link: false } };
|
|
279
311
|
}
|
|
280
312
|
|
|
281
313
|
// --- Resolve block component (injected overrides take priority) ---
|
|
@@ -309,6 +341,57 @@ function needsUpload(type: string): boolean {
|
|
|
309
341
|
function needsSearch(type: string): boolean {
|
|
310
342
|
return type === 'partsList';
|
|
311
343
|
}
|
|
344
|
+
|
|
345
|
+
// --- Keyboard shortcuts ---
|
|
346
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
347
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
348
|
+
const el = document.activeElement;
|
|
349
|
+
const inProseMirror = !!el?.closest('.ProseMirror');
|
|
350
|
+
const inFormField = el?.tagName === 'TEXTAREA' || el?.tagName === 'INPUT' || el?.tagName === 'SELECT';
|
|
351
|
+
|
|
352
|
+
// Undo/Redo: Ctrl+Z / Ctrl+Shift+Z
|
|
353
|
+
if (mod && event.key.toLowerCase() === 'z') {
|
|
354
|
+
if (inProseMirror || inFormField) return;
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
if (event.shiftKey) { props.blockEditor.redo(); } else { props.blockEditor.undo(); }
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Block operations only when a block is selected and not editing text
|
|
361
|
+
const selectedId = props.blockEditor.selectedBlockId.value;
|
|
362
|
+
if (!selectedId || inProseMirror || inFormField) return;
|
|
363
|
+
|
|
364
|
+
// Ctrl+Shift+ArrowUp: move block up
|
|
365
|
+
if (mod && event.shiftKey && event.key === 'ArrowUp') {
|
|
366
|
+
event.preventDefault();
|
|
367
|
+
props.blockEditor.moveBlockUp(selectedId);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Ctrl+Shift+ArrowDown: move block down
|
|
372
|
+
if (mod && event.shiftKey && event.key === 'ArrowDown') {
|
|
373
|
+
event.preventDefault();
|
|
374
|
+
props.blockEditor.moveBlockDown(selectedId);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Ctrl+D: duplicate block
|
|
379
|
+
if (mod && event.key.toLowerCase() === 'd') {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
props.blockEditor.duplicateBlock(selectedId);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Delete / Backspace: remove selected block
|
|
386
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
387
|
+
event.preventDefault();
|
|
388
|
+
props.blockEditor.removeBlock(selectedId);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
onMounted(() => { document.addEventListener('keydown', onKeydown); });
|
|
394
|
+
onUnmounted(() => { document.removeEventListener('keydown', onKeydown); });
|
|
312
395
|
</script>
|
|
313
396
|
|
|
314
397
|
<template>
|
|
@@ -385,23 +468,45 @@ function needsSearch(type: string): boolean {
|
|
|
385
468
|
class="cpub-floating-toolbar"
|
|
386
469
|
:style="{ top: floatingToolbar.top + 'px', left: floatingToolbar.left + 'px' }"
|
|
387
470
|
>
|
|
388
|
-
<button class="cpub-ft-btn" title="Bold" @mousedown.prevent="toggleMark('bold')">
|
|
471
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.bold }" title="Bold" @mousedown.prevent="toggleMark('bold')">
|
|
389
472
|
<i class="fa-solid fa-bold"></i>
|
|
390
473
|
</button>
|
|
391
|
-
<button class="cpub-ft-btn" title="Italic" @mousedown.prevent="toggleMark('italic')">
|
|
474
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.italic }" title="Italic" @mousedown.prevent="toggleMark('italic')">
|
|
392
475
|
<i class="fa-solid fa-italic"></i>
|
|
393
476
|
</button>
|
|
394
|
-
<button class="cpub-ft-btn" title="Strikethrough" @mousedown.prevent="toggleMark('strike')">
|
|
477
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.strike }" title="Strikethrough" @mousedown.prevent="toggleMark('strike')">
|
|
395
478
|
<i class="fa-solid fa-strikethrough"></i>
|
|
396
479
|
</button>
|
|
397
|
-
<button class="cpub-ft-btn" title="Inline code" @mousedown.prevent="toggleMark('code')">
|
|
480
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.code }" title="Inline code" @mousedown.prevent="toggleMark('code')">
|
|
398
481
|
<i class="fa-solid fa-code"></i>
|
|
399
482
|
</button>
|
|
400
483
|
<div class="cpub-ft-divider" />
|
|
401
|
-
<button class="cpub-ft-btn" title="Link" @mousedown.prevent="toggleLink">
|
|
484
|
+
<button class="cpub-ft-btn" :class="{ 'cpub-ft-btn--active': floatingToolbar.activeMarks.link }" title="Link" @mousedown.prevent="toggleLink">
|
|
402
485
|
<i class="fa-solid fa-link"></i>
|
|
403
486
|
</button>
|
|
404
487
|
</div>
|
|
488
|
+
<!-- Inline link URL prompt -->
|
|
489
|
+
<div
|
|
490
|
+
v-if="linkPrompt.visible && floatingToolbar.visible"
|
|
491
|
+
class="cpub-link-prompt"
|
|
492
|
+
:style="{ top: (floatingToolbar.top + 38) + 'px', left: floatingToolbar.left + 'px' }"
|
|
493
|
+
>
|
|
494
|
+
<input
|
|
495
|
+
v-model="linkPrompt.url"
|
|
496
|
+
class="cpub-link-prompt-input"
|
|
497
|
+
type="url"
|
|
498
|
+
placeholder="https://..."
|
|
499
|
+
autofocus
|
|
500
|
+
@keydown.enter.prevent="applyLink"
|
|
501
|
+
@keydown.escape.prevent="cancelLink"
|
|
502
|
+
/>
|
|
503
|
+
<button class="cpub-link-prompt-btn" @mousedown.prevent="applyLink">
|
|
504
|
+
<i class="fa-solid fa-check"></i>
|
|
505
|
+
</button>
|
|
506
|
+
<button class="cpub-link-prompt-btn cpub-link-prompt-btn--cancel" @mousedown.prevent="cancelLink">
|
|
507
|
+
<i class="fa-solid fa-xmark"></i>
|
|
508
|
+
</button>
|
|
509
|
+
</div>
|
|
405
510
|
</Teleport>
|
|
406
511
|
</div>
|
|
407
512
|
</template>
|
|
@@ -512,10 +617,59 @@ function needsSearch(type: string): boolean {
|
|
|
512
617
|
color: var(--surface, #fff);
|
|
513
618
|
}
|
|
514
619
|
|
|
620
|
+
.cpub-ft-btn--active {
|
|
621
|
+
background: var(--ft-surface);
|
|
622
|
+
color: var(--accent, #5b9cf6);
|
|
623
|
+
}
|
|
624
|
+
|
|
515
625
|
.cpub-ft-divider {
|
|
516
626
|
width: 2px;
|
|
517
627
|
height: 18px;
|
|
518
628
|
background: var(--ft-surface);
|
|
519
629
|
margin: 0 2px;
|
|
520
630
|
}
|
|
631
|
+
|
|
632
|
+
.cpub-link-prompt {
|
|
633
|
+
position: fixed;
|
|
634
|
+
z-index: 201;
|
|
635
|
+
display: flex;
|
|
636
|
+
align-items: center;
|
|
637
|
+
gap: 0;
|
|
638
|
+
background: var(--text, #1a1a1a);
|
|
639
|
+
border: var(--border-width-default, 2px) solid var(--border, #1a1a1a);
|
|
640
|
+
box-shadow: var(--shadow-md);
|
|
641
|
+
padding: 3px;
|
|
642
|
+
transform: translateX(-50%);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.cpub-link-prompt-input {
|
|
646
|
+
width: 200px;
|
|
647
|
+
padding: 4px 8px;
|
|
648
|
+
background: transparent;
|
|
649
|
+
border: none;
|
|
650
|
+
color: var(--surface3, #eaeae7);
|
|
651
|
+
font-family: var(--font-mono, monospace);
|
|
652
|
+
font-size: 11px;
|
|
653
|
+
outline: none;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.cpub-link-prompt-input::placeholder {
|
|
657
|
+
color: rgba(255, 255, 255, 0.3);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.cpub-link-prompt-btn {
|
|
661
|
+
width: 26px;
|
|
662
|
+
height: 26px;
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
justify-content: center;
|
|
666
|
+
background: transparent;
|
|
667
|
+
border: none;
|
|
668
|
+
color: var(--surface3, #eaeae7);
|
|
669
|
+
cursor: pointer;
|
|
670
|
+
font-size: 10px;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.cpub-link-prompt-btn:hover { background: rgba(255, 255, 255, 0.15); color: #fff; }
|
|
674
|
+
.cpub-link-prompt-btn--cancel:hover { background: var(--red, #e04030); color: #fff; }
|
|
521
675
|
</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
|
}
|
|
@@ -115,6 +115,10 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
115
115
|
type,
|
|
116
116
|
content: { ...content },
|
|
117
117
|
}));
|
|
118
|
+
// Reset history — loading new content is not an undoable operation
|
|
119
|
+
history.splice(0, history.length);
|
|
120
|
+
historyIndex.value = -1;
|
|
121
|
+
pushHistory();
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
if (initialBlocks && initialBlocks.length > 0) {
|