@squiz/formatted-text-editor 1.21.1-alpha.2 → 1.21.1-alpha.20
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/demo/App.tsx +45 -10
- package/demo/index.scss +11 -10
- package/lib/Editor/Editor.js +38 -6
- package/lib/Editor/EditorContext.d.ts +10 -0
- package/lib/Editor/EditorContext.js +15 -0
- package/lib/EditorToolbar/FloatingToolbar.js +11 -5
- package/lib/EditorToolbar/Toolbar.js +3 -1
- package/lib/EditorToolbar/Tools/Bold/BoldButton.js +2 -2
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +17 -0
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +82 -0
- package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +5 -0
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +76 -0
- package/lib/EditorToolbar/Tools/Image/ImageModal.d.ts +8 -0
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +16 -0
- package/lib/EditorToolbar/Tools/Italic/ItalicButton.js +2 -2
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +14 -5
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +67 -15
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +13 -10
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +12 -5
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +2 -9
- package/lib/EditorToolbar/Tools/Redo/RedoButton.js +2 -2
- package/lib/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.js +2 -2
- package/lib/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.js +2 -2
- package/lib/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.js +2 -2
- package/lib/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.js +2 -2
- package/lib/EditorToolbar/Tools/Underline/UnderlineButton.js +2 -2
- package/lib/EditorToolbar/Tools/Undo/UndoButton.js +2 -2
- package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
- package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
- package/lib/Extensions/Extensions.d.ts +7 -4
- package/lib/Extensions/Extensions.js +32 -19
- package/lib/Extensions/ImageExtension/ImageExtension.d.ts +10 -0
- package/lib/Extensions/ImageExtension/ImageExtension.js +92 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
- package/lib/Extensions/LinkExtension/LinkExtension.d.ts +21 -12
- package/lib/Extensions/LinkExtension/LinkExtension.js +63 -65
- package/lib/Extensions/LinkExtension/common.d.ts +7 -0
- package/lib/Extensions/LinkExtension/common.js +14 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useExpandedSelection.d.ts +23 -0
- package/lib/hooks/useExpandedSelection.js +37 -0
- package/lib/index.css +157 -75
- package/lib/index.d.ts +5 -2
- package/lib/index.js +9 -3
- package/lib/types.d.ts +3 -0
- package/lib/types.js +2 -0
- package/lib/ui/Button/Button.d.ts +11 -0
- package/lib/ui/{ToolbarButton/ToolbarButton.js → Button/Button.js} +6 -3
- package/lib/ui/Fields/Input/Input.d.ts +5 -0
- package/lib/ui/{Inputs/Text/TextInput.js → Fields/Input/Input.js} +10 -5
- package/lib/ui/Modal/Modal.js +2 -1
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +9 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +165 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +129 -0
- package/lib/utils/undefinedIfEmpty.d.ts +1 -0
- package/lib/utils/undefinedIfEmpty.js +7 -0
- package/package.json +9 -4
- package/src/Editor/Editor.spec.tsx +78 -18
- package/src/Editor/Editor.tsx +20 -7
- package/src/Editor/EditorContext.spec.tsx +26 -0
- package/src/Editor/EditorContext.ts +19 -0
- package/src/Editor/_editor.scss +16 -53
- package/src/EditorToolbar/FloatingToolbar.spec.tsx +2 -3
- package/src/EditorToolbar/FloatingToolbar.tsx +18 -6
- package/src/EditorToolbar/Toolbar.tsx +2 -0
- package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/Bold/BoldButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +77 -0
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +90 -0
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +135 -0
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +71 -0
- package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +83 -0
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +24 -0
- package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/Italic/ItalicButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +97 -27
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +104 -26
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +19 -13
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +26 -26
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +4 -12
- package/src/EditorToolbar/Tools/Redo/RedoButton.tsx +2 -2
- package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.tsx +2 -2
- package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.tsx +2 -2
- package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.tsx +2 -2
- package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +1 -1
- package/src/EditorToolbar/Tools/Underline/UnderlineButton.tsx +2 -2
- package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
- package/src/EditorToolbar/Tools/Undo/UndoButton.tsx +2 -2
- package/src/EditorToolbar/_floating-toolbar.scss +5 -0
- package/src/EditorToolbar/_toolbar.scss +11 -5
- package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
- package/src/Extensions/Extensions.ts +32 -17
- package/src/Extensions/ImageExtension/ImageExtension.ts +112 -0
- package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
- package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
- package/src/Extensions/LinkExtension/LinkExtension.ts +88 -82
- package/src/Extensions/LinkExtension/common.ts +10 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useExpandedSelection.ts +44 -0
- package/src/index.scss +2 -2
- package/src/index.ts +5 -2
- package/src/types.ts +5 -0
- package/src/ui/Button/Button.spec.tsx +44 -0
- package/src/ui/Button/Button.tsx +29 -0
- package/src/ui/{_buttons.scss → Button/_button.scss} +19 -1
- package/src/ui/{Inputs/Text/TextInput.spec.tsx → Fields/Input/Input.spec.tsx} +8 -8
- package/src/ui/Fields/Input/Input.tsx +34 -0
- package/src/ui/Modal/Modal.tsx +2 -1
- package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
- package/src/ui/_forms.scss +14 -0
- package/src/ui/_typography.scss +46 -0
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +252 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +480 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +202 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +329 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +151 -0
- package/src/utils/undefinedIfEmpty.spec.ts +12 -0
- package/src/utils/undefinedIfEmpty.ts +3 -0
- package/tailwind.config.cjs +3 -0
- package/tests/renderWithEditor.tsx +28 -15
- package/lib/FormattedTextEditor.d.ts +0 -2
- package/lib/FormattedTextEditor.js +0 -7
- package/lib/ui/Inputs/Text/TextInput.d.ts +0 -4
- package/lib/ui/ToolbarButton/ToolbarButton.d.ts +0 -10
- package/src/Editor/Editor.mock.tsx +0 -43
- package/src/FormattedTextEditor.spec.tsx +0 -10
- package/src/FormattedTextEditor.tsx +0 -3
- package/src/ui/Inputs/Text/TextInput.tsx +0 -20
- package/src/ui/ToolbarButton/ToolbarButton.tsx +0 -26
- package/src/ui/ToolbarButton/_toolbar-button.scss +0 -17
- /package/lib/ui/{Inputs → Fields}/Select/Select.d.ts +0 -0
- /package/lib/ui/{Inputs → Fields}/Select/Select.js +0 -0
- /package/src/ui/{Inputs → Fields}/Select/Select.spec.tsx +0 -0
- /package/src/ui/{Inputs → Fields}/Select/Select.tsx +0 -0
@@ -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
|
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
|
-
<
|
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
|
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
|
-
<
|
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
|
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
|
-
<
|
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
|
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
|
-
<
|
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
|
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
|
-
<
|
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
|
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
|
-
<
|
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
|
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
|
-
<
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
+
});
|
@@ -0,0 +1,128 @@
|
|
1
|
+
import {
|
2
|
+
ApplySchemaAttributes,
|
3
|
+
ExtensionPriority,
|
4
|
+
FromToProps,
|
5
|
+
isElementDomNode,
|
6
|
+
MarkExtensionSpec,
|
7
|
+
MarkSpecOverride,
|
8
|
+
} from 'remirror';
|
9
|
+
import { command, CommandFunction, extension, MarkExtension, omitExtraAttributes, removeMark } from '@remirror/core';
|
10
|
+
import { LinkTarget, validateTarget } from './common';
|
11
|
+
import { CommandsExtension } from '../CommandsExtension/CommandsExtension';
|
12
|
+
import { MarkName } from '../Extensions';
|
13
|
+
|
14
|
+
export type AssetLinkAttributes = {
|
15
|
+
matrixAssetId: string;
|
16
|
+
matrixIdentifier: string;
|
17
|
+
matrixDomain: string;
|
18
|
+
target: LinkTarget;
|
19
|
+
};
|
20
|
+
|
21
|
+
export type AssetLinkOptions = {
|
22
|
+
matrixIdentifier?: string;
|
23
|
+
matrixDomain?: string;
|
24
|
+
defaultTarget?: LinkTarget;
|
25
|
+
supportedTargets?: LinkTarget[];
|
26
|
+
};
|
27
|
+
|
28
|
+
export type UpdateAssetLinkProps = {
|
29
|
+
text: string;
|
30
|
+
attrs: Partial<AssetLinkAttributes>;
|
31
|
+
range: FromToProps;
|
32
|
+
};
|
33
|
+
|
34
|
+
@extension<AssetLinkOptions>({
|
35
|
+
defaultOptions: {
|
36
|
+
matrixIdentifier: '',
|
37
|
+
matrixDomain: '',
|
38
|
+
defaultTarget: LinkTarget.Self,
|
39
|
+
supportedTargets: [LinkTarget.Self, LinkTarget.Blank],
|
40
|
+
},
|
41
|
+
defaultPriority: ExtensionPriority.High,
|
42
|
+
})
|
43
|
+
export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
|
44
|
+
get name(): string {
|
45
|
+
return MarkName.AssetLink;
|
46
|
+
}
|
47
|
+
|
48
|
+
createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
|
49
|
+
return {
|
50
|
+
inclusive: false,
|
51
|
+
excludes: 'link',
|
52
|
+
...override,
|
53
|
+
attrs: {
|
54
|
+
...extra.defaults(),
|
55
|
+
matrixAssetId: {},
|
56
|
+
matrixIdentifier: { default: this.options.matrixIdentifier },
|
57
|
+
matrixDomain: { default: this.options.matrixDomain },
|
58
|
+
target: { default: this.options.defaultTarget },
|
59
|
+
},
|
60
|
+
parseDOM: [
|
61
|
+
{
|
62
|
+
tag: 'a[data-matrix-asset-id]',
|
63
|
+
getAttrs: (node) => {
|
64
|
+
if (!isElementDomNode(node)) {
|
65
|
+
return false;
|
66
|
+
}
|
67
|
+
|
68
|
+
const matrixAssetId = node.getAttribute('data-matrix-asset-id');
|
69
|
+
const matrixIdentifier = node.getAttribute('data-matrix-identifier');
|
70
|
+
const matrixDomain = node.getAttribute('data-matrix-domain');
|
71
|
+
|
72
|
+
if (!matrixAssetId || !matrixIdentifier || !matrixDomain) {
|
73
|
+
return false;
|
74
|
+
}
|
75
|
+
|
76
|
+
return {
|
77
|
+
...extra.parse(node),
|
78
|
+
matrixAssetId,
|
79
|
+
matrixIdentifier,
|
80
|
+
matrixDomain,
|
81
|
+
target: validateTarget(
|
82
|
+
node.getAttribute('target'),
|
83
|
+
this.options.supportedTargets,
|
84
|
+
this.options.defaultTarget,
|
85
|
+
),
|
86
|
+
};
|
87
|
+
},
|
88
|
+
},
|
89
|
+
],
|
90
|
+
toDOM: (node) => {
|
91
|
+
const { matrixAssetId, matrixIdentifier, matrixDomain, target, ...rest } = omitExtraAttributes(
|
92
|
+
node.attrs,
|
93
|
+
extra,
|
94
|
+
);
|
95
|
+
const rel = 'noopener noreferrer nofollow';
|
96
|
+
const attrs = {
|
97
|
+
...extra.dom(node),
|
98
|
+
...rest,
|
99
|
+
rel,
|
100
|
+
// TODO: this won't be acceptable if/when we get to rendering outside of Matrix.
|
101
|
+
href: `/?a=${matrixAssetId}`,
|
102
|
+
target: validateTarget(target, this.options.supportedTargets, this.options.defaultTarget),
|
103
|
+
'data-matrix-asset-id': matrixAssetId,
|
104
|
+
'data-matrix-identifier': matrixIdentifier,
|
105
|
+
'data-matrix-domain': matrixDomain,
|
106
|
+
};
|
107
|
+
|
108
|
+
return ['a', attrs, 0];
|
109
|
+
},
|
110
|
+
};
|
111
|
+
}
|
112
|
+
|
113
|
+
@command()
|
114
|
+
updateAssetLink({ text, attrs, range }: UpdateAssetLinkProps): CommandFunction {
|
115
|
+
return this.store.getExtension(CommandsExtension).updateMark({
|
116
|
+
attrs,
|
117
|
+
text,
|
118
|
+
range,
|
119
|
+
mark: this.type,
|
120
|
+
removeMark: !attrs.matrixAssetId,
|
121
|
+
});
|
122
|
+
}
|
123
|
+
|
124
|
+
@command()
|
125
|
+
removeAssetLink(): CommandFunction {
|
126
|
+
return removeMark({ type: this.type });
|
127
|
+
}
|
128
|
+
}
|