@pie-lib/editable-html 11.1.1 → 11.2.1-beta.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 +43 -167
- package/NEXT.CHANGELOG.json +1 -0
- package/package.json +10 -6
- package/src/__tests__/editor.test.jsx +363 -0
- package/src/__tests__/serialization.test.js +291 -0
- package/src/__tests__/utils.js +36 -0
- package/src/block-tags.js +17 -0
- package/src/constants.js +7 -0
- package/src/editor.jsx +303 -49
- package/src/index.jsx +19 -10
- package/src/plugins/characters/index.jsx +11 -3
- package/src/plugins/characters/utils.js +12 -12
- package/src/plugins/css/icons/index.jsx +17 -0
- package/src/plugins/css/index.jsx +346 -0
- package/src/plugins/customPlugin/index.jsx +85 -0
- package/src/plugins/html/index.jsx +9 -6
- package/src/plugins/image/__tests__/__snapshots__/component.test.jsx.snap +51 -0
- package/src/plugins/image/__tests__/__snapshots__/image-toolbar-logic.test.jsx.snap +27 -0
- package/src/plugins/image/__tests__/__snapshots__/image-toolbar.test.jsx.snap +44 -0
- package/src/plugins/image/__tests__/component.test.jsx +41 -0
- package/src/plugins/image/__tests__/image-toolbar-logic.test.jsx +42 -0
- package/src/plugins/image/__tests__/image-toolbar.test.jsx +11 -0
- package/src/plugins/image/__tests__/index.test.js +95 -0
- package/src/plugins/image/__tests__/insert-image-handler.test.js +113 -0
- package/src/plugins/image/__tests__/mock-change.js +15 -0
- package/src/plugins/image/index.jsx +2 -1
- package/src/plugins/image/insert-image-handler.js +13 -6
- package/src/plugins/index.jsx +248 -5
- package/src/plugins/list/__tests__/index.test.js +54 -0
- package/src/plugins/list/index.jsx +130 -0
- package/src/plugins/math/__tests__/__snapshots__/index.test.jsx.snap +48 -0
- package/src/plugins/math/__tests__/index.test.jsx +245 -0
- package/src/plugins/math/index.jsx +87 -56
- package/src/plugins/media/__tests__/index.test.js +75 -0
- package/src/plugins/media/index.jsx +3 -2
- package/src/plugins/media/media-dialog.js +106 -57
- package/src/plugins/rendering/index.js +31 -0
- package/src/plugins/respArea/drag-in-the-blank/choice.jsx +4 -1
- package/src/plugins/respArea/explicit-constructed-response/index.jsx +10 -8
- package/src/plugins/respArea/index.jsx +53 -7
- package/src/plugins/respArea/inline-dropdown/index.jsx +13 -6
- package/src/plugins/respArea/math-templated/index.jsx +104 -0
- package/src/plugins/respArea/utils.jsx +11 -0
- package/src/plugins/table/CustomTablePlugin.js +113 -0
- package/src/plugins/table/__tests__/__snapshots__/table-toolbar.test.jsx.snap +44 -0
- package/src/plugins/table/__tests__/index.test.jsx +401 -0
- package/src/plugins/table/__tests__/table-toolbar.test.jsx +42 -0
- package/src/plugins/table/index.jsx +46 -59
- package/src/plugins/table/table-toolbar.jsx +39 -2
- package/src/plugins/textAlign/icons/index.jsx +139 -0
- package/src/plugins/textAlign/index.jsx +23 -0
- package/src/plugins/toolbar/__tests__/__snapshots__/default-toolbar.test.jsx.snap +923 -0
- package/src/plugins/toolbar/__tests__/__snapshots__/editor-and-toolbar.test.jsx.snap +20 -0
- package/src/plugins/toolbar/__tests__/__snapshots__/toolbar-buttons.test.jsx.snap +36 -0
- package/src/plugins/toolbar/__tests__/__snapshots__/toolbar.test.jsx.snap +46 -0
- package/src/plugins/toolbar/__tests__/default-toolbar.test.jsx +94 -0
- package/src/plugins/toolbar/__tests__/editor-and-toolbar.test.jsx +37 -0
- package/src/plugins/toolbar/__tests__/toolbar-buttons.test.jsx +51 -0
- package/src/plugins/toolbar/__tests__/toolbar.test.jsx +106 -0
- package/src/plugins/toolbar/default-toolbar.jsx +82 -20
- package/src/plugins/toolbar/done-button.jsx +3 -1
- package/src/plugins/toolbar/editor-and-toolbar.jsx +18 -13
- package/src/plugins/toolbar/toolbar-buttons.jsx +52 -11
- package/src/plugins/toolbar/toolbar.jsx +31 -8
- package/src/serialization.jsx +213 -38
- package/README.md +0 -45
- package/deploy.sh +0 -16
- package/lib/editor.js +0 -1094
- package/lib/editor.js.map +0 -1
- package/lib/index.js +0 -253
- package/lib/index.js.map +0 -1
- package/lib/parse-html.js +0 -16
- package/lib/parse-html.js.map +0 -1
- package/lib/plugins/characters/custom-popper.js +0 -73
- package/lib/plugins/characters/custom-popper.js.map +0 -1
- package/lib/plugins/characters/index.js +0 -300
- package/lib/plugins/characters/index.js.map +0 -1
- package/lib/plugins/characters/utils.js +0 -381
- package/lib/plugins/characters/utils.js.map +0 -1
- package/lib/plugins/html/icons/index.js +0 -38
- package/lib/plugins/html/icons/index.js.map +0 -1
- package/lib/plugins/html/index.js +0 -76
- package/lib/plugins/html/index.js.map +0 -1
- package/lib/plugins/image/alt-dialog.js +0 -129
- package/lib/plugins/image/alt-dialog.js.map +0 -1
- package/lib/plugins/image/component.js +0 -419
- package/lib/plugins/image/component.js.map +0 -1
- package/lib/plugins/image/image-toolbar.js +0 -177
- package/lib/plugins/image/image-toolbar.js.map +0 -1
- package/lib/plugins/image/index.js +0 -262
- package/lib/plugins/image/index.js.map +0 -1
- package/lib/plugins/image/insert-image-handler.js +0 -152
- package/lib/plugins/image/insert-image-handler.js.map +0 -1
- package/lib/plugins/index.js +0 -143
- package/lib/plugins/index.js.map +0 -1
- package/lib/plugins/list/index.js +0 -204
- package/lib/plugins/list/index.js.map +0 -1
- package/lib/plugins/math/index.js +0 -419
- package/lib/plugins/math/index.js.map +0 -1
- package/lib/plugins/media/index.js +0 -384
- package/lib/plugins/media/index.js.map +0 -1
- package/lib/plugins/media/media-dialog.js +0 -668
- package/lib/plugins/media/media-dialog.js.map +0 -1
- package/lib/plugins/media/media-toolbar.js +0 -101
- package/lib/plugins/media/media-toolbar.js.map +0 -1
- package/lib/plugins/media/media-wrapper.js +0 -93
- package/lib/plugins/media/media-wrapper.js.map +0 -1
- package/lib/plugins/respArea/drag-in-the-blank/choice.js +0 -251
- package/lib/plugins/respArea/drag-in-the-blank/choice.js.map +0 -1
- package/lib/plugins/respArea/drag-in-the-blank/index.js +0 -97
- package/lib/plugins/respArea/drag-in-the-blank/index.js.map +0 -1
- package/lib/plugins/respArea/explicit-constructed-response/index.js +0 -55
- package/lib/plugins/respArea/explicit-constructed-response/index.js.map +0 -1
- package/lib/plugins/respArea/icons/index.js +0 -95
- package/lib/plugins/respArea/icons/index.js.map +0 -1
- package/lib/plugins/respArea/index.js +0 -293
- package/lib/plugins/respArea/index.js.map +0 -1
- package/lib/plugins/respArea/inline-dropdown/index.js +0 -70
- package/lib/plugins/respArea/inline-dropdown/index.js.map +0 -1
- package/lib/plugins/respArea/utils.js +0 -110
- package/lib/plugins/respArea/utils.js.map +0 -1
- package/lib/plugins/table/icons/index.js +0 -69
- package/lib/plugins/table/icons/index.js.map +0 -1
- package/lib/plugins/table/index.js +0 -499
- package/lib/plugins/table/index.js.map +0 -1
- package/lib/plugins/table/table-toolbar.js +0 -158
- package/lib/plugins/table/table-toolbar.js.map +0 -1
- package/lib/plugins/toolbar/default-toolbar.js +0 -174
- package/lib/plugins/toolbar/default-toolbar.js.map +0 -1
- package/lib/plugins/toolbar/done-button.js +0 -50
- package/lib/plugins/toolbar/done-button.js.map +0 -1
- package/lib/plugins/toolbar/editor-and-toolbar.js +0 -287
- package/lib/plugins/toolbar/editor-and-toolbar.js.map +0 -1
- package/lib/plugins/toolbar/index.js +0 -34
- package/lib/plugins/toolbar/index.js.map +0 -1
- package/lib/plugins/toolbar/toolbar-buttons.js +0 -161
- package/lib/plugins/toolbar/toolbar-buttons.js.map +0 -1
- package/lib/plugins/toolbar/toolbar.js +0 -352
- package/lib/plugins/toolbar/toolbar.js.map +0 -1
- package/lib/plugins/utils.js +0 -62
- package/lib/plugins/utils.js.map +0 -1
- package/lib/serialization.js +0 -488
- package/lib/serialization.js.map +0 -1
- package/lib/theme.js +0 -9
- package/lib/theme.js.map +0 -1
- package/playground/image/data.js +0 -59
- package/playground/image/index.html +0 -22
- package/playground/image/index.jsx +0 -81
- package/playground/index.html +0 -25
- package/playground/mathquill/index.html +0 -22
- package/playground/mathquill/index.jsx +0 -155
- package/playground/package.json +0 -15
- package/playground/prod-test/index.html +0 -22
- package/playground/prod-test/index.jsx +0 -28
- package/playground/schema-override/data.js +0 -29
- package/playground/schema-override/image-plugin.jsx +0 -41
- package/playground/schema-override/index.html +0 -21
- package/playground/schema-override/index.jsx +0 -97
- package/playground/serialization/data.js +0 -29
- package/playground/serialization/image-plugin.jsx +0 -41
- package/playground/serialization/index.html +0 -22
- package/playground/serialization/index.jsx +0 -12
- package/playground/static.json +0 -3
- package/playground/table-examples.html +0 -70
- package/playground/webpack.config.js +0 -42
- package/static.json +0 -1
package/src/index.jsx
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import Editor, { DEFAULT_PLUGINS, ALL_PLUGINS } from './editor';
|
|
4
|
-
import { htmlToValue, valueToHtml, reduceMultipleBrs } from './serialization';
|
|
4
|
+
import { extraCSSRulesOpts, htmlToValue, valueToHtml, reduceMultipleBrs } from './serialization';
|
|
5
5
|
import { parseDegrees } from './parse-html';
|
|
6
|
+
import constants from './constants';
|
|
6
7
|
import debug from 'debug';
|
|
7
8
|
import { Range } from 'slate';
|
|
8
9
|
|
|
9
10
|
const log = debug('@pie-lib:editable-html');
|
|
10
|
-
/**
|
|
11
|
-
* Export lower level Editor and serialization functions.
|
|
12
|
-
*/
|
|
13
|
-
export { htmlToValue, valueToHtml, Editor, DEFAULT_PLUGINS, ALL_PLUGINS };
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
13
|
* Wrapper around the editor that exposes a `markup` and `onChange(markup:string)` api.
|
|
@@ -26,6 +23,10 @@ export default class EditableHtml extends React.Component {
|
|
|
26
23
|
markup: PropTypes.string.isRequired,
|
|
27
24
|
allowValidation: PropTypes.bool,
|
|
28
25
|
toolbarOpts: PropTypes.object,
|
|
26
|
+
extraCSSRules: PropTypes.shape({
|
|
27
|
+
names: PropTypes.arrayOf(PropTypes.string),
|
|
28
|
+
rules: PropTypes.string,
|
|
29
|
+
}),
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
static defaultProps = {
|
|
@@ -35,6 +36,11 @@ export default class EditableHtml extends React.Component {
|
|
|
35
36
|
|
|
36
37
|
constructor(props) {
|
|
37
38
|
super(props);
|
|
39
|
+
|
|
40
|
+
if (props.extraCSSRules) {
|
|
41
|
+
Object.assign(extraCSSRulesOpts, this.props.extraCSSRules);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
const v = htmlToValue(props.markup);
|
|
39
45
|
this.state = {
|
|
40
46
|
value: v,
|
|
@@ -48,7 +54,7 @@ export default class EditableHtml extends React.Component {
|
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
const v = htmlToValue(props.markup);
|
|
51
|
-
const current = htmlToValue(props.markup);
|
|
57
|
+
const current = htmlToValue(this.props.markup);
|
|
52
58
|
|
|
53
59
|
if (v.equals && !v.equals(current)) {
|
|
54
60
|
this.setState({ value: v });
|
|
@@ -69,13 +75,11 @@ export default class EditableHtml extends React.Component {
|
|
|
69
75
|
const html = valueToHtml(value);
|
|
70
76
|
const htmlParsed = parseDegrees(html);
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (html !== this.props.markup) {
|
|
78
|
+
if (html !== this.props.markup && this.props.onChange) {
|
|
75
79
|
this.props.onChange(htmlParsed);
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
if (done) {
|
|
82
|
+
if (done && this.props.onDone) {
|
|
79
83
|
this.props.onDone(htmlParsed);
|
|
80
84
|
}
|
|
81
85
|
};
|
|
@@ -151,3 +155,8 @@ export default class EditableHtml extends React.Component {
|
|
|
151
155
|
);
|
|
152
156
|
}
|
|
153
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Export lower level Editor and serialization functions.
|
|
161
|
+
*/
|
|
162
|
+
export { htmlToValue, valueToHtml, Editor, DEFAULT_PLUGINS, ALL_PLUGINS, constants };
|
|
@@ -9,6 +9,7 @@ import CustomPopper from './custom-popper';
|
|
|
9
9
|
import { insertSnackBar } from '../respArea/utils';
|
|
10
10
|
import { characterIcons, spanishConfig, specialConfig } from './utils';
|
|
11
11
|
import PropTypes from 'prop-types';
|
|
12
|
+
|
|
12
13
|
const log = debug('@pie-lib:editable-html:plugins:characters');
|
|
13
14
|
|
|
14
15
|
const removePopOvers = () => {
|
|
@@ -131,6 +132,8 @@ const insertDialog = ({ editorDOM, value, callback, opts }) => {
|
|
|
131
132
|
|
|
132
133
|
const el = (
|
|
133
134
|
<PureToolbar
|
|
135
|
+
keyPadCharacterRef={opts.keyPadCharacterRef}
|
|
136
|
+
setKeypadInteraction={opts.setKeypadInteraction}
|
|
134
137
|
autoFocus
|
|
135
138
|
noDecimal
|
|
136
139
|
hideInput
|
|
@@ -219,8 +222,8 @@ const insertDialog = ({ editorDOM, value, callback, opts }) => {
|
|
|
219
222
|
const CharacterIcon = ({ letter }) => (
|
|
220
223
|
<div
|
|
221
224
|
style={{
|
|
222
|
-
fontSize: '
|
|
223
|
-
lineHeight: '
|
|
225
|
+
fontSize: '24px',
|
|
226
|
+
lineHeight: '24px',
|
|
224
227
|
}}
|
|
225
228
|
>
|
|
226
229
|
{letter}
|
|
@@ -233,15 +236,20 @@ CharacterIcon.propTypes = {
|
|
|
233
236
|
|
|
234
237
|
export default function CharactersPlugin(opts) {
|
|
235
238
|
removeDialogs();
|
|
239
|
+
|
|
236
240
|
return {
|
|
237
241
|
name: 'characters',
|
|
238
242
|
toolbar: {
|
|
239
243
|
icon: <CharacterIcon letter={opts.characterIcon || characterIcons[opts.language] || 'ñ'} />,
|
|
244
|
+
ariaLabel: `${opts.language} characters Toolbar`,
|
|
240
245
|
onClick: (value, onChange, getFocusedValue) => {
|
|
241
246
|
const editorDOM = document.querySelector(`[data-key="${value.document.key}"]`);
|
|
242
247
|
let valueToUse = value;
|
|
248
|
+
|
|
243
249
|
const callback = (char, focus) => {
|
|
244
|
-
|
|
250
|
+
if (getFocusedValue) {
|
|
251
|
+
valueToUse = getFocusedValue() || valueToUse;
|
|
252
|
+
}
|
|
245
253
|
|
|
246
254
|
if (char) {
|
|
247
255
|
const change = valueToUse.change().insertTextByKey(valueToUse.anchorKey, valueToUse.anchorOffset, char);
|
|
@@ -226,10 +226,10 @@ export const specialConfig = {
|
|
|
226
226
|
],
|
|
227
227
|
[
|
|
228
228
|
{
|
|
229
|
-
unicode: 'U+
|
|
230
|
-
description: '
|
|
231
|
-
write: String.fromCodePoint('
|
|
232
|
-
label: '&
|
|
229
|
+
unicode: 'U+200A',
|
|
230
|
+
description: 'HAIR SPACE',
|
|
231
|
+
write: String.fromCodePoint('0x200A'),
|
|
232
|
+
label: ' ',
|
|
233
233
|
},
|
|
234
234
|
{
|
|
235
235
|
unicode: 'U+00A7',
|
|
@@ -333,10 +333,10 @@ export const specialConfig = {
|
|
|
333
333
|
],
|
|
334
334
|
[
|
|
335
335
|
{
|
|
336
|
-
unicode: 'U+
|
|
337
|
-
description: '
|
|
338
|
-
write: String.fromCodePoint('
|
|
339
|
-
label: '&
|
|
336
|
+
unicode: 'U+00A0',
|
|
337
|
+
description: 'NO-BREAK SPACE',
|
|
338
|
+
write: String.fromCodePoint('0x00A0'),
|
|
339
|
+
label: ' ',
|
|
340
340
|
},
|
|
341
341
|
{
|
|
342
342
|
unicode: 'U+2022',
|
|
@@ -390,10 +390,10 @@ export const specialConfig = {
|
|
|
390
390
|
],
|
|
391
391
|
[
|
|
392
392
|
{
|
|
393
|
-
unicode: 'U+
|
|
394
|
-
description: '
|
|
395
|
-
write: String.fromCodePoint('
|
|
396
|
-
label: '
|
|
393
|
+
unicode: 'U+2003',
|
|
394
|
+
description: 'EM SPACE',
|
|
395
|
+
write: String.fromCodePoint('0x2003'),
|
|
396
|
+
label: ' ',
|
|
397
397
|
},
|
|
398
398
|
{
|
|
399
399
|
unicode: 'U+25E6',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { withStyles } from '@material-ui/core/styles';
|
|
3
|
+
|
|
4
|
+
const styles = (theme) => ({
|
|
5
|
+
icon: {
|
|
6
|
+
fontFamily: 'Cerebri Sans, Arial, sans-serif',
|
|
7
|
+
fontSize: theme.typography.fontSize,
|
|
8
|
+
fontWeight: 'bold',
|
|
9
|
+
lineHeight: '14px',
|
|
10
|
+
position: 'relative',
|
|
11
|
+
whiteSpace: 'nowrap',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const CssIcon = ({ classes }) => <div className={classes.icon}>CSS</div>;
|
|
16
|
+
|
|
17
|
+
export default withStyles(styles)(CssIcon);
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import List from '@material-ui/core/List';
|
|
4
|
+
import { Leaf, Mark } from 'slate';
|
|
5
|
+
import Immutable from 'immutable';
|
|
6
|
+
import ListItem from '@material-ui/core/ListItem';
|
|
7
|
+
import isEmpty from 'lodash/isEmpty';
|
|
8
|
+
import debug from 'debug';
|
|
9
|
+
import CssIcon from './icons';
|
|
10
|
+
|
|
11
|
+
const log = debug('@pie-lib:editable-html:plugins:characters');
|
|
12
|
+
|
|
13
|
+
export const removeDialogs = () => {
|
|
14
|
+
const prevDialogs = document.querySelectorAll('.insert-css-dialog');
|
|
15
|
+
|
|
16
|
+
log('[characters:removeDialogs]');
|
|
17
|
+
prevDialogs.forEach((s) => s.remove());
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const insertDialog = ({ editorDOM, value, callback, opts, textNode, parentNode }) => {
|
|
21
|
+
const newEl = document.createElement('div');
|
|
22
|
+
|
|
23
|
+
log('[characters:insertDialog]');
|
|
24
|
+
|
|
25
|
+
removeDialogs();
|
|
26
|
+
|
|
27
|
+
newEl.className = 'insert-css-dialog';
|
|
28
|
+
|
|
29
|
+
let popoverEl;
|
|
30
|
+
|
|
31
|
+
const closePopOver = () => {
|
|
32
|
+
if (popoverEl) {
|
|
33
|
+
popoverEl.remove();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let firstCallMade = false;
|
|
38
|
+
|
|
39
|
+
const listener = (e) => {
|
|
40
|
+
// this will be triggered right after setting it because
|
|
41
|
+
// this toolbar is added on the mousedown event
|
|
42
|
+
// so right after mouseup, the click will be triggered
|
|
43
|
+
if (firstCallMade) {
|
|
44
|
+
const focusIsInModals = newEl.contains(e.target) || (popoverEl && popoverEl.contains(e.target));
|
|
45
|
+
const focusIsInEditor = editorDOM.contains(e.target);
|
|
46
|
+
|
|
47
|
+
if (!(focusIsInModals || focusIsInEditor)) {
|
|
48
|
+
handleClose();
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
firstCallMade = true;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleClose = () => {
|
|
56
|
+
callback(undefined, true);
|
|
57
|
+
newEl.remove();
|
|
58
|
+
closePopOver();
|
|
59
|
+
document.body.removeEventListener('click', listener);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleChange = (name) => {
|
|
63
|
+
callback(name, true);
|
|
64
|
+
newEl.remove();
|
|
65
|
+
closePopOver();
|
|
66
|
+
document.body.removeEventListener('click', listener);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const selectedText = textNode.text.slice(value.selection.anchorOffset, value.selection.focusOffset);
|
|
70
|
+
const parentNodeClass = parentNode?.data?.get('attributes')?.class;
|
|
71
|
+
const createHTML = (name) => {
|
|
72
|
+
let html = `<span class="${name}">${selectedText}</span>`;
|
|
73
|
+
|
|
74
|
+
if (parentNode) {
|
|
75
|
+
let tag;
|
|
76
|
+
|
|
77
|
+
if (parentNode?.object === 'inline') {
|
|
78
|
+
tag = 'span';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (parentNode?.object === 'block') {
|
|
82
|
+
tag = 'div';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
html = `<${tag} class="${parentNodeClass}">${parentNode.text.slice(
|
|
86
|
+
0,
|
|
87
|
+
value.selection.anchorOffset,
|
|
88
|
+
)}${html}${parentNode.text.slice(value.selection.focusOffset)}</${tag}>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return html;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const el = (
|
|
95
|
+
<div
|
|
96
|
+
style={{ background: 'white', height: 500, padding: 20, overflow: 'hidden', display: 'flex', flexFlow: 'column' }}
|
|
97
|
+
>
|
|
98
|
+
<h2>Please choose a css class</h2>
|
|
99
|
+
{parentNodeClass && <div>The current parent has this class {parentNodeClass}</div>}
|
|
100
|
+
<List component="nav" style={{ overflow: 'scroll' }}>
|
|
101
|
+
{opts.names.map((name, i) => (
|
|
102
|
+
<ListItem key={`rule-${i}`} button onClick={() => handleChange(name)}>
|
|
103
|
+
<div style={{ marginRight: 20 }}>{name}</div>
|
|
104
|
+
<div
|
|
105
|
+
dangerouslySetInnerHTML={{
|
|
106
|
+
__html: createHTML(name),
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
</ListItem>
|
|
110
|
+
))}
|
|
111
|
+
</List>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
ReactDOM.render(el, newEl, () => {
|
|
116
|
+
const cursorItem = document.querySelector(`[data-key="${value.anchorKey}"]`);
|
|
117
|
+
|
|
118
|
+
if (cursorItem) {
|
|
119
|
+
const bodyRect = editorDOM.parentElement.parentElement.parentElement.getBoundingClientRect();
|
|
120
|
+
const boundRect = cursorItem.getBoundingClientRect();
|
|
121
|
+
|
|
122
|
+
editorDOM.parentElement.parentElement.parentElement.appendChild(newEl);
|
|
123
|
+
|
|
124
|
+
// when height of toolbar exceeds screen - can happen in scrollable contexts
|
|
125
|
+
let additionalTopOffset = 0;
|
|
126
|
+
if (boundRect.y < newEl.offsetHeight) {
|
|
127
|
+
additionalTopOffset = newEl.offsetHeight - boundRect.y + 10;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
newEl.style.maxWidth = '500px';
|
|
131
|
+
newEl.style.position = 'absolute';
|
|
132
|
+
newEl.style.top = 0;
|
|
133
|
+
newEl.style.zIndex = 99999;
|
|
134
|
+
|
|
135
|
+
const leftValue = `${boundRect.left + Math.abs(bodyRect.left) + cursorItem.offsetWidth + 10}px`;
|
|
136
|
+
|
|
137
|
+
const rightValue = `${boundRect.x}px`;
|
|
138
|
+
|
|
139
|
+
newEl.style.left = leftValue;
|
|
140
|
+
|
|
141
|
+
const leftAlignedWidth = newEl.offsetWidth;
|
|
142
|
+
|
|
143
|
+
newEl.style.left = 'unset';
|
|
144
|
+
newEl.style.right = rightValue;
|
|
145
|
+
|
|
146
|
+
const rightAlignedWidth = newEl.offsetWidth;
|
|
147
|
+
|
|
148
|
+
newEl.style.left = 'unset';
|
|
149
|
+
newEl.style.right = 'unset';
|
|
150
|
+
|
|
151
|
+
if (leftAlignedWidth >= rightAlignedWidth) {
|
|
152
|
+
newEl.style.left = leftValue;
|
|
153
|
+
} else {
|
|
154
|
+
newEl.style.right = rightValue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
document.body.addEventListener('click', listener);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const findParentNodeInfo = (value, textNode) => {
|
|
163
|
+
const closestInline = value.document.getClosestInline(value.selection.endKey);
|
|
164
|
+
const closestBlock = value.document.getClosestBlock(value.selection.endKey);
|
|
165
|
+
let nodeToUse = null;
|
|
166
|
+
|
|
167
|
+
if (closestInline?.nodes?.find((n) => n.key === textNode.key)) {
|
|
168
|
+
nodeToUse = closestInline;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (closestBlock?.nodes?.find((n) => n.key === textNode.key)) {
|
|
172
|
+
nodeToUse = closestBlock;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return nodeToUse;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Find the node that has a class attribute and return it.
|
|
180
|
+
* Keeping this in case the implementation of classes needs to be changed
|
|
181
|
+
* @param value
|
|
182
|
+
* @param opts
|
|
183
|
+
* @returns {*}
|
|
184
|
+
*/
|
|
185
|
+
const getNodeWithClass = (value, opts) => {
|
|
186
|
+
const blocksAtRange = value.document.getBlocksAtRangeAsArray(value.selection);
|
|
187
|
+
const inlinesAtRange = value.document.getInlinesAtRangeAsArray(value.selection);
|
|
188
|
+
const blockData = blocksAtRange[0]?.data.toJSON() || {};
|
|
189
|
+
const inlineData = inlinesAtRange[0]?.data.toJSON() || {};
|
|
190
|
+
|
|
191
|
+
if (!blockData.attributes?.class && !inlineData.attributes?.class) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { class: blockClass } = blockData.attributes || {};
|
|
196
|
+
const { class: inlineClass } = inlineData.attributes || {};
|
|
197
|
+
const inlineHasClass = opts.names.find((name) => inlineClass.includes(name));
|
|
198
|
+
|
|
199
|
+
if (inlineHasClass) {
|
|
200
|
+
return inlinesAtRange[0];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const blockHasClass = opts.names.find((name) => blockClass.includes(name));
|
|
204
|
+
|
|
205
|
+
if (blockHasClass) {
|
|
206
|
+
return blocksAtRange[0];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Plugin in order to be able to add a css clas that is provided through the model
|
|
214
|
+
* on a text element. Works like a mark (bold, italic etc.).
|
|
215
|
+
* @param opts
|
|
216
|
+
* @constructor
|
|
217
|
+
*/
|
|
218
|
+
export default function CSSPlugin(opts) {
|
|
219
|
+
const plugin = {
|
|
220
|
+
name: 'extraCSSRules',
|
|
221
|
+
toolbar: {
|
|
222
|
+
isMark: true,
|
|
223
|
+
icon: <CssIcon />,
|
|
224
|
+
ariaLabel: 'CSS editor',
|
|
225
|
+
type: 'css',
|
|
226
|
+
onToggle: (change) => {
|
|
227
|
+
const type = 'css';
|
|
228
|
+
const hasMark = change.value.activeMarks.find((entry) => {
|
|
229
|
+
return entry.type === type;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (hasMark) {
|
|
233
|
+
change.removeMark(hasMark);
|
|
234
|
+
} else {
|
|
235
|
+
const newMark = Mark.create(type);
|
|
236
|
+
|
|
237
|
+
change.addMark(newMark);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return change;
|
|
241
|
+
},
|
|
242
|
+
onClick: (value, onChange, getFocusedValue) => {
|
|
243
|
+
const type = 'css';
|
|
244
|
+
const hasMark = value.activeMarks.find((entry) => {
|
|
245
|
+
return entry.type === type;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
let change = value.change();
|
|
249
|
+
|
|
250
|
+
if (hasMark) {
|
|
251
|
+
change.removeMark(hasMark);
|
|
252
|
+
onChange(change);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// keeping this if implementation needs to be changed to regular blocks instead of marks
|
|
257
|
+
// let nodeWithClass = getNodeWithClass(value, opts);
|
|
258
|
+
//
|
|
259
|
+
// if (nodeWithClass) {
|
|
260
|
+
// const nodeAttributes = nodeWithClass.data.get('attributes');
|
|
261
|
+
//
|
|
262
|
+
// opts.names.forEach((name) => {
|
|
263
|
+
// if (nodeAttributes.class.includes(name)) {
|
|
264
|
+
// nodeAttributes.class = nodeAttributes.class.replace(name, '');
|
|
265
|
+
// }
|
|
266
|
+
// });
|
|
267
|
+
//
|
|
268
|
+
// // keeping only one space between classes
|
|
269
|
+
// nodeAttributes.class = nodeAttributes.class.replace(/ +/g, ' ');
|
|
270
|
+
//
|
|
271
|
+
// nodeWithClass.data.set('attributes', nodeAttributes);
|
|
272
|
+
//
|
|
273
|
+
// let change = value.change();
|
|
274
|
+
// change.replaceNodeByKey(nodeWithClass.key, nodeWithClass);
|
|
275
|
+
//
|
|
276
|
+
// onChange(change);
|
|
277
|
+
// return;
|
|
278
|
+
// }
|
|
279
|
+
|
|
280
|
+
const editorDOM = document.querySelector(`[data-key="${value.document.key}"]`);
|
|
281
|
+
let valueToUse = value;
|
|
282
|
+
|
|
283
|
+
const callback = (className, focus) => {
|
|
284
|
+
if (getFocusedValue) {
|
|
285
|
+
valueToUse = getFocusedValue() || valueToUse;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (className) {
|
|
289
|
+
let change = valueToUse.change();
|
|
290
|
+
|
|
291
|
+
const newMark = Mark.create({
|
|
292
|
+
object: 'mark',
|
|
293
|
+
type: 'css',
|
|
294
|
+
data: {
|
|
295
|
+
attributes: {
|
|
296
|
+
class: className,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
change.addMark(newMark);
|
|
302
|
+
// keeping this if implementation needs to be changed to regular blocks instead of marks
|
|
303
|
+
// change = change.wrapInline({ type: 'span', data: { attributes: { class: className } } });
|
|
304
|
+
//
|
|
305
|
+
// // change = change.splitBlockAtRange(adaptedRange);
|
|
306
|
+
// //
|
|
307
|
+
// // const newBlock = change.value.document.getFurthestBlock(change.value.selection.endKey);
|
|
308
|
+
// //
|
|
309
|
+
// // change = change.setNodeByKey(newBlock.key, { data: { attributes: { class: className } } });
|
|
310
|
+
//
|
|
311
|
+
// valueToUse = change.value;
|
|
312
|
+
// log('[characters:insert]: ', value);
|
|
313
|
+
onChange(change);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
log('[characters:click]');
|
|
317
|
+
|
|
318
|
+
if (focus) {
|
|
319
|
+
setTimeout(() => {
|
|
320
|
+
if (editorDOM) {
|
|
321
|
+
editorDOM.focus();
|
|
322
|
+
}
|
|
323
|
+
}, 0);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const textNode = value.document.getTextsAtRangeAsArray(value.selection)[0];
|
|
327
|
+
|
|
328
|
+
if (textNode) {
|
|
329
|
+
const parentNode = findParentNodeInfo(value, textNode, opts);
|
|
330
|
+
|
|
331
|
+
insertDialog({ editorDOM, value: valueToUse, callback, opts, textNode, parentNode });
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
renderMark(props) {
|
|
336
|
+
if (props.mark.type === 'css') {
|
|
337
|
+
const { data } = props.mark || {};
|
|
338
|
+
const jsonData = data?.toJSON() || {};
|
|
339
|
+
|
|
340
|
+
return <span {...jsonData.attributes}>{props.children}</span>;
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return plugin;
|
|
346
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { htmlToValue } from '../../serialization';
|
|
3
|
+
|
|
4
|
+
// We're possibly going to have to support content types, so starting it as an enum
|
|
5
|
+
export const CONTENT_TYPE = {
|
|
6
|
+
FRAGMENT: 'FRAGMENT',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// We're possibly going to have to support multiple icon types, so starting it as an enum
|
|
10
|
+
export const ICON_TYPE = {
|
|
11
|
+
SVG: 'SVG',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getIcon = (customPluginProps) => {
|
|
15
|
+
const svg = customPluginProps.icon;
|
|
16
|
+
|
|
17
|
+
switch (customPluginProps.iconType) {
|
|
18
|
+
case ICON_TYPE.SVG:
|
|
19
|
+
return <span style={{ width: 28, height: 28 }} dangerouslySetInnerHTML={{ __html: svg }} />;
|
|
20
|
+
default:
|
|
21
|
+
return <span>{customPluginProps.iconAlt}</span>;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default function CustomPlugin(type, customPluginProps) {
|
|
26
|
+
const toolbar = {
|
|
27
|
+
icon: getIcon(customPluginProps),
|
|
28
|
+
onClick: (value, onChange, getFocusedValue) => {
|
|
29
|
+
const editorDOM = document.querySelector(`[data-key="${value.document.key}"]`);
|
|
30
|
+
let valueToUse = value;
|
|
31
|
+
const callback = ({ customContent, contentType }, focus) => {
|
|
32
|
+
valueToUse = getFocusedValue();
|
|
33
|
+
|
|
34
|
+
switch (contentType) {
|
|
35
|
+
case CONTENT_TYPE.FRAGMENT:
|
|
36
|
+
default: {
|
|
37
|
+
const contentValue = htmlToValue(customContent);
|
|
38
|
+
const change = valueToUse.change().insertFragment(contentValue.document);
|
|
39
|
+
|
|
40
|
+
valueToUse = change.value;
|
|
41
|
+
onChange(change);
|
|
42
|
+
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (focus) {
|
|
48
|
+
if (editorDOM) {
|
|
49
|
+
editorDOM.focus();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// NOTE: the emitted event (custom event named by client) will be suffixed with "PIE-"
|
|
55
|
+
window.dispatchEvent(
|
|
56
|
+
new CustomEvent(`PIE-${customPluginProps.event}`, {
|
|
57
|
+
detail: {
|
|
58
|
+
...customPluginProps,
|
|
59
|
+
callback,
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
supports: (node) => node.object === 'inline' && node.type === type,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
name: type,
|
|
69
|
+
toolbar,
|
|
70
|
+
renderNode(props) {
|
|
71
|
+
if (props.node.type === type) {
|
|
72
|
+
const { node } = props;
|
|
73
|
+
const { data } = node;
|
|
74
|
+
const jsonData = data.toJSON();
|
|
75
|
+
const { customContent, contentType } = jsonData;
|
|
76
|
+
|
|
77
|
+
switch (contentType) {
|
|
78
|
+
case CONTENT_TYPE.FRAGMENT:
|
|
79
|
+
default:
|
|
80
|
+
return customContent;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -2,9 +2,9 @@ import React from 'react';
|
|
|
2
2
|
import HtmlModeIcon from './icons';
|
|
3
3
|
import { htmlToValue, valueToHtml } from './../../serialization';
|
|
4
4
|
|
|
5
|
-
const toggleToRichText = (value, onChange) => {
|
|
5
|
+
const toggleToRichText = (value, onChange, dismiss) => {
|
|
6
6
|
const plainText = value.document.text;
|
|
7
|
-
const slateValue = htmlToValue(plainText);
|
|
7
|
+
const slateValue = dismiss ? value : htmlToValue(plainText);
|
|
8
8
|
|
|
9
9
|
const change = value
|
|
10
10
|
.change()
|
|
@@ -15,15 +15,17 @@ const toggleToRichText = (value, onChange) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export default function HtmlPlugin(opts) {
|
|
18
|
-
const { isHtmlMode,
|
|
18
|
+
const { isHtmlMode, isEditedInHtmlMode, toggleHtmlMode, handleAlertDialog, currentValue } = opts;
|
|
19
19
|
|
|
20
20
|
const handleHtmlModeOn = (value, onChange) => {
|
|
21
21
|
const dialogProps = {
|
|
22
22
|
title: 'Warning',
|
|
23
|
-
text: 'Returning to rich text mode
|
|
23
|
+
text: 'Returning to rich text mode without saving will cause edits to be lost.',
|
|
24
|
+
onConfirmText: 'Dismiss changes',
|
|
25
|
+
onCloseText: 'Continue Editing',
|
|
24
26
|
onConfirm: () => {
|
|
25
27
|
handleAlertDialog(false);
|
|
26
|
-
toggleToRichText(
|
|
28
|
+
toggleToRichText(currentValue, onChange, true);
|
|
27
29
|
toggleHtmlMode();
|
|
28
30
|
},
|
|
29
31
|
onClose: () => {
|
|
@@ -47,13 +49,14 @@ export default function HtmlPlugin(opts) {
|
|
|
47
49
|
name: 'html',
|
|
48
50
|
toolbar: {
|
|
49
51
|
icon: <HtmlModeIcon isHtmlMode={isHtmlMode} />,
|
|
52
|
+
ariaLabel: 'Html editor',
|
|
50
53
|
buttonStyles: {
|
|
51
54
|
margin: '0 20px 0 auto',
|
|
52
55
|
},
|
|
53
56
|
type: 'html',
|
|
54
57
|
onClick: (value, onChange) => {
|
|
55
58
|
if (isHtmlMode) {
|
|
56
|
-
if (
|
|
59
|
+
if (isEditedInHtmlMode) {
|
|
57
60
|
handleHtmlModeOn(value, onChange);
|
|
58
61
|
} else {
|
|
59
62
|
toggleToRichText(value, onChange);
|