@pie-lib/editable-html-tip-tap 1.2.0-next.9 → 2.0.1
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 +176 -0
- package/lib/components/CharacterPicker.js +1 -0
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/EditableHtml.js +84 -43
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +74 -43
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/TiptapContainer.js +9 -8
- package/lib/components/TiptapContainer.js.map +1 -1
- package/lib/components/icons/TextAlign.js +2 -2
- package/lib/components/icons/TextAlign.js.map +1 -1
- package/lib/components/image/InsertImageHandler.js +10 -13
- package/lib/components/image/InsertImageHandler.js.map +1 -1
- package/lib/components/media/MediaDialog.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js +6 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/choice.js +15 -7
- package/lib/components/respArea/DragInTheBlank/choice.js.map +1 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js +29 -11
- package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +35 -6
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/extensions/custom-toolbar-wrapper.js +3 -2
- package/lib/extensions/custom-toolbar-wrapper.js.map +1 -1
- package/lib/extensions/div-node.js +83 -0
- package/lib/extensions/div-node.js.map +1 -0
- package/lib/extensions/ensure-empty-root-div.js +48 -0
- package/lib/extensions/ensure-empty-root-div.js.map +1 -0
- 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/extended-table-cell.js +22 -0
- package/lib/extensions/extended-table-cell.js.map +1 -0
- package/lib/extensions/extended-table.js +50 -1
- package/lib/extensions/extended-table.js.map +1 -1
- package/lib/extensions/image-component.js +102 -51
- package/lib/extensions/image-component.js.map +1 -1
- package/lib/extensions/image.js +51 -2
- package/lib/extensions/image.js.map +1 -1
- package/lib/extensions/math.js +50 -9
- package/lib/extensions/math.js.map +1 -1
- package/lib/extensions/media.js +3 -1
- package/lib/extensions/media.js.map +1 -1
- package/lib/extensions/responseArea.js +12 -7
- package/lib/extensions/responseArea.js.map +1 -1
- package/lib/styles/editorContainerStyles.js +5 -4
- package/lib/styles/editorContainerStyles.js.map +1 -1
- package/lib/utils/helper.js +17 -0
- package/lib/utils/helper.js.map +1 -0
- package/package.json +8 -8
- package/src/__tests__/EditableHtml.test.jsx +90 -7
- package/src/__tests__/index.test.jsx +11 -3
- package/src/components/CharacterPicker.jsx +1 -0
- package/src/components/EditableHtml.jsx +91 -41
- package/src/components/MenuBar.jsx +57 -24
- package/src/components/TiptapContainer.jsx +10 -8
- package/src/components/__tests__/CharacterPicker.test.jsx +22 -0
- package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +55 -12
- package/src/components/__tests__/InlineDropdown.test.jsx +203 -10
- package/src/components/__tests__/InsertImageHandler.test.js +28 -21
- package/src/components/__tests__/MenuBar.test.jsx +32 -0
- package/src/components/icons/TextAlign.jsx +1 -1
- package/src/components/image/InsertImageHandler.js +9 -13
- package/src/components/respArea/DragInTheBlank/DragInTheBlank.jsx +6 -1
- package/src/components/respArea/DragInTheBlank/choice.jsx +32 -4
- package/src/components/respArea/ExplicitConstructedResponse.jsx +33 -10
- package/src/components/respArea/InlineDropdown.jsx +45 -10
- package/src/extensions/__tests__/divNode.test.js +87 -0
- package/src/extensions/__tests__/ensure-empty-root-div.test.js +57 -0
- 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__/extended-table-cell.test.js +22 -0
- package/src/extensions/__tests__/extended-table.test.js +98 -1
- package/src/extensions/__tests__/image-component.test.jsx +105 -9
- package/src/extensions/__tests__/image.test.js +109 -8
- package/src/extensions/__tests__/math.test.js +348 -0
- package/src/extensions/__tests__/media-node-view.test.jsx +10 -8
- package/src/extensions/__tests__/responseArea.test.js +291 -0
- package/src/extensions/custom-toolbar-wrapper.jsx +2 -2
- package/src/extensions/div-node.js +86 -0
- package/src/extensions/ensure-empty-root-div.js +47 -0
- package/src/extensions/ensure-list-item-content-is-div.js +62 -0
- package/src/extensions/extended-list-item.js +10 -0
- package/src/extensions/extended-table-cell.js +19 -0
- package/src/extensions/extended-table.js +37 -1
- package/src/extensions/image-component.jsx +114 -69
- package/src/extensions/image.js +56 -1
- package/src/extensions/math.js +62 -10
- package/src/extensions/media.js +1 -1
- package/src/extensions/responseArea.js +13 -11
- package/src/styles/editorContainerStyles.js +5 -4
- package/src/utils/helper.js +17 -0
- /package/src/components/media/{MediaDialog.js → MediaDialog.jsx} +0 -0
|
@@ -100,7 +100,7 @@ describe('MediaNodeView Component', () => {
|
|
|
100
100
|
updateAttributes={mockUpdateAttributes}
|
|
101
101
|
deleteNode={mockDeleteNode}
|
|
102
102
|
options={{}}
|
|
103
|
-
|
|
103
|
+
/>,
|
|
104
104
|
);
|
|
105
105
|
|
|
106
106
|
// Verify the component rendered successfully
|
|
@@ -130,7 +130,7 @@ describe('MediaNodeView Component', () => {
|
|
|
130
130
|
updateAttributes={mockUpdateAttributes}
|
|
131
131
|
deleteNode={mockDeleteNode}
|
|
132
132
|
options={{}}
|
|
133
|
-
|
|
133
|
+
/>,
|
|
134
134
|
);
|
|
135
135
|
|
|
136
136
|
// Verify the component rendered successfully
|
|
@@ -159,7 +159,7 @@ describe('MediaNodeView Component', () => {
|
|
|
159
159
|
updateAttributes={mockUpdateAttributes}
|
|
160
160
|
deleteNode={mockDeleteNode}
|
|
161
161
|
options={{}}
|
|
162
|
-
|
|
162
|
+
/>,
|
|
163
163
|
);
|
|
164
164
|
|
|
165
165
|
// Empty string is falsy, so dialog WOULD open with current implementation
|
|
@@ -185,7 +185,7 @@ describe('MediaNodeView Component', () => {
|
|
|
185
185
|
updateAttributes={mockUpdateAttributes}
|
|
186
186
|
deleteNode={mockDeleteNode}
|
|
187
187
|
options={{}}
|
|
188
|
-
|
|
188
|
+
/>,
|
|
189
189
|
);
|
|
190
190
|
|
|
191
191
|
expect(getByTestId('edit-button')).toBeTruthy();
|
|
@@ -210,7 +210,7 @@ describe('MediaNodeView Component', () => {
|
|
|
210
210
|
updateAttributes={mockUpdateAttributes}
|
|
211
211
|
deleteNode={mockDeleteNode}
|
|
212
212
|
options={{}}
|
|
213
|
-
|
|
213
|
+
/>,
|
|
214
214
|
);
|
|
215
215
|
|
|
216
216
|
const audio = container.querySelector('audio');
|
|
@@ -237,12 +237,14 @@ describe('MediaNodeView Component', () => {
|
|
|
237
237
|
updateAttributes={mockUpdateAttributes}
|
|
238
238
|
deleteNode={mockDeleteNode}
|
|
239
239
|
options={{}}
|
|
240
|
-
|
|
240
|
+
/>,
|
|
241
241
|
);
|
|
242
242
|
|
|
243
243
|
const iframe = container.querySelector('iframe');
|
|
244
244
|
expect(iframe).toBeTruthy();
|
|
245
245
|
expect(iframe.getAttribute('src')).toBe('https://www.youtube.com/embed/dQw4w9WgXcQ');
|
|
246
|
+
expect(iframe.getAttribute('width')).toBe('640');
|
|
247
|
+
expect(iframe.getAttribute('height')).toBe('480');
|
|
246
248
|
});
|
|
247
249
|
|
|
248
250
|
it('should render MediaToolbar', () => {
|
|
@@ -261,7 +263,7 @@ describe('MediaNodeView Component', () => {
|
|
|
261
263
|
updateAttributes={mockUpdateAttributes}
|
|
262
264
|
deleteNode={mockDeleteNode}
|
|
263
265
|
options={{}}
|
|
264
|
-
|
|
266
|
+
/>,
|
|
265
267
|
);
|
|
266
268
|
|
|
267
269
|
expect(getByTestId('media-toolbar')).toBeTruthy();
|
|
@@ -285,7 +287,7 @@ describe('MediaNodeView Component', () => {
|
|
|
285
287
|
updateAttributes={mockUpdateAttributes}
|
|
286
288
|
deleteNode={mockDeleteNode}
|
|
287
289
|
options={{}}
|
|
288
|
-
|
|
290
|
+
/>,
|
|
289
291
|
);
|
|
290
292
|
|
|
291
293
|
fireEvent.click(getByTestId('remove-button'));
|
|
@@ -101,6 +101,297 @@ describe('ResponseAreaExtension', () => {
|
|
|
101
101
|
expect(commands).toHaveProperty('insertResponseArea');
|
|
102
102
|
expect(typeof commands.insertResponseArea).toBe('function');
|
|
103
103
|
});
|
|
104
|
+
|
|
105
|
+
it('returns refreshResponseArea command', () => {
|
|
106
|
+
const commands = ResponseAreaExtension.addCommands();
|
|
107
|
+
|
|
108
|
+
expect(commands).toHaveProperty('refreshResponseArea');
|
|
109
|
+
expect(typeof commands.refreshResponseArea).toBe('function');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('refreshResponseArea handles node with attrs safely', () => {
|
|
113
|
+
const context = {
|
|
114
|
+
options: {
|
|
115
|
+
type: 'explicit-constructed-response',
|
|
116
|
+
maxResponseAreas: 5,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const commands = ResponseAreaExtension.addCommands.call(context);
|
|
121
|
+
const refreshCommand = commands.refreshResponseArea();
|
|
122
|
+
|
|
123
|
+
// Mock transaction and state
|
|
124
|
+
const mockNode = {
|
|
125
|
+
attrs: {
|
|
126
|
+
index: '0',
|
|
127
|
+
value: 'test',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const mockTr = {
|
|
132
|
+
setNodeMarkup: jest.fn(),
|
|
133
|
+
setSelection: jest.fn(),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const mockState = {
|
|
137
|
+
selection: {
|
|
138
|
+
from: 0,
|
|
139
|
+
$from: {
|
|
140
|
+
nodeAfter: mockNode,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
tr: mockTr,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const mockCommands = {
|
|
147
|
+
focus: jest.fn(),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const mockDispatch = jest.fn();
|
|
151
|
+
|
|
152
|
+
refreshCommand({
|
|
153
|
+
tr: mockTr,
|
|
154
|
+
state: mockState,
|
|
155
|
+
commands: mockCommands,
|
|
156
|
+
dispatch: mockDispatch,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(mockTr.setNodeMarkup).toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('refreshResponseArea handles node without attrs safely (optional chaining)', () => {
|
|
163
|
+
const context = {
|
|
164
|
+
options: {
|
|
165
|
+
type: 'explicit-constructed-response',
|
|
166
|
+
maxResponseAreas: 5,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const commands = ResponseAreaExtension.addCommands.call(context);
|
|
171
|
+
const refreshCommand = commands.refreshResponseArea();
|
|
172
|
+
|
|
173
|
+
// Mock transaction and state with node that has no attrs
|
|
174
|
+
const mockNode = null;
|
|
175
|
+
|
|
176
|
+
const mockTr = {
|
|
177
|
+
setNodeMarkup: jest.fn(),
|
|
178
|
+
setSelection: jest.fn(),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const mockState = {
|
|
182
|
+
selection: {
|
|
183
|
+
from: 0,
|
|
184
|
+
$from: {
|
|
185
|
+
nodeAfter: mockNode,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
tr: mockTr,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const mockCommands = {
|
|
192
|
+
focus: jest.fn(),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const mockDispatch = jest.fn();
|
|
196
|
+
|
|
197
|
+
// This should not throw an error due to optional chaining on node?.attrs
|
|
198
|
+
expect(() => {
|
|
199
|
+
refreshCommand({
|
|
200
|
+
tr: mockTr,
|
|
201
|
+
state: mockState,
|
|
202
|
+
commands: mockCommands,
|
|
203
|
+
dispatch: mockDispatch,
|
|
204
|
+
});
|
|
205
|
+
}).not.toThrow();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('refreshResponseArea updates timestamp in node attributes', () => {
|
|
209
|
+
const context = {
|
|
210
|
+
options: {
|
|
211
|
+
type: 'explicit-constructed-response',
|
|
212
|
+
maxResponseAreas: 5,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const commands = ResponseAreaExtension.addCommands.call(context);
|
|
217
|
+
const refreshCommand = commands.refreshResponseArea();
|
|
218
|
+
|
|
219
|
+
const mockNode = {
|
|
220
|
+
attrs: {
|
|
221
|
+
index: '0',
|
|
222
|
+
value: 'test',
|
|
223
|
+
updated: '1234567890',
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const mockTr = {
|
|
228
|
+
setNodeMarkup: jest.fn((pos, type, attrs) => {
|
|
229
|
+
// Verify that updated timestamp is being set
|
|
230
|
+
expect(attrs.updated).toBeDefined();
|
|
231
|
+
expect(attrs.updated).not.toBe('1234567890');
|
|
232
|
+
}),
|
|
233
|
+
setSelection: jest.fn(),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const mockState = {
|
|
237
|
+
selection: {
|
|
238
|
+
from: 0,
|
|
239
|
+
$from: {
|
|
240
|
+
nodeAfter: mockNode,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
tr: mockTr,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const mockCommands = {
|
|
247
|
+
focus: jest.fn(),
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const mockDispatch = jest.fn();
|
|
251
|
+
|
|
252
|
+
refreshCommand({
|
|
253
|
+
tr: mockTr,
|
|
254
|
+
state: mockState,
|
|
255
|
+
commands: mockCommands,
|
|
256
|
+
dispatch: mockDispatch,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(mockTr.setNodeMarkup).toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('insertResponseArea', () => {
|
|
263
|
+
beforeEach(() => {
|
|
264
|
+
jest.resetModules();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const buildInsertCommand = () => {
|
|
268
|
+
const { ResponseAreaExtension } = require('../responseArea');
|
|
269
|
+
const context = {
|
|
270
|
+
options: {
|
|
271
|
+
type: 'inline-dropdown',
|
|
272
|
+
maxResponseAreas: 5,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const commands = ResponseAreaExtension.addCommands.call(context);
|
|
276
|
+
return commands.insertResponseArea('inline-dropdown');
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const createDoc = (existingCount, typeName = 'inline_dropdown') => ({
|
|
280
|
+
descendants: jest.fn((callback) => {
|
|
281
|
+
for (let i = 0; i < existingCount; i += 1) {
|
|
282
|
+
callback({ type: { name: typeName } }, i);
|
|
283
|
+
}
|
|
284
|
+
}),
|
|
285
|
+
content: { size: 50 },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('assigns index 1 and id 1 on the first insert', () => {
|
|
289
|
+
const insert = buildInsertCommand();
|
|
290
|
+
const mockInlineNode = { nodeSize: 1 };
|
|
291
|
+
const create = jest.fn(() => mockInlineNode);
|
|
292
|
+
const mockDoc = createDoc(0);
|
|
293
|
+
const mockTr = {
|
|
294
|
+
insert: jest.fn(),
|
|
295
|
+
doc: mockDoc,
|
|
296
|
+
setSelection: jest.fn(),
|
|
297
|
+
};
|
|
298
|
+
const state = {
|
|
299
|
+
schema: {
|
|
300
|
+
nodes: {
|
|
301
|
+
inline_dropdown: { create },
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
doc: mockDoc,
|
|
305
|
+
selection: { from: 5 },
|
|
306
|
+
};
|
|
307
|
+
const mockDispatch = jest.fn();
|
|
308
|
+
const mockCommands = { focus: jest.fn() };
|
|
309
|
+
|
|
310
|
+
const result = insert({
|
|
311
|
+
tr: mockTr,
|
|
312
|
+
state,
|
|
313
|
+
dispatch: mockDispatch,
|
|
314
|
+
commands: mockCommands,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(result).toBe(true);
|
|
318
|
+
expect(create).toHaveBeenCalledWith({
|
|
319
|
+
index: '1',
|
|
320
|
+
id: '1',
|
|
321
|
+
value: '',
|
|
322
|
+
});
|
|
323
|
+
expect(mockTr.insert).toHaveBeenCalledWith(5, mockInlineNode);
|
|
324
|
+
expect(mockDispatch).toHaveBeenCalled();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('assigns consecutive indices on repeated inserts', () => {
|
|
328
|
+
const insert = buildInsertCommand();
|
|
329
|
+
const mockInlineNode = { nodeSize: 1 };
|
|
330
|
+
const create = jest.fn(() => mockInlineNode);
|
|
331
|
+
const mockDoc = createDoc(0);
|
|
332
|
+
|
|
333
|
+
const runOnce = () => {
|
|
334
|
+
const mockTr = {
|
|
335
|
+
insert: jest.fn(),
|
|
336
|
+
doc: mockDoc,
|
|
337
|
+
setSelection: jest.fn(),
|
|
338
|
+
};
|
|
339
|
+
const state = {
|
|
340
|
+
schema: {
|
|
341
|
+
nodes: {
|
|
342
|
+
inline_dropdown: { create },
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
doc: mockDoc,
|
|
346
|
+
selection: { from: 1 },
|
|
347
|
+
};
|
|
348
|
+
insert({
|
|
349
|
+
tr: mockTr,
|
|
350
|
+
state,
|
|
351
|
+
dispatch: jest.fn(),
|
|
352
|
+
commands: { focus: jest.fn() },
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
runOnce();
|
|
357
|
+
runOnce();
|
|
358
|
+
|
|
359
|
+
expect(create.mock.calls[0][0]).toEqual({ index: '1', id: '1', value: '' });
|
|
360
|
+
expect(create.mock.calls[1][0]).toEqual({ index: '2', id: '2', value: '' });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('returns false when maxResponseAreas is reached', () => {
|
|
364
|
+
const insert = buildInsertCommand();
|
|
365
|
+
const mockInlineNode = { nodeSize: 1 };
|
|
366
|
+
const create = jest.fn(() => mockInlineNode);
|
|
367
|
+
const mockDoc = createDoc(5);
|
|
368
|
+
const mockTr = {
|
|
369
|
+
insert: jest.fn(),
|
|
370
|
+
doc: mockDoc,
|
|
371
|
+
setSelection: jest.fn(),
|
|
372
|
+
};
|
|
373
|
+
const state = {
|
|
374
|
+
schema: {
|
|
375
|
+
nodes: {
|
|
376
|
+
inline_dropdown: { create },
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
doc: mockDoc,
|
|
380
|
+
selection: { from: 5 },
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const result = insert({
|
|
384
|
+
tr: mockTr,
|
|
385
|
+
state,
|
|
386
|
+
dispatch: jest.fn(),
|
|
387
|
+
commands: { focus: jest.fn() },
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(result).toBe(false);
|
|
391
|
+
expect(create).not.toHaveBeenCalled();
|
|
392
|
+
expect(mockTr.insert).not.toHaveBeenCalled();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
104
395
|
});
|
|
105
396
|
});
|
|
106
397
|
|
|
@@ -48,7 +48,7 @@ const SharedContainer = styled('div')({
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
function CustomToolbarWrapper(props) {
|
|
51
|
-
const { children, deletable, toolbarOpts, autoWidth, isFocused, doneButtonRef, onDelete, showDone, onDone } = props;
|
|
51
|
+
const { children, deletable, toolbarOpts, autoWidth, isFocused, doneButtonRef, onDelete, showDone, onDone, style } = props;
|
|
52
52
|
const customStyles = toolbarOpts.minWidth !== undefined ? { minWidth: toolbarOpts.minWidth } : {};
|
|
53
53
|
|
|
54
54
|
return (
|
|
@@ -59,7 +59,7 @@ function CustomToolbarWrapper(props) {
|
|
|
59
59
|
isFocused={toolbarOpts.alwaysVisible || isFocused}
|
|
60
60
|
autoWidth={autoWidth}
|
|
61
61
|
isHidden={toolbarOpts.isHidden === true}
|
|
62
|
-
style={{ ...customStyles }}
|
|
62
|
+
style={{ ...customStyles, ...style }}
|
|
63
63
|
>
|
|
64
64
|
{children}
|
|
65
65
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// DivNode.ts
|
|
2
|
+
import { Node } from '@tiptap/core';
|
|
3
|
+
|
|
4
|
+
export const DivNode = Node.create({
|
|
5
|
+
name: 'div',
|
|
6
|
+
group: 'block',
|
|
7
|
+
content: 'inline*',
|
|
8
|
+
|
|
9
|
+
parseHTML() {
|
|
10
|
+
return [{ tag: 'div' }];
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
renderHTML({ HTMLAttributes }) {
|
|
14
|
+
return ['div', HTMLAttributes, 0];
|
|
15
|
+
},
|
|
16
|
+
|
|
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
|
+
|
|
27
|
+
return {
|
|
28
|
+
Enter: () => {
|
|
29
|
+
const { state } = this.editor;
|
|
30
|
+
const { $from } = state.selection;
|
|
31
|
+
|
|
32
|
+
if ($from.parent.type.name !== 'div') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (isInsideListItem($from)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return this.editor
|
|
40
|
+
.chain()
|
|
41
|
+
.focus()
|
|
42
|
+
.setNode('paragraph') // current div becomes <p>
|
|
43
|
+
.splitBlock() // create another <p>
|
|
44
|
+
.run();
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// When the cursor is in a div and the user presses Backspace,
|
|
48
|
+
// ProseMirror's default handler may try to join/delete the block node
|
|
49
|
+
// once it becomes empty. That triggers the Enter shortcut above
|
|
50
|
+
// (div → p conversion + split), making it look like a new line is
|
|
51
|
+
// inserted instead of deleting.
|
|
52
|
+
// We handle two cases explicitly:
|
|
53
|
+
// 1. The div already IS empty → swallow the event (nothing to delete).
|
|
54
|
+
// 2. The div has exactly ONE character left → delete just that character
|
|
55
|
+
// using a precise transaction, then stop. This prevents ProseMirror
|
|
56
|
+
// from following up with a block-join that triggers the Enter handler.
|
|
57
|
+
Backspace: () => {
|
|
58
|
+
const { state } = this.editor;
|
|
59
|
+
const { $from, empty: selectionEmpty } = state.selection;
|
|
60
|
+
|
|
61
|
+
if ($from.parent.type.name !== 'div') {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!selectionEmpty) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parentText = $from.parent.textContent;
|
|
70
|
+
|
|
71
|
+
if (parentText.length === 0) {
|
|
72
|
+
return state.doc.childCount === 1 ? true : false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (parentText.length === 1 && $from.parentOffset === 1) {
|
|
76
|
+
const { tr } = state;
|
|
77
|
+
tr.delete($from.pos - 1, $from.pos);
|
|
78
|
+
this.editor.view.dispatch(tr);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return false;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* After ProseMirror repairs an empty document, it often inserts a `paragraph`.
|
|
6
|
+
* We want a lone empty top-level block to be `div` (DivNode) so typing stays `<div>A</div>`.
|
|
7
|
+
*/
|
|
8
|
+
export const EnsureEmptyRootIsDiv = Extension.create({
|
|
9
|
+
name: 'ensureEmptyRootIsDiv',
|
|
10
|
+
|
|
11
|
+
addProseMirrorPlugins() {
|
|
12
|
+
const key = new PluginKey(this.name);
|
|
13
|
+
|
|
14
|
+
return [
|
|
15
|
+
new Plugin({
|
|
16
|
+
key,
|
|
17
|
+
appendTransaction(transactions, _oldState, newState) {
|
|
18
|
+
if (!transactions.some((tr) => tr.docChanged)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { doc } = newState;
|
|
23
|
+
if (doc.childCount !== 1) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const first = doc.firstChild;
|
|
28
|
+
if (first.type.name !== 'paragraph' || first.content.size > 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const divType = newState.schema.nodes.div;
|
|
33
|
+
if (!divType) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Top-level content positions are 0 .. doc.content.size. The first block spans
|
|
38
|
+
// [0, first.nodeSize). Using start=1 replaces the wrong slice and can leave the
|
|
39
|
+
// document inconsistent after a full delete (Cmd+A, Backspace).
|
|
40
|
+
const start = 0;
|
|
41
|
+
const end = first.nodeSize;
|
|
42
|
+
return newState.tr.replaceWith(start, end, divType.create());
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TableCell } from '@tiptap/extension-table-cell';
|
|
2
|
+
import { TableHeader } from '@tiptap/extension-table-header';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default table cells use ProseMirror `createAndFill()`, which prefers the first
|
|
6
|
+
* block type allowed by the content expression. Stock cells use `block+`, so
|
|
7
|
+
* `paragraph` wins. Listing `div` first matches the editor default for plain text
|
|
8
|
+
* (see DivNode) while still allowing other blocks (lists, headings, images, …).
|
|
9
|
+
*/
|
|
10
|
+
const TABLE_CELL_BLOCK_CONTENT =
|
|
11
|
+
'(div | paragraph | heading | bulletList | orderedList | blockquote | codeBlock | horizontalRule | image | imageUploadNode)+';
|
|
12
|
+
|
|
13
|
+
export const ExtendedTableCell = TableCell.extend({
|
|
14
|
+
content: TABLE_CELL_BLOCK_CONTENT,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const ExtendedTableHeader = TableHeader.extend({
|
|
18
|
+
content: TABLE_CELL_BLOCK_CONTENT,
|
|
19
|
+
});
|
|
@@ -1,6 +1,41 @@
|
|
|
1
|
-
import { Table } from '@tiptap/extension-table';
|
|
1
|
+
import { Table, TableView } from '@tiptap/extension-table';
|
|
2
|
+
|
|
3
|
+
const applyPresentationToTableElement = (table, attrs) => {
|
|
4
|
+
const border = attrs?.border != null && attrs.border !== '' ? attrs.border : '1';
|
|
5
|
+
|
|
6
|
+
table.setAttribute('border', border);
|
|
7
|
+
table.style.setProperty('color', 'var(--pie-text, black)');
|
|
8
|
+
table.style.setProperty('background-color', 'var(--pie-background, rgba(255, 255, 255))');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class ExtendedTableView extends TableView {
|
|
12
|
+
constructor(node, cellMinWidth, view) {
|
|
13
|
+
super(node, cellMinWidth, view);
|
|
14
|
+
applyPresentationToTableElement(this.table, node.attrs);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
update(node) {
|
|
18
|
+
const ok = super.update(node);
|
|
19
|
+
|
|
20
|
+
if (ok) {
|
|
21
|
+
applyPresentationToTableElement(this.table, node.attrs);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return ok;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
2
27
|
|
|
3
28
|
const ExtendedTable = Table.extend({
|
|
29
|
+
addOptions() {
|
|
30
|
+
return {
|
|
31
|
+
...this.parent?.(),
|
|
32
|
+
// ProseMirror's default table DOM does not prune extra <col> nodes when a column
|
|
33
|
+
// is removed; TableView (via column resizing) does.
|
|
34
|
+
resizable: true,
|
|
35
|
+
handleWidth: 0,
|
|
36
|
+
View: ExtendedTableView,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
4
39
|
addAttributes() {
|
|
5
40
|
return {
|
|
6
41
|
border: { default: '1' },
|
|
@@ -21,4 +56,5 @@ const ExtendedTable = Table.extend({
|
|
|
21
56
|
},
|
|
22
57
|
});
|
|
23
58
|
|
|
59
|
+
export { applyPresentationToTableElement, ExtendedTableView };
|
|
24
60
|
export default ExtendedTable;
|