@squiz/formatted-text-editor 1.21.1-alpha.2 → 1.21.1-alpha.21

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.
Files changed (142) hide show
  1. package/demo/App.tsx +45 -10
  2. package/demo/index.scss +11 -10
  3. package/lib/Editor/Editor.js +45 -7
  4. package/lib/Editor/EditorContext.d.ts +10 -0
  5. package/lib/Editor/EditorContext.js +15 -0
  6. package/lib/EditorToolbar/FloatingToolbar.js +11 -5
  7. package/lib/EditorToolbar/Toolbar.js +3 -1
  8. package/lib/EditorToolbar/Tools/Bold/BoldButton.js +2 -2
  9. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +17 -0
  10. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +82 -0
  11. package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +5 -0
  12. package/lib/EditorToolbar/Tools/Image/ImageButton.js +77 -0
  13. package/lib/EditorToolbar/Tools/Image/ImageModal.d.ts +8 -0
  14. package/lib/EditorToolbar/Tools/Image/ImageModal.js +16 -0
  15. package/lib/EditorToolbar/Tools/Italic/ItalicButton.js +2 -2
  16. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +14 -5
  17. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +67 -15
  18. package/lib/EditorToolbar/Tools/Link/LinkButton.js +22 -14
  19. package/lib/EditorToolbar/Tools/Link/LinkModal.js +12 -5
  20. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +2 -9
  21. package/lib/EditorToolbar/Tools/Redo/RedoButton.js +2 -2
  22. package/lib/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.js +2 -2
  23. package/lib/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.js +2 -2
  24. package/lib/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.js +2 -2
  25. package/lib/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.js +2 -2
  26. package/lib/EditorToolbar/Tools/Underline/UnderlineButton.js +2 -2
  27. package/lib/EditorToolbar/Tools/Undo/UndoButton.js +2 -2
  28. package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
  29. package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
  30. package/lib/Extensions/Extensions.d.ts +7 -4
  31. package/lib/Extensions/Extensions.js +32 -19
  32. package/lib/Extensions/ImageExtension/ImageExtension.d.ts +10 -0
  33. package/lib/Extensions/ImageExtension/ImageExtension.js +92 -0
  34. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
  35. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
  36. package/lib/Extensions/LinkExtension/LinkExtension.d.ts +21 -12
  37. package/lib/Extensions/LinkExtension/LinkExtension.js +63 -65
  38. package/lib/Extensions/LinkExtension/common.d.ts +7 -0
  39. package/lib/Extensions/LinkExtension/common.js +14 -0
  40. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
  41. package/lib/hooks/index.d.ts +1 -0
  42. package/lib/hooks/index.js +1 -0
  43. package/lib/hooks/useExpandedSelection.d.ts +23 -0
  44. package/lib/hooks/useExpandedSelection.js +37 -0
  45. package/lib/index.css +159 -74
  46. package/lib/index.d.ts +5 -2
  47. package/lib/index.js +9 -3
  48. package/lib/types.d.ts +3 -0
  49. package/lib/types.js +2 -0
  50. package/lib/ui/Button/Button.d.ts +11 -0
  51. package/lib/ui/{ToolbarButton/ToolbarButton.js → Button/Button.js} +6 -3
  52. package/lib/ui/Fields/Input/Input.d.ts +5 -0
  53. package/lib/ui/{Inputs/Text/TextInput.js → Fields/Input/Input.js} +10 -5
  54. package/lib/ui/Modal/Modal.js +2 -1
  55. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +9 -0
  56. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +165 -0
  57. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
  58. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +129 -0
  59. package/lib/utils/undefinedIfEmpty.d.ts +1 -0
  60. package/lib/utils/undefinedIfEmpty.js +7 -0
  61. package/package.json +9 -4
  62. package/src/Editor/Editor.spec.tsx +78 -18
  63. package/src/Editor/Editor.tsx +28 -9
  64. package/src/Editor/EditorContext.spec.tsx +26 -0
  65. package/src/Editor/EditorContext.ts +19 -0
  66. package/src/Editor/_editor.scss +20 -51
  67. package/src/EditorToolbar/FloatingToolbar.spec.tsx +2 -3
  68. package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
  69. package/src/EditorToolbar/Toolbar.tsx +2 -0
  70. package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +1 -1
  71. package/src/EditorToolbar/Tools/Bold/BoldButton.tsx +2 -2
  72. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +77 -0
  73. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +90 -0
  74. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +135 -0
  75. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +72 -0
  76. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +83 -0
  77. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +24 -0
  78. package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +1 -1
  79. package/src/EditorToolbar/Tools/Italic/ItalicButton.tsx +2 -2
  80. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
  81. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +97 -27
  82. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +104 -26
  83. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +30 -21
  84. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
  85. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +26 -26
  86. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +4 -12
  87. package/src/EditorToolbar/Tools/Redo/RedoButton.tsx +2 -2
  88. package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.tsx +2 -2
  89. package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.tsx +2 -2
  90. package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.tsx +2 -2
  91. package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.tsx +2 -2
  92. package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +1 -1
  93. package/src/EditorToolbar/Tools/Underline/UnderlineButton.tsx +2 -2
  94. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
  95. package/src/EditorToolbar/Tools/Undo/UndoButton.tsx +2 -2
  96. package/src/EditorToolbar/_floating-toolbar.scss +5 -0
  97. package/src/EditorToolbar/_toolbar.scss +11 -5
  98. package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
  99. package/src/Extensions/Extensions.ts +32 -17
  100. package/src/Extensions/ImageExtension/ImageExtension.ts +112 -0
  101. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
  102. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
  103. package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
  104. package/src/Extensions/LinkExtension/LinkExtension.ts +88 -82
  105. package/src/Extensions/LinkExtension/common.ts +10 -0
  106. package/src/hooks/index.ts +1 -0
  107. package/src/hooks/useExpandedSelection.ts +44 -0
  108. package/src/index.scss +2 -2
  109. package/src/index.ts +5 -2
  110. package/src/types.ts +5 -0
  111. package/src/ui/Button/Button.spec.tsx +44 -0
  112. package/src/ui/Button/Button.tsx +29 -0
  113. package/src/ui/{_buttons.scss → Button/_button.scss} +19 -1
  114. package/src/ui/{Inputs/Text/TextInput.spec.tsx → Fields/Input/Input.spec.tsx} +8 -8
  115. package/src/ui/Fields/Input/Input.tsx +34 -0
  116. package/src/ui/Modal/Modal.tsx +2 -1
  117. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
  118. package/src/ui/_forms.scss +14 -0
  119. package/src/ui/_typography.scss +46 -0
  120. package/src/utils/converters/mocks/squizNodeJson.mock.ts +252 -0
  121. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +480 -0
  122. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +202 -0
  123. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +329 -0
  124. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +151 -0
  125. package/src/utils/undefinedIfEmpty.spec.ts +12 -0
  126. package/src/utils/undefinedIfEmpty.ts +3 -0
  127. package/tailwind.config.cjs +3 -0
  128. package/tests/renderWithEditor.tsx +28 -15
  129. package/lib/FormattedTextEditor.d.ts +0 -2
  130. package/lib/FormattedTextEditor.js +0 -7
  131. package/lib/ui/Inputs/Text/TextInput.d.ts +0 -4
  132. package/lib/ui/ToolbarButton/ToolbarButton.d.ts +0 -10
  133. package/src/Editor/Editor.mock.tsx +0 -43
  134. package/src/FormattedTextEditor.spec.tsx +0 -10
  135. package/src/FormattedTextEditor.tsx +0 -3
  136. package/src/ui/Inputs/Text/TextInput.tsx +0 -20
  137. package/src/ui/ToolbarButton/ToolbarButton.tsx +0 -26
  138. package/src/ui/ToolbarButton/_toolbar-button.scss +0 -17
  139. /package/lib/ui/{Inputs → Fields}/Select/Select.d.ts +0 -0
  140. /package/lib/ui/{Inputs → Fields}/Select/Select.js +0 -0
  141. /package/src/ui/{Inputs → Fields}/Select/Select.spec.tsx +0 -0
  142. /package/src/ui/{Inputs → Fields}/Select/Select.tsx +0 -0
@@ -7,40 +7,40 @@ import RemoveLinkButton from './RemoveLinkButton';
7
7
  describe('RemoveLinkButton', () => {
8
8
  it('Removes a link', async () => {
9
9
  const { editor, getJsonContent } = await renderWithEditor(<RemoveLinkButton />, {
10
- content:
11
- '<a href="https://www.example.org/my-link">Sample link</a> with some other content and ' +
12
- '<a href="https://www.example.org/another-link">another link</a>.',
10
+ content: {
11
+ type: 'doc',
12
+ content: [
13
+ {
14
+ type: 'paragraph',
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: 'Sample link',
19
+ marks: [{ type: 'assetLink', attrs: { matrixAssetId: '123', target: '_blank' } }],
20
+ },
21
+ { type: 'text', text: ' with ' },
22
+ {
23
+ type: 'text',
24
+ text: 'another link',
25
+ marks: [{ type: 'link', attrs: { href: 'https://www.example.org/another-link', target: '_self' } }],
26
+ },
27
+ ],
28
+ },
29
+ ],
30
+ },
13
31
  });
14
32
 
15
- // move the cursor to inside of the link.
16
- await act(() => editor.selectText(5));
33
+ // select all of the text.
34
+ await act(() => editor.selectText('all'));
17
35
 
18
- // remove the link.
36
+ // remove the links.
19
37
  fireEvent.click(screen.getByRole('button', { name: 'Remove link' }));
20
38
 
21
- // make sure the link has been removed.
39
+ // make sure both types of link have been removed.
22
40
  expect(getJsonContent()).toEqual({
23
41
  type: 'paragraph',
24
42
  attrs: expect.any(Object),
25
- content: [
26
- { type: 'text', text: 'Sample link with some other content and ' },
27
- {
28
- type: 'text',
29
- text: 'another link',
30
- marks: [
31
- {
32
- type: 'link',
33
- attrs: {
34
- auto: false,
35
- href: 'https://www.example.org/another-link',
36
- target: null,
37
- title: null,
38
- },
39
- },
40
- ],
41
- },
42
- { type: 'text', text: '.' },
43
- ],
43
+ content: [{ type: 'text', text: 'Sample link with another link' }],
44
44
  });
45
45
  });
46
46
  });
@@ -1,23 +1,15 @@
1
1
  import React from 'react';
2
- import { useRemirrorContext, useChainedCommands } from '@remirror/react';
3
- import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
2
+ import { useChainedCommands } from '@remirror/react';
3
+ import Button from '../../../ui/Button/Button';
4
4
  import LinkOffIcon from '@mui/icons-material/LinkOff';
5
5
 
6
6
  const RemoveLinkButton = () => {
7
- const { commands } = useRemirrorContext({ autoUpdate: true });
8
7
  const chain = useChainedCommands();
9
- const enabled = commands.removeLink.enabled();
10
- const handleClick = () => {
11
- if (enabled) {
12
- chain.removeLink().focus().run();
13
- }
14
- };
15
8
 
16
9
  return (
17
- <ToolbarButton
18
- handleOnClick={handleClick}
10
+ <Button
11
+ handleOnClick={() => chain.removeLink().removeAssetLink().focus().run()}
19
12
  isActive={false}
20
- isDisabled={!enabled}
21
13
  icon={<LinkOffIcon />}
22
14
  label="Remove link"
23
15
  />
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useHelpers } from '@remirror/react';
3
3
  import { HistoryExtension } from 'remirror/extensions';
4
- import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
4
+ import Button from '../../../ui/Button/Button';
5
5
  import RedoRoundedIcon from '@mui/icons-material/RedoRounded';
6
6
 
7
7
  const RedoButton = () => {
@@ -17,7 +17,7 @@ const RedoButton = () => {
17
17
  const enabled = redoDepth() > 0;
18
18
 
19
19
  return (
20
- <ToolbarButton
20
+ <Button
21
21
  handleOnClick={handleSelect}
22
22
  isDisabled={!enabled}
23
23
  isActive={false}
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useChainedCommands } from '@remirror/react';
3
3
  import { NodeFormattingExtension } from '@remirror/extension-node-formatting';
4
- import ToolbarButton from '../../../../ui/ToolbarButton/ToolbarButton';
4
+ import Button from '../../../../ui/Button/Button';
5
5
  import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter';
6
6
 
7
7
  const CenterAlignButton = () => {
@@ -18,7 +18,7 @@ const CenterAlignButton = () => {
18
18
  const enabled = centerAlign.enabled();
19
19
 
20
20
  return (
21
- <ToolbarButton
21
+ <Button
22
22
  handleOnClick={handleSelect}
23
23
  isDisabled={!enabled}
24
24
  isActive={active}
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useChainedCommands } from '@remirror/react';
3
3
  import { NodeFormattingExtension } from '@remirror/extension-node-formatting';
4
- import ToolbarButton from '../../../../ui/ToolbarButton/ToolbarButton';
4
+ import Button from '../../../../ui/Button/Button';
5
5
  import FormatAlignJustifyIcon from '@mui/icons-material/FormatAlignJustify';
6
6
 
7
7
  const JustifyAlignButton = () => {
@@ -18,7 +18,7 @@ const JustifyAlignButton = () => {
18
18
  const enabled = justifyAlign.enabled();
19
19
 
20
20
  return (
21
- <ToolbarButton
21
+ <Button
22
22
  handleOnClick={handleSelect}
23
23
  isDisabled={!enabled}
24
24
  isActive={active}
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useChainedCommands } from '@remirror/react';
3
3
  import { NodeFormattingExtension } from '@remirror/extension-node-formatting';
4
- import ToolbarButton from '../../../../ui/ToolbarButton/ToolbarButton';
4
+ import Button from '../../../../ui/Button/Button';
5
5
  import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
6
6
 
7
7
  const LeftAlignButton = () => {
@@ -18,7 +18,7 @@ const LeftAlignButton = () => {
18
18
  const enabled = leftAlign.enabled();
19
19
 
20
20
  return (
21
- <ToolbarButton
21
+ <Button
22
22
  handleOnClick={handleSelect}
23
23
  isDisabled={!enabled}
24
24
  isActive={active}
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useChainedCommands } from '@remirror/react';
3
3
  import { NodeFormattingExtension } from '@remirror/extension-node-formatting';
4
- import ToolbarButton from '../../../../ui/ToolbarButton/ToolbarButton';
4
+ import Button from '../../../../ui/Button/Button';
5
5
  import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';
6
6
 
7
7
  const RightAlignButton = () => {
@@ -18,7 +18,7 @@ const RightAlignButton = () => {
18
18
  const enabled = rightAlign.enabled();
19
19
 
20
20
  return (
21
- <ToolbarButton
21
+ <Button
22
22
  handleOnClick={handleSelect}
23
23
  isDisabled={!enabled}
24
24
  isActive={active}
@@ -14,6 +14,6 @@ describe('Underline button', () => {
14
14
  expect(screen.getByRole('button', { name: 'Underline (cmd+U)' }).classList.contains('squiz-fte-btn')).toBeTruthy();
15
15
  const underline = screen.getByRole('button', { name: 'Underline (cmd+U)' });
16
16
  fireEvent.click(underline);
17
- expect(underline.classList.contains('is-active')).toBeTruthy();
17
+ expect(underline.classList.contains('squiz-fte-btn--is-active')).toBeTruthy();
18
18
  });
19
19
  });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
3
  import { UnderlineExtension } from '@remirror/extension-underline';
4
- import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
4
+ import Button from '../../../ui/Button/Button';
5
5
  import FormatUnderlinedRoundedIcon from '@mui/icons-material/FormatUnderlinedRounded';
6
6
 
7
7
  const UnderlineButton = () => {
@@ -17,7 +17,7 @@ const UnderlineButton = () => {
17
17
  };
18
18
 
19
19
  return (
20
- <ToolbarButton
20
+ <Button
21
21
  handleOnClick={handleSelect}
22
22
  isDisabled={!enabled}
23
23
  isActive={active.underline()}
@@ -1,7 +1,9 @@
1
1
  import '@testing-library/jest-dom';
2
- import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { render, screen, fireEvent, act } from '@testing-library/react';
3
3
  import Editor from '../../../Editor/Editor';
4
4
  import React from 'react';
5
+ import { renderWithEditor } from '../../../../tests';
6
+ import UndoButton from './UndoButton';
5
7
 
6
8
  describe('Undo button', () => {
7
9
  it('Renders the undo button', () => {
@@ -46,4 +48,23 @@ describe('Undo button', () => {
46
48
  fireEvent.click(undo);
47
49
  expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeFalsy();
48
50
  });
51
+
52
+ it('Reverts text content changes', async () => {
53
+ const { editor, getJsonContent } = await renderWithEditor(<UndoButton />, { content: 'Initial content...' });
54
+
55
+ await act(() => editor.jumpTo('end'));
56
+ await act(() => editor.paste(' with some updated content.'));
57
+ expect(getJsonContent()).toEqual({
58
+ type: 'paragraph',
59
+ attrs: expect.any(Object),
60
+ content: [{ type: 'text', text: 'Initial content... with some updated content.' }],
61
+ });
62
+
63
+ fireEvent.click(screen.getByRole('button', { name: 'Undo (cmd+Z)' }));
64
+ expect(getJsonContent()).toEqual({
65
+ type: 'paragraph',
66
+ attrs: expect.any(Object),
67
+ content: [{ type: 'text', text: 'Initial content...' }],
68
+ });
69
+ });
49
70
  });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useHelpers } from '@remirror/react';
3
3
  import { HistoryExtension } from 'remirror/extensions';
4
- import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
4
+ import Button from '../../../ui/Button/Button';
5
5
  import UndoRoundedIcon from '@mui/icons-material/UndoRounded';
6
6
 
7
7
  const UndoButton = () => {
@@ -17,7 +17,7 @@ const UndoButton = () => {
17
17
  const enabled = undoDepth() > 0;
18
18
 
19
19
  return (
20
- <ToolbarButton
20
+ <Button
21
21
  handleOnClick={handleSelect}
22
22
  isDisabled={!enabled}
23
23
  isActive={false}
@@ -1,4 +1,9 @@
1
1
  /// This class is excluded from the scope of squiz-fte-scope as it is outside of the scoped element
2
2
  .squiz-fte-scope__floating-popover {
3
+ @extend .editor-toolbar;
3
4
  @apply bg-white border-gray-200 p-1 shadow rounded-md border flex;
5
+
6
+ .editor-divider {
7
+ @apply my-0;
8
+ }
4
9
  }
@@ -7,10 +7,16 @@
7
7
  > *:not(:first-child, .editor-divider) {
8
8
  margin: 0 0 0 2px;
9
9
  }
10
- }
11
10
 
12
- .editor-divider {
13
- @apply -my-1 mx-1 border;
14
- margin-right: 2px;
15
- height: auto;
11
+ .editor-divider {
12
+ @apply -my-1 mx-1 border;
13
+ margin-right: 2px;
14
+ height: auto;
15
+ }
16
+ .squiz-fte-btn {
17
+ @apply p-1;
18
+ ~ .squiz-fte-btn {
19
+ margin-left: 2px;
20
+ }
21
+ }
16
22
  }
@@ -0,0 +1,54 @@
1
+ import { PlainExtension } from '@remirror/core';
2
+ import { command, CommandFunction, FromToProps, getTextSelection, MarkType } from 'remirror';
3
+ import { Attrs } from 'prosemirror-model';
4
+
5
+ export class CommandsExtension extends PlainExtension {
6
+ get name(): string {
7
+ return 'squizCommands' as const;
8
+ }
9
+
10
+ /**
11
+ * Updates the attributes of a specific mark for the current selection.
12
+ * Optionally, if text is provided it will replace the current selection.
13
+ * The cursor will be place at the end of the selection after changes.
14
+ *
15
+ * @param {object} options
16
+ */
17
+ @command()
18
+ updateMark(options: {
19
+ mark: MarkType;
20
+ attrs?: Attrs;
21
+ text?: string;
22
+ removeMark?: boolean;
23
+ range: FromToProps;
24
+ }): CommandFunction {
25
+ return (props) => {
26
+ const { tr, dispatch, view } = props;
27
+ const { mark, attrs, text, removeMark, range } = options;
28
+
29
+ const selectedText = tr.doc.textBetween(range.from, range.to);
30
+
31
+ if (text !== undefined && text !== selectedText) {
32
+ // update the text in the editor if it was updated, update the range to cover the length of the new text.
33
+ tr.insertText(text, range.from, range.to);
34
+ range.to = range.from + text.length;
35
+ }
36
+
37
+ // apply the link, or remove it if no URL was provided.
38
+ if (removeMark) {
39
+ tr.removeMark(range.from, range.to, mark);
40
+ } else {
41
+ tr.addMark(range.from, range.to, mark.create(attrs));
42
+ }
43
+
44
+ // move the cursor to the end of the link and re-focus the editor.
45
+ tr.setSelection(getTextSelection({ from: range.to, to: range.to }, tr.doc));
46
+
47
+ // apply the transaction.
48
+ dispatch?.(tr);
49
+ view?.focus();
50
+
51
+ return true;
52
+ };
53
+ }
54
+ }
@@ -7,23 +7,38 @@ import {
7
7
  UnderlineExtension,
8
8
  HistoryExtension,
9
9
  } from 'remirror/extensions';
10
+ import { Extension } from '@remirror/core';
10
11
  import { PreformattedExtension } from './PreformattedExtension/PreformattedExtension';
12
+ import { AssetLinkExtension } from './LinkExtension/AssetLinkExtension';
11
13
  import { LinkExtension } from './LinkExtension/LinkExtension';
14
+ import { ImageExtension } from './ImageExtension/ImageExtension';
15
+ import { CommandsExtension } from './CommandsExtension/CommandsExtension';
16
+ import { EditorContextOptions } from '../Editor/EditorContext';
12
17
 
13
- export const Extensions = () => [
14
- new BoldExtension(),
15
- new HeadingExtension(),
16
- new ItalicExtension(),
17
- new NodeFormattingExtension(),
18
- new ParagraphExtension(),
19
- new PreformattedExtension(),
20
- new UnderlineExtension(),
21
- new HistoryExtension(),
22
- new LinkExtension({
23
- supportedTargets: [
24
- // '_self' is the browser default and will be used when encountering a link with a
25
- // different target is encountered.
26
- '_blank',
27
- ],
28
- }),
29
- ];
18
+ export enum MarkName {
19
+ Link = 'link',
20
+ AssetLink = 'assetLink',
21
+ }
22
+
23
+ export const createExtensions = (context: EditorContextOptions) => {
24
+ return (): Extension[] => {
25
+ return [
26
+ new CommandsExtension(),
27
+ new BoldExtension(),
28
+ new HeadingExtension(),
29
+ new ItalicExtension(),
30
+ new NodeFormattingExtension({ indents: [] }),
31
+ new ParagraphExtension(),
32
+ new PreformattedExtension(),
33
+ new UnderlineExtension(),
34
+ new HistoryExtension(),
35
+ new ImageExtension(),
36
+ new ImageExtension({ preferPastedTextContent: false }),
37
+ new LinkExtension(),
38
+ new AssetLinkExtension({
39
+ matrixIdentifier: context.matrix.matrixIdentifier,
40
+ matrixDomain: context.matrix.matrixDomain,
41
+ }),
42
+ ];
43
+ };
44
+ };
@@ -0,0 +1,112 @@
1
+ import { ImageExtension as RemirrorImageExtension } from 'remirror/extensions';
2
+ import { PasteRule } from 'prosemirror-paste-rules';
3
+ import {
4
+ isElementDomNode,
5
+ omitExtraAttributes,
6
+ ApplySchemaAttributes,
7
+ NodeSpecOverride,
8
+ NodeExtensionSpec,
9
+ getTextSelection,
10
+ PrimitiveSelection,
11
+ } from '@remirror/core';
12
+ import { ImageAttributes } from '@remirror/extension-image/dist-types/image-extension';
13
+ import { CommandFunction } from '@remirror/pm';
14
+
15
+ /**
16
+ * Get the width and the height of the image.
17
+ */
18
+ function getDimensions(element: HTMLElement) {
19
+ let { width, height } = element.style;
20
+ width = width || element.getAttribute('width') || '';
21
+ height = height || element.getAttribute('height') || '';
22
+
23
+ return { width, height };
24
+ }
25
+
26
+ /**
27
+ * Retrieve attributes from the dom for the image extension.
28
+ */
29
+ function getImageAttributes({ element, parse }: { element: HTMLElement; parse: ApplySchemaAttributes['parse'] }) {
30
+ const { width, height } = getDimensions(element);
31
+
32
+ return {
33
+ ...parse(element),
34
+ alt: element.getAttribute('alt') ?? '',
35
+ height: Number.parseInt(height || '0', 10) || null,
36
+ src: element.getAttribute('src') ?? null,
37
+ title: element.getAttribute('title') ?? '',
38
+ width: Number.parseInt(width || '0', 10) || null,
39
+ fileName: element.getAttribute('data-file-name') ?? null,
40
+ };
41
+ }
42
+
43
+ export class ImageExtension extends RemirrorImageExtension {
44
+ createPasteRules(): PasteRule[] {
45
+ return [
46
+ {
47
+ type: 'file',
48
+ regexp: /image/i,
49
+ fileHandler: (): boolean => {
50
+ return false;
51
+ },
52
+ },
53
+ ];
54
+ }
55
+
56
+ createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
57
+ const { preferPastedTextContent } = this.options;
58
+ return {
59
+ inline: true,
60
+ draggable: true,
61
+ selectable: true,
62
+ ...override,
63
+ attrs: {
64
+ ...extra.defaults(),
65
+ alt: { default: '' },
66
+ crop: { default: null },
67
+ height: { default: null },
68
+ width: { default: null },
69
+ rotate: { default: null },
70
+ src: { default: null },
71
+ title: { default: '' },
72
+ fileName: { default: null },
73
+
74
+ resizable: { default: false },
75
+ },
76
+ parseDOM: [
77
+ {
78
+ tag: 'img[src]',
79
+ getAttrs: (element) => {
80
+ if (isElementDomNode(element)) {
81
+ const attrs = getImageAttributes({ element, parse: extra.parse });
82
+
83
+ if (preferPastedTextContent && attrs.src?.startsWith('file:///')) {
84
+ return false;
85
+ }
86
+
87
+ return attrs;
88
+ }
89
+
90
+ return {};
91
+ },
92
+ },
93
+ ...(override.parseDOM ?? []),
94
+ ],
95
+ toDOM: (node) => {
96
+ const attrs = omitExtraAttributes(node.attrs, extra);
97
+ return ['img', { ...extra.dom(node), ...attrs }];
98
+ },
99
+ };
100
+ }
101
+
102
+ insertImage(attributes: ImageAttributes, selection?: PrimitiveSelection): CommandFunction {
103
+ return ({ tr, dispatch }) => {
104
+ const { from, to } = getTextSelection(selection ?? tr.selection, tr.doc);
105
+ const node = this.type.create(attributes);
106
+
107
+ dispatch?.(tr.replaceRangeWith(from, to, node));
108
+
109
+ return true;
110
+ };
111
+ }
112
+ }
@@ -0,0 +1,104 @@
1
+ import { renderWithEditor } from '../../../tests';
2
+
3
+ describe('AssetLinkExtension', () => {
4
+ it('Parses HTML content representing an asset link', async () => {
5
+ const { getJsonContent } = await renderWithEditor(null, {
6
+ content: `<a href="https://my-matrix.squiz.net/?a=this-is-actually-ignored"
7
+ target="_self"
8
+ data-matrix-asset-id="123"
9
+ data-matrix-identifier="matrix-api-identifier"
10
+ data-matrix-domain="https://matrix-domain.squiz.net">
11
+ Hello!
12
+ </a>`,
13
+ });
14
+
15
+ expect(getJsonContent()).toEqual({
16
+ type: 'paragraph',
17
+ attrs: expect.any(Object),
18
+ content: [
19
+ {
20
+ type: 'text',
21
+ text: 'Hello!',
22
+ marks: [
23
+ {
24
+ type: 'assetLink',
25
+ attrs: {
26
+ matrixAssetId: '123',
27
+ matrixDomain: 'https://matrix-domain.squiz.net',
28
+ matrixIdentifier: 'matrix-api-identifier',
29
+ target: '_self',
30
+ },
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ });
36
+ });
37
+
38
+ it('Resolves to a regular link if HTML content is missing some of the expected attributes', async () => {
39
+ const { getJsonContent } = await renderWithEditor(null, {
40
+ content: `<a href="https://my-matrix.squiz.net/?a=123"
41
+ target="_self"
42
+ data-matrix-asset-id="123">
43
+ Hello!
44
+ </a>`,
45
+ });
46
+
47
+ expect(getJsonContent()).toEqual({
48
+ type: 'paragraph',
49
+ attrs: expect.any(Object),
50
+ content: [
51
+ {
52
+ type: 'text',
53
+ text: 'Hello!',
54
+ marks: [
55
+ {
56
+ type: 'link',
57
+ attrs: {
58
+ href: 'https://my-matrix.squiz.net/?a=123',
59
+ target: '_self',
60
+ title: null,
61
+ },
62
+ },
63
+ ],
64
+ },
65
+ ],
66
+ });
67
+ });
68
+
69
+ it('Outputs expected HTML', async () => {
70
+ const { getHtmlContent } = await renderWithEditor(null, {
71
+ content: {
72
+ type: 'paragraph',
73
+ content: [
74
+ {
75
+ type: 'text',
76
+ text: 'Hello!',
77
+ marks: [
78
+ {
79
+ type: 'assetLink',
80
+ attrs: {
81
+ matrixAssetId: '123',
82
+ matrixDomain: 'https://matrix-domain.squiz.net',
83
+ matrixIdentifier: 'matrix-api-identifier',
84
+ target: '_blank',
85
+ },
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ },
91
+ });
92
+
93
+ expect(getHtmlContent()).toEqual(
94
+ '<a rel="noopener noreferrer nofollow" ' +
95
+ 'href="/?a=123" ' +
96
+ 'target="_blank" ' +
97
+ 'data-matrix-asset-id="123" ' +
98
+ 'data-matrix-identifier="matrix-api-identifier" ' +
99
+ 'data-matrix-domain="https://matrix-domain.squiz.net">' +
100
+ 'Hello!' +
101
+ '</a>',
102
+ );
103
+ });
104
+ });