@commonpub/editor 0.6.1 → 0.7.1
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.
|
|
3
|
+
"version": "0.7.1",
|
|
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/config": "0.9.0",
|
|
54
|
+
"@commonpub/schema": "0.9.4"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vue": "^3.4.0",
|
|
@@ -79,8 +79,10 @@
|
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@types/mdast": "^4.0.4",
|
|
82
|
+
"@vitejs/plugin-vue": "^6.0.5",
|
|
82
83
|
"jsdom": "^25.0.0",
|
|
83
84
|
"typescript": "^5.7.0",
|
|
85
|
+
"vite": "^6.4.1",
|
|
84
86
|
"vitest": "^3.0.0",
|
|
85
87
|
"vue": "^3.5.0"
|
|
86
88
|
},
|
|
@@ -90,6 +92,7 @@
|
|
|
90
92
|
"test": "vitest run",
|
|
91
93
|
"lint": "eslint src/",
|
|
92
94
|
"typecheck": "tsc --noEmit",
|
|
95
|
+
"playground": "vite --config playground/vite.config.ts",
|
|
93
96
|
"clean": "rm -rf dist"
|
|
94
97
|
}
|
|
95
98
|
}
|
|
@@ -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';
|
|
@@ -118,7 +118,16 @@ function closePicker(): void {
|
|
|
118
118
|
|
|
119
119
|
function onPickerSelect(type: string, attrs?: Record<string, unknown>): void {
|
|
120
120
|
if (slashCommandBlockId.value) {
|
|
121
|
-
|
|
121
|
+
// Only replace if the block is empty — otherwise insert below to preserve content
|
|
122
|
+
const block = props.blockEditor.blocks.value.find((b) => b.id === slashCommandBlockId.value);
|
|
123
|
+
const html = (block?.content?.html as string) ?? '';
|
|
124
|
+
const isEmpty = !html.replace(/<[^>]*>/g, '').trim();
|
|
125
|
+
if (isEmpty) {
|
|
126
|
+
props.blockEditor.replaceBlock(slashCommandBlockId.value, type, attrs);
|
|
127
|
+
} else {
|
|
128
|
+
const idx = props.blockEditor.getBlockIndex(slashCommandBlockId.value);
|
|
129
|
+
props.blockEditor.addBlock(type, attrs, idx + 1);
|
|
130
|
+
}
|
|
122
131
|
} else {
|
|
123
132
|
props.blockEditor.addBlock(type, attrs, pickerInsertIndex.value);
|
|
124
133
|
}
|
|
@@ -300,6 +309,30 @@ function needsUpload(type: string): boolean {
|
|
|
300
309
|
function needsSearch(type: string): boolean {
|
|
301
310
|
return type === 'partsList';
|
|
302
311
|
}
|
|
312
|
+
|
|
313
|
+
// --- Undo/Redo keyboard shortcuts ---
|
|
314
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
315
|
+
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
|
+
const el = document.activeElement;
|
|
322
|
+
if (el?.closest('.ProseMirror')) return;
|
|
323
|
+
const tag = el?.tagName;
|
|
324
|
+
if (tag === 'TEXTAREA' || tag === 'INPUT' || tag === 'SELECT') return;
|
|
325
|
+
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
if (event.shiftKey) {
|
|
328
|
+
props.blockEditor.redo();
|
|
329
|
+
} else {
|
|
330
|
+
props.blockEditor.undo();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
onMounted(() => { document.addEventListener('keydown', onKeydown); });
|
|
335
|
+
onUnmounted(() => { document.removeEventListener('keydown', onKeydown); });
|
|
303
336
|
</script>
|
|
304
337
|
|
|
305
338
|
<template>
|
|
@@ -42,6 +42,67 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
42
42
|
const blocks = ref<EditorBlock[]>([]);
|
|
43
43
|
const selectedBlockId = ref<string | null>(null);
|
|
44
44
|
|
|
45
|
+
// --- Undo/Redo History ---
|
|
46
|
+
const MAX_HISTORY = 50;
|
|
47
|
+
const history: Array<{ blocks: EditorBlock[]; selectedBlockId: string | null }> = [];
|
|
48
|
+
const historyIndex = ref(-1);
|
|
49
|
+
const isRestoring = ref(false);
|
|
50
|
+
|
|
51
|
+
function cloneBlocks(): EditorBlock[] {
|
|
52
|
+
return blocks.value.map((b) => ({
|
|
53
|
+
id: b.id,
|
|
54
|
+
type: b.type,
|
|
55
|
+
content: JSON.parse(JSON.stringify(b.content)),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Save current state snapshot to history (call AFTER mutation) */
|
|
60
|
+
function pushHistory(): void {
|
|
61
|
+
if (isRestoring.value) return;
|
|
62
|
+
// Truncate any future states if we branched from an earlier point
|
|
63
|
+
if (historyIndex.value < history.length - 1) {
|
|
64
|
+
history.splice(historyIndex.value + 1);
|
|
65
|
+
}
|
|
66
|
+
history.push({ blocks: cloneBlocks(), selectedBlockId: selectedBlockId.value });
|
|
67
|
+
if (history.length > MAX_HISTORY) {
|
|
68
|
+
history.shift();
|
|
69
|
+
}
|
|
70
|
+
historyIndex.value = history.length - 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function undo(): boolean {
|
|
74
|
+
if (historyIndex.value <= 0) return false;
|
|
75
|
+
historyIndex.value--;
|
|
76
|
+
const snapshot = history[historyIndex.value]!;
|
|
77
|
+
isRestoring.value = true;
|
|
78
|
+
blocks.value.splice(0, blocks.value.length, ...snapshot.blocks.map((b) => ({
|
|
79
|
+
id: b.id,
|
|
80
|
+
type: b.type,
|
|
81
|
+
content: JSON.parse(JSON.stringify(b.content)),
|
|
82
|
+
})));
|
|
83
|
+
selectedBlockId.value = snapshot.selectedBlockId;
|
|
84
|
+
isRestoring.value = false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function redo(): boolean {
|
|
89
|
+
if (historyIndex.value >= history.length - 1) return false;
|
|
90
|
+
historyIndex.value++;
|
|
91
|
+
const snapshot = history[historyIndex.value]!;
|
|
92
|
+
isRestoring.value = true;
|
|
93
|
+
blocks.value.splice(0, blocks.value.length, ...snapshot.blocks.map((b) => ({
|
|
94
|
+
id: b.id,
|
|
95
|
+
type: b.type,
|
|
96
|
+
content: JSON.parse(JSON.stringify(b.content)),
|
|
97
|
+
})));
|
|
98
|
+
selectedBlockId.value = snapshot.selectedBlockId;
|
|
99
|
+
isRestoring.value = false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const canUndo = computed(() => historyIndex.value > 0);
|
|
104
|
+
const canRedo = computed(() => historyIndex.value < history.length - 1);
|
|
105
|
+
|
|
45
106
|
// Merge custom defaults with built-in defaults
|
|
46
107
|
const defaults = options?.blockDefaults
|
|
47
108
|
? { ...BLOCK_DEFAULTS, ...options.blockDefaults }
|
|
@@ -54,12 +115,19 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
54
115
|
type,
|
|
55
116
|
content: { ...content },
|
|
56
117
|
}));
|
|
118
|
+
// Reset history — loading new content is not an undoable operation
|
|
119
|
+
history.splice(0, history.length);
|
|
120
|
+
historyIndex.value = -1;
|
|
121
|
+
pushHistory();
|
|
57
122
|
}
|
|
58
123
|
|
|
59
124
|
if (initialBlocks && initialBlocks.length > 0) {
|
|
60
125
|
fromBlockTuples(initialBlocks);
|
|
61
126
|
}
|
|
62
127
|
|
|
128
|
+
// Capture initial state as first history entry
|
|
129
|
+
pushHistory();
|
|
130
|
+
|
|
63
131
|
// --- Serialize back to BlockTuples ---
|
|
64
132
|
function toBlockTuples(): BlockTuple[] {
|
|
65
133
|
return blocks.value.map((b) => [b.type, { ...b.content }]);
|
|
@@ -82,6 +150,7 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
82
150
|
}
|
|
83
151
|
|
|
84
152
|
selectedBlockId.value = block.id;
|
|
153
|
+
pushHistory();
|
|
85
154
|
return block.id;
|
|
86
155
|
}
|
|
87
156
|
|
|
@@ -98,6 +167,7 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
98
167
|
|
|
99
168
|
blocks.value.splice(idx, 1, newBlock);
|
|
100
169
|
selectedBlockId.value = newBlock.id;
|
|
170
|
+
pushHistory();
|
|
101
171
|
return newBlock.id;
|
|
102
172
|
}
|
|
103
173
|
|
|
@@ -108,11 +178,13 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
108
178
|
if (selectedBlockId.value === id) {
|
|
109
179
|
selectedBlockId.value = null;
|
|
110
180
|
}
|
|
181
|
+
pushHistory();
|
|
111
182
|
}
|
|
112
183
|
|
|
113
184
|
function clearBlocks(): void {
|
|
114
185
|
blocks.value.splice(0, blocks.value.length);
|
|
115
186
|
selectedBlockId.value = null;
|
|
187
|
+
pushHistory();
|
|
116
188
|
}
|
|
117
189
|
|
|
118
190
|
function updateBlock(id: string, content: Record<string, unknown>): void {
|
|
@@ -127,6 +199,7 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
127
199
|
if (toIndex < 0 || toIndex >= blocks.value.length) return;
|
|
128
200
|
const [moved] = blocks.value.splice(fromIndex, 1);
|
|
129
201
|
blocks.value.splice(toIndex, 0, moved!);
|
|
202
|
+
pushHistory();
|
|
130
203
|
}
|
|
131
204
|
|
|
132
205
|
function moveBlockUp(id: string): void {
|
|
@@ -150,6 +223,7 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
150
223
|
};
|
|
151
224
|
blocks.value.splice(idx + 1, 0, clone);
|
|
152
225
|
selectedBlockId.value = clone.id;
|
|
226
|
+
pushHistory();
|
|
153
227
|
}
|
|
154
228
|
|
|
155
229
|
function selectBlock(id: string | null): void {
|
|
@@ -184,6 +258,10 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
184
258
|
getBlockIndex,
|
|
185
259
|
toBlockTuples,
|
|
186
260
|
fromBlockTuples,
|
|
261
|
+
undo,
|
|
262
|
+
redo,
|
|
263
|
+
canUndo,
|
|
264
|
+
canRedo,
|
|
187
265
|
};
|
|
188
266
|
}
|
|
189
267
|
|