hero-editor 1.9.4 → 1.9.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hero-editor",
3
- "version": "1.9.4",
3
+ "version": "1.9.7",
4
4
  "description": "",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {
@@ -16,7 +16,7 @@ const svgIcons = {
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
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>',
19
+ '<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="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>',
20
20
  };
21
21
 
22
22
  const styles = {
@@ -25,9 +25,9 @@ const styles = {
25
25
  },
26
26
  };
27
27
 
28
- const Icon = ({ children }) => (
28
+ const Icon = ({ children, height = 20 }) => (
29
29
  <img
30
- height={20}
30
+ height={height}
31
31
  src={`data:image/svg+xml;utf8,${svgIcons[children]}`}
32
32
  style={styles.icon}
33
33
  />
package/src/lib.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useEffect, useRef } from 'react';
1
+ import React, { useState, useMemo, useEffect, useRef } from 'react';
2
2
  import { createEditor } from 'slate';
3
3
  import { Slate, Editable, withReact } from 'slate-react';
4
4
  import {
@@ -62,16 +62,15 @@ const HeroEditor = ({
62
62
  const wrapper = useRef(null);
63
63
  const wrapperLayout = useRef({ width: 0, height: 0 });
64
64
 
65
- const editor = useMemo(
66
- () =>
67
- flow(
68
- createEditor,
69
- withReact,
70
- withId(id),
71
- ...map(get('enhanceEditor'))(plugins),
72
- )(),
73
- [],
74
- );
65
+ const editorRef = useRef();
66
+ if (!editorRef.current)
67
+ editorRef.current = flow(
68
+ createEditor,
69
+ withReact,
70
+ withId(id),
71
+ ...map(get('enhanceEditor'))(plugins),
72
+ )();
73
+ const editor = editorRef.current;
75
74
 
76
75
  const renderLeaf = useMemo(() => composeRenderLeaf(plugins), [plugins]);
77
76
 
@@ -8,6 +8,33 @@ import {
8
8
  withId,
9
9
  } from '../../helpers';
10
10
 
11
+ import { EMPTY_VALUE } from '../../constants';
12
+
13
+ const SAMPLE_PARAGRAPH_WITH_URL = {
14
+ type: 'paragraph',
15
+ children: [
16
+ {
17
+ text: 'this is an ',
18
+ },
19
+ {
20
+ type: 'link',
21
+ children: [
22
+ {
23
+ children: [
24
+ {
25
+ text: 'url',
26
+ },
27
+ ],
28
+ type: 'paragraph',
29
+ },
30
+ ],
31
+ data: {
32
+ url: 'https://www.example.com',
33
+ },
34
+ },
35
+ ],
36
+ };
37
+
11
38
  describe('link plugin', () => {
12
39
  it('is a plugin object', () => {
13
40
  for (const prop in createPlugin()) {
@@ -37,14 +64,7 @@ describe('link plugin', () => {
37
64
  const editor = withId('link-sample')(createEditor());
38
65
  link().handleMessage(editor);
39
66
 
40
- editor.insertNode({
41
- type: 'paragraph',
42
- children: [
43
- {
44
- text: '',
45
- },
46
- ],
47
- });
67
+ editor.insertNode(EMPTY_VALUE);
48
68
 
49
69
  postMessage(
50
70
  'link',
@@ -136,5 +156,96 @@ describe('link plugin', () => {
136
156
  ]);
137
157
  });
138
158
  });
159
+
160
+ it('update url of existing hyperlink', async () => {
161
+ const editor = withId('link-sample')(createEditor());
162
+ link().handleMessage(editor);
163
+
164
+ editor.insertNode(SAMPLE_PARAGRAPH_WITH_URL);
165
+
166
+ Transforms.select(editor, {
167
+ anchor: {
168
+ path: [0, 0],
169
+ offset: 11,
170
+ },
171
+ focus: {
172
+ path: [0, 0],
173
+ offset: 11,
174
+ },
175
+ });
176
+
177
+ postMessage(
178
+ 'link',
179
+ { text: 'url', url: 'https://www.google.com' },
180
+ editor,
181
+ );
182
+
183
+ await waitFor(() => {
184
+ expect(editor.children).toEqual([
185
+ {
186
+ type: 'paragraph',
187
+ children: [{ text: 'this is an ' }],
188
+ },
189
+ {
190
+ type: 'link',
191
+ children: [
192
+ {
193
+ text: 'url',
194
+ },
195
+ ],
196
+ data: {
197
+ url: 'https://www.google.com',
198
+ },
199
+ },
200
+ ]);
201
+ });
202
+ });
203
+
204
+ it('update text of existing hyperlink', async () => {
205
+ const editor = withId('link-sample')(createEditor());
206
+ link().handleMessage(editor);
207
+
208
+ editor.insertNode(SAMPLE_PARAGRAPH_WITH_URL);
209
+
210
+ Transforms.select(editor, {
211
+ anchor: {
212
+ path: [0, 0],
213
+ offset: 11,
214
+ },
215
+ focus: {
216
+ path: [0, 0],
217
+ offset: 11,
218
+ },
219
+ });
220
+
221
+ postMessage(
222
+ 'link',
223
+ {
224
+ text: 'updated example url',
225
+ url: 'https://www.example.com',
226
+ },
227
+ editor,
228
+ );
229
+
230
+ await waitFor(() => {
231
+ expect(editor.children).toEqual([
232
+ {
233
+ type: 'paragraph',
234
+ children: [{ text: 'this is an ' }],
235
+ },
236
+ {
237
+ type: 'link',
238
+ children: [
239
+ {
240
+ text: 'updated example url',
241
+ },
242
+ ],
243
+ data: {
244
+ url: 'https://www.example.com',
245
+ },
246
+ },
247
+ ]);
248
+ });
249
+ });
139
250
  });
140
251
  });
@@ -15,15 +15,31 @@ import { LINK, ADD_LINK } from '../constants';
15
15
  import Toolbar from '../components/Toolbar';
16
16
  import Icon from '../components/Icon';
17
17
 
18
- const getUrlFromNodes = (editor) => {
19
- const [...linkNodes] = Editor.nodes(editor, { match: isLink });
18
+ const getTextFromNode = (element) =>
19
+ element.children.reduce((text, node) => text + node.text, '');
20
20
 
21
- if (linkNodes.length === 1) {
22
- const [node] = linkNodes[0];
23
- return getUrlFromNode(node);
24
- }
21
+ const getLinkNodeAtSelection = (editor) => {
22
+ const { selection } = editor;
25
23
 
26
- return '';
24
+ return selection
25
+ ? Editor.above(editor, {
26
+ at: selection,
27
+ match: isLink,
28
+ })
29
+ : undefined;
30
+ };
31
+
32
+ const getTextFromSelection = (editor) => {
33
+ const linkNode = getLinkNodeAtSelection(editor);
34
+ if (linkNode) return getTextFromNode(linkNode[0]);
35
+
36
+ const { selection } = editor;
37
+ return selection ? Editor.string(editor, selection) : '';
38
+ };
39
+
40
+ const getUrlFromSelection = (editor) => {
41
+ const linkNode = getLinkNodeAtSelection(editor);
42
+ return linkNode ? getUrlFromNode(linkNode[0]) : '';
27
43
  };
28
44
 
29
45
  const LinkElement = ({ attributes, children, element }) => (
@@ -39,22 +55,30 @@ const handleMessage = addMessageListener(
39
55
  ({ editor, data: { text, url } }) => {
40
56
  if (!text || !url || !isUrl(url)) return;
41
57
 
42
- if (isBlockActive(editor, LINK)) {
43
- Transforms.unwrapNodes(editor, {
44
- match: isLink,
45
- });
46
- }
47
-
48
58
  const { selection } = editor;
49
59
  const isCollapsed = selection && Range.isCollapsed(selection);
60
+ const isTextChanged = selection && Editor.string(editor, selection) != text;
61
+ const insertingText = isCollapsed || isTextChanged;
50
62
 
51
63
  const linkNode = {
52
64
  type: LINK,
53
65
  data: { url },
54
- children: isCollapsed ? [{ text: text ?? url }] : [],
66
+ children: insertingText ? [{ text: text ?? url }] : [],
55
67
  };
56
68
 
57
- if (isCollapsed) {
69
+ const linkNodeAtSelection = getLinkNodeAtSelection(editor);
70
+ if (linkNodeAtSelection) {
71
+ const [, path] = linkNodeAtSelection;
72
+ Transforms.select(editor, path);
73
+ }
74
+
75
+ if (isBlockActive(editor, LINK)) {
76
+ Transforms.unwrapNodes(editor, {
77
+ match: isLink,
78
+ });
79
+ }
80
+
81
+ if (insertingText) {
58
82
  Transforms.insertNodes(editor, linkNode);
59
83
  } else {
60
84
  Transforms.wrapNodes(editor, linkNode, { split: true });
@@ -74,9 +98,16 @@ const enhanceEditor = (editor) => {
74
98
 
75
99
  editor.insertData = (data) => {
76
100
  const text = data.getData('text/plain');
77
-
101
+ const { selection } = editor;
78
102
  if (text && isUrl(text)) {
79
- postMessage(LINK, { text, url: text }, editor);
103
+ postMessage(
104
+ LINK,
105
+ {
106
+ text: Editor.string(editor, selection) || text,
107
+ url: text,
108
+ },
109
+ editor,
110
+ );
80
111
  } else {
81
112
  insertData(data);
82
113
  }
@@ -85,36 +116,34 @@ const enhanceEditor = (editor) => {
85
116
  return editor;
86
117
  };
87
118
 
88
- const AddLinkModal = ({ renderAddLinkCustom }) => {
89
- const [showAddLinkCustom, setShowAddLinkCustom] = useState(false);
119
+ const LinkCustomWrapper = ({ renderLinkCustom }) => {
120
+ const [showLinkCustom, setShowLinkCustom] = useState(false);
90
121
  const editor = useSlate();
91
122
 
92
- const { selection } = editor || {};
93
-
94
123
  const handleAddLink = useCallback((data) => {
95
124
  postMessage(LINK, data, editor);
96
125
  }, []);
97
126
 
98
127
  const getSelectedData = (editor) => ({
99
- text: selection ? Editor.string(editor, selection) : '',
100
- url: getUrlFromNodes(editor),
128
+ text: getTextFromSelection(editor),
129
+ url: getUrlFromSelection(editor),
101
130
  });
102
131
 
103
132
  useEffect(() => {
104
- const removeAddLinkListener = addMessageListener(ADD_LINK, () => {
105
- setShowAddLinkCustom(true);
133
+ const removeLinkCustomListener = addMessageListener(ADD_LINK, () => {
134
+ setShowLinkCustom(true);
106
135
  })(editor);
107
136
 
108
- return () => removeAddLinkListener();
137
+ return () => removeLinkCustomListener();
109
138
  });
110
139
 
111
140
  return (
112
141
  <>
113
- {showAddLinkCustom &&
114
- renderAddLinkCustom?.({
142
+ {showLinkCustom &&
143
+ renderLinkCustom?.({
115
144
  handleAddLink,
116
145
  getSelectedData: () => getSelectedData(editor),
117
- hideAddLinkCustom: () => setShowAddLinkCustom(false),
146
+ hideLinkCustom: () => setShowLinkCustom(false),
118
147
  })}
119
148
  </>
120
149
  );
@@ -129,23 +158,28 @@ const ToolbarButton = ({ showToolbarButton }) => {
129
158
 
130
159
  if (!showToolbarButton) return null;
131
160
 
161
+ const isLinkNodeAtSelection = !!getLinkNodeAtSelection(editor);
162
+
132
163
  return (
133
164
  <>
134
- <Toolbar.Button active={false} onMouseDown={handleMouseDown}>
135
- <Icon>link</Icon>
165
+ <Toolbar.Button
166
+ active={isLinkNodeAtSelection}
167
+ onMouseDown={handleMouseDown}
168
+ >
169
+ <Icon height={18}>link</Icon>
136
170
  </Toolbar.Button>
137
171
  <Toolbar.Separator />
138
172
  </>
139
173
  );
140
174
  };
141
175
 
142
- export default ({ renderAddLinkCustom, showToolbarButton } = {}) =>
176
+ export default ({ renderLinkCustom, showToolbarButton } = {}) =>
143
177
  createPlugin({
144
178
  name: LINK,
145
179
  renderElement,
146
180
  handleMessage,
147
181
  renderCustom: () => (
148
- <AddLinkModal renderAddLinkCustom={renderAddLinkCustom} />
182
+ <LinkCustomWrapper key={LINK} renderLinkCustom={renderLinkCustom} />
149
183
  ),
150
184
  ToolbarButton: () => (
151
185
  <ToolbarButton showToolbarButton={showToolbarButton} />