@squiz/formatted-text-editor 2.4.0 → 2.5.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 (70) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/demo/{App.tsx → diff/App.tsx} +3 -2
  3. package/demo/{AppContext.tsx → diff/AppContext.tsx} +1 -2
  4. package/demo/diff/index.html +14 -0
  5. package/demo/{main.tsx → diff/main.tsx} +1 -1
  6. package/demo/index.html +47 -2
  7. package/demo/portals/Accordion.tsx +50 -0
  8. package/demo/portals/App.tsx +150 -0
  9. package/demo/portals/index.html +13 -0
  10. package/demo/portals/index.scss +8 -0
  11. package/demo/portals/index.tsx +12 -0
  12. package/demo/portals/preview.html +91 -0
  13. package/demo/portals/preview.tsx +10 -0
  14. package/lib/Editor/Editor.d.ts +11 -6
  15. package/lib/Editor/Editor.js +17 -26
  16. package/lib/EditorToolbar/Toolbar.d.ts +2 -1
  17. package/lib/EditorToolbar/Toolbar.js +4 -2
  18. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +0 -3
  19. package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.d.ts +13 -3
  20. package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.js +74 -8
  21. package/lib/Extensions/Extensions.d.ts +1 -1
  22. package/lib/Extensions/Extensions.js +3 -3
  23. package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +5 -2
  24. package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +8 -1
  25. package/lib/hooks/index.d.ts +3 -2
  26. package/lib/hooks/index.js +3 -2
  27. package/lib/hooks/useFocus/useFocus.d.ts +6 -0
  28. package/lib/hooks/{useFocus.js → useFocus/useFocus.js} +29 -15
  29. package/lib/index.css +7 -2
  30. package/lib/ui/EditorInput/EditorInput.d.ts +3 -0
  31. package/lib/ui/EditorInput/EditorInput.js +49 -0
  32. package/lib/ui/EditorInput/EditorInput.props.d.ts +4 -0
  33. package/lib/ui/EditorInput/EditorInput.props.js +2 -0
  34. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +0 -3
  35. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +0 -6
  36. package/package.json +1 -1
  37. package/src/Editor/Editor.spec.tsx +35 -10
  38. package/src/Editor/Editor.tsx +48 -44
  39. package/src/Editor/_editor.scss +4 -0
  40. package/src/EditorToolbar/Toolbar.tsx +8 -4
  41. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +0 -3
  42. package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.tsx +3 -3
  43. package/src/EditorToolbar/_toolbar.scss +3 -2
  44. package/src/Extensions/CodeBlockExtension/CodeBlockExtension.props.ts +3 -0
  45. package/src/Extensions/CodeBlockExtension/CodeBlockExtension.spec.ts +59 -0
  46. package/src/Extensions/CodeBlockExtension/CodeBlockExtension.ts +82 -7
  47. package/src/Extensions/Extensions.ts +4 -4
  48. package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +15 -3
  49. package/src/hooks/index.ts +3 -2
  50. package/src/hooks/useFocus/useFocus.spec.tsx +48 -0
  51. package/src/hooks/useFocus/useFocus.ts +71 -0
  52. package/src/ui/EditorInput/EditorInput.props.ts +5 -0
  53. package/src/ui/EditorInput/EditorInput.spec.tsx +38 -0
  54. package/src/ui/EditorInput/EditorInput.tsx +30 -0
  55. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +1 -3
  56. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +0 -4
  57. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +1 -4
  58. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +0 -5
  59. package/lib/hooks/useFocus.d.ts +0 -8
  60. package/src/hooks/useFocus.ts +0 -61
  61. /package/demo/{index.scss → diff/index.scss} +0 -0
  62. /package/demo/{resources.json → diff/resources.json} +0 -0
  63. /package/demo/{sources.json → diff/sources.json} +0 -0
  64. /package/demo/{vite-env.d.ts → diff/vite-env.d.ts} +0 -0
  65. /package/lib/hooks/{useExpandedSelection.d.ts → useExpandedSelection/useExpandedSelection.d.ts} +0 -0
  66. /package/lib/hooks/{useExpandedSelection.js → useExpandedSelection/useExpandedSelection.js} +0 -0
  67. /package/lib/hooks/{useExtensionNames.d.ts → useExtensionNames/useExtensionNames.d.ts} +0 -0
  68. /package/lib/hooks/{useExtensionNames.js → useExtensionNames/useExtensionNames.js} +0 -0
  69. /package/src/hooks/{useExpandedSelection.ts → useExpandedSelection/useExpandedSelection.ts} +0 -0
  70. /package/src/hooks/{useExtensionNames.ts → useExtensionNames/useExtensionNames.ts} +0 -0
@@ -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';
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { screen } from '@testing-library/react';
3
+ import { useFocus } from './useFocus';
4
+ import { render, renderHook } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+
7
+ describe('useFocus', () => {
8
+ beforeEach(() => {
9
+ jest.useRealTimers();
10
+ global.requestAnimationFrame = process.nextTick as any;
11
+ });
12
+
13
+ it.each([true, false])('Should set the focus value from initial state - %s', (initialState: boolean) => {
14
+ const isChildElement = jest.fn();
15
+ const { result } = renderHook(() => useFocus(initialState, isChildElement));
16
+
17
+ expect(result.current.isFocused).toBe(initialState);
18
+ });
19
+
20
+ it('Should update the focused value when an element is focused or blurred', async () => {
21
+ const isChildElement = jest.fn((element: Element) => !!element.closest('.focus-container'));
22
+ let lastIsFocused: boolean | undefined = undefined;
23
+ const Component = () => {
24
+ const { isFocused, handleFocus, handleBlur } = useFocus(false, isChildElement);
25
+ lastIsFocused = isFocused;
26
+
27
+ return (
28
+ <>
29
+ <div className="focus-container" onFocus={handleFocus} onBlur={handleBlur}>
30
+ <button type="button">Child element</button>
31
+ </div>
32
+ <div>
33
+ <button type="button">Non-child element</button>
34
+ </div>
35
+ </>
36
+ );
37
+ };
38
+
39
+ const { rerender } = render(<Component />);
40
+
41
+ await userEvent.click(screen.getByRole('button', { name: 'Child element' }));
42
+ expect(lastIsFocused).toBe(true);
43
+
44
+ await userEvent.click(screen.getByRole('button', { name: 'Non-child element' }));
45
+ rerender(<Component />);
46
+ expect(lastIsFocused).toBe(false);
47
+ });
48
+ });
@@ -0,0 +1,71 @@
1
+ import { useState, useCallback, FocusEvent as ReactFocusEvent, FocusEventHandler } from 'react';
2
+
3
+ export const useFocus = (
4
+ initialState: boolean,
5
+ isChildElement: (element: Element) => boolean,
6
+ ): {
7
+ handleFocus: (event: ReactFocusEvent) => void;
8
+ handleBlur: FocusEventHandler<HTMLDivElement>;
9
+ isFocused: boolean;
10
+ } => {
11
+ const [isFocused, setIsFocused] = useState(initialState);
12
+ const getFocusedElement = useCallback((): Element | null => {
13
+ let element = document.activeElement;
14
+
15
+ while (element instanceof HTMLIFrameElement) {
16
+ element = element.contentDocument?.activeElement || null;
17
+ }
18
+
19
+ return element?.parentElement ? element : null;
20
+ }, []);
21
+
22
+ const handleFocus = useCallback((event: ReactFocusEvent) => {
23
+ // Ignore elements flagged to be ignored, this allows us to add extra, clickable, elements
24
+ // without triggering a focus, such as action menus.
25
+ if (!event.target?.classList?.contains('fte-ignore') && !event.target?.closest('.fte-ignore')) {
26
+ setIsFocused(true);
27
+ }
28
+ }, []);
29
+
30
+ const handleBlur = useCallback(
31
+ (event: ReactFocusEvent<Element>) => {
32
+ // React event bubbling is interesting, it bubbles up the React tree rather than the DOM tree.
33
+ // The tree deviates when rendering portals (eg. for modals).
34
+ //
35
+ // Only hide the toolbar if:
36
+ // 1. We are blurring a node in the editor **DOM** tree.
37
+ // 2. We are focusing on something that is not in the editor DOM tree
38
+ // (elements in the portal won't be in the tree but don't influence the focus state per #1).
39
+ //
40
+ // This avoids the scenario where an element in a portal is blurred and another one in the portal focused.
41
+ // Without this logic the blur and focus handlers are called (in that order). The impact of these handlers being
42
+ // called is that the "isFocused" state changes inconsistently. This state changing then causes subtle issues.
43
+ // eg. unable to drill down in resource browser, toolbar appearing/disappearing.
44
+ //
45
+ // Ideally we would instead solely seeing if the "relatedTarget" is in the React tree. This isn't easily
46
+ // identifiable however without reaching into React internals.
47
+ //
48
+ // An assumption here is that anything in a portal will only blur to another element that is also in the portal
49
+ // (and therefore still in our React tree resulting in the element still effectively being focused).
50
+ // TODO: PLATFORM-1611 this shit still doesn't work properly, notably issues with the link/image modals.
51
+ requestAnimationFrame(() => {
52
+ // "relatedTarget" in the event object will be null if the element gaining focus is in a different
53
+ // document to the element being blurred (eg. floating toolbar rendered in top level frame,
54
+ // editor rendered in iframe). instead grab the active element to determine current focus.
55
+ const isBlurringEditor = isChildElement(event.target);
56
+ const focusedElement = event.relatedTarget || getFocusedElement();
57
+ const isFocusedInEditor = focusedElement && isChildElement(focusedElement);
58
+
59
+ // Detect if the blur event happens when the related/clicked target is the floating popover
60
+ const isClickingFloatingToolbar = !!focusedElement?.closest('.squiz-fte-scope__floating-popover');
61
+
62
+ if (isBlurringEditor && !isFocusedInEditor && !isClickingFloatingToolbar) {
63
+ setIsFocused(false);
64
+ }
65
+ });
66
+ },
67
+ [getFocusedElement, isChildElement],
68
+ );
69
+
70
+ return { handleFocus, handleBlur, isFocused };
71
+ };
@@ -0,0 +1,5 @@
1
+ import { HTMLAttributes } from 'react';
2
+
3
+ export type EditorInputProps = HTMLAttributes<HTMLDivElement> & {
4
+ container?: Element | null;
5
+ };
@@ -0,0 +1,38 @@
1
+ import '@testing-library/jest-dom';
2
+ import React from 'react';
3
+ import { render, screen, within } from '@testing-library/react';
4
+ import { EditorInput } from './EditorInput';
5
+ import { Remirror, useRemirror } from '@remirror/react';
6
+ import { EditorInputProps } from './EditorInput.props';
7
+
8
+ describe('EditorInput', () => {
9
+ const inputLabel = 'Text editor';
10
+
11
+ const Component = (props: EditorInputProps) => {
12
+ const { manager, state } = useRemirror({
13
+ content: 'Hello world',
14
+ stringHandler: 'html',
15
+ });
16
+
17
+ return (
18
+ <Remirror manager={manager} state={state} label={inputLabel}>
19
+ <EditorInput {...props} />
20
+ </Remirror>
21
+ );
22
+ };
23
+
24
+ it('Mounts the input inline if container is not provided', () => {
25
+ render(<Component />);
26
+
27
+ expect(screen.getByRole('textbox', { name: 'Text editor' })).toBeInTheDocument();
28
+ });
29
+
30
+ it('Mounts the input inside the container if provided', () => {
31
+ const container = document.createElement('div');
32
+ document.body.append(container);
33
+
34
+ render(<Component container={container} />);
35
+
36
+ expect(within(container).getByRole('textbox', { name: 'Text editor' })).toBeInTheDocument();
37
+ });
38
+ });
@@ -0,0 +1,30 @@
1
+ import { useEditorEvent, useRemirrorContext } from '@remirror/react';
2
+ import React, { useCallback } from 'react';
3
+ import { ClipboardEventHandler } from '@remirror/extension-events/dist-types/events-extension';
4
+ import { createPortal } from 'react-dom';
5
+ import { EditorInputProps } from './EditorInput.props';
6
+
7
+ export const EditorInput = ({ container, ...other }: EditorInputProps) => {
8
+ const { getRootProps } = useRemirrorContext();
9
+ const { key, ...rootProps } = getRootProps();
10
+ const preventImagePaste = useCallback((event) => {
11
+ const { clipboardData } = event;
12
+ const pastedData = clipboardData?.files[0];
13
+ if (
14
+ pastedData?.type &&
15
+ pastedData?.type.startsWith('image/') &&
16
+ // Still allow paste of any text that came through (Word, etc)
17
+ !clipboardData?.types.includes('text/plain')
18
+ ) {
19
+ event.preventDefault();
20
+ }
21
+
22
+ // Allow other paste event handlers to be run.
23
+ return false;
24
+ }, []) as ClipboardEventHandler;
25
+ const input = <div key={key} {...rootProps} {...other} />;
26
+
27
+ useEditorEvent('paste', preventImagePaste);
28
+
29
+ return container ? createPortal(input, container) : input;
30
+ };
@@ -102,9 +102,7 @@ describe('remirrorNodeToSquizNode', () => {
102
102
  type: 'tag',
103
103
  tag: 'code',
104
104
  children: [{ type: 'text', value: 'Hello world' }],
105
- attributes: {
106
- language: 'js',
107
- },
105
+ attributes: {},
108
106
  },
109
107
  ];
110
108
 
@@ -22,10 +22,6 @@ export const resolveNodeTag = (node: ProsemirrorNode): string => {
22
22
  return 'span';
23
23
  }
24
24
 
25
- if (node.type.name === NodeName.CodeBlock) {
26
- return 'code';
27
- }
28
-
29
25
  if (node.type.spec?.toDOM) {
30
26
  const domNode = node.type.spec.toDOM(node);
31
27
 
@@ -283,10 +283,7 @@ describe('squizNodeToRemirrorNode', () => {
283
283
  content: [
284
284
  {
285
285
  type: 'codeBlock',
286
- attrs: {
287
- language: 'markup',
288
- wrap: true,
289
- },
286
+ attrs: { nodeIndent: null, nodeLineHeight: null, nodeTextAlignment: null, style: '' },
290
287
  content: [
291
288
  {
292
289
  type: 'text',
@@ -85,11 +85,6 @@ const getNodeAttributes = (node: FormattedNodes): Attrs => {
85
85
  src: node.attributes?.src,
86
86
  title: node.attributes?.title,
87
87
  };
88
- } else if (node.type === 'tag' && node.tag === 'code') {
89
- return {
90
- language: node.attributes?.language || 'markup',
91
- wrap: node.attributes?.wrap || true,
92
- };
93
88
  } else if (node.type === 'matrix-image') {
94
89
  return {
95
90
  matrixAssetId: node.matrixAssetId,
@@ -1,8 +0,0 @@
1
- import { FocusEvent, FocusEventHandler, RefObject } from 'react';
2
- declare const useFocus: (initialState: boolean) => {
3
- handleFocus: (event: FocusEvent) => void;
4
- handleBlur: FocusEventHandler<HTMLDivElement>;
5
- isVisible: boolean;
6
- wrapperRef: RefObject<HTMLDivElement>;
7
- };
8
- export default useFocus;
@@ -1,61 +0,0 @@
1
- import { useState, useCallback, FocusEvent, FocusEventHandler, RefObject, createRef } from 'react';
2
-
3
- const useFocus = (
4
- initialState: boolean,
5
- ): {
6
- handleFocus: (event: FocusEvent) => void;
7
- handleBlur: FocusEventHandler<HTMLDivElement>;
8
- isVisible: boolean;
9
- wrapperRef: RefObject<HTMLDivElement>;
10
- } => {
11
- const wrapperRef = createRef<HTMLDivElement>();
12
- const [isVisible, setIsVisible] = useState(initialState);
13
-
14
- const handleFocus = useCallback(
15
- (event: FocusEvent) => {
16
- // Ignore elements flagged to be ignored, this allows us to add extra, clickable, elements
17
- // without triggering a focus, such as action menus.
18
- if (!event.target?.classList?.contains('fte-ignore') && !event.target?.closest('.fte-ignore')) {
19
- setIsVisible(true);
20
- }
21
- },
22
- [wrapperRef],
23
- );
24
-
25
- const handleBlur = useCallback(
26
- (event: FocusEvent<HTMLDivElement>) => {
27
- // React event bubbling is interesting, it bubbles up the React tree rather than the DOM tree.
28
- // The tree deviates when rendering portals (eg. for modals).
29
- //
30
- // Only hide the toolbar if:
31
- // 1. We are blurring a node in the editor **DOM** tree.
32
- // 2. We are focusing on something that is not in the editor DOM tree
33
- // (elements in the portal won't be in the tree but don't influence the focus state per #1).
34
- //
35
- // This avoids the scenario where an element in a portal is blurred and another one in the portal focused.
36
- // Without this logic the blur and focus handlers are called (in that order). The impact of these handlers being
37
- // called is that the "isFocused" state changes inconsistently. This state changing then causes subtle issues.
38
- // eg. unable to drill down in resource browser, toolbar appearing/disappearing.
39
- //
40
- // Ideally we would instead solely seeing if the "relatedTarget" is in the React tree. This isn't easily
41
- // identifiable however without reaching into React internals.
42
- //
43
- // An assumption here is that anything in a portal will only blur to another element that is also in the portal
44
- // (and therefore still in our React tree resulting in the element still effectively being focused).
45
- const isBlurringEditor = wrapperRef.current?.contains(event.target);
46
- const isFocusedInEditor = wrapperRef.current?.contains(event.relatedTarget);
47
-
48
- // Detect if the blur event happens when the related/clicked target is the floating popover
49
- const isClickingFloatingToolbar = !!event.relatedTarget?.closest('.squiz-fte-scope__floating-popover');
50
-
51
- if (isBlurringEditor && !isFocusedInEditor && !isClickingFloatingToolbar) {
52
- setIsVisible(false);
53
- }
54
- },
55
- [wrapperRef],
56
- );
57
-
58
- return { handleFocus, handleBlur, isVisible, wrapperRef };
59
- };
60
-
61
- export default useFocus;
File without changes
File without changes
File without changes
File without changes