@use-kona/editor 0.1.24 → 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.
Files changed (36) hide show
  1. package/dist/core/createEditable.js +146 -15
  2. package/dist/core/createEditor.js +1 -1
  3. package/dist/core/styles.module.js +3 -1
  4. package/dist/core/styles_module.css +13 -0
  5. package/dist/elements/Block.js +25 -0
  6. package/dist/index.js +2 -1
  7. package/dist/plugins/CodeBlockPlugin/CodeBlockPlugin.js +1 -1
  8. package/dist/plugins/DnDPlugin/DnDPlugin.js +93 -45
  9. package/dist/plugins/DnDPlugin/types.js +2 -0
  10. package/dist/plugins/HeadingsPlugin/HeadingsPlugin.js +1 -1
  11. package/dist/plugins/ListsPlugin/ListsPlugin.js +1 -1
  12. package/dist/plugins/NodeIdPlugin/NodeIdPlugin.js +6 -4
  13. package/dist/provider.js +15 -0
  14. package/dist/src/core/createEditable.d.ts +1 -1
  15. package/dist/src/elements/Block.d.ts +2 -0
  16. package/dist/src/index.d.ts +1 -0
  17. package/dist/src/plugins/DnDPlugin/DnDPlugin.d.ts +0 -7
  18. package/dist/src/plugins/DnDPlugin/types.d.ts +29 -15
  19. package/dist/src/plugins/NodeIdPlugin/NodeIdPlugin.d.ts +1 -0
  20. package/dist/src/provider.d.ts +8 -0
  21. package/dist/src/types.d.ts +4 -0
  22. package/package.json +9 -8
  23. package/src/core/createEditable.tsx +170 -18
  24. package/src/core/createEditor.ts +1 -1
  25. package/src/core/styles.module.css +12 -0
  26. package/src/editor.tsx +4 -3
  27. package/src/elements/Block.tsx +31 -0
  28. package/src/index.ts +1 -0
  29. package/src/plugins/CodeBlockPlugin/CodeBlockPlugin.tsx +1 -3
  30. package/src/plugins/DnDPlugin/DnDPlugin.tsx +147 -62
  31. package/src/plugins/DnDPlugin/types.ts +35 -20
  32. package/src/plugins/HeadingsPlugin/HeadingsPlugin.tsx +1 -2
  33. package/src/plugins/ListsPlugin/ListsPlugin.tsx +5 -5
  34. package/src/plugins/NodeIdPlugin/NodeIdPlugin.ts +9 -7
  35. package/src/provider.tsx +28 -0
  36. package/src/types.ts +5 -0
@@ -2,9 +2,16 @@ import type React from 'react';
2
2
  import type { ConnectDragPreview, ConnectDragSource, ConnectDropTarget } from 'react-dnd';
3
3
  import type { Editor, Path } from 'slate';
4
4
  import type { RenderElementProps } from 'slate-react';
5
- import type { CustomElement } from '../../../types';
6
- export type NodeWithId = CustomElement & {
5
+ import type { CustomElement, CustomText } from '../../../types';
6
+ import type { NodeIdPlugin } from '../NodeIdPlugin';
7
+ export type NodeWithId = Omit<CustomElement, 'children'> & {
7
8
  nodeId: string;
9
+ children: (NodeWithId | CustomText)[];
10
+ };
11
+ export declare const isNodeWithId: (node: unknown) => node is NodeWithId;
12
+ export type CustomDropHandler<T = unknown> = {
13
+ type: string | symbol;
14
+ onDrop: (context: CustomDropContext<T>) => DropResult;
8
15
  };
9
16
  export type Options = {
10
17
  editorId?: string;
@@ -16,8 +23,8 @@ export type Options = {
16
23
  dropRef: ConnectDropTarget;
17
24
  previewRef: ConnectDragPreview;
18
25
  position: 'top' | 'bottom' | null;
19
- selected?: boolean;
20
- onToggleSelected: () => void;
26
+ onNativeDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
27
+ onToggleSelected?: (event: React.MouseEvent<HTMLDivElement>) => void;
21
28
  }) => React.ReactNode;
22
29
  ignoreNodes?: string[];
23
30
  customTypes?: {
@@ -27,25 +34,23 @@ export type Options = {
27
34
  };
28
35
  };
29
36
  customDropHandlers?: {
30
- [type: string | symbol]: {
31
- type: string | symbol;
32
- onDrop: <T = unknown>(context: CustomDropContext<T>) => DropResult;
33
- };
37
+ [type: string | symbol]: CustomDropHandler;
34
38
  };
39
+ nodeIdPlugin?: NodeIdPlugin;
35
40
  };
36
- type CustomDropContext<T = unknown> = {
41
+ export type CustomDropContext<TItem = unknown> = {
37
42
  editor: Editor;
38
- item: T;
43
+ data: EditorOuterDragItem<TItem>;
39
44
  insertAt: Path;
40
45
  };
41
46
  type DropResult = 'handled' | 'ignored';
42
- /**
43
- * We're using this type to represent a drag item that is being dragged from the editor.
44
- */
45
- export type EditorDragItem = {
47
+ export type EditorInnerDragItem<TItem = unknown> = {
48
+ kind: 'inner';
46
49
  nodes: NodeWithId[];
47
- items?: unknown[];
50
+ item?: TItem;
51
+ items?: TItem[];
48
52
  source: {
53
+ editor: Editor;
49
54
  /**
50
55
  * Different editors have different unique identifiers.
51
56
  */
@@ -59,4 +64,13 @@ export type EditorDragItem = {
59
64
  };
60
65
  data?: Record<string, unknown>;
61
66
  };
67
+ export type EditorOuterDragItem<TItem = unknown> = {
68
+ kind: 'outer';
69
+ item: TItem;
70
+ items: TItem[];
71
+ };
72
+ /**
73
+ * We're using this type to represent a drag item that is being dragged from the editor.
74
+ */
75
+ export type EditorDragItem<TItem = unknown> = EditorInnerDragItem<TItem> | EditorOuterDragItem<TItem>;
62
76
  export {};
@@ -9,6 +9,7 @@ type Options = {
9
9
  };
10
10
  export declare class NodeIdPlugin implements IPlugin<Editor, NodeIdBlock> {
11
11
  private options;
12
+ generateId: () => string;
12
13
  constructor(options: Options);
13
14
  init(editor: Editor): import("slate").BaseEditor & import("slate-react").ReactEditor & {
14
15
  getCommands: <T extends IPlugin>(plugin: new () => T) => T["commands"];
@@ -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;
@@ -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.24",
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.117.2",
46
+ "slate": "^0.126.0",
47
+ "slate-dom": "^0.126.0",
47
48
  "slate-history": "^0.113.1",
48
- "slate-hyperscript": "^0.100.0",
49
- "slate-react": "^0.117.3",
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.117.2",
62
+ "slate": "^0.126.0",
63
+ "slate-dom": "^0.126.0",
62
64
  "slate-history": "^0.113.1",
63
- "slate-hyperscript": "^0.100.0",
64
- "slate-react": "^0.117.3"
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
- type Editor,
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
- return result || <BaseElement {...props} />;
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?.map((element) => {
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 className={styles.ui} onClick={(event) => {
158
- event.stopPropagation();
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!({ readOnly, children, editor })}
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
- <div className={styles.root}>
174
- <Ui>
175
- <Editable
176
- decorate={decorate}
177
- renderElement={renderElement}
178
- renderLeaf={renderLeaf}
179
- readOnly={readOnly}
180
- {...handlers}
181
- />
182
- </Ui>
183
- </div>
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
  };
@@ -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!([node]);
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
- !Editor.isEditor(n) &&
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) {