@pie-lib/editable-html-tip-tap 2.1.1 → 2.1.2-next.102
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/lib/components/CharacterPicker.js +51 -20
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/EditableHtml.js +37 -24
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +15 -6
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/TiptapContainer.js +9 -1
- package/lib/components/TiptapContainer.js.map +1 -1
- package/lib/components/common/toolbar-buttons.js +5 -0
- package/lib/components/common/toolbar-buttons.js.map +1 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js +2 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +74 -16
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/components/respArea/inlineDropdownUtils.js +67 -0
- package/lib/components/respArea/inlineDropdownUtils.js.map +1 -0
- package/lib/extensions/css.js +17 -7
- package/lib/extensions/css.js.map +1 -1
- package/lib/extensions/image-component.js +9 -6
- package/lib/extensions/image-component.js.map +1 -1
- package/lib/extensions/math.js +179 -71
- package/lib/extensions/math.js.map +1 -1
- package/lib/extensions/responseArea.js +7 -1
- package/lib/extensions/responseArea.js.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/utils/toolbar.js +19 -0
- package/lib/utils/toolbar.js.map +1 -0
- package/package.json +18 -18
- package/src/__tests__/EditableHtml.test.jsx +116 -2
- package/src/components/CharacterPicker.jsx +56 -19
- package/src/components/EditableHtml.jsx +43 -23
- package/src/components/MenuBar.jsx +15 -3
- package/src/components/TiptapContainer.jsx +18 -2
- package/src/components/__tests__/CharacterPicker.test.jsx +57 -6
- package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +6 -1
- package/src/components/__tests__/InlineDropdown.test.jsx +185 -8
- package/src/components/__tests__/MenuBar.test.jsx +1 -0
- package/src/components/common/toolbar-buttons.jsx +5 -0
- package/src/components/respArea/ExplicitConstructedResponse.jsx +2 -1
- package/src/components/respArea/InlineDropdown.jsx +97 -24
- package/src/components/respArea/inlineDropdownUtils.js +79 -0
- package/src/extensions/__tests__/image-component.test.jsx +1 -0
- package/src/extensions/__tests__/math.test.js +481 -42
- package/src/extensions/__tests__/responseArea.test.js +57 -3
- package/src/extensions/css.js +16 -3
- package/src/extensions/image-component.jsx +8 -6
- package/src/extensions/math.js +188 -69
- package/src/extensions/responseArea.js +7 -1
- package/src/index.jsx +2 -2
- package/src/utils/__tests__/toolbar.test.js +43 -0
- package/src/utils/toolbar.js +15 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, waitFor, fireEvent } from '@testing-library/react';
|
|
3
|
-
import { MathNode, MathNodeView } from '../math';
|
|
2
|
+
import { render, waitFor, fireEvent, act } from '@testing-library/react';
|
|
3
|
+
import { EnsureTextAfterMathPlugin, MathNode, MathNodeView, ZeroWidthSpaceHandlingPlugin } from '../math';
|
|
4
|
+
import * as toolbarUtils from '../../utils/toolbar';
|
|
4
5
|
|
|
5
6
|
jest.mock('@tiptap/react', () => ({
|
|
6
7
|
NodeViewWrapper: ({ children, ...props }) => (
|
|
@@ -147,6 +148,35 @@ describe('MathNode', () => {
|
|
|
147
148
|
expect(commands).toHaveProperty('insertMath');
|
|
148
149
|
expect(typeof commands.insertMath).toBe('function');
|
|
149
150
|
});
|
|
151
|
+
|
|
152
|
+
it('insertMath opens the toolbar after inserting a math node', () => {
|
|
153
|
+
const setToolbarOpenedSpy = jest.spyOn(toolbarUtils, 'setToolbarOpened');
|
|
154
|
+
const mathNode = { type: { name: 'math' }, nodeSize: 1 };
|
|
155
|
+
const tr = {
|
|
156
|
+
insert: jest.fn().mockReturnThis(),
|
|
157
|
+
setSelection: jest.fn().mockReturnThis(),
|
|
158
|
+
doc: {},
|
|
159
|
+
};
|
|
160
|
+
const editor = {
|
|
161
|
+
view: {
|
|
162
|
+
state: {
|
|
163
|
+
schema: { nodes: { math: { create: jest.fn(() => mathNode) } } },
|
|
164
|
+
selection: { $from: { pos: 1 } },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
const dispatch = jest.fn();
|
|
169
|
+
const insertMath = MathNode.addCommands().insertMath('x^2');
|
|
170
|
+
|
|
171
|
+
insertMath({ tr, editor, dispatch });
|
|
172
|
+
|
|
173
|
+
expect(tr.insert).toHaveBeenCalledWith(1, mathNode);
|
|
174
|
+
expect(tr.setSelection).toHaveBeenCalled();
|
|
175
|
+
expect(dispatch).toHaveBeenCalledWith(tr);
|
|
176
|
+
expect(setToolbarOpenedSpy).toHaveBeenCalledWith(editor, true);
|
|
177
|
+
|
|
178
|
+
setToolbarOpenedSpy.mockRestore();
|
|
179
|
+
});
|
|
150
180
|
});
|
|
151
181
|
|
|
152
182
|
describe('addNodeView', () => {
|
|
@@ -156,17 +186,215 @@ describe('MathNode', () => {
|
|
|
156
186
|
expect(result).toBeDefined();
|
|
157
187
|
});
|
|
158
188
|
});
|
|
189
|
+
|
|
190
|
+
describe('addProseMirrorPlugins', () => {
|
|
191
|
+
it('registers ensure-text-after-math and zero-width-space plugins', () => {
|
|
192
|
+
const plugins = MathNode.addProseMirrorPlugins();
|
|
193
|
+
|
|
194
|
+
expect(plugins).toHaveLength(2);
|
|
195
|
+
expect(plugins[0].appendTransaction).toBeDefined();
|
|
196
|
+
expect(plugins[1].props.handleKeyDown).toBeDefined();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('EnsureTextAfterMathPlugin', () => {
|
|
202
|
+
it('inserts a zero-width space after a math node when no text follows', () => {
|
|
203
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
204
|
+
const textNode = { type: { name: 'text' } };
|
|
205
|
+
const mathNode = { type: { name: 'math' }, nodeSize: 3 };
|
|
206
|
+
const tr = { insert: jest.fn() };
|
|
207
|
+
|
|
208
|
+
const newState = {
|
|
209
|
+
schema: { text: jest.fn((value) => ({ type: textNode.type, text: value })) },
|
|
210
|
+
tr,
|
|
211
|
+
doc: {
|
|
212
|
+
descendants: (cb) => cb(mathNode, 5),
|
|
213
|
+
nodeAt: jest.fn(() => null),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const result = plugin.appendTransaction([{ docChanged: true }], {}, newState);
|
|
218
|
+
|
|
219
|
+
expect(tr.insert).toHaveBeenCalledWith(8, expect.anything());
|
|
220
|
+
expect(result).toBe(tr);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not insert when text already follows the math node', () => {
|
|
224
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
225
|
+
const tr = { insert: jest.fn() };
|
|
226
|
+
const mathNode = { type: { name: 'math' }, nodeSize: 3 };
|
|
227
|
+
|
|
228
|
+
const newState = {
|
|
229
|
+
schema: { text: jest.fn() },
|
|
230
|
+
tr,
|
|
231
|
+
doc: {
|
|
232
|
+
descendants: (cb) => cb(mathNode, 5),
|
|
233
|
+
nodeAt: jest.fn(() => ({ type: { name: 'text' } })),
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = plugin.appendTransaction([{ docChanged: true }], {}, newState);
|
|
238
|
+
|
|
239
|
+
expect(tr.insert).not.toHaveBeenCalled();
|
|
240
|
+
expect(result).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns null when the document did not change', () => {
|
|
244
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
245
|
+
|
|
246
|
+
const result = plugin.appendTransaction([{ docChanged: false }], {}, {});
|
|
247
|
+
expect(result).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('ZeroWidthSpaceHandlingPlugin', () => {
|
|
252
|
+
const createDocWithMathAndZwsp = (resolveOverrides = {}) => ({
|
|
253
|
+
resolve: jest.fn((pos) => ({
|
|
254
|
+
nodeAfter: null,
|
|
255
|
+
nodeBefore: null,
|
|
256
|
+
pos,
|
|
257
|
+
...resolveOverrides[pos],
|
|
258
|
+
})),
|
|
259
|
+
nodeAt: jest.fn((pos) => {
|
|
260
|
+
if (pos === 1) {
|
|
261
|
+
return { type: { name: 'text' }, textContent: '\u200b' };
|
|
262
|
+
}
|
|
263
|
+
if (pos === 0) {
|
|
264
|
+
return { type: { name: 'math' }, nodeSize: 1 };
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const createDocWithRegularTextBeforeCursor = () => ({
|
|
271
|
+
resolve: jest.fn((pos) => ({ nodeAfter: null, nodeBefore: null, pos })),
|
|
272
|
+
nodeAt: jest.fn((pos) => {
|
|
273
|
+
if (pos === 1) {
|
|
274
|
+
return { type: { name: 'text' }, textContent: 'a' };
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const createView = ({ state: stateOverrides = {} } = {}) => {
|
|
281
|
+
const dispatch = jest.fn();
|
|
282
|
+
const tr = {
|
|
283
|
+
delete: jest.fn().mockReturnThis(),
|
|
284
|
+
setSelection: jest.fn().mockReturnThis(),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
state: {
|
|
289
|
+
selection: { from: 2, empty: true },
|
|
290
|
+
doc: createDocWithMathAndZwsp(),
|
|
291
|
+
tr,
|
|
292
|
+
...stateOverrides,
|
|
293
|
+
},
|
|
294
|
+
dispatch,
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
describe('Backspace', () => {
|
|
299
|
+
it('deletes the inline node and zero-width space before the cursor', () => {
|
|
300
|
+
const view = createView();
|
|
301
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'Backspace' });
|
|
302
|
+
|
|
303
|
+
expect(handled).toBe(true);
|
|
304
|
+
expect(view.state.tr.delete).toHaveBeenCalledWith(0, 2);
|
|
305
|
+
expect(view.dispatch).toHaveBeenCalledWith(view.state.tr);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('returns false when regular text precedes the cursor', () => {
|
|
309
|
+
const view = createView({
|
|
310
|
+
state: {
|
|
311
|
+
doc: createDocWithRegularTextBeforeCursor(),
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'Backspace' });
|
|
316
|
+
|
|
317
|
+
expect(handled).toBe(false);
|
|
318
|
+
expect(view.state.tr.delete).not.toHaveBeenCalled();
|
|
319
|
+
expect(view.dispatch).not.toHaveBeenCalled();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('ArrowLeft', () => {
|
|
324
|
+
it('selects the inline node before a zero-width space', () => {
|
|
325
|
+
const mathNode = { nodeSize: 3 };
|
|
326
|
+
const view = createView({
|
|
327
|
+
state: {
|
|
328
|
+
doc: createDocWithMathAndZwsp({
|
|
329
|
+
0: { nodeAfter: mathNode, nodeBefore: null, pos: 0 },
|
|
330
|
+
}),
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
const { NodeSelection } = require('prosemirror-state');
|
|
334
|
+
|
|
335
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'ArrowLeft' });
|
|
336
|
+
|
|
337
|
+
expect(handled).toBe(true);
|
|
338
|
+
expect(view.state.doc.resolve).toHaveBeenCalledWith(0);
|
|
339
|
+
expect(NodeSelection.create).toHaveBeenCalledWith(view.state.doc, 0);
|
|
340
|
+
expect(view.dispatch).toHaveBeenCalled();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('moves the text cursor before the zero-width space when no inline node precedes it', () => {
|
|
344
|
+
const view = createView();
|
|
345
|
+
const { TextSelection } = require('prosemirror-state');
|
|
346
|
+
|
|
347
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'ArrowLeft' });
|
|
348
|
+
|
|
349
|
+
expect(handled).toBe(true);
|
|
350
|
+
expect(view.state.doc.resolve).toHaveBeenCalledWith(0);
|
|
351
|
+
expect(TextSelection.create).toHaveBeenCalledWith(view.state.doc, 0);
|
|
352
|
+
expect(view.dispatch).toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('returns false when regular text precedes the cursor', () => {
|
|
356
|
+
const view = createView({
|
|
357
|
+
state: {
|
|
358
|
+
doc: createDocWithRegularTextBeforeCursor(),
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'ArrowLeft' });
|
|
363
|
+
|
|
364
|
+
expect(handled).toBe(false);
|
|
365
|
+
expect(view.state.tr.setSelection).not.toHaveBeenCalled();
|
|
366
|
+
expect(view.dispatch).not.toHaveBeenCalled();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('returns false for unrelated keys', () => {
|
|
371
|
+
const view = createView();
|
|
372
|
+
|
|
373
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'Enter' });
|
|
374
|
+
expect(handled).toBe(false);
|
|
375
|
+
});
|
|
159
376
|
});
|
|
160
377
|
|
|
161
378
|
describe('MathNodeView', () => {
|
|
379
|
+
const createEditorElement = (rect = { top: 0, left: 0, width: 600, height: 400 }) => {
|
|
380
|
+
const element = document.createElement('div');
|
|
381
|
+
Object.defineProperty(element, 'getBoundingClientRect', {
|
|
382
|
+
value: jest.fn(() => rect),
|
|
383
|
+
configurable: true,
|
|
384
|
+
});
|
|
385
|
+
return element;
|
|
386
|
+
};
|
|
387
|
+
|
|
162
388
|
const createMockEditor = () => ({
|
|
163
389
|
state: {
|
|
164
390
|
selection: {
|
|
165
391
|
from: 0,
|
|
166
392
|
to: 1,
|
|
393
|
+
node: { type: { name: 'math' } },
|
|
167
394
|
},
|
|
168
395
|
tr: {
|
|
169
396
|
setSelection: jest.fn().mockReturnThis(),
|
|
397
|
+
setMeta: jest.fn().mockReturnThis(),
|
|
170
398
|
},
|
|
171
399
|
doc: {},
|
|
172
400
|
},
|
|
@@ -174,6 +402,9 @@ describe('MathNodeView', () => {
|
|
|
174
402
|
coordsAtPos: jest.fn(() => ({ top: 100, left: 50 })),
|
|
175
403
|
dispatch: jest.fn(),
|
|
176
404
|
},
|
|
405
|
+
options: {
|
|
406
|
+
element: createEditorElement(),
|
|
407
|
+
},
|
|
177
408
|
commands: {
|
|
178
409
|
focus: jest.fn(),
|
|
179
410
|
},
|
|
@@ -185,17 +416,11 @@ describe('MathNodeView', () => {
|
|
|
185
416
|
attrs: {
|
|
186
417
|
latex: 'x^2',
|
|
187
418
|
},
|
|
419
|
+
nodeSize: 1,
|
|
188
420
|
};
|
|
189
421
|
|
|
190
422
|
let defaultProps;
|
|
191
423
|
|
|
192
|
-
beforeAll(() => {
|
|
193
|
-
Object.defineProperty(document.body, 'getBoundingClientRect', {
|
|
194
|
-
value: jest.fn(() => ({ top: 0, left: 0 })),
|
|
195
|
-
configurable: true,
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
424
|
beforeEach(() => {
|
|
200
425
|
jest.clearAllMocks();
|
|
201
426
|
mockCreatePortal.mockImplementation((node) => node);
|
|
@@ -205,6 +430,7 @@ describe('MathNodeView', () => {
|
|
|
205
430
|
editor: createMockEditor(),
|
|
206
431
|
selected: false,
|
|
207
432
|
options: {},
|
|
433
|
+
getPos: jest.fn(() => 0),
|
|
208
434
|
};
|
|
209
435
|
});
|
|
210
436
|
|
|
@@ -244,38 +470,31 @@ describe('MathNodeView', () => {
|
|
|
244
470
|
});
|
|
245
471
|
|
|
246
472
|
describe('toolbar positioning', () => {
|
|
247
|
-
it('
|
|
248
|
-
const
|
|
249
|
-
containerEl.getBoundingClientRect = jest.fn(() => ({ top: -50, left: 20, width: 600, height: 400 }));
|
|
250
|
-
|
|
251
|
-
const editor = {
|
|
252
|
-
...defaultProps.editor,
|
|
253
|
-
_tiptapContainerEl: containerEl,
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
473
|
+
it('positions relative to portal container using coordsAtPos', async () => {
|
|
474
|
+
const { container } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
257
475
|
await waitFor(() => {
|
|
258
476
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
259
477
|
expect(toolbar).toBeInTheDocument();
|
|
260
|
-
|
|
261
|
-
expect(toolbar.style.top).toBe('190px');
|
|
478
|
+
expect(toolbar.style.top).toBe('100px');
|
|
262
479
|
expect(toolbar.style.left).toBe('50px');
|
|
263
480
|
});
|
|
264
481
|
});
|
|
265
482
|
|
|
266
|
-
it('
|
|
483
|
+
it('offsets position by portal container getBoundingClientRect', async () => {
|
|
484
|
+
const containerEl = document.createElement('div');
|
|
485
|
+
containerEl.getBoundingClientRect = jest.fn(() => ({ top: 100, left: 50, width: 600, height: 400 }));
|
|
486
|
+
|
|
267
487
|
const editor = {
|
|
268
488
|
...defaultProps.editor,
|
|
269
|
-
_tiptapContainerEl:
|
|
489
|
+
_tiptapContainerEl: containerEl,
|
|
270
490
|
};
|
|
271
491
|
|
|
272
492
|
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
273
493
|
await waitFor(() => {
|
|
274
494
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
275
495
|
expect(toolbar).toBeInTheDocument();
|
|
276
|
-
|
|
277
|
-
expect(toolbar.style.
|
|
278
|
-
expect(toolbar.style.left).toBe('50px');
|
|
496
|
+
expect(toolbar.style.top).toBe('0px');
|
|
497
|
+
expect(toolbar.style.left).toBe('0px');
|
|
279
498
|
});
|
|
280
499
|
});
|
|
281
500
|
|
|
@@ -288,7 +507,16 @@ describe('MathNodeView', () => {
|
|
|
288
507
|
});
|
|
289
508
|
});
|
|
290
509
|
|
|
291
|
-
it('
|
|
510
|
+
it('renders above other editor overlays with a high z-index', async () => {
|
|
511
|
+
const { container } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
512
|
+
await waitFor(() => {
|
|
513
|
+
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
514
|
+
expect(toolbar).toBeInTheDocument();
|
|
515
|
+
expect(toolbar.style.zIndex).toBe('1000');
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('updates position from coordsAtPos when selection changes', async () => {
|
|
292
520
|
const editor = {
|
|
293
521
|
...defaultProps.editor,
|
|
294
522
|
view: {
|
|
@@ -302,28 +530,62 @@ describe('MathNodeView', () => {
|
|
|
302
530
|
await waitFor(() => {
|
|
303
531
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
304
532
|
expect(toolbar).toBeInTheDocument();
|
|
305
|
-
|
|
306
|
-
expect(toolbar.style.top).toBe('240px');
|
|
533
|
+
expect(toolbar.style.top).toBe('200px');
|
|
307
534
|
expect(toolbar.style.left).toBe('150px');
|
|
308
535
|
});
|
|
309
536
|
});
|
|
310
537
|
|
|
311
|
-
it('
|
|
312
|
-
const
|
|
313
|
-
|
|
538
|
+
it('clamps toolbar position to viewport margins', async () => {
|
|
539
|
+
const originalInnerHeight = window.innerHeight;
|
|
540
|
+
const originalInnerWidth = window.innerWidth;
|
|
541
|
+
|
|
542
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, writable: true, value: 200 });
|
|
543
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: 300 });
|
|
314
544
|
|
|
315
545
|
const editor = {
|
|
316
546
|
...defaultProps.editor,
|
|
317
|
-
|
|
547
|
+
view: {
|
|
548
|
+
...defaultProps.editor.view,
|
|
549
|
+
coordsAtPos: jest.fn(() => ({ top: 190, left: 280, bottom: 195 })),
|
|
550
|
+
dispatch: jest.fn(),
|
|
551
|
+
},
|
|
318
552
|
};
|
|
319
553
|
|
|
320
554
|
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
555
|
+
|
|
556
|
+
let toolbar;
|
|
321
557
|
await waitFor(() => {
|
|
322
|
-
|
|
558
|
+
toolbar = container.querySelector('[data-toolbar-for]');
|
|
323
559
|
expect(toolbar).toBeInTheDocument();
|
|
324
|
-
// top = 100 + Math.abs(-200) + 40 = 340
|
|
325
|
-
expect(toolbar.style.top).toBe('340px');
|
|
326
560
|
});
|
|
561
|
+
|
|
562
|
+
Object.defineProperty(toolbar, 'offsetHeight', { configurable: true, value: 100 });
|
|
563
|
+
Object.defineProperty(toolbar, 'offsetWidth', { configurable: true, value: 150 });
|
|
564
|
+
|
|
565
|
+
await act(async () => {
|
|
566
|
+
window.dispatchEvent(new Event('resize'));
|
|
567
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
await waitFor(() => {
|
|
571
|
+
expect(parseInt(toolbar.style.top, 10)).toBeLessThanOrEqual(200 - 100 - 8);
|
|
572
|
+
expect(parseInt(toolbar.style.left, 10)).toBeLessThanOrEqual(300 - 150 - 8);
|
|
573
|
+
expect(parseInt(toolbar.style.top, 10)).toBeGreaterThanOrEqual(8);
|
|
574
|
+
expect(parseInt(toolbar.style.left, 10)).toBeGreaterThanOrEqual(8);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
Object.defineProperty(window, 'innerHeight', { configurable: true, writable: true, value: originalInnerHeight });
|
|
578
|
+
Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: originalInnerWidth });
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('attaches scroll and resize listeners while toolbar is open', async () => {
|
|
582
|
+
const addSpy = jest.spyOn(window, 'addEventListener');
|
|
583
|
+
render(<MathNodeView {...defaultProps} selected={true} />);
|
|
584
|
+
await waitFor(() => {
|
|
585
|
+
expect(addSpy).toHaveBeenCalledWith('scroll', expect.any(Function), true);
|
|
586
|
+
expect(addSpy).toHaveBeenCalledWith('resize', expect.any(Function));
|
|
587
|
+
});
|
|
588
|
+
addSpy.mockRestore();
|
|
327
589
|
});
|
|
328
590
|
|
|
329
591
|
it('portals toolbar into _tiptapContainerEl when available', async () => {
|
|
@@ -395,23 +657,45 @@ describe('MathNodeView', () => {
|
|
|
395
657
|
});
|
|
396
658
|
});
|
|
397
659
|
|
|
398
|
-
it('unsets editor._toolbarOpened when toolbar is closed', async () => {
|
|
660
|
+
it('unsets editor._toolbarOpened when toolbar is closed and node is not selected', async () => {
|
|
661
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} selected={false} />);
|
|
662
|
+
|
|
663
|
+
fireEvent.click(getByTestId('math-preview'));
|
|
664
|
+
|
|
665
|
+
await waitFor(() => {
|
|
666
|
+
expect(getByTestId('math-toolbar')).toBeInTheDocument();
|
|
667
|
+
expect(defaultProps.editor._toolbarOpened).toBe(true);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
fireEvent.click(getByTestId('done-button'));
|
|
671
|
+
|
|
672
|
+
await waitFor(() => {
|
|
673
|
+
expect(defaultProps.editor._toolbarOpened).toBe(false);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('keeps editor._toolbarOpened true while the math node remains selected', async () => {
|
|
399
678
|
const { getByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
400
679
|
|
|
401
680
|
await waitFor(() => {
|
|
402
681
|
expect(getByTestId('done-button')).toBeInTheDocument();
|
|
682
|
+
expect(defaultProps.editor._toolbarOpened).toBe(true);
|
|
403
683
|
});
|
|
404
684
|
|
|
405
|
-
|
|
406
|
-
fireEvent.click(doneButton);
|
|
685
|
+
fireEvent.click(getByTestId('done-button'));
|
|
407
686
|
|
|
408
687
|
await waitFor(() => {
|
|
409
|
-
expect(defaultProps.editor._toolbarOpened).toBe(
|
|
688
|
+
expect(defaultProps.editor._toolbarOpened).toBe(true);
|
|
410
689
|
});
|
|
411
690
|
});
|
|
412
691
|
|
|
413
|
-
it('closes toolbar on outside click', async () => {
|
|
414
|
-
const
|
|
692
|
+
it('closes toolbar on outside click and runs handleDone', async () => {
|
|
693
|
+
const updateAttributes = jest.fn();
|
|
694
|
+
const editor = createMockEditor();
|
|
695
|
+
const { TextSelection } = require('prosemirror-state');
|
|
696
|
+
const { queryByTestId } = render(
|
|
697
|
+
<MathNodeView {...defaultProps} editor={editor} updateAttributes={updateAttributes} selected={true} />,
|
|
698
|
+
);
|
|
415
699
|
|
|
416
700
|
await waitFor(() => {
|
|
417
701
|
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
@@ -421,6 +705,161 @@ describe('MathNodeView', () => {
|
|
|
421
705
|
|
|
422
706
|
await waitFor(() => {
|
|
423
707
|
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
708
|
+
expect(updateAttributes).toHaveBeenCalledWith({ latex: 'x^2' });
|
|
709
|
+
expect(TextSelection.create).toHaveBeenCalledWith(editor.state.doc, 1);
|
|
710
|
+
expect(editor.state.tr.setSelection).toHaveBeenCalled();
|
|
711
|
+
expect(editor.view.dispatch).toHaveBeenCalledWith(editor.state.tr);
|
|
712
|
+
expect(editor.commands.focus).toHaveBeenCalled();
|
|
713
|
+
expect(editor._toolbarOpened).toBe(true);
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('re-registers click listener when node changes', async () => {
|
|
718
|
+
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
|
|
719
|
+
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
|
|
720
|
+
const nodeA = { attrs: { latex: 'x^2' } };
|
|
721
|
+
const nodeB = { attrs: { latex: 'y^2' } };
|
|
722
|
+
|
|
723
|
+
const { rerender } = render(<MathNodeView {...defaultProps} node={nodeA} selected={true} />);
|
|
724
|
+
|
|
725
|
+
await waitFor(() => {
|
|
726
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function));
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const initialCallCount = addEventListenerSpy.mock.calls.length;
|
|
730
|
+
|
|
731
|
+
rerender(<MathNodeView {...defaultProps} node={nodeB} selected={true} />);
|
|
732
|
+
|
|
733
|
+
await waitFor(() => {
|
|
734
|
+
expect(removeEventListenerSpy).toHaveBeenCalled();
|
|
735
|
+
expect(addEventListenerSpy.mock.calls.length).toBeGreaterThan(initialCallCount);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
addEventListenerSpy.mockRestore();
|
|
739
|
+
removeEventListenerSpy.mockRestore();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('does not close toolbar when clicking the math node preview', async () => {
|
|
743
|
+
const { getByTestId, queryByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
744
|
+
|
|
745
|
+
await waitFor(() => {
|
|
746
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
fireEvent.click(getByTestId('math-preview'));
|
|
750
|
+
|
|
751
|
+
await waitFor(() => {
|
|
752
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
describe('unique math-node class per instance', () => {
|
|
757
|
+
let dateNowSpy;
|
|
758
|
+
|
|
759
|
+
beforeEach(() => {
|
|
760
|
+
let timestamp = 1000;
|
|
761
|
+
dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => timestamp++);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
afterEach(() => {
|
|
765
|
+
dateNowSpy.mockRestore();
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('assigns a timestamp-based class to NodeViewWrapper', () => {
|
|
769
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} />);
|
|
770
|
+
const wrapper = getByTestId('node-view-wrapper');
|
|
771
|
+
|
|
772
|
+
expect(wrapper.className).toMatch(/^math-node-\d+$/);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('assigns a different class to each MathNodeView instance', () => {
|
|
776
|
+
const { getAllByTestId } = render(
|
|
777
|
+
<>
|
|
778
|
+
<MathNodeView {...defaultProps} />
|
|
779
|
+
<MathNodeView {...defaultProps} node={{ attrs: { latex: 'y^2' } }} />
|
|
780
|
+
</>,
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
const wrappers = getAllByTestId('node-view-wrapper');
|
|
784
|
+
expect(wrappers[0].className).toBe('math-node-1000');
|
|
785
|
+
expect(wrappers[1].className).toBe('math-node-1001');
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('closes toolbar when clicking a different math node', async () => {
|
|
789
|
+
const updateAttributes = jest.fn();
|
|
790
|
+
const editorA = createMockEditor();
|
|
791
|
+
const editorB = createMockEditor();
|
|
792
|
+
|
|
793
|
+
const { getAllByTestId, queryAllByTestId } = render(
|
|
794
|
+
<>
|
|
795
|
+
<MathNodeView {...defaultProps} editor={editorA} updateAttributes={updateAttributes} selected={false} />
|
|
796
|
+
<MathNodeView {...defaultProps} editor={editorB} node={{ attrs: { latex: 'y^2' } }} selected={false} />
|
|
797
|
+
</>,
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
fireEvent.click(getAllByTestId('math-preview')[0]);
|
|
801
|
+
|
|
802
|
+
await waitFor(() => {
|
|
803
|
+
expect(queryAllByTestId('math-toolbar')).toHaveLength(1);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const secondPreview = getAllByTestId('math-preview')[1];
|
|
807
|
+
fireEvent.click(secondPreview);
|
|
808
|
+
|
|
809
|
+
await waitFor(() => {
|
|
810
|
+
expect(queryAllByTestId('math-toolbar')).toHaveLength(1);
|
|
811
|
+
expect(updateAttributes).toHaveBeenCalledWith({ latex: 'x^2' });
|
|
812
|
+
expect(editorA._toolbarOpened).toBe(false);
|
|
813
|
+
expect(getAllByTestId('math-input')[0]).toHaveValue('y^2');
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
describe('selection-based toolbar guard', () => {
|
|
819
|
+
it('opens toolbar when selected transitions to true and the editor has a NodeSelection on math', async () => {
|
|
820
|
+
const { queryByTestId, rerender } = render(<MathNodeView {...defaultProps} selected={false} />);
|
|
821
|
+
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
822
|
+
|
|
823
|
+
rerender(<MathNodeView {...defaultProps} selected={true} />);
|
|
824
|
+
await waitFor(() => {
|
|
825
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('does not open toolbar when selected briefly becomes true but editor selection has no node (Cmd+A / drag case)', async () => {
|
|
830
|
+
const editor = {
|
|
831
|
+
...defaultProps.editor,
|
|
832
|
+
state: {
|
|
833
|
+
...defaultProps.editor.state,
|
|
834
|
+
selection: { from: 0, to: 100 }, // no .node — TextSelection / AllSelection shape
|
|
835
|
+
},
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const { queryByTestId, rerender } = render(
|
|
839
|
+
<MathNodeView {...defaultProps} editor={editor} selected={false} />,
|
|
840
|
+
);
|
|
841
|
+
rerender(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
842
|
+
|
|
843
|
+
await act(async () => {});
|
|
844
|
+
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('does not open toolbar when selected briefly becomes true but NodeSelection targets a non-math node', async () => {
|
|
848
|
+
const editor = {
|
|
849
|
+
...defaultProps.editor,
|
|
850
|
+
state: {
|
|
851
|
+
...defaultProps.editor.state,
|
|
852
|
+
selection: { from: 0, to: 1, node: { type: { name: 'image' } } },
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const { queryByTestId, rerender } = render(
|
|
857
|
+
<MathNodeView {...defaultProps} editor={editor} selected={false} />,
|
|
858
|
+
);
|
|
859
|
+
rerender(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
860
|
+
|
|
861
|
+
await act(async () => {});
|
|
862
|
+
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
424
863
|
});
|
|
425
864
|
});
|
|
426
865
|
|