@squiz/formatted-text-editor 1.40.1-alpha.7 → 1.41.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/demo/AppContext.tsx +14 -3
- package/demo/resources.json +135 -3
- package/lib/Editor/Editor.js +3 -2
- package/lib/EditorToolbar/Toolbar.js +2 -0
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +1 -1
- package/lib/EditorToolbar/Tools/Lists/ListButtons.d.ts +2 -0
- package/lib/EditorToolbar/Tools/Lists/ListButtons.js +14 -0
- package/lib/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.d.ts +2 -0
- package/lib/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.js +22 -0
- package/lib/EditorToolbar/Tools/Lists/UnorderedList/UnorderedListButton.d.ts +2 -0
- package/lib/EditorToolbar/Tools/Lists/UnorderedList/UnorderedListButton.js +22 -0
- package/lib/Extensions/Extensions.d.ts +2 -1
- package/lib/Extensions/Extensions.js +9 -1
- package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.d.ts +14 -0
- package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +63 -0
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +1 -0
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +4 -4
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +2 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +4 -4
- package/lib/hooks/useFocus.js +24 -5
- package/lib/index.css +13 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +1 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +1 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +3 -0
- package/package.json +4 -4
- package/src/Editor/Editor.spec.tsx +91 -16
- package/src/Editor/Editor.tsx +3 -2
- package/src/EditorToolbar/Toolbar.tsx +2 -0
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +12 -2
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +1 -1
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +14 -2
- package/src/EditorToolbar/Tools/Lists/ListButtons.tsx +14 -0
- package/src/EditorToolbar/Tools/Lists/OrderedList/OrderListButton.spec.tsx +39 -0
- package/src/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.tsx +30 -0
- package/src/EditorToolbar/Tools/Lists/UnorderedList/UnorderedList.spec.tsx +19 -0
- package/src/EditorToolbar/Tools/Lists/UnorderedList/UnorderedListButton.tsx +30 -0
- package/src/Extensions/Extensions.ts +14 -2
- package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +73 -0
- package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +2 -1
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +5 -5
- package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +3 -1
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +6 -5
- package/src/hooks/useFocus.ts +30 -7
- package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +1 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +2 -0
- package/src/ui/_typography.scss +16 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +191 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +3 -0
- package/src/utils/getNodeNamesByGroup.spec.ts +11 -1
- package/tests/mockResourceBrowserContext.tsx +2 -2
- package/tests/renderWithContext.tsx +30 -1
- package/tests/renderWithEditor.tsx +18 -13
- package/lib/utils/resolveMatrixAssetUrl.d.ts +0 -1
- package/lib/utils/resolveMatrixAssetUrl.js +0 -10
- package/src/utils/resolveMatrixAssetUrl.spec.ts +0 -26
- package/src/utils/resolveMatrixAssetUrl.ts +0 -7
@@ -1,27 +1,16 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
1
2
|
import React from 'react';
|
2
|
-
import { act, fireEvent, render, screen } from '@testing-library/react';
|
3
|
+
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
4
|
+
import { ResourceBrowserContext } from '@squiz/resource-browser';
|
3
5
|
import Editor from './Editor';
|
4
|
-
import '@testing-library/jest-dom';
|
5
6
|
import { renderWithEditor } from '../../tests';
|
6
7
|
import ImageButton from '../EditorToolbar/Tools/Image/ImageButton';
|
8
|
+
import * as useFocus from '../hooks/useFocus';
|
7
9
|
|
8
|
-
const isVisible = jest.fn().mockReturnValue(false);
|
9
10
|
const handleFocusMock = jest.fn();
|
10
11
|
const handleBlurMock = jest.fn();
|
11
|
-
jest.mock('../hooks/useFocus', () => ({
|
12
|
-
__esModule: true,
|
13
|
-
default: () => ({
|
14
|
-
isVisible: isVisible(),
|
15
|
-
handleFocus: handleFocusMock,
|
16
|
-
handleBlur: handleBlurMock,
|
17
|
-
}),
|
18
|
-
}));
|
19
12
|
|
20
13
|
describe('Formatted text editor', () => {
|
21
|
-
afterEach(() => {
|
22
|
-
jest.clearAllMocks();
|
23
|
-
});
|
24
|
-
|
25
14
|
it('Renders the text editor', () => {
|
26
15
|
render(<Editor />);
|
27
16
|
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
@@ -329,6 +318,13 @@ describe('Formatted text editor', () => {
|
|
329
318
|
});
|
330
319
|
|
331
320
|
it('triggers handleFocus when editor is focused', async () => {
|
321
|
+
jest.spyOn(useFocus, 'default').mockReturnValue({
|
322
|
+
isVisible: false,
|
323
|
+
handleFocus: handleFocusMock,
|
324
|
+
handleBlur: handleBlurMock,
|
325
|
+
wrapperRef: { current: null },
|
326
|
+
});
|
327
|
+
|
332
328
|
const { getByLabelText } = render(<Editor />);
|
333
329
|
const editorInput = getByLabelText('Text editor');
|
334
330
|
|
@@ -339,6 +335,13 @@ describe('Formatted text editor', () => {
|
|
339
335
|
});
|
340
336
|
|
341
337
|
it('triggers handleBlur when editor is blurred', () => {
|
338
|
+
jest.spyOn(useFocus, 'default').mockReturnValue({
|
339
|
+
isVisible: false,
|
340
|
+
handleFocus: handleFocusMock,
|
341
|
+
handleBlur: handleBlurMock,
|
342
|
+
wrapperRef: { current: null },
|
343
|
+
});
|
344
|
+
|
342
345
|
const { getByLabelText } = render(<Editor />);
|
343
346
|
const editorInput = getByLabelText('Text editor');
|
344
347
|
|
@@ -349,9 +352,81 @@ describe('Formatted text editor', () => {
|
|
349
352
|
});
|
350
353
|
|
351
354
|
it('should apply hide class when focus hook returns false', () => {
|
352
|
-
|
355
|
+
jest.spyOn(useFocus, 'default').mockReturnValue({
|
356
|
+
isVisible: true,
|
357
|
+
handleFocus: handleFocusMock,
|
358
|
+
handleBlur: handleBlurMock,
|
359
|
+
wrapperRef: { current: null },
|
360
|
+
});
|
361
|
+
|
353
362
|
const { container } = render(<Editor />);
|
354
363
|
|
355
364
|
expect(container.querySelector('.show-toolbar')).toBeInTheDocument();
|
356
365
|
});
|
366
|
+
|
367
|
+
it('Interactivity in modals functions correctly, toolbar remains visible throughout', async () => {
|
368
|
+
const onRequestSources = jest.fn().mockResolvedValue([
|
369
|
+
{
|
370
|
+
id: 'my-matrix-instance',
|
371
|
+
name: 'My Matrix instance',
|
372
|
+
nodes: [],
|
373
|
+
},
|
374
|
+
]);
|
375
|
+
const onRequestChildren = jest
|
376
|
+
.fn()
|
377
|
+
.mockResolvedValueOnce([
|
378
|
+
{
|
379
|
+
id: '1',
|
380
|
+
name: 'Folder 1',
|
381
|
+
type: { code: 'folder', name: 'Folder' },
|
382
|
+
status: { code: 'live', name: 'Live' },
|
383
|
+
childCount: 1,
|
384
|
+
},
|
385
|
+
])
|
386
|
+
.mockResolvedValueOnce([
|
387
|
+
{
|
388
|
+
id: '2',
|
389
|
+
name: 'Folder 2',
|
390
|
+
type: { code: 'folder', name: 'Folder' },
|
391
|
+
status: { code: 'live', name: 'Live' },
|
392
|
+
childCount: 1,
|
393
|
+
},
|
394
|
+
])
|
395
|
+
.mockResolvedValueOnce([
|
396
|
+
{
|
397
|
+
id: '3',
|
398
|
+
name: 'My image',
|
399
|
+
type: { code: 'image', name: 'Image' },
|
400
|
+
status: { code: 'live', name: 'Live' },
|
401
|
+
childCount: 0,
|
402
|
+
},
|
403
|
+
]);
|
404
|
+
const onRequestResource = jest.fn().mockResolvedValue({
|
405
|
+
id: '3',
|
406
|
+
name: 'My image',
|
407
|
+
type: { code: 'image', name: 'Image' },
|
408
|
+
status: { code: 'live', name: 'Live' },
|
409
|
+
childCount: '1',
|
410
|
+
});
|
411
|
+
|
412
|
+
render(
|
413
|
+
<ResourceBrowserContext.Provider value={{ onRequestSources, onRequestChildren, onRequestResource }}>
|
414
|
+
<Editor />
|
415
|
+
</ResourceBrowserContext.Provider>,
|
416
|
+
);
|
417
|
+
|
418
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Link (cmd+K)' })));
|
419
|
+
await act(() => fireEvent.click(screen.getByText('Choose asset')));
|
420
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Drill down to My Matrix instance children' })));
|
421
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Drill down to Folder 1 children' })));
|
422
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Drill down to Folder 2 children' })));
|
423
|
+
await act(() => fireEvent.blur(screen.getByRole('button', { name: /My image/ })));
|
424
|
+
|
425
|
+
// Contents of "Folder 2" should be mounted.
|
426
|
+
// Toolbar (Link button) should be mounted.
|
427
|
+
// Toolbar should be shown (.show-toolbar class).
|
428
|
+
expect(screen.getByRole('button', { name: /My image/ })).toBeInTheDocument();
|
429
|
+
expect(within(document.body).getByRole('button', { name: 'Link (cmd+K)', hidden: true })).toBeInTheDocument();
|
430
|
+
expect(document.querySelector('.show-toolbar')).toBeInTheDocument();
|
431
|
+
});
|
357
432
|
});
|
package/src/Editor/Editor.tsx
CHANGED
@@ -7,6 +7,7 @@ import { Toolbar, FloatingToolbar } from '../EditorToolbar';
|
|
7
7
|
import { EditorContext } from './EditorContext';
|
8
8
|
import { createExtensions } from '../Extensions/Extensions';
|
9
9
|
import useFocus from '../hooks/useFocus';
|
10
|
+
import { ResourceBrowserContext } from '@squiz/resource-browser';
|
10
11
|
|
11
12
|
type EditorProps = {
|
12
13
|
content?: RemirrorContentType;
|
@@ -34,7 +35,7 @@ const WrappedEditor = () => {
|
|
34
35
|
|
35
36
|
const Editor = ({ content, editable = true, onChange, children, isFocused }: EditorProps) => {
|
36
37
|
const { manager, state, setState } = useRemirror({
|
37
|
-
extensions: createExtensions(useContext(EditorContext)),
|
38
|
+
extensions: createExtensions(useContext(EditorContext), useContext(ResourceBrowserContext)),
|
38
39
|
content,
|
39
40
|
selection: 'start',
|
40
41
|
stringHandler: 'html',
|
@@ -60,7 +61,7 @@ const Editor = ({ content, editable = true, onChange, children, isFocused }: Edi
|
|
60
61
|
ref={wrapperRef}
|
61
62
|
onBlur={handleBlur}
|
62
63
|
onFocusCapture={handleFocus}
|
63
|
-
className={clsx('
|
64
|
+
className={clsx('formatted-text-editor', !editable && 'formatted-text-editor--is-disabled')}
|
64
65
|
>
|
65
66
|
<Remirror
|
66
67
|
manager={manager}
|
@@ -12,6 +12,7 @@ import LinkButton from './Tools/Link/LinkButton';
|
|
12
12
|
import ImageButton from './Tools/Image/ImageButton';
|
13
13
|
import RemoveLinkButton from './Tools/Link/RemoveLinkButton';
|
14
14
|
import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton';
|
15
|
+
import ListButtons from './Tools/Lists/ListButtons';
|
15
16
|
import { useExtensionNames } from '../hooks';
|
16
17
|
|
17
18
|
type ToolbarProps = {
|
@@ -38,6 +39,7 @@ export const Toolbar = ({ isVisible }: ToolbarProps) => {
|
|
38
39
|
{extensionNames.italic && <ItalicButton />}
|
39
40
|
{extensionNames.underline && <UnderlineButton />}
|
40
41
|
{extensionNames.nodeFormatting && <TextAlignButtons />}
|
42
|
+
{extensionNames.listItem && <ListButtons />}
|
41
43
|
{extensionNames.link && (
|
42
44
|
<>
|
43
45
|
<LinkButton />
|
@@ -280,7 +280,12 @@ describe('ImageButton', () => {
|
|
280
280
|
content: [
|
281
281
|
{
|
282
282
|
type: 'assetImage',
|
283
|
-
attrs: {
|
283
|
+
attrs: {
|
284
|
+
matrixAssetId: 'image-resource-id',
|
285
|
+
matrixIdentifier,
|
286
|
+
matrixDomain,
|
287
|
+
src: 'https://default-resource/',
|
288
|
+
},
|
284
289
|
},
|
285
290
|
{ type: 'text', text: 'Some nonsense content here' },
|
286
291
|
],
|
@@ -339,7 +344,12 @@ describe('ImageButton', () => {
|
|
339
344
|
},
|
340
345
|
{
|
341
346
|
type: 'assetImage',
|
342
|
-
attrs: {
|
347
|
+
attrs: {
|
348
|
+
matrixAssetId: 'image-resource-id',
|
349
|
+
matrixIdentifier,
|
350
|
+
matrixDomain,
|
351
|
+
src: 'https://default-resource/',
|
352
|
+
},
|
343
353
|
},
|
344
354
|
{ type: 'text', text: ' nonsense' },
|
345
355
|
],
|
@@ -274,7 +274,13 @@ describe('LinkButton', () => {
|
|
274
274
|
marks: [
|
275
275
|
{
|
276
276
|
type: 'assetLink',
|
277
|
-
attrs: {
|
277
|
+
attrs: {
|
278
|
+
matrixAssetId: 'my-resource-id',
|
279
|
+
target: '_self',
|
280
|
+
matrixDomain,
|
281
|
+
matrixIdentifier,
|
282
|
+
href: 'https://default-resource/',
|
283
|
+
},
|
278
284
|
},
|
279
285
|
],
|
280
286
|
},
|
@@ -328,7 +334,13 @@ describe('LinkButton', () => {
|
|
328
334
|
marks: [
|
329
335
|
{
|
330
336
|
type: 'assetLink',
|
331
|
-
attrs: {
|
337
|
+
attrs: {
|
338
|
+
matrixAssetId: 'my-resource-id',
|
339
|
+
target: '_blank',
|
340
|
+
matrixDomain,
|
341
|
+
matrixIdentifier,
|
342
|
+
href: 'https://default-resource/',
|
343
|
+
},
|
332
344
|
},
|
333
345
|
],
|
334
346
|
},
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import OrderedListButton from './OrderedList/OrderedListButton';
|
3
|
+
import UnorderedListButton from './UnorderedList/UnorderedListButton';
|
4
|
+
import { VerticalDivider } from '@remirror/react-components';
|
5
|
+
|
6
|
+
const ListButtons = () => (
|
7
|
+
<>
|
8
|
+
<UnorderedListButton />
|
9
|
+
<OrderedListButton />
|
10
|
+
<VerticalDivider />
|
11
|
+
</>
|
12
|
+
);
|
13
|
+
|
14
|
+
export default ListButtons;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, fireEvent } from '@testing-library/react';
|
3
|
+
import Editor from '../../../../Editor/Editor';
|
4
|
+
import React from 'react';
|
5
|
+
|
6
|
+
describe('Ordered list button', () => {
|
7
|
+
it('Renders the ordered list button', () => {
|
8
|
+
const { baseElement, getByRole } = render(<Editor />);
|
9
|
+
expect(baseElement).toBeTruthy();
|
10
|
+
expect(getByRole('button', { name: 'Ordered list' })).toBeTruthy();
|
11
|
+
});
|
12
|
+
|
13
|
+
it('Applies ordered list after selecting ordered list button', () => {
|
14
|
+
const { baseElement, getByRole } = render(<Editor />);
|
15
|
+
expect(baseElement).toBeTruthy();
|
16
|
+
|
17
|
+
const OrderedListButton = getByRole('button', { name: 'Ordered list' });
|
18
|
+
expect(OrderedListButton).toBeTruthy();
|
19
|
+
expect(OrderedListButton.classList.contains('is-active')).toBeFalsy();
|
20
|
+
|
21
|
+
fireEvent.click(OrderedListButton);
|
22
|
+
|
23
|
+
setTimeout(() => {
|
24
|
+
expect(OrderedListButton.classList.contains('is-active')).toBeTruthy();
|
25
|
+
}, 50);
|
26
|
+
});
|
27
|
+
|
28
|
+
it('Should apply order list to editor after clicking button', () => {
|
29
|
+
const { baseElement, getByRole } = render(<Editor />);
|
30
|
+
expect(baseElement).toBeTruthy();
|
31
|
+
expect(baseElement.querySelector('ol')).toBeFalsy();
|
32
|
+
|
33
|
+
const OrderedListButton = getByRole('button', { name: 'Ordered list' });
|
34
|
+
expect(OrderedListButton).toBeTruthy();
|
35
|
+
fireEvent.click(OrderedListButton);
|
36
|
+
|
37
|
+
expect(baseElement.querySelector('ol')).toBeTruthy();
|
38
|
+
});
|
39
|
+
});
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { useCommands, useActive, useChainedCommands } from '@remirror/react';
|
3
|
+
import { OrderedListExtension } from '@remirror/extension-list';
|
4
|
+
import Button from '../../../../ui/Button/Button';
|
5
|
+
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
|
6
|
+
|
7
|
+
const OrderedListButton = () => {
|
8
|
+
const { toggleOrderedList } = useCommands();
|
9
|
+
const chain = useChainedCommands();
|
10
|
+
|
11
|
+
const active = useActive<OrderedListExtension>();
|
12
|
+
const enabled = toggleOrderedList.enabled();
|
13
|
+
const handleSelect = () => {
|
14
|
+
if (toggleOrderedList.enabled()) {
|
15
|
+
chain.toggleOrderedList().focus().run();
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
return (
|
20
|
+
<Button
|
21
|
+
handleOnClick={handleSelect}
|
22
|
+
isDisabled={!enabled}
|
23
|
+
isActive={active.orderedList()}
|
24
|
+
icon={<FormatListNumberedIcon />}
|
25
|
+
label="Ordered list"
|
26
|
+
/>
|
27
|
+
);
|
28
|
+
};
|
29
|
+
|
30
|
+
export default OrderedListButton;
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import '@testing-library/jest-dom';
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
4
|
+
import Editor from '../../../../Editor/Editor';
|
5
|
+
|
6
|
+
describe('Unordered list button', () => {
|
7
|
+
it('Renders the unordered list button', () => {
|
8
|
+
render(<Editor />);
|
9
|
+
expect(screen.getByRole('button', { name: 'Unordered list' })).toBeInTheDocument();
|
10
|
+
});
|
11
|
+
|
12
|
+
it('Activates the button if clicked', () => {
|
13
|
+
render(<Editor />);
|
14
|
+
expect(screen.getByRole('button', { name: 'Unordered list' }).classList.contains('squiz-fte-btn')).toBeTruthy();
|
15
|
+
const ul = screen.getByRole('button', { name: 'Unordered list' });
|
16
|
+
fireEvent.click(ul);
|
17
|
+
expect(ul.classList.contains('squiz-fte-btn--is-active')).toBeTruthy();
|
18
|
+
});
|
19
|
+
});
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { useCommands, useActive, useChainedCommands } from '@remirror/react';
|
3
|
+
import { BulletListExtension } from 'remirror/extensions';
|
4
|
+
import Button from '../../../../ui/Button/Button';
|
5
|
+
import FormatListBulletedRoundedIcon from '@mui/icons-material/FormatListBulletedRounded';
|
6
|
+
|
7
|
+
const UnorderedListButton = () => {
|
8
|
+
const { toggleBulletList } = useCommands();
|
9
|
+
const chain = useChainedCommands();
|
10
|
+
|
11
|
+
const active = useActive<BulletListExtension>();
|
12
|
+
const enabled = toggleBulletList.enabled();
|
13
|
+
const handleSelect = () => {
|
14
|
+
if (toggleBulletList.enabled()) {
|
15
|
+
chain.toggleBulletList().focus().run();
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
return (
|
20
|
+
<Button
|
21
|
+
handleOnClick={handleSelect}
|
22
|
+
isDisabled={!enabled}
|
23
|
+
isActive={active.bulletList()}
|
24
|
+
icon={<FormatListBulletedRoundedIcon />}
|
25
|
+
label="Unordered list"
|
26
|
+
/>
|
27
|
+
);
|
28
|
+
};
|
29
|
+
|
30
|
+
export default UnorderedListButton;
|
@@ -6,6 +6,10 @@ import {
|
|
6
6
|
ParagraphExtension,
|
7
7
|
UnderlineExtension,
|
8
8
|
HistoryExtension,
|
9
|
+
OrderedListExtension,
|
10
|
+
BulletListExtension,
|
11
|
+
ListItemExtension,
|
12
|
+
PlaceholderExtension,
|
9
13
|
} from 'remirror/extensions';
|
10
14
|
import { Extension } from '@remirror/core';
|
11
15
|
import { PreformattedExtension } from './PreformattedExtension/PreformattedExtension';
|
@@ -18,7 +22,8 @@ import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
|
|
18
22
|
import { ExtendedCodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
|
19
23
|
import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
|
20
24
|
import { UnsupportedNodeExtension } from './UnsuportedExtension/UnsupportedNodeExtension';
|
21
|
-
|
25
|
+
import { ResourceBrowserContextProps } from '@squiz/resource-browser';
|
26
|
+
import { FetchUrlExtension } from './FetchUrlExtension/FetchUrlExtension';
|
22
27
|
export enum NodeName {
|
23
28
|
Image = 'image',
|
24
29
|
CodeBlock = 'codeBlock',
|
@@ -32,7 +37,7 @@ export enum MarkName {
|
|
32
37
|
AssetLink = 'assetLink',
|
33
38
|
}
|
34
39
|
|
35
|
-
export const createExtensions = (context: EditorContextOptions) => {
|
40
|
+
export const createExtensions = (context: EditorContextOptions, browserContext: ResourceBrowserContextProps) => {
|
36
41
|
return (): Extension[] => {
|
37
42
|
return [
|
38
43
|
new CommandsExtension(),
|
@@ -56,6 +61,13 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
56
61
|
}),
|
57
62
|
new UnsupportedNodeExtension(),
|
58
63
|
new ClearFormattingExtension(),
|
64
|
+
new BulletListExtension(),
|
65
|
+
new ListItemExtension(),
|
66
|
+
new OrderedListExtension(),
|
67
|
+
new PlaceholderExtension(),
|
68
|
+
new FetchUrlExtension({
|
69
|
+
fetchUrl: browserContext.onRequestResource,
|
70
|
+
}),
|
59
71
|
];
|
60
72
|
};
|
61
73
|
};
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import { extension, PlainExtension, Mark } from '@remirror/core';
|
2
|
+
import { Dispose, EditorView } from '@remirror/core-types';
|
3
|
+
import { Resource } from '@squiz/resource-browser';
|
4
|
+
import { MarkName, NodeName } from '../Extensions';
|
5
|
+
|
6
|
+
export type FetchUrlOptions = {
|
7
|
+
fetchUrl?: (params: { resource: string; source: string }) => Promise<any>;
|
8
|
+
};
|
9
|
+
|
10
|
+
@extension<FetchUrlOptions>({
|
11
|
+
defaultOptions: {
|
12
|
+
fetchUrl: () => Promise.resolve(),
|
13
|
+
},
|
14
|
+
})
|
15
|
+
export class FetchUrlExtension extends PlainExtension<FetchUrlOptions> {
|
16
|
+
get name() {
|
17
|
+
return 'fetchUrl';
|
18
|
+
}
|
19
|
+
|
20
|
+
// Implement the `onView` lifecycle method to scan for `[fetch]` and replace attributes
|
21
|
+
onView(view: EditorView): Dispose | void {
|
22
|
+
const { state } = view;
|
23
|
+
const { tr } = state;
|
24
|
+
|
25
|
+
const promises: Promise<void>[] = [];
|
26
|
+
|
27
|
+
state.doc.descendants((node, pos) => {
|
28
|
+
if (node.type.name === NodeName.AssetImage && node.attrs.src === '') {
|
29
|
+
promises.push(
|
30
|
+
this.fetchAndReplace(node.attrs.matrixAssetId, node.attrs.matrixIdentifier, (url: string) => {
|
31
|
+
const newNode = state.schema.nodes[NodeName.AssetImage].create({ ...node.attrs, src: url });
|
32
|
+
tr.replaceWith(pos, pos + node.nodeSize, newNode);
|
33
|
+
}),
|
34
|
+
);
|
35
|
+
}
|
36
|
+
|
37
|
+
const assetLinkMark = this.findAssetLinkMark(node.marks as Mark[]);
|
38
|
+
if (node.type.name === 'text' && assetLinkMark) {
|
39
|
+
promises.push(
|
40
|
+
this.fetchAndReplace(
|
41
|
+
assetLinkMark.attrs.matrixAssetId,
|
42
|
+
assetLinkMark.attrs.matrixIdentifier,
|
43
|
+
(url: string) => {
|
44
|
+
const updatedMark = assetLinkMark.type.create({ ...assetLinkMark.attrs, href: url });
|
45
|
+
tr.addMark(pos, pos + node.nodeSize, updatedMark);
|
46
|
+
},
|
47
|
+
),
|
48
|
+
);
|
49
|
+
}
|
50
|
+
});
|
51
|
+
|
52
|
+
if (promises.length) {
|
53
|
+
Promise.all(promises).then(() => {
|
54
|
+
view.dispatch(tr);
|
55
|
+
});
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
private findAssetLinkMark(marks: Mark[]): Mark | undefined {
|
60
|
+
return marks.find((mark) => mark.type.name === MarkName.AssetLink && mark.attrs.href === '');
|
61
|
+
}
|
62
|
+
|
63
|
+
private fetchAndReplace(resource: string, source: string, onFetched: (url: string) => void): Promise<void> {
|
64
|
+
return this.options
|
65
|
+
.fetchUrl({ resource, source })
|
66
|
+
.then((asset: Resource) => {
|
67
|
+
onFetched(asset.url);
|
68
|
+
})
|
69
|
+
.catch((error) => {
|
70
|
+
console.error('Error fetching URL:', error);
|
71
|
+
});
|
72
|
+
}
|
73
|
+
}
|
@@ -20,6 +20,7 @@ describe('AssetImageExtension', () => {
|
|
20
20
|
matrixAssetId: '123',
|
21
21
|
matrixDomain: 'https://matrix-domain.squiz.net',
|
22
22
|
matrixIdentifier: 'matrix-api-identifier',
|
23
|
+
src: 'https://default-resource/',
|
23
24
|
},
|
24
25
|
},
|
25
26
|
],
|
@@ -64,7 +65,7 @@ describe('AssetImageExtension', () => {
|
|
64
65
|
|
65
66
|
expect(getHtmlContent()).toEqual(
|
66
67
|
'<img ' +
|
67
|
-
'src="https://
|
68
|
+
'src="https://default-resource/" ' +
|
68
69
|
'data-matrix-asset-id="123" ' +
|
69
70
|
'data-matrix-identifier="matrix-api-identifier" ' +
|
70
71
|
'data-matrix-domain="https://matrix-domain.squiz.net" ' +
|
@@ -12,7 +12,6 @@ import {
|
|
12
12
|
CommandFunction,
|
13
13
|
} from '@remirror/core';
|
14
14
|
import { getTextSelection } from 'remirror';
|
15
|
-
import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
|
16
15
|
import { NodeName } from '../Extensions';
|
17
16
|
|
18
17
|
export type AssetImageOptions = {
|
@@ -23,6 +22,7 @@ export type AssetImageAttributes = {
|
|
23
22
|
matrixAssetId: string;
|
24
23
|
matrixIdentifier: string;
|
25
24
|
matrixDomain: string;
|
25
|
+
url: string;
|
26
26
|
};
|
27
27
|
|
28
28
|
@extension<AssetImageOptions>({
|
@@ -51,6 +51,7 @@ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
|
|
51
51
|
matrixAssetId: {},
|
52
52
|
matrixIdentifier: {},
|
53
53
|
matrixDomain: { default: this.options.matrixDomain },
|
54
|
+
src: { default: '' },
|
54
55
|
},
|
55
56
|
parseDOM: [
|
56
57
|
{
|
@@ -78,14 +79,13 @@ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
|
|
78
79
|
},
|
79
80
|
],
|
80
81
|
toDOM: (node) => {
|
81
|
-
const { matrixAssetId, matrixIdentifier, matrixDomain, ...rest } = omitExtraAttributes(node.attrs, extra);
|
82
|
-
|
82
|
+
const { matrixAssetId, matrixIdentifier, matrixDomain, src, ...rest } = omitExtraAttributes(node.attrs, extra);
|
83
83
|
return [
|
84
84
|
'img',
|
85
85
|
{
|
86
86
|
...extra.dom(node),
|
87
87
|
...rest,
|
88
|
-
src
|
88
|
+
src,
|
89
89
|
'data-matrix-asset-id': matrixAssetId,
|
90
90
|
'data-matrix-identifier': matrixIdentifier,
|
91
91
|
'data-matrix-domain': matrixDomain,
|
@@ -99,7 +99,7 @@ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
|
|
99
99
|
insertAssetImage(attrs: AssetImageAttributes): CommandFunction {
|
100
100
|
return ({ tr, dispatch }) => {
|
101
101
|
const { from, to } = getTextSelection(tr.selection, tr.doc);
|
102
|
-
const node = this.type.create(attrs);
|
102
|
+
const node = this.type.create({ ...attrs, src: attrs.url });
|
103
103
|
|
104
104
|
dispatch?.(tr.replaceRangeWith(from, to, node));
|
105
105
|
|
@@ -23,6 +23,7 @@ describe('AssetLinkExtension', () => {
|
|
23
23
|
{
|
24
24
|
type: 'assetLink',
|
25
25
|
attrs: {
|
26
|
+
href: 'https://default-resource/',
|
26
27
|
matrixAssetId: '123',
|
27
28
|
matrixDomain: 'https://matrix-domain.squiz.net',
|
28
29
|
matrixIdentifier: 'matrix-api-identifier',
|
@@ -78,6 +79,7 @@ describe('AssetLinkExtension', () => {
|
|
78
79
|
{
|
79
80
|
type: 'assetLink',
|
80
81
|
attrs: {
|
82
|
+
href: 'https://default-resource/',
|
81
83
|
matrixAssetId: '123',
|
82
84
|
matrixDomain: 'https://matrix-domain.squiz.net',
|
83
85
|
matrixIdentifier: 'matrix-api-identifier',
|
@@ -92,7 +94,7 @@ describe('AssetLinkExtension', () => {
|
|
92
94
|
|
93
95
|
expect(getHtmlContent()).toEqual(
|
94
96
|
'<a rel="noopener noreferrer nofollow" ' +
|
95
|
-
'href="https://
|
97
|
+
'href="https://default-resource/" ' +
|
96
98
|
'target="_blank" ' +
|
97
99
|
'data-matrix-asset-id="123" ' +
|
98
100
|
'data-matrix-identifier="matrix-api-identifier" ' +
|
@@ -10,13 +10,14 @@ import { command, CommandFunction, extension, MarkExtension, omitExtraAttributes
|
|
10
10
|
import { LinkTarget, validateTarget } from './common';
|
11
11
|
import { CommandsExtension } from '../CommandsExtension/CommandsExtension';
|
12
12
|
import { MarkName } from '../Extensions';
|
13
|
-
import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
|
14
13
|
|
15
14
|
export type AssetLinkAttributes = {
|
16
15
|
matrixAssetId: string;
|
17
16
|
matrixIdentifier: string;
|
18
17
|
matrixDomain: string;
|
19
18
|
target: LinkTarget;
|
19
|
+
href: string;
|
20
|
+
url: string;
|
20
21
|
};
|
21
22
|
|
22
23
|
export type AssetLinkOptions = {
|
@@ -53,6 +54,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
|
|
53
54
|
...extra.defaults(),
|
54
55
|
matrixAssetId: {},
|
55
56
|
matrixIdentifier: {},
|
57
|
+
href: { default: '' },
|
56
58
|
matrixDomain: { default: this.options.matrixDomain },
|
57
59
|
target: { default: this.options.defaultTarget },
|
58
60
|
},
|
@@ -87,7 +89,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
|
|
87
89
|
},
|
88
90
|
],
|
89
91
|
toDOM: (node) => {
|
90
|
-
const { matrixAssetId, matrixIdentifier, matrixDomain, target, ...rest } = omitExtraAttributes(
|
92
|
+
const { matrixAssetId, matrixIdentifier, matrixDomain, target, href, ...rest } = omitExtraAttributes(
|
91
93
|
node.attrs,
|
92
94
|
extra,
|
93
95
|
);
|
@@ -96,7 +98,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
|
|
96
98
|
...extra.dom(node),
|
97
99
|
...rest,
|
98
100
|
rel,
|
99
|
-
href
|
101
|
+
href,
|
100
102
|
target: validateTarget(target, this.options.supportedTargets, this.options.defaultTarget),
|
101
103
|
'data-matrix-asset-id': matrixAssetId,
|
102
104
|
'data-matrix-identifier': matrixIdentifier,
|
@@ -111,14 +113,13 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
|
|
111
113
|
@command()
|
112
114
|
updateAssetLink({ text, attrs, range }: UpdateAssetLinkProps): CommandFunction {
|
113
115
|
return this.store.getExtension(CommandsExtension).updateMark({
|
114
|
-
attrs,
|
116
|
+
attrs: { ...attrs, href: attrs.url },
|
115
117
|
text,
|
116
118
|
range,
|
117
119
|
mark: this.type,
|
118
120
|
removeMark: !attrs.matrixAssetId,
|
119
121
|
});
|
120
122
|
}
|
121
|
-
|
122
123
|
@command()
|
123
124
|
removeAssetLink(): CommandFunction {
|
124
125
|
return removeMark({ type: this.type });
|