@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/components/EditableHtml.js +9 -4
  3. package/lib/components/EditableHtml.js.map +1 -1
  4. package/lib/components/image/InsertImageHandler.js +6 -13
  5. package/lib/components/image/InsertImageHandler.js.map +1 -1
  6. package/lib/extensions/div-node.js +11 -3
  7. package/lib/extensions/div-node.js.map +1 -1
  8. package/lib/extensions/ensure-list-item-content-is-div.js +64 -0
  9. package/lib/extensions/ensure-list-item-content-is-div.js.map +1 -0
  10. package/lib/extensions/extended-list-item.js +15 -0
  11. package/lib/extensions/extended-list-item.js.map +1 -0
  12. package/lib/extensions/image-component.js +40 -36
  13. package/lib/extensions/image-component.js.map +1 -1
  14. package/lib/extensions/image.js +11 -2
  15. package/lib/extensions/image.js.map +1 -1
  16. package/package.json +2 -2
  17. package/src/__tests__/EditableHtml.test.jsx +8 -0
  18. package/src/__tests__/index.test.jsx +2 -0
  19. package/src/components/EditableHtml.jsx +9 -3
  20. package/src/components/__tests__/InsertImageHandler.test.js +23 -21
  21. package/src/components/image/InsertImageHandler.js +4 -13
  22. package/src/extensions/__tests__/ensure-list-item-content-is-div.test.js +44 -0
  23. package/src/extensions/__tests__/extended-list-item.test.js +13 -0
  24. package/src/extensions/__tests__/image-component.test.jsx +23 -3
  25. package/src/extensions/__tests__/image.test.js +15 -10
  26. package/src/extensions/div-node.js +12 -2
  27. package/src/extensions/ensure-list-item-content-is-div.js +62 -0
  28. package/src/extensions/extended-list-item.js +10 -0
  29. package/src/extensions/image-component.jsx +38 -39
  30. 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
- type: 'imageUploadNode',
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
- type: 'imageUploadNode',
225
- attrs: {
226
- src: 'data:image/png;base64,Zm9v',
227
- loaded: true,
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 applySizeData = useCallback(() => {
99
- if (!node.attrs.width || !imgRef.current) return;
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
- const update = {
102
- ...node.attrs,
103
- resizePercent: getPercentFromWidth(node.attrs.width),
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
- if (!isEqual(update, node.attrs)) {
107
- editor.commands.updateAttributes('imageUploadNode', update);
108
- }
109
- }, [editor, node.attrs, getPercentFromWidth]);
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
- latestNodeRef.current,
130
- (finish) => new InsertImageHandler(editor, latestNodeRef.current, finish),
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
- editor.commands.updateAttributes('imageUploadNode', update);
189
+ if (!isEqual(update, { width: node.attrs.width, height: node.attrs.height })) {
190
+ updateThisNode(update);
173
191
  }
174
192
  }
175
- }, [editor, node.attrs, maxImageWidth, maxImageHeight]);
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
- const update = { width: next.width, height: next.height };
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, node.attrs],
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
- editor.commands.updateAttributes('imageUploadNode', newValues);
229
+ updateThisNode(newValues);
231
230
  },
232
- [editor],
231
+ [editor, updateThisNode],
233
232
  );
234
233
 
235
234
  const stopResize = useCallback(() => {
@@ -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
  };