@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.
Files changed (166) hide show
  1. package/CHANGELOG.md +43 -167
  2. package/NEXT.CHANGELOG.json +1 -0
  3. package/package.json +10 -6
  4. package/src/__tests__/editor.test.jsx +363 -0
  5. package/src/__tests__/serialization.test.js +291 -0
  6. package/src/__tests__/utils.js +36 -0
  7. package/src/block-tags.js +17 -0
  8. package/src/constants.js +7 -0
  9. package/src/editor.jsx +303 -49
  10. package/src/index.jsx +19 -10
  11. package/src/plugins/characters/index.jsx +11 -3
  12. package/src/plugins/characters/utils.js +12 -12
  13. package/src/plugins/css/icons/index.jsx +17 -0
  14. package/src/plugins/css/index.jsx +346 -0
  15. package/src/plugins/customPlugin/index.jsx +85 -0
  16. package/src/plugins/html/index.jsx +9 -6
  17. package/src/plugins/image/__tests__/__snapshots__/component.test.jsx.snap +51 -0
  18. package/src/plugins/image/__tests__/__snapshots__/image-toolbar-logic.test.jsx.snap +27 -0
  19. package/src/plugins/image/__tests__/__snapshots__/image-toolbar.test.jsx.snap +44 -0
  20. package/src/plugins/image/__tests__/component.test.jsx +41 -0
  21. package/src/plugins/image/__tests__/image-toolbar-logic.test.jsx +42 -0
  22. package/src/plugins/image/__tests__/image-toolbar.test.jsx +11 -0
  23. package/src/plugins/image/__tests__/index.test.js +95 -0
  24. package/src/plugins/image/__tests__/insert-image-handler.test.js +113 -0
  25. package/src/plugins/image/__tests__/mock-change.js +15 -0
  26. package/src/plugins/image/index.jsx +2 -1
  27. package/src/plugins/image/insert-image-handler.js +13 -6
  28. package/src/plugins/index.jsx +248 -5
  29. package/src/plugins/list/__tests__/index.test.js +54 -0
  30. package/src/plugins/list/index.jsx +130 -0
  31. package/src/plugins/math/__tests__/__snapshots__/index.test.jsx.snap +48 -0
  32. package/src/plugins/math/__tests__/index.test.jsx +245 -0
  33. package/src/plugins/math/index.jsx +87 -56
  34. package/src/plugins/media/__tests__/index.test.js +75 -0
  35. package/src/plugins/media/index.jsx +3 -2
  36. package/src/plugins/media/media-dialog.js +106 -57
  37. package/src/plugins/rendering/index.js +31 -0
  38. package/src/plugins/respArea/drag-in-the-blank/choice.jsx +4 -1
  39. package/src/plugins/respArea/explicit-constructed-response/index.jsx +10 -8
  40. package/src/plugins/respArea/index.jsx +53 -7
  41. package/src/plugins/respArea/inline-dropdown/index.jsx +13 -6
  42. package/src/plugins/respArea/math-templated/index.jsx +104 -0
  43. package/src/plugins/respArea/utils.jsx +11 -0
  44. package/src/plugins/table/CustomTablePlugin.js +113 -0
  45. package/src/plugins/table/__tests__/__snapshots__/table-toolbar.test.jsx.snap +44 -0
  46. package/src/plugins/table/__tests__/index.test.jsx +401 -0
  47. package/src/plugins/table/__tests__/table-toolbar.test.jsx +42 -0
  48. package/src/plugins/table/index.jsx +46 -59
  49. package/src/plugins/table/table-toolbar.jsx +39 -2
  50. package/src/plugins/textAlign/icons/index.jsx +139 -0
  51. package/src/plugins/textAlign/index.jsx +23 -0
  52. package/src/plugins/toolbar/__tests__/__snapshots__/default-toolbar.test.jsx.snap +923 -0
  53. package/src/plugins/toolbar/__tests__/__snapshots__/editor-and-toolbar.test.jsx.snap +20 -0
  54. package/src/plugins/toolbar/__tests__/__snapshots__/toolbar-buttons.test.jsx.snap +36 -0
  55. package/src/plugins/toolbar/__tests__/__snapshots__/toolbar.test.jsx.snap +46 -0
  56. package/src/plugins/toolbar/__tests__/default-toolbar.test.jsx +94 -0
  57. package/src/plugins/toolbar/__tests__/editor-and-toolbar.test.jsx +37 -0
  58. package/src/plugins/toolbar/__tests__/toolbar-buttons.test.jsx +51 -0
  59. package/src/plugins/toolbar/__tests__/toolbar.test.jsx +106 -0
  60. package/src/plugins/toolbar/default-toolbar.jsx +82 -20
  61. package/src/plugins/toolbar/done-button.jsx +3 -1
  62. package/src/plugins/toolbar/editor-and-toolbar.jsx +18 -13
  63. package/src/plugins/toolbar/toolbar-buttons.jsx +52 -11
  64. package/src/plugins/toolbar/toolbar.jsx +31 -8
  65. package/src/serialization.jsx +213 -38
  66. package/README.md +0 -45
  67. package/deploy.sh +0 -16
  68. package/lib/editor.js +0 -1094
  69. package/lib/editor.js.map +0 -1
  70. package/lib/index.js +0 -253
  71. package/lib/index.js.map +0 -1
  72. package/lib/parse-html.js +0 -16
  73. package/lib/parse-html.js.map +0 -1
  74. package/lib/plugins/characters/custom-popper.js +0 -73
  75. package/lib/plugins/characters/custom-popper.js.map +0 -1
  76. package/lib/plugins/characters/index.js +0 -300
  77. package/lib/plugins/characters/index.js.map +0 -1
  78. package/lib/plugins/characters/utils.js +0 -381
  79. package/lib/plugins/characters/utils.js.map +0 -1
  80. package/lib/plugins/html/icons/index.js +0 -38
  81. package/lib/plugins/html/icons/index.js.map +0 -1
  82. package/lib/plugins/html/index.js +0 -76
  83. package/lib/plugins/html/index.js.map +0 -1
  84. package/lib/plugins/image/alt-dialog.js +0 -129
  85. package/lib/plugins/image/alt-dialog.js.map +0 -1
  86. package/lib/plugins/image/component.js +0 -419
  87. package/lib/plugins/image/component.js.map +0 -1
  88. package/lib/plugins/image/image-toolbar.js +0 -177
  89. package/lib/plugins/image/image-toolbar.js.map +0 -1
  90. package/lib/plugins/image/index.js +0 -262
  91. package/lib/plugins/image/index.js.map +0 -1
  92. package/lib/plugins/image/insert-image-handler.js +0 -152
  93. package/lib/plugins/image/insert-image-handler.js.map +0 -1
  94. package/lib/plugins/index.js +0 -143
  95. package/lib/plugins/index.js.map +0 -1
  96. package/lib/plugins/list/index.js +0 -204
  97. package/lib/plugins/list/index.js.map +0 -1
  98. package/lib/plugins/math/index.js +0 -419
  99. package/lib/plugins/math/index.js.map +0 -1
  100. package/lib/plugins/media/index.js +0 -384
  101. package/lib/plugins/media/index.js.map +0 -1
  102. package/lib/plugins/media/media-dialog.js +0 -668
  103. package/lib/plugins/media/media-dialog.js.map +0 -1
  104. package/lib/plugins/media/media-toolbar.js +0 -101
  105. package/lib/plugins/media/media-toolbar.js.map +0 -1
  106. package/lib/plugins/media/media-wrapper.js +0 -93
  107. package/lib/plugins/media/media-wrapper.js.map +0 -1
  108. package/lib/plugins/respArea/drag-in-the-blank/choice.js +0 -251
  109. package/lib/plugins/respArea/drag-in-the-blank/choice.js.map +0 -1
  110. package/lib/plugins/respArea/drag-in-the-blank/index.js +0 -97
  111. package/lib/plugins/respArea/drag-in-the-blank/index.js.map +0 -1
  112. package/lib/plugins/respArea/explicit-constructed-response/index.js +0 -55
  113. package/lib/plugins/respArea/explicit-constructed-response/index.js.map +0 -1
  114. package/lib/plugins/respArea/icons/index.js +0 -95
  115. package/lib/plugins/respArea/icons/index.js.map +0 -1
  116. package/lib/plugins/respArea/index.js +0 -293
  117. package/lib/plugins/respArea/index.js.map +0 -1
  118. package/lib/plugins/respArea/inline-dropdown/index.js +0 -70
  119. package/lib/plugins/respArea/inline-dropdown/index.js.map +0 -1
  120. package/lib/plugins/respArea/utils.js +0 -110
  121. package/lib/plugins/respArea/utils.js.map +0 -1
  122. package/lib/plugins/table/icons/index.js +0 -69
  123. package/lib/plugins/table/icons/index.js.map +0 -1
  124. package/lib/plugins/table/index.js +0 -499
  125. package/lib/plugins/table/index.js.map +0 -1
  126. package/lib/plugins/table/table-toolbar.js +0 -158
  127. package/lib/plugins/table/table-toolbar.js.map +0 -1
  128. package/lib/plugins/toolbar/default-toolbar.js +0 -174
  129. package/lib/plugins/toolbar/default-toolbar.js.map +0 -1
  130. package/lib/plugins/toolbar/done-button.js +0 -50
  131. package/lib/plugins/toolbar/done-button.js.map +0 -1
  132. package/lib/plugins/toolbar/editor-and-toolbar.js +0 -287
  133. package/lib/plugins/toolbar/editor-and-toolbar.js.map +0 -1
  134. package/lib/plugins/toolbar/index.js +0 -34
  135. package/lib/plugins/toolbar/index.js.map +0 -1
  136. package/lib/plugins/toolbar/toolbar-buttons.js +0 -161
  137. package/lib/plugins/toolbar/toolbar-buttons.js.map +0 -1
  138. package/lib/plugins/toolbar/toolbar.js +0 -352
  139. package/lib/plugins/toolbar/toolbar.js.map +0 -1
  140. package/lib/plugins/utils.js +0 -62
  141. package/lib/plugins/utils.js.map +0 -1
  142. package/lib/serialization.js +0 -488
  143. package/lib/serialization.js.map +0 -1
  144. package/lib/theme.js +0 -9
  145. package/lib/theme.js.map +0 -1
  146. package/playground/image/data.js +0 -59
  147. package/playground/image/index.html +0 -22
  148. package/playground/image/index.jsx +0 -81
  149. package/playground/index.html +0 -25
  150. package/playground/mathquill/index.html +0 -22
  151. package/playground/mathquill/index.jsx +0 -155
  152. package/playground/package.json +0 -15
  153. package/playground/prod-test/index.html +0 -22
  154. package/playground/prod-test/index.jsx +0 -28
  155. package/playground/schema-override/data.js +0 -29
  156. package/playground/schema-override/image-plugin.jsx +0 -41
  157. package/playground/schema-override/index.html +0 -21
  158. package/playground/schema-override/index.jsx +0 -97
  159. package/playground/serialization/data.js +0 -29
  160. package/playground/serialization/image-plugin.jsx +0 -41
  161. package/playground/serialization/index.html +0 -22
  162. package/playground/serialization/index.jsx +0 -12
  163. package/playground/static.json +0 -3
  164. package/playground/table-examples.html +0 -70
  165. package/playground/webpack.config.js +0 -42
  166. 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
- log('value as html: ', html);
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: '25px',
223
- lineHeight: '15px',
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
- valueToUse = getFocusedValue();
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+00A0',
230
- description: 'NO-BREAK SPACE',
231
- write: String.fromCodePoint('0x00A0'),
232
- label: '&nbsp;',
229
+ unicode: 'U+200A',
230
+ description: 'HAIR SPACE',
231
+ write: String.fromCodePoint('0x200A'),
232
+ label: '&hairsp;',
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+200A',
337
- description: 'HAIR SPACE',
338
- write: String.fromCodePoint('0x200A'),
339
- label: '&hairsp;',
336
+ unicode: 'U+00A0',
337
+ description: 'NO-BREAK SPACE',
338
+ write: String.fromCodePoint('0x00A0'),
339
+ label: '&nbsp;',
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+0009',
394
- description: 'TAB',
395
- write: String.fromCodePoint('0x0009'),
396
- label: 'TAB',
393
+ unicode: 'U+2003',
394
+ description: 'EM SPACE',
395
+ write: String.fromCodePoint('0x2003'),
396
+ label: '&emsp;',
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, isEdited, toggleHtmlMode, handleAlertDialog } = opts;
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 may cause edits to be lost.',
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(value, onChange);
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 (isEdited) {
59
+ if (isEditedInHtmlMode) {
57
60
  handleHtmlModeOn(value, onChange);
58
61
  } else {
59
62
  toggleToRichText(value, onChange);