@squiz/formatted-text-editor 1.40.1-alpha.9 → 1.41.1-alpha.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 (56) hide show
  1. package/demo/AppContext.tsx +14 -3
  2. package/demo/resources.json +135 -3
  3. package/lib/Editor/Editor.js +3 -2
  4. package/lib/EditorToolbar/Toolbar.js +2 -3
  5. package/lib/EditorToolbar/Tools/Image/ImageButton.js +1 -1
  6. package/lib/EditorToolbar/Tools/Lists/ListButtons.d.ts +2 -0
  7. package/lib/EditorToolbar/Tools/Lists/ListButtons.js +14 -0
  8. package/lib/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.d.ts +2 -0
  9. package/lib/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.js +22 -0
  10. package/lib/EditorToolbar/Tools/{UnorderedList → Lists/UnorderedList}/UnorderedListButton.js +1 -1
  11. package/lib/Extensions/Extensions.d.ts +2 -1
  12. package/lib/Extensions/Extensions.js +7 -1
  13. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.d.ts +14 -0
  14. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +63 -0
  15. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +1 -0
  16. package/lib/Extensions/ImageExtension/AssetImageExtension.js +4 -4
  17. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +2 -0
  18. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +4 -4
  19. package/lib/hooks/useFocus.js +24 -5
  20. package/lib/index.css +9 -1
  21. package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +1 -0
  22. package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +1 -0
  23. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +2 -1
  24. package/package.json +4 -4
  25. package/src/Editor/Editor.spec.tsx +91 -16
  26. package/src/Editor/Editor.tsx +3 -2
  27. package/src/EditorToolbar/Toolbar.tsx +2 -3
  28. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +12 -2
  29. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +1 -1
  30. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +14 -2
  31. package/src/EditorToolbar/Tools/Lists/ListButtons.tsx +14 -0
  32. package/src/EditorToolbar/Tools/Lists/OrderedList/OrderListButton.spec.tsx +39 -0
  33. package/src/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.tsx +30 -0
  34. package/src/EditorToolbar/Tools/{UnorderedList → Lists/UnorderedList}/UnorderedList.spec.tsx +1 -1
  35. package/src/EditorToolbar/Tools/{UnorderedList → Lists/UnorderedList}/UnorderedListButton.tsx +1 -1
  36. package/src/Extensions/Extensions.ts +10 -2
  37. package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +73 -0
  38. package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +2 -1
  39. package/src/Extensions/ImageExtension/AssetImageExtension.ts +5 -5
  40. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +3 -1
  41. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +6 -5
  42. package/src/hooks/useFocus.ts +30 -7
  43. package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +1 -0
  44. package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +2 -0
  45. package/src/ui/_typography.scss +10 -1
  46. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +73 -0
  47. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +2 -1
  48. package/src/utils/getNodeNamesByGroup.spec.ts +1 -0
  49. package/tests/mockResourceBrowserContext.tsx +2 -2
  50. package/tests/renderWithContext.tsx +30 -1
  51. package/tests/renderWithEditor.tsx +18 -13
  52. package/lib/utils/resolveMatrixAssetUrl.d.ts +0 -1
  53. package/lib/utils/resolveMatrixAssetUrl.js +0 -10
  54. package/src/utils/resolveMatrixAssetUrl.spec.ts +0 -26
  55. package/src/utils/resolveMatrixAssetUrl.ts +0 -7
  56. /package/lib/EditorToolbar/Tools/{UnorderedList → Lists/UnorderedList}/UnorderedListButton.d.ts +0 -0
@@ -13,14 +13,37 @@ const useFocus = (
13
13
 
14
14
  const handleFocus = useCallback(() => {
15
15
  setIsVisible(true);
16
- }, []);
16
+ }, [wrapperRef]);
17
17
 
18
- const handleBlur: FocusEventHandler<HTMLDivElement> = (event: FocusEvent<HTMLDivElement>) => {
19
- const isOutside = wrapperRef.current !== null && !wrapperRef.current.contains(event.relatedTarget as Node);
20
- if (isOutside) {
21
- setIsVisible(false);
22
- }
23
- };
18
+ const handleBlur = useCallback(
19
+ (event: FocusEvent<HTMLDivElement>) => {
20
+ // React event bubbling is interesting, it bubbles up the React tree rather than the DOM tree.
21
+ // The tree deviates when rendering portals (eg. for modals).
22
+ //
23
+ // Only hide the toolbar if:
24
+ // 1. We are blurring a node in the editor **DOM** tree.
25
+ // 2. We are focusing on something that is not in the editor DOM tree
26
+ // (elements in the portal won't be in the tree but don't influence the focus state per #1).
27
+ //
28
+ // This avoids the scenario where an element in a portal is blurred and another one in the portal focused.
29
+ // Without this logic the blur and focus handlers are called (in that order). The impact of these handlers being
30
+ // called is that the "isFocused" state changes inconsistently. This state changing then causes subtle issues.
31
+ // eg. unable to drill down in resource browser, toolbar appearing/disappearing.
32
+ //
33
+ // Ideally we would instead solely seeing if the "relatedTarget" is in the React tree. This isn't easily
34
+ // identifiable however without reaching into React internals.
35
+ //
36
+ // An assumption here is that anything in a portal will only blur to another element that is also in the portal
37
+ // (and therefore still in our React tree resulting in the element still effectively being focused).
38
+ const isBlurringEditor = wrapperRef.current?.contains(event.target);
39
+ const isFocusedInEditor = wrapperRef.current?.contains(event.relatedTarget);
40
+
41
+ if (isBlurringEditor && !isFocusedInEditor) {
42
+ setIsVisible(false);
43
+ }
44
+ },
45
+ [wrapperRef],
46
+ );
24
47
 
25
48
  return { handleFocus, handleBlur, isVisible, wrapperRef };
26
49
  };
@@ -59,6 +59,7 @@ describe('MatrixAsset', () => {
59
59
  additional: 'additional data',
60
60
  matrixAssetId: 'my-resource-id',
61
61
  matrixIdentifier: 'my-source-id',
62
+ url: 'https://default-resource/',
62
63
  },
63
64
  },
64
65
  });
@@ -5,6 +5,7 @@ import { InputContainer, InputContainerProps } from '../InputContainer/InputCont
5
5
  type MatrixAssetValue = {
6
6
  matrixIdentifier?: string;
7
7
  matrixAssetId?: string;
8
+ url?: string;
8
9
  };
9
10
 
10
11
  export type MatrixAssetProps<T extends MatrixAssetValue> = Omit<InputContainerProps, 'children'> & {
@@ -45,6 +46,7 @@ export const MatrixAsset = <T extends MatrixAssetValue>({
45
46
  ...value,
46
47
  matrixIdentifier: reference?.source?.id,
47
48
  matrixAssetId: reference?.resource?.id,
49
+ url: reference?.resource?.url,
48
50
  } as T,
49
51
  },
50
52
  });
@@ -5,6 +5,10 @@
5
5
  text-decoration: underline;
6
6
  }
7
7
 
8
+ p {
9
+ margin-block-end: 0.8rem;
10
+ }
11
+
8
12
  h1 {
9
13
  font-size: 1.625rem;
10
14
  font-weight: 600;
@@ -73,9 +77,14 @@
73
77
  }
74
78
  }
75
79
 
76
- ul {
80
+ ul,
81
+ ol {
77
82
  list-style-type: disc;
78
83
  padding: 0 0 0 2.5rem;
79
84
  margin: 1rem 0;
80
85
  }
86
+
87
+ ol {
88
+ list-style-type: decimal;
89
+ }
81
90
  }
@@ -426,6 +426,79 @@ describe('squizNodeToRemirrorNode', () => {
426
426
  },
427
427
  );
428
428
 
429
+ it('should handle ordered lists', () => {
430
+ const squizComponentJSON: FormattedText = [
431
+ {
432
+ type: 'tag',
433
+ tag: 'ol',
434
+ children: [
435
+ {
436
+ type: 'tag',
437
+ tag: 'li',
438
+ children: [
439
+ {
440
+ type: 'tag',
441
+ tag: 'p',
442
+ children: [
443
+ {
444
+ type: 'text',
445
+ value: 'ddd',
446
+ },
447
+ ],
448
+ },
449
+ ],
450
+ },
451
+ ],
452
+ },
453
+ ];
454
+
455
+ const expected: RemirrorJSON = {
456
+ content: [
457
+ {
458
+ attrs: { level: undefined, nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
459
+ type: 'orderedList',
460
+ marks: undefined,
461
+ text: undefined,
462
+ content: [
463
+ {
464
+ attrs: { level: undefined, nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
465
+ type: 'listItem',
466
+ marks: undefined,
467
+ text: undefined,
468
+ content: [
469
+ {
470
+ attrs: {
471
+ level: undefined,
472
+ nodeIndent: null,
473
+ nodeLineHeight: null,
474
+ nodeTextAlignment: null,
475
+ style: '',
476
+ },
477
+ marks: undefined,
478
+ text: undefined,
479
+ type: 'paragraph',
480
+ content: [
481
+ {
482
+ attrs: undefined,
483
+ content: undefined,
484
+ marks: undefined,
485
+ text: 'ddd',
486
+ type: 'text',
487
+ },
488
+ ],
489
+ },
490
+ ],
491
+ },
492
+ ],
493
+ },
494
+ ],
495
+ type: 'doc',
496
+ };
497
+
498
+ const result = squizNodeToRemirrorNode(squizComponentJSON);
499
+ expect(result).toEqual(expected);
500
+ });
501
+
429
502
  it('should handle unordered lists', () => {
430
503
  const squizComponentJSON: FormattedText = [
431
504
  {
@@ -24,8 +24,9 @@ const getNodeType = (node: FormattedNodes): string => {
24
24
  img: 'image',
25
25
  pre: 'preformatted',
26
26
  p: 'paragraph',
27
- ul: 'bulletList',
27
+ ol: 'orderedList',
28
28
  li: 'listItem',
29
+ ul: 'bulletList',
29
30
  a: NodeName.Text,
30
31
  span: NodeName.Text,
31
32
  code: NodeName.CodeBlock,
@@ -24,6 +24,7 @@ describe('getNodeNamesByGroup', () => {
24
24
  'unsupportedNode',
25
25
  'bulletList',
26
26
  'listItem',
27
+ 'orderedList',
27
28
  ]);
28
29
  });
29
30
  });
@@ -8,7 +8,7 @@ export type MockResourceBrowserContextOptions = DeepPartial<{
8
8
  resources: Resource[];
9
9
  }>;
10
10
 
11
- const mockResource = (resource: DeepPartial<Resource> = {}): Resource =>
11
+ export const mockResource = (resource: DeepPartial<Resource> = {}): Resource =>
12
12
  ({
13
13
  id: 'default-resource',
14
14
  name: 'Default resource',
@@ -26,7 +26,7 @@ const mockResource = (resource: DeepPartial<Resource> = {}): Resource =>
26
26
  ...resource,
27
27
  } as Resource);
28
28
 
29
- const mockSource = (source: DeepPartial<Source>): Source => ({
29
+ export const mockSource = (source: DeepPartial<Source> = {}): Source => ({
30
30
  id: 'default-source',
31
31
  name: 'Default source',
32
32
  ...source,
@@ -4,6 +4,8 @@ import merge from 'deepmerge';
4
4
  import { EditorContext } from '../src';
5
5
  import { defaultEditorContext, EditorContextOptions } from '../src/Editor/EditorContext';
6
6
  import { DeepPartial } from '../src/types';
7
+ import { ResourceBrowserContext } from '@squiz/resource-browser';
8
+ import { mockSource } from './mockResourceBrowserContext';
7
9
 
8
10
  export type ContextRenderOptions = RenderOptions & {
9
11
  context?: DeepPartial<{
@@ -13,6 +15,33 @@ export type ContextRenderOptions = RenderOptions & {
13
15
 
14
16
  export const renderWithContext = (ui: ReactElement | null, options?: ContextRenderOptions): RenderResult => {
15
17
  const editorContext = merge(defaultEditorContext, options?.context?.editor || {}) as EditorContextOptions;
18
+ const sources = mockSource();
19
+ const resources = [
20
+ {
21
+ id: 'default-resource',
22
+ name: 'Default resource',
23
+ url: 'https://default-resource/',
24
+ urls: [],
25
+ childCount: 0,
26
+ type: {
27
+ code: 'unspecified',
28
+ name: 'Unspecified',
29
+ },
30
+ status: {
31
+ code: 'live',
32
+ name: 'Live',
33
+ },
34
+ },
35
+ ];
36
+ const onRequestSources = jest.fn().mockResolvedValue(sources);
37
+ const onRequestChildren = jest.fn().mockResolvedValue(resources);
38
+ const onRequestResource = jest.fn(() => Promise.resolve(resources[0]));
16
39
 
17
- return render(<EditorContext.Provider value={editorContext}>{ui}</EditorContext.Provider>);
40
+ return render(
41
+ <EditorContext.Provider value={editorContext}>
42
+ <ResourceBrowserContext.Provider value={{ onRequestSources, onRequestChildren, onRequestResource }}>
43
+ {ui}
44
+ </ResourceBrowserContext.Provider>
45
+ </EditorContext.Provider>,
46
+ );
18
47
  };
@@ -1,6 +1,7 @@
1
1
  import React, { ReactElement, useContext, useEffect } from 'react';
2
2
  import { Extension, RemirrorContentType, RemirrorManager } from '@remirror/core';
3
3
  import { CorePreset } from '@remirror/preset-core';
4
+ import { ResourceBrowserContext } from '@squiz/resource-browser';
4
5
  import { BuiltinPreset } from 'remirror';
5
6
  import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
6
7
  import { RemirrorTestChain } from 'jest-remirror';
@@ -8,6 +9,7 @@ import { createExtensions } from '../src/Extensions/Extensions';
8
9
  import { EditorContext } from '../src';
9
10
  import { FloatingToolbar } from '../src/EditorToolbar';
10
11
  import { renderWithContext, ContextRenderOptions } from './renderWithContext';
12
+ import { act } from '@testing-library/react';
11
13
 
12
14
  export type EditorRenderOptions = ContextRenderOptions & {
13
15
  content?: RemirrorContentType;
@@ -20,7 +22,7 @@ type TestEditorProps = EditorRenderOptions & {
20
22
  onReady: (manager: RemirrorManager<Extension>) => void;
21
23
  };
22
24
 
23
- type EditorRenderResult = {
25
+ export type EditorRenderResult = {
24
26
  editor: RemirrorTestChain<Extension | CorePreset | BuiltinPreset>;
25
27
  getHtmlContent: () => string | undefined;
26
28
  getJsonContent: () => any;
@@ -32,8 +34,9 @@ type EditorRenderResult = {
32
34
 
33
35
  const TestEditor = ({ children, extensions, content, onReady, editable }: TestEditorProps) => {
34
36
  const context = useContext(EditorContext);
37
+ const browserContext = useContext(ResourceBrowserContext);
35
38
  const { manager, state, setState } = useRemirror({
36
- extensions: () => extensions || createExtensions(context)(),
39
+ extensions: () => extensions || createExtensions(context, browserContext)(),
37
40
  content: content,
38
41
  selection: 'start',
39
42
  stringHandler: 'html',
@@ -95,17 +98,19 @@ export const renderWithEditor = async (
95
98
  };
96
99
  let isReady = false;
97
100
 
98
- const { container } = renderWithContext(
99
- <TestEditor
100
- onReady={(manager) => {
101
- result.editor = RemirrorTestChain.create(manager);
102
- isReady = true;
103
- }}
104
- {...options}
105
- >
106
- {ui}
107
- </TestEditor>,
108
- options,
101
+ const { container } = await act(() =>
102
+ renderWithContext(
103
+ <TestEditor
104
+ onReady={(manager) => {
105
+ result.editor = RemirrorTestChain.create(manager);
106
+ isReady = true;
107
+ }}
108
+ {...options}
109
+ >
110
+ {ui}
111
+ </TestEditor>,
112
+ options,
113
+ ),
109
114
  );
110
115
 
111
116
  if (!isReady) {
@@ -1 +0,0 @@
1
- export declare const resolveMatrixAssetUrl: (id: string, matrixDomain: string) => string;
@@ -1,10 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveMatrixAssetUrl = void 0;
4
- const resolveMatrixAssetUrl = (id, matrixDomain) => {
5
- if (matrixDomain.indexOf('://') < 0) {
6
- matrixDomain = `${window.location.protocol}//${matrixDomain}`;
7
- }
8
- return new URL(`/_nocache?a=${encodeURIComponent(id)}`, matrixDomain).toString();
9
- };
10
- exports.resolveMatrixAssetUrl = resolveMatrixAssetUrl;
@@ -1,26 +0,0 @@
1
- import { resolveMatrixAssetUrl } from './resolveMatrixAssetUrl';
2
-
3
- describe('resolveMatrixAssetUrl', () => {
4
- it.each([
5
- [
6
- 'domain with no scheme',
7
- '123:hello?.txt',
8
- 'matrix.labs.squiz.test',
9
- 'http://matrix.labs.squiz.test/_nocache?a=123%3Ahello%3F.txt',
10
- ],
11
- [
12
- 'domain with scheme',
13
- '123:hello?.txt',
14
- 'https://matrix.labs.squiz.test',
15
- 'https://matrix.labs.squiz.test/_nocache?a=123%3Ahello%3F.txt',
16
- ],
17
- [
18
- 'domain with path',
19
- '123:hello?.txt',
20
- 'https://matrix.labs.squiz.test/site-1',
21
- 'https://matrix.labs.squiz.test/_nocache?a=123%3Ahello%3F.txt',
22
- ],
23
- ])('Resolves to expected URL for %s', (description: string, id: string, matrixDomain: string, expected: string) => {
24
- expect(resolveMatrixAssetUrl(id, matrixDomain)).toBe(expected);
25
- });
26
- });
@@ -1,7 +0,0 @@
1
- export const resolveMatrixAssetUrl = (id: string, matrixDomain: string): string => {
2
- if (matrixDomain.indexOf('://') < 0) {
3
- matrixDomain = `${window.location.protocol}//${matrixDomain}`;
4
- }
5
-
6
- return new URL(`/_nocache?a=${encodeURIComponent(id)}`, matrixDomain).toString();
7
- };