hero-editor 1.9.2 → 1.9.5

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.2",
3
+ "version": "1.9.5",
4
4
  "description": "",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {
package/src/constants.js CHANGED
@@ -30,6 +30,9 @@ const EMPTY_VALUE = [
30
30
  },
31
31
  ];
32
32
 
33
+ // Link actions
34
+ const ADD_LINK = 'add-link';
35
+
33
36
  export {
34
37
  EMPTY_VALUE,
35
38
  BULLETED_LIST,
@@ -47,4 +50,5 @@ export {
47
50
  EDITOR_FOCUS,
48
51
  EDITOR_BLUR,
49
52
  EDITOR_LAYOUT,
53
+ ADD_LINK,
50
54
  };
@@ -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(
@@ -0,0 +1,6 @@
1
+ import { LINK } from '../constants';
2
+
3
+ export default (node) => {
4
+ if (node?.type !== LINK) return null;
5
+ return node.data.url;
6
+ };
@@ -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
@@ -151,6 +151,6 @@ export { default as Icon } from './components/Icon';
151
151
  export { default as plainSerializer } from './serializers/plain';
152
152
  export { default as makeReactTransformer } from './transformers/react';
153
153
  export { defaultReactTransformer } from './transformers/react';
154
- export { isEmptyContent, getUrl, isUrl } from './helpers';
154
+ export { isEmptyContent, getUrl, isUrl, getUrlFromNode } from './helpers';
155
155
  export { EMPTY_VALUE } from './constants';
156
156
  export default HeroEditor;
@@ -4,7 +4,7 @@ exports[`link plugin renders a link element 1`] = `
4
4
  <body>
5
5
  <div>
6
6
  <a
7
- href="Link example"
7
+ href="https://www.example.com"
8
8
  />
9
9
  </div>
10
10
  </body>
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
2
  import { useSlate } from 'slate-react';
3
3
  import isUrl from 'is-url';
4
4
  import { Transforms, Editor, Range } from 'slate';
@@ -9,29 +9,41 @@ import {
9
9
  postMessage,
10
10
  isBlockActive,
11
11
  isLink,
12
+ getUrlFromNode,
12
13
  } from '../helpers';
13
- import { LINK } from '../constants';
14
+ import { LINK, ADD_LINK } from '../constants';
15
+ import Toolbar from '../components/Toolbar';
16
+ import Icon from '../components/Icon';
14
17
 
15
- const getUrl = (element) =>
16
- element.children.reduce((url, node) => url + node.text, '');
18
+ const getTextFromNode = (element) =>
19
+ element.children.reduce((text, node) => text + node.text, '');
17
20
 
18
- const getUrlFromNodes = (editor) => {
19
- const [...linkNodes] = Editor.nodes(editor, { match: isLink });
21
+ const getLinkNodeAtSelection = (editor) => {
22
+ const { selection } = editor;
20
23
 
21
- if (linkNodes.length === 1) {
22
- const [nodes] = linkNodes[0];
23
- return nodes?.data.url;
24
- }
25
- return '';
24
+ return selection
25
+ ? Editor.above(editor, {
26
+ at: selection,
27
+ match: isLink,
28
+ })
29
+ : undefined;
26
30
  };
27
31
 
28
- const getSelectedData = (editor) => ({
29
- text: Editor.string(editor, editor.selection),
30
- url: getUrlFromNodes(editor),
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]) : '';
43
+ };
32
44
 
33
45
  const LinkElement = ({ attributes, children, element }) => (
34
- <a {...attributes} href={getUrl(element)}>
46
+ <a {...attributes} href={getUrlFromNode(element)}>
35
47
  {children}
36
48
  </a>
37
49
  );
@@ -51,14 +63,16 @@ const handleMessage = addMessageListener(
51
63
 
52
64
  const { selection } = editor;
53
65
  const isCollapsed = selection && Range.isCollapsed(selection);
66
+ const isTextChanged = selection && Editor.string(editor, selection) != text;
67
+ const insertingText = isCollapsed || isTextChanged;
54
68
 
55
69
  const linkNode = {
56
70
  type: LINK,
57
71
  data: { url },
58
- children: isCollapsed ? [{ text: text ?? url }] : [],
72
+ children: insertingText ? [{ text: text ?? url }] : [],
59
73
  };
60
74
 
61
- if (isCollapsed) {
75
+ if (insertingText) {
62
76
  Transforms.insertNodes(editor, linkNode);
63
77
  } else {
64
78
  Transforms.wrapNodes(editor, linkNode, { split: true });
@@ -78,9 +92,16 @@ const enhanceEditor = (editor) => {
78
92
 
79
93
  editor.insertData = (data) => {
80
94
  const text = data.getData('text/plain');
81
-
95
+ const { selection } = editor;
82
96
  if (text && isUrl(text)) {
83
- postMessage(LINK, { url: text }, editor);
97
+ postMessage(
98
+ LINK,
99
+ {
100
+ text: Editor.string(editor, selection) || text,
101
+ url: text,
102
+ },
103
+ editor,
104
+ );
84
105
  } else {
85
106
  insertData(data);
86
107
  }
@@ -89,30 +110,73 @@ const enhanceEditor = (editor) => {
89
110
  return editor;
90
111
  };
91
112
 
92
- const ToolbarButton = ({ renderToolbarButton }) => {
113
+ const LinkCustomWrapper = ({ renderLinkCustom }) => {
114
+ const [showLinkCustom, setShowLinkCustom] = useState(false);
93
115
  const editor = useSlate();
94
116
 
95
- const handleAddLink = (text, url) => {
96
- postMessage(LINK, { text, url }, editor);
97
- };
117
+ const handleAddLink = useCallback((data) => {
118
+ postMessage(LINK, data, editor);
119
+ }, []);
120
+
121
+ const getSelectedData = (editor) => ({
122
+ text: getTextFromSelection(editor),
123
+ url: getUrlFromSelection(editor),
124
+ });
125
+
126
+ useEffect(() => {
127
+ const removeLinkCustomListener = addMessageListener(ADD_LINK, () => {
128
+ setShowLinkCustom(true);
129
+ })(editor);
130
+
131
+ return () => removeLinkCustomListener();
132
+ });
98
133
 
99
134
  return (
100
135
  <>
101
- {renderToolbarButton?.({
102
- handleAddLink,
103
- getSelectedData: () => getSelectedData(editor),
104
- })}
136
+ {showLinkCustom &&
137
+ renderLinkCustom?.({
138
+ handleAddLink,
139
+ getSelectedData: () => getSelectedData(editor),
140
+ hideLinkCustom: () => setShowLinkCustom(false),
141
+ })}
105
142
  </>
106
143
  );
107
144
  };
108
145
 
109
- export default ({ renderToolbarButton } = {}) =>
146
+ const ToolbarButton = ({ showToolbarButton }) => {
147
+ const editor = useSlate();
148
+
149
+ const handleMouseDown = useCallback(() => {
150
+ postMessage(ADD_LINK, {}, editor);
151
+ }, []);
152
+
153
+ if (!showToolbarButton) return null;
154
+
155
+ const isLinkNodeAtSelection = !!getLinkNodeAtSelection(editor);
156
+
157
+ return (
158
+ <>
159
+ <Toolbar.Button
160
+ active={isLinkNodeAtSelection}
161
+ onMouseDown={handleMouseDown}
162
+ >
163
+ <Icon>link</Icon>
164
+ </Toolbar.Button>
165
+ <Toolbar.Separator />
166
+ </>
167
+ );
168
+ };
169
+
170
+ export default ({ renderLinkCustom, showToolbarButton } = {}) =>
110
171
  createPlugin({
111
172
  name: LINK,
112
173
  renderElement,
113
174
  handleMessage,
175
+ renderCustom: () => (
176
+ <LinkCustomWrapper renderLinkCustom={renderLinkCustom} />
177
+ ),
114
178
  ToolbarButton: () => (
115
- <ToolbarButton renderToolbarButton={renderToolbarButton} />
179
+ <ToolbarButton showToolbarButton={showToolbarButton} />
116
180
  ),
117
181
  enhanceEditor,
118
182
  });