@alpaca-editor/core 1.0.4045 → 1.0.4048
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/dist/editor/field-types/RichTextEditorComponent.js +3 -10
- package/dist/editor/field-types/RichTextEditorComponent.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.js +300 -342
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
- package/dist/editor/field-types/richtext/components/SimpleRichTextEditor.js.map +1 -1
- package/dist/editor/field-types/richtext/components/SimpleToolbar.js +9 -9
- package/dist/editor/field-types/richtext/components/SimpleToolbar.js.map +1 -1
- package/dist/editor/field-types/richtext/config/pluginFactory.d.ts +7 -6
- package/dist/editor/field-types/richtext/config/pluginFactory.js +2 -1
- package/dist/editor/field-types/richtext/config/pluginFactory.js.map +1 -1
- package/dist/editor/field-types/richtext/hooks/useProfileCache.js +24 -18
- package/dist/editor/field-types/richtext/hooks/useProfileCache.js.map +1 -1
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js +1 -1
- package/dist/editor/field-types/richtext/hooks/useRichTextProfile.js.map +1 -1
- package/dist/editor/field-types/richtext/types.d.ts +236 -90
- package/dist/editor/field-types/richtext/types.js +3 -3
- package/dist/editor/field-types/richtext/types.js.map +1 -1
- package/dist/editor/field-types/richtext/utils/conversion.d.ts +4 -2
- package/dist/editor/field-types/richtext/utils/conversion.js +79 -12
- package/dist/editor/field-types/richtext/utils/conversion.js.map +1 -1
- package/dist/editor/field-types/richtext/utils/plugins.d.ts +66 -39
- package/dist/editor/field-types/richtext/utils/plugins.js +377 -233
- package/dist/editor/field-types/richtext/utils/plugins.js.map +1 -1
- package/dist/editor/field-types/richtext/utils/profileMapper.js +22 -2
- package/dist/editor/field-types/richtext/utils/profileMapper.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/package.json +1 -1
- package/src/editor/field-types/RichTextEditorComponent.tsx +4 -10
- package/src/editor/field-types/richtext/components/ReactSlate.css +85 -24
- package/src/editor/field-types/richtext/components/ReactSlate.tsx +375 -428
- package/src/editor/field-types/richtext/components/SimpleRichTextEditor.tsx +4 -2
- package/src/editor/field-types/richtext/components/SimpleToolbar.tsx +3 -3
- package/src/editor/field-types/richtext/config/pluginFactory.tsx +2 -1
- package/src/editor/field-types/richtext/hooks/useProfileCache.ts +25 -19
- package/src/editor/field-types/richtext/hooks/useRichTextProfile.ts +1 -1
- package/src/editor/field-types/richtext/types.ts +150 -112
- package/src/editor/field-types/richtext/utils/conversion.ts +100 -27
- package/src/editor/field-types/richtext/utils/plugins.ts +469 -268
- package/src/editor/field-types/richtext/utils/profileMapper.ts +26 -3
- package/src/revision.ts +2 -2
|
@@ -1,346 +1,547 @@
|
|
|
1
|
-
import { Editor, Element, Transforms, Text } from
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { Editor, Element, Transforms, Text, Path, Node, Range, Point } from "slate";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {
|
|
4
|
+
CustomElement,
|
|
5
|
+
LinkElement,
|
|
6
|
+
Alignment,
|
|
7
|
+
MarkId,
|
|
8
|
+
BlockId,
|
|
9
|
+
SLATE_MARKS,
|
|
10
|
+
SLATE_BLOCKS,
|
|
11
|
+
} from "../types";
|
|
12
|
+
|
|
13
|
+
// Hot key utility function
|
|
14
|
+
export const isHotkey = (hotkey: string, event: React.KeyboardEvent): boolean => {
|
|
15
|
+
const keys = hotkey.split('+');
|
|
16
|
+
const modKeys = keys.slice(0, -1);
|
|
17
|
+
const key = keys[keys.length - 1]?.toLowerCase();
|
|
18
|
+
|
|
19
|
+
let hasCtrlOrCmd = false;
|
|
20
|
+
let hasShift = false;
|
|
21
|
+
let hasAlt = false;
|
|
22
|
+
|
|
23
|
+
for (const modKey of modKeys) {
|
|
24
|
+
switch (modKey) {
|
|
25
|
+
case 'mod':
|
|
26
|
+
hasCtrlOrCmd = event.ctrlKey || event.metaKey;
|
|
27
|
+
break;
|
|
28
|
+
case 'ctrl':
|
|
29
|
+
hasCtrlOrCmd = event.ctrlKey;
|
|
30
|
+
break;
|
|
31
|
+
case 'cmd':
|
|
32
|
+
hasCtrlOrCmd = event.metaKey;
|
|
33
|
+
break;
|
|
34
|
+
case 'shift':
|
|
35
|
+
hasShift = event.shiftKey;
|
|
36
|
+
break;
|
|
37
|
+
case 'alt':
|
|
38
|
+
hasAlt = event.altKey;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
10
41
|
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
hasCtrlOrCmd === (modKeys.includes('mod') || modKeys.includes('ctrl') || modKeys.includes('cmd')) &&
|
|
45
|
+
hasShift === modKeys.includes('shift') &&
|
|
46
|
+
hasAlt === modKeys.includes('alt') &&
|
|
47
|
+
event.key.toLowerCase() === key
|
|
48
|
+
);
|
|
49
|
+
};
|
|
11
50
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
51
|
+
// Mark plugin with strict typing
|
|
52
|
+
export const withMark = (editor: Editor) => {
|
|
53
|
+
editor.isMarkActive = (format: MarkId): boolean => {
|
|
54
|
+
const marks = Editor.marks(editor);
|
|
55
|
+
return marks ? marks[format] === true : false;
|
|
56
|
+
};
|
|
15
57
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
58
|
+
editor.toggleMark = (format: MarkId): void => {
|
|
59
|
+
const isActive = editor.isMarkActive(format);
|
|
60
|
+
|
|
61
|
+
if (isActive) {
|
|
62
|
+
Editor.removeMark(editor, format);
|
|
63
|
+
} else {
|
|
64
|
+
Editor.addMark(editor, format, true);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
23
67
|
|
|
24
68
|
return editor;
|
|
25
69
|
};
|
|
26
70
|
|
|
71
|
+
// Block plugin with strict typing
|
|
27
72
|
export const withBlock = (editor: Editor) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!selection) return false;
|
|
73
|
+
editor.isBlockActive = (format: BlockId): boolean => {
|
|
74
|
+
const { selection } = editor;
|
|
75
|
+
if (!selection) return false;
|
|
32
76
|
|
|
33
|
-
|
|
34
|
-
|
|
77
|
+
const [match] = Array.from(
|
|
78
|
+
Editor.nodes(editor, {
|
|
79
|
+
at: Editor.unhangRange(editor, selection),
|
|
35
80
|
match: n =>
|
|
36
|
-
!Editor.isEditor(n) &&
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return !!match;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
81
|
+
!Editor.isEditor(n) && Element.isElement(n) && n.type === format,
|
|
82
|
+
})
|
|
83
|
+
);
|
|
44
84
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const isActive = editor.isBlockActive(format);
|
|
85
|
+
return !!match;
|
|
86
|
+
};
|
|
48
87
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
editor.toggleBlock = (format: BlockId): void => {
|
|
89
|
+
const isActive = editor.isBlockActive(format);
|
|
90
|
+
|
|
91
|
+
Transforms.setNodes(
|
|
92
|
+
editor,
|
|
93
|
+
{ type: isActive ? 'paragraph' : format },
|
|
94
|
+
{ match: n => !Editor.isEditor(n) && Element.isElement(n) }
|
|
95
|
+
);
|
|
96
|
+
};
|
|
56
97
|
|
|
57
98
|
return editor;
|
|
58
99
|
};
|
|
59
100
|
|
|
101
|
+
// Alignment plugin with strict typing
|
|
60
102
|
export const withAlignment = (editor: Editor) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!selection) return false;
|
|
65
|
-
|
|
66
|
-
const [match] = Array.from(
|
|
67
|
-
Editor.nodes(editor, {
|
|
68
|
-
at: Editor.unhangRange(editor, selection),
|
|
69
|
-
match: n =>
|
|
70
|
-
!Editor.isEditor(n) &&
|
|
71
|
-
Element.isElement(n) &&
|
|
72
|
-
n.align === align,
|
|
73
|
-
})
|
|
74
|
-
);
|
|
103
|
+
editor.isAlignActive = (align: Alignment): boolean => {
|
|
104
|
+
const { selection } = editor;
|
|
105
|
+
if (!selection) return false;
|
|
75
106
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!selection) return;
|
|
107
|
+
const [match] = Array.from(
|
|
108
|
+
Editor.nodes(editor, {
|
|
109
|
+
at: Editor.unhangRange(editor, selection),
|
|
110
|
+
match: n =>
|
|
111
|
+
!Editor.isEditor(n) && Element.isElement(n) && (n as CustomElement).align === align,
|
|
112
|
+
})
|
|
113
|
+
);
|
|
84
114
|
|
|
85
|
-
|
|
115
|
+
return !!match;
|
|
116
|
+
};
|
|
86
117
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
editor.toggleAlign = (align: Alignment): void => {
|
|
119
|
+
const isActive = editor.isAlignActive(align);
|
|
120
|
+
|
|
121
|
+
Transforms.setNodes(
|
|
122
|
+
editor,
|
|
123
|
+
{ align: isActive ? undefined : align },
|
|
124
|
+
{ match: n => !Editor.isEditor(n) && Element.isElement(n) }
|
|
125
|
+
);
|
|
126
|
+
};
|
|
94
127
|
|
|
95
128
|
return editor;
|
|
96
129
|
};
|
|
97
130
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const { selection } = editor;
|
|
102
|
-
if (!selection) return false;
|
|
103
|
-
|
|
104
|
-
const [match] = Editor.nodes(editor, {
|
|
105
|
-
at: selection,
|
|
106
|
-
match: n =>
|
|
107
|
-
!Editor.isEditor(n) &&
|
|
108
|
-
Element.isElement(n) &&
|
|
109
|
-
n.type === 'list-item' &&
|
|
110
|
-
(listType ? n.listType === listType : true)
|
|
111
|
-
});
|
|
131
|
+
// Link plugin
|
|
132
|
+
export const withLink = (editor: Editor) => {
|
|
133
|
+
const { insertData, insertText, isInline, isElementReadOnly, isSelectable } = editor;
|
|
112
134
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
135
|
+
editor.isInline = (element: Element) => {
|
|
136
|
+
return element.type === 'link' ? true : isInline(element);
|
|
137
|
+
};
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (!selection) return;
|
|
139
|
+
editor.isElementReadOnly = (element: Element) => {
|
|
140
|
+
return element.type === 'link' ? false : isElementReadOnly(element);
|
|
141
|
+
};
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
editor.isSelectable = (element: Element) => {
|
|
144
|
+
return element.type === 'link' ? true : isSelectable(element);
|
|
145
|
+
};
|
|
124
146
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
split: true
|
|
133
|
-
}
|
|
134
|
-
);
|
|
135
|
-
} else {
|
|
136
|
-
// Convert to list items
|
|
137
|
-
Transforms.setNodes(
|
|
138
|
-
editor,
|
|
139
|
-
{
|
|
140
|
-
type: 'list-item',
|
|
141
|
-
listType: listType,
|
|
142
|
-
indent: isAnyListActive ? 1 : 0 // If already in a list, indent by 1
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
match: n => !Editor.isEditor(n) && Element.isElement(n) &&
|
|
146
|
-
(n.type === 'paragraph' || n.type === 'list-item'),
|
|
147
|
-
split: true
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
}
|
|
147
|
+
editor.insertText = (text: string) => {
|
|
148
|
+
if (text && isUrl(text)) {
|
|
149
|
+
wrapLink(editor, text);
|
|
150
|
+
} else {
|
|
151
|
+
insertText(text);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const { selection } = editor;
|
|
157
|
-
if (!selection) return;
|
|
155
|
+
editor.insertData = (data: DataTransfer) => {
|
|
156
|
+
const text = data.getData('text/plain');
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
if (text && isUrl(text)) {
|
|
159
|
+
wrapLink(editor, text);
|
|
160
|
+
} else {
|
|
161
|
+
insertData(data);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
editor.isLinkActive = (): boolean => {
|
|
166
|
+
const [link] = Editor.nodes(editor, {
|
|
167
|
+
match: n =>
|
|
168
|
+
!Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
|
|
169
|
+
});
|
|
170
|
+
return !!link;
|
|
171
|
+
};
|
|
168
172
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
editor.insertLink = (options?: {
|
|
174
|
+
onOpenLinkDialog?: (callback: (link: any) => void) => void;
|
|
175
|
+
}): void => {
|
|
176
|
+
if (editor.isLinkActive()) {
|
|
177
|
+
unwrapLink(editor);
|
|
178
|
+
} else {
|
|
179
|
+
if (options?.onOpenLinkDialog) {
|
|
180
|
+
options.onOpenLinkDialog((linkData: any) => {
|
|
181
|
+
wrapLink(editor, linkData.url || linkData.link?.url || '', linkData);
|
|
182
|
+
});
|
|
174
183
|
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const { selection } = editor;
|
|
181
|
-
if (!selection) return;
|
|
187
|
+
return editor;
|
|
188
|
+
};
|
|
182
189
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
// List plugin with strict typing
|
|
191
|
+
export const withList = (editor: Editor) => {
|
|
192
|
+
editor.isListActive = (listType?: "unordered" | "ordered"): boolean => {
|
|
193
|
+
const { selection } = editor;
|
|
194
|
+
if (!selection) return false;
|
|
195
|
+
|
|
196
|
+
const [match] = Array.from(
|
|
197
|
+
Editor.nodes(editor, {
|
|
198
|
+
at: Editor.unhangRange(editor, selection),
|
|
199
|
+
match: n =>
|
|
200
|
+
!Editor.isEditor(n) &&
|
|
201
|
+
Element.isElement(n) &&
|
|
202
|
+
n.type === 'list-item' &&
|
|
203
|
+
(!listType || (n as CustomElement).listType === listType),
|
|
204
|
+
})
|
|
205
|
+
);
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
207
|
+
return !!match;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
editor.toggleList = (listType: "unordered" | "ordered"): void => {
|
|
211
|
+
const isActive = editor.isListActive(listType);
|
|
212
|
+
|
|
213
|
+
if (isActive) {
|
|
214
|
+
// Convert list items back to paragraphs
|
|
215
|
+
Transforms.setNodes(
|
|
216
|
+
editor,
|
|
217
|
+
{ type: 'paragraph', listType: undefined, indent: undefined },
|
|
218
|
+
{
|
|
219
|
+
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'list-item',
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
// Check if we're switching between list types (preserve indentation)
|
|
224
|
+
// or converting from non-list elements (reset to indent 0)
|
|
225
|
+
const selectedNodes = Array.from(
|
|
226
|
+
Editor.nodes(editor, {
|
|
227
|
+
match: n => !Editor.isEditor(n) && Element.isElement(n),
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
selectedNodes.forEach(([node, path]) => {
|
|
232
|
+
const element = node as CustomElement;
|
|
191
233
|
|
|
192
|
-
if (
|
|
193
|
-
//
|
|
234
|
+
if (element.type === 'list-item') {
|
|
235
|
+
// Switching between list types - preserve existing indentation
|
|
194
236
|
Transforms.setNodes(
|
|
195
237
|
editor,
|
|
196
|
-
{ type: '
|
|
238
|
+
{ type: 'list-item', listType },
|
|
197
239
|
{ at: path }
|
|
198
240
|
);
|
|
199
241
|
} else {
|
|
200
|
-
//
|
|
201
|
-
const newIndent = Math.max(currentIndent - 1, 0);
|
|
242
|
+
// Converting from non-list element - start at indent 0
|
|
202
243
|
Transforms.setNodes(
|
|
203
244
|
editor,
|
|
204
|
-
{ indent:
|
|
245
|
+
{ type: 'list-item', listType, indent: 0 },
|
|
205
246
|
{ at: path }
|
|
206
247
|
);
|
|
207
248
|
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return editor;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
export const withLink = (editor: Editor) => {
|
|
216
|
-
const { isInline } = editor;
|
|
217
|
-
|
|
218
|
-
// Override isInline to mark link elements as inline
|
|
219
|
-
editor.isInline = element => {
|
|
220
|
-
return element.type === 'link' ? true : isInline(element);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
221
251
|
};
|
|
222
252
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
253
|
+
editor.indentList = (): void => {
|
|
254
|
+
const { selection } = editor;
|
|
255
|
+
if (!selection) return;
|
|
256
|
+
|
|
257
|
+
const [match] = Array.from(
|
|
258
|
+
Editor.nodes(editor, {
|
|
259
|
+
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'list-item',
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (match) {
|
|
264
|
+
const [node, path] = match;
|
|
265
|
+
const currentIndent = (node as CustomElement).indent || 0;
|
|
266
|
+
|
|
267
|
+
// Check if this is the first list item in the document
|
|
268
|
+
const allListItems = Array.from(
|
|
229
269
|
Editor.nodes(editor, {
|
|
230
|
-
at:
|
|
231
|
-
match: n =>
|
|
232
|
-
!Editor.isEditor(n) &&
|
|
233
|
-
Element.isElement(n) &&
|
|
234
|
-
n.type === 'link'
|
|
270
|
+
at: [],
|
|
271
|
+
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'list-item',
|
|
235
272
|
})
|
|
236
273
|
);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (!editor.insertLink) {
|
|
243
|
-
editor.insertLink = (options: { onOpenLinkDialog?: (callback: (link: any) => void) => void } = {}) => {
|
|
244
|
-
const { selection } = editor;
|
|
245
|
-
const { onOpenLinkDialog } = options;
|
|
246
|
-
|
|
247
|
-
const createLinkElement = (link: any, defaultText: string = "Link"): LinkElement => {
|
|
248
|
-
return {
|
|
249
|
-
type: 'link',
|
|
250
|
-
url: link.url || '',
|
|
251
|
-
link: {
|
|
252
|
-
type: link.type || 'external',
|
|
253
|
-
url: link.url || '',
|
|
254
|
-
target: link.target || '_blank',
|
|
255
|
-
targetItemLongId: link.targetItemLongId,
|
|
256
|
-
itemId: link.itemId,
|
|
257
|
-
queryString: link.queryString
|
|
258
|
-
},
|
|
259
|
-
children: link.text ? [{ text: link.text }] : [{ text: defaultText }]
|
|
260
|
-
};
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// If there's a link active, remove it
|
|
264
|
-
if (editor.isLinkActive()) {
|
|
265
|
-
Transforms.unwrapNodes(editor, {
|
|
266
|
-
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link'
|
|
267
|
-
});
|
|
274
|
+
|
|
275
|
+
const currentIndex = allListItems.findIndex(([, itemPath]) => Path.equals(itemPath, path));
|
|
276
|
+
|
|
277
|
+
// Don't allow indenting the first list item
|
|
278
|
+
if (currentIndex === 0) {
|
|
268
279
|
return;
|
|
269
280
|
}
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
Transforms.
|
|
281
|
+
|
|
282
|
+
// Find the previous list item to check maximum allowed indent
|
|
283
|
+
if (currentIndex > 0) {
|
|
284
|
+
const prevEntry = allListItems[currentIndex - 1];
|
|
285
|
+
if (prevEntry) {
|
|
286
|
+
const [prevNode] = prevEntry;
|
|
287
|
+
const prevIndent = (prevNode as CustomElement).indent || 0;
|
|
288
|
+
const maxAllowedIndent = prevIndent + 1;
|
|
289
|
+
|
|
290
|
+
// Only allow indenting if it doesn't exceed previous item's indent + 1
|
|
291
|
+
if (currentIndent < maxAllowedIndent) {
|
|
292
|
+
Transforms.setNodes(
|
|
293
|
+
editor,
|
|
294
|
+
{ indent: Math.min(currentIndent + 1, Math.min(5, maxAllowedIndent)) },
|
|
295
|
+
{ at: path }
|
|
296
|
+
);
|
|
282
297
|
}
|
|
283
|
-
|
|
284
|
-
// Add space after link
|
|
285
|
-
Transforms.insertText(editor, ' ');
|
|
286
|
-
});
|
|
287
|
-
} else {
|
|
288
|
-
// Default link creation
|
|
289
|
-
const defaultLink = {
|
|
290
|
-
type: 'external',
|
|
291
|
-
url: 'https://example.com',
|
|
292
|
-
target: '_blank'
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
if (selection && Editor.string(editor, selection)) {
|
|
296
|
-
const linkElement = createLinkElement(defaultLink);
|
|
297
|
-
Transforms.wrapNodes(editor, linkElement, { split: true });
|
|
298
|
-
} else {
|
|
299
|
-
const linkElement = createLinkElement(defaultLink, "Link");
|
|
300
|
-
Transforms.insertNodes(editor, linkElement);
|
|
301
298
|
}
|
|
302
|
-
|
|
303
|
-
// Add space after link
|
|
304
|
-
Transforms.insertText(editor, ' ');
|
|
305
299
|
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
editor.outdentList = (): void => {
|
|
304
|
+
const { selection } = editor;
|
|
305
|
+
if (!selection) return;
|
|
306
|
+
|
|
307
|
+
const [match] = Array.from(
|
|
308
|
+
Editor.nodes(editor, {
|
|
309
|
+
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'list-item',
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (match) {
|
|
314
|
+
const [node, path] = match;
|
|
315
|
+
const currentIndent = (node as CustomElement).indent || 0;
|
|
316
|
+
|
|
317
|
+
if (currentIndent > 0) {
|
|
318
|
+
Transforms.setNodes(
|
|
319
|
+
editor,
|
|
320
|
+
{ indent: currentIndent - 1 },
|
|
321
|
+
{ at: path }
|
|
322
|
+
);
|
|
323
|
+
} else {
|
|
324
|
+
// Convert back to paragraph if at root level
|
|
325
|
+
Transforms.setNodes(
|
|
326
|
+
editor,
|
|
327
|
+
{ type: 'paragraph', listType: undefined, indent: undefined },
|
|
328
|
+
{ at: path }
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
308
333
|
|
|
309
334
|
return editor;
|
|
310
335
|
};
|
|
311
336
|
|
|
312
|
-
//
|
|
337
|
+
// Normalization plugin
|
|
313
338
|
export const withNormalization = (editor: Editor) => {
|
|
314
339
|
const { normalizeNode } = editor;
|
|
315
340
|
|
|
316
|
-
editor.normalizeNode = (entry) => {
|
|
341
|
+
editor.normalizeNode = (entry: [Node, Path]) => {
|
|
317
342
|
const [node, path] = entry;
|
|
318
343
|
|
|
319
|
-
//
|
|
320
|
-
|
|
344
|
+
// Ensure all text nodes have proper mark structure
|
|
345
|
+
if (Text.isText(node)) {
|
|
346
|
+
const validMarks = Object.keys(SLATE_MARKS) as MarkId[];
|
|
347
|
+
const nodeMarks = Object.keys(node).filter(key => key !== 'text');
|
|
348
|
+
|
|
349
|
+
for (const mark of nodeMarks) {
|
|
350
|
+
if (!validMarks.includes(mark as MarkId)) {
|
|
351
|
+
// Remove invalid marks
|
|
352
|
+
Transforms.unsetNodes(editor, mark, { at: path });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
321
357
|
|
|
322
|
-
//
|
|
358
|
+
// Ensure all elements have valid types
|
|
323
359
|
if (Element.isElement(node)) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
360
|
+
const validBlockTypes = Object.keys(SLATE_BLOCKS) as BlockId[];
|
|
361
|
+
const specialTypes = ['link', 'list-item', 'horizontal-rule'];
|
|
362
|
+
const allValidTypes = [...validBlockTypes, ...specialTypes];
|
|
363
|
+
|
|
364
|
+
if (!allValidTypes.includes(node.type)) {
|
|
365
|
+
// Convert invalid element types to paragraphs
|
|
366
|
+
Transforms.setNodes(editor, { type: 'paragraph' }, { at: path });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
normalizeNode(entry);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
return editor;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// Insertion plugin for content insertion features
|
|
378
|
+
export const withInsertion = (editor: Editor) => {
|
|
379
|
+
editor.insertHorizontalRule = (): void => {
|
|
380
|
+
const hrElement: CustomElement = {
|
|
381
|
+
type: 'horizontal-rule',
|
|
382
|
+
children: [{ text: '' }],
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
Transforms.insertNodes(editor, hrElement);
|
|
386
|
+
|
|
387
|
+
// Move cursor to after the hr element by inserting a new paragraph
|
|
388
|
+
const newParagraph: CustomElement = {
|
|
389
|
+
type: 'paragraph',
|
|
390
|
+
children: [{ text: '' }],
|
|
391
|
+
};
|
|
392
|
+
Transforms.insertNodes(editor, newParagraph);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
return editor;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Keyboard handler function
|
|
399
|
+
export const createKeyboardHandler = (editor: Editor) => {
|
|
400
|
+
return (event: React.KeyboardEvent) => {
|
|
401
|
+
// Handle hotkeys for marks
|
|
402
|
+
Object.entries(SLATE_MARKS).forEach(([markId, config]) => {
|
|
403
|
+
if ('hotkey' in config && config.hotkey && isHotkey(config.hotkey, event)) {
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
editor.toggleMark(markId as MarkId);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Handle Shift+Enter for line break
|
|
410
|
+
if (isHotkey('shift+enter', event)) {
|
|
411
|
+
event.preventDefault();
|
|
412
|
+
editor.insertText('<br>');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Handle Enter to escape from links
|
|
417
|
+
if (event.key === 'Enter') {
|
|
418
|
+
const { selection } = editor;
|
|
419
|
+
if (selection && Range.isCollapsed(selection)) {
|
|
420
|
+
const [linkNode] = Editor.nodes(editor, {
|
|
421
|
+
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
if (linkNode) {
|
|
425
|
+
const [, linkPath] = linkNode;
|
|
426
|
+
const linkEnd = Editor.end(editor, linkPath);
|
|
427
|
+
|
|
428
|
+
// Check if cursor is at the end of the link
|
|
429
|
+
if (Point.equals(selection.anchor, linkEnd)) {
|
|
430
|
+
event.preventDefault();
|
|
431
|
+
|
|
432
|
+
// Move cursor after the link
|
|
433
|
+
const after = Editor.after(editor, linkPath);
|
|
434
|
+
if (after) {
|
|
435
|
+
Transforms.select(editor, after);
|
|
436
|
+
editor.insertBreak();
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
329
440
|
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
330
443
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
444
|
+
// Handle Right arrow to escape from links
|
|
445
|
+
if (event.key === 'ArrowRight') {
|
|
446
|
+
const { selection } = editor;
|
|
447
|
+
if (selection && Range.isCollapsed(selection)) {
|
|
448
|
+
const [linkNode] = Editor.nodes(editor, {
|
|
449
|
+
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
if (linkNode) {
|
|
453
|
+
const [, linkPath] = linkNode;
|
|
454
|
+
const linkEnd = Editor.end(editor, linkPath);
|
|
455
|
+
|
|
456
|
+
// Check if cursor is at the end of the link
|
|
457
|
+
if (Point.equals(selection.anchor, linkEnd)) {
|
|
458
|
+
event.preventDefault();
|
|
459
|
+
|
|
460
|
+
// Move cursor after the link
|
|
461
|
+
const after = Editor.after(editor, linkPath);
|
|
462
|
+
if (after) {
|
|
463
|
+
Transforms.select(editor, after);
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
334
467
|
}
|
|
335
468
|
}
|
|
469
|
+
}
|
|
336
470
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
471
|
+
// Handle Tab for list indentation
|
|
472
|
+
if (event.key === 'Tab') {
|
|
473
|
+
event.preventDefault();
|
|
474
|
+
if (event.shiftKey) {
|
|
475
|
+
editor.outdentList();
|
|
476
|
+
} else {
|
|
477
|
+
editor.indentList();
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Handle Backspace/Delete for empty list items
|
|
483
|
+
if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
484
|
+
const { selection } = editor;
|
|
485
|
+
if (selection && Range.isCollapsed(selection)) {
|
|
486
|
+
const [match] = Array.from(
|
|
487
|
+
Editor.nodes(editor, {
|
|
488
|
+
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'list-item',
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
if (match) {
|
|
493
|
+
const [node] = match;
|
|
494
|
+
const text = Node.string(node);
|
|
495
|
+
|
|
496
|
+
if (text === '') {
|
|
497
|
+
event.preventDefault();
|
|
498
|
+
editor.outdentList();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
341
501
|
}
|
|
342
502
|
}
|
|
343
503
|
};
|
|
504
|
+
};
|
|
344
505
|
|
|
345
|
-
|
|
506
|
+
// Helper functions
|
|
507
|
+
const isUrl = (text: string): boolean => {
|
|
508
|
+
try {
|
|
509
|
+
new URL(text);
|
|
510
|
+
return true;
|
|
511
|
+
} catch {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const wrapLink = (editor: Editor, url: string, linkData?: any): void => {
|
|
517
|
+
if (editor.isLinkActive()) {
|
|
518
|
+
unwrapLink(editor);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const { selection } = editor;
|
|
522
|
+
const isCollapsed = selection && Range.isCollapsed(selection);
|
|
523
|
+
const link: LinkElement = {
|
|
524
|
+
type: 'link',
|
|
525
|
+
url,
|
|
526
|
+
link: linkData || {
|
|
527
|
+
type: 'external',
|
|
528
|
+
url,
|
|
529
|
+
target: '_blank',
|
|
530
|
+
},
|
|
531
|
+
children: isCollapsed ? [{ text: url }] : [],
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
if (isCollapsed) {
|
|
535
|
+
Transforms.insertNodes(editor, link);
|
|
536
|
+
} else {
|
|
537
|
+
Transforms.wrapNodes(editor, link, { split: true });
|
|
538
|
+
Transforms.collapse(editor, { edge: 'end' });
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const unwrapLink = (editor: Editor): void => {
|
|
543
|
+
Transforms.unwrapNodes(editor, {
|
|
544
|
+
match: n =>
|
|
545
|
+
!Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
|
|
546
|
+
});
|
|
346
547
|
};
|