@pie-lib/editable-html 11.1.2-next.0 → 11.2.0-beta.2

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 +50 -158
  2. package/NEXT.CHANGELOG.json +1 -0
  3. package/package.json +11 -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
@@ -1,7 +1,13 @@
1
+ import Hotkeys from 'slate-hotkeys';
2
+ import { IS_IOS } from 'slate-dev-environment';
3
+ import { Mark } from 'slate';
1
4
  import Bold from '@material-ui/icons/FormatBold';
5
+ import FormatQuote from '@material-ui/icons/FormatQuote';
2
6
  //import Code from '@material-ui/icons/Code';
3
7
  import BulletedListIcon from '@material-ui/icons/FormatListBulleted';
4
8
  import NumberedListIcon from '@material-ui/icons/FormatListNumbered';
9
+ import Redo from '@material-ui/icons/Redo';
10
+ import Undo from '@material-ui/icons/Undo';
5
11
  import ImagePlugin from './image';
6
12
  import MediaPlugin from './media';
7
13
  import CharactersPlugin from './characters';
@@ -12,34 +18,153 @@ import Strikethrough from '@material-ui/icons/FormatStrikethrough';
12
18
  import ToolbarPlugin from './toolbar';
13
19
  import Underline from '@material-ui/icons/FormatUnderlined';
14
20
  import compact from 'lodash/compact';
21
+ import isEmpty from 'lodash/isEmpty';
15
22
  import SoftBreakPlugin from 'slate-soft-break';
16
23
  import debug from 'debug';
17
24
  import List from './list';
18
25
  import TablePlugin from './table';
19
26
  import RespAreaPlugin from './respArea';
20
27
  import HtmlPlugin from './html';
28
+ import CSSPlugin from './css';
29
+ import CustomPlugin from './customPlugin';
30
+ import RenderingPlugin from './rendering';
31
+ import TextAlign from './textAlign';
21
32
 
22
33
  const log = debug('@pie-lib:editable-html:plugins');
23
34
 
35
+ const SuperscriptIcon = () => (
36
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="none">
37
+ <path
38
+ d="M22,7h-2v1h3v1h-4V7c0-0.55,0.45-1,1-1h2V5h-3V4h3c0.55,0,1,0.45,1,1v1C23,6.55,22.55,7,22,7z M5.88,20h2.66l3.4-5.42h0.12 l3.4,5.42h2.66l-4.65-7.27L17.81,6h-2.68l-3.07,4.99h-0.12L8.85,6H6.19l4.32,6.73L5.88,20z"
39
+ fill="currentColor"
40
+ />
41
+ </svg>
42
+ );
43
+
44
+ const SubscriptIcon = () => (
45
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="none">
46
+ <path
47
+ d="M22,18h-2v1h3v1h-4v-2c0-0.55,0.45-1,1-1h2v-1h-3v-1h3c0.55,0,1,0.45,1,1v1C23,17.55,22.55,18,22,18z M5.88,18h2.66 l3.4-5.42h0.12l3.4,5.42h2.66l-4.65-7.27L17.81,4h-2.68l-3.07,4.99h-0.12L8.85,4H6.19l4.32,6.73L5.88,18z"
48
+ fill="currentColor"
49
+ />
50
+ </svg>
51
+ );
52
+
53
+ const HeadingIcon = () => (
54
+ <svg
55
+ width="30"
56
+ height="28"
57
+ viewBox="0 0 30 28"
58
+ fill="none"
59
+ xmlns="http://www.w3.org/2000/svg"
60
+ style={{ width: '20px', height: '18px' }}
61
+ >
62
+ <path
63
+ d="M27 4V24H29C29.5 24 30 24.5 30 25V27C30 27.5625 29.5 28 29 28H19C18.4375 28 18 27.5625 18 27V25C18 24.5 18.4375 24 19 24H21V16H9V24H11C11.5 24 12 24.5 12 25V27C12 27.5625 11.5 28 11 28H1C0.4375 28 0 27.5625 0 27V25C0 24.5 0.4375 24 1 24H3V4H1C0.4375 4 0 3.5625 0 3V1C0 0.5 0.4375 0 1 0H11C11.5 0 12 0.5 12 1V3C12 3.5625 11.5 4 11 4H9V12H21V4H19C18.4375 4 18 3.5625 18 3V1C18 0.5 18.4375 0 19 0H29C29.5 0 30 0.5 30 1V3C30 3.5625 29.5 4 29 4H27Z"
64
+ fill="currentColor"
65
+ />
66
+ </svg>
67
+ );
68
+ const STYLES_MAP = {
69
+ h3: {
70
+ fontSize: 'inherit',
71
+ fontWeight: 'inherit',
72
+ },
73
+ blockquote: {
74
+ background: '#f9f9f9',
75
+ borderLeft: '5px solid #ccc',
76
+ margin: '1.5em 10px',
77
+ padding: '.5em 10px',
78
+ },
79
+ };
80
+
24
81
  function MarkHotkey(options) {
25
82
  const { type, key, icon, tag } = options;
26
83
 
27
84
  // Return our "plugin" object, containing the `onKeyDown` handler.
28
85
  return {
86
+ name: type,
29
87
  toolbar: {
30
88
  isMark: true,
31
89
  type,
32
90
  icon,
33
91
  onToggle: (change) => {
34
92
  log('[onToggleMark] type: ', type);
93
+ const { selection } = change.value;
94
+
95
+ if (['blockquote', 'h3'].includes(type)) {
96
+ const texts = change.value.document.getTextsAtRangeAsArray(selection);
97
+ const onlyOneText = texts.length === 1;
98
+ let hasMark = false;
99
+ let sameMark = true;
100
+
101
+ texts.forEach((t) => {
102
+ const marks = t.getMarksAsArray();
103
+ const markIsThere = marks.find((m) => m.type === type);
104
+
105
+ if (!markIsThere) {
106
+ // not all texts have this mark
107
+ sameMark = false;
108
+ } else {
109
+ // at least one mark
110
+ hasMark = true;
111
+ }
112
+ });
113
+
114
+ const shouldContinue = onlyOneText || sameMark || !hasMark;
115
+
116
+ if (!shouldContinue) {
117
+ return change;
118
+ }
119
+
120
+ if (selection.startKey === selection.endKey && selection.anchorOffset === selection.focusOffset) {
121
+ const textNode = change.value.document.getNode(selection.startKey);
122
+
123
+ // select the whole line if there is no selection
124
+ change.moveFocusTo(textNode.key, 0).moveAnchorTo(textNode.key, textNode.text.length);
125
+
126
+ // remove toggle
127
+ const hasMark = change.value.activeMarks.find((entry) => {
128
+ return entry.type === type;
129
+ });
130
+
131
+ if (hasMark) {
132
+ change.removeMark(hasMark);
133
+ } else {
134
+ const newMark = Mark.create(type);
135
+
136
+ change.addMark(newMark);
137
+ }
138
+
139
+ // move focus to end of text
140
+ return change
141
+ .moveFocusTo(textNode.key, textNode.text.length)
142
+ .moveAnchorTo(textNode.key, textNode.text.length);
143
+ }
144
+ }
145
+
35
146
  return change.toggleMark(type);
36
147
  },
37
148
  },
38
149
  renderMark(props) {
39
150
  if (props.mark.type === type) {
151
+ const { data } = props.node || {};
152
+ const jsonData = data?.toJSON() || {};
40
153
  const K = tag || type;
154
+ const additionalStyles = STYLES_MAP[K];
155
+
156
+ if (additionalStyles) {
157
+ if (!jsonData.attributes) {
158
+ jsonData.attributes = {};
159
+ }
160
+
161
+ jsonData.attributes.style = {
162
+ ...jsonData.attributes.style,
163
+ ...additionalStyles,
164
+ };
165
+ }
41
166
 
42
- return <K>{props.children}</K>;
167
+ return <K {...jsonData.attributes}>{props.children}</K>;
43
168
  }
44
169
  },
45
170
  onKeyDown(event, change) {
@@ -60,6 +185,7 @@ export const ALL_PLUGINS = [
60
185
  'bold',
61
186
  // 'code',
62
187
  'html',
188
+ 'extraCSSRules',
63
189
  'italic',
64
190
  'underline',
65
191
  'strikethrough',
@@ -68,27 +194,131 @@ export const ALL_PLUGINS = [
68
194
  'image',
69
195
  'math',
70
196
  'languageCharacters',
197
+ 'text-align',
198
+ 'blockquote',
199
+ 'h3',
71
200
  'table',
72
201
  'video',
73
202
  'audio',
74
203
  'responseArea',
204
+ 'redo',
205
+ 'undo',
206
+ 'superscript',
207
+ 'subscript',
75
208
  ];
76
209
 
77
- export const DEFAULT_PLUGINS = ALL_PLUGINS.filter((plug) => plug !== 'responseArea');
210
+ export const DEFAULT_PLUGINS = ALL_PLUGINS.filter((plug) => !['responseArea', 'h3', 'blockquote'].includes(plug));
78
211
 
79
- export const buildPlugins = (activePlugins, opts) => {
212
+ const ICON_MAP = {
213
+ undo: Undo,
214
+ redo: Redo,
215
+ };
216
+ function UndoRedo(type) {
217
+ const IconToUse = ICON_MAP[type];
218
+
219
+ return {
220
+ name: type,
221
+ toolbar: {
222
+ type,
223
+ icon: <IconToUse />,
224
+ ariaLabel: type === 'undo' ? 'Undo (revert the last action)' : 'Redo (reapply the last undone action)',
225
+ onClick: (value, onChange) => {
226
+ const change = value.change();
227
+
228
+ onChange(change[type]());
229
+ },
230
+ },
231
+ };
232
+ }
233
+
234
+ function EnterHandlingPlugin() {
235
+ return {
236
+ name: 'enterHandling',
237
+ onKeyDown: (event, change) => {
238
+ if (Hotkeys.isSplitBlock(event) && !IS_IOS) {
239
+ if (change.value.isInVoid) {
240
+ return change.collapseToStartOfNextText();
241
+ }
242
+
243
+ change.splitBlock();
244
+
245
+ const range = change.value.selection;
246
+ const newBlock = change.value.document.getClosestBlock(range.startKey);
247
+
248
+ if (newBlock.type !== 'paragraph') {
249
+ change.setNodeByKey(newBlock.key, {
250
+ type: 'paragraph',
251
+ });
252
+ }
253
+
254
+ return change;
255
+ }
256
+
257
+ return undefined;
258
+ },
259
+ };
260
+ }
261
+
262
+ export const buildPlugins = (activePlugins, customPlugins, opts) => {
80
263
  log('[buildPlugins] opts: ', opts);
81
264
 
82
265
  activePlugins = activePlugins || DEFAULT_PLUGINS;
83
266
 
84
267
  const addIf = (key, p) => activePlugins.includes(key) && p;
268
+
85
269
  const imagePlugin = opts.image && opts.image.onDelete && ImagePlugin(opts.image);
86
270
  const mathPlugin = MathPlugin(opts.math);
87
271
  const respAreaPlugin =
88
272
  opts.responseArea && opts.responseArea.type && RespAreaPlugin(opts.responseArea, compact([mathPlugin]));
273
+ const cssPlugin = !isEmpty(opts.extraCSSRules) && CSSPlugin(opts.extraCSSRules);
274
+
275
+ const languageCharactersPlugins = (opts?.languageCharacters || []).map((config) =>
276
+ CharactersPlugin({
277
+ ...config,
278
+ keyPadCharacterRef: opts.keyPadCharacterRef,
279
+ setKeypadInteraction: opts.setKeypadInteraction,
280
+ }),
281
+ );
282
+
283
+ const tablePlugins = [imagePlugin, mathPlugin, respAreaPlugin, ...languageCharactersPlugins];
284
+
285
+ if (opts.responseArea && opts.responseArea.type === 'math-templated') {
286
+ tablePlugins.push(respAreaPlugin);
287
+ }
288
+
289
+ let builtCustomPlugins = [];
290
+
291
+ customPlugins.forEach((customPlugin) => {
292
+ const { event, icon, iconType, iconAlt } = customPlugin || {};
293
+
294
+ function isValidEventName(eventName) {
295
+ // Check if eventName is a non-empty string
296
+ if (typeof eventName !== 'string' || eventName.length === 0) {
297
+ return false;
298
+ }
299
+
300
+ // Regular expression to match valid event names (only alphanumeric characters and underscore)
301
+ const regex = /^[a-zA-Z0-9_]+$/;
302
+
303
+ // Check if the eventName matches the regular expression
304
+ return regex.test(eventName);
305
+ }
306
+
307
+ if (!isValidEventName(event)) {
308
+ console.error(`The event name: ${event} is not a valid event name!`);
309
+ return;
310
+ }
311
+
312
+ if (!icon && !iconType && !iconAlt) {
313
+ console.error('Your custom button requires icon, iconType and iconAlt');
314
+ return;
315
+ }
316
+
317
+ builtCustomPlugins.push(CustomPlugin('custom-plugin', customPlugin));
318
+ });
89
319
 
90
320
  return compact([
91
- addIf('table', TablePlugin(opts.table, compact([imagePlugin, mathPlugin, respAreaPlugin]))),
321
+ addIf('table', TablePlugin(opts.table, compact(tablePlugins))),
92
322
  addIf('bold', MarkHotkey({ key: 'b', type: 'bold', icon: <Bold />, tag: 'strong' })),
93
323
  // addIf('code', MarkHotkey({ key: '`', type: 'code', icon: <Code /> })),
94
324
  addIf('italic', MarkHotkey({ key: 'i', type: 'italic', icon: <Italic />, tag: 'em' })),
@@ -102,16 +332,29 @@ export const buildPlugins = (activePlugins, opts) => {
102
332
  }),
103
333
  ),
104
334
  addIf('underline', MarkHotkey({ key: 'u', type: 'underline', icon: <Underline />, tag: 'u' })),
335
+ // icon should be modifies accordingly
336
+ addIf('superscript', MarkHotkey({ type: 'sup', icon: <SuperscriptIcon />, tag: 'sup' })),
337
+ // icon should be modifies accordingly
338
+ addIf('subscript', MarkHotkey({ type: 'sub', icon: <SubscriptIcon />, tag: 'sub' })),
105
339
  addIf('image', imagePlugin),
106
340
  addIf('video', MediaPlugin('video', opts.media)),
107
341
  addIf('audio', MediaPlugin('audio', opts.media)),
108
342
  addIf('math', mathPlugin),
109
- ...opts.languageCharacters.map((config) => addIf('languageCharacters', CharactersPlugin(config))),
343
+ ...languageCharactersPlugins.map((plugin) => addIf('languageCharacters', plugin)),
344
+ addIf('text-align', TextAlign(opts.textAlign)),
345
+ addIf('blockquote', MarkHotkey({ key: 'q', type: 'blockquote', icon: <FormatQuote />, tag: 'blockquote' })),
346
+ addIf('h3', MarkHotkey({ key: 'h3', type: 'h3', icon: <HeadingIcon />, tag: 'h3' })),
110
347
  addIf('bulleted-list', List({ key: 'l', type: 'ul_list', icon: <BulletedListIcon /> })),
111
348
  addIf('numbered-list', List({ key: 'n', type: 'ol_list', icon: <NumberedListIcon /> })),
349
+ addIf('undo', UndoRedo('undo')),
350
+ addIf('redo', UndoRedo('redo')),
112
351
  ToolbarPlugin(opts.toolbar),
113
352
  SoftBreakPlugin({ shift: true }),
353
+ ...builtCustomPlugins,
114
354
  addIf('responseArea', respAreaPlugin),
355
+ cssPlugin,
115
356
  addIf('html', HtmlPlugin(opts.html)),
357
+ EnterHandlingPlugin(),
358
+ RenderingPlugin(),
116
359
  ]);
117
360
  };
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+
3
+ import List, { serialization } from '../index';
4
+ import debug from 'debug';
5
+
6
+ const log = debug('@pie-lib:editable-html:test:plugins:list');
7
+
8
+ describe('ListPlugin', () => {
9
+ let next;
10
+
11
+ describe('deserialize', () => {
12
+ next = jest.fn();
13
+
14
+ const assertDeserialize = (tagName, expectedType) => {
15
+ it(`should deserialize ${tagName} to ${expectedType}`, () => {
16
+ const out = serialization.deserialize({ tagName, children: [], childNodes: [] }, next);
17
+
18
+ expect(out).toMatchObject({ object: 'block', type: expectedType });
19
+ expect(next).toHaveBeenCalledWith([]);
20
+ });
21
+ };
22
+ assertDeserialize('ul', 'ul_list');
23
+ assertDeserialize('ol', 'ol_list');
24
+ assertDeserialize('li', 'list_item');
25
+ });
26
+
27
+ describe('serialize', () => {
28
+ const assertSerialize = (type, expectedType) => {
29
+ it(`should serialize ${type} to ${expectedType}`, () => {
30
+ const out = serialization.serialize({ object: 'block', type }, {});
31
+ log('out: ', out);
32
+ expect(out.type).toMatch(expectedType);
33
+ });
34
+ };
35
+ assertSerialize('ul_list', 'ul');
36
+ assertSerialize('ol_list', 'ol');
37
+ assertSerialize('list_item', 'li');
38
+ });
39
+
40
+ describe('renderNode', () => {
41
+ let plugin = List({});
42
+
43
+ const assertRenderNode = (type, expectedType) => {
44
+ it(`should renderNode ${type} to ${expectedType}`, () => {
45
+ const out = plugin.renderNode({ node: { type } });
46
+ expect(out.type).toMatch(expectedType);
47
+ });
48
+ };
49
+
50
+ assertRenderNode('ul_list', 'ul');
51
+ assertRenderNode('ol_list', 'ol');
52
+ assertRenderNode('list_item', 'li');
53
+ });
54
+ });
@@ -3,6 +3,7 @@ import { Data } from 'slate';
3
3
  import Immutable from 'immutable';
4
4
  import PropTypes from 'prop-types';
5
5
  import EditList from 'slate-edit-list';
6
+ import ListOptions from 'slate-edit-list/dist/options';
6
7
  import debug from 'debug';
7
8
 
8
9
  const log = debug('@pie-lib:editable-html:plugins:list');
@@ -51,6 +52,10 @@ const createEditList = () => {
51
52
  typeDefault: 'span',
52
53
  });
53
54
 
55
+ const listOptions = new ListOptions({
56
+ typeDefault: 'span',
57
+ });
58
+
54
59
  // fix outdated schema
55
60
  if (core.schema && core.schema.blocks) {
56
61
  Object.keys(core.schema.blocks).forEach((key) => {
@@ -119,6 +124,85 @@ const createEditList = () => {
119
124
  return change.normalize();
120
125
  };
121
126
 
127
+ core.changes.unwrapList = function unwrapList(opts, change) {
128
+ const items = core.utils.getItemsAtRange(change.value);
129
+
130
+ if (items.isEmpty()) {
131
+ return change;
132
+ }
133
+
134
+ // Unwrap the items from their list
135
+ items.forEach((item) => change.unwrapNodeByKey(item.key, { normalize: false }));
136
+
137
+ // Parent of the list of the items
138
+ const firstItem = items.first();
139
+ const parent = change.value.document.getParent(firstItem.key);
140
+
141
+ let index = parent.nodes.findIndex((node) => node.key === firstItem.key);
142
+
143
+ // Unwrap the items' children
144
+ items.forEach((item) => {
145
+ item.nodes.forEach((node) => {
146
+ change.moveNodeByKey(node.key, parent.key, index, {
147
+ normalize: false,
148
+ });
149
+ index += 1;
150
+ });
151
+ });
152
+
153
+ // Finally, remove the now empty items
154
+ items.forEach((item) => change.removeNodeByKey(item.key, { normalize: false }));
155
+
156
+ return change;
157
+ }.bind(this, listOptions);
158
+
159
+ core.utils.getItemsAtRange = function(opts, value, range) {
160
+ range = range || value.selection;
161
+
162
+ if (!range.startKey) {
163
+ return Immutable.List();
164
+ }
165
+
166
+ const { document } = value;
167
+
168
+ const startBlock = document.getClosestBlock(range.startKey);
169
+ const endBlock = document.getClosestBlock(range.endKey);
170
+
171
+ if (startBlock === endBlock) {
172
+ const item = core.utils.getCurrentItem(value, startBlock);
173
+ return item ? Immutable.List([item]) : Immutable.List();
174
+ }
175
+
176
+ const ancestor = document.getCommonAncestor(startBlock.key, endBlock.key);
177
+
178
+ if (core.utils.isList(ancestor)) {
179
+ const startPath = ancestor.getPath(startBlock.key);
180
+ const endPath = ancestor.getPath(endBlock.key);
181
+
182
+ return ancestor.nodes.slice(startPath.get(0), endPath.get(0) + 1);
183
+ } else if (ancestor.type === opts.typeItem) {
184
+ // The ancestor is the highest list item that covers the range
185
+ return Immutable.List([ancestor]);
186
+ }
187
+ // No list of items can cover the range
188
+ return Immutable.List();
189
+ }.bind(this, listOptions);
190
+
191
+ core.utils.getListForItem = function(opts, value, item) {
192
+ const { document } = value;
193
+ const parent = document.getParent(item.key);
194
+ return parent && core.utils.isList(parent) ? parent : null;
195
+ }.bind(this, listOptions);
196
+
197
+ core.utils.isSelectionInList = function(opts, value, type) {
198
+ const items = core.utils.getItemsAtRange(value);
199
+ return (
200
+ !items.isEmpty() &&
201
+ // Check the type of the list if needed
202
+ (!type || core.utils.getListForItem(value, items.first()).get('type') === type)
203
+ );
204
+ }.bind(this, listOptions);
205
+
122
206
  return core;
123
207
  };
124
208
 
@@ -143,6 +227,7 @@ export default (options) => {
143
227
 
144
228
  core.toolbar = {
145
229
  isMark: false,
230
+ ariaLabel: type == 'ul_list' ? 'bulleted list' : 'numbered-list',
146
231
  type,
147
232
  icon,
148
233
  isActive: (value, type) => {
@@ -165,11 +250,56 @@ export default (options) => {
165
250
  },
166
251
  };
167
252
 
253
+ core.normalizeNode = (node) => {
254
+ if (node.object !== 'document' && node.object !== 'block') {
255
+ return undefined;
256
+ }
257
+
258
+ const response = core.validateNode(node);
259
+
260
+ const invalidListItems = [];
261
+
262
+ node.forEachDescendant((d) => {
263
+ if (d.type === 'list_item' && d.nodes.size === 1 && d.nodes.first().object === 'text') {
264
+ // if we have a list_item that has only a text inside, we need to add a block in it
265
+ invalidListItems.push(d);
266
+ }
267
+ });
268
+
269
+ if (!invalidListItems.length && !response) {
270
+ return undefined;
271
+ }
272
+
273
+ return (change) => {
274
+ if (response) {
275
+ response(change);
276
+ }
277
+
278
+ if (invalidListItems.length) {
279
+ change.withoutNormalization(() => {
280
+ invalidListItems.forEach((node) => {
281
+ const textNode = node.nodes.first();
282
+ const wrappedBlock = {
283
+ object: 'block',
284
+ type: 'div',
285
+ nodes: [textNode.toJSON()],
286
+ };
287
+
288
+ change.removeNodeByKey(textNode.key);
289
+
290
+ change.insertNodeByKey(node.key, 0, wrappedBlock);
291
+ });
292
+ });
293
+ }
294
+ };
295
+ };
296
+
168
297
  core.renderNode.propTypes = {
169
298
  node: PropTypes.object,
170
299
  attributes: PropTypes.object,
171
300
  children: PropTypes.func,
172
301
  };
302
+ core.name = type;
173
303
 
174
304
  return core;
175
305
  };
@@ -0,0 +1,48 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`CustomToolbarComp render renders with default keypadMode 1`] = `
4
+ <MathToolbar
5
+ additionalKeys={Array []}
6
+ autoFocus={true}
7
+ controlledKeypadMode={true}
8
+ keypadMode="geometry"
9
+ latex="foo"
10
+ onChange={[Function]}
11
+ onDone={[Function]}
12
+ />
13
+ `;
14
+
15
+ exports[`CustomToolbarComp render renders with default keypadMode 2`] = `
16
+ <MathToolbar
17
+ additionalKeys={Array []}
18
+ autoFocus={true}
19
+ controlledKeypadMode={true}
20
+ keypadMode={3}
21
+ latex="foo"
22
+ onChange={[Function]}
23
+ onDone={[Function]}
24
+ />
25
+ `;
26
+
27
+ exports[`CustomToolbarComp render renders without default keypadMode 1`] = `
28
+ <MathToolbar
29
+ additionalKeys={Array []}
30
+ autoFocus={true}
31
+ controlledKeypadMode={true}
32
+ latex="foo"
33
+ onChange={[Function]}
34
+ onDone={[Function]}
35
+ />
36
+ `;
37
+
38
+ exports[`CustomToolbarComp render renders without default keypadMode 2`] = `
39
+ <MathToolbar
40
+ additionalKeys={Array []}
41
+ autoFocus={true}
42
+ controlledKeypadMode={true}
43
+ keypadMode={3}
44
+ latex="foo"
45
+ onChange={[Function]}
46
+ onDone={[Function]}
47
+ />
48
+ `;