@squiz/formatted-text-editor 2.1.0 → 2.2.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.
- package/CHANGELOG.md +18 -0
- package/demo/App.tsx +5 -0
- package/demo/AppContext.tsx +107 -70
- package/lib/EditorToolbar/FloatingToolbar.js +1 -1
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +2 -1
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +53 -10
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +8 -5
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -1
- package/lib/Extensions/Extensions.d.ts +1 -0
- package/lib/Extensions/Extensions.js +3 -0
- package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +6 -0
- package/lib/Extensions/ImageExtension/DAMImageExtension.d.ts +17 -0
- package/lib/Extensions/ImageExtension/DAMImageExtension.js +97 -0
- package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.d.ts +28 -0
- package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.js +88 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +9 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +9 -0
- package/package.json +4 -2
- package/src/EditorToolbar/FloatingToolbar.spec.tsx +3 -1
- package/src/EditorToolbar/FloatingToolbar.tsx +1 -1
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +27 -2
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +61 -14
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +70 -2
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +12 -6
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +4 -1
- package/src/Extensions/Extensions.ts +3 -0
- package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +9 -0
- package/src/Extensions/ImageExtension/DAMImageExtension.spec.ts +87 -0
- package/src/Extensions/ImageExtension/DAMImageExtension.ts +119 -0
- package/src/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.spec.tsx +219 -0
- package/src/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.tsx +109 -0
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +21 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +10 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +8 -0
- package/src/utils/getNodeNamesByGroup.spec.ts +1 -0
- package/tests/index.ts +1 -0
- package/tests/mockResourceBrowser.tsx +46 -0
@@ -0,0 +1,88 @@
|
|
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
|
+
};
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
26
|
+
exports.ResourceBrowserSelector = void 0;
|
27
|
+
const react_1 = __importStar(require("react"));
|
28
|
+
const resource_browser_1 = require("@squiz/resource-browser");
|
29
|
+
const InputContainer_1 = require("../InputContainer/InputContainer");
|
30
|
+
const Extensions_1 = require("../../../Extensions/Extensions");
|
31
|
+
const ResourceBrowserSelector = ({ modalTitle, allowedTypes, value, onChange, ...props }) => {
|
32
|
+
const convertFormDataToResourceBrowserValue = (0, react_1.useCallback)((value) => {
|
33
|
+
if (value?.matrixIdentifier && value?.matrixAssetId) {
|
34
|
+
return {
|
35
|
+
sourceId: value.matrixIdentifier,
|
36
|
+
resourceId: value.matrixAssetId,
|
37
|
+
};
|
38
|
+
}
|
39
|
+
else if (value?.damSystemIdentifier && value?.damObjectId) {
|
40
|
+
return {
|
41
|
+
sourceId: value.damSystemIdentifier,
|
42
|
+
resourceId: value.damObjectId,
|
43
|
+
};
|
44
|
+
}
|
45
|
+
return null;
|
46
|
+
}, []);
|
47
|
+
const handleResourceChange = (0, react_1.useCallback)((resource) => {
|
48
|
+
// Clear out any key properties for clear resource use case
|
49
|
+
let onChangeData = {
|
50
|
+
...value,
|
51
|
+
matrixIdentifier: undefined,
|
52
|
+
matrixAssetId: undefined,
|
53
|
+
damSystemIdentifier: undefined,
|
54
|
+
damObjectId: undefined,
|
55
|
+
damSystemType: undefined,
|
56
|
+
url: undefined,
|
57
|
+
};
|
58
|
+
if (resource?.source?.type === 'matrix') {
|
59
|
+
onChangeData = {
|
60
|
+
...value,
|
61
|
+
matrixIdentifier: resource?.source?.id,
|
62
|
+
matrixAssetId: resource?.id,
|
63
|
+
url: resource?.url,
|
64
|
+
nodeType: Extensions_1.NodeName.AssetImage,
|
65
|
+
};
|
66
|
+
}
|
67
|
+
else if (resource?.source?.type === 'dam') {
|
68
|
+
onChangeData = {
|
69
|
+
...value,
|
70
|
+
damSystemIdentifier: resource?.source?.id,
|
71
|
+
damObjectId: resource?.id,
|
72
|
+
damSystemType: (resource?.source).configuration.externalType,
|
73
|
+
url: resource?.url,
|
74
|
+
nodeType: Extensions_1.NodeName.DAMImage,
|
75
|
+
};
|
76
|
+
}
|
77
|
+
onChange({
|
78
|
+
target: {
|
79
|
+
value: {
|
80
|
+
...onChangeData,
|
81
|
+
},
|
82
|
+
},
|
83
|
+
});
|
84
|
+
}, []);
|
85
|
+
return (react_1.default.createElement(InputContainer_1.InputContainer, { ...props },
|
86
|
+
react_1.default.createElement(resource_browser_1.ResourceBrowser, { modalTitle: modalTitle, allowedTypes: allowedTypes, value: convertFormDataToResourceBrowserValue(value), onChange: handleResourceChange })));
|
87
|
+
};
|
88
|
+
exports.ResourceBrowserSelector = ResourceBrowserSelector;
|
@@ -117,6 +117,15 @@ const transformNode = (node) => {
|
|
117
117
|
matrixDomain: node.attrs.matrixDomain,
|
118
118
|
};
|
119
119
|
}
|
120
|
+
if (node.type.name === Extensions_1.NodeName.DAMImage) {
|
121
|
+
transformedNode = {
|
122
|
+
type: 'dam-image',
|
123
|
+
damObjectId: node.attrs.damObjectId,
|
124
|
+
damSystemIdentifier: node.attrs.damSystemIdentifier,
|
125
|
+
damSystemType: node.attrs.damSystemType,
|
126
|
+
damAdditional: node.attrs.damAdditional ? JSON.parse(node.attrs.damAdditional) : undefined,
|
127
|
+
};
|
128
|
+
}
|
120
129
|
node.marks.forEach((mark) => {
|
121
130
|
transformedNode = transformMark(mark, transformedNode);
|
122
131
|
});
|
@@ -7,6 +7,7 @@ const getNodeType = (node) => {
|
|
7
7
|
const typeMap = {
|
8
8
|
'link-to-matrix-asset': Extensions_1.NodeName.Text,
|
9
9
|
'matrix-image': Extensions_1.NodeName.AssetImage,
|
10
|
+
'dam-image': Extensions_1.NodeName.DAMImage,
|
10
11
|
text: 'text',
|
11
12
|
};
|
12
13
|
const tagMap = {
|
@@ -87,6 +88,14 @@ const getNodeAttributes = (node) => {
|
|
87
88
|
matrixIdentifier: node.matrixIdentifier,
|
88
89
|
};
|
89
90
|
}
|
91
|
+
else if (node.type === 'dam-image') {
|
92
|
+
return {
|
93
|
+
damObjectId: node.damObjectId,
|
94
|
+
damSystemIdentifier: node.damSystemIdentifier,
|
95
|
+
damSystemType: node.damSystemType,
|
96
|
+
damAdditional: node.damAdditional ? JSON.stringify(node.damAdditional) : undefined,
|
97
|
+
};
|
98
|
+
}
|
90
99
|
else if (node.type === 'tag') {
|
91
100
|
return {
|
92
101
|
nodeIndent: null,
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/formatted-text-editor",
|
3
|
-
"version": "2.
|
3
|
+
"version": "2.2.0",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"private": false,
|
@@ -24,11 +24,12 @@
|
|
24
24
|
"@mui/icons-material": "5.15.18",
|
25
25
|
"@remirror/extension-react-tables": "^2.2.19",
|
26
26
|
"@remirror/react": "2.0.35",
|
27
|
-
"@squiz/dx-json-schema-lib": "^1.
|
27
|
+
"@squiz/dx-json-schema-lib": "^1.72.0",
|
28
28
|
"@squiz/dxp-ai-client-react": "^0.1.3-alpha",
|
29
29
|
"@squiz/matrix-resource-browser-plugin": "^2.0.0",
|
30
30
|
"@squiz/resource-browser": "^2.0.0",
|
31
31
|
"@squiz/sds": "^1.0.0-alpha.50",
|
32
|
+
"@squiz/dam-resource-browser-plugin": "^0.9.0-rc.0",
|
32
33
|
"clsx": "2.1.1",
|
33
34
|
"react-hook-form": "7.51.4",
|
34
35
|
"react-image-size": "2.0.0",
|
@@ -58,6 +59,7 @@
|
|
58
59
|
"react": "18.2.0",
|
59
60
|
"react-diff-viewer-continued": "3.2.6",
|
60
61
|
"react-dom": "18.2.0",
|
62
|
+
"react-query": "^3.19.6",
|
61
63
|
"rimraf": "5.0.7",
|
62
64
|
"tailwindcss": "3.2.6",
|
63
65
|
"ts-jest": "29.0.5",
|
@@ -31,13 +31,15 @@ describe('FloatingToolbar', () => {
|
|
31
31
|
it.each([
|
32
32
|
['Image selected', 1, ['Image (Ctrl+L)']],
|
33
33
|
['Asset image selected', 2, ['Image (Ctrl+L)']],
|
34
|
+
['DAM image selected', 3, ['Image (Ctrl+L)']],
|
34
35
|
])(
|
35
36
|
'Renders formatting buttons when node is selected - %s',
|
36
37
|
async (description: string, pos: number, expectedButtons: string[]) => {
|
37
38
|
const { editor } = await renderWithEditor(null, {
|
38
39
|
content:
|
39
40
|
'<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" />' +
|
40
|
-
'<img data-matrix-asset-id="100" data-matrix-identifier="key" data-matrix-domain="my-matrix.squiz.net" />'
|
41
|
+
'<img data-matrix-asset-id="100" data-matrix-identifier="key" data-matrix-domain="my-matrix.squiz.net" />' +
|
42
|
+
'<img data-dam-object-id="5ce8e8dc-1adc-4254-87a8-d1f5a5c9045a" data-dam-system-identifier="byder001" data-dam-system-type="bynder" />',
|
41
43
|
editable: true,
|
42
44
|
});
|
43
45
|
|
@@ -30,7 +30,7 @@ export const FloatingToolbar = () => {
|
|
30
30
|
extensionNames.underline && <UnderlineButton key="underline" />,
|
31
31
|
];
|
32
32
|
|
33
|
-
if (active.image() || active.assetImage()) {
|
33
|
+
if (active.image() || active.assetImage() || active.DAMImage()) {
|
34
34
|
buttons = [<ImageButton key="add-image" inPopover={true} />];
|
35
35
|
} else if (marks?.[MarkName.Link].isExclusivelyActive || marks?.[MarkName.AssetLink].isExclusivelyActive) {
|
36
36
|
// if all of the selected text is a link show the options to update/remove the link instead of the regular
|
@@ -3,7 +3,13 @@ import { render, screen, act, fireEvent, waitFor } from '@testing-library/react'
|
|
3
3
|
import React from 'react';
|
4
4
|
import ImageForm from './ImageForm';
|
5
5
|
import { NodeName } from '../../../../Extensions/Extensions';
|
6
|
-
import { mockResourceBrowserContext } from '../../../../../tests';
|
6
|
+
import { mockResourceBrowser, mockResourceBrowserContext } from '../../../../../tests';
|
7
|
+
|
8
|
+
const { setShouldUseMockResourceBrowser, mockResourceBrowserImpl } = mockResourceBrowser();
|
9
|
+
jest.mock('@squiz/resource-browser', () => ({
|
10
|
+
...jest.requireActual('@squiz/resource-browser'),
|
11
|
+
ResourceBrowser: (props: any) => mockResourceBrowserImpl(props),
|
12
|
+
}));
|
7
13
|
|
8
14
|
describe('Image Form', () => {
|
9
15
|
const handleSubmit = jest.fn();
|
@@ -48,7 +54,7 @@ describe('Image Form', () => {
|
|
48
54
|
data={{
|
49
55
|
...data,
|
50
56
|
imageType: NodeName.AssetImage,
|
51
|
-
|
57
|
+
resourceImage: { matrixAssetId: '100', matrixIdentifier: 'matrix-api-identifier' },
|
52
58
|
}}
|
53
59
|
onSubmit={handleSubmit}
|
54
60
|
/>
|
@@ -61,6 +67,25 @@ describe('Image Form', () => {
|
|
61
67
|
});
|
62
68
|
});
|
63
69
|
|
70
|
+
it('Renders the form with the relevant fields for dam images', async () => {
|
71
|
+
setShouldUseMockResourceBrowser(true);
|
72
|
+
render(
|
73
|
+
<ImageForm
|
74
|
+
data={{
|
75
|
+
...data,
|
76
|
+
imageType: NodeName.DAMImage,
|
77
|
+
resourceImage: { damObjectId: '100', damSystemIdentifier: 'dam-api-identifier' },
|
78
|
+
}}
|
79
|
+
onSubmit={handleSubmit}
|
80
|
+
/>,
|
81
|
+
);
|
82
|
+
|
83
|
+
expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
|
84
|
+
await waitFor(() => {
|
85
|
+
expect(screen.getByText('ResourceBrowser was rendered')).toBeInTheDocument();
|
86
|
+
});
|
87
|
+
});
|
88
|
+
|
64
89
|
it('calculates the height when width changes and aspect ratio is locked', () => {
|
65
90
|
render(<ImageForm data={data} onSubmit={handleSubmit} />);
|
66
91
|
const widthInput = screen.getByLabelText('Width');
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { ReactElement, useState } from 'react';
|
1
|
+
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
|
2
2
|
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
3
3
|
import { getImageSize } from 'react-image-size';
|
4
4
|
import clsx from 'clsx';
|
@@ -9,20 +9,25 @@ import LinkOffIcon from '@mui/icons-material/LinkOff';
|
|
9
9
|
import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
|
10
10
|
import { NodeName } from '../../../../Extensions/Extensions';
|
11
11
|
import { AssetImageAttributes } from '../../../../Extensions/ImageExtension/AssetImageExtension';
|
12
|
+
import { DAMImageAttributes } from '../../../../Extensions/ImageExtension/DAMImageExtension';
|
12
13
|
import { DeepPartial } from '../../../../types';
|
13
14
|
import { noEmptySpacesValidation, regexDataURI, hasProperties } from '../../../../utils/validation';
|
14
15
|
import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
|
15
|
-
import {
|
16
|
+
import { ResourceBrowserSelector } from '../../../../ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector';
|
16
17
|
|
17
18
|
export type ImageFormData = {
|
18
19
|
imageType: NodeName;
|
19
20
|
image: Pick<ImageAttributes, 'src' | 'alt' | 'width' | 'height'>;
|
20
|
-
|
21
|
+
resourceImage: AssetImageAttributes & DAMImageAttributes;
|
21
22
|
};
|
22
23
|
|
24
|
+
enum ViewTypes {
|
25
|
+
Resource = 'Resource',
|
26
|
+
URL = 'URL',
|
27
|
+
}
|
23
28
|
const imageTypeOptions: TabOptions = {
|
24
|
-
[
|
25
|
-
[
|
29
|
+
[ViewTypes.Resource]: { label: 'From source' },
|
30
|
+
[ViewTypes.URL]: { label: 'From URL' },
|
26
31
|
};
|
27
32
|
|
28
33
|
export type FormProps = {
|
@@ -42,7 +47,8 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
42
47
|
} = useForm<ImageFormData>({
|
43
48
|
defaultValues: data,
|
44
49
|
});
|
45
|
-
const imageType = watch('imageType')
|
50
|
+
const imageType = watch('imageType');
|
51
|
+
const [viewType, setViewType] = useState<ViewTypes>(ViewTypes.Resource);
|
46
52
|
const [aspectRatioFromWidth, setAspectRatioFromWidth] = useState(9 / 16);
|
47
53
|
const [aspectRatioFromHeight, setAspectRatioFromHeight] = useState(16 / 9);
|
48
54
|
const [aspectRatioLocked, setAspectRatioLocked] = useState(true);
|
@@ -86,16 +92,41 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
86
92
|
setAspectRatioLocked(!aspectRatioLocked);
|
87
93
|
};
|
88
94
|
|
95
|
+
useEffect(() => {
|
96
|
+
if (imageType === NodeName.Image) {
|
97
|
+
setViewType(ViewTypes.URL);
|
98
|
+
} else {
|
99
|
+
setViewType(ViewTypes.Resource);
|
100
|
+
}
|
101
|
+
}, [imageType, setViewType]);
|
102
|
+
|
103
|
+
const handleChangeViewType = useCallback(
|
104
|
+
(value: string) => {
|
105
|
+
setViewType(value as ViewTypes);
|
106
|
+
// If its the URL field type we know what the imageType should be
|
107
|
+
if (value === ViewTypes.URL) {
|
108
|
+
console.log(`handleChangeViewType: ${value} NodeName.Image`);
|
109
|
+
setValue('imageType', NodeName.Image);
|
110
|
+
} else {
|
111
|
+
// Need a value here and this is the assumed default elsewhere
|
112
|
+
// Will be set again later once Resource Browser returns a resource value
|
113
|
+
console.log(`handleChangeViewType: ${value} NodeName.AssetImage`);
|
114
|
+
setValue('imageType', NodeName.AssetImage);
|
115
|
+
}
|
116
|
+
},
|
117
|
+
[setViewType, setValue],
|
118
|
+
);
|
119
|
+
|
89
120
|
return (
|
90
121
|
<form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
|
91
122
|
<div className="squiz-fte-form-group mb-4">
|
92
123
|
<Tabs
|
93
|
-
value={imageType}
|
124
|
+
value={imageType === NodeName.Image ? ViewTypes.URL : ViewTypes.Resource}
|
94
125
|
options={imageTypeOptions}
|
95
|
-
onChange={
|
126
|
+
onChange={handleChangeViewType}
|
96
127
|
/>
|
97
128
|
</div>
|
98
|
-
{
|
129
|
+
{viewType === ViewTypes.URL && (
|
99
130
|
<>
|
100
131
|
<div className="squiz-fte-form-group mb-2">
|
101
132
|
<Input
|
@@ -174,20 +205,36 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
174
205
|
</div>
|
175
206
|
</>
|
176
207
|
)}
|
177
|
-
{
|
208
|
+
{viewType === ViewTypes.Resource && (
|
178
209
|
<div className="squiz-fte-form-group mb-2">
|
179
210
|
<Controller
|
180
211
|
control={control}
|
181
|
-
name="
|
212
|
+
name="resourceImage"
|
182
213
|
rules={{
|
183
|
-
validate:
|
214
|
+
validate: (value) => {
|
215
|
+
const matrixValidation = hasProperties('An image must be selected', [
|
216
|
+
'matrixIdentifier',
|
217
|
+
'matrixAssetId',
|
218
|
+
])(value);
|
219
|
+
const damValidation = hasProperties('An image must be selected', [
|
220
|
+
'damObjectId',
|
221
|
+
'damSystemIdentifier',
|
222
|
+
])(value);
|
223
|
+
|
224
|
+
// One of the two needs to validate
|
225
|
+
return matrixValidation && damValidation;
|
226
|
+
},
|
184
227
|
}}
|
185
228
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
186
|
-
<
|
229
|
+
<ResourceBrowserSelector
|
187
230
|
modalTitle="Insert image"
|
188
231
|
allowedTypes={['image']}
|
189
232
|
value={value}
|
190
|
-
onChange={
|
233
|
+
onChange={(value: { target: { value: any } }) => {
|
234
|
+
console.log(`onChange: ${value}`);
|
235
|
+
setValue('imageType', value.target.value.nodeType);
|
236
|
+
onChange(value);
|
237
|
+
}}
|
191
238
|
error={error?.message}
|
192
239
|
/>
|
193
240
|
)}
|
@@ -1,11 +1,18 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
2
|
import { screen, fireEvent, waitForElementToBeRemoved, act, waitFor } from '@testing-library/react';
|
3
|
+
import { ResourceBrowserResource } from '@squiz/resource-browser';
|
3
4
|
import { NodeSelection } from 'prosemirror-state';
|
4
5
|
import React from 'react';
|
5
|
-
import { renderWithEditor, mockResourceBrowserContext } from '../../../../tests';
|
6
|
+
import { renderWithEditor, mockResourceBrowserContext, mockResourceBrowser } from '../../../../tests';
|
6
7
|
import ImageButton from './ImageButton';
|
7
8
|
import { getImageSize } from 'react-image-size';
|
8
9
|
|
10
|
+
const { setSelectedResource, setShouldUseMockResourceBrowser, mockResourceBrowserImpl } = mockResourceBrowser();
|
11
|
+
jest.mock('@squiz/resource-browser', () => ({
|
12
|
+
...jest.requireActual('@squiz/resource-browser'),
|
13
|
+
ResourceBrowser: (props: any) => mockResourceBrowserImpl(props),
|
14
|
+
}));
|
15
|
+
|
9
16
|
jest.mock('react-image-size');
|
10
17
|
|
11
18
|
describe('ImageButton', () => {
|
@@ -295,7 +302,7 @@ describe('ImageButton', () => {
|
|
295
302
|
});
|
296
303
|
});
|
297
304
|
|
298
|
-
it('Updates the attributes of an existing asset image', async () => {
|
305
|
+
it('assetImage: Updates the attributes of an existing asset image', async () => {
|
299
306
|
const matrixIdentifier = 'matrix-api-identifier';
|
300
307
|
const matrixDomain = 'https://my-matrix.squiz.net';
|
301
308
|
const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
|
@@ -362,6 +369,67 @@ describe('ImageButton', () => {
|
|
362
369
|
});
|
363
370
|
});
|
364
371
|
|
372
|
+
it('DAMImage: Updates the attributes of an existing dam image', async () => {
|
373
|
+
setShouldUseMockResourceBrowser(true);
|
374
|
+
setSelectedResource({
|
375
|
+
id: 'my-resource-id',
|
376
|
+
name: 'My resource',
|
377
|
+
url: 'myResourceUrl',
|
378
|
+
source: {
|
379
|
+
id: 'my-source-id',
|
380
|
+
type: 'dam',
|
381
|
+
configuration: {
|
382
|
+
externalType: 'bynder',
|
383
|
+
},
|
384
|
+
},
|
385
|
+
} as unknown as ResourceBrowserResource);
|
386
|
+
|
387
|
+
const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
|
388
|
+
content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
|
389
|
+
context: {
|
390
|
+
editor: {
|
391
|
+
matrix: {
|
392
|
+
matrixDomain: 'https://my-matrix.squiz.net',
|
393
|
+
},
|
394
|
+
},
|
395
|
+
},
|
396
|
+
});
|
397
|
+
|
398
|
+
await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
|
399
|
+
|
400
|
+
await openModal();
|
401
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
402
|
+
// Trigger the mock Resource Browser to invoke its onChange callback to fill the form with data
|
403
|
+
await waitFor(() => {
|
404
|
+
expect(screen.getByRole('button', { name: 'Select ResourceBrowser Resource' })).toBeInTheDocument();
|
405
|
+
});
|
406
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Select ResourceBrowser Resource' }));
|
407
|
+
// Close the image selection modal
|
408
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
409
|
+
|
410
|
+
expect(getJsonContent()).toEqual({
|
411
|
+
type: 'paragraph',
|
412
|
+
attrs: expect.any(Object),
|
413
|
+
content: [
|
414
|
+
{
|
415
|
+
text: 'Some ',
|
416
|
+
type: 'text',
|
417
|
+
},
|
418
|
+
{
|
419
|
+
type: 'DAMImage',
|
420
|
+
attrs: {
|
421
|
+
damObjectId: 'my-resource-id',
|
422
|
+
damSystemIdentifier: 'my-source-id',
|
423
|
+
damAdditional: undefined,
|
424
|
+
damSystemType: 'bynder',
|
425
|
+
src: 'myResourceUrl',
|
426
|
+
},
|
427
|
+
},
|
428
|
+
{ type: 'text', text: ' nonsense' },
|
429
|
+
],
|
430
|
+
});
|
431
|
+
});
|
432
|
+
|
365
433
|
it('Shows an error if a resource is not selected', async () => {
|
366
434
|
await renderWithEditor(<ImageButton />);
|
367
435
|
await openModal();
|
@@ -6,7 +6,8 @@ import { ImageFormData } from './Form/ImageForm';
|
|
6
6
|
import Button from '../../../ui/Button/Button';
|
7
7
|
import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
|
8
8
|
import { NodeName } from '../../../Extensions/Extensions';
|
9
|
-
import { AssetImageExtension } from '../../../Extensions/ImageExtension/AssetImageExtension';
|
9
|
+
import { AssetImageExtension, AssetImageAttributes } from '../../../Extensions/ImageExtension/AssetImageExtension';
|
10
|
+
import { DAMImageExtension, DAMImageAttributes } from '../../../Extensions/ImageExtension/DAMImageExtension';
|
10
11
|
import { CodeBlockExtension } from 'remirror/dist-types/extensions';
|
11
12
|
import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
|
12
13
|
|
@@ -16,11 +17,14 @@ type ImageButtonProps = {
|
|
16
17
|
|
17
18
|
const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
|
18
19
|
const [showModal, setShowModal] = useState(false);
|
19
|
-
const { insertImage, insertAssetImage } = useCommands<
|
20
|
+
const { insertImage, insertAssetImage, insertDAMImage } = useCommands<
|
21
|
+
ImageExtension | AssetImageExtension | DAMImageExtension
|
22
|
+
>();
|
20
23
|
const active = useActive<ImageExtension | AssetImageExtension | CodeBlockExtension>();
|
21
24
|
const selection = useCurrentSelection();
|
22
25
|
// if the active selection is not an image, disable the button as it means it will be text
|
23
|
-
const disabled =
|
26
|
+
const disabled =
|
27
|
+
(!selection.empty && !active.image() && !active.assetImage() && !active.DAMImage()) || active.codeBlock();
|
24
28
|
|
25
29
|
const handleClick = () => {
|
26
30
|
if (!showModal) {
|
@@ -29,11 +33,13 @@ const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
|
|
29
33
|
};
|
30
34
|
|
31
35
|
const insertImageFromData = (data: ImageFormData) => {
|
32
|
-
const { imageType, image,
|
36
|
+
const { imageType, image, resourceImage } = data;
|
33
37
|
if (imageType === NodeName.Image) {
|
34
38
|
insertImage(image);
|
39
|
+
} else if (imageType === NodeName.DAMImage) {
|
40
|
+
insertDAMImage(resourceImage as DAMImageAttributes);
|
35
41
|
} else {
|
36
|
-
insertAssetImage(
|
42
|
+
insertAssetImage(resourceImage as AssetImageAttributes);
|
37
43
|
}
|
38
44
|
};
|
39
45
|
|
@@ -59,7 +65,7 @@ const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
|
|
59
65
|
<>
|
60
66
|
<Button
|
61
67
|
handleOnClick={handleClick}
|
62
|
-
isActive={active.image() || active.assetImage()}
|
68
|
+
isActive={active.image() || active.assetImage() || active.DAMImage()}
|
63
69
|
icon={<ImageRoundedIcon />}
|
64
70
|
label={`Image (${getShortcutSymbol()}+L)`}
|
65
71
|
isDisabled={disabled}
|
@@ -19,7 +19,10 @@ const ImageModal = ({ onCancel, onSubmit }: ImageModalProps) => {
|
|
19
19
|
const formData = {
|
20
20
|
imageType: currentImage?.type.name === NodeName.Image ? NodeName.Image : NodeName.AssetImage,
|
21
21
|
image: currentImage?.type?.name === NodeName.Image ? currentImageAttrs : {},
|
22
|
-
|
22
|
+
resourceImage:
|
23
|
+
currentImage?.type?.name === NodeName.DAMImage || currentImage?.type?.name === NodeName.AssetImage
|
24
|
+
? currentImageAttrs
|
25
|
+
: {},
|
23
26
|
};
|
24
27
|
|
25
28
|
return (
|
@@ -23,6 +23,7 @@ import { ImageExtension } from './ImageExtension/ImageExtension';
|
|
23
23
|
import { CommandsExtension } from './CommandsExtension/CommandsExtension';
|
24
24
|
import { EditorContextOptions } from '../Editor/EditorContext';
|
25
25
|
import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
|
26
|
+
import { DAMImageExtension } from './ImageExtension/DAMImageExtension';
|
26
27
|
import { ExtendedCodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
|
27
28
|
import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
|
28
29
|
import { UnsupportedNodeExtension } from './UnsuportedExtension/UnsupportedNodeExtension';
|
@@ -34,6 +35,7 @@ export enum NodeName {
|
|
34
35
|
Image = 'image',
|
35
36
|
CodeBlock = 'codeBlock',
|
36
37
|
AssetImage = 'assetImage',
|
38
|
+
DAMImage = 'DAMImage',
|
37
39
|
Text = 'text',
|
38
40
|
TableControllerCell = 'tableControllerCell',
|
39
41
|
tableCell = 'tableCell',
|
@@ -66,6 +68,7 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
66
68
|
new AssetImageExtension({
|
67
69
|
matrixDomain: context.matrix.matrixDomain,
|
68
70
|
}),
|
71
|
+
new DAMImageExtension(),
|
69
72
|
new LinkExtension(),
|
70
73
|
new AssetLinkExtension({
|
71
74
|
matrixDomain: context.matrix.matrixDomain,
|
@@ -33,6 +33,15 @@ export class FetchUrlExtension extends PlainExtension<FetchUrlOptions> {
|
|
33
33
|
);
|
34
34
|
}
|
35
35
|
|
36
|
+
if (node.type.name === NodeName.DAMImage && node.attrs.src === '') {
|
37
|
+
promises.push(
|
38
|
+
this.fetchAndReplace(node.attrs, (url: string) => {
|
39
|
+
const newNode = state.schema.nodes[NodeName.DAMImage].create({ ...node.attrs, src: url });
|
40
|
+
tr.replaceWith(pos, pos + node.nodeSize, newNode);
|
41
|
+
}),
|
42
|
+
);
|
43
|
+
}
|
44
|
+
|
36
45
|
const assetLinkMark = this.findAssetLinkMark(node.marks as Mark[]);
|
37
46
|
if (node.type.name === 'text' && assetLinkMark) {
|
38
47
|
promises.push(
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import { renderWithEditor } from '../../../tests';
|
2
|
+
|
3
|
+
describe('DAMImageExtension', () => {
|
4
|
+
it('Parses HTML content representing an asset image', async () => {
|
5
|
+
const { getJsonContent } = await renderWithEditor(null, {
|
6
|
+
content: `<img
|
7
|
+
src="https://my-matrix.squiz.net/?a=this-is-actually-ignored"
|
8
|
+
data-dam-object-id="5ce8e8dc-1adc-4254-87a8-d1f5a5c9045a"
|
9
|
+
data-dam-system-identifier="byder001"
|
10
|
+
data-dam-system-type="bynder"
|
11
|
+
data-dam-additional='{"variant":"xyz"}'
|
12
|
+
/>`,
|
13
|
+
});
|
14
|
+
|
15
|
+
expect(getJsonContent()).toEqual({
|
16
|
+
type: 'paragraph',
|
17
|
+
attrs: expect.any(Object),
|
18
|
+
content: [
|
19
|
+
{
|
20
|
+
type: 'DAMImage',
|
21
|
+
attrs: {
|
22
|
+
damObjectId: '5ce8e8dc-1adc-4254-87a8-d1f5a5c9045a',
|
23
|
+
damSystemIdentifier: 'byder001',
|
24
|
+
damSystemType: 'bynder',
|
25
|
+
damAdditional: {
|
26
|
+
variant: 'xyz',
|
27
|
+
},
|
28
|
+
src: 'https://default-resource/',
|
29
|
+
},
|
30
|
+
},
|
31
|
+
],
|
32
|
+
});
|
33
|
+
});
|
34
|
+
|
35
|
+
it('Resolves to a regular image if HTML content is missing some of the expected attributes', async () => {
|
36
|
+
const { getJsonContent } = await renderWithEditor(null, {
|
37
|
+
content:
|
38
|
+
'<img src="https://my-matrix.squiz.net/?a=123" data-dam-object-id="5ce8e8dc-1adc-4254-87a8-d1f5a5c9045a" />',
|
39
|
+
});
|
40
|
+
|
41
|
+
expect(getJsonContent()).toEqual({
|
42
|
+
type: 'paragraph',
|
43
|
+
attrs: expect.any(Object),
|
44
|
+
content: [
|
45
|
+
{
|
46
|
+
type: 'image',
|
47
|
+
attrs: expect.objectContaining({
|
48
|
+
src: 'https://my-matrix.squiz.net/?a=123',
|
49
|
+
}),
|
50
|
+
},
|
51
|
+
],
|
52
|
+
});
|
53
|
+
});
|
54
|
+
|
55
|
+
it('Outputs expected HTML', async () => {
|
56
|
+
const { getHtmlContent } = await renderWithEditor(null, {
|
57
|
+
content: {
|
58
|
+
type: 'paragraph',
|
59
|
+
content: [
|
60
|
+
{
|
61
|
+
type: 'DAMImage',
|
62
|
+
attrs: {
|
63
|
+
damObjectId: '5ce8e8dc-1adc-4254-87a8-d1f5a5c9045a',
|
64
|
+
damSystemIdentifier: 'byder001',
|
65
|
+
damSystemType: 'bynder',
|
66
|
+
damAdditional: {
|
67
|
+
variant: 'xyz',
|
68
|
+
},
|
69
|
+
},
|
70
|
+
},
|
71
|
+
],
|
72
|
+
},
|
73
|
+
});
|
74
|
+
|
75
|
+
expect(getHtmlContent()).toEqual(
|
76
|
+
'<img ' +
|
77
|
+
'src="https://default-resource/" ' +
|
78
|
+
'data-dam-object-id="5ce8e8dc-1adc-4254-87a8-d1f5a5c9045a" ' +
|
79
|
+
'data-dam-system-identifier="byder001" ' +
|
80
|
+
'data-dam-system-type="bynder" ' +
|
81
|
+
'data-dam-additional="{"variant":"xyz"}" ' +
|
82
|
+
'draggable="true">' +
|
83
|
+
'<img class="ProseMirror-separator" alt="">' +
|
84
|
+
'<br class="ProseMirror-trailingBreak">',
|
85
|
+
);
|
86
|
+
});
|
87
|
+
});
|