hero-editor 1.9.0 → 1.9.1

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.0",
3
+ "version": "1.9.1",
4
4
  "description": "",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {
@@ -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 = {
@@ -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/lib.js CHANGED
@@ -147,6 +147,7 @@ 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';
@@ -0,0 +1,11 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`link plugin renders a link element 1`] = `
4
+ <body>
5
+ <div>
6
+ <a
7
+ href="Link example"
8
+ />
9
+ </div>
10
+ </body>
11
+ `;
@@ -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
+ });
@@ -1,17 +1,35 @@
1
1
  import React 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,
9
12
  } from '../helpers';
10
13
  import { LINK } from '../constants';
11
14
 
12
15
  const getUrl = (element) =>
13
16
  element.children.reduce((url, node) => url + node.text, '');
14
17
 
18
+ const getUrlFromNodes = (editor) => {
19
+ const [...linkNodes] = Editor.nodes(editor, { match: isLink });
20
+
21
+ if (linkNodes.length === 1) {
22
+ const [nodes] = linkNodes[0];
23
+ return nodes?.data.url;
24
+ }
25
+ return '';
26
+ };
27
+
28
+ const getSelectedData = (editor) => ({
29
+ text: Editor.string(editor, editor.selection),
30
+ url: getUrlFromNodes(editor),
31
+ });
32
+
15
33
  const LinkElement = ({ attributes, children, element }) => (
16
34
  <a {...attributes} href={getUrl(element)}>
17
35
  {children}
@@ -20,13 +38,36 @@ const LinkElement = ({ attributes, children, element }) => (
20
38
 
21
39
  const renderElement = makeRenderElement(LINK, LinkElement);
22
40
 
23
- const handleMessage = addMessageListener(LINK, ({ editor, data: { url } }) => {
24
- Transforms.insertNodes(editor, {
25
- type: LINK,
26
- data: { url },
27
- children: [{ text: url }],
28
- });
29
- });
41
+ const handleMessage = addMessageListener(
42
+ LINK,
43
+ ({ editor, data: { text, url } }) => {
44
+ if (!text || !url || !isUrl(url)) return;
45
+
46
+ if (isBlockActive(editor, LINK)) {
47
+ Transforms.unwrapNodes(editor, {
48
+ match: isLink,
49
+ });
50
+ }
51
+
52
+ const { selection } = editor;
53
+ const isCollapsed = selection && Range.isCollapsed(selection);
54
+
55
+ const linkNode = {
56
+ type: LINK,
57
+ data: { url },
58
+ children: isCollapsed ? [{ text: text ?? url }] : [],
59
+ };
60
+
61
+ if (isCollapsed) {
62
+ Transforms.insertNodes(editor, linkNode);
63
+ } else {
64
+ Transforms.wrapNodes(editor, linkNode, { split: true });
65
+ Transforms.collapse(editor, { edge: 'end' });
66
+ }
67
+
68
+ Transforms.move(editor, { unit: 'offset' });
69
+ },
70
+ );
30
71
 
31
72
  const enhanceEditor = (editor) => {
32
73
  const { isInline, insertData } = editor;
@@ -48,10 +89,30 @@ const enhanceEditor = (editor) => {
48
89
  return editor;
49
90
  };
50
91
 
51
- export default () =>
92
+ const ToolbarButton = ({ CustomButton }) => {
93
+ const editor = useSlate();
94
+
95
+ const handleAddLink = (text, url) => {
96
+ postMessage(LINK, { text, url }, editor);
97
+ };
98
+
99
+ if (!CustomButton) {
100
+ return null;
101
+ }
102
+
103
+ return (
104
+ <CustomButton
105
+ handleAddLink={handleAddLink}
106
+ getSelectedData={() => getSelectedData(editor)}
107
+ />
108
+ );
109
+ };
110
+
111
+ export default ({ CustomButton } = {}) =>
52
112
  createPlugin({
53
113
  name: LINK,
54
114
  renderElement,
55
115
  handleMessage,
116
+ ToolbarButton: () => <ToolbarButton CustomButton={CustomButton} />,
56
117
  enhanceEditor,
57
118
  });