@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.
- package/CHANGELOG.md +12 -0
- package/demo/{App.tsx → diff/App.tsx} +3 -7
- package/demo/{AppContext.tsx → diff/AppContext.tsx} +15 -10
- package/demo/diff/index.html +14 -0
- package/demo/{index.scss → diff/index.scss} +3 -0
- package/demo/{main.tsx → diff/main.tsx} +1 -1
- package/demo/index.html +47 -2
- package/demo/portals/Accordion.tsx +50 -0
- package/demo/portals/App.tsx +150 -0
- package/demo/portals/index.html +13 -0
- package/demo/portals/index.scss +8 -0
- package/demo/portals/index.tsx +12 -0
- package/demo/portals/preview.html +91 -0
- package/demo/portals/preview.tsx +10 -0
- package/lib/Editor/Editor.d.ts +11 -6
- package/lib/Editor/Editor.js +17 -26
- package/lib/EditorToolbar/Toolbar.d.ts +2 -1
- package/lib/EditorToolbar/Toolbar.js +4 -2
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +0 -3
- package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.d.ts +13 -3
- package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.js +74 -8
- package/lib/Extensions/Extensions.d.ts +1 -1
- package/lib/Extensions/Extensions.js +3 -3
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +5 -2
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +8 -1
- package/lib/hooks/index.d.ts +3 -2
- package/lib/hooks/index.js +3 -2
- package/lib/hooks/useFocus/useFocus.d.ts +6 -0
- package/lib/hooks/{useFocus.js → useFocus/useFocus.js} +29 -15
- package/lib/index.css +164 -5
- package/lib/ui/EditorInput/EditorInput.d.ts +3 -0
- package/lib/ui/EditorInput/EditorInput.js +49 -0
- package/lib/ui/EditorInput/EditorInput.props.d.ts +4 -0
- package/lib/ui/EditorInput/EditorInput.props.js +2 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +0 -3
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +0 -6
- package/package.json +5 -4
- package/src/Editor/Editor.spec.tsx +36 -10
- package/src/Editor/Editor.tsx +48 -44
- package/src/Editor/_editor.scss +4 -0
- package/src/EditorToolbar/Toolbar.tsx +8 -4
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +0 -3
- package/src/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.tsx +3 -3
- package/src/EditorToolbar/_toolbar.scss +3 -2
- package/src/Extensions/CodeBlockExtension/CodeBlockExtension.props.ts +3 -0
- package/src/Extensions/CodeBlockExtension/CodeBlockExtension.spec.ts +59 -0
- package/src/Extensions/CodeBlockExtension/CodeBlockExtension.ts +82 -7
- package/src/Extensions/Extensions.ts +4 -4
- package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +15 -3
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useFocus/useFocus.spec.tsx +48 -0
- package/src/hooks/useFocus/useFocus.ts +71 -0
- package/src/ui/EditorInput/EditorInput.props.ts +5 -0
- package/src/ui/EditorInput/EditorInput.spec.tsx +38 -0
- package/src/ui/EditorInput/EditorInput.tsx +30 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +1 -3
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +0 -4
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +1 -4
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +0 -5
- package/tests/mockResourceBrowserContext.tsx +17 -1
- package/tests/renderWithContext.tsx +3 -0
- package/lib/hooks/useFocus.d.ts +0 -8
- package/src/hooks/useFocus.ts +0 -61
- /package/demo/{resources.json → diff/resources.json} +0 -0
- /package/demo/{sources.json → diff/sources.json} +0 -0
- /package/demo/{vite-env.d.ts → diff/vite-env.d.ts} +0 -0
- /package/lib/hooks/{useExpandedSelection.d.ts → useExpandedSelection/useExpandedSelection.d.ts} +0 -0
- /package/lib/hooks/{useExpandedSelection.js → useExpandedSelection/useExpandedSelection.js} +0 -0
- /package/lib/hooks/{useExtensionNames.d.ts → useExtensionNames/useExtensionNames.d.ts} +0 -0
- /package/lib/hooks/{useExtensionNames.js → useExtensionNames/useExtensionNames.js} +0 -0
- /package/src/hooks/{useExpandedSelection.ts → useExpandedSelection/useExpandedSelection.ts} +0 -0
- /package/src/hooks/{useExtensionNames.ts → useExtensionNames/useExtensionNames.ts} +0 -0
@@ -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,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
|
+
};
|
@@ -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,
|
@@ -63,10 +63,25 @@ export const mockResourceBrowserContext = ({ sources, resources, pluginProps }:
|
|
63
63
|
Promise.resolve(resources?.find((resource) => resource.id === reference.resource) || null),
|
64
64
|
);
|
65
65
|
|
66
|
+
const onSearchRequest = () =>
|
67
|
+
Promise.resolve({
|
68
|
+
results: [],
|
69
|
+
filters: [],
|
70
|
+
resultsSummary: {
|
71
|
+
totalMatching: 0,
|
72
|
+
numRanks: 10,
|
73
|
+
currStart: 0,
|
74
|
+
currEnd: 0,
|
75
|
+
prevStart: null,
|
76
|
+
nextStart: null,
|
77
|
+
},
|
78
|
+
});
|
79
|
+
|
66
80
|
return {
|
67
81
|
MockResourceBrowserContext: ({ children }: { children: ReactNode }) => (
|
68
82
|
<ResourceBrowserContextProvider
|
69
83
|
value={{
|
84
|
+
searchEnabled: false,
|
70
85
|
onRequestSources: (): Promise<ResourceBrowserSource[]> =>
|
71
86
|
Promise.resolve([
|
72
87
|
{
|
@@ -80,7 +95,8 @@ export const mockResourceBrowserContext = ({ sources, resources, pluginProps }:
|
|
80
95
|
onRequestSources: onRequestSources,
|
81
96
|
onRequestChildren: onRequestChildren,
|
82
97
|
onRequestResource: onRequestResource,
|
83
|
-
|
98
|
+
onSearchRequest,
|
99
|
+
} as unknown as MatrixResourceBrowserPluginProps),
|
84
100
|
],
|
85
101
|
}}
|
86
102
|
>
|
@@ -45,12 +45,14 @@ export const renderWithContext = (ui: ReactElement | null, options?: ContextRend
|
|
45
45
|
const onRequestSources = jest.fn().mockResolvedValue(sources);
|
46
46
|
const onRequestChildren = jest.fn().mockResolvedValue(resources);
|
47
47
|
const onRequestResource = jest.fn(() => Promise.resolve(resources[0]));
|
48
|
+
const onSearchRequest = jest.fn();
|
48
49
|
editorContext.resolveNodeToUrl = jest.fn(() => Promise.resolve(resources[0].url));
|
49
50
|
|
50
51
|
return render(
|
51
52
|
<EditorContext.Provider value={editorContext}>
|
52
53
|
<ResourceBrowserContextProvider
|
53
54
|
value={{
|
55
|
+
searchEnabled: false,
|
54
56
|
onRequestSources: (): Promise<ResourceBrowserSource[]> =>
|
55
57
|
Promise.resolve([
|
56
58
|
{
|
@@ -64,6 +66,7 @@ export const renderWithContext = (ui: ReactElement | null, options?: ContextRend
|
|
64
66
|
onRequestSources: onRequestSources,
|
65
67
|
onRequestChildren: onRequestChildren,
|
66
68
|
onRequestResource: onRequestResource,
|
69
|
+
onSearchRequest,
|
67
70
|
}),
|
68
71
|
],
|
69
72
|
}}
|
package/lib/hooks/useFocus.d.ts
DELETED
@@ -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;
|
package/src/hooks/useFocus.ts
DELETED
@@ -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
|
/package/lib/hooks/{useExpandedSelection.d.ts → useExpandedSelection/useExpandedSelection.d.ts}
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|