@use-kona/editor 0.1.24-dnd.7 → 0.1.25
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/core/createEditable.js +146 -15
- package/dist/core/createEditor.js +1 -1
- package/dist/core/styles.module.js +3 -1
- package/dist/core/styles_module.css +13 -0
- package/dist/elements/Block.js +25 -0
- package/dist/index.js +2 -1
- package/dist/plugins/CodeBlockPlugin/CodeBlockPlugin.js +1 -1
- package/dist/plugins/DnDPlugin/DnDPlugin.js +30 -40
- package/dist/plugins/HeadingsPlugin/HeadingsPlugin.js +1 -1
- package/dist/plugins/ListsPlugin/ListsPlugin.js +1 -1
- package/dist/provider.js +15 -0
- package/dist/src/core/createEditable.d.ts +1 -1
- package/dist/src/elements/Block.d.ts +2 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/plugins/DnDPlugin/DnDPlugin.d.ts +1 -8
- package/dist/src/plugins/DnDPlugin/types.d.ts +1 -2
- package/dist/src/provider.d.ts +8 -0
- package/dist/src/types.d.ts +4 -0
- package/package.json +9 -8
- package/src/core/createEditable.tsx +170 -18
- package/src/core/createEditor.ts +1 -1
- package/src/core/styles.module.css +12 -0
- package/src/editor.tsx +4 -3
- package/src/elements/Block.tsx +31 -0
- package/src/index.ts +1 -0
- package/src/plugins/CodeBlockPlugin/CodeBlockPlugin.tsx +1 -3
- package/src/plugins/DnDPlugin/DnDPlugin.tsx +41 -65
- package/src/plugins/DnDPlugin/types.ts +1 -8
- package/src/plugins/HeadingsPlugin/HeadingsPlugin.tsx +1 -2
- package/src/plugins/ListsPlugin/ListsPlugin.tsx +5 -5
- package/src/provider.tsx +28 -0
- package/src/types.ts +5 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStore } from "@nanostores/react";
|
|
2
3
|
import is_hotkey from "is-hotkey";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { atom } from "nanostores";
|
|
5
|
+
import react, { isValidElement, useCallback, useState } from "react";
|
|
6
|
+
import { Editor, Element, Path, Transforms } from "slate";
|
|
7
|
+
import { Editable, ReactEditor, useReadOnly } from "slate-react";
|
|
6
8
|
import { BaseElement } from "../elements/BaseElement.js";
|
|
9
|
+
import { Block } from "../elements/Block.js";
|
|
10
|
+
import { EditorProvider } from "../provider.js";
|
|
7
11
|
import { deserialize } from "./deserialize.js";
|
|
8
12
|
import styles_module from "./styles.module.js";
|
|
9
13
|
const SUPPORTED_HANDLERS = [
|
|
@@ -13,6 +17,8 @@ const SUPPORTED_HANDLERS = [
|
|
|
13
17
|
'onPaste'
|
|
14
18
|
];
|
|
15
19
|
const createEditable = (editor, plugins)=>({ readOnly })=>{
|
|
20
|
+
const [selectedNodes] = useState(()=>atom([]));
|
|
21
|
+
const $selectedNodes = useStore(selectedNodes);
|
|
16
22
|
const renderElement = (props)=>{
|
|
17
23
|
let result = null;
|
|
18
24
|
for (const plugin of plugins)for (const element of plugin.blocks || []){
|
|
@@ -24,13 +30,20 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
|
|
|
24
30
|
...props,
|
|
25
31
|
children: result || props.children
|
|
26
32
|
});
|
|
27
|
-
return result || /*#__PURE__*/ jsx(BaseElement, {
|
|
33
|
+
if (Editor.isInline(editor, props.element)) return result || /*#__PURE__*/ jsx(BaseElement, {
|
|
28
34
|
...props
|
|
29
35
|
});
|
|
36
|
+
return /*#__PURE__*/ jsx(Block, {
|
|
37
|
+
element: props.element,
|
|
38
|
+
attributes: props.attributes,
|
|
39
|
+
children: result || /*#__PURE__*/ jsx(BaseElement, {
|
|
40
|
+
...props
|
|
41
|
+
})
|
|
42
|
+
});
|
|
30
43
|
};
|
|
31
44
|
const renderLeaf = (props)=>{
|
|
32
45
|
let result;
|
|
33
|
-
for (const plugin of plugins)plugin.leafs?.
|
|
46
|
+
for (const plugin of plugins)plugin.leafs?.forEach((element)=>{
|
|
34
47
|
const newElement = element.render({
|
|
35
48
|
...props,
|
|
36
49
|
children: result || props.children
|
|
@@ -41,6 +54,121 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
|
|
|
41
54
|
};
|
|
42
55
|
const handleHotkey = (event)=>{
|
|
43
56
|
if ('Tab' === event.key) event.preventDefault();
|
|
57
|
+
if ('Escape' === event.key) {
|
|
58
|
+
const currentBlock = Editor.above(editor, {
|
|
59
|
+
at: editor.selection?.focus,
|
|
60
|
+
match: (n)=>Element.isElement(n) && Editor.isBlock(editor, n)
|
|
61
|
+
});
|
|
62
|
+
if (!currentBlock) return;
|
|
63
|
+
if (0 === $selectedNodes.length) selectedNodes.set([
|
|
64
|
+
currentBlock[0]
|
|
65
|
+
]);
|
|
66
|
+
else {
|
|
67
|
+
selectedNodes.set([]);
|
|
68
|
+
ReactEditor.blur(editor);
|
|
69
|
+
}
|
|
70
|
+
} else if ('ArrowDown' === event.key) {
|
|
71
|
+
const currentBlock = Editor.above(editor, {
|
|
72
|
+
at: editor.selection?.focus,
|
|
73
|
+
match: (n)=>Element.isElement(n)
|
|
74
|
+
});
|
|
75
|
+
if (!currentBlock) return;
|
|
76
|
+
const nextBlock = Editor.next(editor, {
|
|
77
|
+
at: currentBlock[1],
|
|
78
|
+
match: (n)=>Element.isElement(n),
|
|
79
|
+
voids: true
|
|
80
|
+
});
|
|
81
|
+
if (nextBlock) {
|
|
82
|
+
if (Editor.isVoid(editor, nextBlock[0])) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
selectedNodes.set([
|
|
85
|
+
nextBlock[0]
|
|
86
|
+
]);
|
|
87
|
+
} else selectedNodes.set([]);
|
|
88
|
+
if (Editor.isVoid(editor, currentBlock[0]) || Editor.isVoid(editor, nextBlock[0])) {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
Transforms.select(editor, nextBlock[1]);
|
|
91
|
+
Transforms.collapse(editor, {
|
|
92
|
+
edge: 'start'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if ('ArrowUp' === event.key) {
|
|
97
|
+
const currentBlock = Editor.above(editor, {
|
|
98
|
+
at: editor.selection?.focus,
|
|
99
|
+
match: (n)=>Element.isElement(n)
|
|
100
|
+
});
|
|
101
|
+
if (!currentBlock) return;
|
|
102
|
+
const prevBlock = Editor.previous(editor, {
|
|
103
|
+
at: currentBlock[1],
|
|
104
|
+
match: (n)=>Element.isElement(n),
|
|
105
|
+
voids: true
|
|
106
|
+
});
|
|
107
|
+
if (prevBlock) {
|
|
108
|
+
if (Editor.isVoid(editor, prevBlock[0])) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
selectedNodes.set([
|
|
111
|
+
prevBlock[0]
|
|
112
|
+
]);
|
|
113
|
+
} else selectedNodes.set([]);
|
|
114
|
+
if (Editor.isVoid(editor, currentBlock[0]) || Editor.isVoid(editor, prevBlock[0])) {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
Transforms.select(editor, prevBlock[1]);
|
|
117
|
+
Transforms.collapse(editor, {
|
|
118
|
+
edge: 'end'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if ('Backspace' === event.key) {
|
|
123
|
+
const currentBlock = Editor.above(editor, {
|
|
124
|
+
at: editor.selection?.focus,
|
|
125
|
+
match: (n)=>Element.isElement(n) && Editor.isBlock(editor, n)
|
|
126
|
+
});
|
|
127
|
+
if (!currentBlock) return;
|
|
128
|
+
if (editor.selection?.focus.offset !== 0) return;
|
|
129
|
+
const prevBlock = Editor.previous(editor, {
|
|
130
|
+
at: currentBlock[1],
|
|
131
|
+
match: (n)=>Element.isElement(n),
|
|
132
|
+
voids: true
|
|
133
|
+
});
|
|
134
|
+
if (prevBlock && Editor.isVoid(editor, prevBlock[0])) {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
Transforms.removeNodes(editor, {
|
|
137
|
+
at: currentBlock[1]
|
|
138
|
+
});
|
|
139
|
+
Transforms.select(editor, prevBlock[1]);
|
|
140
|
+
selectedNodes.set([
|
|
141
|
+
prevBlock[0]
|
|
142
|
+
]);
|
|
143
|
+
Transforms.collapse(editor, {
|
|
144
|
+
edge: 'start'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} else if ('Enter' === event.key) {
|
|
148
|
+
const currentBlock = Editor.above(editor, {
|
|
149
|
+
at: editor.selection?.focus,
|
|
150
|
+
match: (n)=>Element.isElement(n)
|
|
151
|
+
});
|
|
152
|
+
if (!currentBlock) return;
|
|
153
|
+
if (Editor.isVoid(editor, currentBlock[0])) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
Transforms.insertNodes(editor, {
|
|
156
|
+
type: 'paragraph',
|
|
157
|
+
children: [
|
|
158
|
+
{
|
|
159
|
+
text: ''
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}, {
|
|
163
|
+
at: Path.next(currentBlock[1])
|
|
164
|
+
});
|
|
165
|
+
selectedNodes.set([]);
|
|
166
|
+
Transforms.select(editor, Path.next(currentBlock[1]));
|
|
167
|
+
Transforms.collapse(editor, {
|
|
168
|
+
edge: 'start'
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
44
172
|
for (const plugin of plugins)if (plugin.hotkeys) {
|
|
45
173
|
for (const hotkey of plugin.hotkeys)if (is_hotkey(hotkey[0], event) && hotkey[1]) {
|
|
46
174
|
event.preventDefault();
|
|
@@ -95,7 +223,7 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
|
|
|
95
223
|
},
|
|
96
224
|
children: [
|
|
97
225
|
ui.map((p, index)=>/*#__PURE__*/ jsx(react.Fragment, {
|
|
98
|
-
children: p.ui({
|
|
226
|
+
children: p.ui?.({
|
|
99
227
|
readOnly,
|
|
100
228
|
children,
|
|
101
229
|
editor
|
|
@@ -111,15 +239,18 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
|
|
|
111
239
|
editor,
|
|
112
240
|
plugins.filter
|
|
113
241
|
]);
|
|
114
|
-
return /*#__PURE__*/ jsx(
|
|
115
|
-
|
|
116
|
-
children: /*#__PURE__*/ jsx(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
242
|
+
return /*#__PURE__*/ jsx(EditorProvider, {
|
|
243
|
+
selectedNodes: selectedNodes,
|
|
244
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
245
|
+
className: styles_module.root,
|
|
246
|
+
children: /*#__PURE__*/ jsx(Ui, {
|
|
247
|
+
children: /*#__PURE__*/ jsx(Editable, {
|
|
248
|
+
decorate: decorate,
|
|
249
|
+
renderElement: renderElement,
|
|
250
|
+
renderLeaf: renderLeaf,
|
|
251
|
+
readOnly: readOnly,
|
|
252
|
+
...handlers
|
|
253
|
+
})
|
|
123
254
|
})
|
|
124
255
|
})
|
|
125
256
|
});
|
|
@@ -136,7 +136,7 @@ const createEditor_createEditor = (plugins)=>()=>{
|
|
|
136
136
|
const hasOnBeforeDelete = match?.onBeforeDelete;
|
|
137
137
|
if (!hasOnBeforeDelete) return void deleteFragment(options);
|
|
138
138
|
{
|
|
139
|
-
const result = await match.onBeforeDelete([
|
|
139
|
+
const result = await match.onBeforeDelete?.([
|
|
140
140
|
node
|
|
141
141
|
]);
|
|
142
142
|
if (!result) return;
|
|
@@ -33,3 +33,16 @@
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
.block-sBBbL1 {
|
|
37
|
+
transition: background-color .2s ease-in-out;
|
|
38
|
+
|
|
39
|
+
&.selected-dNsAIz {
|
|
40
|
+
background-color: var(--kona-editor-selected-color);
|
|
41
|
+
|
|
42
|
+
& > * {
|
|
43
|
+
transition: transform .2s ease-in-out;
|
|
44
|
+
transform: scale(.98);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useStore } from "@nanostores/react";
|
|
3
|
+
import { Editor } from "slate";
|
|
4
|
+
import { useSlateStatic } from "slate-react";
|
|
5
|
+
import styles_module from "../core/styles.module.js";
|
|
6
|
+
import { useEditorContext } from "../provider.js";
|
|
7
|
+
const Block = (props)=>{
|
|
8
|
+
const editor = useSlateStatic();
|
|
9
|
+
const { selectedNodes } = useEditorContext();
|
|
10
|
+
const $selectedNodes = useStore(selectedNodes);
|
|
11
|
+
const isSelected = $selectedNodes.some((node)=>node === props.element);
|
|
12
|
+
const classes = isSelected ? `${styles_module.block} ${styles_module.selected}` : styles_module.block;
|
|
13
|
+
const handleClick = ()=>{
|
|
14
|
+
if (Editor.isVoid(editor, props.element)) selectedNodes.set([
|
|
15
|
+
props.element
|
|
16
|
+
]);
|
|
17
|
+
else selectedNodes.set([]);
|
|
18
|
+
};
|
|
19
|
+
return /*#__PURE__*/ jsx("div", {
|
|
20
|
+
className: classes,
|
|
21
|
+
onClick: handleClick,
|
|
22
|
+
children: props.children
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
export { Block };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { deserialize } from "./core/deserialize.js";
|
|
2
2
|
import { serialize } from "./core/serialize.js";
|
|
3
3
|
import { defaultValue } from "./defaultValue.js";
|
|
4
|
+
import { useEditorContext } from "./provider.js";
|
|
4
5
|
import { isEmpty } from "./utils.js";
|
|
5
6
|
export * from "./editor.js";
|
|
6
7
|
export * from "./plugins/index.js";
|
|
7
|
-
export { defaultValue, deserialize, isEmpty, serialize };
|
|
8
|
+
export { defaultValue, deserialize, isEmpty, serialize, useEditorContext };
|
|
@@ -78,7 +78,7 @@ class CodeBlockPlugin {
|
|
|
78
78
|
if ('a' === event.key && (event.ctrlKey || event.metaKey) && node && node.type === CodeBlockPlugin.CODE_ELEMENT) {
|
|
79
79
|
event.preventDefault();
|
|
80
80
|
const entry = Editor.above(editor, {
|
|
81
|
-
match: (n)
|
|
81
|
+
match: (n)=>Element.isElement(n) && n.type === CodeBlockPlugin.CODE_ELEMENT
|
|
82
82
|
});
|
|
83
83
|
if (entry) {
|
|
84
84
|
const [, codeBlockPath] = entry;
|
|
@@ -1,38 +1,20 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
1
2
|
import { useStore } from "@nanostores/react";
|
|
2
|
-
import { nanoid } from "nanoid";
|
|
3
|
-
import { map } from "nanostores";
|
|
4
3
|
import { useEffect, useRef, useState } from "react";
|
|
5
4
|
import { useDrag, useDrop } from "react-dnd";
|
|
6
5
|
import { NativeTypes } from "react-dnd-html5-backend";
|
|
7
6
|
import { Editor, Element, Path, Transforms } from "slate";
|
|
8
7
|
import { ReactEditor, useReadOnly, useSlate } from "slate-react";
|
|
8
|
+
import { useEditorContext } from "../../provider.js";
|
|
9
9
|
import { getNodesByNodeIds } from "./utils.js";
|
|
10
10
|
class DnDPlugin {
|
|
11
11
|
options;
|
|
12
|
-
store;
|
|
13
|
-
editors;
|
|
14
12
|
constructor(options){
|
|
15
13
|
this.options = options;
|
|
16
|
-
this.store = map({
|
|
17
|
-
selected: new Set()
|
|
18
|
-
});
|
|
19
|
-
this.editors = new WeakMap();
|
|
20
14
|
}
|
|
21
15
|
init(editor) {
|
|
22
|
-
this.editors.set(editor, nanoid());
|
|
23
16
|
return editor;
|
|
24
17
|
}
|
|
25
|
-
getEditorId(editor) {
|
|
26
|
-
return this.editors.get(editor);
|
|
27
|
-
}
|
|
28
|
-
handleToggleSelected = (nodeId)=>{
|
|
29
|
-
const selected = new Set(this.store.get().selected);
|
|
30
|
-
if (selected.has(nodeId)) selected.delete(nodeId);
|
|
31
|
-
else selected.add(nodeId);
|
|
32
|
-
this.store.set({
|
|
33
|
-
selected
|
|
34
|
-
});
|
|
35
|
-
};
|
|
36
18
|
static DND_BLOCK_ELEMENT = 'block';
|
|
37
19
|
handlers = {
|
|
38
20
|
onDrop: (event)=>{
|
|
@@ -41,7 +23,8 @@ class DnDPlugin {
|
|
|
41
23
|
};
|
|
42
24
|
renderBlock = (props)=>{
|
|
43
25
|
const editor = useSlate();
|
|
44
|
-
const
|
|
26
|
+
const { selectedNodes } = useEditorContext();
|
|
27
|
+
const $selectedNodes = useStore(selectedNodes);
|
|
45
28
|
const isCopying = useRef(false);
|
|
46
29
|
const options = this.options;
|
|
47
30
|
const generateId = this.options?.nodeIdPlugin?.generateId;
|
|
@@ -53,8 +36,8 @@ class DnDPlugin {
|
|
|
53
36
|
const [, drag, preview] = useDrag({
|
|
54
37
|
type: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
|
|
55
38
|
item: ()=>{
|
|
56
|
-
const selected =
|
|
57
|
-
const nodes = selected.length > 0 && selected.
|
|
39
|
+
const selected = $selectedNodes;
|
|
40
|
+
const nodes = selected.length > 0 && selected.find((node)=>node.nodeId === currentElement.nodeId) ? getNodesByNodeIds(editor, selected.map((node)=>node.nodeId)) : [
|
|
58
41
|
structuredClone(props.element)
|
|
59
42
|
];
|
|
60
43
|
const item = this.options.customTypes?.[currentElement.type]?.getData?.(currentElement);
|
|
@@ -69,7 +52,6 @@ class DnDPlugin {
|
|
|
69
52
|
items,
|
|
70
53
|
source: {
|
|
71
54
|
editor,
|
|
72
|
-
editorId: this.getEditorId(editor),
|
|
73
55
|
documentId: options.documentId
|
|
74
56
|
}
|
|
75
57
|
};
|
|
@@ -77,7 +59,7 @@ class DnDPlugin {
|
|
|
77
59
|
},
|
|
78
60
|
canDrag: !isReadOnly,
|
|
79
61
|
end: ()=>{
|
|
80
|
-
|
|
62
|
+
selectedNodes.set([]);
|
|
81
63
|
}
|
|
82
64
|
});
|
|
83
65
|
const allCustomTypes = Object.values(options.customTypes || {}).map((customType)=>customType.type);
|
|
@@ -108,7 +90,7 @@ class DnDPlugin {
|
|
|
108
90
|
const itemType = monitor.getItemType();
|
|
109
91
|
const sourceEditor = 'inner' === data.kind ? data.source?.editor : null;
|
|
110
92
|
const isSameDocument = 'inner' === data.kind && data.source?.documentId === this.options.documentId && void 0 !== this.options.documentId;
|
|
111
|
-
const isSameEditor = 'inner' === data.kind && data.source?.
|
|
93
|
+
const isSameEditor = 'inner' === data.kind && data.source?.editor === editor;
|
|
112
94
|
const sourceTo = ReactEditor.findPath(editor, props.element);
|
|
113
95
|
if (!sourceTo) return;
|
|
114
96
|
const insertAt = getDropPath(editor, sourceTo, dropPosition || 'bottom');
|
|
@@ -140,7 +122,7 @@ class DnDPlugin {
|
|
|
140
122
|
insertNodes(editor, insertAt, clonedNodes);
|
|
141
123
|
if (!isCopying.current && sourceEditor) removeNodes(sourceEditor, nodeIds);
|
|
142
124
|
}
|
|
143
|
-
|
|
125
|
+
selectedNodes.set([]);
|
|
144
126
|
break;
|
|
145
127
|
}
|
|
146
128
|
}
|
|
@@ -159,19 +141,27 @@ class DnDPlugin {
|
|
|
159
141
|
]);
|
|
160
142
|
if (this.options.ignoreNodes?.includes(props.element.type)) return props.children;
|
|
161
143
|
if (editor.isInline(props.element)) return props.children;
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
144
|
+
return /*#__PURE__*/ jsx("div", {
|
|
145
|
+
children: options.renderBlock?.({
|
|
146
|
+
props,
|
|
147
|
+
dragRef: drag,
|
|
148
|
+
dropRef: connectDropRef,
|
|
149
|
+
previewRef: preview,
|
|
150
|
+
position: dropPosition,
|
|
151
|
+
onNativeDrop: (event)=>{
|
|
152
|
+
isCopying.current = event.altKey;
|
|
153
|
+
},
|
|
154
|
+
onToggleSelected: (event)=>{
|
|
155
|
+
event.stopPropagation();
|
|
156
|
+
if ($selectedNodes.includes(props.element)) selectedNodes.set([
|
|
157
|
+
...$selectedNodes
|
|
158
|
+
].filter((element)=>element !== props.element));
|
|
159
|
+
else selectedNodes.set([
|
|
160
|
+
...$selectedNodes,
|
|
161
|
+
props.element
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
})
|
|
175
165
|
});
|
|
176
166
|
};
|
|
177
167
|
}
|
|
@@ -85,7 +85,7 @@ const isBlockActive = (editor, type)=>{
|
|
|
85
85
|
if (!selection) return false;
|
|
86
86
|
const [match] = Array.from(Editor.nodes(editor, {
|
|
87
87
|
at: Editor.unhangRange(editor, selection),
|
|
88
|
-
match: (node)
|
|
88
|
+
match: (node)=>Element.isElement(node) && node.type === type
|
|
89
89
|
}));
|
|
90
90
|
return !!match;
|
|
91
91
|
};
|
|
@@ -195,7 +195,7 @@ class ListsPlugin {
|
|
|
195
195
|
if (!selection) return false;
|
|
196
196
|
const [match] = Array.from(Editor.nodes(editor, {
|
|
197
197
|
at: Editor.unhangRange(editor, selection),
|
|
198
|
-
match: (node)
|
|
198
|
+
match: (node)=>Element.isElement(node) && node.type === type
|
|
199
199
|
}));
|
|
200
200
|
return !!match;
|
|
201
201
|
};
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const Context = /*#__PURE__*/ createContext(null);
|
|
4
|
+
const EditorProvider = ({ children, selectedNodes })=>/*#__PURE__*/ jsx(Context.Provider, {
|
|
5
|
+
value: {
|
|
6
|
+
selectedNodes
|
|
7
|
+
},
|
|
8
|
+
children: children
|
|
9
|
+
});
|
|
10
|
+
const useEditorContext = ()=>{
|
|
11
|
+
const context = useContext(Context);
|
|
12
|
+
if (!context) throw new Error('useEditorContext must be used within an EditorProvider');
|
|
13
|
+
return context;
|
|
14
|
+
};
|
|
15
|
+
export { EditorProvider, useEditorContext };
|
package/dist/src/index.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export { serialize } from './core/serialize';
|
|
|
4
4
|
export { defaultValue } from './defaultValue';
|
|
5
5
|
export * from './editor';
|
|
6
6
|
export * from './plugins';
|
|
7
|
+
export { useEditorContext } from './provider';
|
|
7
8
|
export type { EditorRef, IPlugin } from './types';
|
|
8
9
|
export { isEmpty } from './utils';
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import { type MapStore } from 'nanostores';
|
|
2
1
|
import { Editor } from 'slate';
|
|
3
2
|
import { ReactEditor, type RenderElementProps } from 'slate-react';
|
|
4
3
|
import type { IPlugin } from '../../types';
|
|
5
|
-
import {
|
|
4
|
+
import type { Options } from './types';
|
|
6
5
|
export declare class DnDPlugin implements IPlugin {
|
|
7
6
|
private options;
|
|
8
|
-
store: MapStore<{
|
|
9
|
-
selected: Set<string>;
|
|
10
|
-
}>;
|
|
11
|
-
editors: WeakMap<Editor, string>;
|
|
12
7
|
constructor(options: Options);
|
|
13
8
|
init(editor: Editor): import("slate").BaseEditor & ReactEditor & {
|
|
14
9
|
getCommands: <T extends IPlugin>(plugin: new () => T) => T["commands"];
|
|
15
10
|
};
|
|
16
|
-
getEditorId(editor: Editor): string | undefined;
|
|
17
|
-
handleToggleSelected: (nodeId: string) => void;
|
|
18
11
|
static DND_BLOCK_ELEMENT: string;
|
|
19
12
|
handlers: {
|
|
20
13
|
onDrop: (event: DragEvent) => void;
|
|
@@ -23,9 +23,8 @@ export type Options = {
|
|
|
23
23
|
dropRef: ConnectDropTarget;
|
|
24
24
|
previewRef: ConnectDragPreview;
|
|
25
25
|
position: 'top' | 'bottom' | null;
|
|
26
|
-
selected?: boolean;
|
|
27
|
-
onToggleSelected: () => void;
|
|
28
26
|
onNativeDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
|
|
27
|
+
onToggleSelected?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
29
28
|
}) => React.ReactNode;
|
|
30
29
|
ignoreNodes?: string[];
|
|
31
30
|
customTypes?: {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StoreValue } from 'nanostores';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import type { EditorContext } from './types';
|
|
4
|
+
export declare const EditorProvider: ({ children, selectedNodes, }: {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
selectedNodes: StoreValue<string[]>;
|
|
7
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare const useEditorContext: () => EditorContext;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StoreValue } from 'nanostores';
|
|
1
2
|
import type { KeyboardEvent, ReactElement, ReactNode } from 'react';
|
|
2
3
|
import type { DecoratedRange, Descendant, Editor, Location, NodeEntry, NodeMatch } from 'slate';
|
|
3
4
|
import type { RenderElementProps, RenderLeafProps } from 'slate-react';
|
|
@@ -57,4 +58,7 @@ export type EditorRef = {
|
|
|
57
58
|
};
|
|
58
59
|
export type Serialize = (node: CustomElement | CustomText, children?: string) => string | undefined;
|
|
59
60
|
export type Deserialize = (element: HTMLElement, children?: (string | Descendant)[]) => CustomElement | CustomText[] | undefined;
|
|
61
|
+
export type EditorContext = {
|
|
62
|
+
selectedNodes: StoreValue<string[]>;
|
|
63
|
+
};
|
|
60
64
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@use-kona/editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -43,10 +43,11 @@
|
|
|
43
43
|
"react-dnd": "^16.0.1",
|
|
44
44
|
"react-dnd-html5-backend": "^16.0.1",
|
|
45
45
|
"react-dom": "18",
|
|
46
|
-
"slate": "^0.
|
|
46
|
+
"slate": "^0.126.0",
|
|
47
|
+
"slate-dom": "^0.126.0",
|
|
47
48
|
"slate-history": "^0.113.1",
|
|
48
|
-
"slate-hyperscript": "^0.
|
|
49
|
-
"slate-react": "^0.
|
|
49
|
+
"slate-hyperscript": "^0.125.0",
|
|
50
|
+
"slate-react": "^0.126.0",
|
|
50
51
|
"storybook": "^9.0.13",
|
|
51
52
|
"storybook-addon-rslib": "^2.0.2",
|
|
52
53
|
"storybook-react-rsbuild": "^2.0.2",
|
|
@@ -58,10 +59,11 @@
|
|
|
58
59
|
"react-dnd": "^16.0.1",
|
|
59
60
|
"react-dnd-html5-backend": "^16.0.1",
|
|
60
61
|
"react-dom": "18",
|
|
61
|
-
"slate": "^0.
|
|
62
|
+
"slate": "^0.126.0",
|
|
63
|
+
"slate-dom": "^0.126.0",
|
|
62
64
|
"slate-history": "^0.113.1",
|
|
63
|
-
"slate-hyperscript": "^0.
|
|
64
|
-
"slate-react": "^0.
|
|
65
|
+
"slate-hyperscript": "^0.125.0",
|
|
66
|
+
"slate-react": "^0.126.0"
|
|
65
67
|
},
|
|
66
68
|
"dependencies": {
|
|
67
69
|
"@nanostores/react": "^1.0.0",
|
|
@@ -69,7 +71,6 @@
|
|
|
69
71
|
"escape-html": "^1.0.3",
|
|
70
72
|
"is-hotkey": "^0.2.0",
|
|
71
73
|
"is-url": "^1.2.4",
|
|
72
|
-
"nanoid": "^5.1.16",
|
|
73
74
|
"nanostores": "^1.0.1",
|
|
74
75
|
"prismjs": "^1.30.0"
|
|
75
76
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
1
2
|
import isHotkey from 'is-hotkey';
|
|
3
|
+
import { atom } from 'nanostores';
|
|
2
4
|
import React, {
|
|
3
5
|
type ClipboardEvent,
|
|
4
6
|
isValidElement,
|
|
@@ -7,21 +9,28 @@ import React, {
|
|
|
7
9
|
type ReactElement,
|
|
8
10
|
type ReactNode,
|
|
9
11
|
useCallback,
|
|
12
|
+
useState,
|
|
10
13
|
} from 'react';
|
|
11
14
|
import {
|
|
12
15
|
type DecoratedRange,
|
|
13
16
|
type Descendant,
|
|
14
|
-
|
|
17
|
+
Editor,
|
|
18
|
+
Element,
|
|
15
19
|
type NodeEntry,
|
|
20
|
+
Path,
|
|
16
21
|
Transforms,
|
|
17
22
|
} from 'slate';
|
|
18
23
|
import {
|
|
19
24
|
Editable,
|
|
25
|
+
ReactEditor,
|
|
20
26
|
type RenderElementProps,
|
|
21
27
|
type RenderLeafProps,
|
|
22
28
|
useReadOnly,
|
|
23
29
|
} from 'slate-react';
|
|
30
|
+
import type { CustomElement } from '../../types';
|
|
24
31
|
import { BaseElement } from '../elements/BaseElement';
|
|
32
|
+
import { Block } from '../elements/Block';
|
|
33
|
+
import { EditorProvider } from '../provider';
|
|
25
34
|
import type { IPlugin } from '../types';
|
|
26
35
|
import { deserialize } from './deserialize';
|
|
27
36
|
import styles from './styles.module.css';
|
|
@@ -35,6 +44,10 @@ type Props = {
|
|
|
35
44
|
export const createEditable =
|
|
36
45
|
(editor: Editor, plugins: IPlugin[]) =>
|
|
37
46
|
({ readOnly }: Props) => {
|
|
47
|
+
const [selectedNodes] = useState(() => atom<CustomElement[]>([]));
|
|
48
|
+
|
|
49
|
+
const $selectedNodes = useStore(selectedNodes);
|
|
50
|
+
|
|
38
51
|
const renderElement = (props: RenderElementProps): ReactElement => {
|
|
39
52
|
let result: ReactElement | null = null;
|
|
40
53
|
|
|
@@ -56,13 +69,21 @@ export const createEditable =
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
if (Editor.isInline(editor, props.element)) {
|
|
73
|
+
return result || <BaseElement {...props} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Block element={props.element} attributes={props.attributes}>
|
|
78
|
+
{result || <BaseElement {...props} />}
|
|
79
|
+
</Block>
|
|
80
|
+
);
|
|
60
81
|
};
|
|
61
82
|
|
|
62
83
|
const renderLeaf = (props: RenderLeafProps): JSX.Element => {
|
|
63
84
|
let result: ReactNode;
|
|
64
85
|
for (const plugin of plugins) {
|
|
65
|
-
plugin.leafs?.
|
|
86
|
+
plugin.leafs?.forEach((element) => {
|
|
66
87
|
const newElement = element.render(
|
|
67
88
|
{
|
|
68
89
|
...props,
|
|
@@ -84,6 +105,132 @@ export const createEditable =
|
|
|
84
105
|
event.preventDefault();
|
|
85
106
|
}
|
|
86
107
|
|
|
108
|
+
if (event.key === 'Escape') {
|
|
109
|
+
const currentBlock = Editor.above<CustomElement>(editor, {
|
|
110
|
+
at: editor.selection?.focus,
|
|
111
|
+
match: (n) => Element.isElement(n) && Editor.isBlock(editor, n),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!currentBlock) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if ($selectedNodes.length === 0) {
|
|
119
|
+
selectedNodes.set([currentBlock[0]]);
|
|
120
|
+
} else {
|
|
121
|
+
selectedNodes.set([]);
|
|
122
|
+
ReactEditor.blur(editor);
|
|
123
|
+
}
|
|
124
|
+
} else if (event.key === 'ArrowDown') {
|
|
125
|
+
const currentBlock = Editor.above(editor, {
|
|
126
|
+
at: editor.selection?.focus,
|
|
127
|
+
match: (n) => Element.isElement(n),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!currentBlock) return;
|
|
131
|
+
|
|
132
|
+
const nextBlock = Editor.next(editor, {
|
|
133
|
+
at: currentBlock[1],
|
|
134
|
+
match: (n) => Element.isElement(n),
|
|
135
|
+
voids: true,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (nextBlock) {
|
|
139
|
+
if (Editor.isVoid(editor, nextBlock[0])) {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
selectedNodes.set([nextBlock[0]]);
|
|
142
|
+
} else {
|
|
143
|
+
selectedNodes.set([]);
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
Editor.isVoid(editor, currentBlock[0]) ||
|
|
147
|
+
Editor.isVoid(editor, nextBlock[0])
|
|
148
|
+
) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
Transforms.select(editor, nextBlock[1]);
|
|
151
|
+
Transforms.collapse(editor, { edge: 'start' });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else if (event.key === 'ArrowUp') {
|
|
155
|
+
const currentBlock = Editor.above(editor, {
|
|
156
|
+
at: editor.selection?.focus,
|
|
157
|
+
match: (n) => Element.isElement(n),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!currentBlock) return;
|
|
161
|
+
|
|
162
|
+
const prevBlock = Editor.previous(editor, {
|
|
163
|
+
at: currentBlock[1],
|
|
164
|
+
match: (n) => Element.isElement(n),
|
|
165
|
+
voids: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (prevBlock) {
|
|
169
|
+
if (Editor.isVoid(editor, prevBlock[0])) {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
selectedNodes.set([prevBlock[0]]);
|
|
172
|
+
} else {
|
|
173
|
+
selectedNodes.set([]);
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
Editor.isVoid(editor, currentBlock[0]) ||
|
|
177
|
+
Editor.isVoid(editor, prevBlock[0])
|
|
178
|
+
) {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
Transforms.select(editor, prevBlock[1]);
|
|
181
|
+
Transforms.collapse(editor, { edge: 'end' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else if (event.key === 'Backspace') {
|
|
185
|
+
const currentBlock = Editor.above(editor, {
|
|
186
|
+
at: editor.selection?.focus,
|
|
187
|
+
match: (n) => Element.isElement(n) && Editor.isBlock(editor, n),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!currentBlock) return;
|
|
191
|
+
if (editor.selection?.focus.offset !== 0) return;
|
|
192
|
+
|
|
193
|
+
const prevBlock = Editor.previous(editor, {
|
|
194
|
+
at: currentBlock[1],
|
|
195
|
+
match: (n) => Element.isElement(n),
|
|
196
|
+
voids: true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (prevBlock && Editor.isVoid(editor, prevBlock[0])) {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
Transforms.removeNodes(editor, {
|
|
202
|
+
at: currentBlock[1],
|
|
203
|
+
});
|
|
204
|
+
Transforms.select(editor, prevBlock[1]);
|
|
205
|
+
selectedNodes.set([prevBlock[0]]);
|
|
206
|
+
Transforms.collapse(editor, { edge: 'start' });
|
|
207
|
+
}
|
|
208
|
+
} else if (event.key === 'Enter') {
|
|
209
|
+
const currentBlock = Editor.above(editor, {
|
|
210
|
+
at: editor.selection?.focus,
|
|
211
|
+
match: (n) => Element.isElement(n),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!currentBlock) return;
|
|
215
|
+
|
|
216
|
+
if (Editor.isVoid(editor, currentBlock[0])) {
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
Transforms.insertNodes(
|
|
219
|
+
editor,
|
|
220
|
+
{
|
|
221
|
+
type: 'paragraph',
|
|
222
|
+
children: [{ text: '' }],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
at: Path.next(currentBlock[1]),
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
selectedNodes.set([]);
|
|
229
|
+
Transforms.select(editor, Path.next(currentBlock[1]));
|
|
230
|
+
Transforms.collapse(editor, { edge: 'start' });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
87
234
|
for (const plugin of plugins) {
|
|
88
235
|
if (plugin.hotkeys) {
|
|
89
236
|
for (const hotkey of plugin.hotkeys) {
|
|
@@ -154,12 +301,15 @@ export const createEditable =
|
|
|
154
301
|
}
|
|
155
302
|
|
|
156
303
|
return (
|
|
157
|
-
<div
|
|
158
|
-
|
|
159
|
-
|
|
304
|
+
<div
|
|
305
|
+
className={styles.ui}
|
|
306
|
+
onClick={(event) => {
|
|
307
|
+
event.stopPropagation();
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
160
310
|
{ui.map((p, index) => (
|
|
161
311
|
<React.Fragment key={index}>
|
|
162
|
-
{p.ui
|
|
312
|
+
{p.ui?.({ readOnly, children, editor })}
|
|
163
313
|
</React.Fragment>
|
|
164
314
|
))}
|
|
165
315
|
<div className={styles.editor}>{children}</div>
|
|
@@ -170,16 +320,18 @@ export const createEditable =
|
|
|
170
320
|
);
|
|
171
321
|
|
|
172
322
|
return (
|
|
173
|
-
<
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
323
|
+
<EditorProvider selectedNodes={selectedNodes}>
|
|
324
|
+
<div className={styles.root}>
|
|
325
|
+
<Ui>
|
|
326
|
+
<Editable
|
|
327
|
+
decorate={decorate}
|
|
328
|
+
renderElement={renderElement}
|
|
329
|
+
renderLeaf={renderLeaf}
|
|
330
|
+
readOnly={readOnly}
|
|
331
|
+
{...handlers}
|
|
332
|
+
/>
|
|
333
|
+
</Ui>
|
|
334
|
+
</div>
|
|
335
|
+
</EditorProvider>
|
|
184
336
|
);
|
|
185
337
|
};
|
package/src/core/createEditor.ts
CHANGED
|
@@ -219,7 +219,7 @@ export const createEditor = (plugins: IPlugin[]) => () => {
|
|
|
219
219
|
const hasOnBeforeDelete = match?.onBeforeDelete;
|
|
220
220
|
|
|
221
221
|
if (hasOnBeforeDelete) {
|
|
222
|
-
const result = await match.onBeforeDelete
|
|
222
|
+
const result = await match.onBeforeDelete?.([node]);
|
|
223
223
|
|
|
224
224
|
if (result) {
|
|
225
225
|
Transforms.removeNodes(editorWithPlugins, {
|
|
@@ -32,3 +32,15 @@
|
|
|
32
32
|
outline: none;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
.block {
|
|
37
|
+
transition: background-color 0.2s ease-in-out;
|
|
38
|
+
|
|
39
|
+
&.selected {
|
|
40
|
+
background-color: var(--kona-editor-selected-color);
|
|
41
|
+
& > * {
|
|
42
|
+
transition: transform 0.2s ease-in-out;
|
|
43
|
+
transform: scale(0.98);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/editor.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { atom } from 'nanostores';
|
|
1
2
|
import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
|
|
2
|
-
import {type Descendant, Editor, type NodeMatch, Transforms} from 'slate';
|
|
3
|
+
import { type Descendant, Editor, type NodeMatch, Transforms } from 'slate';
|
|
3
4
|
import { ReactEditor, Slate } from 'slate-react';
|
|
4
5
|
import type { CustomElement } from '../types';
|
|
5
6
|
import { createEditable } from './core/createEditable';
|
|
@@ -54,8 +55,8 @@ export const KonaEditor = forwardRef<EditorRef, KonaEditorProps>(
|
|
|
54
55
|
},
|
|
55
56
|
focus: (mode?: 'end') => {
|
|
56
57
|
if (mode === 'end') {
|
|
57
|
-
const endPoint = Editor.end(editor, [])
|
|
58
|
-
Transforms.select(editor, endPoint)
|
|
58
|
+
const endPoint = Editor.end(editor, []);
|
|
59
|
+
Transforms.select(editor, endPoint);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
ReactEditor.focus(editor);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { Editor } from 'slate';
|
|
3
|
+
import { type RenderElementProps, useSlateStatic } from 'slate-react';
|
|
4
|
+
import styles from '../core/styles.module.css';
|
|
5
|
+
import { useEditorContext } from '../provider';
|
|
6
|
+
|
|
7
|
+
export const Block = (props: RenderElementProps) => {
|
|
8
|
+
const editor = useSlateStatic();
|
|
9
|
+
const { selectedNodes } = useEditorContext();
|
|
10
|
+
const $selectedNodes = useStore(selectedNodes);
|
|
11
|
+
|
|
12
|
+
const isSelected = $selectedNodes.some((node) => node === props.element);
|
|
13
|
+
|
|
14
|
+
const classes = isSelected
|
|
15
|
+
? `${styles.block} ${styles.selected}`
|
|
16
|
+
: styles.block;
|
|
17
|
+
|
|
18
|
+
const handleClick = () => {
|
|
19
|
+
if (Editor.isVoid(editor, props.element)) {
|
|
20
|
+
selectedNodes.set([props.element]);
|
|
21
|
+
} else {
|
|
22
|
+
selectedNodes.set([]);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={classes} onClick={handleClick}>
|
|
28
|
+
{props.children}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -4,5 +4,6 @@ export { serialize } from './core/serialize';
|
|
|
4
4
|
export { defaultValue } from './defaultValue';
|
|
5
5
|
export * from './editor';
|
|
6
6
|
export * from './plugins';
|
|
7
|
+
export { useEditorContext } from './provider';
|
|
7
8
|
export type { EditorRef, IPlugin } from './types';
|
|
8
9
|
export { isEmpty } from './utils';
|
|
@@ -117,9 +117,7 @@ export class CodeBlockPlugin implements IPlugin {
|
|
|
117
117
|
// Find the code block node and its path
|
|
118
118
|
const entry = Editor.above(editor, {
|
|
119
119
|
match: (n) =>
|
|
120
|
-
|
|
121
|
-
Element.isElement(n) &&
|
|
122
|
-
n.type === CodeBlockPlugin.CODE_ELEMENT,
|
|
120
|
+
Element.isElement(n) && n.type === CodeBlockPlugin.CODE_ELEMENT,
|
|
123
121
|
});
|
|
124
122
|
|
|
125
123
|
if (entry) {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react';
|
|
2
|
-
import { nanoid } from 'nanoid';
|
|
3
|
-
import { type MapStore, map } from 'nanostores';
|
|
4
2
|
import { useEffect, useRef, useState } from 'react';
|
|
5
3
|
import { type ConnectDropTarget, useDrag, useDrop } from 'react-dnd';
|
|
6
4
|
import { NativeTypes } from 'react-dnd-html5-backend';
|
|
@@ -12,49 +10,18 @@ import {
|
|
|
12
10
|
useSlate,
|
|
13
11
|
} from 'slate-react';
|
|
14
12
|
import type { CustomElement } from '../../../types';
|
|
13
|
+
import { useEditorContext } from '../../provider';
|
|
15
14
|
import type { IPlugin } from '../../types';
|
|
16
|
-
import {
|
|
17
|
-
type EditorDragItem,
|
|
18
|
-
isNodeWithId,
|
|
19
|
-
type NodeWithId,
|
|
20
|
-
type Options,
|
|
21
|
-
} from './types';
|
|
15
|
+
import type { EditorDragItem, NodeWithId, Options } from './types';
|
|
22
16
|
import { getNodesByNodeIds } from './utils';
|
|
23
17
|
|
|
24
18
|
export class DnDPlugin implements IPlugin {
|
|
25
|
-
|
|
26
|
-
editors: WeakMap<Editor, string>;
|
|
27
|
-
|
|
28
|
-
constructor(private options: Options) {
|
|
29
|
-
this.store = map({
|
|
30
|
-
selected: new Set(),
|
|
31
|
-
});
|
|
32
|
-
this.editors = new WeakMap();
|
|
33
|
-
}
|
|
19
|
+
constructor(private options: Options) {}
|
|
34
20
|
|
|
35
21
|
init(editor: Editor) {
|
|
36
|
-
this.editors.set(editor, nanoid());
|
|
37
|
-
|
|
38
22
|
return editor;
|
|
39
23
|
}
|
|
40
24
|
|
|
41
|
-
getEditorId(editor: Editor) {
|
|
42
|
-
return this.editors.get(editor);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
handleToggleSelected = (nodeId: string) => {
|
|
46
|
-
const selected = new Set(this.store.get().selected);
|
|
47
|
-
if (selected.has(nodeId)) {
|
|
48
|
-
selected.delete(nodeId);
|
|
49
|
-
} else {
|
|
50
|
-
selected.add(nodeId);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
this.store.set({
|
|
54
|
-
selected,
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
|
|
58
25
|
static DND_BLOCK_ELEMENT = 'block';
|
|
59
26
|
|
|
60
27
|
handlers = {
|
|
@@ -66,7 +33,8 @@ export class DnDPlugin implements IPlugin {
|
|
|
66
33
|
renderBlock = (props: RenderElementProps) => {
|
|
67
34
|
const editor = useSlate();
|
|
68
35
|
|
|
69
|
-
const
|
|
36
|
+
const { selectedNodes } = useEditorContext();
|
|
37
|
+
const $selectedNodes = useStore(selectedNodes);
|
|
70
38
|
|
|
71
39
|
const isCopying = useRef(false);
|
|
72
40
|
const options = this.options;
|
|
@@ -85,7 +53,7 @@ export class DnDPlugin implements IPlugin {
|
|
|
85
53
|
const [, drag, preview] = useDrag({
|
|
86
54
|
type: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
|
|
87
55
|
item: () => {
|
|
88
|
-
const selected =
|
|
56
|
+
const selected: Array<NodeWithId> = $selectedNodes;
|
|
89
57
|
|
|
90
58
|
/**
|
|
91
59
|
* We copy nodes that the user is dragging.
|
|
@@ -93,8 +61,12 @@ export class DnDPlugin implements IPlugin {
|
|
|
93
61
|
* we copy selected nodes. Otherwise, we copy only the current node.
|
|
94
62
|
*/
|
|
95
63
|
const nodes =
|
|
96
|
-
selected.length > 0 &&
|
|
97
|
-
|
|
64
|
+
selected.length > 0 &&
|
|
65
|
+
selected.find((node) => node.nodeId === currentElement.nodeId)
|
|
66
|
+
? getNodesByNodeIds(
|
|
67
|
+
editor,
|
|
68
|
+
selected.map((node) => node.nodeId),
|
|
69
|
+
)
|
|
98
70
|
: [structuredClone(props.element as NodeWithId)];
|
|
99
71
|
|
|
100
72
|
const item =
|
|
@@ -118,7 +90,6 @@ export class DnDPlugin implements IPlugin {
|
|
|
118
90
|
items,
|
|
119
91
|
source: {
|
|
120
92
|
editor,
|
|
121
|
-
editorId: this.getEditorId(editor),
|
|
122
93
|
documentId: options.documentId,
|
|
123
94
|
},
|
|
124
95
|
};
|
|
@@ -127,7 +98,7 @@ export class DnDPlugin implements IPlugin {
|
|
|
127
98
|
},
|
|
128
99
|
canDrag: !isReadOnly,
|
|
129
100
|
end: () => {
|
|
130
|
-
|
|
101
|
+
selectedNodes.set([]);
|
|
131
102
|
},
|
|
132
103
|
});
|
|
133
104
|
|
|
@@ -179,9 +150,7 @@ export class DnDPlugin implements IPlugin {
|
|
|
179
150
|
data.source?.documentId === this.options.documentId &&
|
|
180
151
|
this.options.documentId !== undefined;
|
|
181
152
|
const isSameEditor =
|
|
182
|
-
data.kind === 'inner' &&
|
|
183
|
-
data.source?.editorId === this.editors.get(editor) &&
|
|
184
|
-
this.editors.get(editor) !== undefined;
|
|
153
|
+
data.kind === 'inner' && data.source?.editor === editor;
|
|
185
154
|
|
|
186
155
|
const sourceTo = ReactEditor.findPath(editor, props.element);
|
|
187
156
|
|
|
@@ -248,7 +217,7 @@ export class DnDPlugin implements IPlugin {
|
|
|
248
217
|
removeNodes(sourceEditor, nodeIds);
|
|
249
218
|
}
|
|
250
219
|
}
|
|
251
|
-
|
|
220
|
+
selectedNodes.set([]);
|
|
252
221
|
break;
|
|
253
222
|
}
|
|
254
223
|
}
|
|
@@ -276,25 +245,32 @@ export class DnDPlugin implements IPlugin {
|
|
|
276
245
|
return props.children;
|
|
277
246
|
}
|
|
278
247
|
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
248
|
+
return (
|
|
249
|
+
<div>
|
|
250
|
+
{options.renderBlock?.({
|
|
251
|
+
props,
|
|
252
|
+
dragRef: drag,
|
|
253
|
+
dropRef: connectDropRef,
|
|
254
|
+
previewRef: preview,
|
|
255
|
+
position: dropPosition,
|
|
256
|
+
onNativeDrop: (event) => {
|
|
257
|
+
isCopying.current = event.altKey;
|
|
258
|
+
},
|
|
259
|
+
onToggleSelected: (event) => {
|
|
260
|
+
event.stopPropagation();
|
|
261
|
+
if ($selectedNodes.includes(props.element)) {
|
|
262
|
+
selectedNodes.set(
|
|
263
|
+
[...$selectedNodes].filter(
|
|
264
|
+
(element) => element !== props.element,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
selectedNodes.set([...$selectedNodes, props.element]);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
})}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
298
274
|
};
|
|
299
275
|
}
|
|
300
276
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type MapStore, map } from 'nanostores';
|
|
2
1
|
import type React from 'react';
|
|
3
2
|
import type {
|
|
4
3
|
ConnectDragPreview,
|
|
@@ -10,11 +9,6 @@ import type { RenderElementProps } from 'slate-react';
|
|
|
10
9
|
import type { CustomElement, CustomText } from '../../../types';
|
|
11
10
|
import type { NodeIdPlugin } from '../NodeIdPlugin';
|
|
12
11
|
|
|
13
|
-
type DropParams = {
|
|
14
|
-
editor: Editor;
|
|
15
|
-
item: unknown;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
12
|
export type NodeWithId = Omit<CustomElement, 'children'> & {
|
|
19
13
|
nodeId: string;
|
|
20
14
|
children: (NodeWithId | CustomText)[];
|
|
@@ -38,9 +32,8 @@ export type Options = {
|
|
|
38
32
|
dropRef: ConnectDropTarget;
|
|
39
33
|
previewRef: ConnectDragPreview;
|
|
40
34
|
position: 'top' | 'bottom' | null;
|
|
41
|
-
selected?: boolean;
|
|
42
|
-
onToggleSelected: () => void;
|
|
43
35
|
onNativeDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
|
|
36
|
+
onToggleSelected?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
44
37
|
}) => React.ReactNode;
|
|
45
38
|
ignoreNodes?: string[];
|
|
46
39
|
customTypes?: {
|
|
@@ -124,8 +124,7 @@ const isBlockActive = (editor, type) => {
|
|
|
124
124
|
const [match] = Array.from(
|
|
125
125
|
Editor.nodes(editor, {
|
|
126
126
|
at: Editor.unhangRange(editor, selection),
|
|
127
|
-
match: (node) =>
|
|
128
|
-
!Editor.isEditor(node) && Element.isElement(node) && node.type === type,
|
|
127
|
+
match: (node) => Element.isElement(node) && node.type === type,
|
|
129
128
|
}),
|
|
130
129
|
);
|
|
131
130
|
|
|
@@ -96,7 +96,10 @@ export class ListsPlugin implements IPlugin {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/* Merge adjacent lists */
|
|
99
|
-
if (
|
|
99
|
+
if (
|
|
100
|
+
this.isList(editor, node as CustomElement) &&
|
|
101
|
+
Path.hasPrevious(path)
|
|
102
|
+
) {
|
|
100
103
|
const prevPath = Path.previous(path);
|
|
101
104
|
const prevNode = Node.get(editor, prevPath) as CustomElement;
|
|
102
105
|
|
|
@@ -331,10 +334,7 @@ export class ListsPlugin implements IPlugin {
|
|
|
331
334
|
const [match] = Array.from(
|
|
332
335
|
Editor.nodes(editor, {
|
|
333
336
|
at: Editor.unhangRange(editor, selection),
|
|
334
|
-
match: (node) =>
|
|
335
|
-
!Editor.isEditor(node) &&
|
|
336
|
-
Element.isElement(node) &&
|
|
337
|
-
node.type === type,
|
|
337
|
+
match: (node) => Element.isElement(node) && node.type === type,
|
|
338
338
|
}),
|
|
339
339
|
);
|
|
340
340
|
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StoreValue } from 'nanostores';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import type { EditorContext } from './types';
|
|
5
|
+
|
|
6
|
+
const Context = createContext<EditorContext | null>(null);
|
|
7
|
+
|
|
8
|
+
export const EditorProvider = ({
|
|
9
|
+
children,
|
|
10
|
+
selectedNodes,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
selectedNodes: StoreValue<string[]>;
|
|
14
|
+
}) => {
|
|
15
|
+
return (
|
|
16
|
+
<Context.Provider value={{ selectedNodes }}>{children}</Context.Provider>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const useEditorContext = () => {
|
|
21
|
+
const context = useContext(Context);
|
|
22
|
+
|
|
23
|
+
if (!context) {
|
|
24
|
+
throw new Error('useEditorContext must be used within an EditorProvider');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return context;
|
|
28
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StoreValue } from 'nanostores';
|
|
1
2
|
import type { KeyboardEvent, ReactElement, ReactNode } from 'react';
|
|
2
3
|
import type {
|
|
3
4
|
DecoratedRange,
|
|
@@ -98,3 +99,7 @@ export type Deserialize = (
|
|
|
98
99
|
element: HTMLElement,
|
|
99
100
|
children?: (string | Descendant)[],
|
|
100
101
|
) => CustomElement | CustomText[] | undefined;
|
|
102
|
+
|
|
103
|
+
export type EditorContext = {
|
|
104
|
+
selectedNodes: StoreValue<string[]>;
|
|
105
|
+
};
|