@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.
- package/demo/App.tsx +45 -10
- package/demo/index.scss +11 -10
- package/lib/Editor/Editor.js +45 -7
- 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 +77 -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 +22 -14
- 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 +159 -74
- 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 +28 -9
- package/src/Editor/EditorContext.spec.tsx +26 -0
- package/src/Editor/EditorContext.ts +19 -0
- package/src/Editor/_editor.scss +20 -51
- package/src/EditorToolbar/FloatingToolbar.spec.tsx +2 -3
- package/src/EditorToolbar/FloatingToolbar.tsx +15 -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 +72 -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 +30 -21
- 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
@@ -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
|
+
}
|
@@ -0,0 +1,68 @@
|
|
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://example.org/some-page"
|
7
|
+
title="Link title"
|
8
|
+
target="_blank"
|
9
|
+
rel="this is ignored">
|
10
|
+
Hello!
|
11
|
+
</a>`,
|
12
|
+
});
|
13
|
+
|
14
|
+
expect(getJsonContent()).toEqual({
|
15
|
+
type: 'paragraph',
|
16
|
+
attrs: expect.any(Object),
|
17
|
+
content: [
|
18
|
+
{
|
19
|
+
type: 'text',
|
20
|
+
text: 'Hello!',
|
21
|
+
marks: [
|
22
|
+
{
|
23
|
+
type: 'link',
|
24
|
+
attrs: {
|
25
|
+
href: 'https://example.org/some-page',
|
26
|
+
title: 'Link title',
|
27
|
+
target: '_blank',
|
28
|
+
},
|
29
|
+
},
|
30
|
+
],
|
31
|
+
},
|
32
|
+
],
|
33
|
+
});
|
34
|
+
});
|
35
|
+
|
36
|
+
it('Outputs expected HTML', async () => {
|
37
|
+
const { getHtmlContent } = await renderWithEditor(null, {
|
38
|
+
content: {
|
39
|
+
type: 'paragraph',
|
40
|
+
content: [
|
41
|
+
{
|
42
|
+
type: 'text',
|
43
|
+
text: 'Hello!',
|
44
|
+
marks: [
|
45
|
+
{
|
46
|
+
type: 'link',
|
47
|
+
attrs: {
|
48
|
+
href: 'https://example.org/some-page',
|
49
|
+
title: 'Link title',
|
50
|
+
target: '_blank',
|
51
|
+
},
|
52
|
+
},
|
53
|
+
],
|
54
|
+
},
|
55
|
+
],
|
56
|
+
},
|
57
|
+
});
|
58
|
+
|
59
|
+
expect(getHtmlContent()).toEqual(
|
60
|
+
'<a href="https://example.org/some-page" ' +
|
61
|
+
'title="Link title" ' +
|
62
|
+
'rel="noopener noreferrer nofollow" ' +
|
63
|
+
'target="_blank">' +
|
64
|
+
'Hello!' +
|
65
|
+
'</a>',
|
66
|
+
);
|
67
|
+
});
|
68
|
+
});
|
@@ -1,26 +1,68 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
import {
|
2
|
+
ApplySchemaAttributes,
|
3
|
+
ExtensionPriority,
|
4
|
+
FromToProps,
|
5
|
+
isElementDomNode,
|
6
|
+
MarkExtensionSpec,
|
7
|
+
MarkSpecOverride,
|
8
|
+
} from 'remirror';
|
9
|
+
import {
|
10
|
+
command,
|
11
|
+
CommandFunction,
|
12
|
+
extension,
|
13
|
+
Handler,
|
14
|
+
keyBinding,
|
15
|
+
KeyBindingProps,
|
16
|
+
MarkExtension,
|
17
|
+
NamedShortcut,
|
18
|
+
omitExtraAttributes,
|
19
|
+
removeMark,
|
20
|
+
} from '@remirror/core';
|
21
|
+
import { LinkTarget, validateTarget } from './common';
|
22
|
+
import { CommandsExtension } from '../CommandsExtension/CommandsExtension';
|
23
|
+
import { MarkName } from '../Extensions';
|
24
|
+
|
25
|
+
export type LinkAttributes = {
|
26
|
+
href: string;
|
27
|
+
title?: string;
|
28
|
+
target: LinkTarget;
|
29
|
+
};
|
5
30
|
|
6
|
-
export type
|
7
|
-
|
31
|
+
export type LinkOptions = {
|
32
|
+
defaultTarget?: LinkTarget;
|
33
|
+
supportedTargets?: LinkTarget[];
|
34
|
+
onShortcut?: Handler<() => void>;
|
8
35
|
};
|
9
36
|
|
10
|
-
export type
|
11
|
-
|
37
|
+
export type UpdateLinkProps = {
|
38
|
+
text: string;
|
39
|
+
attrs: Partial<LinkAttributes>;
|
40
|
+
range: FromToProps;
|
12
41
|
};
|
13
42
|
|
14
|
-
|
15
|
-
|
16
|
-
|
43
|
+
@extension<LinkOptions>({
|
44
|
+
defaultOptions: {
|
45
|
+
defaultTarget: LinkTarget.Self,
|
46
|
+
supportedTargets: [LinkTarget.Self, LinkTarget.Blank],
|
47
|
+
},
|
48
|
+
handlerKeys: ['onShortcut'],
|
49
|
+
defaultPriority: ExtensionPriority.Medium,
|
50
|
+
})
|
51
|
+
export class LinkExtension extends MarkExtension<LinkOptions> {
|
52
|
+
get name(): string {
|
53
|
+
return MarkName.Link;
|
54
|
+
}
|
17
55
|
|
56
|
+
createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
|
18
57
|
return {
|
19
|
-
|
20
|
-
excludes:
|
58
|
+
inclusive: false,
|
59
|
+
excludes: 'assetLink',
|
60
|
+
...override,
|
21
61
|
attrs: {
|
22
|
-
...
|
62
|
+
...extra.defaults(),
|
63
|
+
href: {},
|
23
64
|
title: { default: undefined },
|
65
|
+
target: { default: this.options.defaultTarget },
|
24
66
|
},
|
25
67
|
parseDOM: [
|
26
68
|
{
|
@@ -32,88 +74,52 @@ export class LinkExtension extends RemirrorLinkExtension {
|
|
32
74
|
|
33
75
|
return {
|
34
76
|
...extra.parse(node),
|
35
|
-
auto: false,
|
36
77
|
href: node.getAttribute('href'),
|
37
|
-
target: node.getAttribute('target'),
|
38
78
|
title: node.getAttribute('title'),
|
79
|
+
target: validateTarget(
|
80
|
+
node.getAttribute('target'),
|
81
|
+
this.options.supportedTargets,
|
82
|
+
this.options.defaultTarget,
|
83
|
+
),
|
39
84
|
};
|
40
85
|
},
|
41
86
|
},
|
42
87
|
],
|
88
|
+
toDOM: (node) => {
|
89
|
+
const { target, ...rest } = omitExtraAttributes(node.attrs, extra);
|
90
|
+
const rel = 'noopener noreferrer nofollow';
|
91
|
+
const attrs = {
|
92
|
+
...extra.dom(node),
|
93
|
+
...rest,
|
94
|
+
rel,
|
95
|
+
target: validateTarget(target, this.options.supportedTargets, this.options.defaultTarget),
|
96
|
+
};
|
97
|
+
|
98
|
+
return ['a', attrs, 0];
|
99
|
+
},
|
43
100
|
};
|
44
101
|
}
|
45
102
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
const mark = getMarkRange($from, this.type);
|
50
|
-
const selectedText = tr.doc.textBetween(from, to);
|
51
|
-
|
52
|
-
this.options.onShortcut({
|
53
|
-
activeLink: mark ? { attrs: mark.mark.attrs as LinkAttributes, from: mark.from, to: mark.to } : undefined,
|
54
|
-
selectedText,
|
55
|
-
from,
|
56
|
-
to,
|
57
|
-
});
|
103
|
+
@keyBinding({ shortcut: NamedShortcut.InsertLink })
|
104
|
+
shortcut(_: KeyBindingProps): boolean {
|
105
|
+
this.options.onShortcut();
|
58
106
|
|
59
107
|
return true;
|
60
108
|
}
|
61
109
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
}
|
72
|
-
|
73
|
-
// work out the start position of the first link and end position of the last link in the selection.
|
74
|
-
const from = Math.min(tr.selection.from, ...ranges.map((range) => range.from));
|
75
|
-
const to = Math.max(tr.selection.to, ...ranges.map((range) => range.to));
|
76
|
-
|
77
|
-
// don't need to widen the selection, return early.
|
78
|
-
if (tr.selection.from === from && tr.selection.to === to) {
|
79
|
-
return false;
|
80
|
-
}
|
81
|
-
|
82
|
-
// widen the selection to make sure the full link is included.
|
83
|
-
this.store.commands.selectText.original({ from, to })(props);
|
84
|
-
return true;
|
85
|
-
};
|
110
|
+
@command()
|
111
|
+
updateLink({ text, attrs, range }: UpdateLinkProps): CommandFunction {
|
112
|
+
return this.store.getExtension(CommandsExtension).updateMark({
|
113
|
+
attrs,
|
114
|
+
text,
|
115
|
+
range,
|
116
|
+
mark: this.type,
|
117
|
+
removeMark: !attrs.href,
|
118
|
+
});
|
86
119
|
}
|
87
120
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
return (props) => {
|
92
|
-
const { tr, dispatch, view } = props;
|
93
|
-
const range = { from: tr.selection.from, to: tr.selection.to };
|
94
|
-
const selectedText = tr.doc.textBetween(range.from, range.to);
|
95
|
-
|
96
|
-
if (text !== selectedText) {
|
97
|
-
// update the text in the editor if it was updated, update the range to cover the length of the new text.
|
98
|
-
tr.insertText(text);
|
99
|
-
range.to = range.from + text.length;
|
100
|
-
}
|
101
|
-
|
102
|
-
// apply the link, or remove it if no URL was provided.
|
103
|
-
if (attrs.href.length > 0) {
|
104
|
-
tr.addMark(range.from, range.to, this.type.create(attrs));
|
105
|
-
} else {
|
106
|
-
tr.removeMark(range.from, range.to, this.type);
|
107
|
-
}
|
108
|
-
|
109
|
-
// move the cursor to the end of the link and re-focus the editor.
|
110
|
-
tr.setSelection(getTextSelection({ from: range.to, to: range.to }, tr.doc));
|
111
|
-
|
112
|
-
// apply the transaction.
|
113
|
-
dispatch?.(tr);
|
114
|
-
view?.focus();
|
115
|
-
|
116
|
-
return true;
|
117
|
-
};
|
121
|
+
@command()
|
122
|
+
removeLink(): CommandFunction {
|
123
|
+
return removeMark({ type: this.type });
|
118
124
|
}
|
119
125
|
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
export enum LinkTarget {
|
2
|
+
Self = '_self',
|
3
|
+
Blank = '_blank',
|
4
|
+
Parent = '_parent',
|
5
|
+
Top = '_top',
|
6
|
+
}
|
7
|
+
|
8
|
+
export const validateTarget = (target: unknown, supportedTargets: LinkTarget[], defaultTarget: LinkTarget) => {
|
9
|
+
return supportedTargets.includes(target as LinkTarget) ? (target as LinkTarget) : defaultTarget;
|
10
|
+
};
|
package/src/hooks/index.ts
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
import { Selection } from '@remirror/core';
|
2
|
+
import { GetMarkRange, getMarkRanges, getTextSelection, Mark, MarkType } from 'remirror';
|
3
|
+
import { useEditorState } from '@remirror/react';
|
4
|
+
|
5
|
+
export type ExpandedSelection = {
|
6
|
+
selection: Selection;
|
7
|
+
marks: Mark[];
|
8
|
+
};
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Returns a single range that is either:
|
12
|
+
* 1. The current selection if the current selection does not contain any of the indicated marks.
|
13
|
+
* 2. An expanded version of the current selection if the range contains any of the indicated marks.
|
14
|
+
*
|
15
|
+
* For example, given the content:
|
16
|
+
*
|
17
|
+
* <strong>Bold</strong> regular <u>underline</u> <u>more underline</u>
|
18
|
+
*
|
19
|
+
* If the marks passed in are 'bold' and 'underline' and the text "old regular under" is selected
|
20
|
+
* the returned range will be "Bold regular underline".
|
21
|
+
*
|
22
|
+
* @param {(MarkType|string)[]} markTypes
|
23
|
+
*
|
24
|
+
* @return {ExpandedSelection}
|
25
|
+
*/
|
26
|
+
export const useExpandedSelection = (markTypes: (MarkType | string)[]): ExpandedSelection => {
|
27
|
+
const { doc, selection } = useEditorState();
|
28
|
+
const ranges: GetMarkRange[] = [];
|
29
|
+
|
30
|
+
markTypes.forEach((markType) => {
|
31
|
+
ranges.push(...getMarkRanges(selection, markType));
|
32
|
+
});
|
33
|
+
|
34
|
+
if (ranges.length === 0) {
|
35
|
+
return { selection, marks: [] };
|
36
|
+
}
|
37
|
+
|
38
|
+
// work out the start position of the first link and end position of the last link in the selection.
|
39
|
+
const from = Math.min(selection.from, ...ranges.map((range) => range.from));
|
40
|
+
const to = Math.max(selection.to, ...ranges.map((range) => range.to));
|
41
|
+
const marks = ranges.map((range) => range.mark);
|
42
|
+
|
43
|
+
return { selection: getTextSelection({ from, to }, doc), marks };
|
44
|
+
};
|
package/src/index.scss
CHANGED
@@ -4,15 +4,15 @@
|
|
4
4
|
@import 'tailwindcss/utilities';
|
5
5
|
|
6
6
|
/* Global */
|
7
|
+
@import './ui/typography';
|
7
8
|
@import './ui/forms';
|
8
|
-
@import './ui/buttons';
|
9
9
|
|
10
10
|
/* Components */
|
11
11
|
@import './Editor/editor';
|
12
12
|
@import './EditorToolbar/toolbar';
|
13
13
|
@import './EditorToolbar/floating-toolbar';
|
14
14
|
|
15
|
-
@import './ui/
|
15
|
+
@import './ui/Button/button';
|
16
16
|
@import './ui/ToolbarDropdown/toolbar-dropdown';
|
17
17
|
@import './ui/ToolbarDropdownButton/toolbar-dropdown-button';
|
18
18
|
|
package/src/index.ts
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
-
import
|
1
|
+
import Editor from './Editor/Editor';
|
2
|
+
import { EditorContext } from './Editor/EditorContext';
|
3
|
+
import { remirrorNodeToSquizNode } from './utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode';
|
4
|
+
import { squizNodeToRemirrorNode } from './utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode';
|
2
5
|
|
3
|
-
export {
|
6
|
+
export { Editor, EditorContext, remirrorNodeToSquizNode, squizNodeToRemirrorNode };
|
package/src/types.ts
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, screen } from '@testing-library/react';
|
3
|
+
import React from 'react';
|
4
|
+
import Button from './Button';
|
5
|
+
import AccessTimeRoundedIcon from '@mui/icons-material/AccessTimeRounded';
|
6
|
+
|
7
|
+
describe('Button', () => {
|
8
|
+
const mockOnClick = jest.fn();
|
9
|
+
|
10
|
+
const ButtonComponent = () => {
|
11
|
+
return (
|
12
|
+
<Button
|
13
|
+
handleOnClick={mockOnClick}
|
14
|
+
isDisabled
|
15
|
+
isActive
|
16
|
+
label="Am a button"
|
17
|
+
text="Hello"
|
18
|
+
icon={<AccessTimeRoundedIcon />}
|
19
|
+
/>
|
20
|
+
);
|
21
|
+
};
|
22
|
+
|
23
|
+
it('Renders the label, text and icon', () => {
|
24
|
+
render(<ButtonComponent />);
|
25
|
+
const label = screen.getByLabelText('Am a button');
|
26
|
+
expect(label).toBeInTheDocument();
|
27
|
+
const text = screen.getByText('Hello');
|
28
|
+
expect(text).toBeInTheDocument();
|
29
|
+
const icon = screen.getByTestId('AccessTimeRoundedIcon');
|
30
|
+
expect(icon).toBeInTheDocument();
|
31
|
+
});
|
32
|
+
|
33
|
+
it('Renders the button in a disabled state if set to be disabled', () => {
|
34
|
+
render(<ButtonComponent />);
|
35
|
+
const button = screen.getByLabelText('Am a button');
|
36
|
+
expect(button).toBeDisabled();
|
37
|
+
});
|
38
|
+
|
39
|
+
it('Adds the active class is set to be active', () => {
|
40
|
+
render(<ButtonComponent />);
|
41
|
+
const button = screen.getByLabelText('Am a button');
|
42
|
+
expect(button).toHaveClass('squiz-fte-btn--is-active');
|
43
|
+
});
|
44
|
+
});
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import React, { ReactElement } from 'react';
|
2
|
+
import clsx from 'clsx';
|
3
|
+
|
4
|
+
type ButtonProps = {
|
5
|
+
handleOnClick: () => void;
|
6
|
+
isDisabled?: boolean;
|
7
|
+
isActive: boolean;
|
8
|
+
label: string;
|
9
|
+
text?: string;
|
10
|
+
icon?: ReactElement;
|
11
|
+
};
|
12
|
+
|
13
|
+
const Button = ({ handleOnClick, isDisabled, isActive, label, text, icon }: ButtonProps) => {
|
14
|
+
return (
|
15
|
+
<button
|
16
|
+
aria-label={label}
|
17
|
+
title={label}
|
18
|
+
type="button"
|
19
|
+
onClick={handleOnClick}
|
20
|
+
disabled={isDisabled}
|
21
|
+
className={clsx('squiz-fte-btn', isActive && 'squiz-fte-btn--is-active', icon && ' squiz-fte-btn--is-icon')}
|
22
|
+
>
|
23
|
+
{text && <span>{text}</span>}
|
24
|
+
{icon && icon}
|
25
|
+
</button>
|
26
|
+
);
|
27
|
+
};
|
28
|
+
|
29
|
+
export default Button;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
.squiz-fte-btn {
|
2
|
-
@apply font-normal rounded ease-linear transition-all duration-150;
|
2
|
+
@apply font-normal rounded ease-linear transition-all bg-white text-gray-600 duration-150;
|
3
3
|
display: flex;
|
4
4
|
align-items: center;
|
5
5
|
text-align: center;
|
@@ -11,9 +11,27 @@
|
|
11
11
|
border: 1px solid transparent;
|
12
12
|
padding: 6px 12px;
|
13
13
|
|
14
|
+
&--is-icon {
|
15
|
+
padding: 6px;
|
16
|
+
}
|
17
|
+
|
18
|
+
~ .squiz-fte-btn {
|
19
|
+
margin-left: 2px;
|
20
|
+
}
|
21
|
+
|
14
22
|
&.disabled,
|
15
23
|
&[disabled] {
|
16
24
|
cursor: not-allowed;
|
17
25
|
@apply opacity-50;
|
18
26
|
}
|
27
|
+
|
28
|
+
&:hover,
|
29
|
+
&:focus {
|
30
|
+
background-color: rgba(black, 0.04);
|
31
|
+
}
|
32
|
+
|
33
|
+
&--is-active,
|
34
|
+
&:active {
|
35
|
+
@apply text-blue-300 bg-blue-100;
|
36
|
+
}
|
19
37
|
}
|
@@ -1,30 +1,30 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
2
|
import { render, screen, fireEvent } from '@testing-library/react';
|
3
3
|
import React from 'react';
|
4
|
-
import {
|
4
|
+
import { Input } from './Input';
|
5
5
|
|
6
|
-
describe('
|
6
|
+
describe('Input', () => {
|
7
7
|
const mockOnChange = jest.fn();
|
8
8
|
|
9
|
-
const
|
10
|
-
return <
|
9
|
+
const InputComponent = () => {
|
10
|
+
return <Input name="text-input" defaultValue="Water" label="Text input" onChange={mockOnChange} />;
|
11
11
|
};
|
12
12
|
|
13
13
|
it('Renders the label', () => {
|
14
|
-
render(<
|
14
|
+
render(<InputComponent />);
|
15
15
|
// Check that the supplied label renders
|
16
16
|
const inputLabel = screen.getByLabelText('Text input');
|
17
17
|
expect(inputLabel).toBeInTheDocument();
|
18
18
|
});
|
19
19
|
|
20
20
|
it('Renders the default value', () => {
|
21
|
-
render(<
|
21
|
+
render(<InputComponent />);
|
22
22
|
// Check that default value supplied renders
|
23
23
|
expect(screen.getByDisplayValue('Water')).toBeInTheDocument();
|
24
24
|
});
|
25
25
|
|
26
26
|
it('Changes the value when new value entered', () => {
|
27
|
-
render(<
|
27
|
+
render(<InputComponent />);
|
28
28
|
const input = screen.getByLabelText('Text input') as HTMLInputElement;
|
29
29
|
// Check that default value supplied renders
|
30
30
|
expect(input.value).toBe('Water');
|
@@ -33,7 +33,7 @@ describe('Text input', () => {
|
|
33
33
|
});
|
34
34
|
|
35
35
|
it('Fires the change function when new value entered', () => {
|
36
|
-
render(<
|
36
|
+
render(<InputComponent />);
|
37
37
|
const input = screen.getByLabelText('Text input') as HTMLInputElement;
|
38
38
|
// Check that default value supplied renders
|
39
39
|
expect(input.value).toBe('Water');
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import React, { ForwardedRef, forwardRef, InputHTMLAttributes } from 'react';
|
2
|
+
import clsx from 'clsx';
|
3
|
+
|
4
|
+
type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
5
|
+
label?: string;
|
6
|
+
error?: string;
|
7
|
+
};
|
8
|
+
|
9
|
+
const InputInternal = (
|
10
|
+
{ name, label, type = 'text', error, ...rest }: InputProps,
|
11
|
+
ref: ForwardedRef<HTMLInputElement>,
|
12
|
+
) => {
|
13
|
+
return (
|
14
|
+
<div className={clsx(error && 'squiz-fte-invalid-form-field')}>
|
15
|
+
{label && (
|
16
|
+
<label htmlFor={name} className="squiz-fte-form-label">
|
17
|
+
{label}
|
18
|
+
</label>
|
19
|
+
)}
|
20
|
+
<input
|
21
|
+
ref={ref}
|
22
|
+
id={name}
|
23
|
+
name={name}
|
24
|
+
type={type}
|
25
|
+
aria-invalid={!!error}
|
26
|
+
className="squiz-fte-form-control"
|
27
|
+
{...rest}
|
28
|
+
/>
|
29
|
+
{error && <div className="squiz-fte-form-error">{error}</div>}
|
30
|
+
</div>
|
31
|
+
);
|
32
|
+
};
|
33
|
+
|
34
|
+
export const Input = forwardRef(InputInternal);
|