@squiz/formatted-text-editor 1.21.1-alpha.7 → 1.22.0

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 (121) hide show
  1. package/demo/App.tsx +52 -10
  2. package/demo/index.scss +11 -10
  3. package/jest.config.ts +0 -2
  4. package/lib/Editor/Editor.js +45 -7
  5. package/lib/Editor/EditorContext.d.ts +15 -0
  6. package/lib/Editor/EditorContext.js +15 -0
  7. package/lib/EditorToolbar/FloatingToolbar.js +11 -5
  8. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +9 -8
  9. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +91 -23
  10. package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +4 -1
  11. package/lib/EditorToolbar/Tools/Image/ImageButton.js +22 -14
  12. package/lib/EditorToolbar/Tools/Image/ImageModal.js +9 -5
  13. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +14 -5
  14. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +66 -14
  15. package/lib/EditorToolbar/Tools/Link/LinkButton.js +21 -13
  16. package/lib/EditorToolbar/Tools/Link/LinkModal.js +12 -5
  17. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -8
  18. package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
  19. package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
  20. package/lib/Extensions/Extensions.d.ts +12 -5
  21. package/lib/Extensions/Extensions.js +42 -20
  22. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
  23. package/lib/Extensions/ImageExtension/AssetImageExtension.js +92 -0
  24. package/lib/Extensions/ImageExtension/ImageExtension.d.ts +4 -0
  25. package/lib/Extensions/ImageExtension/ImageExtension.js +11 -0
  26. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
  27. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
  28. package/lib/Extensions/LinkExtension/LinkExtension.d.ts +19 -12
  29. package/lib/Extensions/LinkExtension/LinkExtension.js +56 -66
  30. package/lib/Extensions/LinkExtension/common.d.ts +7 -0
  31. package/lib/Extensions/LinkExtension/common.js +14 -0
  32. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
  33. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +6 -2
  34. package/lib/hooks/index.d.ts +1 -0
  35. package/lib/hooks/index.js +1 -0
  36. package/lib/hooks/useExpandedSelection.d.ts +23 -0
  37. package/lib/hooks/useExpandedSelection.js +37 -0
  38. package/lib/index.css +58 -23
  39. package/lib/index.d.ts +5 -2
  40. package/lib/index.js +9 -3
  41. package/lib/types.d.ts +3 -0
  42. package/lib/types.js +2 -0
  43. package/lib/ui/Button/Button.d.ts +2 -1
  44. package/lib/ui/Button/Button.js +4 -5
  45. package/lib/ui/Fields/Input/Input.d.ts +1 -0
  46. package/lib/ui/Fields/Input/Input.js +9 -3
  47. package/lib/ui/Modal/Modal.js +5 -3
  48. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +9 -0
  49. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +174 -0
  50. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
  51. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +138 -0
  52. package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
  53. package/lib/utils/resolveMatrixAssetUrl.js +10 -0
  54. package/lib/utils/undefinedIfEmpty.d.ts +1 -0
  55. package/lib/utils/undefinedIfEmpty.js +7 -0
  56. package/package.json +10 -4
  57. package/src/Editor/Editor.spec.tsx +78 -18
  58. package/src/Editor/Editor.tsx +28 -9
  59. package/src/Editor/EditorContext.spec.tsx +26 -0
  60. package/src/Editor/EditorContext.ts +26 -0
  61. package/src/Editor/_editor.scss +20 -4
  62. package/src/EditorToolbar/FloatingToolbar.spec.tsx +26 -7
  63. package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
  64. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +81 -6
  65. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +167 -47
  66. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +250 -2
  67. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +29 -16
  68. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +59 -20
  69. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +12 -10
  70. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
  71. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +96 -26
  72. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +137 -26
  73. package/src/EditorToolbar/Tools/Link/LinkButton.tsx +28 -19
  74. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
  75. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +27 -26
  76. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
  77. package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
  78. package/src/EditorToolbar/_floating-toolbar.scss +4 -5
  79. package/src/EditorToolbar/_toolbar.scss +1 -1
  80. package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
  81. package/src/Extensions/Extensions.ts +42 -18
  82. package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
  83. package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
  84. package/src/Extensions/ImageExtension/ImageExtension.ts +17 -1
  85. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
  86. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
  87. package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
  88. package/src/Extensions/LinkExtension/LinkExtension.ts +71 -85
  89. package/src/Extensions/LinkExtension/common.ts +10 -0
  90. package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +41 -0
  91. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +6 -2
  92. package/src/hooks/index.ts +1 -0
  93. package/src/hooks/useExpandedSelection.ts +44 -0
  94. package/src/index.ts +5 -2
  95. package/src/types.ts +5 -0
  96. package/src/ui/Button/Button.tsx +10 -6
  97. package/src/ui/Button/_button.scss +1 -1
  98. package/src/ui/Fields/Input/Input.spec.tsx +7 -1
  99. package/src/ui/Fields/Input/Input.tsx +23 -4
  100. package/src/ui/Modal/Modal.spec.tsx +15 -0
  101. package/src/ui/Modal/Modal.tsx +8 -4
  102. package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
  103. package/src/ui/_forms.scss +14 -0
  104. package/src/utils/converters/mocks/squizNodeJson.mock.ts +271 -0
  105. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +480 -0
  106. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +212 -0
  107. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +341 -0
  108. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +159 -0
  109. package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
  110. package/src/utils/resolveMatrixAssetUrl.ts +7 -0
  111. package/src/utils/undefinedIfEmpty.spec.ts +12 -0
  112. package/src/utils/undefinedIfEmpty.ts +3 -0
  113. package/tailwind.config.cjs +3 -0
  114. package/tests/renderWithEditor.tsx +28 -15
  115. package/tsconfig.json +1 -1
  116. package/lib/FormattedTextEditor.d.ts +0 -2
  117. package/lib/FormattedTextEditor.js +0 -7
  118. package/src/Editor/Editor.mock.tsx +0 -43
  119. package/src/FormattedTextEditor.spec.tsx +0 -10
  120. package/src/FormattedTextEditor.tsx +0 -3
  121. /package/tests/{select.tsx → select.ts} +0 -0
package/demo/App.tsx CHANGED
@@ -1,13 +1,26 @@
1
1
  import React, { useState } from 'react';
2
- import FormattedTextEditor from '../src/FormattedTextEditor';
2
+ import { Editor, EditorContext, remirrorNodeToSquizNode, squizNodeToRemirrorNode } from '../src';
3
3
  import { RemirrorEventListener, Extension } from '@remirror/core';
4
+ import ReactDiffViewer from 'react-diff-viewer-continued';
4
5
 
5
6
  function App() {
6
- const [doc, setDoc] = useState(null as any);
7
+ const [doc, setDoc] = useState('');
8
+ const [squizDoc, setSquizDoc] = useState('');
9
+ const [reconvertedDoc, setReconvertedDoc] = useState('');
10
+ const [error, setError] = useState<unknown>(null);
7
11
  const [editable, setEditable] = useState(true);
12
+
8
13
  const handleEditorChange: RemirrorEventListener<Extension> = (parameter) => {
9
- if (doc !== parameter.state.doc) {
10
- setDoc(parameter.state.doc);
14
+ try {
15
+ setDoc(JSON.stringify(parameter.state.doc, null, 2));
16
+
17
+ const newSquizDoc = remirrorNodeToSquizNode(parameter.state.doc);
18
+ const newReconvertedDoc = parameter.state.schema.nodeFromJSON(squizNodeToRemirrorNode(newSquizDoc));
19
+
20
+ setSquizDoc(JSON.stringify(newSquizDoc, null, 2));
21
+ setReconvertedDoc(JSON.stringify(newReconvertedDoc, null, 2));
22
+ } catch (e) {
23
+ setError(e);
11
24
  }
12
25
  };
13
26
 
@@ -22,15 +35,44 @@ function App() {
22
35
  </div>
23
36
  <h1>Editor</h1>
24
37
  <div className="page-section">
25
- <FormattedTextEditor
26
- editable={editable}
27
- content={`<p>Hello <a href="https://www.google.com"><strong>Mr Bean</strong></a>, nice to <a href="https://www.google.com">meet you</a>.</p>`}
28
- onChange={handleEditorChange}
29
- />
38
+ <EditorContext.Provider
39
+ value={{
40
+ matrix: {
41
+ matrixDomain: 'https://matrix-domain.squiz.net',
42
+ matrixIdentifier: 'matrix-api-identifier',
43
+ resolveMatrixAsset: (assetId: string) => {
44
+ return new Promise((resolve) => {
45
+ setTimeout(() => {
46
+ if (assetId.match(/invalid/i)) {
47
+ resolve(null);
48
+ } else {
49
+ resolve({
50
+ id: assetId,
51
+ type: assetId.match(/(image)/i)?.[0] || 'unknown',
52
+ });
53
+ }
54
+ }, 200);
55
+ });
56
+ },
57
+ },
58
+ }}
59
+ >
60
+ <Editor
61
+ editable={editable}
62
+ content={`<p>Hello <a href="https://www.google.com"><strong>Mr Bean</strong></a>, nice to <a href="https://www.google.com">meet you</a>.<img src="https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif" height="150" width="200"/></p>`}
63
+ onChange={handleEditorChange}
64
+ />
65
+ </EditorContext.Provider>
30
66
  </div>
31
67
  <h1>Document</h1>
32
68
  <div className="page-section">
33
- <code>{JSON.stringify(doc, null, 2)}</code>
69
+ {error instanceof Error && (
70
+ <div className="error">An error occurred when transforming the document: {error.message}</div>
71
+ )}
72
+ <div className="code-section">
73
+ <ReactDiffViewer oldValue={doc} newValue={reconvertedDoc} splitView={false} showDiffOnly={false} />
74
+ <ReactDiffViewer oldValue={squizDoc} newValue={squizDoc} splitView={false} showDiffOnly={false} />
75
+ </div>
34
76
  </div>
35
77
  </div>
36
78
  );
package/demo/index.scss CHANGED
@@ -22,19 +22,20 @@ h1 {
22
22
  align-items: center;
23
23
  }
24
24
 
25
- code {
26
- padding: 8px;
27
- display: block;
28
- white-space: pre;
29
- background-color: #eee;
30
- font-size: 0.8rem;
31
- height: 40vh;
32
- max-height: 40vh;
25
+ .code-section {
26
+ display: flex;
27
+ flex-direction: row;
28
+ gap: 8px;
29
+ height: 35vh;
33
30
  overflow: scroll;
34
31
  }
35
32
 
36
33
  .remirror-editor {
37
- height: 40vh;
38
- max-height: 40vh;
34
+ height: 35vh;
35
+ max-height: 35vh;
39
36
  overflow: scroll;
40
37
  }
38
+
39
+ .error {
40
+ color: red;
41
+ }
package/jest.config.ts CHANGED
@@ -22,8 +22,6 @@ export default {
22
22
  moduleNameMapper: {
23
23
  '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css|scss)$':
24
24
  '<rootDir>/file-transformer.js',
25
- '^react($|/.+)': '<rootDir>/node_modules/react$1',
26
- '^react-dom($|/.+)': '<rootDir>/node_modules/react-dom$1',
27
25
  },
28
26
  setupFilesAfterEnv: ['<rootDir>/jest.bootstrap.ts'],
29
27
  };
@@ -1,15 +1,53 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
5
28
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const react_1 = __importDefault(require("react"));
29
+ const react_1 = __importStar(require("react"));
7
30
  const react_2 = require("@remirror/react");
8
31
  const EditorToolbar_1 = require("../EditorToolbar");
32
+ const EditorContext_1 = require("./EditorContext");
9
33
  const Extensions_1 = require("../Extensions/Extensions");
10
- const Editor = ({ content, editable, onChange }) => {
34
+ const clsx_1 = __importDefault(require("clsx"));
35
+ const WrappedEditor = () => {
36
+ const preventImagePaste = (0, react_1.useCallback)((event) => {
37
+ const { clipboardData } = event;
38
+ const pastedData = clipboardData?.files[0];
39
+ if (pastedData?.type && pastedData?.type.startsWith('image/')) {
40
+ event.preventDefault();
41
+ }
42
+ // Allow other paste event handlers to be run.
43
+ return false;
44
+ }, []);
45
+ (0, react_2.useEditorEvent)('paste', preventImagePaste);
46
+ return react_1.default.createElement(react_2.EditorComponent, null);
47
+ };
48
+ const Editor = ({ content, editable = true, onChange }) => {
11
49
  const { manager, state, setState } = (0, react_2.useRemirror)({
12
- extensions: Extensions_1.Extensions,
50
+ extensions: (0, Extensions_1.createExtensions)((0, react_1.useContext)(EditorContext_1.EditorContext)),
13
51
  content,
14
52
  selection: 'start',
15
53
  stringHandler: 'html',
@@ -19,10 +57,10 @@ const Editor = ({ content, editable, onChange }) => {
19
57
  onChange?.(parameter);
20
58
  };
21
59
  return (react_1.default.createElement("div", { className: "squiz-fte-scope" },
22
- react_1.default.createElement("div", { className: "remirror-theme formatted-text-editor editor-wrapper" },
60
+ react_1.default.createElement("div", { className: (0, clsx_1.default)('remirror-theme formatted-text-editor', !editable && 'formatted-text-editor--is-disabled') },
23
61
  react_1.default.createElement(react_2.Remirror, { manager: manager, state: state, editable: editable, onChange: handleChange, placeholder: "Write something", label: "Text editor" },
24
- react_1.default.createElement(EditorToolbar_1.Toolbar, null),
25
- react_1.default.createElement(react_2.EditorComponent, null),
26
- react_1.default.createElement(EditorToolbar_1.FloatingToolbar, null)))));
62
+ editable && react_1.default.createElement(EditorToolbar_1.Toolbar, null),
63
+ react_1.default.createElement(WrappedEditor, null),
64
+ editable && react_1.default.createElement(EditorToolbar_1.FloatingToolbar, null)))));
27
65
  };
28
66
  exports.default = Editor;
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ export type MatrixAsset = {
3
+ id: string;
4
+ type: string | 'image';
5
+ };
6
+ export type MatrixAssetResolver = (assetId: string) => Promise<MatrixAsset | null>;
7
+ export type EditorContextOptions = {
8
+ matrix: {
9
+ matrixIdentifier: string;
10
+ matrixDomain: string;
11
+ resolveMatrixAsset: MatrixAssetResolver;
12
+ };
13
+ };
14
+ export declare const defaultEditorContext: EditorContextOptions;
15
+ export declare const EditorContext: React.Context<EditorContextOptions>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.EditorContext = exports.defaultEditorContext = void 0;
7
+ const react_1 = __importDefault(require("react"));
8
+ exports.defaultEditorContext = {
9
+ matrix: {
10
+ matrixIdentifier: '',
11
+ matrixDomain: '',
12
+ resolveMatrixAsset: () => Promise.resolve(null),
13
+ },
14
+ };
15
+ exports.EditorContext = react_1.default.createContext(exports.defaultEditorContext);
@@ -37,22 +37,28 @@ const LinkButton_1 = __importDefault(require("./Tools/Link/LinkButton"));
37
37
  const react_2 = require("@remirror/react");
38
38
  const react_components_1 = require("@remirror/react-components");
39
39
  const createToolbarPositioner_1 = require("../utils/createToolbarPositioner");
40
- // The editor main toolbar
40
+ const ImageButton_1 = __importDefault(require("./Tools/Image/ImageButton"));
41
+ const Extensions_1 = require("../Extensions/Extensions");
41
42
  const FloatingToolbar = () => {
43
+ const watchedMarks = [Extensions_1.MarkName.Link, Extensions_1.MarkName.AssetLink];
42
44
  const extensionNames = (0, hooks_1.useExtensionNames)();
43
- const positioner = (0, react_1.useMemo)(() => (0, createToolbarPositioner_1.createToolbarPositioner)({ types: ['link'] }), []);
44
- const { data } = (0, react_2.usePositioner)(positioner, []);
45
+ const positioner = (0, react_1.useMemo)(() => (0, createToolbarPositioner_1.createToolbarPositioner)({ types: watchedMarks }), []);
46
+ const active = (0, react_2.useActive)();
47
+ const { data: { marks }, } = (0, react_2.usePositioner)(positioner, []);
45
48
  let buttons = [
46
49
  extensionNames.bold && react_1.default.createElement(BoldButton_1.default, { key: "bold" }),
47
50
  extensionNames.italic && react_1.default.createElement(ItalicButton_1.default, { key: "italic" }),
48
51
  extensionNames.underline && react_1.default.createElement(UnderlineButton_1.default, { key: "underline" }),
49
52
  ];
50
- if (data.marks?.link.isExclusivelyActive) {
53
+ if (active.image() || active.assetImage()) {
54
+ buttons = [react_1.default.createElement(ImageButton_1.default, { key: "add-image", inPopover: true })];
55
+ }
56
+ else if (marks?.[Extensions_1.MarkName.Link].isExclusivelyActive || marks?.[Extensions_1.MarkName.AssetLink].isExclusivelyActive) {
51
57
  // if all of the selected text is a link show the options to update/remove the link instead of the regular
52
58
  // formatting options.
53
59
  buttons = [react_1.default.createElement(LinkButton_1.default, { key: "update-link", inPopover: true }), react_1.default.createElement(RemoveLinkButton_1.default, { key: "remove-link" })];
54
60
  }
55
- else if (!data.marks?.link.isActive) {
61
+ else if (!marks?.[Extensions_1.MarkName.Link].isActive && !marks?.[Extensions_1.MarkName.AssetLink].isActive) {
56
62
  // if none of the selected text is a link show the option to create a link.
57
63
  buttons.push(react_1.default.createElement(react_components_1.VerticalDivider, { key: "link-divider", className: "editor-divider" }), react_1.default.createElement(LinkButton_1.default, { key: "add-link", inPopover: true }));
58
64
  }
@@ -1,17 +1,18 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { SubmitHandler } from 'react-hook-form';
3
3
  import { ImageAttributes } from '@remirror/extension-image/dist-types/image-extension';
4
- export type UpdateImageOptions = ImageAttributes & {
5
- src: string;
6
- alt: string;
7
- width: number;
8
- height: number;
4
+ import { NodeName } from '../../../../Extensions/Extensions';
5
+ import { AssetImageAttributes } from '../../../../Extensions/ImageExtension/AssetImageExtension';
6
+ import { DeepPartial } from '../../../../types';
7
+ export type ImageFormData = {
8
+ imageType: NodeName;
9
+ image: Pick<ImageAttributes, 'src' | 'alt' | 'width' | 'height'>;
10
+ assetImage: AssetImageAttributes;
9
11
  };
10
- export type ImageFormData = Pick<UpdateImageOptions, 'src' | 'alt' | 'width' | 'height'>;
11
12
  export type FormProps = {
12
- data: Partial<ImageFormData>;
13
+ data: DeepPartial<ImageFormData>;
13
14
  onSubmit: SubmitHandler<ImageFormData>;
14
15
  };
15
- export type Dimensions = 'width' | 'height';
16
+ export type Dimensions = 'image.width' | 'image.height';
16
17
  declare const ImageForm: ({ data, onSubmit }: FormProps) => ReactElement;
17
18
  export default ImageForm;
@@ -33,34 +33,53 @@ const react_image_size_1 = require("react-image-size");
33
33
  const Button_1 = __importDefault(require("../../../../ui/Button/Button"));
34
34
  const LinkOff_1 = __importDefault(require("@mui/icons-material/LinkOff"));
35
35
  const InsertLinkRounded_1 = __importDefault(require("@mui/icons-material/InsertLinkRounded"));
36
+ const clsx_1 = __importDefault(require("clsx"));
37
+ const Extensions_1 = require("../../../../Extensions/Extensions");
38
+ const Select_1 = require("../../../../ui/Fields/Select/Select");
39
+ const EditorContext_1 = require("../../../../Editor/EditorContext");
40
+ const imageTypeOptions = {
41
+ [Extensions_1.NodeName.Image]: { label: 'External image' },
42
+ [Extensions_1.NodeName.AssetImage]: { label: 'Asset image' },
43
+ };
44
+ const regexDataURI = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)$/i;
36
45
  const ImageForm = ({ data, onSubmit }) => {
37
- const { register, handleSubmit, setValue } = (0, react_hook_form_1.useForm)({
46
+ const { register, handleSubmit, setValue, watch, formState: { errors }, } = (0, react_hook_form_1.useForm)({
38
47
  defaultValues: data,
39
48
  });
49
+ const imageType = watch('imageType') || Extensions_1.NodeName.Image;
50
+ const context = (0, react_1.useContext)(EditorContext_1.EditorContext);
40
51
  const [aspectRatioFromWidth, setAspectRatioFromWidth] = (0, react_1.useState)(9 / 16);
41
52
  const [aspectRatioFromHeight, setAspectRatioFromHeight] = (0, react_1.useState)(16 / 9);
42
53
  const [aspectRatioLocked, setAspectRatioLocked] = (0, react_1.useState)(true);
43
- const setDimensionsFromURL = (e) => {
44
- // get the new url, calculate the width and height and set those fields
45
- (0, react_image_size_1.getImageSize)(e.target.value)
46
- .then(({ width, height }) => {
47
- setValue('width', width);
48
- setValue('height', height);
54
+ const setDimensionsFromURL = async (e) => {
55
+ try {
56
+ const { width, height } = await (0, react_image_size_1.getImageSize)(e.target.value);
57
+ setValue('image.width', width);
58
+ setValue('image.height', height);
49
59
  setAspectRatioFromWidth(height / width);
50
60
  setAspectRatioFromHeight(width / height);
51
- })
52
- .catch((errorMessage) => {
53
- // TODO: we will use this when we add validation in a follow-up ticket
54
- console.log(errorMessage);
55
- });
61
+ }
62
+ catch (error) {
63
+ // swallow the error for fetching the image size, will occur if the URL does not point to an image.
64
+ // will be handled by validation when attempting to add the image.
65
+ }
66
+ };
67
+ const validateIsNotImage = async (src) => {
68
+ try {
69
+ await (0, react_image_size_1.getImageSize)(src);
70
+ return false;
71
+ }
72
+ catch (error) {
73
+ return true;
74
+ }
56
75
  };
57
- const calculateDimensions = () => {
76
+ const calculateDimensions = (event) => {
58
77
  if (aspectRatioLocked) {
59
78
  const currentTarget = event?.target;
60
79
  const type = currentTarget.name;
61
80
  const currentValue = currentTarget.value;
62
- const otherValue = type === 'width' ? 'height' : 'width';
63
- const aspectRatio = type === 'width' ? aspectRatioFromWidth : aspectRatioFromHeight;
81
+ const otherValue = type === 'image.width' ? 'image.height' : 'image.width';
82
+ const aspectRatio = type === 'image.width' ? aspectRatioFromWidth : aspectRatioFromHeight;
64
83
  const newValue = Math.round(aspectRatio * Number(currentValue) * 100) / 100;
65
84
  setValue(otherValue, newValue);
66
85
  }
@@ -70,15 +89,64 @@ const ImageForm = ({ data, onSubmit }) => {
70
89
  };
71
90
  return (react_1.default.createElement("form", { className: "squiz-fte-form", onSubmit: handleSubmit(onSubmit) },
72
91
  react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
73
- react_1.default.createElement(Input_1.Input, { label: "Source", ...register('src'), onBlur: setDimensionsFromURL })),
74
- react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
75
- react_1.default.createElement(Input_1.Input, { label: "Alternative description", ...register('alt') })),
76
- react_1.default.createElement("div", { className: "flex flex-row items-end" },
92
+ react_1.default.createElement(Select_1.Select, { name: "imageType", label: "Type", value: imageType, options: imageTypeOptions, onChange: (value) => setValue('imageType', value) })),
93
+ imageType === Extensions_1.NodeName.Image && (react_1.default.createElement(react_1.default.Fragment, null,
94
+ react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
95
+ react_1.default.createElement(Input_1.Input, { label: "Source", required: true, error: errors?.image?.src?.message, ...register('image.src', {
96
+ onChange: setDimensionsFromURL,
97
+ required: 'Source is required',
98
+ validate: {
99
+ isValidImage: async (value) => {
100
+ if (value && regexDataURI.test(value)) {
101
+ return 'Must not be a data URI';
102
+ }
103
+ if (value && (await validateIsNotImage(value))) {
104
+ return 'Must be a valid image URL';
105
+ }
106
+ },
107
+ },
108
+ }) })),
77
109
  react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
78
- react_1.default.createElement(Input_1.Input, { label: "Width", ...register('width'), type: "number", name: "width", onChange: calculateDimensions })),
79
- react_1.default.createElement("div", { className: "flex mx-1 mb-2" },
80
- react_1.default.createElement(Button_1.default, { handleOnClick: toggleAspectRatio, isActive: false, icon: aspectRatioLocked ? react_1.default.createElement(InsertLinkRounded_1.default, null) : react_1.default.createElement(LinkOff_1.default, null), label: "Constrain properties", isDisabled: false })),
110
+ react_1.default.createElement(Input_1.Input, { label: "Alternative description", required: true, error: errors?.image?.alt?.message, ...register('image.alt', { required: 'Alternative description is required' }) })),
111
+ react_1.default.createElement("div", { className: "flex flex-row" },
112
+ react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
113
+ react_1.default.createElement(Input_1.Input, { label: "Width", type: "number", required: true, error: errors?.image?.width?.message, ...register('image.width', {
114
+ onChange: calculateDimensions,
115
+ required: 'Width is required',
116
+ validate: {
117
+ isValidWidth: (value) => {
118
+ if (value && !(value > 0)) {
119
+ return 'Must be higher than 0';
120
+ }
121
+ },
122
+ },
123
+ }) })),
124
+ react_1.default.createElement("div", { className: "flex mx-1 mb-2" },
125
+ react_1.default.createElement(Button_1.default, { handleOnClick: toggleAspectRatio, isActive: false, icon: aspectRatioLocked ? react_1.default.createElement(InsertLinkRounded_1.default, null) : react_1.default.createElement(LinkOff_1.default, null), label: "Constrain properties", isDisabled: false, className: (0, clsx_1.default)('my-auto', !errors?.image?.height && !errors?.image?.width && 'mb-0') })),
126
+ react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
127
+ react_1.default.createElement(Input_1.Input, { label: "Height", type: "number", required: true, error: errors?.image?.height?.message, ...register('image.height', {
128
+ onChange: calculateDimensions,
129
+ required: 'Height is required',
130
+ validate: {
131
+ isValidHeight: (value) => {
132
+ if (value && !(value > 0)) {
133
+ return 'Must be higher than 0';
134
+ }
135
+ },
136
+ },
137
+ }) }))))),
138
+ imageType === Extensions_1.NodeName.AssetImage && (react_1.default.createElement(react_1.default.Fragment, null,
81
139
  react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
82
- react_1.default.createElement(Input_1.Input, { label: "Height", ...register('height'), type: "number", name: "height", onChange: calculateDimensions })))));
140
+ react_1.default.createElement(Input_1.Input, { label: "Asset ID", required: true, error: errors?.assetImage?.matrixAssetId?.message, ...register('assetImage.matrixAssetId', {
141
+ required: 'Asset ID is required',
142
+ validate: {
143
+ isImage: async (assetId) => {
144
+ const asset = await context.matrix.resolveMatrixAsset(assetId);
145
+ if (asset?.type !== 'image') {
146
+ return 'Asset ID is invalid or not an image';
147
+ }
148
+ },
149
+ },
150
+ }) }))))));
83
151
  };
84
152
  exports.default = ImageForm;
@@ -1,2 +1,5 @@
1
- declare const ImageButton: () => JSX.Element;
1
+ type ImageButtonProps = {
2
+ inPopover?: boolean;
3
+ };
4
+ declare const ImageButton: ({ inPopover }: ImageButtonProps) => JSX.Element;
2
5
  export default ImageButton;
@@ -27,27 +27,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  const react_1 = __importStar(require("react"));
30
+ const react_2 = require("@remirror/react");
30
31
  const ImageRounded_1 = __importDefault(require("@mui/icons-material/ImageRounded"));
31
32
  const ImageModal_1 = __importDefault(require("./ImageModal"));
32
33
  const Button_1 = __importDefault(require("../../../ui/Button/Button"));
33
- const react_2 = require("@remirror/react");
34
- const ImageButton = () => {
34
+ const Extensions_1 = require("../../../Extensions/Extensions");
35
+ const ImageButton = ({ inPopover = false }) => {
35
36
  const [showModal, setShowModal] = (0, react_1.useState)(false);
36
- const { insertImage } = (0, react_2.useCommands)();
37
- const active = showModal;
37
+ const { insertImage, insertAssetImage } = (0, react_2.useCommands)();
38
+ const active = (0, react_2.useActive)();
39
+ const selection = (0, react_2.useCurrentSelection)();
40
+ // if the active selection is not an image, disable the button as it means it will be text
41
+ const disabled = !selection.empty && !active.image() && !active.assetImage();
38
42
  const handleClick = () => {
39
43
  if (!showModal) {
40
- // form element are uncontrolled, let the event loop run to
41
- // update the selected text in state before showing the modal.
42
- requestAnimationFrame(() => {
43
- setShowModal(true);
44
- });
44
+ setShowModal(true);
45
45
  }
46
46
  };
47
47
  const insertImageFromData = (data) => {
48
- const { src, alt, width, height } = data;
49
- if (src) {
50
- insertImage({ src, alt, width, height });
48
+ const { imageType, image, assetImage } = data;
49
+ if (imageType === Extensions_1.NodeName.Image) {
50
+ insertImage(image);
51
+ }
52
+ else {
53
+ insertAssetImage(assetImage);
51
54
  }
52
55
  };
53
56
  const handleSubmit = (data) => {
@@ -59,9 +62,14 @@ const ImageButton = () => {
59
62
  // Prevent other key handlers being run
60
63
  return true;
61
64
  }, []);
62
- (0, react_2.useKeymap)('Mod-l', handleShortcut);
65
+ // when Ctrl+l is pressed show the modal, only registered in the toolbar button instance to avoid the key press
66
+ // being double handled.
67
+ if (!inPopover) {
68
+ // disable the shortcut if the button is disabled
69
+ (0, react_2.useKeymap)('Mod-l', disabled ? () => false : handleShortcut);
70
+ }
63
71
  return (react_1.default.createElement(react_1.default.Fragment, null,
64
- react_1.default.createElement(Button_1.default, { handleOnClick: handleClick, isActive: active, icon: react_1.default.createElement(ImageRounded_1.default, null), label: "Image (cmd+L)", isDisabled: false }),
72
+ react_1.default.createElement(Button_1.default, { handleOnClick: handleClick, isActive: active.image() || active.assetImage(), icon: react_1.default.createElement(ImageRounded_1.default, null), label: "Image (cmd+L)", isDisabled: disabled }),
65
73
  showModal && react_1.default.createElement(ImageModal_1.default, { onCancel: () => setShowModal(false), onSubmit: handleSubmit })));
66
74
  };
67
75
  exports.default = ImageButton;
@@ -3,17 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const remirror_1 = require("remirror");
7
6
  const ImageForm_1 = __importDefault(require("./Form/ImageForm"));
8
7
  const react_1 = __importDefault(require("react"));
9
8
  const react_2 = require("@remirror/react");
10
9
  const FormModal_1 = __importDefault(require("../../../ui/Modal/FormModal"));
10
+ const Extensions_1 = require("../../../Extensions/Extensions");
11
11
  const ImageModal = ({ onCancel, onSubmit }) => {
12
- const { helpers, view: { state }, } = (0, react_2.useRemirrorContext)();
13
12
  const selection = (0, react_2.useCurrentSelection)();
14
- const currentImage = (0, remirror_1.getMarkRanges)(selection, 'image')[0];
15
- const selectedImage = helpers.getTextBetween(selection.from, selection.to, state.doc);
13
+ const currentImage = selection?.node;
14
+ const currentImageAttrs = { ...currentImage?.attrs };
15
+ const formData = {
16
+ imageType: currentImage?.type.name === Extensions_1.NodeName.AssetImage ? Extensions_1.NodeName.AssetImage : Extensions_1.NodeName.Image,
17
+ image: currentImage?.type?.name === Extensions_1.NodeName.Image ? currentImageAttrs : {},
18
+ assetImage: currentImage?.type?.name === Extensions_1.NodeName.AssetImage ? currentImageAttrs : {},
19
+ };
16
20
  return (react_1.default.createElement(FormModal_1.default, { title: "Image", onCancel: onCancel },
17
- react_1.default.createElement(ImageForm_1.default, { data: { ...currentImage?.mark.attrs, src: selectedImage }, onSubmit: onSubmit })));
21
+ react_1.default.createElement(ImageForm_1.default, { data: formData, onSubmit: onSubmit })));
18
22
  };
19
23
  exports.default = ImageModal;
@@ -1,10 +1,19 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { SubmitHandler } from 'react-hook-form';
3
- import { UpdateLinkOptions } from '../../../../Extensions/LinkExtension/LinkExtension';
4
- export type LinkFormData = Pick<UpdateLinkOptions, 'href' | 'target' | 'title' | 'text'>;
3
+ import { FromToProps } from 'remirror';
4
+ import { UpdateLinkProps } from '../../../../Extensions/LinkExtension/LinkExtension';
5
+ import { UpdateAssetLinkProps } from '../../../../Extensions/LinkExtension/AssetLinkExtension';
6
+ import { MarkName } from '../../../../Extensions/Extensions';
7
+ import { DeepPartial } from '../../../../types';
8
+ export type LinkFormData = {
9
+ linkType: MarkName;
10
+ text: string;
11
+ link: UpdateLinkProps['attrs'];
12
+ assetLink: UpdateAssetLinkProps['attrs'];
13
+ range: FromToProps;
14
+ };
5
15
  export type FormProps = {
6
- data: Partial<LinkFormData>;
16
+ data?: DeepPartial<LinkFormData>;
7
17
  onSubmit: SubmitHandler<LinkFormData>;
8
18
  };
9
- declare const LinkForm: ({ data, onSubmit }: FormProps) => ReactElement;
10
- export default LinkForm;
19
+ export declare const LinkForm: ({ data, onSubmit }: FormProps) => ReactElement;