@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.6.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.8.0",
54
- "@commonpub/schema": "0.9.2"
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
- props.blockEditor.replaceBlock(slashCommandBlockId.value, type, attrs);
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: { source: string };
7
+ content: Record<string, unknown>;
8
8
  }>();
9
9
 
10
10
  const emit = defineEmits<{
11
- update: [content: { source: string }];
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