@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.
- package/CHANGELOG.md +6 -0
- package/demo/{App.tsx → diff/App.tsx} +3 -2
- package/demo/{AppContext.tsx → diff/AppContext.tsx} +1 -2
- package/demo/diff/index.html +14 -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 +7 -2
- 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 +1 -1
- package/src/Editor/Editor.spec.tsx +35 -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/lib/hooks/useFocus.d.ts +0 -8
- package/src/hooks/useFocus.ts +0 -61
- /package/demo/{index.scss → diff/index.scss} +0 -0
- /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
@@ -1,18 +1,65 @@
|
|
1
1
|
"use strict";
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
7
|
+
};
|
2
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.
|
4
|
-
const
|
5
|
-
|
9
|
+
exports.CodeBlockExtension = void 0;
|
10
|
+
const core_1 = require("@remirror/core");
|
11
|
+
const remirror_1 = require("remirror");
|
12
|
+
let CodeBlockExtension = class CodeBlockExtension extends core_1.NodeExtension {
|
13
|
+
get name() {
|
14
|
+
return 'codeBlock';
|
15
|
+
}
|
16
|
+
createTags() {
|
17
|
+
return [core_1.ExtensionTag.Block, core_1.ExtensionTag.Code];
|
18
|
+
}
|
19
|
+
createNodeSpec(extra, override) {
|
20
|
+
return {
|
21
|
+
content: 'text*',
|
22
|
+
marks: '',
|
23
|
+
defining: true,
|
24
|
+
isolating: true,
|
25
|
+
draggable: false,
|
26
|
+
...override,
|
27
|
+
code: true,
|
28
|
+
attrs: {
|
29
|
+
...extra.defaults(),
|
30
|
+
},
|
31
|
+
parseDOM: [
|
32
|
+
{
|
33
|
+
tag: 'code',
|
34
|
+
preserveWhitespace: 'full',
|
35
|
+
},
|
36
|
+
{
|
37
|
+
tag: 'pre',
|
38
|
+
preserveWhitespace: 'full',
|
39
|
+
getAttrs: (node) => {
|
40
|
+
if (!(0, remirror_1.isElementDomNode)(node) || !(0, remirror_1.isElementDomNode)(node.querySelector('code'))) {
|
41
|
+
return false;
|
42
|
+
}
|
43
|
+
return extra.parse(node);
|
44
|
+
},
|
45
|
+
},
|
46
|
+
],
|
47
|
+
toDOM: (node) => {
|
48
|
+
return ['code', extra.dom(node), 0];
|
49
|
+
},
|
50
|
+
};
|
51
|
+
}
|
6
52
|
createNodeViews() {
|
7
|
-
|
8
|
-
|
53
|
+
if (!this.options.enableDecorations) {
|
54
|
+
return {};
|
55
|
+
}
|
56
|
+
return () => {
|
9
57
|
// This is the pre container for the code block
|
10
58
|
const dom = document.createElement('pre');
|
11
59
|
dom.setAttribute('spellcheck', 'false');
|
12
60
|
dom.classList.add(`code-block`);
|
13
61
|
// This is the actual code content in the code block
|
14
62
|
const contentDOM = document.createElement('code');
|
15
|
-
contentDOM.setAttribute('data-code-block-language', language);
|
16
63
|
// Divider between code block and pre container
|
17
64
|
const dividerElement = document.createElement('div');
|
18
65
|
dividerElement.classList.add('block-divider');
|
@@ -26,5 +73,24 @@ class ExtendedCodeBlockExtension extends extension_code_block_1.CodeBlockExtensi
|
|
26
73
|
return { dom, contentDOM };
|
27
74
|
};
|
28
75
|
}
|
29
|
-
|
30
|
-
|
76
|
+
/**
|
77
|
+
* Toggle the <code> for the current block.
|
78
|
+
*/
|
79
|
+
toggleCodeBlock() {
|
80
|
+
return (0, core_1.toggleBlockItem)({
|
81
|
+
type: this.type,
|
82
|
+
toggleType: 'paragraph',
|
83
|
+
});
|
84
|
+
}
|
85
|
+
};
|
86
|
+
__decorate([
|
87
|
+
(0, core_1.command)()
|
88
|
+
], CodeBlockExtension.prototype, "toggleCodeBlock", null);
|
89
|
+
CodeBlockExtension = __decorate([
|
90
|
+
(0, core_1.extension)({
|
91
|
+
defaultOptions: {
|
92
|
+
enableDecorations: false,
|
93
|
+
},
|
94
|
+
})
|
95
|
+
], CodeBlockExtension);
|
96
|
+
exports.CodeBlockExtension = CodeBlockExtension;
|
@@ -15,4 +15,4 @@ export declare enum MarkName {
|
|
15
15
|
Link = "link",
|
16
16
|
AssetLink = "assetLink"
|
17
17
|
}
|
18
|
-
export declare const createExtensions: (context: EditorContextOptions) => () => Extension[];
|
18
|
+
export declare const createExtensions: (context: EditorContextOptions, enableDecorations?: boolean) => () => Extension[];
|
@@ -32,7 +32,7 @@ var MarkName;
|
|
32
32
|
MarkName["Link"] = "link";
|
33
33
|
MarkName["AssetLink"] = "assetLink";
|
34
34
|
})(MarkName = exports.MarkName || (exports.MarkName = {}));
|
35
|
-
const createExtensions = (context) => {
|
35
|
+
const createExtensions = (context, enableDecorations = true) => {
|
36
36
|
return () => {
|
37
37
|
return [
|
38
38
|
new CommandsExtension_1.CommandsExtension(),
|
@@ -43,8 +43,8 @@ const createExtensions = (context) => {
|
|
43
43
|
new extensions_1.NodeFormattingExtension({ indents: [] }),
|
44
44
|
new extensions_1.ParagraphExtension(),
|
45
45
|
new extensions_1.HardBreakExtension(),
|
46
|
-
new
|
47
|
-
new
|
46
|
+
new CodeBlockExtension_1.CodeBlockExtension({ enableDecorations }),
|
47
|
+
new PreformattedExtension_1.PreformattedExtension({ enableDecorations }),
|
48
48
|
new extensions_1.UnderlineExtension(),
|
49
49
|
new extensions_1.HistoryExtension(),
|
50
50
|
new ImageExtension_1.ImageExtension(),
|
@@ -1,10 +1,13 @@
|
|
1
1
|
import { ApplySchemaAttributes, CommandFunction, NodeExtension, NodeExtensionSpec, NodeSpecOverride } from '@remirror/core';
|
2
2
|
import { NodeViewMethod } from 'remirror';
|
3
|
-
export
|
3
|
+
export type PreformattedExtensionOptions = {
|
4
|
+
enableDecorations?: boolean;
|
5
|
+
};
|
6
|
+
export declare class PreformattedExtension extends NodeExtension<PreformattedExtensionOptions> {
|
4
7
|
get name(): "preformatted";
|
5
8
|
createTags(): ("block" | "formattingNode" | "textBlock")[];
|
6
9
|
createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec;
|
7
|
-
createNodeViews(): NodeViewMethod
|
10
|
+
createNodeViews(): NodeViewMethod | Record<string, never>;
|
8
11
|
/**
|
9
12
|
* Toggle the <pre> for the current block.
|
10
13
|
*/
|
@@ -35,6 +35,9 @@ let PreformattedExtension = class PreformattedExtension extends core_1.NodeExten
|
|
35
35
|
};
|
36
36
|
}
|
37
37
|
createNodeViews() {
|
38
|
+
if (!this.options.enableDecorations) {
|
39
|
+
return {};
|
40
|
+
}
|
38
41
|
return (node) => {
|
39
42
|
const { nodeTextAlignment } = node.attrs;
|
40
43
|
// This is the pre container for the code block
|
@@ -71,6 +74,10 @@ __decorate([
|
|
71
74
|
(0, core_1.command)()
|
72
75
|
], PreformattedExtension.prototype, "togglePreformatted", null);
|
73
76
|
PreformattedExtension = __decorate([
|
74
|
-
(0, core_1.extension)({
|
77
|
+
(0, core_1.extension)({
|
78
|
+
defaultOptions: {
|
79
|
+
enableDecorations: false,
|
80
|
+
},
|
81
|
+
})
|
75
82
|
], PreformattedExtension);
|
76
83
|
exports.PreformattedExtension = PreformattedExtension;
|
package/lib/hooks/index.d.ts
CHANGED
@@ -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';
|
package/lib/hooks/index.js
CHANGED
@@ -14,5 +14,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
15
15
|
};
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
17
|
-
__exportStar(require("./useExtensionNames"), exports);
|
18
|
-
__exportStar(require("./useExpandedSelection"), exports);
|
17
|
+
__exportStar(require("./useExtensionNames/useExtensionNames"), exports);
|
18
|
+
__exportStar(require("./useExpandedSelection/useExpandedSelection"), exports);
|
19
|
+
__exportStar(require("./useFocus/useFocus"), exports);
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { FocusEvent as ReactFocusEvent, FocusEventHandler } from 'react';
|
2
|
+
export declare const useFocus: (initialState: boolean, isChildElement: (element: Element) => boolean) => {
|
3
|
+
handleFocus: (event: ReactFocusEvent) => void;
|
4
|
+
handleBlur: FocusEventHandler<HTMLDivElement>;
|
5
|
+
isFocused: boolean;
|
6
|
+
};
|
@@ -1,16 +1,23 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useFocus = void 0;
|
3
4
|
const react_1 = require("react");
|
4
|
-
const useFocus = (initialState) => {
|
5
|
-
const
|
6
|
-
const
|
5
|
+
const useFocus = (initialState, isChildElement) => {
|
6
|
+
const [isFocused, setIsFocused] = (0, react_1.useState)(initialState);
|
7
|
+
const getFocusedElement = (0, react_1.useCallback)(() => {
|
8
|
+
let element = document.activeElement;
|
9
|
+
while (element instanceof HTMLIFrameElement) {
|
10
|
+
element = element.contentDocument?.activeElement || null;
|
11
|
+
}
|
12
|
+
return element?.parentElement ? element : null;
|
13
|
+
}, []);
|
7
14
|
const handleFocus = (0, react_1.useCallback)((event) => {
|
8
15
|
// Ignore elements flagged to be ignored, this allows us to add extra, clickable, elements
|
9
16
|
// without triggering a focus, such as action menus.
|
10
17
|
if (!event.target?.classList?.contains('fte-ignore') && !event.target?.closest('.fte-ignore')) {
|
11
|
-
|
18
|
+
setIsFocused(true);
|
12
19
|
}
|
13
|
-
}, [
|
20
|
+
}, []);
|
14
21
|
const handleBlur = (0, react_1.useCallback)((event) => {
|
15
22
|
// React event bubbling is interesting, it bubbles up the React tree rather than the DOM tree.
|
16
23
|
// The tree deviates when rendering portals (eg. for modals).
|
@@ -30,14 +37,21 @@ const useFocus = (initialState) => {
|
|
30
37
|
//
|
31
38
|
// An assumption here is that anything in a portal will only blur to another element that is also in the portal
|
32
39
|
// (and therefore still in our React tree resulting in the element still effectively being focused).
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
// TODO: PLATFORM-1611 this shit still doesn't work properly, notably issues with the link/image modals.
|
41
|
+
requestAnimationFrame(() => {
|
42
|
+
// "relatedTarget" in the event object will be null if the element gaining focus is in a different
|
43
|
+
// document to the element being blurred (eg. floating toolbar rendered in top level frame,
|
44
|
+
// editor rendered in iframe). instead grab the active element to determine current focus.
|
45
|
+
const isBlurringEditor = isChildElement(event.target);
|
46
|
+
const focusedElement = event.relatedTarget || getFocusedElement();
|
47
|
+
const isFocusedInEditor = focusedElement && isChildElement(focusedElement);
|
48
|
+
// Detect if the blur event happens when the related/clicked target is the floating popover
|
49
|
+
const isClickingFloatingToolbar = !!focusedElement?.closest('.squiz-fte-scope__floating-popover');
|
50
|
+
if (isBlurringEditor && !isFocusedInEditor && !isClickingFloatingToolbar) {
|
51
|
+
setIsFocused(false);
|
52
|
+
}
|
53
|
+
});
|
54
|
+
}, [getFocusedElement, isChildElement]);
|
55
|
+
return { handleFocus, handleBlur, isFocused };
|
42
56
|
};
|
43
|
-
exports.
|
57
|
+
exports.useFocus = useFocus;
|
package/lib/index.css
CHANGED
@@ -5145,6 +5145,11 @@
|
|
5145
5145
|
border-width: 2px;
|
5146
5146
|
border-style: solid;
|
5147
5147
|
}
|
5148
|
+
.squiz-fte-scope.squiz-fte-scope__editor--empty {
|
5149
|
+
height: 0px;
|
5150
|
+
width: 0px;
|
5151
|
+
border-width: 0px;
|
5152
|
+
}
|
5148
5153
|
.squiz-fte-scope.squiz-fte-scope__editor--is-disabled {
|
5149
5154
|
cursor: not-allowed;
|
5150
5155
|
border-width: 0px;
|
@@ -5511,11 +5516,11 @@
|
|
5511
5516
|
padding: 6px 8px;
|
5512
5517
|
}
|
5513
5518
|
.squiz-fte-scope .header-toolbar {
|
5519
|
+
opacity: 0;
|
5520
|
+
max-height: 0;
|
5514
5521
|
transition-duration: 0.3s;
|
5515
5522
|
transition-property: max-height, opacity;
|
5516
5523
|
transition-timing-function: ease-out;
|
5517
|
-
opacity: 0;
|
5518
|
-
max-height: 0;
|
5519
5524
|
}
|
5520
5525
|
.squiz-fte-scope .header-toolbar.show-toolbar {
|
5521
5526
|
opacity: 1;
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
19
|
+
if (mod && mod.__esModule) return mod;
|
20
|
+
var result = {};
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
22
|
+
__setModuleDefault(result, mod);
|
23
|
+
return result;
|
24
|
+
};
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
26
|
+
exports.EditorInput = void 0;
|
27
|
+
const react_1 = require("@remirror/react");
|
28
|
+
const react_2 = __importStar(require("react"));
|
29
|
+
const react_dom_1 = require("react-dom");
|
30
|
+
const EditorInput = ({ container, ...other }) => {
|
31
|
+
const { getRootProps } = (0, react_1.useRemirrorContext)();
|
32
|
+
const { key, ...rootProps } = getRootProps();
|
33
|
+
const preventImagePaste = (0, react_2.useCallback)((event) => {
|
34
|
+
const { clipboardData } = event;
|
35
|
+
const pastedData = clipboardData?.files[0];
|
36
|
+
if (pastedData?.type &&
|
37
|
+
pastedData?.type.startsWith('image/') &&
|
38
|
+
// Still allow paste of any text that came through (Word, etc)
|
39
|
+
!clipboardData?.types.includes('text/plain')) {
|
40
|
+
event.preventDefault();
|
41
|
+
}
|
42
|
+
// Allow other paste event handlers to be run.
|
43
|
+
return false;
|
44
|
+
}, []);
|
45
|
+
const input = react_2.default.createElement("div", { key: key, ...rootProps, ...other });
|
46
|
+
(0, react_1.useEditorEvent)('paste', preventImagePaste);
|
47
|
+
return container ? (0, react_dom_1.createPortal)(input, container) : input;
|
48
|
+
};
|
49
|
+
exports.EditorInput = EditorInput;
|
@@ -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
@@ -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
|
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(
|
345
|
-
|
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(
|
362
|
-
|
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(
|
379
|
-
|
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 />);
|
@@ -462,4 +464,27 @@ describe('Formatted text editor', () => {
|
|
462
464
|
expect(within(document.body).getByRole('button', { name: 'Link (Ctrl+K)', hidden: true })).toBeInTheDocument();
|
463
465
|
expect(document.querySelector('.show-toolbar')).toBeInTheDocument();
|
464
466
|
});
|
467
|
+
|
468
|
+
it('Renders the input and toolbar in container elements if provided', () => {
|
469
|
+
const toolbar = document.createElement('div');
|
470
|
+
const input = document.createElement('div');
|
471
|
+
document.body.append(toolbar, input);
|
472
|
+
|
473
|
+
const { container, unmount } = render(<Editor containers={{ toolbar, input }} />);
|
474
|
+
|
475
|
+
// Toolbar and input should be rendered in the provided containers.
|
476
|
+
expect(within(input).getByRole('textbox')).toBeInTheDocument();
|
477
|
+
expect(within(toolbar).getByRole('button', { name: 'Bold (Ctrl+B)' })).toBeInTheDocument();
|
478
|
+
|
479
|
+
// They should not be rendered inside of the editor element.
|
480
|
+
expect(within(container).queryByRole('textbox')).not.toBeInTheDocument();
|
481
|
+
expect(within(container).queryByRole('button', { name: 'Bold (Ctrl+B)' })).not.toBeInTheDocument();
|
482
|
+
expect(container.querySelector('.squiz-fte-scope__editor--empty')).toBeInTheDocument();
|
483
|
+
|
484
|
+
unmount();
|
485
|
+
|
486
|
+
// Toolbar and input should be removed on unmount.
|
487
|
+
expect(within(input).queryByRole('textbox')).not.toBeInTheDocument();
|
488
|
+
expect(within(toolbar).queryByRole('button', { name: 'Bold (Ctrl+B)' })).not.toBeInTheDocument();
|
489
|
+
});
|
465
490
|
});
|
package/src/Editor/Editor.tsx
CHANGED
@@ -1,61 +1,49 @@
|
|
1
|
-
import React, { useContext, useCallback, ReactNode, useEffect } from 'react';
|
2
|
-
import {
|
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
|
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
|
-
|
18
|
-
|
23
|
+
enableDecorations?: boolean;
|
24
|
+
enableTableTool?: boolean;
|
19
25
|
isFocused?: boolean;
|
20
26
|
label?: string;
|
21
|
-
|
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
|
-
|
48
|
-
className,
|
31
|
+
attributes,
|
49
32
|
border = true,
|
50
|
-
editable = true,
|
51
|
-
onChange,
|
52
33
|
children,
|
53
|
-
|
54
|
-
|
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
|
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 (
|
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
|
-
|
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=
|
106
|
+
label={label}
|
105
107
|
attributes={attributes}
|
106
108
|
>
|
107
|
-
{editable &&
|
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
|
-
<
|
113
|
+
<EditorInput container={containers?.input} />
|
110
114
|
{enableTableTool && <TableComponents enableTableCellMenu={false} />}
|
111
|
-
{editable &&
|
115
|
+
{editable && isFocused && <FloatingToolbar />}
|
112
116
|
</Remirror>
|
113
117
|
</div>
|
114
118
|
);
|
package/src/Editor/_editor.scss
CHANGED
@@ -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
|
-
|
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
|
};
|