@commonpub/editor 0.6.0 → 0.7.0
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.0",
|
|
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/config": "0.
|
|
54
|
-
"@commonpub/schema": "0.9.
|
|
53
|
+
"@commonpub/config": "0.9.0",
|
|
54
|
+
"@commonpub/schema": "0.9.3"
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -4,11 +4,11 @@ import { markdownToBlockTuples } from '@commonpub/editor';
|
|
|
4
4
|
import type { BlockTuple } from '@commonpub/editor';
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{
|
|
7
|
-
content:
|
|
7
|
+
content: Record<string, unknown>;
|
|
8
8
|
}>();
|
|
9
9
|
|
|
10
10
|
const emit = defineEmits<{
|
|
11
|
-
update: [content:
|
|
11
|
+
update: [content: Record<string, unknown>];
|
|
12
12
|
}>();
|
|
13
13
|
|
|
14
14
|
/** Type-safe block content accessors — cast content to record for dynamic key access */
|
|
@@ -20,10 +20,10 @@ function bNum(block: BlockTuple, key: string): number {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const viewMode = ref<'edit' | 'split' | 'preview'>('split');
|
|
23
|
-
const source = ref(props.content.source || '');
|
|
23
|
+
const source = ref((props.content.source as string) || '');
|
|
24
24
|
|
|
25
|
-
watch(() => props.content.source, (val) => {
|
|
26
|
-
if (val !== source.value) source.value = val;
|
|
25
|
+
watch(() => props.content.source as string, (val) => {
|
|
26
|
+
if (val !== source.value) source.value = val ?? '';
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
const previewBlocks = computed(() => {
|
|
@@ -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 }
|
|
@@ -60,6 +121,9 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
60
121
|
fromBlockTuples(initialBlocks);
|
|
61
122
|
}
|
|
62
123
|
|
|
124
|
+
// Capture initial state as first history entry
|
|
125
|
+
pushHistory();
|
|
126
|
+
|
|
63
127
|
// --- Serialize back to BlockTuples ---
|
|
64
128
|
function toBlockTuples(): BlockTuple[] {
|
|
65
129
|
return blocks.value.map((b) => [b.type, { ...b.content }]);
|
|
@@ -82,6 +146,7 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
82
146
|
}
|
|
83
147
|
|
|
84
148
|
selectedBlockId.value = block.id;
|
|
149
|
+
pushHistory();
|
|
85
150
|
return block.id;
|
|
86
151
|
}
|
|
87
152
|
|
|
@@ -98,6 +163,7 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
98
163
|
|
|
99
164
|
blocks.value.splice(idx, 1, newBlock);
|
|
100
165
|
selectedBlockId.value = newBlock.id;
|
|
166
|
+
pushHistory();
|
|
101
167
|
return newBlock.id;
|
|
102
168
|
}
|
|
103
169
|
|
|
@@ -108,11 +174,13 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
108
174
|
if (selectedBlockId.value === id) {
|
|
109
175
|
selectedBlockId.value = null;
|
|
110
176
|
}
|
|
177
|
+
pushHistory();
|
|
111
178
|
}
|
|
112
179
|
|
|
113
180
|
function clearBlocks(): void {
|
|
114
181
|
blocks.value.splice(0, blocks.value.length);
|
|
115
182
|
selectedBlockId.value = null;
|
|
183
|
+
pushHistory();
|
|
116
184
|
}
|
|
117
185
|
|
|
118
186
|
function updateBlock(id: string, content: Record<string, unknown>): void {
|
|
@@ -127,6 +195,7 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
127
195
|
if (toIndex < 0 || toIndex >= blocks.value.length) return;
|
|
128
196
|
const [moved] = blocks.value.splice(fromIndex, 1);
|
|
129
197
|
blocks.value.splice(toIndex, 0, moved!);
|
|
198
|
+
pushHistory();
|
|
130
199
|
}
|
|
131
200
|
|
|
132
201
|
function moveBlockUp(id: string): void {
|
|
@@ -150,6 +219,7 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
150
219
|
};
|
|
151
220
|
blocks.value.splice(idx + 1, 0, clone);
|
|
152
221
|
selectedBlockId.value = clone.id;
|
|
222
|
+
pushHistory();
|
|
153
223
|
}
|
|
154
224
|
|
|
155
225
|
function selectBlock(id: string | null): void {
|
|
@@ -184,6 +254,10 @@ export function useBlockEditor(initialBlocks?: BlockTuple[], options?: BlockEdit
|
|
|
184
254
|
getBlockIndex,
|
|
185
255
|
toBlockTuples,
|
|
186
256
|
fromBlockTuples,
|
|
257
|
+
undo,
|
|
258
|
+
redo,
|
|
259
|
+
canUndo,
|
|
260
|
+
canRedo,
|
|
187
261
|
};
|
|
188
262
|
}
|
|
189
263
|
|