@pie-lib/editable-html-tip-tap 1.2.0-next.32 → 1.2.0-next.34
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 +18 -0
- package/lib/components/EditableHtml.js +9 -4
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/image/InsertImageHandler.js +6 -13
- package/lib/components/image/InsertImageHandler.js.map +1 -1
- package/lib/extensions/div-node.js +11 -3
- package/lib/extensions/div-node.js.map +1 -1
- package/lib/extensions/ensure-list-item-content-is-div.js +64 -0
- package/lib/extensions/ensure-list-item-content-is-div.js.map +1 -0
- package/lib/extensions/extended-list-item.js +15 -0
- package/lib/extensions/extended-list-item.js.map +1 -0
- package/lib/extensions/image-component.js +40 -36
- package/lib/extensions/image-component.js.map +1 -1
- package/lib/extensions/image.js +11 -2
- package/lib/extensions/image.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/EditableHtml.test.jsx +8 -0
- package/src/__tests__/index.test.jsx +2 -0
- package/src/components/EditableHtml.jsx +9 -3
- package/src/components/__tests__/InsertImageHandler.test.js +23 -21
- package/src/components/image/InsertImageHandler.js +4 -13
- package/src/extensions/__tests__/ensure-list-item-content-is-div.test.js +44 -0
- package/src/extensions/__tests__/extended-list-item.test.js +13 -0
- package/src/extensions/__tests__/image-component.test.jsx +23 -3
- package/src/extensions/__tests__/image.test.js +15 -10
- package/src/extensions/div-node.js +12 -2
- package/src/extensions/ensure-list-item-content-is-div.js +62 -0
- package/src/extensions/extended-list-item.js +10 -0
- package/src/extensions/image-component.jsx +38 -39
- package/src/extensions/image.js +5 -0
|
@@ -144,9 +144,12 @@ describe('ImageUploadNode', () => {
|
|
|
144
144
|
insertContent: jest.fn(() => true),
|
|
145
145
|
};
|
|
146
146
|
const result = commands.setImageUploadNode()({ commands: mockCommands });
|
|
147
|
-
expect(mockCommands.insertContent).toHaveBeenCalledWith(
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
expect(mockCommands.insertContent).toHaveBeenCalledWith(
|
|
148
|
+
expect.objectContaining({
|
|
149
|
+
type: 'imageUploadNode',
|
|
150
|
+
attrs: expect.objectContaining({ nodeKey: expect.any(String) }),
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
150
153
|
expect(result).toBe(true);
|
|
151
154
|
});
|
|
152
155
|
});
|
|
@@ -220,13 +223,15 @@ describe('ImageUploadNode', () => {
|
|
|
220
223
|
|
|
221
224
|
await new Promise((resolve) => queueMicrotask(resolve));
|
|
222
225
|
|
|
223
|
-
expect(insertContent).toHaveBeenCalledWith(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
expect(insertContent).toHaveBeenCalledWith(
|
|
227
|
+
expect.objectContaining({
|
|
228
|
+
type: 'imageUploadNode',
|
|
229
|
+
attrs: expect.objectContaining({
|
|
230
|
+
src: 'data:image/png;base64,Zm9v',
|
|
231
|
+
loaded: true,
|
|
232
|
+
}),
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
230
235
|
});
|
|
231
236
|
});
|
|
232
237
|
});
|
|
@@ -15,6 +15,15 @@ export const DivNode = Node.create({
|
|
|
15
15
|
},
|
|
16
16
|
|
|
17
17
|
addKeyboardShortcuts() {
|
|
18
|
+
const isInsideListItem = ($from) => {
|
|
19
|
+
for (let depth = $from.depth; depth >= 0; depth -= 1) {
|
|
20
|
+
if ($from.node(depth).type.name === 'listItem') {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
return {
|
|
19
28
|
Enter: () => {
|
|
20
29
|
const { state } = this.editor;
|
|
@@ -23,6 +32,9 @@ export const DivNode = Node.create({
|
|
|
23
32
|
if ($from.parent.type.name !== 'div') {
|
|
24
33
|
return false;
|
|
25
34
|
}
|
|
35
|
+
if (isInsideListItem($from)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
26
38
|
|
|
27
39
|
return this.editor
|
|
28
40
|
.chain()
|
|
@@ -60,8 +72,6 @@ export const DivNode = Node.create({
|
|
|
60
72
|
return state.doc.childCount === 1 ? true : false;
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
// one character left and cursor is after it — delete
|
|
64
|
-
// only that character and stop, preventing the block-join that fires Enter.
|
|
65
75
|
if (parentText.length === 1 && $from.parentOffset === 1) {
|
|
66
76
|
const { tr } = state;
|
|
67
77
|
tr.delete($from.pos - 1, $from.pos);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Some list operations preserve/create `paragraph` children in `listItem`.
|
|
6
|
+
* Normalize direct `listItem > paragraph` children into `div` so list content
|
|
7
|
+
* stays consistent with DivNode-based editing.
|
|
8
|
+
*/
|
|
9
|
+
export const EnsureListItemContentIsDiv = Extension.create({
|
|
10
|
+
name: 'ensureListItemContentIsDiv',
|
|
11
|
+
|
|
12
|
+
addProseMirrorPlugins() {
|
|
13
|
+
const key = new PluginKey(this.name);
|
|
14
|
+
|
|
15
|
+
return [
|
|
16
|
+
new Plugin({
|
|
17
|
+
key,
|
|
18
|
+
appendTransaction(transactions, _oldState, newState) {
|
|
19
|
+
if (!transactions.some((tr) => tr.docChanged)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { doc, schema } = newState;
|
|
24
|
+
const divType = schema.nodes.div;
|
|
25
|
+
if (!divType) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const positionsToConvert = [];
|
|
30
|
+
|
|
31
|
+
doc.descendants((node, pos) => {
|
|
32
|
+
if (node.type.name !== 'listItem') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let childOffset = 1;
|
|
37
|
+
node.forEach((child) => {
|
|
38
|
+
if (child.type.name === 'paragraph') {
|
|
39
|
+
positionsToConvert.push({
|
|
40
|
+
pos: pos + childOffset,
|
|
41
|
+
attrs: child.attrs,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
childOffset += child.nodeSize;
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (positionsToConvert.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tr = newState.tr;
|
|
53
|
+
positionsToConvert
|
|
54
|
+
.sort((a, b) => b.pos - a.pos)
|
|
55
|
+
.forEach(({ pos, attrs }) => tr.setNodeMarkup(pos, divType, attrs));
|
|
56
|
+
|
|
57
|
+
return tr;
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ListItem } from '@tiptap/extension-list-item';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default list items use `paragraph block*`, so empty/new items become `<p>`.
|
|
5
|
+
* Prefer `div` first to keep consistency with DivNode at root and table cells.
|
|
6
|
+
*/
|
|
7
|
+
export const ExtendedListItem = ListItem.extend({
|
|
8
|
+
content:
|
|
9
|
+
'(div | paragraph | heading | bulletList | orderedList | blockquote | codeBlock | horizontalRule | image | imageUploadNode)+',
|
|
10
|
+
});
|
|
@@ -95,18 +95,36 @@ function ImageComponent(props) {
|
|
|
95
95
|
return parseInt(floored.toFixed(0) * 25, 10);
|
|
96
96
|
}, []);
|
|
97
97
|
|
|
98
|
-
const
|
|
99
|
-
|
|
98
|
+
const findNodePos = useCallback(() => {
|
|
99
|
+
const key = latestNodeRef.current.attrs.nodeKey;
|
|
100
|
+
let found = null;
|
|
101
|
+
editor.state.doc.descendants((n, pos) => {
|
|
102
|
+
if (found !== null) return false;
|
|
103
|
+
if (n.type.name === 'imageUploadNode' && n.attrs.nodeKey === key) {
|
|
104
|
+
found = pos;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return found;
|
|
109
|
+
}, [editor]);
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
// dispatch an attribute update targeted precisely at this node by nodeKey.
|
|
112
|
+
const updateThisNode = useCallback((newAttrs) => {
|
|
113
|
+
const nodePos = findNodePos();
|
|
114
|
+
if (nodePos === null) return;
|
|
115
|
+
const currentNode = editor.state.doc.nodeAt(nodePos);
|
|
116
|
+
if (!currentNode) return;
|
|
117
|
+
editor.view.dispatch(
|
|
118
|
+
editor.state.tr.setNodeMarkup(nodePos, undefined, { ...currentNode.attrs, ...newAttrs }),
|
|
119
|
+
);
|
|
120
|
+
}, [editor, findNodePos]);
|
|
105
121
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
const applySizeData = useCallback(() => {
|
|
123
|
+
if (!node.attrs.width || !imgRef.current) return;
|
|
124
|
+
const resizePercent = getPercentFromWidth(node.attrs.width);
|
|
125
|
+
if (node.attrs.resizePercent === resizePercent) return;
|
|
126
|
+
updateThisNode({ resizePercent });
|
|
127
|
+
}, [node.attrs.width, node.attrs.resizePercent, getPercentFromWidth, updateThisNode]);
|
|
110
128
|
|
|
111
129
|
// keep ref in sync with latest node
|
|
112
130
|
useEffect(() => {
|
|
@@ -126,8 +144,8 @@ function ImageComponent(props) {
|
|
|
126
144
|
if (!hasImageSrc && options.imageHandling?.insertImageRequested) {
|
|
127
145
|
options.imageHandling.insertImageRequested(
|
|
128
146
|
editor,
|
|
129
|
-
|
|
130
|
-
(finish) => new InsertImageHandler(editor,
|
|
147
|
+
[node, pos],
|
|
148
|
+
(finish) => new InsertImageHandler(editor, [node, pos], finish),
|
|
131
149
|
);
|
|
132
150
|
}
|
|
133
151
|
|
|
@@ -136,7 +154,7 @@ function ImageComponent(props) {
|
|
|
136
154
|
} else {
|
|
137
155
|
setShowToolbar(selected);
|
|
138
156
|
}
|
|
139
|
-
}, [editor, node, selected]);
|
|
157
|
+
}, [editor, node, pos, selected]);
|
|
140
158
|
|
|
141
159
|
useEffect(() => {
|
|
142
160
|
applySizeData();
|
|
@@ -168,11 +186,11 @@ function ImageComponent(props) {
|
|
|
168
186
|
box.style.height = `${h}px`;
|
|
169
187
|
|
|
170
188
|
const update = { width: w, height: h };
|
|
171
|
-
if (!isEqual(update, node.attrs)) {
|
|
172
|
-
|
|
189
|
+
if (!isEqual(update, { width: node.attrs.width, height: node.attrs.height })) {
|
|
190
|
+
updateThisNode(update);
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
|
-
}, [
|
|
193
|
+
}, [node.attrs.width, node.attrs.height, maxImageWidth, maxImageHeight, updateThisNode]);
|
|
176
194
|
|
|
177
195
|
const updateAspect = (initial, next, keepAspect = true, resizeType) => {
|
|
178
196
|
if (keepAspect) {
|
|
@@ -200,36 +218,17 @@ function ImageComponent(props) {
|
|
|
200
218
|
box.style.width = `${next.width}px`;
|
|
201
219
|
box.style.height = `${next.height}px`;
|
|
202
220
|
|
|
203
|
-
|
|
204
|
-
if (!isEqual(update, node.attrs)) {
|
|
205
|
-
editor.commands.updateAttributes('imageUploadNode', update);
|
|
206
|
-
}
|
|
221
|
+
updateThisNode({ width: next.width, height: next.height });
|
|
207
222
|
}
|
|
208
223
|
},
|
|
209
|
-
[editor,
|
|
224
|
+
[editor, updateThisNode],
|
|
210
225
|
);
|
|
211
226
|
|
|
212
|
-
// Helper to find this node's current position in the doc.
|
|
213
|
-
// We cannot use object identity (n === node) because ProseMirror replaces
|
|
214
|
-
// node objects after every transaction — match by src instead.
|
|
215
|
-
const findNodePos = useCallback(() => {
|
|
216
|
-
let found = null;
|
|
217
|
-
const src = latestNodeRef.current.attrs.src;
|
|
218
|
-
editor.state.doc.descendants((n, pos) => {
|
|
219
|
-
if (found !== null) return false;
|
|
220
|
-
if (n.type.name === 'imageUploadNode' && n.attrs.src === src) {
|
|
221
|
-
found = pos;
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
return found;
|
|
226
|
-
}, [editor]);
|
|
227
|
-
|
|
228
227
|
const onChange = useCallback(
|
|
229
228
|
(newValues) => {
|
|
230
|
-
|
|
229
|
+
updateThisNode(newValues);
|
|
231
230
|
},
|
|
232
|
-
[editor],
|
|
231
|
+
[editor, updateThisNode],
|
|
233
232
|
);
|
|
234
233
|
|
|
235
234
|
const stopResize = useCallback(() => {
|
package/src/extensions/image.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ReactNodeViewRenderer } from '@tiptap/react';
|
|
|
3
3
|
import { Plugin } from '@tiptap/pm/state';
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import ImageComponent from './image-component';
|
|
6
|
+
import { node } from 'prop-types';
|
|
6
7
|
|
|
7
8
|
export const ImageUploadNode = Node.create({
|
|
8
9
|
name: 'imageUploadNode',
|
|
@@ -14,6 +15,7 @@ export const ImageUploadNode = Node.create({
|
|
|
14
15
|
|
|
15
16
|
addAttributes() {
|
|
16
17
|
return {
|
|
18
|
+
nodeKey: { default: null },
|
|
17
19
|
loaded: { default: false },
|
|
18
20
|
deleteStatus: { default: null },
|
|
19
21
|
alignment: { default: null },
|
|
@@ -48,6 +50,8 @@ export const ImageUploadNode = Node.create({
|
|
|
48
50
|
({ commands }) => {
|
|
49
51
|
return commands.insertContent({
|
|
50
52
|
type: this.name,
|
|
53
|
+
// adding a unique nodeKey attribute to help identify this node instance later due to issues with multiple images
|
|
54
|
+
attrs: { nodeKey: `img-${Date.now()}-${Math.random().toString(36).slice(2)}` },
|
|
51
55
|
});
|
|
52
56
|
},
|
|
53
57
|
};
|
|
@@ -89,6 +93,7 @@ export const ImageUploadNode = Node.create({
|
|
|
89
93
|
attrs: {
|
|
90
94
|
src,
|
|
91
95
|
loaded: true,
|
|
96
|
+
nodeKey: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
92
97
|
},
|
|
93
98
|
});
|
|
94
99
|
};
|