@squiz/formatted-text-editor 2.4.0 → 2.6.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 (72) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/demo/{App.tsx → diff/App.tsx} +3 -7
  3. package/demo/{AppContext.tsx → diff/AppContext.tsx} +15 -10
  4. package/demo/diff/index.html +14 -0
  5. package/demo/{index.scss → diff/index.scss} +3 -0
  6. package/demo/{main.tsx → diff/main.tsx} +1 -1
  7. package/demo/index.html +47 -2
  8. package/demo/portals/Accordion.tsx +50 -0
  9. package/demo/portals/App.tsx +150 -0
  10. package/demo/portals/index.html +13 -0
  11. package/demo/portals/index.scss +8 -0
  12. package/demo/portals/index.tsx +12 -0
  13. package/demo/portals/preview.html +91 -0
  14. package/demo/portals/preview.tsx +10 -0
  15. package/lib/Editor/Editor.d.ts +11 -6
  16. package/lib/Editor/Editor.js +17 -26
  17. package/lib/EditorToolbar/Toolbar.d.ts +2 -1
  18. package/lib/EditorToolbar/Toolbar.js +4 -2
  19. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +0 -3
  20. package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.d.ts +13 -3
  21. package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.js +74 -8
  22. package/lib/Extensions/Extensions.d.ts +1 -1
  23. package/lib/Extensions/Extensions.js +3 -3
  24. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +5 -2
  25. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +8 -1
  26. package/lib/hooks/index.d.ts +3 -2
  27. package/lib/hooks/index.js +3 -2
  28. package/lib/hooks/useFocus/useFocus.d.ts +6 -0
  29. package/lib/hooks/{useFocus.js → useFocus/useFocus.js} +29 -15
  30. package/lib/index.css +164 -5
  31. package/lib/ui/EditorInput/EditorInput.d.ts +3 -0
  32. package/lib/ui/EditorInput/EditorInput.js +49 -0
  33. package/lib/ui/EditorInput/EditorInput.props.d.ts +4 -0
  34. package/lib/ui/EditorInput/EditorInput.props.js +2 -0
  35. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +0 -3
  36. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +0 -6
  37. package/package.json +5 -4
  38. package/src/Editor/Editor.spec.tsx +36 -10
  39. package/src/Editor/Editor.tsx +48 -44
  40. package/src/Editor/_editor.scss +4 -0
  41. package/src/EditorToolbar/Toolbar.tsx +8 -4
  42. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +0 -3
  43. package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.tsx +3 -3
  44. package/src/EditorToolbar/_toolbar.scss +3 -2
  45. package/src/Extensions/CodeBlockExtension/CodeBlockExtension.props.ts +3 -0
  46. package/src/Extensions/CodeBlockExtension/CodeBlockExtension.spec.ts +59 -0
  47. package/src/Extensions/CodeBlockExtension/CodeBlockExtension.ts +82 -7
  48. package/src/Extensions/Extensions.ts +4 -4
  49. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +15 -3
  50. package/src/hooks/index.ts +3 -2
  51. package/src/hooks/useFocus/useFocus.spec.tsx +48 -0
  52. package/src/hooks/useFocus/useFocus.ts +71 -0
  53. package/src/ui/EditorInput/EditorInput.props.ts +5 -0
  54. package/src/ui/EditorInput/EditorInput.spec.tsx +38 -0
  55. package/src/ui/EditorInput/EditorInput.tsx +30 -0
  56. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +1 -3
  57. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +0 -4
  58. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +1 -4
  59. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +0 -5
  60. package/tests/mockResourceBrowserContext.tsx +17 -1
  61. package/tests/renderWithContext.tsx +3 -0
  62. package/lib/hooks/useFocus.d.ts +0 -8
  63. package/src/hooks/useFocus.ts +0 -61
  64. /package/demo/{resources.json → diff/resources.json} +0 -0
  65. /package/demo/{sources.json → diff/sources.json} +0 -0
  66. /package/demo/{vite-env.d.ts → diff/vite-env.d.ts} +0 -0
  67. /package/lib/hooks/{useExpandedSelection.d.ts → useExpandedSelection/useExpandedSelection.d.ts} +0 -0
  68. /package/lib/hooks/{useExpandedSelection.js → useExpandedSelection/useExpandedSelection.js} +0 -0
  69. /package/lib/hooks/{useExtensionNames.d.ts → useExtensionNames/useExtensionNames.d.ts} +0 -0
  70. /package/lib/hooks/{useExtensionNames.js → useExtensionNames/useExtensionNames.js} +0 -0
  71. /package/src/hooks/{useExpandedSelection.ts → useExpandedSelection/useExpandedSelection.ts} +0 -0
  72. /package/src/hooks/{useExtensionNames.ts → useExtensionNames/useExtensionNames.ts} +0 -0
@@ -0,0 +1,4 @@
1
+ import { HTMLAttributes } from 'react';
2
+ export type EditorInputProps = HTMLAttributes<HTMLDivElement> & {
3
+ container?: Element | null;
4
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -8,9 +8,6 @@ const resolveNodeTag = (node) => {
8
8
  if (node.type.name === Extensions_1.NodeName.Text) {
9
9
  return 'span';
10
10
  }
11
- if (node.type.name === Extensions_1.NodeName.CodeBlock) {
12
- return 'code';
13
- }
14
11
  if (node.type.spec?.toDOM) {
15
12
  const domNode = node.type.spec.toDOM(node);
16
13
  if (domNode instanceof window.Node) {
@@ -75,12 +75,6 @@ const getNodeAttributes = (node) => {
75
75
  title: node.attributes?.title,
76
76
  };
77
77
  }
78
- else if (node.type === 'tag' && node.tag === 'code') {
79
- return {
80
- language: node.attributes?.language || 'markup',
81
- wrap: node.attributes?.wrap || true,
82
- };
83
- }
84
78
  else if (node.type === 'matrix-image') {
85
79
  return {
86
80
  matrixAssetId: node.matrixAssetId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -25,12 +25,12 @@
25
25
  "@mui/icons-material": "5.15.18",
26
26
  "@remirror/extension-react-tables": "^2.2.19",
27
27
  "@remirror/react": "2.0.35",
28
- "@squiz/dam-resource-browser-plugin": "^0.10.0",
28
+ "@squiz/dam-resource-browser-plugin": "^3.0.3-rc.0",
29
29
  "@squiz/dx-json-schema-lib": "^1.74.1",
30
30
  "@squiz/dxp-ai-client-react": "^0.2.0",
31
31
  "@squiz/dxp-content-tools-modal": "^0.3.2",
32
- "@squiz/matrix-resource-browser-plugin": "^2.0.0",
33
- "@squiz/resource-browser": "^2.0.0",
32
+ "@squiz/matrix-resource-browser-plugin": "^3.0.3-rc.0",
33
+ "@squiz/resource-browser": "^3.0.1-rc.0",
34
34
  "@squiz/sds": "^1.0.2",
35
35
  "clsx": "2.1.1",
36
36
  "react-hook-form": "7.51.4",
@@ -49,6 +49,7 @@
49
49
  "autoprefixer": "10.4.13",
50
50
  "cypress": "12.5.1",
51
51
  "deepmerge": "4.3.0",
52
+ "esbuild-sass-plugin": "^2.8.0",
52
53
  "eslint-plugin-cypress": "2.12.1",
53
54
  "eslint-plugin-jsx-a11y": "6.7.1",
54
55
  "eslint-plugin-react": "7.32.2",
@@ -5,7 +5,12 @@ import { MatrixResourceBrowserPluginProps } from '@squiz/matrix-resource-browser
5
5
  import Editor from './Editor';
6
6
  import { renderWithEditor, mockResourceBrowserContext } from '../../tests';
7
7
  import ImageButton from '../EditorToolbar/Tools/Image/ImageButton';
8
- import * as useFocus from '../hooks/useFocus';
8
+ import * as hooks from '../hooks';
9
+
10
+ jest.mock('../hooks', () => ({
11
+ __esModule: true,
12
+ ...jest.requireActual('../hooks'),
13
+ }));
9
14
 
10
15
  const handleFocusMock = jest.fn();
11
16
  const handleBlurMock = jest.fn();
@@ -341,11 +346,10 @@ describe('Formatted text editor', () => {
341
346
  });
342
347
 
343
348
  it('triggers handleFocus when editor is focused', async () => {
344
- jest.spyOn(useFocus, 'default').mockReturnValue({
345
- isVisible: false,
349
+ jest.spyOn(hooks, 'useFocus').mockReturnValue({
350
+ isFocused: false,
346
351
  handleFocus: handleFocusMock,
347
352
  handleBlur: handleBlurMock,
348
- wrapperRef: { current: null },
349
353
  });
350
354
 
351
355
  const { getByLabelText } = render(<Editor />);
@@ -358,11 +362,10 @@ describe('Formatted text editor', () => {
358
362
  });
359
363
 
360
364
  it('triggers handleBlur when editor is blurred', () => {
361
- jest.spyOn(useFocus, 'default').mockReturnValue({
362
- isVisible: false,
365
+ jest.spyOn(hooks, 'useFocus').mockReturnValue({
366
+ isFocused: false,
363
367
  handleFocus: handleFocusMock,
364
368
  handleBlur: handleBlurMock,
365
- wrapperRef: { current: null },
366
369
  });
367
370
 
368
371
  const { getByLabelText } = render(<Editor />);
@@ -375,11 +378,10 @@ describe('Formatted text editor', () => {
375
378
  });
376
379
 
377
380
  it('should apply hide class when focus hook returns false', () => {
378
- jest.spyOn(useFocus, 'default').mockReturnValue({
379
- isVisible: true,
381
+ jest.spyOn(hooks, 'useFocus').mockReturnValue({
382
+ isFocused: true,
380
383
  handleFocus: handleFocusMock,
381
384
  handleBlur: handleBlurMock,
382
- wrapperRef: { current: null },
383
385
  });
384
386
 
385
387
  const { container } = render(<Editor />);
@@ -439,6 +441,7 @@ describe('Formatted text editor', () => {
439
441
  onRequestSources,
440
442
  onRequestChildren,
441
443
  onRequestResource,
444
+ onSearchRequest: jest.fn(),
442
445
  } as MatrixResourceBrowserPluginProps,
443
446
  });
444
447
 
@@ -462,4 +465,27 @@ describe('Formatted text editor', () => {
462
465
  expect(within(document.body).getByRole('button', { name: 'Link (Ctrl+K)', hidden: true })).toBeInTheDocument();
463
466
  expect(document.querySelector('.show-toolbar')).toBeInTheDocument();
464
467
  });
468
+
469
+ it('Renders the input and toolbar in container elements if provided', () => {
470
+ const toolbar = document.createElement('div');
471
+ const input = document.createElement('div');
472
+ document.body.append(toolbar, input);
473
+
474
+ const { container, unmount } = render(<Editor containers={{ toolbar, input }} />);
475
+
476
+ // Toolbar and input should be rendered in the provided containers.
477
+ expect(within(input).getByRole('textbox')).toBeInTheDocument();
478
+ expect(within(toolbar).getByRole('button', { name: 'Bold (Ctrl+B)' })).toBeInTheDocument();
479
+
480
+ // They should not be rendered inside of the editor element.
481
+ expect(within(container).queryByRole('textbox')).not.toBeInTheDocument();
482
+ expect(within(container).queryByRole('button', { name: 'Bold (Ctrl+B)' })).not.toBeInTheDocument();
483
+ expect(container.querySelector('.squiz-fte-scope__editor--empty')).toBeInTheDocument();
484
+
485
+ unmount();
486
+
487
+ // Toolbar and input should be removed on unmount.
488
+ expect(within(input).queryByRole('textbox')).not.toBeInTheDocument();
489
+ expect(within(toolbar).queryByRole('button', { name: 'Bold (Ctrl+B)' })).not.toBeInTheDocument();
490
+ });
465
491
  });
@@ -1,61 +1,49 @@
1
- import React, { useContext, useCallback, ReactNode, useEffect } from 'react';
2
- import { EditorComponent, Remirror, useRemirror, useEditorEvent } from '@remirror/react';
1
+ import React, { useContext, useCallback, ReactNode, useEffect, useRef } from 'react';
2
+ import { Remirror, useRemirror } from '@remirror/react';
3
3
  import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
4
- import { ClipboardEventHandler } from '@remirror/extension-events/dist-types/events-extension';
5
4
  import clsx from 'clsx';
6
5
  import { Toolbar, FloatingToolbar } from '../EditorToolbar';
7
6
  import { EditorContext } from './EditorContext';
8
7
  import { createExtensions } from '../Extensions/Extensions';
9
- import useFocus from '../hooks/useFocus';
8
+ import { useFocus } from '../hooks';
10
9
  import { TableComponents } from '@remirror/extension-react-tables';
10
+ import { EditorInput } from '../ui/EditorInput/EditorInput';
11
11
 
12
12
  type EditorProps = {
13
+ attributes?: Record<string, string>;
14
+ border?: boolean;
15
+ children?: ReactNode;
13
16
  className?: string;
17
+ containers?: {
18
+ input?: Element | null;
19
+ toolbar?: Element | null;
20
+ };
14
21
  content?: RemirrorContentType;
15
- onChange?: RemirrorEventListener<Extension>;
16
22
  editable?: boolean;
17
- border?: boolean;
18
- children?: ReactNode;
23
+ enableDecorations?: boolean;
24
+ enableTableTool?: boolean;
19
25
  isFocused?: boolean;
20
26
  label?: string;
21
- attributes?: Record<string, string>;
22
- enableTableTool?: boolean;
23
- };
24
-
25
- const WrappedEditor = () => {
26
- const preventImagePaste = useCallback((event) => {
27
- const { clipboardData } = event;
28
- const pastedData = clipboardData?.files[0];
29
- if (
30
- pastedData?.type &&
31
- pastedData?.type.startsWith('image/') &&
32
- // Still allow paste of any text that came through (Word, etc)
33
- !clipboardData?.types.includes('text/plain')
34
- ) {
35
- event.preventDefault();
36
- }
37
-
38
- // Allow other paste event handlers to be run.
39
- return false;
40
- }, []) as ClipboardEventHandler;
41
-
42
- useEditorEvent('paste', preventImagePaste);
43
- return <EditorComponent />;
27
+ onChange?: RemirrorEventListener<Extension>;
44
28
  };
45
29
 
46
30
  const Editor = ({
47
- content,
48
- className,
31
+ attributes,
49
32
  border = true,
50
- editable = true,
51
- onChange,
52
33
  children,
53
- isFocused,
54
- attributes,
34
+ className,
35
+ containers,
36
+ content,
37
+ editable = true,
38
+ enableDecorations = true,
55
39
  enableTableTool = false,
40
+ isFocused: isInitiallyFocused = false,
41
+ label = 'Text editor',
42
+ onChange,
56
43
  }: EditorProps) => {
44
+ const isEmpty = containers?.toolbar && containers.input && !children;
57
45
  const { manager, state, setState } = useRemirror({
58
- extensions: createExtensions(useContext(EditorContext)),
46
+ extensions: createExtensions(useContext(EditorContext), enableDecorations),
59
47
  content,
60
48
  selection: 'start',
61
49
  stringHandler: 'html',
@@ -66,11 +54,24 @@ const Editor = ({
66
54
  onChange?.(parameter);
67
55
  };
68
56
 
69
- const { isVisible, handleFocus, handleBlur, wrapperRef } = useFocus(isFocused || false);
57
+ const wrapperRef = useRef<HTMLDivElement>(null);
58
+ const { isFocused, handleFocus, handleBlur } = useFocus(
59
+ isInitiallyFocused,
60
+ useCallback(
61
+ (element: Node) => {
62
+ return Boolean(
63
+ wrapperRef.current?.contains(element) ||
64
+ containers?.input?.contains(element) ||
65
+ containers?.toolbar?.contains(element),
66
+ );
67
+ },
68
+ [containers?.input, containers?.toolbar],
69
+ ),
70
+ );
70
71
 
71
72
  // On initial load, check if we need to focus the actual text content
72
73
  useEffect(() => {
73
- if (isFocused) {
74
+ if (isInitiallyFocused) {
74
75
  manager.view.dom.focus();
75
76
  }
76
77
 
@@ -85,13 +86,14 @@ const Editor = ({
85
86
  return (
86
87
  <div
87
88
  ref={wrapperRef}
88
- onBlur={handleBlur}
89
+ onBlurCapture={handleBlur}
89
90
  onFocusCapture={handleFocus}
90
91
  className={clsx(
91
92
  'squiz-fte-scope',
92
93
  'squiz-fte-scope__editor',
93
94
  !editable && 'squiz-fte-scope__editor--is-disabled',
94
95
  border && 'squiz-fte-scope__editor--bordered',
96
+ isEmpty && 'squiz-fte-scope__editor--empty',
95
97
  className,
96
98
  )}
97
99
  >
@@ -101,14 +103,16 @@ const Editor = ({
101
103
  editable={editable}
102
104
  onChange={handleChange}
103
105
  placeholder="Write something"
104
- label="Text editor"
106
+ label={label}
105
107
  attributes={attributes}
106
108
  >
107
- {editable && <Toolbar isVisible={isVisible} enableTableTool={enableTableTool} />}
109
+ {editable && (
110
+ <Toolbar isVisible={isFocused} enableTableTool={enableTableTool} container={containers?.toolbar} />
111
+ )}
108
112
  {children && <div className="squiz-fte-scope__editor__children">{children}</div>}
109
- <WrappedEditor />
113
+ <EditorInput container={containers?.input} />
110
114
  {enableTableTool && <TableComponents enableTableCellMenu={false} />}
111
- {editable && isVisible && <FloatingToolbar />}
115
+ {editable && isFocused && <FloatingToolbar />}
112
116
  </Remirror>
113
117
  </div>
114
118
  );
@@ -42,6 +42,10 @@
42
42
  @apply border-2 border-solid;
43
43
  }
44
44
 
45
+ &--empty {
46
+ @apply w-0 h-0 border-0;
47
+ }
48
+
45
49
  &--is-disabled {
46
50
  @apply bg-gray-50 border-0 cursor-not-allowed;
47
51
 
@@ -17,17 +17,19 @@ import HorizontalLineButton from './Tools/HorizontalLine/HorizontalLineButton';
17
17
  import TableButton from './Tools/Table/TableButton';
18
18
  import ContentToolsDropdown from './Tools/ContentTools/ContentToolsDropdown';
19
19
  import { useExtensionNames } from '../hooks';
20
+ import { createPortal } from 'react-dom';
20
21
 
21
22
  type ToolbarProps = {
22
23
  isVisible: boolean;
23
24
  enableTableTool: boolean;
25
+ container?: Element | null;
24
26
  };
25
- export const Toolbar = ({ isVisible, enableTableTool }: ToolbarProps) => {
26
- const extensionNames = useExtensionNames();
27
27
 
28
- return (
28
+ export const Toolbar = ({ isVisible, enableTableTool, container }: ToolbarProps) => {
29
+ const extensionNames = useExtensionNames();
30
+ const toolbar = (
29
31
  <RemirrorToolbar
30
- className={clsx('editor-toolbar header-toolbar', isVisible && 'show-toolbar')}
32
+ className={clsx('editor-toolbar header-toolbar', isVisible && 'show-toolbar', container && 'fte-portal-toolbar')}
31
33
  role="toolbar"
32
34
  tabIndex={0}
33
35
  >
@@ -59,4 +61,6 @@ export const Toolbar = ({ isVisible, enableTableTool }: ToolbarProps) => {
59
61
  </div>
60
62
  </RemirrorToolbar>
61
63
  );
64
+
65
+ return container ? createPortal(toolbar, container) : toolbar;
62
66
  };
@@ -105,12 +105,10 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
105
105
  setViewType(value as ViewTypes);
106
106
  // If its the URL field type we know what the imageType should be
107
107
  if (value === ViewTypes.URL) {
108
- console.log(`handleChangeViewType: ${value} NodeName.Image`);
109
108
  setValue('imageType', NodeName.Image);
110
109
  } else {
111
110
  // Need a value here and this is the assumed default elsewhere
112
111
  // Will be set again later once Resource Browser returns a resource value
113
- console.log(`handleChangeViewType: ${value} NodeName.AssetImage`);
114
112
  setValue('imageType', NodeName.AssetImage);
115
113
  }
116
114
  },
@@ -231,7 +229,6 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
231
229
  allowedTypes={['image']}
232
230
  value={value}
233
231
  onChange={(value: { target: { value: any } }) => {
234
- console.log(`onChange: ${value}`);
235
232
  setValue('imageType', value.target.value.nodeType);
236
233
  onChange(value);
237
234
  }}
@@ -1,13 +1,13 @@
1
1
  import React from 'react';
2
2
  import { useCommands, useActive } from '@remirror/react';
3
- import { ExtendedCodeBlockExtension } from '../../../../Extensions/CodeBlockExtension/CodeBlockExtension';
3
+ import { CodeBlockExtension } from '../../../../Extensions/CodeBlockExtension/CodeBlockExtension';
4
4
  import DropdownButton from '../../../../ui/ToolbarDropdownButton/ToolbarDropdownButton';
5
5
  import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
6
6
 
7
7
  const CodeBlockButton = () => {
8
- const { toggleCodeBlock } = useCommands<ExtendedCodeBlockExtension>();
8
+ const { toggleCodeBlock } = useCommands<CodeBlockExtension>();
9
9
 
10
- const active = useActive<ExtendedCodeBlockExtension>();
10
+ const active = useActive<CodeBlockExtension>();
11
11
  const enabled = toggleCodeBlock.enabled();
12
12
 
13
13
  const handleSelect = () => {
@@ -30,11 +30,12 @@
30
30
  }
31
31
 
32
32
  .header-toolbar {
33
+ opacity: 0;
34
+ max-height: 0;
35
+ // TODO: PLATFORM-1611 animation looks strange when the toolbar is rendered in an arbitrary location.
33
36
  transition-duration: 0.3s;
34
37
  transition-property: max-height, opacity;
35
38
  transition-timing-function: ease-out;
36
- opacity: 0;
37
- max-height: 0;
38
39
 
39
40
  &.show-toolbar {
40
41
  opacity: 1;
@@ -0,0 +1,3 @@
1
+ export type ExtendedCodeBlockExtensionProps = {
2
+ enableDecorations: boolean;
3
+ };
@@ -0,0 +1,59 @@
1
+ import { renderWithEditor } from '../../../tests';
2
+
3
+ describe('CodeBlockExtension', () => {
4
+ it('Parses a <code> HTML tag', async () => {
5
+ // This is the structure <code> "blocks" are persisted as.
6
+ // When displaying without decorations this is how a code "block" will also
7
+ // be rendered.
8
+ const { getJsonContent } = await renderWithEditor(null, {
9
+ content: `<code>Code block content</code.`,
10
+ });
11
+
12
+ expect(getJsonContent()).toEqual({
13
+ type: 'codeBlock',
14
+ content: [
15
+ {
16
+ type: 'text',
17
+ text: 'Code block content',
18
+ },
19
+ ],
20
+ });
21
+ });
22
+
23
+ it('Parses a <code> HTML tag wrapped in a <pre> tag', async () => {
24
+ // This is the structure <code> blocks are rendered as in the editor
25
+ // when decorations are enabled. Supporting parsing this format
26
+ // allows copy-pasting it between editor instances.
27
+ const { getJsonContent } = await renderWithEditor(null, {
28
+ content: `<pre><code>Code block content</code></code.`,
29
+ });
30
+
31
+ expect(getJsonContent()).toEqual({
32
+ type: 'codeBlock',
33
+ content: [
34
+ {
35
+ type: 'text',
36
+ text: 'Code block content',
37
+ },
38
+ ],
39
+ });
40
+ });
41
+
42
+ it('Does not parse a non-<code> HTML tag wrapped in a <pre> tag', async () => {
43
+ const { getJsonContent } = await renderWithEditor(null, {
44
+ content: `<pre><p>This is an un-expected node structure.</p></code>`,
45
+ });
46
+
47
+ expect(getJsonContent()).toEqual({
48
+ // parsed by the PreformattedExtension, not the CodeBlockExtension.
49
+ type: 'preformatted',
50
+ attrs: expect.any(Object),
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: 'This is an un-expected node structure.',
55
+ },
56
+ ],
57
+ });
58
+ });
59
+ });
@@ -1,11 +1,76 @@
1
- import { CodeBlockExtension } from '@remirror/extension-code-block';
2
- import { NodeViewMethod, ProsemirrorNode } from 'remirror';
1
+ import {
2
+ ApplySchemaAttributes,
3
+ command,
4
+ CommandFunction,
5
+ extension,
6
+ ExtensionTag,
7
+ NodeExtension,
8
+ NodeExtensionSpec,
9
+ NodeSpecOverride,
10
+ ProsemirrorNode,
11
+ toggleBlockItem,
12
+ } from '@remirror/core';
13
+ import { isElementDomNode, NodeViewMethod } from 'remirror';
3
14
 
4
- export class ExtendedCodeBlockExtension extends CodeBlockExtension {
5
- createNodeViews(): NodeViewMethod {
6
- return (node: ProsemirrorNode) => {
7
- const { language } = node.attrs;
15
+ export type CodeBlockOptions = {
16
+ enableDecorations?: boolean;
17
+ };
8
18
 
19
+ @extension<CodeBlockOptions>({
20
+ defaultOptions: {
21
+ enableDecorations: false,
22
+ },
23
+ })
24
+ export class CodeBlockExtension extends NodeExtension<CodeBlockOptions> {
25
+ get name() {
26
+ return 'codeBlock' as const;
27
+ }
28
+
29
+ createTags() {
30
+ return [ExtensionTag.Block, ExtensionTag.Code];
31
+ }
32
+
33
+ createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
34
+ return {
35
+ content: 'text*',
36
+ marks: '',
37
+ defining: true,
38
+ isolating: true,
39
+ draggable: false,
40
+ ...override,
41
+ code: true,
42
+ attrs: {
43
+ ...extra.defaults(),
44
+ },
45
+ parseDOM: [
46
+ {
47
+ tag: 'code',
48
+ preserveWhitespace: 'full',
49
+ },
50
+ {
51
+ tag: 'pre',
52
+ preserveWhitespace: 'full',
53
+ getAttrs: (node) => {
54
+ if (!isElementDomNode(node) || !isElementDomNode(node.querySelector('code'))) {
55
+ return false;
56
+ }
57
+
58
+ return extra.parse(node);
59
+ },
60
+ },
61
+ ],
62
+ toDOM: (node: ProsemirrorNode) => {
63
+ return ['code', extra.dom(node), 0];
64
+ },
65
+ };
66
+ }
67
+
68
+ createNodeViews(): NodeViewMethod | Record<string, never> {
69
+ if (!this.options.enableDecorations) {
70
+ return {};
71
+ }
72
+
73
+ return () => {
9
74
  // This is the pre container for the code block
10
75
  const dom = document.createElement('pre');
11
76
  dom.setAttribute('spellcheck', 'false');
@@ -13,7 +78,6 @@ export class ExtendedCodeBlockExtension extends CodeBlockExtension {
13
78
 
14
79
  // This is the actual code content in the code block
15
80
  const contentDOM = document.createElement('code');
16
- contentDOM.setAttribute('data-code-block-language', language);
17
81
 
18
82
  // Divider between code block and pre container
19
83
  const dividerElement = document.createElement('div');
@@ -31,4 +95,15 @@ export class ExtendedCodeBlockExtension extends CodeBlockExtension {
31
95
  return { dom, contentDOM };
32
96
  };
33
97
  }
98
+
99
+ /**
100
+ * Toggle the <code> for the current block.
101
+ */
102
+ @command()
103
+ toggleCodeBlock(): CommandFunction {
104
+ return toggleBlockItem({
105
+ type: this.type,
106
+ toggleType: 'paragraph',
107
+ });
108
+ }
34
109
  }
@@ -24,7 +24,7 @@ import { CommandsExtension } from './CommandsExtension/CommandsExtension';
24
24
  import { EditorContextOptions } from '../Editor/EditorContext';
25
25
  import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
26
26
  import { DAMImageExtension } from './ImageExtension/DAMImageExtension';
27
- import { ExtendedCodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
27
+ import { CodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
28
28
  import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
29
29
  import { UnsupportedNodeExtension } from './UnsuportedExtension/UnsupportedNodeExtension';
30
30
  import { FetchUrlExtension } from './FetchUrlExtension/FetchUrlExtension';
@@ -48,7 +48,7 @@ export enum MarkName {
48
48
  AssetLink = 'assetLink',
49
49
  }
50
50
 
51
- export const createExtensions = (context: EditorContextOptions) => {
51
+ export const createExtensions = (context: EditorContextOptions, enableDecorations: boolean = true) => {
52
52
  return (): Extension[] => {
53
53
  return [
54
54
  new CommandsExtension(),
@@ -59,8 +59,8 @@ export const createExtensions = (context: EditorContextOptions) => {
59
59
  new NodeFormattingExtension({ indents: [] }),
60
60
  new ParagraphExtension(),
61
61
  new HardBreakExtension(),
62
- new PreformattedExtension(),
63
- new ExtendedCodeBlockExtension({ defaultWrap: true }),
62
+ new CodeBlockExtension({ enableDecorations }),
63
+ new PreformattedExtension({ enableDecorations }),
64
64
  new UnderlineExtension(),
65
65
  new HistoryExtension(),
66
66
  new ImageExtension(),
@@ -12,8 +12,16 @@ import {
12
12
  } from '@remirror/core';
13
13
  import { NodeViewMethod } from 'remirror';
14
14
 
15
- @extension({})
16
- export class PreformattedExtension extends NodeExtension {
15
+ export type PreformattedExtensionOptions = {
16
+ enableDecorations?: boolean;
17
+ };
18
+
19
+ @extension<PreformattedExtensionOptions>({
20
+ defaultOptions: {
21
+ enableDecorations: false,
22
+ },
23
+ })
24
+ export class PreformattedExtension extends NodeExtension<PreformattedExtensionOptions> {
17
25
  get name() {
18
26
  return 'preformatted' as const;
19
27
  }
@@ -42,7 +50,11 @@ export class PreformattedExtension extends NodeExtension {
42
50
  };
43
51
  }
44
52
 
45
- createNodeViews(): NodeViewMethod {
53
+ createNodeViews(): NodeViewMethod | Record<string, never> {
54
+ if (!this.options.enableDecorations) {
55
+ return {};
56
+ }
57
+
46
58
  return (node: ProsemirrorNode) => {
47
59
  const { nodeTextAlignment } = node.attrs;
48
60
 
@@ -1,2 +1,3 @@
1
- export * from './useExtensionNames';
2
- export * from './useExpandedSelection';
1
+ export * from './useExtensionNames/useExtensionNames';
2
+ export * from './useExpandedSelection/useExpandedSelection';
3
+ export * from './useFocus/useFocus';