@pie-lib/editable-html-tip-tap 2.1.3 → 2.1.5
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 +14 -0
- package/lib/components/MenuBar.js +5 -4
- package/lib/components/MenuBar.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 +2 -1
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/extensions/math.js +29 -29
- package/lib/extensions/math.js.map +1 -1
- package/lib/utils/toolbar.js +19 -0
- package/lib/utils/toolbar.js.map +1 -0
- package/package.json +6 -6
- package/src/components/MenuBar.jsx +5 -3
- package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +6 -1
- package/src/components/__tests__/InlineDropdown.test.jsx +6 -1
- package/src/components/respArea/ExplicitConstructedResponse.jsx +2 -1
- package/src/components/respArea/InlineDropdown.jsx +2 -1
- package/src/extensions/__tests__/math.test.js +245 -40
- package/src/extensions/math.js +25 -26
- package/src/utils/__tests__/toolbar.test.js +43 -0
- package/src/utils/toolbar.js +15 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, waitFor, fireEvent } from '@testing-library/react';
|
|
3
|
-
import { MathNode, MathNodeView } from '../math';
|
|
3
|
+
import { EnsureTextAfterMathPlugin, MathNode, MathNodeView, ZeroWidthSpaceHandlingPlugin } from '../math';
|
|
4
4
|
|
|
5
5
|
jest.mock('@tiptap/react', () => ({
|
|
6
6
|
NodeViewWrapper: ({ children, ...props }) => (
|
|
@@ -156,6 +156,151 @@ describe('MathNode', () => {
|
|
|
156
156
|
expect(result).toBeDefined();
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
|
+
|
|
160
|
+
describe('addProseMirrorPlugins', () => {
|
|
161
|
+
it('registers ensure-text-after-math and zero-width-space plugins', () => {
|
|
162
|
+
const plugins = MathNode.addProseMirrorPlugins();
|
|
163
|
+
|
|
164
|
+
expect(plugins).toHaveLength(2);
|
|
165
|
+
expect(plugins[0].appendTransaction).toBeDefined();
|
|
166
|
+
expect(plugins[1].props.handleKeyDown).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('EnsureTextAfterMathPlugin', () => {
|
|
172
|
+
it('inserts a zero-width space after a math node when no text follows', () => {
|
|
173
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
174
|
+
const textNode = { type: { name: 'text' } };
|
|
175
|
+
const mathNode = { type: { name: 'math' }, nodeSize: 3 };
|
|
176
|
+
const tr = { insert: jest.fn() };
|
|
177
|
+
|
|
178
|
+
const newState = {
|
|
179
|
+
schema: { text: jest.fn((value) => ({ type: textNode.type, text: value })) },
|
|
180
|
+
tr,
|
|
181
|
+
doc: {
|
|
182
|
+
descendants: (cb) => cb(mathNode, 5),
|
|
183
|
+
nodeAt: jest.fn(() => null),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const result = plugin.appendTransaction([{ docChanged: true }], {}, newState);
|
|
188
|
+
|
|
189
|
+
expect(tr.insert).toHaveBeenCalledWith(8, expect.anything());
|
|
190
|
+
expect(result).toBe(tr);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('does not insert when text already follows the math node', () => {
|
|
194
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
195
|
+
const tr = { insert: jest.fn() };
|
|
196
|
+
const mathNode = { type: { name: 'math' }, nodeSize: 3 };
|
|
197
|
+
|
|
198
|
+
const newState = {
|
|
199
|
+
schema: { text: jest.fn() },
|
|
200
|
+
tr,
|
|
201
|
+
doc: {
|
|
202
|
+
descendants: (cb) => cb(mathNode, 5),
|
|
203
|
+
nodeAt: jest.fn(() => ({ type: { name: 'text' } })),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = plugin.appendTransaction([{ docChanged: true }], {}, newState);
|
|
208
|
+
|
|
209
|
+
expect(tr.insert).not.toHaveBeenCalled();
|
|
210
|
+
expect(result).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('returns null when the document did not change', () => {
|
|
214
|
+
const plugin = EnsureTextAfterMathPlugin('math');
|
|
215
|
+
|
|
216
|
+
const result = plugin.appendTransaction([{ docChanged: false }], {}, {});
|
|
217
|
+
expect(result).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('ZeroWidthSpaceHandlingPlugin', () => {
|
|
222
|
+
const createDefaultDoc = () => ({
|
|
223
|
+
textBetween: jest.fn(() => '\u200b'),
|
|
224
|
+
resolve: jest.fn(() => ({
|
|
225
|
+
nodeAfter: null,
|
|
226
|
+
nodeBefore: null,
|
|
227
|
+
})),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const createView = ({ state: stateOverrides = {} } = {}) => {
|
|
231
|
+
const dispatch = jest.fn();
|
|
232
|
+
const tr = {
|
|
233
|
+
delete: jest.fn().mockReturnThis(),
|
|
234
|
+
setSelection: jest.fn().mockReturnThis(),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
state: {
|
|
239
|
+
selection: { from: 2, empty: true },
|
|
240
|
+
doc: createDefaultDoc(),
|
|
241
|
+
tr,
|
|
242
|
+
...stateOverrides,
|
|
243
|
+
doc: { ...createDefaultDoc(), ...stateOverrides.doc },
|
|
244
|
+
},
|
|
245
|
+
dispatch,
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
it('deletes math and zero-width space on Backspace', () => {
|
|
250
|
+
const view = createView();
|
|
251
|
+
const event = { key: 'Backspace' };
|
|
252
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, event);
|
|
253
|
+
|
|
254
|
+
expect(handled).toBe(true);
|
|
255
|
+
expect(view.state.tr.delete).toHaveBeenCalledWith(0, 2);
|
|
256
|
+
expect(view.dispatch).toHaveBeenCalledWith(view.state.tr);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('selects the math node on ArrowLeft before a zero-width space', () => {
|
|
260
|
+
const mathNode = { nodeSize: 3 };
|
|
261
|
+
const view = createView({
|
|
262
|
+
state: {
|
|
263
|
+
doc: {
|
|
264
|
+
resolve: jest
|
|
265
|
+
.fn()
|
|
266
|
+
.mockReturnValueOnce({ nodeAfter: mathNode, nodeBefore: null })
|
|
267
|
+
.mockReturnValueOnce({ pos: 4 }),
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const { NodeSelection } = require('prosemirror-state');
|
|
272
|
+
|
|
273
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'ArrowLeft' });
|
|
274
|
+
|
|
275
|
+
expect(handled).toBe(true);
|
|
276
|
+
expect(NodeSelection.create).toHaveBeenCalledWith(view.state.doc, 4);
|
|
277
|
+
expect(view.dispatch).toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('moves the text cursor before the zero-width space when no inline node precedes it', () => {
|
|
281
|
+
const view = createView();
|
|
282
|
+
const { TextSelection } = require('prosemirror-state');
|
|
283
|
+
|
|
284
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'ArrowLeft' });
|
|
285
|
+
|
|
286
|
+
expect(handled).toBe(true);
|
|
287
|
+
expect(TextSelection.create).toHaveBeenCalledWith(view.state.doc, 0);
|
|
288
|
+
expect(view.dispatch).toHaveBeenCalled();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('returns false for unrelated keys', () => {
|
|
292
|
+
const view = createView({
|
|
293
|
+
state: {
|
|
294
|
+
doc: {
|
|
295
|
+
textBetween: jest.fn(() => 'a'),
|
|
296
|
+
resolve: jest.fn(),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const handled = ZeroWidthSpaceHandlingPlugin.props.handleKeyDown(view, { key: 'Enter' });
|
|
302
|
+
expect(handled).toBe(false);
|
|
303
|
+
});
|
|
159
304
|
});
|
|
160
305
|
|
|
161
306
|
describe('MathNodeView', () => {
|
|
@@ -167,6 +312,7 @@ describe('MathNodeView', () => {
|
|
|
167
312
|
},
|
|
168
313
|
tr: {
|
|
169
314
|
setSelection: jest.fn().mockReturnThis(),
|
|
315
|
+
setMeta: jest.fn().mockReturnThis(),
|
|
170
316
|
},
|
|
171
317
|
doc: {},
|
|
172
318
|
},
|
|
@@ -244,37 +390,30 @@ describe('MathNodeView', () => {
|
|
|
244
390
|
});
|
|
245
391
|
|
|
246
392
|
describe('toolbar positioning', () => {
|
|
247
|
-
it('uses
|
|
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} />);
|
|
393
|
+
it('uses a fixed top offset and horizontal position from coordsAtPos', async () => {
|
|
394
|
+
const { container } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
257
395
|
await waitFor(() => {
|
|
258
396
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
259
397
|
expect(toolbar).toBeInTheDocument();
|
|
260
|
-
|
|
261
|
-
expect(toolbar.style.top).toBe('190px');
|
|
398
|
+
expect(toolbar.style.top).toBe('40px');
|
|
262
399
|
expect(toolbar.style.left).toBe('50px');
|
|
263
400
|
});
|
|
264
401
|
});
|
|
265
402
|
|
|
266
|
-
it('
|
|
403
|
+
it('keeps the fixed top offset when the editor container is scrolled', async () => {
|
|
404
|
+
const containerEl = document.createElement('div');
|
|
405
|
+
containerEl.getBoundingClientRect = jest.fn(() => ({ top: -200, left: 0, width: 600, height: 400 }));
|
|
406
|
+
|
|
267
407
|
const editor = {
|
|
268
408
|
...defaultProps.editor,
|
|
269
|
-
_tiptapContainerEl:
|
|
409
|
+
_tiptapContainerEl: containerEl,
|
|
270
410
|
};
|
|
271
411
|
|
|
272
412
|
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
273
413
|
await waitFor(() => {
|
|
274
414
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
275
415
|
expect(toolbar).toBeInTheDocument();
|
|
276
|
-
|
|
277
|
-
expect(toolbar.style.top).toBe('140px');
|
|
416
|
+
expect(toolbar.style.top).toBe('40px');
|
|
278
417
|
expect(toolbar.style.left).toBe('50px');
|
|
279
418
|
});
|
|
280
419
|
});
|
|
@@ -288,7 +427,7 @@ describe('MathNodeView', () => {
|
|
|
288
427
|
});
|
|
289
428
|
});
|
|
290
429
|
|
|
291
|
-
it('
|
|
430
|
+
it('updates horizontal position from coordsAtPos when selection changes', async () => {
|
|
292
431
|
const editor = {
|
|
293
432
|
...defaultProps.editor,
|
|
294
433
|
view: {
|
|
@@ -302,30 +441,11 @@ describe('MathNodeView', () => {
|
|
|
302
441
|
await waitFor(() => {
|
|
303
442
|
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
304
443
|
expect(toolbar).toBeInTheDocument();
|
|
305
|
-
|
|
306
|
-
expect(toolbar.style.top).toBe('240px');
|
|
444
|
+
expect(toolbar.style.top).toBe('40px');
|
|
307
445
|
expect(toolbar.style.left).toBe('150px');
|
|
308
446
|
});
|
|
309
447
|
});
|
|
310
448
|
|
|
311
|
-
it('accounts for negative container top (scrolled container)', async () => {
|
|
312
|
-
const containerEl = document.createElement('div');
|
|
313
|
-
containerEl.getBoundingClientRect = jest.fn(() => ({ top: -200, left: 0, width: 600, height: 400 }));
|
|
314
|
-
|
|
315
|
-
const editor = {
|
|
316
|
-
...defaultProps.editor,
|
|
317
|
-
_tiptapContainerEl: containerEl,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
const { container } = render(<MathNodeView {...defaultProps} editor={editor} selected={true} />);
|
|
321
|
-
await waitFor(() => {
|
|
322
|
-
const toolbar = container.querySelector('[data-toolbar-for]');
|
|
323
|
-
expect(toolbar).toBeInTheDocument();
|
|
324
|
-
// top = 100 + Math.abs(-200) + 40 = 340
|
|
325
|
-
expect(toolbar.style.top).toBe('340px');
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
449
|
it('portals toolbar into _tiptapContainerEl when available', async () => {
|
|
330
450
|
const containerEl = document.createElement('div');
|
|
331
451
|
containerEl.getBoundingClientRect = jest.fn(() => ({ top: 0, left: 0, width: 600, height: 400 }));
|
|
@@ -410,8 +530,13 @@ describe('MathNodeView', () => {
|
|
|
410
530
|
});
|
|
411
531
|
});
|
|
412
532
|
|
|
413
|
-
it('closes toolbar on outside click', async () => {
|
|
414
|
-
const
|
|
533
|
+
it('closes toolbar on outside click and runs handleDone', async () => {
|
|
534
|
+
const updateAttributes = jest.fn();
|
|
535
|
+
const editor = createMockEditor();
|
|
536
|
+
const { TextSelection } = require('prosemirror-state');
|
|
537
|
+
const { queryByTestId } = render(
|
|
538
|
+
<MathNodeView {...defaultProps} editor={editor} updateAttributes={updateAttributes} selected={true} />,
|
|
539
|
+
);
|
|
415
540
|
|
|
416
541
|
await waitFor(() => {
|
|
417
542
|
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
@@ -421,6 +546,86 @@ describe('MathNodeView', () => {
|
|
|
421
546
|
|
|
422
547
|
await waitFor(() => {
|
|
423
548
|
expect(queryByTestId('math-toolbar')).not.toBeInTheDocument();
|
|
549
|
+
expect(updateAttributes).toHaveBeenCalledWith({ latex: 'x^2' });
|
|
550
|
+
expect(TextSelection.create).toHaveBeenCalledWith(editor.state.doc, 1);
|
|
551
|
+
expect(editor.state.tr.setSelection).toHaveBeenCalled();
|
|
552
|
+
expect(editor.view.dispatch).toHaveBeenCalledWith(editor.state.tr);
|
|
553
|
+
expect(editor.commands.focus).toHaveBeenCalled();
|
|
554
|
+
expect(editor._toolbarOpened).toBe(false);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('does not close toolbar when clicking the math node preview', async () => {
|
|
559
|
+
const { getByTestId, queryByTestId } = render(<MathNodeView {...defaultProps} selected={true} />);
|
|
560
|
+
|
|
561
|
+
await waitFor(() => {
|
|
562
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
fireEvent.click(getByTestId('math-preview'));
|
|
566
|
+
|
|
567
|
+
await waitFor(() => {
|
|
568
|
+
expect(queryByTestId('math-toolbar')).toBeInTheDocument();
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe('unique math-node class per instance', () => {
|
|
573
|
+
let dateNowSpy;
|
|
574
|
+
|
|
575
|
+
beforeEach(() => {
|
|
576
|
+
let timestamp = 1000;
|
|
577
|
+
dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => timestamp++);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
afterEach(() => {
|
|
581
|
+
dateNowSpy.mockRestore();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('assigns a timestamp-based class to NodeViewWrapper', () => {
|
|
585
|
+
const { getByTestId } = render(<MathNodeView {...defaultProps} />);
|
|
586
|
+
const wrapper = getByTestId('node-view-wrapper');
|
|
587
|
+
|
|
588
|
+
expect(wrapper.className).toMatch(/^math-node-\d+$/);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('assigns a different class to each MathNodeView instance', () => {
|
|
592
|
+
const { getAllByTestId } = render(
|
|
593
|
+
<>
|
|
594
|
+
<MathNodeView {...defaultProps} />
|
|
595
|
+
<MathNodeView {...defaultProps} node={{ attrs: { latex: 'y^2' } }} />
|
|
596
|
+
</>,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const wrappers = getAllByTestId('node-view-wrapper');
|
|
600
|
+
expect(wrappers[0].className).toBe('math-node-1000');
|
|
601
|
+
expect(wrappers[1].className).toBe('math-node-1001');
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('closes toolbar when clicking a different math node', async () => {
|
|
605
|
+
const updateAttributes = jest.fn();
|
|
606
|
+
const editorA = createMockEditor();
|
|
607
|
+
const editorB = createMockEditor();
|
|
608
|
+
|
|
609
|
+
const { getAllByTestId, queryAllByTestId } = render(
|
|
610
|
+
<>
|
|
611
|
+
<MathNodeView {...defaultProps} editor={editorA} updateAttributes={updateAttributes} selected={true} />
|
|
612
|
+
<MathNodeView {...defaultProps} editor={editorB} node={{ attrs: { latex: 'y^2' } }} selected={false} />
|
|
613
|
+
</>,
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
await waitFor(() => {
|
|
617
|
+
expect(queryAllByTestId('math-toolbar')).toHaveLength(1);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const secondPreview = getAllByTestId('math-preview')[1];
|
|
621
|
+
fireEvent.click(secondPreview);
|
|
622
|
+
|
|
623
|
+
await waitFor(() => {
|
|
624
|
+
expect(queryAllByTestId('math-toolbar')).toHaveLength(1);
|
|
625
|
+
expect(updateAttributes).toHaveBeenCalledWith({ latex: 'x^2' });
|
|
626
|
+
expect(editorA._toolbarOpened).toBe(false);
|
|
627
|
+
expect(getAllByTestId('math-input')[0]).toHaveValue('y^2');
|
|
628
|
+
});
|
|
424
629
|
});
|
|
425
630
|
});
|
|
426
631
|
|
package/src/extensions/math.js
CHANGED
|
@@ -5,6 +5,7 @@ import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
|
|
|
5
5
|
import { NodeSelection, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
|
|
6
6
|
import { MathPreview, MathToolbar } from '@pie-lib/math-toolbar';
|
|
7
7
|
import { wrapMath } from '@pie-lib/math-rendering';
|
|
8
|
+
import { setToolbarOpened } from '../utils/toolbar';
|
|
8
9
|
|
|
9
10
|
const ensureTextAfterMathPluginKey = new PluginKey('ensureTextAfterMath');
|
|
10
11
|
|
|
@@ -183,6 +184,7 @@ export const MathNodeView = (props) => {
|
|
|
183
184
|
const { node, updateAttributes, editor, selected, options } = props;
|
|
184
185
|
const [showToolbar, setShowToolbar] = useState(selected);
|
|
185
186
|
const toolbarRef = useRef(null);
|
|
187
|
+
const timestamp = useRef(Date.now());
|
|
186
188
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
187
189
|
const { math: mathOptions = {} } = options || {};
|
|
188
190
|
const {
|
|
@@ -195,6 +197,23 @@ export const MathNodeView = (props) => {
|
|
|
195
197
|
|
|
196
198
|
const latex = node.attrs.latex || '';
|
|
197
199
|
|
|
200
|
+
const handleChange = (newLatex) => {
|
|
201
|
+
updateAttributes({ latex: newLatex });
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleDone = (newLatex) => {
|
|
205
|
+
updateAttributes({ latex: newLatex });
|
|
206
|
+
setShowToolbar(false);
|
|
207
|
+
|
|
208
|
+
const { selection, tr, doc } = editor.state;
|
|
209
|
+
const sel = TextSelection.create(doc, selection.from + 1);
|
|
210
|
+
|
|
211
|
+
// Build a fresh transaction from the current state and set the selection
|
|
212
|
+
tr.setSelection(sel);
|
|
213
|
+
editor.view.dispatch(tr);
|
|
214
|
+
editor.commands.focus();
|
|
215
|
+
};
|
|
216
|
+
|
|
198
217
|
useEffect(() => {
|
|
199
218
|
if (selected) {
|
|
200
219
|
setShowToolbar(true);
|
|
@@ -202,17 +221,15 @@ export const MathNodeView = (props) => {
|
|
|
202
221
|
}, [selected]);
|
|
203
222
|
|
|
204
223
|
useEffect(() => {
|
|
205
|
-
editor
|
|
206
|
-
}, [showToolbar]);
|
|
224
|
+
setToolbarOpened(editor, showToolbar);
|
|
225
|
+
}, [editor, showToolbar]);
|
|
207
226
|
|
|
208
227
|
useEffect(() => {
|
|
209
228
|
// Calculate position relative to selection
|
|
210
|
-
const container = editor?._tiptapContainerEl || document.body;
|
|
211
|
-
const bodyRect = container.getBoundingClientRect();
|
|
212
229
|
const { from } = editor.state.selection;
|
|
213
230
|
const start = editor.view.coordsAtPos(from);
|
|
214
231
|
setPosition({
|
|
215
|
-
top:
|
|
232
|
+
top: 40, // shift above
|
|
216
233
|
left: start.left,
|
|
217
234
|
});
|
|
218
235
|
|
|
@@ -236,7 +253,7 @@ export const MathNodeView = (props) => {
|
|
|
236
253
|
// will keep/re-open the toolbar. Without this guard, closing and then
|
|
237
254
|
// immediately clicking the math node would fire this listener in the same
|
|
238
255
|
// event cycle and close the toolbar before it could open.
|
|
239
|
-
const clickedMathNode = !!target?.closest?.(
|
|
256
|
+
const clickedMathNode = !!target?.closest?.(`.math-node-${timestamp.current}`);
|
|
240
257
|
|
|
241
258
|
if (
|
|
242
259
|
toolbarRef.current &&
|
|
@@ -247,6 +264,7 @@ export const MathNodeView = (props) => {
|
|
|
247
264
|
!clickedMathNode
|
|
248
265
|
) {
|
|
249
266
|
setShowToolbar(false);
|
|
267
|
+
handleDone(node.attrs.latex);
|
|
250
268
|
}
|
|
251
269
|
};
|
|
252
270
|
|
|
@@ -261,28 +279,9 @@ export const MathNodeView = (props) => {
|
|
|
261
279
|
return () => document.removeEventListener('click', handleClickOutside);
|
|
262
280
|
}, [editor, showToolbar]);
|
|
263
281
|
|
|
264
|
-
const handleChange = (newLatex) => {
|
|
265
|
-
updateAttributes({ latex: newLatex });
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const handleDone = (newLatex) => {
|
|
269
|
-
updateAttributes({ latex: newLatex });
|
|
270
|
-
setShowToolbar(false);
|
|
271
|
-
|
|
272
|
-
editor._toolbarOpened = false;
|
|
273
|
-
|
|
274
|
-
const { selection, tr, doc } = editor.state;
|
|
275
|
-
const sel = TextSelection.create(doc, selection.from + 1);
|
|
276
|
-
|
|
277
|
-
// Build a fresh transaction from the current state and set the selection
|
|
278
|
-
tr.setSelection(sel);
|
|
279
|
-
editor.view.dispatch(tr);
|
|
280
|
-
editor.commands.focus();
|
|
281
|
-
};
|
|
282
|
-
|
|
283
282
|
return (
|
|
284
283
|
<NodeViewWrapper
|
|
285
|
-
className=
|
|
284
|
+
className={`math-node-${timestamp.current}`}
|
|
286
285
|
style={{
|
|
287
286
|
display: 'inline-flex',
|
|
288
287
|
cursor: 'pointer',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { setToolbarOpened, TOOLBAR_OPENED_META_KEY } from '../toolbar';
|
|
2
|
+
|
|
3
|
+
describe('setToolbarOpened', () => {
|
|
4
|
+
const createEditor = (toolbarOpened = false) => {
|
|
5
|
+
const tr = {
|
|
6
|
+
setMeta: jest.fn().mockReturnThis(),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
_toolbarOpened: toolbarOpened,
|
|
11
|
+
state: { tr },
|
|
12
|
+
view: {
|
|
13
|
+
dispatch: jest.fn(),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
it('sets editor._toolbarOpened and dispatches a meta transaction', () => {
|
|
19
|
+
const editor = createEditor(false);
|
|
20
|
+
|
|
21
|
+
setToolbarOpened(editor, true);
|
|
22
|
+
|
|
23
|
+
expect(editor._toolbarOpened).toBe(true);
|
|
24
|
+
expect(editor.state.tr.setMeta).toHaveBeenCalledWith(TOOLBAR_OPENED_META_KEY, true);
|
|
25
|
+
expect(editor.view.dispatch).toHaveBeenCalledWith(editor.state.tr);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('does nothing when the value is unchanged', () => {
|
|
29
|
+
const editor = createEditor(true);
|
|
30
|
+
|
|
31
|
+
setToolbarOpened(editor, true);
|
|
32
|
+
|
|
33
|
+
expect(editor.view.dispatch).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('coerces the opened value to a boolean', () => {
|
|
37
|
+
const editor = createEditor(false);
|
|
38
|
+
|
|
39
|
+
setToolbarOpened(editor, 1);
|
|
40
|
+
|
|
41
|
+
expect(editor._toolbarOpened).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const TOOLBAR_OPENED_META_KEY = 'toolbarOpenedChanged';
|
|
2
|
+
|
|
3
|
+
export const setToolbarOpened = (editor, opened) => {
|
|
4
|
+
const next = !!opened;
|
|
5
|
+
|
|
6
|
+
if (editor._toolbarOpened === next) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
editor._toolbarOpened = next;
|
|
11
|
+
|
|
12
|
+
if (editor?.view && editor?.state?.tr) {
|
|
13
|
+
editor.view.dispatch(editor.state.tr.setMeta(TOOLBAR_OPENED_META_KEY, true));
|
|
14
|
+
}
|
|
15
|
+
};
|