@use-kona/editor 0.1.30 → 0.1.32
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 +3 -4
- package/dist/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.js +5 -3
- package/dist/plugins/DnDPlugin/DnDPlugin.js +4 -2
- package/dist/src/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/createEditable.spec.tsx +87 -0
- package/src/core/createEditable.tsx +10 -4
- package/src/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.tsx +5 -6
- package/src/plugins/DnDPlugin/DnDPlugin.spec.ts +62 -0
- package/src/plugins/DnDPlugin/DnDPlugin.tsx +7 -3
|
@@ -3,7 +3,7 @@ import { useStore } from "@nanostores/react";
|
|
|
3
3
|
import is_hotkey from "is-hotkey";
|
|
4
4
|
import { atom } from "nanostores";
|
|
5
5
|
import react, { isValidElement, useCallback, useState } from "react";
|
|
6
|
-
import { Editor, Element, Path, Transforms } from "slate";
|
|
6
|
+
import { Editor, Element, Path, Range, Transforms } from "slate";
|
|
7
7
|
import { Editable, ReactEditor, useReadOnly } from "slate-react";
|
|
8
8
|
import { BaseElement } from "../elements/BaseElement.js";
|
|
9
9
|
import { Block } from "../elements/Block.js";
|
|
@@ -128,11 +128,10 @@ const createEditable = (editor, plugins)=>({ readOnly })=>{
|
|
|
128
128
|
at: editor.selection?.focus,
|
|
129
129
|
match: (n)=>Element.isElement(n) && Editor.isBlock(editor, n)
|
|
130
130
|
});
|
|
131
|
-
if (!currentBlock) return;
|
|
132
|
-
if (editor.selection?.focus.offset !== 0) return;
|
|
131
|
+
if (!currentBlock || !editor.selection || !Range.isCollapsed(editor.selection) || !Editor.isStart(editor, editor.selection.focus, currentBlock[1])) return;
|
|
133
132
|
const prevBlock = Editor.previous(editor, {
|
|
134
133
|
at: currentBlock[1],
|
|
135
|
-
match: (n)=>Element.isElement(n),
|
|
134
|
+
match: (n)=>Element.isElement(n) && Editor.isBlock(editor, n),
|
|
136
135
|
voids: true
|
|
137
136
|
});
|
|
138
137
|
if (prevBlock && Editor.isVoid(editor, prevBlock[0])) {
|
|
@@ -17,7 +17,7 @@ class CollapsibleBlocksPlugin {
|
|
|
17
17
|
const editor = useSlate();
|
|
18
18
|
const path = ReactEditor.findPath(editor, props.element);
|
|
19
19
|
if (1 !== path.length) return props.children;
|
|
20
|
-
const isHidden =
|
|
20
|
+
const isHidden = isHiddenByCollapsedSection(editor, path);
|
|
21
21
|
if (isHidden) return /*#__PURE__*/ jsx("div", {
|
|
22
22
|
className: styles_module.hidden,
|
|
23
23
|
"data-collapsible-block-hidden": true,
|
|
@@ -94,7 +94,9 @@ const getCollapsibleSectionPaths = (editor, path)=>{
|
|
|
94
94
|
path[0] + index
|
|
95
95
|
]);
|
|
96
96
|
};
|
|
97
|
-
const
|
|
97
|
+
const isHiddenByCollapsedSection = (editor, path)=>{
|
|
98
|
+
const types = sectionTypesByEditor.get(editor);
|
|
99
|
+
if (!types) return false;
|
|
98
100
|
for(let index = 0; index < path[0]; index++){
|
|
99
101
|
const node = editor.children[index];
|
|
100
102
|
if (Element.isElement(node) && true === node.collapsed && types.includes(node.type) && path[0] < getSectionEnd(editor, index, types)) return true;
|
|
@@ -118,4 +120,4 @@ const getSectionEnd = (editor, boundaryIndex, types)=>{
|
|
|
118
120
|
}
|
|
119
121
|
return editor.children.length;
|
|
120
122
|
};
|
|
121
|
-
export { CollapsibleBlocksPlugin, getCollapsibleSectionPaths };
|
|
123
|
+
export { CollapsibleBlocksPlugin, getCollapsibleSectionPaths, isHiddenByCollapsedSection };
|
|
@@ -6,7 +6,7 @@ import { NativeTypes } from "react-dnd-html5-backend";
|
|
|
6
6
|
import { Editor, Element, Path, Transforms } from "slate";
|
|
7
7
|
import { ReactEditor, useReadOnly, useSlate } from "slate-react";
|
|
8
8
|
import { useEditorContext } from "../../provider.js";
|
|
9
|
-
import { getCollapsibleSectionPaths } from "../CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.js";
|
|
9
|
+
import { getCollapsibleSectionPaths, isHiddenByCollapsedSection } from "../CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.js";
|
|
10
10
|
import { getNodesByNodeIds } from "./utils.js";
|
|
11
11
|
class DnDPlugin {
|
|
12
12
|
options;
|
|
@@ -34,11 +34,12 @@ class DnDPlugin {
|
|
|
34
34
|
const dropTargetRef = useRef(null);
|
|
35
35
|
const customType = options.customTypes?.[props.element.type];
|
|
36
36
|
const currentElement = props.element;
|
|
37
|
+
const path = ReactEditor.findPath(editor, props.element);
|
|
38
|
+
const isHidden = isHiddenByCollapsedSection(editor, path);
|
|
37
39
|
const [, drag, preview] = useDrag({
|
|
38
40
|
type: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
|
|
39
41
|
item: ()=>{
|
|
40
42
|
const selected = $selectedNodes;
|
|
41
|
-
const path = ReactEditor.findPath(editor, props.element);
|
|
42
43
|
const sectionPaths = getCollapsibleSectionPaths(editor, path);
|
|
43
44
|
const nodes = sectionPaths ? getNodesAtPaths(editor, sectionPaths) : selected.length > 0 && selected.find((node)=>node.nodeId === currentElement.nodeId) ? getNodesByNodeIds(editor, selected.map((node)=>node.nodeId)) : [
|
|
44
45
|
structuredClone(props.element)
|
|
@@ -142,6 +143,7 @@ class DnDPlugin {
|
|
|
142
143
|
if (this.options.ignoreNodes?.includes(props.element.type)) return props.children;
|
|
143
144
|
if (editor.isInline(props.element)) return props.children;
|
|
144
145
|
return /*#__PURE__*/ jsx("div", {
|
|
146
|
+
hidden: isHidden,
|
|
145
147
|
children: options.renderBlock?.({
|
|
146
148
|
props,
|
|
147
149
|
dragRef: drag,
|
|
@@ -11,3 +11,4 @@ export declare class CollapsibleBlocksPlugin implements IPlugin {
|
|
|
11
11
|
renderBlock: (props: RenderElementProps) => any;
|
|
12
12
|
}
|
|
13
13
|
export declare const getCollapsibleSectionPaths: (editor: Editor, path: Path) => number[][] | null;
|
|
14
|
+
export declare const isHiddenByCollapsedSection: (editor: Editor, path: Path) => boolean;
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, cleanup, fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import { Node, Transforms } from 'slate';
|
|
4
|
+
import { ReactEditor, Slate } from 'slate-react';
|
|
5
|
+
import { afterEach, expect, it } from 'vitest';
|
|
6
|
+
import type { IPlugin } from '../types';
|
|
7
|
+
import { createEditable } from './createEditable';
|
|
8
|
+
import { createEditor } from './createEditor';
|
|
9
|
+
|
|
10
|
+
afterEach(cleanup);
|
|
11
|
+
|
|
12
|
+
it.each([
|
|
13
|
+
{
|
|
14
|
+
name: 'after an inline void deletes only the chip',
|
|
15
|
+
point: { path: [1, 2], offset: 0 },
|
|
16
|
+
text: ['Previous link.', 'Keep this text '],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'at a paragraph start merges text rather than selecting a preceding inline void',
|
|
20
|
+
point: { path: [1, 0], offset: 0 },
|
|
21
|
+
text: ['Previous link.Keep this text delete me'],
|
|
22
|
+
},
|
|
23
|
+
])('Backspace $name', async ({ point, text }) => {
|
|
24
|
+
const plugin: IPlugin = {
|
|
25
|
+
blocks: [
|
|
26
|
+
{
|
|
27
|
+
type: 'chip',
|
|
28
|
+
isInline: true,
|
|
29
|
+
isVoid: true,
|
|
30
|
+
render: (props) => (
|
|
31
|
+
<span {...props.attributes}>
|
|
32
|
+
{props.children}
|
|
33
|
+
<span contentEditable={false}>Chip</span>
|
|
34
|
+
</span>
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
leafs: [
|
|
39
|
+
{
|
|
40
|
+
render: (props) => <span {...props.attributes}>{props.children}</span>,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
const editor = createEditor([plugin])();
|
|
45
|
+
const Editable = createEditable(editor, [plugin]);
|
|
46
|
+
const value = [
|
|
47
|
+
{
|
|
48
|
+
type: 'paragraph',
|
|
49
|
+
children: [
|
|
50
|
+
{ text: 'Previous ' },
|
|
51
|
+
{ type: 'chip', children: [{ text: 'link' }] },
|
|
52
|
+
{ text: '.' },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'paragraph',
|
|
57
|
+
children: [
|
|
58
|
+
{ text: 'Keep this text ' },
|
|
59
|
+
{ type: 'chip', children: [{ text: 'delete me' }] },
|
|
60
|
+
{ text: '' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const { container } = render(
|
|
65
|
+
<Slate editor={editor} initialValue={value}>
|
|
66
|
+
<Editable />
|
|
67
|
+
</Slate>,
|
|
68
|
+
);
|
|
69
|
+
const editable = container.querySelector<HTMLElement>('[data-slate-editor]')!;
|
|
70
|
+
// jsdom does not implement this browser property, which Slate checks for keyboard events.
|
|
71
|
+
Object.defineProperty(editable, 'isContentEditable', { value: true });
|
|
72
|
+
await act(async () => {
|
|
73
|
+
Transforms.select(editor, point);
|
|
74
|
+
ReactEditor.focus(editor);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await act(async () => {
|
|
78
|
+
const useDefault = fireEvent.keyDown(editable, {
|
|
79
|
+
key: 'Backspace',
|
|
80
|
+
keyCode: 8,
|
|
81
|
+
});
|
|
82
|
+
// jsdom does not dispatch the browser's beforeinput deletion.
|
|
83
|
+
if (useDefault) await editor.deleteBackward('character');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(editor.children.map(Node.string)).toEqual(text);
|
|
87
|
+
});
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
Element,
|
|
19
19
|
type NodeEntry,
|
|
20
20
|
Path,
|
|
21
|
+
Range,
|
|
21
22
|
Transforms,
|
|
22
23
|
} from 'slate';
|
|
23
24
|
import {
|
|
@@ -192,12 +193,17 @@ export const createEditable =
|
|
|
192
193
|
match: (n) => Element.isElement(n) && Editor.isBlock(editor, n),
|
|
193
194
|
});
|
|
194
195
|
|
|
195
|
-
if (
|
|
196
|
-
|
|
196
|
+
if (
|
|
197
|
+
!currentBlock ||
|
|
198
|
+
!editor.selection ||
|
|
199
|
+
!Range.isCollapsed(editor.selection) ||
|
|
200
|
+
!Editor.isStart(editor, editor.selection.focus, currentBlock[1])
|
|
201
|
+
)
|
|
202
|
+
return;
|
|
197
203
|
|
|
198
|
-
const prevBlock = Editor.previous(editor, {
|
|
204
|
+
const prevBlock = Editor.previous<CustomElement>(editor, {
|
|
199
205
|
at: currentBlock[1],
|
|
200
|
-
match: (n) => Element.isElement(n),
|
|
206
|
+
match: (n) => Element.isElement(n) && Editor.isBlock(editor, n),
|
|
201
207
|
voids: true,
|
|
202
208
|
});
|
|
203
209
|
|
|
@@ -29,7 +29,7 @@ export class CollapsibleBlocksPlugin implements IPlugin {
|
|
|
29
29
|
return props.children;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const isHidden =
|
|
32
|
+
const isHidden = isHiddenByCollapsedSection(editor, path);
|
|
33
33
|
|
|
34
34
|
if (isHidden) {
|
|
35
35
|
return (
|
|
@@ -129,11 +129,10 @@ export const getCollapsibleSectionPaths = (editor: Editor, path: Path) => {
|
|
|
129
129
|
return Array.from({ length: end - path[0] }, (_, index) => [path[0] + index]);
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
const
|
|
133
|
-
editor
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
) => {
|
|
132
|
+
export const isHiddenByCollapsedSection = (editor: Editor, path: Path) => {
|
|
133
|
+
const types = sectionTypesByEditor.get(editor);
|
|
134
|
+
if (!types) return false;
|
|
135
|
+
|
|
137
136
|
for (let index = 0; index < path[0]; index++) {
|
|
138
137
|
const node = editor.children[index] as CollapsibleElement;
|
|
139
138
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
+
import '@testing-library/jest-dom/vitest';
|
|
3
4
|
import { act, render, waitFor } from '@testing-library/react';
|
|
4
5
|
import { createDragDropManager, type DragDropManager } from 'dnd-core';
|
|
5
6
|
import React from 'react';
|
|
@@ -343,4 +344,65 @@ describe('DnDPlugin', () => {
|
|
|
343
344
|
]);
|
|
344
345
|
await waitFor(() => expect(onChange).toHaveBeenCalled());
|
|
345
346
|
});
|
|
347
|
+
|
|
348
|
+
it('hides custom drag wrappers for collapsed section content', async () => {
|
|
349
|
+
const manager = createDndManager();
|
|
350
|
+
const dndPlugin = new DnDPlugin({
|
|
351
|
+
onDropFiles: () => {},
|
|
352
|
+
renderBlock: ({ dragRef, dropRef, previewRef, props }) =>
|
|
353
|
+
React.createElement(
|
|
354
|
+
'div',
|
|
355
|
+
{
|
|
356
|
+
...props.attributes,
|
|
357
|
+
'data-dnd-block': (props.element as { nodeId: string }).nodeId,
|
|
358
|
+
ref: (element) => dropRef(previewRef(element)),
|
|
359
|
+
},
|
|
360
|
+
React.createElement(
|
|
361
|
+
'button',
|
|
362
|
+
{ contentEditable: false, ref: dragRef, type: 'button' },
|
|
363
|
+
'Drag',
|
|
364
|
+
),
|
|
365
|
+
props.children,
|
|
366
|
+
),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
render(
|
|
370
|
+
React.createElement(
|
|
371
|
+
DndProvider,
|
|
372
|
+
{ manager },
|
|
373
|
+
React.createElement(KonaEditor, {
|
|
374
|
+
initialValue: [
|
|
375
|
+
{
|
|
376
|
+
type: HeadingsPlugin.HeadingLevel1,
|
|
377
|
+
nodeId: 'section',
|
|
378
|
+
collapsed: true,
|
|
379
|
+
children: [{ text: 'Section' }],
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
type: 'paragraph',
|
|
383
|
+
nodeId: 'section-text',
|
|
384
|
+
children: [{ text: 'Section text' }],
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
type: HeadingsPlugin.HeadingLevel1,
|
|
388
|
+
nodeId: 'next-section',
|
|
389
|
+
children: [{ text: 'Next section' }],
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
plugins: [
|
|
393
|
+
new HeadingsPlugin(),
|
|
394
|
+
new CollapsibleBlocksPlugin([HeadingsPlugin.HeadingLevel1]),
|
|
395
|
+
dndPlugin,
|
|
396
|
+
],
|
|
397
|
+
onChange: () => {},
|
|
398
|
+
}),
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const hiddenDragBlock = document.querySelector(
|
|
403
|
+
'[data-dnd-block="section-text"]',
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(hiddenDragBlock).not.toBeVisible();
|
|
407
|
+
});
|
|
346
408
|
});
|
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
import type { CustomElement } from '../../../types';
|
|
13
13
|
import { useEditorContext } from '../../provider';
|
|
14
14
|
import type { IPlugin } from '../../types';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getCollapsibleSectionPaths,
|
|
17
|
+
isHiddenByCollapsedSection,
|
|
18
|
+
} from '../CollapsibleBlocksPlugin/CollapsibleBlocksPlugin';
|
|
16
19
|
import type { EditorDragItem, NodeWithId, Options } from './types';
|
|
17
20
|
import { getNodesByNodeIds } from './utils';
|
|
18
21
|
|
|
@@ -50,12 +53,13 @@ export class DnDPlugin implements IPlugin {
|
|
|
50
53
|
|
|
51
54
|
const customType = options.customTypes?.[props.element.type];
|
|
52
55
|
const currentElement = props.element as NodeWithId;
|
|
56
|
+
const path = ReactEditor.findPath(editor, props.element);
|
|
57
|
+
const isHidden = isHiddenByCollapsedSection(editor, path);
|
|
53
58
|
|
|
54
59
|
const [, drag, preview] = useDrag({
|
|
55
60
|
type: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
|
|
56
61
|
item: () => {
|
|
57
62
|
const selected: Array<NodeWithId> = $selectedNodes;
|
|
58
|
-
const path = ReactEditor.findPath(editor, props.element);
|
|
59
63
|
const sectionPaths = getCollapsibleSectionPaths(editor, path);
|
|
60
64
|
|
|
61
65
|
/**
|
|
@@ -252,7 +256,7 @@ export class DnDPlugin implements IPlugin {
|
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
return (
|
|
255
|
-
<div>
|
|
259
|
+
<div hidden={isHidden}>
|
|
256
260
|
{options.renderBlock?.({
|
|
257
261
|
props,
|
|
258
262
|
dragRef: drag,
|