@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
package/CHANGELOG.md
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
import React, { useState } from 'react';
|
2
|
-
import { Editor, remirrorNodeToSquizNode, squizNodeToRemirrorNode } from '
|
2
|
+
import { Editor, remirrorNodeToSquizNode, squizNodeToRemirrorNode } from '../../src';
|
3
3
|
import { RemirrorEventListener, Extension } from '@remirror/core';
|
4
4
|
import ReactDiffViewer from 'react-diff-viewer-continued';
|
5
5
|
import { AppContext } from './AppContext';
|
6
|
-
import Button from '
|
6
|
+
import Button from '../../src/ui/Button/Button';
|
7
7
|
import TextFieldsOutlinedIcon from '@mui/icons-material/TextFieldsOutlined';
|
8
8
|
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
9
9
|
import { VerticalDivider } from '@remirror/react-components';
|
10
10
|
import { Dialog, useDialogStore } from '@squiz/sds';
|
11
|
+
|
11
12
|
const ComponentHandlers = () => (
|
12
13
|
<div style={{ display: 'flex', justifyContent: 'flex-end', maxHeight: '2rem' }}>
|
13
14
|
<Button icon={<TextFieldsOutlinedIcon />} onClick={(x) => x} />
|
@@ -8,8 +8,7 @@ import MatrixResourceBrowserPlugin, {
|
|
8
8
|
import { ResourceBrowserContextProvider, ResourceBrowserSource } from '@squiz/resource-browser';
|
9
9
|
import React, { PropsWithChildren } from 'react';
|
10
10
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
11
|
-
import { EditorContext } from '
|
12
|
-
import { ResolveNodeType } from '../src/types';
|
11
|
+
import { EditorContext, ResolveNodeType } from '../../src';
|
13
12
|
import resources from './resources.json';
|
14
13
|
import sources from './sources.json';
|
15
14
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8" />
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon-dxp.svg" />
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7
|
+
<title>Formatted text editor - DXP package (SQUIZ)</title>
|
8
|
+
<style rel="./index.scss" lang="scss"></style>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
<div id="root"></div>
|
12
|
+
<script type="module" src="./main.tsx"></script>
|
13
|
+
</body>
|
14
|
+
</html>
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
2
2
|
import ReactDOM from 'react-dom/client';
|
3
3
|
import App from './App';
|
4
4
|
import '@squiz/resource-browser/lib/index.css';
|
5
|
-
import '
|
5
|
+
import '../../src/index.scss';
|
6
6
|
import './index.scss';
|
7
7
|
|
8
8
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
package/demo/index.html
CHANGED
@@ -5,9 +5,54 @@
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon-dxp.svg" />
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7
7
|
<title>Formatted text editor - DXP package (SQUIZ)</title>
|
8
|
+
<style>
|
9
|
+
pre {
|
10
|
+
white-space: pre-line;
|
11
|
+
}
|
12
|
+
</style>
|
8
13
|
</head>
|
9
14
|
<body>
|
10
|
-
<
|
11
|
-
<
|
15
|
+
<h1>Formatted text editor demo</h1>
|
16
|
+
<h2>Diff</h2>
|
17
|
+
<p>
|
18
|
+
<a href="./diff/index.html">View demo</a>
|
19
|
+
</p>
|
20
|
+
<pre>
|
21
|
+
Demonstrates how the formatted text editor is typically rendered.
|
22
|
+
|
23
|
+
Beneath the editor is 2 code blocks. On the left is the Remirror JSON representation of the content.
|
24
|
+
On the right is the Squiz JSON representation of the content.
|
25
|
+
|
26
|
+
You should typically expect there is no highlighted (red/green) content in either block. If there is
|
27
|
+
highlighted content this is indicative of the transformation logic not producing identical input
|
28
|
+
when run through a cycle of Remirror -> Squiz -> Remirror -> Squiz conversion. This may indicate that
|
29
|
+
the transformation logic is losing information during one of the transformation phases.
|
30
|
+
</pre>
|
31
|
+
|
32
|
+
<h2>Portals</h2>
|
33
|
+
<p>
|
34
|
+
<a href="./portals/index.html?withRemirror">View demo</a>
|
35
|
+
(<a href="./portals/index.html">without formatted text editor</a>)
|
36
|
+
</p>
|
37
|
+
<pre>
|
38
|
+
Demonstrates rendering the input and toolbar of a formatted text editor in arbitrary DOM locations.
|
39
|
+
|
40
|
+
Intended to show an "inline editing" variant where the editor will attempt to minimize how much it changes
|
41
|
+
the DOM where it is mounted. Including:
|
42
|
+
* Code/pre blocks don't have the decorative icons around them.
|
43
|
+
* Code blocks don't have a wrapping <pre> element.
|
44
|
+
|
45
|
+
The following manipulations still occur due to limitations which have not been overcome:
|
46
|
+
* Each editor input has 3 wrapping <div> elements.
|
47
|
+
* Each list item is wrapped in a <p> element.
|
48
|
+
* Remirror is strongly opinionated about its DOM structure. This is fine if the content being placed
|
49
|
+
in the editor was constructed by Remirror previously but if it wasn't then anything unexpected will
|
50
|
+
be transformed.
|
51
|
+
|
52
|
+
Including:
|
53
|
+
* Rendering the input in an iframe, with the other parts of the editor rendered outside.
|
54
|
+
* Attaching an editor to formatted content that is dynamically mounted after initially loaded
|
55
|
+
(collapse/expand accordion).
|
56
|
+
</pre>
|
12
57
|
</body>
|
13
58
|
</html>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
2
|
+
import clsx from 'clsx';
|
3
|
+
|
4
|
+
export type AccordionProps = {
|
5
|
+
items: Array<{ title: string; content: string; defaultExpanded: boolean }>;
|
6
|
+
};
|
7
|
+
|
8
|
+
export const Accordion = ({ items }: AccordionProps) => {
|
9
|
+
const [expandedItems, setExpandedItems] = useState(() => items.map((item) => !!item.defaultExpanded));
|
10
|
+
const toggleItem = useCallback((index: number) => {
|
11
|
+
setExpandedItems((previous) => {
|
12
|
+
const updated = [...previous];
|
13
|
+
updated[index] = !previous[index];
|
14
|
+
return updated;
|
15
|
+
});
|
16
|
+
}, []);
|
17
|
+
|
18
|
+
return (
|
19
|
+
<>
|
20
|
+
{items.map((item, index) => (
|
21
|
+
<div
|
22
|
+
key={index}
|
23
|
+
className={clsx(
|
24
|
+
'accordion-item',
|
25
|
+
expandedItems[index] && 'accordion-item-expanded',
|
26
|
+
!expandedItems[index] && 'accordion-item-collapsed',
|
27
|
+
)}
|
28
|
+
>
|
29
|
+
<button
|
30
|
+
type="button"
|
31
|
+
className="accordion-item-header"
|
32
|
+
data-sq-content-type="string"
|
33
|
+
data-sq-field="items[0].title"
|
34
|
+
onClick={() => toggleItem(index)}
|
35
|
+
>
|
36
|
+
{item.title}
|
37
|
+
</button>
|
38
|
+
{expandedItems[index] && (
|
39
|
+
<div
|
40
|
+
className="accordion-item-content"
|
41
|
+
data-sq-content-type="formatted-text"
|
42
|
+
data-sq-field="items[0].content"
|
43
|
+
dangerouslySetInnerHTML={{ __html: item.content }}
|
44
|
+
></div>
|
45
|
+
)}
|
46
|
+
</div>
|
47
|
+
))}
|
48
|
+
</>
|
49
|
+
);
|
50
|
+
};
|
@@ -0,0 +1,150 @@
|
|
1
|
+
import { Editor } from '../../src';
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
3
|
+
|
4
|
+
const editorIdentifierAttribute = 'data-sq-editor-key';
|
5
|
+
let editorIndex = 0;
|
6
|
+
|
7
|
+
const extractFormattedTextBlocks = (root: Element): Record<string, { container: Element; content: string }> => {
|
8
|
+
const selector = '[data-sq-content-type="formatted-text"][data-sq-block-index]';
|
9
|
+
const ungrouped = root.matches(selector) ? [root] : Array.from(root.querySelectorAll(selector));
|
10
|
+
const grouped: Record<string, Array<Element>> = {};
|
11
|
+
const editors: Record<string, { container: Element; content: string }> = {};
|
12
|
+
|
13
|
+
ungrouped?.forEach((element) => {
|
14
|
+
if (!element.parentElement) {
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
|
18
|
+
const index = element.getAttribute('data-sq-block-index') as string;
|
19
|
+
|
20
|
+
grouped[index] ||= [];
|
21
|
+
grouped[index].push(element);
|
22
|
+
});
|
23
|
+
|
24
|
+
Object.values(grouped).forEach((elements) => {
|
25
|
+
const container = document.createElement('div');
|
26
|
+
const content = elements.map((element) => element.outerHTML).join('');
|
27
|
+
const key = `editor-${editorIndex++}`;
|
28
|
+
|
29
|
+
container.setAttribute(editorIdentifierAttribute, key);
|
30
|
+
elements[0].parentElement?.insertBefore(container, elements[0]);
|
31
|
+
elements.forEach((element) => element.remove());
|
32
|
+
|
33
|
+
editors[key] = { container, content };
|
34
|
+
});
|
35
|
+
|
36
|
+
return editors;
|
37
|
+
};
|
38
|
+
|
39
|
+
const extractFormattedTextComponentInputs = (
|
40
|
+
root: Element,
|
41
|
+
): Record<string, { container: Element; content: string }> => {
|
42
|
+
const selector = '[data-sq-content-type="formatted-text"][data-sq-field]';
|
43
|
+
const elements = root.matches(selector) ? [root] : Array.from(root.querySelectorAll(selector));
|
44
|
+
const editors: Record<string, { container: Element; content: string }> = {};
|
45
|
+
|
46
|
+
elements.forEach((element) => {
|
47
|
+
const container = element;
|
48
|
+
const content = container.innerHTML;
|
49
|
+
let key = container.getAttribute(editorIdentifierAttribute);
|
50
|
+
|
51
|
+
if (!key) {
|
52
|
+
key = `editor-${editorIndex++}`;
|
53
|
+
container.setAttribute(editorIdentifierAttribute, key);
|
54
|
+
}
|
55
|
+
|
56
|
+
container.innerHTML = '';
|
57
|
+
|
58
|
+
editors[key] = { container, content };
|
59
|
+
});
|
60
|
+
|
61
|
+
return editors;
|
62
|
+
};
|
63
|
+
|
64
|
+
export const App = () => {
|
65
|
+
const toolbar = useRef<HTMLDivElement>(null);
|
66
|
+
const preview = useRef<HTMLIFrameElement>(null);
|
67
|
+
const mutationObserver = useRef<MutationObserver>();
|
68
|
+
const [, setIsMounting] = useState(true);
|
69
|
+
const [editors, setEditors] = useState<Record<string, { container: Element; content: string }>>({});
|
70
|
+
const withRemirror = window.location.search.includes('withRemirror');
|
71
|
+
const handleLoad = useCallback(() => {
|
72
|
+
if (!withRemirror) {
|
73
|
+
return;
|
74
|
+
}
|
75
|
+
|
76
|
+
const document = preview.current?.contentDocument;
|
77
|
+
|
78
|
+
if (!document) {
|
79
|
+
throw new Error('Preview is not loaded.');
|
80
|
+
}
|
81
|
+
|
82
|
+
setEditors({
|
83
|
+
...extractFormattedTextBlocks(preview.current.contentDocument.body),
|
84
|
+
...extractFormattedTextComponentInputs(preview.current.contentDocument.body),
|
85
|
+
});
|
86
|
+
}, [withRemirror]);
|
87
|
+
|
88
|
+
useEffect(() => {
|
89
|
+
mutationObserver.current?.disconnect();
|
90
|
+
|
91
|
+
if (!preview.current?.contentDocument) {
|
92
|
+
return;
|
93
|
+
}
|
94
|
+
|
95
|
+
const updatedEditors = { ...editors };
|
96
|
+
let hasUpdatedEditors = false;
|
97
|
+
|
98
|
+
mutationObserver.current = new MutationObserver((mutations) => {
|
99
|
+
mutations.forEach((mutation) => {
|
100
|
+
if (mutation.removedNodes.length > 0) {
|
101
|
+
Object.keys(updatedEditors).forEach((key) => {
|
102
|
+
if (!updatedEditors[key].container.parentElement) {
|
103
|
+
delete updatedEditors[key];
|
104
|
+
hasUpdatedEditors = true;
|
105
|
+
}
|
106
|
+
});
|
107
|
+
} else if (mutation.addedNodes.length > 0) {
|
108
|
+
mutation.addedNodes.forEach((node) => {
|
109
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
110
|
+
const newEditors = extractFormattedTextComponentInputs(node as Element);
|
111
|
+
Object.assign(updatedEditors, newEditors);
|
112
|
+
hasUpdatedEditors ||= Object.keys(newEditors).length > 0;
|
113
|
+
}
|
114
|
+
});
|
115
|
+
}
|
116
|
+
});
|
117
|
+
|
118
|
+
if (hasUpdatedEditors) {
|
119
|
+
setEditors(updatedEditors);
|
120
|
+
}
|
121
|
+
});
|
122
|
+
|
123
|
+
mutationObserver.current.observe(preview.current?.contentDocument, {
|
124
|
+
childList: true,
|
125
|
+
subtree: true,
|
126
|
+
});
|
127
|
+
}, [editors]);
|
128
|
+
|
129
|
+
useEffect(() => {
|
130
|
+
setIsMounting(false);
|
131
|
+
}, []);
|
132
|
+
|
133
|
+
return (
|
134
|
+
<>
|
135
|
+
<div ref={toolbar} id="toolbar"></div>
|
136
|
+
<iframe ref={preview} id="preview" title="Preview" src="./preview.html" onLoad={handleLoad} />
|
137
|
+
{Object.keys(editors).map((key) => (
|
138
|
+
<Editor
|
139
|
+
key={key}
|
140
|
+
enableDecorations={false}
|
141
|
+
content={editors[key].content}
|
142
|
+
containers={{
|
143
|
+
input: editors[key].container,
|
144
|
+
toolbar: toolbar.current,
|
145
|
+
}}
|
146
|
+
/>
|
147
|
+
))}
|
148
|
+
</>
|
149
|
+
);
|
150
|
+
};
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8" />
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon-dxp.svg" />
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7
|
+
<title>Formatted text editor - DXP package (SQUIZ)</title>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<div id="root"></div>
|
11
|
+
<script type="module" src="./index.tsx"></script>
|
12
|
+
</body>
|
13
|
+
</html>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import ReactDOM from 'react-dom/client';
|
3
|
+
import { App } from './App';
|
4
|
+
import '@squiz/resource-browser/lib/index.css';
|
5
|
+
import '../../src/index.scss';
|
6
|
+
import './index.scss';
|
7
|
+
|
8
|
+
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
9
|
+
<React.StrictMode>
|
10
|
+
<App />
|
11
|
+
</React.StrictMode>,
|
12
|
+
);
|
@@ -0,0 +1,91 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>Remirror portal example</title>
|
4
|
+
<style>
|
5
|
+
body {
|
6
|
+
font-family: Palatino, Georgia, Times, 'Times New Roman', Serif;
|
7
|
+
}
|
8
|
+
|
9
|
+
#content {
|
10
|
+
margin: 24px;
|
11
|
+
}
|
12
|
+
|
13
|
+
.separator {
|
14
|
+
background-color: yellow;
|
15
|
+
}
|
16
|
+
|
17
|
+
.underline {
|
18
|
+
text-decoration: underline;
|
19
|
+
}
|
20
|
+
|
21
|
+
.accordion-item-collapsed .accordion-item-content {
|
22
|
+
display: none;
|
23
|
+
}
|
24
|
+
</style>
|
25
|
+
</head>
|
26
|
+
<body>
|
27
|
+
<div id="content" spellcheck="false">
|
28
|
+
<h1 data-sq-content-type="formatted-text" data-sq-block-index="1">Remirror example</h1>
|
29
|
+
<!-- comment goes here la -->
|
30
|
+
<div data-sq-content-type="formatted-text" data-sq-block-index="1">
|
31
|
+
Australian governments at all levels have
|
32
|
+
<u
|
33
|
+
><strong><em>endorsed</em></strong></u
|
34
|
+
>
|
35
|
+
WCAG 2.0, and require all government websites (federal, state and territory) to meet the new guidelines at the
|
36
|
+
minimum compliance level (Single A) by the end of 2012. In addition, the Australian Government requires all
|
37
|
+
federal websites to meet the medium conformance level (Double A) by the end of 2014.
|
38
|
+
</div>
|
39
|
+
<p data-sq-content-type="formatted-text" data-sq-block-index="1">
|
40
|
+
Federal government agencies must update <strong>all government</strong> websites (as specified within scope
|
41
|
+
under the Website Accessibility National Transition Strategy (NTS)) to WCAG 2.0 conformance. Agencies should use
|
42
|
+
the principle of progressive enhancement when building and managing websites, and test for conformance across
|
43
|
+
multiple browsers and operating system configurations.
|
44
|
+
</p>
|
45
|
+
|
46
|
+
<p class="separator">The below section shows most formatting options/supported element tags.</p>
|
47
|
+
|
48
|
+
<p data-sq-content-type="formatted-text" data-sq-block-index="2">
|
49
|
+
<img
|
50
|
+
src="https://www.squiz.net/__data/assets/file/0014/11075/glyphsquiz-darkblue.svg"
|
51
|
+
alt="Squiz logo"
|
52
|
+
width="200px"
|
53
|
+
/>
|
54
|
+
</p>
|
55
|
+
<p data-sq-content-type="formatted-text" data-sq-block-index="2">
|
56
|
+
Visit <a href="https://www.squiz.net/" target="_blank">squiz.net</a>.
|
57
|
+
</p>
|
58
|
+
<code data-sq-content-type="formatted-text" data-sq-block-index="2">This is a code block.</code>
|
59
|
+
<pre data-sq-content-type="formatted-text" data-sq-block-index="2">This is a pre block.</pre>
|
60
|
+
<ul data-sq-content-type="formatted-text" data-sq-block-index="2">
|
61
|
+
<li>Item 1</li>
|
62
|
+
<li>Item 2</li>
|
63
|
+
<li>Item 3</li>
|
64
|
+
</ul>
|
65
|
+
<ol data-sq-content-type="formatted-text" data-sq-block-index="2">
|
66
|
+
<li>Item 1</li>
|
67
|
+
<li>Item 2</li>
|
68
|
+
<li>Item 3</li>
|
69
|
+
</ol>
|
70
|
+
<hr data-sq-content-type="formatted-text" data-sq-block-index="2" />
|
71
|
+
<h1 data-sq-content-type="formatted-text" data-sq-block-index="2">Heading</h1>
|
72
|
+
<p data-sq-content-type="formatted-text" data-sq-block-index="2">
|
73
|
+
<strong>Bold</strong>, <em>Italics</em>, <span class="underline">underline</span>
|
74
|
+
</p>
|
75
|
+
|
76
|
+
<p class="separator">
|
77
|
+
The below section shows a React mounted element with formatted text input. The input is dynamically
|
78
|
+
added/removed from the DOM by clicking the button.
|
79
|
+
</p>
|
80
|
+
|
81
|
+
<div
|
82
|
+
data-sq-content-type="component"
|
83
|
+
data-sq-block-index="3"
|
84
|
+
class="accordion"
|
85
|
+
data-hydration-component="accordion"
|
86
|
+
data-hydration-props='{"items":[{"title":"Accordion title","content":"<p><strong>Accordion content</strong></p>","defaultExpanded":true}]}'
|
87
|
+
></div>
|
88
|
+
</div>
|
89
|
+
<script type="module" src="./preview.tsx"></script>
|
90
|
+
</body>
|
91
|
+
</html>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import ReactDOM from 'react-dom/client';
|
3
|
+
import { Accordion } from './Accordion';
|
4
|
+
|
5
|
+
document.querySelectorAll('[data-hydration-component="accordion"]').forEach((element) => {
|
6
|
+
const props = JSON.parse(element.getAttribute('data-hydration-props') || '');
|
7
|
+
const root = ReactDOM.createRoot(element);
|
8
|
+
|
9
|
+
root.render(<Accordion {...props} />);
|
10
|
+
});
|
package/lib/Editor/Editor.d.ts
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
import React, { ReactNode } from 'react';
|
2
2
|
import { RemirrorContentType, RemirrorEventListener, Extension } from '@remirror/core';
|
3
3
|
type EditorProps = {
|
4
|
+
attributes?: Record<string, string>;
|
5
|
+
border?: boolean;
|
6
|
+
children?: ReactNode;
|
4
7
|
className?: string;
|
8
|
+
containers?: {
|
9
|
+
input?: Element | null;
|
10
|
+
toolbar?: Element | null;
|
11
|
+
};
|
5
12
|
content?: RemirrorContentType;
|
6
|
-
onChange?: RemirrorEventListener<Extension>;
|
7
13
|
editable?: boolean;
|
8
|
-
|
9
|
-
|
14
|
+
enableDecorations?: boolean;
|
15
|
+
enableTableTool?: boolean;
|
10
16
|
isFocused?: boolean;
|
11
17
|
label?: string;
|
12
|
-
|
13
|
-
enableTableTool?: boolean;
|
18
|
+
onChange?: RemirrorEventListener<Extension>;
|
14
19
|
};
|
15
|
-
declare const Editor: ({
|
20
|
+
declare const Editor: ({ attributes, border, children, className, containers, content, editable, enableDecorations, enableTableTool, isFocused: isInitiallyFocused, label, onChange, }: EditorProps) => React.JSX.Element;
|
16
21
|
export default Editor;
|
package/lib/Editor/Editor.js
CHANGED
@@ -32,27 +32,13 @@ const clsx_1 = __importDefault(require("clsx"));
|
|
32
32
|
const EditorToolbar_1 = require("../EditorToolbar");
|
33
33
|
const EditorContext_1 = require("./EditorContext");
|
34
34
|
const Extensions_1 = require("../Extensions/Extensions");
|
35
|
-
const
|
35
|
+
const hooks_1 = require("../hooks");
|
36
36
|
const extension_react_tables_1 = require("@remirror/extension-react-tables");
|
37
|
-
const
|
38
|
-
|
39
|
-
|
40
|
-
const pastedData = clipboardData?.files[0];
|
41
|
-
if (pastedData?.type &&
|
42
|
-
pastedData?.type.startsWith('image/') &&
|
43
|
-
// Still allow paste of any text that came through (Word, etc)
|
44
|
-
!clipboardData?.types.includes('text/plain')) {
|
45
|
-
event.preventDefault();
|
46
|
-
}
|
47
|
-
// Allow other paste event handlers to be run.
|
48
|
-
return false;
|
49
|
-
}, []);
|
50
|
-
(0, react_2.useEditorEvent)('paste', preventImagePaste);
|
51
|
-
return react_1.default.createElement(react_2.EditorComponent, null);
|
52
|
-
};
|
53
|
-
const Editor = ({ content, className, border = true, editable = true, onChange, children, isFocused, attributes, enableTableTool = false, }) => {
|
37
|
+
const EditorInput_1 = require("../ui/EditorInput/EditorInput");
|
38
|
+
const Editor = ({ attributes, border = true, children, className, containers, content, editable = true, enableDecorations = true, enableTableTool = false, isFocused: isInitiallyFocused = false, label = 'Text editor', onChange, }) => {
|
39
|
+
const isEmpty = containers?.toolbar && containers.input && !children;
|
54
40
|
const { manager, state, setState } = (0, react_2.useRemirror)({
|
55
|
-
extensions: (0, Extensions_1.createExtensions)((0, react_1.useContext)(EditorContext_1.EditorContext)),
|
41
|
+
extensions: (0, Extensions_1.createExtensions)((0, react_1.useContext)(EditorContext_1.EditorContext), enableDecorations),
|
56
42
|
content,
|
57
43
|
selection: 'start',
|
58
44
|
stringHandler: 'html',
|
@@ -61,10 +47,15 @@ const Editor = ({ content, className, border = true, editable = true, onChange,
|
|
61
47
|
setState(parameter.state);
|
62
48
|
onChange?.(parameter);
|
63
49
|
};
|
64
|
-
const
|
50
|
+
const wrapperRef = (0, react_1.useRef)(null);
|
51
|
+
const { isFocused, handleFocus, handleBlur } = (0, hooks_1.useFocus)(isInitiallyFocused, (0, react_1.useCallback)((element) => {
|
52
|
+
return Boolean(wrapperRef.current?.contains(element) ||
|
53
|
+
containers?.input?.contains(element) ||
|
54
|
+
containers?.toolbar?.contains(element));
|
55
|
+
}, [containers?.input, containers?.toolbar]));
|
65
56
|
// On initial load, check if we need to focus the actual text content
|
66
57
|
(0, react_1.useEffect)(() => {
|
67
|
-
if (
|
58
|
+
if (isInitiallyFocused) {
|
68
59
|
manager.view.dom.focus();
|
69
60
|
}
|
70
61
|
// TODO: May want to come back to this and see if there's a better solution
|
@@ -74,12 +65,12 @@ const Editor = ({ content, className, border = true, editable = true, onChange,
|
|
74
65
|
button.setAttribute('type', 'button');
|
75
66
|
});
|
76
67
|
}, []);
|
77
|
-
return (react_1.default.createElement("div", { ref: wrapperRef,
|
78
|
-
react_1.default.createElement(react_2.Remirror, { manager: manager, initialContent: state, editable: editable, onChange: handleChange, placeholder: "Write something", label:
|
79
|
-
editable && react_1.default.createElement(EditorToolbar_1.Toolbar, { isVisible:
|
68
|
+
return (react_1.default.createElement("div", { ref: wrapperRef, onBlurCapture: handleBlur, onFocusCapture: handleFocus, className: (0, clsx_1.default)('squiz-fte-scope', 'squiz-fte-scope__editor', !editable && 'squiz-fte-scope__editor--is-disabled', border && 'squiz-fte-scope__editor--bordered', isEmpty && 'squiz-fte-scope__editor--empty', className) },
|
69
|
+
react_1.default.createElement(react_2.Remirror, { manager: manager, initialContent: state, editable: editable, onChange: handleChange, placeholder: "Write something", label: label, attributes: attributes },
|
70
|
+
editable && (react_1.default.createElement(EditorToolbar_1.Toolbar, { isVisible: isFocused, enableTableTool: enableTableTool, container: containers?.toolbar })),
|
80
71
|
children && react_1.default.createElement("div", { className: "squiz-fte-scope__editor__children" }, children),
|
81
|
-
react_1.default.createElement(
|
72
|
+
react_1.default.createElement(EditorInput_1.EditorInput, { container: containers?.input }),
|
82
73
|
enableTableTool && react_1.default.createElement(extension_react_tables_1.TableComponents, { enableTableCellMenu: false }),
|
83
|
-
editable &&
|
74
|
+
editable && isFocused && react_1.default.createElement(EditorToolbar_1.FloatingToolbar, null))));
|
84
75
|
};
|
85
76
|
exports.default = Editor;
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
2
2
|
type ToolbarProps = {
|
3
3
|
isVisible: boolean;
|
4
4
|
enableTableTool: boolean;
|
5
|
+
container?: Element | null;
|
5
6
|
};
|
6
|
-
export declare const Toolbar: ({ isVisible, enableTableTool }: ToolbarProps) => React.JSX.Element;
|
7
|
+
export declare const Toolbar: ({ isVisible, enableTableTool, container }: ToolbarProps) => React.JSX.Element;
|
7
8
|
export {};
|
@@ -23,9 +23,10 @@ const HorizontalLineButton_1 = __importDefault(require("./Tools/HorizontalLine/H
|
|
23
23
|
const TableButton_1 = __importDefault(require("./Tools/Table/TableButton"));
|
24
24
|
const ContentToolsDropdown_1 = __importDefault(require("./Tools/ContentTools/ContentToolsDropdown"));
|
25
25
|
const hooks_1 = require("../hooks");
|
26
|
-
const
|
26
|
+
const react_dom_1 = require("react-dom");
|
27
|
+
const Toolbar = ({ isVisible, enableTableTool, container }) => {
|
27
28
|
const extensionNames = (0, hooks_1.useExtensionNames)();
|
28
|
-
|
29
|
+
const toolbar = (react_1.default.createElement(react_components_1.Toolbar, { className: (0, clsx_1.default)('editor-toolbar header-toolbar', isVisible && 'show-toolbar', container && 'fte-portal-toolbar'), role: "toolbar", tabIndex: 0 },
|
29
30
|
react_1.default.createElement("div", { className: "editor-toolbar__tools" },
|
30
31
|
extensionNames.history && (react_1.default.createElement(react_1.default.Fragment, null,
|
31
32
|
react_1.default.createElement(UndoButton_1.default, null),
|
@@ -45,5 +46,6 @@ const Toolbar = ({ isVisible, enableTableTool }) => {
|
|
45
46
|
extensionNames.clearFormatting && react_1.default.createElement(ClearFormattingButton_1.default, null),
|
46
47
|
enableTableTool && extensionNames.table && react_1.default.createElement(TableButton_1.default, null),
|
47
48
|
react_1.default.createElement(ContentToolsDropdown_1.default, null))));
|
49
|
+
return container ? (0, react_dom_1.createPortal)(toolbar, container) : toolbar;
|
48
50
|
};
|
49
51
|
exports.Toolbar = Toolbar;
|
@@ -104,13 +104,11 @@ const ImageForm = ({ data, onSubmit }) => {
|
|
104
104
|
setViewType(value);
|
105
105
|
// If its the URL field type we know what the imageType should be
|
106
106
|
if (value === ViewTypes.URL) {
|
107
|
-
console.log(`handleChangeViewType: ${value} NodeName.Image`);
|
108
107
|
setValue('imageType', Extensions_1.NodeName.Image);
|
109
108
|
}
|
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', Extensions_1.NodeName.AssetImage);
|
115
113
|
}
|
116
114
|
}, [setViewType, setValue]);
|
@@ -178,7 +176,6 @@ const ImageForm = ({ data, onSubmit }) => {
|
|
178
176
|
return matrixValidation && damValidation;
|
179
177
|
},
|
180
178
|
}, render: ({ field: { onChange, value }, fieldState: { error } }) => (react_1.default.createElement(ResourceBrowserSelector_1.ResourceBrowserSelector, { modalTitle: "Insert image", allowedTypes: ['image'], value: value, onChange: (value) => {
|
181
|
-
console.log(`onChange: ${value}`);
|
182
179
|
setValue('imageType', value.target.value.nodeType);
|
183
180
|
onChange(value);
|
184
181
|
}, error: error?.message })) })))));
|
@@ -1,5 +1,15 @@
|
|
1
|
-
import {
|
1
|
+
import { ApplySchemaAttributes, CommandFunction, NodeExtension, NodeExtensionSpec, NodeSpecOverride } from '@remirror/core';
|
2
2
|
import { NodeViewMethod } from 'remirror';
|
3
|
-
export
|
4
|
-
|
3
|
+
export type CodeBlockOptions = {
|
4
|
+
enableDecorations?: boolean;
|
5
|
+
};
|
6
|
+
export declare class CodeBlockExtension extends NodeExtension<CodeBlockOptions> {
|
7
|
+
get name(): "codeBlock";
|
8
|
+
createTags(): ("block" | "code")[];
|
9
|
+
createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec;
|
10
|
+
createNodeViews(): NodeViewMethod | Record<string, never>;
|
11
|
+
/**
|
12
|
+
* Toggle the <code> for the current block.
|
13
|
+
*/
|
14
|
+
toggleCodeBlock(): CommandFunction;
|
5
15
|
}
|