@squiz/formatted-text-editor 2.3.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 +12 -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 +21 -0
- package/lib/Editor/Editor.js +76 -0
- package/lib/Editor/EditorContext.d.ts +10 -0
- package/lib/Editor/EditorContext.js +16 -0
- package/lib/EditorToolbar/FloatingToolbar.d.ts +2 -0
- package/lib/EditorToolbar/FloatingToolbar.js +76 -0
- package/lib/EditorToolbar/Toolbar.d.ts +8 -0
- package/lib/EditorToolbar/Toolbar.js +51 -0
- package/lib/EditorToolbar/Tools/Bold/BoldButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Bold/BoldButton.js +23 -0
- package/lib/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/ClearFormatting/ClearFormattingButton.js +57 -0
- package/lib/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.d.ts +3 -0
- package/lib/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.js +104 -0
- package/lib/EditorToolbar/Tools/HorizontalLine/HorizontalLineButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/HorizontalLine/HorizontalLineButton.js +25 -0
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +19 -0
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +183 -0
- package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +6 -0
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +79 -0
- package/lib/EditorToolbar/Tools/Image/ImageModal.d.ts +9 -0
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +26 -0
- package/lib/EditorToolbar/Tools/Italic/ItalicButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Italic/ItalicButton.js +23 -0
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +19 -0
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +63 -0
- package/lib/EditorToolbar/Tools/Link/LinkButton.d.ts +6 -0
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +71 -0
- package/lib/EditorToolbar/Tools/Link/LinkModal.d.ts +9 -0
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +27 -0
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.d.ts +4 -0
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +54 -0
- package/lib/EditorToolbar/Tools/Lists/ListButtons.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Lists/ListButtons.js +14 -0
- package/lib/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Lists/OrderedList/OrderedListButton.js +22 -0
- package/lib/EditorToolbar/Tools/Lists/UnorderedList/UnorderedListButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Lists/UnorderedList/UnorderedListButton.js +22 -0
- package/lib/EditorToolbar/Tools/Redo/RedoButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Redo/RedoButton.js +22 -0
- package/lib/EditorToolbar/Tools/Table/TableButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Table/TableButton.js +20 -0
- package/lib/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.js +22 -0
- package/lib/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.js +22 -0
- package/lib/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.js +22 -0
- package/lib/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.js +22 -0
- package/lib/EditorToolbar/Tools/TextAlign/TextAlignButtons.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextAlign/TextAlignButtons.js +21 -0
- package/lib/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextType/CodeBlock/CodeBlockButton.js +22 -0
- package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.d.ts +6 -0
- package/lib/EditorToolbar/Tools/TextType/Heading/HeadingButton.js +37 -0
- package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextType/Paragraph/ParagraphButton.js +21 -0
- package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextType/Preformatted/PreformattedButton.js +22 -0
- package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.d.ts +3 -0
- package/lib/EditorToolbar/Tools/TextType/TextTypeDropdown.js +46 -0
- package/lib/EditorToolbar/Tools/Underline/UnderlineButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Underline/UnderlineButton.js +23 -0
- package/lib/EditorToolbar/Tools/Undo/UndoButton.d.ts +3 -0
- package/lib/EditorToolbar/Tools/Undo/UndoButton.js +22 -0
- package/lib/EditorToolbar/index.d.ts +2 -0
- package/lib/EditorToolbar/index.js +18 -0
- package/lib/Extensions/ClearFormattingExtension/ClearFormattingExtension.d.ts +5 -0
- package/lib/Extensions/ClearFormattingExtension/ClearFormattingExtension.js +63 -0
- package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.d.ts +15 -0
- package/lib/Extensions/CodeBlockExtension/CodeBlockExtension.js +96 -0
- package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
- package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
- package/lib/Extensions/Extensions.d.ts +18 -0
- package/lib/Extensions/Extensions.js +76 -0
- package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.d.ts +12 -0
- package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +69 -0
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +91 -0
- package/lib/Extensions/ImageExtension/DAMImageExtension.d.ts +17 -0
- package/lib/Extensions/ImageExtension/DAMImageExtension.js +97 -0
- package/lib/Extensions/ImageExtension/ImageExtension.d.ts +7 -0
- package/lib/Extensions/ImageExtension/ImageExtension.js +18 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +27 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +101 -0
- package/lib/Extensions/LinkExtension/LinkExtension.d.ts +23 -0
- package/lib/Extensions/LinkExtension/LinkExtension.js +87 -0
- package/lib/Extensions/LinkExtension/common.d.ts +7 -0
- package/lib/Extensions/LinkExtension/common.js +14 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +15 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +83 -0
- package/lib/Extensions/UnsuportedExtension/UnsupportedNodeExtension.d.ts +10 -0
- package/lib/Extensions/UnsuportedExtension/UnsupportedNodeExtension.js +76 -0
- package/lib/Icons/AiIcon.d.ts +2 -0
- package/lib/Icons/AiIcon.js +60 -0
- package/lib/hooks/index.d.ts +3 -0
- package/lib/hooks/index.js +19 -0
- package/lib/hooks/useExpandedSelection/useExpandedSelection.d.ts +23 -0
- package/lib/hooks/useExpandedSelection/useExpandedSelection.js +37 -0
- package/lib/hooks/useExtensionNames/useExtensionNames.d.ts +1 -0
- package/lib/hooks/useExtensionNames/useExtensionNames.js +16 -0
- package/lib/hooks/useFocus/useFocus.d.ts +6 -0
- package/lib/hooks/useFocus/useFocus.js +57 -0
- package/lib/index.css +5894 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +16 -0
- package/lib/types.d.ts +7 -0
- package/lib/types.js +2 -0
- package/lib/ui/Button/Button.d.ts +12 -0
- package/lib/ui/Button/Button.js +13 -0
- package/lib/ui/CollapseBox/CollapseBox.d.ts +7 -0
- package/lib/ui/CollapseBox/CollapseBox.js +48 -0
- 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/ui/Fields/Checkbox/Checkbox.d.ts +9 -0
- package/lib/ui/Fields/Checkbox/Checkbox.js +47 -0
- package/lib/ui/Fields/Input/Input.d.ts +3 -0
- package/lib/ui/Fields/Input/Input.js +33 -0
- package/lib/ui/Fields/InputContainer/InputContainer.d.ts +9 -0
- package/lib/ui/Fields/InputContainer/InputContainer.js +16 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +19 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +30 -0
- package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.d.ts +28 -0
- package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.js +88 -0
- package/lib/ui/Modal/FormModal.d.ts +5 -0
- package/lib/ui/Modal/FormModal.js +39 -0
- package/lib/ui/Modal/Modal.d.ts +11 -0
- package/lib/ui/Modal/Modal.js +79 -0
- package/lib/ui/Tabs/Tabs.d.ts +11 -0
- package/lib/ui/Tabs/Tabs.js +46 -0
- package/lib/ui/ToolbarDropdown/ToolbarDropdown.d.ts +7 -0
- package/lib/ui/ToolbarDropdown/ToolbarDropdown.js +48 -0
- package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.d.ts +11 -0
- package/lib/ui/ToolbarDropdownButton/ToolbarDropdownButton.js +15 -0
- package/lib/utils/converters/htmlToSquizNode/htmlToSquizNode.d.ts +5 -0
- package/lib/utils/converters/htmlToSquizNode/htmlToSquizNode.js +23 -0
- package/lib/utils/converters/htmlToSquizNode/htmlToSquizNode.props.d.ts +3 -0
- package/lib/utils/converters/htmlToSquizNode/htmlToSquizNode.props.js +2 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +11 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +235 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +208 -0
- package/lib/utils/createToolbarPositioner.d.ts +18 -0
- package/lib/utils/createToolbarPositioner.js +96 -0
- package/lib/utils/getCursorRect.d.ts +2 -0
- package/lib/utils/getCursorRect.js +7 -0
- package/lib/utils/getMarkNamesByGroup.d.ts +2 -0
- package/lib/utils/getMarkNamesByGroup.js +9 -0
- package/lib/utils/getNodeNamesByGroup.d.ts +2 -0
- package/lib/utils/getNodeNamesByGroup.js +9 -0
- package/lib/utils/getShortcutSymbol.d.ts +1 -0
- package/lib/utils/getShortcutSymbol.js +8 -0
- package/lib/utils/undefinedIfEmpty.d.ts +1 -0
- package/lib/utils/undefinedIfEmpty.js +7 -0
- package/lib/utils/validation.d.ts +3 -0
- package/lib/utils/validation.js +16 -0
- 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/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/src/hooks/{useExpandedSelection.ts → useExpandedSelection/useExpandedSelection.ts} +0 -0
- /package/src/hooks/{useExtensionNames.ts → useExtensionNames/useExtensionNames.ts} +0 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.createToolbarPositioner = void 0;
|
4
|
+
const extensions_1 = require("remirror/extensions");
|
5
|
+
const remirror_1 = require("remirror");
|
6
|
+
const getCursorRect_1 = require("./getCursorRect");
|
7
|
+
/* istanbul ignore next */
|
8
|
+
const createToolbarPositioner = ({ types }) => {
|
9
|
+
// Inspired by "createMarkPositioner".
|
10
|
+
// See: https://github.com/remirror/remirror/blob/107cba/packages/remirror__extension-positioner/src/core-positioners.ts#L267
|
11
|
+
return extensions_1.Positioner.create({
|
12
|
+
hasChanged: extensions_1.hasStateChanged,
|
13
|
+
getActive: (props) => {
|
14
|
+
const { state, view } = props;
|
15
|
+
try {
|
16
|
+
const selection = (0, remirror_1.getTextSelection)(state.selection, state.doc);
|
17
|
+
const cursor = { from: view.coordsAtPos(selection.from), to: view.coordsAtPos(selection.to) };
|
18
|
+
const data = {
|
19
|
+
isSelectionInView: (0, extensions_1.isPositionVisible)((0, getCursorRect_1.getCursorRect)(cursor.from), view.dom) ||
|
20
|
+
(0, extensions_1.isPositionVisible)((0, getCursorRect_1.getCursorRect)(cursor.to), view.dom),
|
21
|
+
cursor: cursor,
|
22
|
+
visible: false,
|
23
|
+
marks: {},
|
24
|
+
};
|
25
|
+
data.visible = !selection.empty && data.isSelectionInView;
|
26
|
+
types.forEach((type) => {
|
27
|
+
const markRange = (0, remirror_1.getMarkRange)(selection.$from, type, selection.$to);
|
28
|
+
if (!markRange) {
|
29
|
+
data.marks[type] = { isActive: false, isExclusivelyActive: false };
|
30
|
+
return;
|
31
|
+
}
|
32
|
+
// exclusively active =
|
33
|
+
// the entire selection has the mark applied.
|
34
|
+
// active =
|
35
|
+
// at least part of the selection has the mark applied. "getMarkRanges" will return an empty array
|
36
|
+
// if this isn't the case. if there is no selection the cursor must be within the bounds of the mark,
|
37
|
+
// not on the edges.
|
38
|
+
const isExclusivelyActive = selection.empty
|
39
|
+
? selection.from > markRange.from && selection.to < markRange.to
|
40
|
+
: selection.from >= markRange.from && selection.to <= markRange.to;
|
41
|
+
const isActive = selection.empty ? selection.from > markRange.from && selection.to < markRange.to : true;
|
42
|
+
// the toolbar will be visible if there is either a selection, or we are within the bounds of a mark
|
43
|
+
// we have formatting tools for.
|
44
|
+
data.visible = data.visible || isExclusivelyActive;
|
45
|
+
data.marks[type] = { isExclusivelyActive, isActive };
|
46
|
+
});
|
47
|
+
return data.visible ? [data] : extensions_1.Positioner.EMPTY;
|
48
|
+
}
|
49
|
+
catch {
|
50
|
+
return extensions_1.Positioner.EMPTY;
|
51
|
+
}
|
52
|
+
},
|
53
|
+
getPosition: (props) => {
|
54
|
+
const { element, data, view } = props;
|
55
|
+
const { cursor, visible } = data;
|
56
|
+
const { from, to } = cursor;
|
57
|
+
const parent = element.offsetParent ?? view.dom;
|
58
|
+
const parentRect = parent.getBoundingClientRect();
|
59
|
+
const height = Math.abs(to.bottom - from.top);
|
60
|
+
// Hack to get JSDOM to work, positioning doesn't work great here.
|
61
|
+
if (isNaN(parentRect.top)) {
|
62
|
+
return {
|
63
|
+
rect: new DOMRect(0, 0, 0, 0),
|
64
|
+
y: 0,
|
65
|
+
x: 0,
|
66
|
+
height: 0,
|
67
|
+
width: 0,
|
68
|
+
visible: false,
|
69
|
+
};
|
70
|
+
}
|
71
|
+
// True when the selection spans multiple lines.
|
72
|
+
const spansMultipleLines = height > from.bottom - from.top;
|
73
|
+
// The position furthest to the left.
|
74
|
+
const leftmost = Math.min(from.left, to.left);
|
75
|
+
// The position nearest the top.
|
76
|
+
const topmost = Math.min(from.top, to.top);
|
77
|
+
const bottommost = Math.max(from.bottom, to.bottom);
|
78
|
+
const left = parent.scrollLeft + (spansMultipleLines ? to.left - parentRect.left : leftmost - parentRect.left);
|
79
|
+
const top = parent.scrollTop + topmost - parentRect.top;
|
80
|
+
const width = spansMultipleLines ? 1 : Math.abs(from.left - to.right);
|
81
|
+
const rect = new DOMRect(spansMultipleLines ? to.left : leftmost, topmost, width, height);
|
82
|
+
// Get header toolbar by class inside the current element/editor
|
83
|
+
const headerToolbar = element.parentElement?.getElementsByClassName('header-toolbar')?.[0];
|
84
|
+
const headerBarRects = headerToolbar?.getBoundingClientRect();
|
85
|
+
const headerBarHeight = headerBarRects?.height ?? 0;
|
86
|
+
const headerBarBottom = (headerBarRects?.top ?? 0) + headerBarHeight;
|
87
|
+
// If floating toolbar will overlap fixed toolbar, set position to under selection
|
88
|
+
const isOverlapping = headerBarBottom + headerBarHeight > topmost;
|
89
|
+
// This is a good bottom position instead of it being on top of the toolbar when there is an overlap
|
90
|
+
const bottomPosition = parent.scrollTop + bottommost - parentRect.top + 62;
|
91
|
+
const y = isOverlapping ? bottomPosition : top;
|
92
|
+
return { rect, y, x: left, height, width, visible };
|
93
|
+
},
|
94
|
+
});
|
95
|
+
};
|
96
|
+
exports.createToolbarPositioner = createToolbarPositioner;
|
@@ -0,0 +1,7 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getCursorRect = void 0;
|
4
|
+
const getCursorRect = (coords) => {
|
5
|
+
return new DOMRect(coords.left, coords.top, 1, coords.top - coords.bottom);
|
6
|
+
};
|
7
|
+
exports.getCursorRect = getCursorRect;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getMarkNamesByGroup = void 0;
|
4
|
+
const getMarkNamesByGroup = (schema, group) => {
|
5
|
+
return Object.values(schema.marks)
|
6
|
+
.filter((mark) => mark.spec.group?.includes(group))
|
7
|
+
.map((mark) => mark.name);
|
8
|
+
};
|
9
|
+
exports.getMarkNamesByGroup = getMarkNamesByGroup;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getNodeNamesByGroup = void 0;
|
4
|
+
const getNodeNamesByGroup = (schema, group) => {
|
5
|
+
return Object.values(schema.nodes)
|
6
|
+
.filter((node) => node.spec.group?.includes(group))
|
7
|
+
.map((node) => node.name);
|
8
|
+
};
|
9
|
+
exports.getNodeNamesByGroup = getNodeNamesByGroup;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const getShortcutSymbol: () => string;
|
@@ -0,0 +1,8 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getShortcutSymbol = void 0;
|
4
|
+
const getShortcutSymbol = () => {
|
5
|
+
// If we can detect Mac then we return "⌘" otherwise default to "Ctrl"
|
6
|
+
return window.navigator.userAgent.toLowerCase().indexOf('mac') > -1 ? '⌘' : 'Ctrl';
|
7
|
+
};
|
8
|
+
exports.getShortcutSymbol = getShortcutSymbol;
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const undefinedIfEmpty: <T extends object>(object: T) => T | undefined;
|
@@ -0,0 +1,7 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.undefinedIfEmpty = void 0;
|
4
|
+
const undefinedIfEmpty = (object) => {
|
5
|
+
return Object.keys(object).length > 0 ? object : undefined;
|
6
|
+
};
|
7
|
+
exports.undefinedIfEmpty = undefinedIfEmpty;
|
@@ -0,0 +1,3 @@
|
|
1
|
+
export declare const noEmptySpacesValidation: (value: string | undefined) => "Empty space is not allowed" | undefined;
|
2
|
+
export declare const hasProperties: <T>(message: string, properties: (keyof T)[]) => (value: T) => string | undefined;
|
3
|
+
export declare const regexDataURI: RegExp;
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.regexDataURI = exports.hasProperties = exports.noEmptySpacesValidation = void 0;
|
4
|
+
const noEmptySpacesValidation = (value) => {
|
5
|
+
if (value && !(value.trim().length > 0)) {
|
6
|
+
return 'Empty space is not allowed';
|
7
|
+
}
|
8
|
+
};
|
9
|
+
exports.noEmptySpacesValidation = noEmptySpacesValidation;
|
10
|
+
const hasProperties = (message, properties) => (value) => {
|
11
|
+
if (!value || properties.filter((property) => value[property]).length !== properties.length) {
|
12
|
+
return message;
|
13
|
+
}
|
14
|
+
};
|
15
|
+
exports.hasProperties = hasProperties;
|
16
|
+
exports.regexDataURI = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)$/i;
|
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
|
};
|
@@ -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 {
|
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<
|
8
|
+
const { toggleCodeBlock } = useCommands<CodeBlockExtension>();
|
9
9
|
|
10
|
-
const active = useActive<
|
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,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
|
+
});
|