hero-editor 1.9.0 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/Icon.js +2 -0
- package/src/constants.js +4 -0
- package/src/helpers/__tests__/helpers.test.js +21 -0
- package/src/helpers/elementHelpers.js +4 -1
- package/src/helpers/getUrlFromNode.js +6 -0
- package/src/helpers/index.js +1 -0
- package/src/lib.js +2 -1
- package/src/plugins/__tests__/__snapshots__/link.test.js.snap +11 -0
- package/src/plugins/__tests__/link.test.js +140 -0
- package/src/plugins/link.js +112 -15
package/package.json
CHANGED
package/src/components/Icon.js
CHANGED
|
@@ -15,6 +15,8 @@ const svgIcons = {
|
|
|
15
15
|
'<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14h-2V9h-2V7h4v10z"/></svg>',
|
|
16
16
|
looks_two:
|
|
17
17
|
'<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4 8c0 1.11-.9 2-2 2h-2v2h4v2H9v-4c0-1.11.9-2 2-2h2V9H9V7h4c1.1 0 2 .89 2 2v2z"/></svg>',
|
|
18
|
+
link:
|
|
19
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13.723 18.654l-3.61 3.609c-2.316 2.315-6.063 2.315-8.378 0-1.12-1.118-1.735-2.606-1.735-4.188 0-1.582.615-3.07 1.734-4.189l4.866-4.865c2.355-2.355 6.114-2.262 8.377 0 .453.453.81.973 1.089 1.527l-1.593 1.592c-.18-.613-.5-1.189-.964-1.652-1.448-1.448-3.93-1.51-5.439-.001l-.001.002-4.867 4.865c-1.5 1.499-1.5 3.941 0 5.44 1.517 1.517 3.958 1.488 5.442 0l2.425-2.424c.993.284 1.791.335 2.654.284zm.161-16.918l-3.574 3.576c.847-.05 1.655 0 2.653.283l2.393-2.389c1.498-1.502 3.94-1.5 5.44-.001 1.517 1.518 1.486 3.959 0 5.442l-4.831 4.831-.003.002c-1.438 1.437-3.886 1.552-5.439-.002-.473-.474-.785-1.042-.956-1.643l-.084.068-1.517 1.515c.28.556.635 1.075 1.088 1.528 2.245 2.245 6.004 2.374 8.378 0l4.832-4.831c2.314-2.316 2.316-6.062-.001-8.377-2.317-2.321-6.067-2.313-8.379-.002z"/></svg>',
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
const styles = {
|
package/src/constants.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
composeRenderLeaf,
|
|
8
8
|
makeRenderLeaf,
|
|
9
9
|
getUrl,
|
|
10
|
+
getUrlFromNode,
|
|
10
11
|
isEmptyContent,
|
|
11
12
|
} from '../index';
|
|
12
13
|
|
|
@@ -47,6 +48,26 @@ describe('getUrl', () => {
|
|
|
47
48
|
});
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
describe('getUrlFromNode', () => {
|
|
52
|
+
it.each`
|
|
53
|
+
node | expected
|
|
54
|
+
${{ type: 'link', data: { url: 'https://google.com' }, children: [{ text: 'link' }] }} | ${'https://google.com'}
|
|
55
|
+
${{ type: 'link', data: { url: 'https://google.com' }, children: [{ text: 'https:/' }, { text: 'google.com', bold: true }] }} | ${'https://google.com'}
|
|
56
|
+
`('gets URL from link node', ({ node, expected }) => {
|
|
57
|
+
expect(getUrlFromNode(node)).toEqual(expected);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.each`
|
|
61
|
+
node | expected
|
|
62
|
+
${{ type: 'paragraph', children: [{ text: 'text' }] }} | ${null}
|
|
63
|
+
${null} | ${null}
|
|
64
|
+
${undefined} | ${null}
|
|
65
|
+
${{}} | ${null}
|
|
66
|
+
`('returns null when link node is not provided', ({ node, expected }) => {
|
|
67
|
+
expect(getUrlFromNode(node)).toEqual(expected);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
50
71
|
describe('compose', () => {
|
|
51
72
|
it('composes functions', () => {
|
|
52
73
|
expect(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Editor } from 'slate';
|
|
3
3
|
import compose from './compose';
|
|
4
|
-
import { NUMBERED_LIST, BULLETED_LIST } from '../constants';
|
|
4
|
+
import { NUMBERED_LIST, BULLETED_LIST, LINK } from '../constants';
|
|
5
5
|
|
|
6
6
|
const isBlockActive = (editor, format) => {
|
|
7
7
|
const [match] = Editor.nodes(editor, {
|
|
@@ -12,6 +12,8 @@ const isBlockActive = (editor, format) => {
|
|
|
12
12
|
|
|
13
13
|
const isList = (node) => [BULLETED_LIST, NUMBERED_LIST].includes(node.type);
|
|
14
14
|
|
|
15
|
+
const isLink = (node) => node.type === LINK;
|
|
16
|
+
|
|
15
17
|
const DefaultElement = React.forwardRef((props, ref) => (
|
|
16
18
|
<p ref={ref} style={{ margin: 0, lineHeight: 1.5 }} {...props} />
|
|
17
19
|
));
|
|
@@ -35,6 +37,7 @@ const composeRenderElement = (plugins) =>
|
|
|
35
37
|
export {
|
|
36
38
|
isBlockActive,
|
|
37
39
|
isList,
|
|
40
|
+
isLink,
|
|
38
41
|
renderDefaultElement,
|
|
39
42
|
makeRenderElement,
|
|
40
43
|
composeRenderElement,
|
package/src/helpers/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { default as apply } from './apply';
|
|
|
9
9
|
export { default as createPlugin } from './createPlugin';
|
|
10
10
|
export { default as withId } from './withId';
|
|
11
11
|
export { default as getUrl } from './getUrl';
|
|
12
|
+
export { default as getUrlFromNode } from './getUrlFromNode';
|
|
12
13
|
export { default as isEmptyContent } from './isEmptyContent';
|
|
13
14
|
|
|
14
15
|
export { default as useForceUpdate } from './useForceUpdate';
|
package/src/lib.js
CHANGED
|
@@ -147,9 +147,10 @@ const HeroEditor = ({
|
|
|
147
147
|
export * from './plugins';
|
|
148
148
|
export { default as JsonViewer } from './components/JsonViewer';
|
|
149
149
|
export { default as Toolbar } from './components/Toolbar';
|
|
150
|
+
export { default as Icon } from './components/Icon';
|
|
150
151
|
export { default as plainSerializer } from './serializers/plain';
|
|
151
152
|
export { default as makeReactTransformer } from './transformers/react';
|
|
152
153
|
export { defaultReactTransformer } from './transformers/react';
|
|
153
|
-
export { isEmptyContent, getUrl, isUrl } from './helpers';
|
|
154
|
+
export { isEmptyContent, getUrl, isUrl, getUrlFromNode } from './helpers';
|
|
154
155
|
export { EMPTY_VALUE } from './constants';
|
|
155
156
|
export default HeroEditor;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createEditor, Transforms } from 'slate';
|
|
2
|
+
import { render, waitFor } from '@testing-library/react';
|
|
3
|
+
import link from '../link';
|
|
4
|
+
import {
|
|
5
|
+
createPlugin,
|
|
6
|
+
renderDefaultElement,
|
|
7
|
+
postMessage,
|
|
8
|
+
withId,
|
|
9
|
+
} from '../../helpers';
|
|
10
|
+
|
|
11
|
+
describe('link plugin', () => {
|
|
12
|
+
it('is a plugin object', () => {
|
|
13
|
+
for (const prop in createPlugin()) {
|
|
14
|
+
expect(link()[prop]).toBeDefined();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders a link element', () => {
|
|
19
|
+
const { baseElement } = render(
|
|
20
|
+
link().renderElement(renderDefaultElement)({
|
|
21
|
+
element: {
|
|
22
|
+
type: 'link',
|
|
23
|
+
data: { url: 'https://www.example.com' },
|
|
24
|
+
children: [
|
|
25
|
+
{
|
|
26
|
+
text: 'Link example',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
expect(baseElement).toMatchSnapshot();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('handle add link message properly', () => {
|
|
36
|
+
it('add new text with url', async () => {
|
|
37
|
+
const editor = withId('link-sample')(createEditor());
|
|
38
|
+
link().handleMessage(editor);
|
|
39
|
+
|
|
40
|
+
editor.insertNode({
|
|
41
|
+
type: 'paragraph',
|
|
42
|
+
children: [
|
|
43
|
+
{
|
|
44
|
+
text: '',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
postMessage(
|
|
50
|
+
'link',
|
|
51
|
+
{ text: 'Link example', url: 'https://www.example.com' },
|
|
52
|
+
editor,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(editor.children).toEqual([
|
|
57
|
+
{
|
|
58
|
+
type: 'paragraph',
|
|
59
|
+
children: [
|
|
60
|
+
{
|
|
61
|
+
text: '',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: 'link',
|
|
67
|
+
children: [
|
|
68
|
+
{
|
|
69
|
+
text: 'Link example',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
data: {
|
|
73
|
+
url: 'https://www.example.com',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('add url to an existing text', async () => {
|
|
81
|
+
const editor = withId('link-sample')(createEditor());
|
|
82
|
+
link().handleMessage(editor);
|
|
83
|
+
|
|
84
|
+
editor.insertNode({
|
|
85
|
+
type: 'paragraph',
|
|
86
|
+
children: [
|
|
87
|
+
{
|
|
88
|
+
text: 'this is an url',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
Transforms.select(editor, {
|
|
94
|
+
anchor: {
|
|
95
|
+
path: [0, 0],
|
|
96
|
+
offset: 14,
|
|
97
|
+
},
|
|
98
|
+
focus: {
|
|
99
|
+
path: [0, 0],
|
|
100
|
+
offset: 11,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
postMessage(
|
|
105
|
+
'link',
|
|
106
|
+
{ text: 'url', url: 'https://www.example.com' },
|
|
107
|
+
editor,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(editor.children).toEqual([
|
|
112
|
+
{
|
|
113
|
+
type: 'paragraph',
|
|
114
|
+
children: [
|
|
115
|
+
{
|
|
116
|
+
text: 'this is an ',
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: 'link',
|
|
122
|
+
children: [
|
|
123
|
+
{
|
|
124
|
+
children: [
|
|
125
|
+
{
|
|
126
|
+
text: 'url',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
type: 'paragraph',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
data: {
|
|
133
|
+
url: 'https://www.example.com',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
package/src/plugins/link.js
CHANGED
|
@@ -1,32 +1,69 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { useSlate } from 'slate-react';
|
|
2
3
|
import isUrl from 'is-url';
|
|
3
|
-
import { Transforms } from 'slate';
|
|
4
|
+
import { Transforms, Editor, Range } from 'slate';
|
|
4
5
|
import {
|
|
5
6
|
createPlugin,
|
|
6
7
|
makeRenderElement,
|
|
7
8
|
addMessageListener,
|
|
8
9
|
postMessage,
|
|
10
|
+
isBlockActive,
|
|
11
|
+
isLink,
|
|
12
|
+
getUrlFromNode,
|
|
9
13
|
} from '../helpers';
|
|
10
|
-
import { LINK } from '../constants';
|
|
14
|
+
import { LINK, ADD_LINK } from '../constants';
|
|
15
|
+
import Toolbar from '../components/Toolbar';
|
|
16
|
+
import Icon from '../components/Icon';
|
|
11
17
|
|
|
12
|
-
const
|
|
13
|
-
|
|
18
|
+
const getUrlFromNodes = (editor) => {
|
|
19
|
+
const [...linkNodes] = Editor.nodes(editor, { match: isLink });
|
|
20
|
+
|
|
21
|
+
if (linkNodes.length === 1) {
|
|
22
|
+
const [node] = linkNodes[0];
|
|
23
|
+
return getUrlFromNode(node);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return '';
|
|
27
|
+
};
|
|
14
28
|
|
|
15
29
|
const LinkElement = ({ attributes, children, element }) => (
|
|
16
|
-
<a {...attributes} href={
|
|
30
|
+
<a {...attributes} href={getUrlFromNode(element)}>
|
|
17
31
|
{children}
|
|
18
32
|
</a>
|
|
19
33
|
);
|
|
20
34
|
|
|
21
35
|
const renderElement = makeRenderElement(LINK, LinkElement);
|
|
22
36
|
|
|
23
|
-
const handleMessage = addMessageListener(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
const handleMessage = addMessageListener(
|
|
38
|
+
LINK,
|
|
39
|
+
({ editor, data: { text, url } }) => {
|
|
40
|
+
if (!text || !url || !isUrl(url)) return;
|
|
41
|
+
|
|
42
|
+
if (isBlockActive(editor, LINK)) {
|
|
43
|
+
Transforms.unwrapNodes(editor, {
|
|
44
|
+
match: isLink,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { selection } = editor;
|
|
49
|
+
const isCollapsed = selection && Range.isCollapsed(selection);
|
|
50
|
+
|
|
51
|
+
const linkNode = {
|
|
52
|
+
type: LINK,
|
|
53
|
+
data: { url },
|
|
54
|
+
children: isCollapsed ? [{ text: text ?? url }] : [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (isCollapsed) {
|
|
58
|
+
Transforms.insertNodes(editor, linkNode);
|
|
59
|
+
} else {
|
|
60
|
+
Transforms.wrapNodes(editor, linkNode, { split: true });
|
|
61
|
+
Transforms.collapse(editor, { edge: 'end' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Transforms.move(editor, { unit: 'offset' });
|
|
65
|
+
},
|
|
66
|
+
);
|
|
30
67
|
|
|
31
68
|
const enhanceEditor = (editor) => {
|
|
32
69
|
const { isInline, insertData } = editor;
|
|
@@ -39,7 +76,7 @@ const enhanceEditor = (editor) => {
|
|
|
39
76
|
const text = data.getData('text/plain');
|
|
40
77
|
|
|
41
78
|
if (text && isUrl(text)) {
|
|
42
|
-
postMessage(LINK, { url: text }, editor);
|
|
79
|
+
postMessage(LINK, { text, url: text }, editor);
|
|
43
80
|
} else {
|
|
44
81
|
insertData(data);
|
|
45
82
|
}
|
|
@@ -48,10 +85,70 @@ const enhanceEditor = (editor) => {
|
|
|
48
85
|
return editor;
|
|
49
86
|
};
|
|
50
87
|
|
|
51
|
-
|
|
88
|
+
const AddLinkModal = ({ renderAddLinkCustom }) => {
|
|
89
|
+
const [showAddLinkCustom, setShowAddLinkCustom] = useState(false);
|
|
90
|
+
const editor = useSlate();
|
|
91
|
+
|
|
92
|
+
const { selection } = editor || {};
|
|
93
|
+
|
|
94
|
+
const handleAddLink = useCallback((data) => {
|
|
95
|
+
postMessage(LINK, data, editor);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const getSelectedData = (editor) => ({
|
|
99
|
+
text: selection ? Editor.string(editor, selection) : '',
|
|
100
|
+
url: getUrlFromNodes(editor),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const removeAddLinkListener = addMessageListener(ADD_LINK, () => {
|
|
105
|
+
setShowAddLinkCustom(true);
|
|
106
|
+
})(editor);
|
|
107
|
+
|
|
108
|
+
return () => removeAddLinkListener();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<>
|
|
113
|
+
{showAddLinkCustom &&
|
|
114
|
+
renderAddLinkCustom?.({
|
|
115
|
+
handleAddLink,
|
|
116
|
+
getSelectedData: () => getSelectedData(editor),
|
|
117
|
+
hideAddLinkCustom: () => setShowAddLinkCustom(false),
|
|
118
|
+
})}
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ToolbarButton = ({ showToolbarButton }) => {
|
|
124
|
+
const editor = useSlate();
|
|
125
|
+
|
|
126
|
+
const handleMouseDown = useCallback(() => {
|
|
127
|
+
postMessage(ADD_LINK, {}, editor);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
if (!showToolbarButton) return null;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<Toolbar.Button active={false} onMouseDown={handleMouseDown}>
|
|
135
|
+
<Icon>link</Icon>
|
|
136
|
+
</Toolbar.Button>
|
|
137
|
+
<Toolbar.Separator />
|
|
138
|
+
</>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default ({ renderAddLinkCustom, showToolbarButton } = {}) =>
|
|
52
143
|
createPlugin({
|
|
53
144
|
name: LINK,
|
|
54
145
|
renderElement,
|
|
55
146
|
handleMessage,
|
|
147
|
+
renderCustom: () => (
|
|
148
|
+
<AddLinkModal renderAddLinkCustom={renderAddLinkCustom} />
|
|
149
|
+
),
|
|
150
|
+
ToolbarButton: () => (
|
|
151
|
+
<ToolbarButton showToolbarButton={showToolbarButton} />
|
|
152
|
+
),
|
|
56
153
|
enhanceEditor,
|
|
57
154
|
});
|