@squiz/formatted-text-editor 1.70.0 → 2.0.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 (44) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/demo/App.tsx +7 -1
  3. package/demo/AppContext.tsx +49 -18
  4. package/demo/resources.json +44 -0
  5. package/demo/sources.json +4 -0
  6. package/lib/Editor/Editor.js +8 -3
  7. package/lib/Editor/EditorContext.d.ts +2 -0
  8. package/lib/Editor/EditorContext.js +3 -0
  9. package/lib/Extensions/Extensions.d.ts +3 -2
  10. package/lib/Extensions/Extensions.js +4 -2
  11. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.d.ts +2 -4
  12. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +7 -7
  13. package/lib/index.d.ts +2 -0
  14. package/lib/types.d.ts +4 -0
  15. package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +7 -7
  16. package/lib/utils/converters/htmlToSquizNode/htmlToSquizNode.js +2 -9
  17. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +19 -4
  18. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +23 -0
  19. package/package.json +11 -10
  20. package/src/Editor/Editor.spec.tsx +14 -4
  21. package/src/Editor/Editor.tsx +9 -3
  22. package/src/Editor/EditorContext.spec.tsx +1 -0
  23. package/src/Editor/EditorContext.ts +5 -0
  24. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +5 -3
  25. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +6 -0
  26. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +19 -7
  27. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +7 -1
  28. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +3 -3
  29. package/src/Extensions/Extensions.ts +4 -3
  30. package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +12 -17
  31. package/src/Extensions/UnsuportedExtension/UnsupportedNodeExtension.spec.ts +1 -1
  32. package/src/index.ts +2 -0
  33. package/src/types.ts +3 -0
  34. package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +26 -9
  35. package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +8 -8
  36. package/src/utils/converters/htmlToSquizNode/htmlToSquizNode.ts +6 -16
  37. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +457 -0
  38. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +24 -5
  39. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +210 -0
  40. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +23 -0
  41. package/tests/mockResourceBrowserContext.tsx +48 -12
  42. package/tests/renderWithContext.tsx +31 -3
  43. package/tests/renderWithEditor.tsx +1 -3
  44. package/vite.config.ts +2 -2
@@ -665,4 +665,214 @@ describe('squizNodeToRemirrorNode', () => {
665
665
  const result = squizNodeToRemirrorNode(squizComponentJSON);
666
666
  expect(result).toEqual(expected);
667
667
  });
668
+
669
+ it('should handle tables', () => {
670
+ const squizJSON: FormattedText = [
671
+ {
672
+ type: 'tag',
673
+ tag: 'table',
674
+ children: [
675
+ {
676
+ type: 'tag',
677
+ tag: 'tr',
678
+ children: [
679
+ {
680
+ type: 'tag',
681
+ tag: 'th',
682
+ children: [],
683
+ attributes: {
684
+ colspan: '1',
685
+ rowspan: '1',
686
+ tableControllerCell: 'true',
687
+ },
688
+ },
689
+ {
690
+ type: 'tag',
691
+ tag: 'th',
692
+ children: [],
693
+ attributes: {
694
+ colspan: '1',
695
+ rowspan: '1',
696
+ tableControllerCell: 'true',
697
+ colwidth: '366',
698
+ },
699
+ },
700
+ {
701
+ type: 'tag',
702
+ tag: 'th',
703
+ children: [],
704
+ attributes: {
705
+ colspan: '1',
706
+ rowspan: '1',
707
+ tableControllerCell: 'true',
708
+ },
709
+ },
710
+ ],
711
+ },
712
+ {
713
+ type: 'tag',
714
+ tag: 'tr',
715
+ children: [
716
+ {
717
+ type: 'tag',
718
+ tag: 'th',
719
+ children: [],
720
+ attributes: {
721
+ colspan: '1',
722
+ rowspan: '1',
723
+ tableControllerCell: 'true',
724
+ },
725
+ },
726
+ {
727
+ type: 'tag',
728
+ tag: 'td',
729
+ children: [
730
+ {
731
+ type: 'tag',
732
+ tag: 'p',
733
+ children: [],
734
+ },
735
+ ],
736
+ attributes: {
737
+ colspan: '1',
738
+ rowspan: '1',
739
+ colwidth: '366',
740
+ },
741
+ },
742
+ {
743
+ type: 'tag',
744
+ tag: 'td',
745
+ children: [
746
+ {
747
+ type: 'tag',
748
+ tag: 'p',
749
+ children: [],
750
+ },
751
+ ],
752
+ attributes: {
753
+ colspan: '1',
754
+ rowspan: '1',
755
+ },
756
+ },
757
+ ],
758
+ },
759
+ ],
760
+ },
761
+ ];
762
+
763
+ const expected: RemirrorJSON = {
764
+ type: 'doc',
765
+ content: [
766
+ {
767
+ type: 'table',
768
+ attrs: {
769
+ isControllersInjected: true,
770
+ },
771
+ content: [
772
+ {
773
+ type: 'tableRow',
774
+ attrs: {
775
+ nodeIndent: null,
776
+ nodeLineHeight: null,
777
+ nodeTextAlignment: null,
778
+ style: '',
779
+ },
780
+ content: [
781
+ {
782
+ type: 'tableControllerCell',
783
+ attrs: {
784
+ colspan: 1,
785
+ rowspan: 1,
786
+ colwidth: null,
787
+ background: null,
788
+ },
789
+ },
790
+ {
791
+ type: 'tableControllerCell',
792
+ attrs: {
793
+ colspan: 1,
794
+ rowspan: 1,
795
+ colwidth: [366],
796
+ background: null,
797
+ },
798
+ },
799
+ {
800
+ type: 'tableControllerCell',
801
+ attrs: {
802
+ colspan: 1,
803
+ rowspan: 1,
804
+ colwidth: null,
805
+ background: null,
806
+ },
807
+ },
808
+ ],
809
+ },
810
+ {
811
+ type: 'tableRow',
812
+ attrs: {
813
+ nodeIndent: null,
814
+ nodeLineHeight: null,
815
+ nodeTextAlignment: null,
816
+ style: '',
817
+ },
818
+ content: [
819
+ {
820
+ type: 'tableControllerCell',
821
+ attrs: {
822
+ colspan: 1,
823
+ rowspan: 1,
824
+ colwidth: null,
825
+ background: null,
826
+ },
827
+ },
828
+ {
829
+ type: 'tableCell',
830
+ attrs: {
831
+ colspan: 1,
832
+ rowspan: 1,
833
+ colwidth: [366],
834
+ background: null,
835
+ },
836
+ content: [
837
+ {
838
+ type: 'paragraph',
839
+ attrs: {
840
+ nodeIndent: null,
841
+ nodeTextAlignment: null,
842
+ nodeLineHeight: null,
843
+ style: '',
844
+ },
845
+ },
846
+ ],
847
+ },
848
+ {
849
+ type: 'tableCell',
850
+ attrs: {
851
+ colspan: 1,
852
+ rowspan: 1,
853
+ colwidth: null,
854
+ background: null,
855
+ },
856
+ content: [
857
+ {
858
+ type: 'paragraph',
859
+ attrs: {
860
+ nodeIndent: null,
861
+ nodeTextAlignment: null,
862
+ nodeLineHeight: null,
863
+ style: '',
864
+ },
865
+ },
866
+ ],
867
+ },
868
+ ],
869
+ },
870
+ ],
871
+ },
872
+ ],
873
+ };
874
+
875
+ const result = squizNodeToRemirrorNode(squizJSON);
876
+ expect(result).toEqual(expected);
877
+ });
668
878
  });
@@ -28,6 +28,10 @@ const getNodeType = (node: FormattedNodes): string => {
28
28
  li: 'listItem',
29
29
  ul: 'bulletList',
30
30
  hr: 'horizontalRule',
31
+ table: 'table',
32
+ tr: 'tableRow',
33
+ th: 'tableHeaderCell',
34
+ td: 'tableCell',
31
35
  a: NodeName.Text,
32
36
  em: NodeName.Text,
33
37
  span: NodeName.Text,
@@ -41,6 +45,12 @@ const getNodeType = (node: FormattedNodes): string => {
41
45
  }
42
46
 
43
47
  if (node.type === 'tag' && tagMap[node.tag]) {
48
+ // This is a specific check case for tables as there are some <th> tags which need to be returned
49
+ // as table controller cells.
50
+ if (node.attributes?.tableControllerCell) {
51
+ return 'tableControllerCell';
52
+ }
53
+ // Return regular tag for everything else
44
54
  return tagMap[node.tag];
45
55
  }
46
56
 
@@ -53,6 +63,19 @@ const getNodeType = (node: FormattedNodes): string => {
53
63
  };
54
64
 
55
65
  const getNodeAttributes = (node: FormattedNodes): Attrs => {
66
+ if (node.type === 'tag' && node.tag === 'table') {
67
+ return {
68
+ isControllersInjected: true,
69
+ };
70
+ }
71
+ if (node.type === 'tag' && (node.tag === 'th' || node.tag === 'td')) {
72
+ return {
73
+ colspan: parseInt(node.attributes?.colspan ?? '1'),
74
+ rowspan: parseInt(node.attributes?.rowspan ?? '1'),
75
+ colwidth: node.attributes?.colwidth ? [parseInt(node.attributes.colwidth)] : null,
76
+ background: null,
77
+ };
78
+ }
56
79
  if (node.type === 'tag' && node.tag === 'img') {
57
80
  return {
58
81
  alt: node.attributes?.alt,
@@ -1,11 +1,23 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { ResourceBrowserContext, Source, Resource, ResourceReference } from '@squiz/resource-browser';
2
+ import {
3
+ ResourceBrowserSource,
4
+ ResourceBrowserContextProvider,
5
+ ResourceBrowserPluginType,
6
+ } from '@squiz/resource-browser';
7
+ import MatrixResourceBrowserPlugin, {
8
+ Source,
9
+ Resource,
10
+ ResourceReference,
11
+ MatrixResourceBrowserPluginProps,
12
+ } from '@squiz/matrix-resource-browser-plugin';
13
+
3
14
  import { fireEvent, screen } from '@testing-library/react';
4
15
  import { DeepPartial } from '../src/types';
5
16
 
6
17
  export type MockResourceBrowserContextOptions = DeepPartial<{
7
18
  sources: Source[];
8
19
  resources: Resource[];
20
+ pluginProps?: MatrixResourceBrowserPluginProps;
9
21
  }>;
10
22
 
11
23
  export const mockResource = (resource: DeepPartial<Resource> = {}): Resource =>
@@ -23,6 +35,10 @@ export const mockResource = (resource: DeepPartial<Resource> = {}): Resource =>
23
35
  code: 'live',
24
36
  name: 'Live',
25
37
  },
38
+ source: {
39
+ id: '1',
40
+ type: 'matrix' as ResourceBrowserPluginType,
41
+ },
26
42
  ...resource,
27
43
  } as Resource);
28
44
 
@@ -33,29 +49,49 @@ export const mockSource = (source: DeepPartial<Source> = {}): Source => ({
33
49
  nodes: (source.nodes || [mockResource()]).map((resource) => mockResource(resource)),
34
50
  });
35
51
 
36
- export const mockResourceBrowserContext = ({ sources, resources }: MockResourceBrowserContextOptions) => {
52
+ export const mockResourceBrowserContext = ({ sources, resources, pluginProps }: MockResourceBrowserContextOptions) => {
37
53
  sources = (sources || []).map((source) => mockSource(source));
38
54
  resources = (resources || []).map((resource) => mockResource(resource));
39
55
 
40
- const onRequestSources = jest.fn().mockResolvedValue(sources);
41
- const onRequestChildren = jest.fn().mockResolvedValue(resources);
42
- const onRequestResource = jest
43
- .fn()
44
- .mockImplementation(
45
- (reference: ResourceReference) => resources?.find((resource) => resource.id === reference.resource) || null,
46
- );
56
+ const onRequestSources = pluginProps?.onRequestSources || jest.fn().mockResolvedValue(sources);
57
+ const onRequestChildren = pluginProps?.onRequestChildren || jest.fn().mockResolvedValue(resources);
58
+ const onRequestResource =
59
+ pluginProps?.onRequestResource ||
60
+ jest
61
+ .fn()
62
+ .mockImplementation((reference: ResourceReference) =>
63
+ Promise.resolve(resources?.find((resource) => resource.id === reference.resource) || null),
64
+ );
47
65
 
48
66
  return {
49
67
  MockResourceBrowserContext: ({ children }: { children: ReactNode }) => (
50
- <ResourceBrowserContext.Provider value={{ onRequestSources, onRequestChildren, onRequestResource }}>
68
+ <ResourceBrowserContextProvider
69
+ value={{
70
+ onRequestSources: (): Promise<ResourceBrowserSource[]> =>
71
+ Promise.resolve([
72
+ {
73
+ name: 'Matrix API',
74
+ id: 'matrix-api-identifier',
75
+ type: 'matrix',
76
+ },
77
+ ]),
78
+ plugins: [
79
+ MatrixResourceBrowserPlugin({
80
+ onRequestSources: onRequestSources,
81
+ onRequestChildren: onRequestChildren,
82
+ onRequestResource: onRequestResource,
83
+ } as MatrixResourceBrowserPluginProps),
84
+ ],
85
+ }}
86
+ >
51
87
  {children}
52
- </ResourceBrowserContext.Provider>
88
+ </ResourceBrowserContextProvider>
53
89
  ),
54
90
  selectResource: async (opener: HTMLElement, resourceName: string) => {
55
91
  const sourceLabel = `Drill down to ${sources?.[0]?.nodes?.[0]?.name} children`;
56
92
 
57
93
  fireEvent.click(opener);
58
- fireEvent.click(await screen.findByRole('button', { name: sourceLabel }));
94
+ fireEvent.click((await screen.findAllByRole('button', { name: sourceLabel }))[0]);
59
95
  fireEvent.click(await screen.findByTitle(resourceName));
60
96
  fireEvent.click(await screen.findByRole('button', { name: 'Select' }));
61
97
  },
@@ -4,7 +4,12 @@ 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';
7
+ import {
8
+ ResourceBrowserSource,
9
+ ResourceBrowserContextProvider,
10
+ ResourceBrowserPluginType,
11
+ } from '@squiz/resource-browser';
12
+ import MatrixResourceBrowserPlugin from '@squiz/matrix-resource-browser-plugin';
8
13
  import { mockSource } from './mockResourceBrowserContext';
9
14
 
10
15
  export type ContextRenderOptions = RenderOptions & {
@@ -31,17 +36,40 @@ export const renderWithContext = (ui: ReactElement | null, options?: ContextRend
31
36
  code: 'live',
32
37
  name: 'Live',
33
38
  },
39
+ source: {
40
+ id: '1',
41
+ type: 'matrix' as ResourceBrowserPluginType,
42
+ },
34
43
  },
35
44
  ];
36
45
  const onRequestSources = jest.fn().mockResolvedValue(sources);
37
46
  const onRequestChildren = jest.fn().mockResolvedValue(resources);
38
47
  const onRequestResource = jest.fn(() => Promise.resolve(resources[0]));
48
+ editorContext.resolveNodeToUrl = jest.fn(() => Promise.resolve(resources[0].url));
39
49
 
40
50
  return render(
41
51
  <EditorContext.Provider value={editorContext}>
42
- <ResourceBrowserContext.Provider value={{ onRequestSources, onRequestChildren, onRequestResource }}>
52
+ <ResourceBrowserContextProvider
53
+ value={{
54
+ onRequestSources: (): Promise<ResourceBrowserSource[]> =>
55
+ Promise.resolve([
56
+ {
57
+ name: 'Matrix',
58
+ id: 'matrixIdentifier',
59
+ type: 'matrix',
60
+ },
61
+ ]),
62
+ plugins: [
63
+ MatrixResourceBrowserPlugin({
64
+ onRequestSources: onRequestSources,
65
+ onRequestChildren: onRequestChildren,
66
+ onRequestResource: onRequestResource,
67
+ }),
68
+ ],
69
+ }}
70
+ >
43
71
  {ui}
44
- </ResourceBrowserContext.Provider>
72
+ </ResourceBrowserContextProvider>
45
73
  </EditorContext.Provider>,
46
74
  );
47
75
  };
@@ -1,7 +1,6 @@
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';
5
4
  import { BuiltinPreset } from 'remirror';
6
5
  import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
7
6
  import { RemirrorTestChain } from 'jest-remirror';
@@ -34,9 +33,8 @@ export type EditorRenderResult = {
34
33
 
35
34
  const TestEditor = ({ children, extensions, content, onReady, editable }: TestEditorProps) => {
36
35
  const context = useContext(EditorContext);
37
- const browserContext = useContext(ResourceBrowserContext);
38
36
  const { manager, state, setState } = useRemirror({
39
- extensions: () => extensions || createExtensions(context, browserContext)(),
37
+ extensions: () => extensions || createExtensions(context)(),
40
38
  content: content,
41
39
  selection: 'start',
42
40
  stringHandler: 'html',
package/vite.config.ts CHANGED
@@ -7,12 +7,12 @@ import react from '@vitejs/plugin-react';
7
7
  export default defineConfig({
8
8
  root: 'demo',
9
9
  optimizeDeps: {
10
- include: ['@squiz/resource-browser'],
10
+ include: [],
11
11
  },
12
12
  build: {
13
13
  outDir: 'build/demo',
14
14
  commonjsOptions: {
15
- include: [/node_modules/, /resource-browser/],
15
+ include: [/node_modules/],
16
16
  },
17
17
  },
18
18
  plugins: [