@pie-lib/editable-html-tip-tap 1.0.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 (167) hide show
  1. package/CHANGELOG.json +32 -0
  2. package/CHANGELOG.md +2280 -0
  3. package/lib/__tests__/editor.test.js +470 -0
  4. package/lib/__tests__/serialization.test.js +246 -0
  5. package/lib/__tests__/utils.js +106 -0
  6. package/lib/block-tags.js +25 -0
  7. package/lib/constants.js +16 -0
  8. package/lib/editor.js +1356 -0
  9. package/lib/extensions/MediaView.js +112 -0
  10. package/lib/extensions/characters.js +65 -0
  11. package/lib/extensions/component.js +325 -0
  12. package/lib/extensions/css.js +252 -0
  13. package/lib/extensions/custom-toolbar-wrapper.js +124 -0
  14. package/lib/extensions/image.js +106 -0
  15. package/lib/extensions/math.js +330 -0
  16. package/lib/extensions/media.js +276 -0
  17. package/lib/extensions/responseArea.js +278 -0
  18. package/lib/index.js +1213 -0
  19. package/lib/old-index.js +269 -0
  20. package/lib/parse-html.js +16 -0
  21. package/lib/plugins/characters/custom-popper.js +73 -0
  22. package/lib/plugins/characters/index.js +305 -0
  23. package/lib/plugins/characters/utils.js +381 -0
  24. package/lib/plugins/css/icons/index.js +37 -0
  25. package/lib/plugins/css/index.js +390 -0
  26. package/lib/plugins/customPlugin/index.js +114 -0
  27. package/lib/plugins/html/icons/index.js +38 -0
  28. package/lib/plugins/html/index.js +81 -0
  29. package/lib/plugins/image/__tests__/component.test.js +51 -0
  30. package/lib/plugins/image/__tests__/image-toolbar-logic.test.js +56 -0
  31. package/lib/plugins/image/__tests__/image-toolbar.test.js +26 -0
  32. package/lib/plugins/image/__tests__/index.test.js +98 -0
  33. package/lib/plugins/image/__tests__/insert-image-handler.test.js +125 -0
  34. package/lib/plugins/image/__tests__/mock-change.js +25 -0
  35. package/lib/plugins/image/alt-dialog.js +129 -0
  36. package/lib/plugins/image/component.js +419 -0
  37. package/lib/plugins/image/image-toolbar.js +177 -0
  38. package/lib/plugins/image/index.js +263 -0
  39. package/lib/plugins/image/insert-image-handler.js +117 -0
  40. package/lib/plugins/index.js +413 -0
  41. package/lib/plugins/list/__tests__/index.test.js +79 -0
  42. package/lib/plugins/list/index.js +334 -0
  43. package/lib/plugins/math/__tests__/index.test.js +300 -0
  44. package/lib/plugins/math/index.js +454 -0
  45. package/lib/plugins/media/__tests__/index.test.js +71 -0
  46. package/lib/plugins/media/index.js +387 -0
  47. package/lib/plugins/media/media-dialog.js +709 -0
  48. package/lib/plugins/media/media-toolbar.js +101 -0
  49. package/lib/plugins/media/media-wrapper.js +93 -0
  50. package/lib/plugins/rendering/index.js +46 -0
  51. package/lib/plugins/respArea/drag-in-the-blank/choice.js +289 -0
  52. package/lib/plugins/respArea/drag-in-the-blank/index.js +94 -0
  53. package/lib/plugins/respArea/explicit-constructed-response/index.js +120 -0
  54. package/lib/plugins/respArea/icons/index.js +95 -0
  55. package/lib/plugins/respArea/index.js +341 -0
  56. package/lib/plugins/respArea/inline-dropdown/index.js +126 -0
  57. package/lib/plugins/respArea/math-templated/index.js +130 -0
  58. package/lib/plugins/respArea/utils.js +125 -0
  59. package/lib/plugins/table/CustomTablePlugin.js +133 -0
  60. package/lib/plugins/table/__tests__/index.test.js +442 -0
  61. package/lib/plugins/table/__tests__/table-toolbar.test.js +54 -0
  62. package/lib/plugins/table/icons/index.js +69 -0
  63. package/lib/plugins/table/index.js +483 -0
  64. package/lib/plugins/table/table-toolbar.js +187 -0
  65. package/lib/plugins/textAlign/icons/index.js +194 -0
  66. package/lib/plugins/textAlign/index.js +34 -0
  67. package/lib/plugins/toolbar/__tests__/default-toolbar.test.js +128 -0
  68. package/lib/plugins/toolbar/__tests__/editor-and-toolbar.test.js +51 -0
  69. package/lib/plugins/toolbar/__tests__/toolbar-buttons.test.js +54 -0
  70. package/lib/plugins/toolbar/__tests__/toolbar.test.js +120 -0
  71. package/lib/plugins/toolbar/default-toolbar.js +229 -0
  72. package/lib/plugins/toolbar/done-button.js +53 -0
  73. package/lib/plugins/toolbar/editor-and-toolbar.js +286 -0
  74. package/lib/plugins/toolbar/index.js +34 -0
  75. package/lib/plugins/toolbar/toolbar-buttons.js +194 -0
  76. package/lib/plugins/toolbar/toolbar.js +376 -0
  77. package/lib/plugins/utils.js +62 -0
  78. package/lib/serialization.js +677 -0
  79. package/lib/shared/alert-dialog.js +75 -0
  80. package/lib/theme.js +9 -0
  81. package/package.json +69 -0
  82. package/src/__tests__/editor.test.jsx +363 -0
  83. package/src/__tests__/serialization.test.js +291 -0
  84. package/src/__tests__/utils.js +36 -0
  85. package/src/block-tags.js +17 -0
  86. package/src/constants.js +7 -0
  87. package/src/editor.jsx +1197 -0
  88. package/src/extensions/characters.js +46 -0
  89. package/src/extensions/component.jsx +294 -0
  90. package/src/extensions/css.js +217 -0
  91. package/src/extensions/custom-toolbar-wrapper.jsx +100 -0
  92. package/src/extensions/image.js +55 -0
  93. package/src/extensions/math.js +259 -0
  94. package/src/extensions/media.js +182 -0
  95. package/src/extensions/responseArea.js +205 -0
  96. package/src/index.jsx +1462 -0
  97. package/src/old-index.jsx +162 -0
  98. package/src/parse-html.js +8 -0
  99. package/src/plugins/README.md +27 -0
  100. package/src/plugins/characters/custom-popper.js +48 -0
  101. package/src/plugins/characters/index.jsx +284 -0
  102. package/src/plugins/characters/utils.js +447 -0
  103. package/src/plugins/css/icons/index.jsx +17 -0
  104. package/src/plugins/css/index.jsx +340 -0
  105. package/src/plugins/customPlugin/index.jsx +85 -0
  106. package/src/plugins/html/icons/index.jsx +19 -0
  107. package/src/plugins/html/index.jsx +72 -0
  108. package/src/plugins/image/__tests__/__snapshots__/component.test.jsx.snap +51 -0
  109. package/src/plugins/image/__tests__/__snapshots__/image-toolbar-logic.test.jsx.snap +27 -0
  110. package/src/plugins/image/__tests__/__snapshots__/image-toolbar.test.jsx.snap +44 -0
  111. package/src/plugins/image/__tests__/component.test.jsx +41 -0
  112. package/src/plugins/image/__tests__/image-toolbar-logic.test.jsx +42 -0
  113. package/src/plugins/image/__tests__/image-toolbar.test.jsx +11 -0
  114. package/src/plugins/image/__tests__/index.test.js +95 -0
  115. package/src/plugins/image/__tests__/insert-image-handler.test.js +113 -0
  116. package/src/plugins/image/__tests__/mock-change.js +15 -0
  117. package/src/plugins/image/alt-dialog.jsx +82 -0
  118. package/src/plugins/image/component.jsx +343 -0
  119. package/src/plugins/image/image-toolbar.jsx +100 -0
  120. package/src/plugins/image/index.jsx +227 -0
  121. package/src/plugins/image/insert-image-handler.js +79 -0
  122. package/src/plugins/index.jsx +377 -0
  123. package/src/plugins/list/__tests__/index.test.js +54 -0
  124. package/src/plugins/list/index.jsx +305 -0
  125. package/src/plugins/math/__tests__/__snapshots__/index.test.jsx.snap +48 -0
  126. package/src/plugins/math/__tests__/index.test.jsx +245 -0
  127. package/src/plugins/math/index.jsx +379 -0
  128. package/src/plugins/media/__tests__/index.test.js +75 -0
  129. package/src/plugins/media/index.jsx +325 -0
  130. package/src/plugins/media/media-dialog.js +624 -0
  131. package/src/plugins/media/media-toolbar.jsx +56 -0
  132. package/src/plugins/media/media-wrapper.jsx +43 -0
  133. package/src/plugins/rendering/index.js +31 -0
  134. package/src/plugins/respArea/drag-in-the-blank/choice.jsx +215 -0
  135. package/src/plugins/respArea/drag-in-the-blank/index.jsx +70 -0
  136. package/src/plugins/respArea/explicit-constructed-response/index.jsx +92 -0
  137. package/src/plugins/respArea/icons/index.jsx +71 -0
  138. package/src/plugins/respArea/index.jsx +299 -0
  139. package/src/plugins/respArea/inline-dropdown/index.jsx +108 -0
  140. package/src/plugins/respArea/math-templated/index.jsx +104 -0
  141. package/src/plugins/respArea/utils.jsx +90 -0
  142. package/src/plugins/table/CustomTablePlugin.js +113 -0
  143. package/src/plugins/table/__tests__/__snapshots__/table-toolbar.test.jsx.snap +44 -0
  144. package/src/plugins/table/__tests__/index.test.jsx +401 -0
  145. package/src/plugins/table/__tests__/table-toolbar.test.jsx +42 -0
  146. package/src/plugins/table/icons/index.jsx +53 -0
  147. package/src/plugins/table/index.jsx +427 -0
  148. package/src/plugins/table/table-toolbar.jsx +136 -0
  149. package/src/plugins/textAlign/icons/index.jsx +114 -0
  150. package/src/plugins/textAlign/index.jsx +23 -0
  151. package/src/plugins/toolbar/__tests__/__snapshots__/default-toolbar.test.jsx.snap +923 -0
  152. package/src/plugins/toolbar/__tests__/__snapshots__/editor-and-toolbar.test.jsx.snap +20 -0
  153. package/src/plugins/toolbar/__tests__/__snapshots__/toolbar-buttons.test.jsx.snap +36 -0
  154. package/src/plugins/toolbar/__tests__/__snapshots__/toolbar.test.jsx.snap +46 -0
  155. package/src/plugins/toolbar/__tests__/default-toolbar.test.jsx +94 -0
  156. package/src/plugins/toolbar/__tests__/editor-and-toolbar.test.jsx +37 -0
  157. package/src/plugins/toolbar/__tests__/toolbar-buttons.test.jsx +51 -0
  158. package/src/plugins/toolbar/__tests__/toolbar.test.jsx +106 -0
  159. package/src/plugins/toolbar/default-toolbar.jsx +206 -0
  160. package/src/plugins/toolbar/done-button.jsx +38 -0
  161. package/src/plugins/toolbar/editor-and-toolbar.jsx +257 -0
  162. package/src/plugins/toolbar/index.jsx +23 -0
  163. package/src/plugins/toolbar/toolbar-buttons.jsx +138 -0
  164. package/src/plugins/toolbar/toolbar.jsx +338 -0
  165. package/src/plugins/utils.js +31 -0
  166. package/src/serialization.jsx +621 -0
  167. package/src/theme.js +1 -0
@@ -0,0 +1,46 @@
1
+ // InlineNodes.js
2
+ import React from 'react';
3
+ import { Node, ReactNodeViewRenderer } from '@tiptap/react';
4
+ import ExplicitConstructedResponse from '../plugins/respArea/explicit-constructed-response';
5
+ import DragInTheBlank from '../plugins/respArea/drag-in-the-blank';
6
+ import InlineDropdown from '../plugins/respArea/inline-dropdown';
7
+
8
+ /**
9
+ * ExplicitConstructedResponse Node
10
+ */
11
+ export const ExplicitConstructedResponseNode = Node.create({
12
+ name: 'explicit_constructed_response',
13
+ group: 'inline',
14
+ inline: true,
15
+ atom: true,
16
+ addAttributes() {
17
+ return {
18
+ index: { default: null },
19
+ value: { default: '' },
20
+ };
21
+ },
22
+ parseHTML() {
23
+ return [
24
+ {
25
+ tag: 'span[data-type="explicit_constructed_response"]',
26
+ getAttrs: (el) => ({
27
+ index: el.dataset.index,
28
+ value: el.dataset.value,
29
+ }),
30
+ },
31
+ ];
32
+ },
33
+ renderHTML({ HTMLAttributes }) {
34
+ return [
35
+ 'span',
36
+ {
37
+ 'data-type': 'explicit_constructed_response',
38
+ 'data-index': HTMLAttributes.index,
39
+ 'data-value': HTMLAttributes.value,
40
+ },
41
+ ];
42
+ },
43
+ addNodeView() {
44
+ return ReactNodeViewRenderer(ExplicitConstructedResponse);
45
+ },
46
+ });
@@ -0,0 +1,294 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import classNames from 'classnames';
4
+ import isEqual from 'lodash/isEqual';
5
+ import debug from 'debug';
6
+ import LinearProgress from '@material-ui/core/LinearProgress';
7
+ import { withStyles } from '@material-ui/core/styles';
8
+ import { NodeViewWrapper } from '@tiptap/react';
9
+ import InsertImageHandler from '../plugins/image/insert-image-handler';
10
+ import ImageToolbar from '../plugins/image/image-toolbar';
11
+ import CustomToolbarWrapper from "./custom-toolbar-wrapper";
12
+
13
+ const log = debug('@pie-lib:editable-html:plugins:image:component');
14
+
15
+ const sizePx = (s) => (s ? `${s}px` : 'calc(20px)');
16
+
17
+ function ImageComponent(props) {
18
+ const {
19
+ node,
20
+ editor,
21
+ classes,
22
+ attributes,
23
+ onFocus,
24
+ selected,
25
+ options,
26
+ maxImageWidth = 700,
27
+ maxImageHeight = 900,
28
+ latex,
29
+ handleChange,
30
+ handleDone,
31
+ } = props;
32
+ const { alt } = node.attrs;
33
+
34
+ const [showToolbar, setShowToolbar] = useState(false);
35
+
36
+ const imgRef = useRef(null);
37
+ const resizeRef = useRef(null);
38
+ const toolbarRef = useRef(null);
39
+
40
+ const getPercentFromWidth = useCallback((width) => {
41
+ const floored = (width / imgRef.current.naturalWidth) * 4;
42
+ return parseInt(floored.toFixed(0) * 25, 10);
43
+ }, []);
44
+
45
+ const applySizeData = useCallback(() => {
46
+ if (!node.attrs.width || !imgRef.current) return;
47
+
48
+ const update = {
49
+ ...node.attrs,
50
+ resizePercent: getPercentFromWidth(node.attrs.width),
51
+ };
52
+
53
+ if (!isEqual(update, node.attrs)) {
54
+ editor.commands.updateAttributes('imageUploadNode', update);
55
+ }
56
+ }, [editor, node.attrs, getPercentFromWidth]);
57
+
58
+ useEffect(() => {
59
+ setShowToolbar(selected);
60
+ }, [selected]);
61
+
62
+ useEffect(() => {
63
+ options.imageHandling.insertImageRequested(node, (finish) => new InsertImageHandler(editor, finish));
64
+ applySizeData();
65
+
66
+ const resizeHandle = resizeRef.current;
67
+ if (resizeHandle) {
68
+ resizeHandle.addEventListener('mousedown', initResize, false);
69
+ }
70
+ return () => {
71
+ if (resizeHandle) resizeHandle.removeEventListener('mousedown', initResize, false);
72
+ };
73
+ }, []);
74
+
75
+ useEffect(() => {
76
+ applySizeData();
77
+ });
78
+
79
+ const loadImage = useCallback(() => {
80
+ const box = imgRef.current;
81
+ if (!box) return;
82
+
83
+ if (!box.style.width || box.style.width === 'calc(20px)') {
84
+ const w = Math.min(box.naturalWidth, maxImageWidth);
85
+ const h = Math.min(box.naturalHeight, maxImageHeight);
86
+
87
+ box.style.width = `${w}px`;
88
+ box.style.height = `${h}px`;
89
+
90
+ const update = { width: w, height: h };
91
+ if (!isEqual(update, node.attrs)) {
92
+ editor.commands.updateAttributes('imageUploadNode', update);
93
+ }
94
+ }
95
+ }, [editor, node.attrs, maxImageWidth, maxImageHeight]);
96
+
97
+ const updateAspect = (initial, next, keepAspect = true, resizeType) => {
98
+ if (keepAspect) {
99
+ const ratio = initial.width / initial.height;
100
+ if (resizeType === 'height') return { width: next.height * ratio, height: next.height };
101
+ return { width: next.width, height: next.width / ratio };
102
+ }
103
+ return next;
104
+ };
105
+
106
+ const startResize = useCallback(
107
+ (e) => {
108
+ const box = imgRef.current;
109
+ if (!box) return;
110
+
111
+ const bounds = e.target.getBoundingClientRect();
112
+ const initial = { width: box.naturalWidth, height: box.naturalHeight };
113
+
114
+ const next = updateAspect(initial, {
115
+ width: e.clientX - bounds.left,
116
+ height: e.clientY - bounds.top,
117
+ });
118
+
119
+ if (next.width > 50 && next.height > 50 && next.width <= 700 && next.height <= 900) {
120
+ box.style.width = `${next.width}px`;
121
+ box.style.height = `${next.height}px`;
122
+
123
+ const update = { width: next.width, height: next.height };
124
+ if (!isEqual(update, node.attrs)) {
125
+ editor.commands.updateAttributes('imageUploadNode', update);
126
+ }
127
+ }
128
+ },
129
+ [editor, node.attrs],
130
+ );
131
+
132
+ const onChange = useCallback(
133
+ (newValues) => {
134
+ editor.commands.updateAttributes('imageUploadNode', newValues);
135
+ },
136
+ [editor],
137
+ );
138
+
139
+ const stopResize = useCallback(() => {
140
+ window.removeEventListener('mousemove', startResize);
141
+ window.removeEventListener('mouseup', stopResize);
142
+ }, [startResize]);
143
+
144
+ const initResize = useCallback(() => {
145
+ window.addEventListener('mousemove', startResize);
146
+ window.addEventListener('mouseup', stopResize);
147
+ }, [startResize, stopResize]);
148
+
149
+ const style = {
150
+ width: sizePx(node.attrs.width),
151
+ height: sizePx(node.attrs.height),
152
+ objectFit: 'contain',
153
+ };
154
+
155
+ const flexAlign = { left: 'flex-start', center: 'center', right: 'flex-end' }[node.attrs.alignment] || 'flex-start';
156
+
157
+ return (
158
+ <NodeViewWrapper>
159
+ <div
160
+ onFocus={onFocus}
161
+ className={classNames(
162
+ classes.root,
163
+ !node.attrs.loaded && classes.loading,
164
+ node.attrs.deleteStatus === 'pending' && classes.pendingDelete,
165
+ )}
166
+ style={{ justifyContent: flexAlign }}
167
+ >
168
+ <LinearProgress
169
+ mode="determinate"
170
+ value={node.attrs.percent || 0}
171
+ className={classNames(classes.progress, node.attrs.loaded && classes.hideProgress)}
172
+ />
173
+
174
+ <div className={classes.imageContainer}>
175
+ <img
176
+ {...attributes}
177
+ ref={imgRef}
178
+ src={node.attrs.src}
179
+ className={classNames(classes.image, selected && classes.active)}
180
+ style={style}
181
+ onLoad={loadImage}
182
+ alt={node.attrs.alt}
183
+ />
184
+ <div ref={resizeRef} className={classNames(classes.resize, 'resize')} />
185
+ </div>
186
+ </div>
187
+
188
+ {showToolbar && (
189
+ <div
190
+ ref={toolbarRef}
191
+ style={{
192
+ position: 'absolute',
193
+ top: '100%',
194
+ left: 0,
195
+ zIndex: 20,
196
+ background: 'var(--editable-html-toolbar-bg, #efefef)',
197
+ boxShadow:
198
+ '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
199
+ width: '100%'
200
+ }}
201
+ >
202
+ <CustomToolbarWrapper showDone {...options}>
203
+ <ImageToolbar
204
+ disableImageAlignmentButtons={options.disableImageAlignmentButtons}
205
+ alt={node.attrs.alt}
206
+ imageLoaded={node.attrs.loaded}
207
+ alignment={node.attrs.alignment || 'left'}
208
+ onChange={onChange}
209
+ />
210
+ </CustomToolbarWrapper>
211
+ </div>
212
+ )}
213
+ </NodeViewWrapper>
214
+ );
215
+ }
216
+
217
+ ImageComponent.propTypes = {
218
+ node: PropTypes.object.isRequired,
219
+ editor: PropTypes.object.isRequired,
220
+ classes: PropTypes.object.isRequired,
221
+ attributes: PropTypes.object,
222
+ onFocus: PropTypes.func,
223
+ maxImageWidth: PropTypes.number,
224
+ maxImageHeight: PropTypes.number,
225
+ };
226
+
227
+ export default withStyles((theme) => ({
228
+ portal: {
229
+ position: 'absolute',
230
+ opacity: 0,
231
+ transition: 'opacity 200ms linear',
232
+ },
233
+ floatingButtonRow: {
234
+ backgroundColor: theme.palette.background.paper,
235
+ borderRadius: '1px',
236
+ display: 'flex',
237
+ padding: '10px',
238
+ border: `solid 1px ${theme.palette.grey[200]}`,
239
+ boxShadow:
240
+ '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
241
+ },
242
+ progress: {
243
+ position: 'absolute',
244
+ left: '0',
245
+ width: 'fit-content',
246
+ top: '0%',
247
+ transition: 'opacity 200ms linear',
248
+ },
249
+ hideProgress: {
250
+ opacity: 0,
251
+ },
252
+ loading: {
253
+ opacity: 0.3,
254
+ },
255
+ pendingDelete: {
256
+ opacity: 0.3,
257
+ },
258
+ root: {
259
+ position: 'relative',
260
+ border: `solid 1px ${theme.palette.common.white}`,
261
+ display: 'flex',
262
+ transition: 'opacity 200ms linear',
263
+ },
264
+ delete: {
265
+ position: 'absolute',
266
+ right: 0,
267
+ },
268
+ imageContainer: {
269
+ position: 'relative',
270
+ width: 'fit-content',
271
+ display: 'flex',
272
+ alignItems: 'center',
273
+
274
+ '&&:hover > .resize': {
275
+ display: 'block',
276
+ },
277
+ },
278
+ active: {
279
+ border: `solid 1px ${theme.palette.primary.main}`,
280
+ },
281
+ resize: {
282
+ backgroundColor: theme.palette.primary.main,
283
+ cursor: 'col-resize',
284
+ height: '35px',
285
+ width: '5px',
286
+ borderRadius: 8,
287
+ marginLeft: '5px',
288
+ marginRight: '10px',
289
+ display: 'none',
290
+ },
291
+ drawableHeight: {
292
+ minHeight: 350,
293
+ },
294
+ }))(ImageComponent);
@@ -0,0 +1,217 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { Mark, mergeAttributes } from '@tiptap/core';
4
+ import List from '@material-ui/core/List';
5
+ import ListItem from '@material-ui/core/ListItem';
6
+
7
+ export const removeDialogs = () => {
8
+ const prevDialogs = document.querySelectorAll('.insert-css-dialog');
9
+
10
+ prevDialogs.forEach((s) => s.remove());
11
+ };
12
+
13
+ const insertDialog = ({ editor, callback, opts, selectedText, parentNode }) => {
14
+ const editorDOM = editor.options.element;
15
+ const newEl = document.createElement('div');
16
+ const { selection } = editor.state;
17
+
18
+ removeDialogs();
19
+
20
+ newEl.className = 'insert-css-dialog';
21
+
22
+ let popoverEl;
23
+
24
+ const closePopOver = () => {
25
+ if (popoverEl) {
26
+ popoverEl.remove();
27
+ }
28
+ };
29
+
30
+ let firstCallMade = false;
31
+
32
+ const listener = (e) => {
33
+ // this will be triggered right after setting it because
34
+ // this toolbar is added on the mousedown event
35
+ // so right after mouseup, the click will be triggered
36
+ if (firstCallMade) {
37
+ const focusIsInModals = newEl.contains(e.target) || (popoverEl && popoverEl.contains(e.target));
38
+ const focusIsInEditor = editorDOM.contains(e.target);
39
+
40
+ if (!(focusIsInModals || focusIsInEditor)) {
41
+ handleClose();
42
+ }
43
+ } else {
44
+ firstCallMade = true;
45
+ }
46
+ };
47
+
48
+ const handleClose = () => {
49
+ callback(undefined, true);
50
+ newEl.remove();
51
+ closePopOver();
52
+ document.body.removeEventListener('click', listener);
53
+ };
54
+
55
+ const handleChange = (name) => {
56
+ callback(name, true);
57
+ newEl.remove();
58
+ closePopOver();
59
+ document.body.removeEventListener('click', listener);
60
+ };
61
+
62
+ const parentNodeClass = parentNode?.attrs.class;
63
+ const createHTML = (name) => {
64
+ let html = `<span class="${name}">${selectedText}</span>`;
65
+
66
+ if (parentNode) {
67
+ let tag = 'span';
68
+
69
+ if (parentNode?.object === 'inline') {
70
+ tag = 'span';
71
+ }
72
+
73
+ if (parentNode?.object === 'block') {
74
+ tag = 'div';
75
+ }
76
+
77
+ html = `<${tag} class="${parentNodeClass}">${parentNode.text.slice(0, selection.$anchor.textOffset)}${html}${parentNode.text.slice(
78
+ selection.$head.textOffset,
79
+ )}</${tag}>`;
80
+ }
81
+
82
+ return html;
83
+ };
84
+
85
+ const el = (
86
+ <div
87
+ style={{ background: 'white', height: 500, padding: 20, overflow: 'hidden', display: 'flex', flexFlow: 'column' }}
88
+ >
89
+ <h2>Please choose a css class</h2>
90
+ {parentNodeClass && <div>The current parent has this class {parentNodeClass}</div>}
91
+ <List component="nav" style={{ overflow: 'scroll' }}>
92
+ {opts.names.map((name, i) => (
93
+ <ListItem key={`rule-${i}`} button onClick={() => handleChange(name)}>
94
+ <div style={{ marginRight: 20 }}>{name}</div>
95
+ <div
96
+ dangerouslySetInnerHTML={{
97
+ __html: createHTML(name),
98
+ }}
99
+ />
100
+ </ListItem>
101
+ ))}
102
+ </List>
103
+ </div>
104
+ );
105
+
106
+ ReactDOM.render(el, newEl, () => {
107
+ const cursorItem = editor.view.nodeDOM(editor.state.selection.from);
108
+ const cursorNode = cursorItem?.parentNode;
109
+
110
+ if (cursorNode) {
111
+ const bodyRect = editorDOM.parentElement.parentElement.parentElement.getBoundingClientRect();
112
+ const boundRect = cursorNode.getBoundingClientRect();
113
+
114
+ editorDOM.parentElement.parentElement.parentElement.appendChild(newEl);
115
+
116
+ newEl.style.maxWidth = '500px';
117
+ newEl.style.position = 'absolute';
118
+ newEl.style.top = 0;
119
+ newEl.style.zIndex = 99999;
120
+
121
+ const leftValue = `${boundRect.left + Math.abs(bodyRect.left) + cursorNode.offsetWidth + 10}px`;
122
+
123
+ const rightValue = `${boundRect.x}px`;
124
+
125
+ newEl.style.left = leftValue;
126
+
127
+ const leftAlignedWidth = newEl.offsetWidth;
128
+
129
+ newEl.style.left = 'unset';
130
+ newEl.style.right = rightValue;
131
+
132
+ const rightAlignedWidth = newEl.offsetWidth;
133
+
134
+ newEl.style.left = 'unset';
135
+ newEl.style.right = 'unset';
136
+
137
+ if (leftAlignedWidth >= rightAlignedWidth) {
138
+ newEl.style.left = leftValue;
139
+ } else {
140
+ newEl.style.right = rightValue;
141
+ }
142
+
143
+ document.body.addEventListener('click', listener);
144
+ }
145
+ });
146
+ };
147
+
148
+ export const CSSMark = Mark.create({
149
+ name: 'cssmark',
150
+
151
+ addOptions() {
152
+ return {
153
+ classes: [],
154
+ };
155
+ },
156
+
157
+ addAttributes() {
158
+ return {
159
+ class: {
160
+ default: null,
161
+ parseHTML: (el) => el.getAttribute('class'),
162
+ renderHTML: (attributes) => {
163
+ if (!attributes.class) return {};
164
+ return { class: attributes.class };
165
+ },
166
+ },
167
+ };
168
+ },
169
+
170
+ parseHTML() {
171
+ // Any span with a class that matches one of allowed classes
172
+ return [
173
+ {
174
+ tag: 'span[class]',
175
+ getAttrs: (el) => {
176
+ const cls = el.getAttribute('class') || '';
177
+ const match = this.options.classes.find((name) => cls.includes(name));
178
+ return match ? { class: match } : false;
179
+ },
180
+ },
181
+ ];
182
+ },
183
+
184
+ renderHTML({ HTMLAttributes }) {
185
+ return ['span', mergeAttributes(HTMLAttributes), 0];
186
+ },
187
+
188
+ addCommands() {
189
+ return {
190
+ setCSSClass: (className) => ({ commands }) => {
191
+ return commands.setMark(this.name, { class: className });
192
+ },
193
+
194
+ unsetCSSClass: () => ({ commands }) => {
195
+ return commands.unsetMark(this.name);
196
+ },
197
+
198
+ openCSSClassDialog: () => ({ editor }) => {
199
+ insertDialog({
200
+ editor,
201
+ selectedText: editor.state.doc.textBetween(editor.state.selection.from, editor.state.selection.to),
202
+ parentNode: editor.state.selection.$from.nodeAfter,
203
+ opts: this.options.extraCSSRules,
204
+ callback: (className) => {
205
+ if (className) {
206
+ editor
207
+ .chain()
208
+ .focus()
209
+ .setCSSClass(className)
210
+ .run();
211
+ }
212
+ },
213
+ });
214
+ },
215
+ };
216
+ },
217
+ });
@@ -0,0 +1,100 @@
1
+ import React, { useCallback } from 'react';
2
+ import IconButton from "@material-ui/core/IconButton";
3
+ import Delete from "@material-ui/icons/Delete";
4
+ import { DoneButton } from "../plugins/toolbar/done-button";
5
+ import classNames from "classnames";
6
+ import { PIE_TOOLBAR__CLASS } from "../constants";
7
+ import { withStyles } from "@material-ui/core/styles";
8
+ import { Toolbar } from "../plugins/toolbar/toolbar";
9
+
10
+ function CustomToolbarWrapper(props) {
11
+ const {
12
+ children,
13
+ deletable,
14
+ classes,
15
+ toolbarOpts,
16
+ autoWidth,
17
+ isFocused,
18
+ doneButtonRef,
19
+ onDelete,
20
+ showDone,
21
+ onDone,
22
+ } = props;
23
+ const names = classNames(classes.toolbar, PIE_TOOLBAR__CLASS, {
24
+ [classes.toolbarWithNoDone]: !showDone,
25
+ [classes.toolbarRight]: toolbarOpts.alignment === 'right',
26
+ [classes.focused]: toolbarOpts.alwaysVisible || isFocused,
27
+ [classes.autoWidth]: autoWidth,
28
+ [classes.fullWidth]: !autoWidth,
29
+ [classes.hidden]: toolbarOpts.isHidden === true,
30
+ });
31
+ const customStyles = toolbarOpts.minWidth !== undefined ? { minWidth: toolbarOpts.minWidth } : {};
32
+
33
+ return (
34
+ <div className={names} style={{ ...customStyles }}>
35
+ {children}
36
+
37
+ <div className={classes.shared}>
38
+ {deletable && (
39
+ <IconButton
40
+ aria-label="Delete"
41
+ className={classes.iconRoot}
42
+ onMouseDown={(e) => onDelete?.(e)}
43
+ classes={{
44
+ root: classes.iconRoot,
45
+ }}
46
+ >
47
+ <Delete />
48
+ </IconButton>
49
+ )}
50
+ {showDone && <DoneButton doneButtonRef={doneButtonRef} onClick={onDone} />}
51
+ </div>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ const style = {
57
+ toolbar: {
58
+ position: 'absolute',
59
+ zIndex: 10,
60
+ cursor: 'pointer',
61
+ justifyContent: 'space-between',
62
+ background: 'var(--editable-html-toolbar-bg, #efefef)',
63
+ minWidth: '280px',
64
+ margin: '5px 0 0 0',
65
+ padding: '2px',
66
+ boxShadow:
67
+ '0px 1px 5px 0px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 3px 1px -2px rgba(0, 0, 0, 0.12)',
68
+ boxSizing: 'border-box',
69
+ display: 'flex',
70
+ opacity: 1,
71
+ },
72
+ toolbarWithNoDone: {
73
+ minWidth: '265px',
74
+ },
75
+ toolbarRight: {
76
+ right: 0,
77
+ },
78
+ fullWidth: {
79
+ width: '100%',
80
+ },
81
+ hidden: {
82
+ visibility: 'hidden',
83
+ },
84
+ autoWidth: {
85
+ width: 'auto',
86
+ },
87
+ iconRoot: {
88
+ width: '28px',
89
+ height: '28px',
90
+ padding: '4px',
91
+ verticalAlign: 'top',
92
+ },
93
+ label: {
94
+ color: 'var(--editable-html-toolbar-check, #00bb00)',
95
+ },
96
+ shared: {
97
+ display: 'flex',
98
+ },
99
+ };
100
+ export default withStyles(style, { index: 1000 })(CustomToolbarWrapper);
@@ -0,0 +1,55 @@
1
+ import { Node, mergeAttributes } from '@tiptap/core';
2
+ import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
3
+ import React, { useRef, useState, useCallback } from 'react';
4
+ import ImageComponent from './component';
5
+
6
+ // ---- Tiptap Extension ---- //
7
+
8
+ export const ImageUploadNode = Node.create({
9
+ name: 'imageUploadNode',
10
+
11
+ group: 'block',
12
+ atom: true, // ✅ prevents content holes
13
+ selectable: true, // optional
14
+ draggable: true, // optional
15
+
16
+ addAttributes() {
17
+ return {
18
+ loaded: { default: false },
19
+ deleteStatus: { default: null },
20
+ alignment: { default: null },
21
+ percent: { default: null },
22
+ width: { default: null },
23
+ height: { default: null },
24
+ src: { default: null },
25
+ alt: { default: null },
26
+ };
27
+ },
28
+
29
+ parseHTML() {
30
+ return [
31
+ {
32
+ tag: 'div[data-type="image-upload-node"]',
33
+ },
34
+ ];
35
+ },
36
+
37
+ // ✅ No `0` here!
38
+ renderHTML({ HTMLAttributes }) {
39
+ return ['img', mergeAttributes(HTMLAttributes, { 'data-type': 'image-upload-node' })];
40
+ },
41
+
42
+ addNodeView() {
43
+ return ReactNodeViewRenderer((props) => <ImageComponent {...{ ...props, options: this.options }} />);
44
+ },
45
+
46
+ addCommands() {
47
+ return {
48
+ setImageUploadNode: () => ({ commands }) => {
49
+ return commands.insertContent({
50
+ type: this.name,
51
+ });
52
+ },
53
+ };
54
+ },
55
+ });