@pie-lib/editable-html-tip-tap 1.2.0-next.21 → 1.2.0-next.23
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 +15 -0
- package/lib/components/EditableHtml.js +11 -4
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/TiptapContainer.js +5 -0
- package/lib/components/TiptapContainer.js.map +1 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js +25 -8
- package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +1 -2
- 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 +36 -0
- package/lib/extensions/div-node.js.map +1 -1
- package/lib/extensions/math.js +35 -2
- package/lib/extensions/math.js.map +1 -1
- package/lib/extensions/responseArea.js +10 -4
- package/lib/extensions/responseArea.js.map +1 -1
- package/package.json +6 -6
- package/src/components/EditableHtml.jsx +9 -3
- package/src/components/TiptapContainer.jsx +6 -0
- package/src/components/__tests__/InlineDropdown.test.jsx +8 -0
- package/src/components/respArea/ExplicitConstructedResponse.jsx +23 -7
- package/src/components/respArea/InlineDropdown.jsx +1 -2
- package/src/extensions/__tests__/math.test.js +21 -0
- package/src/extensions/__tests__/responseArea.test.js +134 -0
- package/src/extensions/custom-toolbar-wrapper.jsx +2 -2
- package/src/extensions/div-node.js +40 -0
- package/src/extensions/math.js +46 -3
- package/src/extensions/responseArea.js +11 -4
|
@@ -258,6 +258,140 @@ describe('ResponseAreaExtension', () => {
|
|
|
258
258
|
|
|
259
259
|
expect(mockTr.setNodeMarkup).toHaveBeenCalled();
|
|
260
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
|
+
});
|
|
261
395
|
});
|
|
262
396
|
});
|
|
263
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
|
|
|
@@ -31,6 +31,46 @@ export const DivNode = Node.create({
|
|
|
31
31
|
.splitBlock() // create another <p>
|
|
32
32
|
.run();
|
|
33
33
|
},
|
|
34
|
+
|
|
35
|
+
// When the cursor is in a div and the user presses Backspace,
|
|
36
|
+
// ProseMirror's default handler may try to join/delete the block node
|
|
37
|
+
// once it becomes empty. That triggers the Enter shortcut above
|
|
38
|
+
// (div → p conversion + split), making it look like a new line is
|
|
39
|
+
// inserted instead of deleting.
|
|
40
|
+
// We handle two cases explicitly:
|
|
41
|
+
// 1. The div already IS empty → swallow the event (nothing to delete).
|
|
42
|
+
// 2. The div has exactly ONE character left → delete just that character
|
|
43
|
+
// using a precise transaction, then stop. This prevents ProseMirror
|
|
44
|
+
// from following up with a block-join that triggers the Enter handler.
|
|
45
|
+
Backspace: () => {
|
|
46
|
+
const { state } = this.editor;
|
|
47
|
+
const { $from, empty: selectionEmpty } = state.selection;
|
|
48
|
+
|
|
49
|
+
if ($from.parent.type.name !== 'div') {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!selectionEmpty) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parentText = $from.parent.textContent;
|
|
58
|
+
|
|
59
|
+
if (parentText.length === 0) {
|
|
60
|
+
return state.doc.childCount === 1 ? true : false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// one character left and cursor is after it — delete
|
|
64
|
+
// only that character and stop, preventing the block-join that fires Enter.
|
|
65
|
+
if (parentText.length === 1 && $from.parentOffset === 1) {
|
|
66
|
+
const { tr } = state;
|
|
67
|
+
tr.delete($from.pos - 1, $from.pos);
|
|
68
|
+
this.editor.view.dispatch(tr);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return false;
|
|
73
|
+
},
|
|
34
74
|
};
|
|
35
75
|
},
|
|
36
76
|
});
|
package/src/extensions/math.js
CHANGED
|
@@ -8,6 +8,15 @@ import { wrapMath } from '@pie-lib/math-rendering';
|
|
|
8
8
|
|
|
9
9
|
const ensureTextAfterMathPluginKey = new PluginKey('ensureTextAfterMath');
|
|
10
10
|
|
|
11
|
+
const generateAdditionalKeys = (keyData = []) => {
|
|
12
|
+
return keyData.map((key) => ({
|
|
13
|
+
name: key,
|
|
14
|
+
latex: key,
|
|
15
|
+
write: key,
|
|
16
|
+
label: key,
|
|
17
|
+
}));
|
|
18
|
+
};
|
|
19
|
+
|
|
11
20
|
export const EnsureTextAfterMathPlugin = (mathNodeName) =>
|
|
12
21
|
new Plugin({
|
|
13
22
|
key: ensureTextAfterMathPluginKey,
|
|
@@ -175,6 +184,14 @@ export const MathNodeView = (props) => {
|
|
|
175
184
|
const [showToolbar, setShowToolbar] = useState(selected);
|
|
176
185
|
const toolbarRef = useRef(null);
|
|
177
186
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
187
|
+
const { math: mathOptions = {} } = options || {};
|
|
188
|
+
const {
|
|
189
|
+
keypadMode,
|
|
190
|
+
controlledKeypadMode = true,
|
|
191
|
+
customKeys = [],
|
|
192
|
+
keyPadCharacterRef,
|
|
193
|
+
setKeypadInteraction,
|
|
194
|
+
} = mathOptions;
|
|
178
195
|
|
|
179
196
|
const latex = node.attrs.latex || '';
|
|
180
197
|
|
|
@@ -199,10 +216,26 @@ export const MathNodeView = (props) => {
|
|
|
199
216
|
});
|
|
200
217
|
|
|
201
218
|
const handleClickOutside = (event) => {
|
|
219
|
+
const target = event?.target;
|
|
220
|
+
|
|
221
|
+
// MUI's `Select` renders its dropdown options in a portal attached to `document.body`.
|
|
222
|
+
// Those clicks should not dismiss the math toolbar.
|
|
223
|
+
const equationEditorListboxes =
|
|
224
|
+
document.querySelectorAll?.(
|
|
225
|
+
'[id^="equation-editor-select"][id*="listbox"], [aria-labelledby="equation-editor-label"][role="listbox"]',
|
|
226
|
+
) || [];
|
|
227
|
+
|
|
228
|
+
const equationEditorPopoverOpen = equationEditorListboxes.length > 0;
|
|
229
|
+
const clickedEquationEditorSelect =
|
|
230
|
+
!!(target?.id && target.id.includes('equation-editor-select')) ||
|
|
231
|
+
!!target?.closest?.('[id*="equation-editor-select"]');
|
|
232
|
+
|
|
202
233
|
if (
|
|
203
234
|
toolbarRef.current &&
|
|
204
|
-
!toolbarRef.current.contains(
|
|
205
|
-
!
|
|
235
|
+
!toolbarRef.current.contains(target) &&
|
|
236
|
+
!target?.closest?.('[data-inline-node]') &&
|
|
237
|
+
!equationEditorPopoverOpen &&
|
|
238
|
+
!clickedEquationEditorSelect
|
|
206
239
|
) {
|
|
207
240
|
setShowToolbar(false);
|
|
208
241
|
}
|
|
@@ -266,7 +299,17 @@ export const MathNodeView = (props) => {
|
|
|
266
299
|
'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)',
|
|
267
300
|
}}
|
|
268
301
|
>
|
|
269
|
-
<MathToolbar
|
|
302
|
+
<MathToolbar
|
|
303
|
+
latex={latex}
|
|
304
|
+
autoFocus
|
|
305
|
+
onChange={handleChange}
|
|
306
|
+
onDone={handleDone}
|
|
307
|
+
keypadMode={keypadMode}
|
|
308
|
+
controlledKeypadMode={controlledKeypadMode}
|
|
309
|
+
additionalKeys={generateAdditionalKeys(customKeys)}
|
|
310
|
+
keyPadCharacterRef={keyPadCharacterRef}
|
|
311
|
+
setKeypadInteraction={setKeypadInteraction}
|
|
312
|
+
/>
|
|
270
313
|
</div>,
|
|
271
314
|
document.body,
|
|
272
315
|
)}
|
|
@@ -142,10 +142,12 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
// --- Slate: indexing logic (kept identical) ---
|
|
145
|
-
if (lastIndexMap[typeName] === undefined)
|
|
145
|
+
if (lastIndexMap[typeName] === undefined) {
|
|
146
|
+
lastIndexMap[typeName] = 0;
|
|
147
|
+
}
|
|
146
148
|
|
|
147
149
|
const prevIndex = lastIndexMap[typeName];
|
|
148
|
-
const newIndex = prevIndex
|
|
150
|
+
const newIndex = prevIndex + 1;
|
|
149
151
|
|
|
150
152
|
// Slate increments map even if newIndex === 0
|
|
151
153
|
lastIndexMap[typeName] += 1;
|
|
@@ -156,7 +158,9 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
156
158
|
index: newIndex,
|
|
157
159
|
});
|
|
158
160
|
|
|
159
|
-
if (!newInline)
|
|
161
|
+
if (!newInline) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
160
164
|
|
|
161
165
|
// --- Insert logic ---
|
|
162
166
|
const { selection } = state;
|
|
@@ -182,7 +186,10 @@ export const ResponseAreaExtension = Extension.create({
|
|
|
182
186
|
if (usedPos == null) {
|
|
183
187
|
usedPos = tryInsertAt(tr.doc.content.size);
|
|
184
188
|
}
|
|
185
|
-
|
|
189
|
+
|
|
190
|
+
if (usedPos == null) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
186
193
|
|
|
187
194
|
// Optionally select the node you just inserted (like your original command)
|
|
188
195
|
// tr.setSelection(NodeSelection.create(tr.doc, usedPos))
|