Skip to content

Commit d31f084

Browse files
committed
revert: undo context-menu paste fix attempts
Reverts the paste whitespace/selection fixes (3a9a980, 1912016, bafd1ce). The underlying issue β€” ProseMirror stripping trailing whitespace during paste into run-wrapped content β€” needs deeper investigation. Tracked separately.
1 parent 61da0f1 commit d31f084

5 files changed

Lines changed: 46 additions & 51 deletions

File tree

β€Žpackages/super-editor/src/components/context-menu/ContextMenu.vueβ€Ž

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,14 +296,21 @@ const handleRightClick = async (event) => {
296296
297297
event.preventDefault();
298298
299-
// Update cursor position to the right-click location before opening context menu.
300-
// When the user has a range selection, keep it β€” coordinate-based hit testing
301-
// in presentation mode can misreport whether the click is inside the selection,
302-
// which would collapse it unexpectedly.
299+
// Update cursor position to the right-click location before opening context menu,
300+
// unless the click lands inside an active selection (keep selection intact).
303301
const editorState = props.editor?.state;
304302
const hasRangeSelection = editorState?.selection?.from !== editorState?.selection?.to;
303+
let isClickInsideSelection = false;
305304
306-
if (!hasRangeSelection) {
305+
if (hasRangeSelection && Number.isFinite(event.clientX) && Number.isFinite(event.clientY)) {
306+
const hit = props.editor?.posAtCoords?.({ left: event.clientX, top: event.clientY });
307+
if (typeof hit?.pos === 'number') {
308+
const { from, to } = editorState.selection;
309+
isClickInsideSelection = hit.pos >= from && hit.pos <= to;
310+
}
311+
}
312+
313+
if (!isClickInsideSelection) {
307314
moveCursorToMouseEvent(event, props.editor);
308315
}
309316

β€Žpackages/super-editor/src/components/context-menu/menuItems.jsβ€Ž

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -300,14 +300,19 @@ export function getItems(context, customItems = [], includeDefaultItems = true)
300300
label: TEXTS.paste,
301301
icon: ICONS.paste,
302302
isDefault: true,
303-
action: async (editor, context) => {
303+
action: async (editor) => {
304304
const { view } = editor ?? {};
305305
if (!view) return;
306-
// Use the selection captured when the context menu opened β€” the
307-
// right-click handler may have collapsed a range selection via
308-
// moveCursorToMouseEvent before this action runs.
309-
const savedFrom = context?.selectionStart ?? view.state.selection.from;
310-
const savedTo = context?.selectionEnd ?? view.state.selection.to;
306+
// Save the current selection before focusing. When the context menu
307+
// is open, its hidden search input holds focus, so the PM editor's
308+
// contenteditable is blurred. A raw `view.dom.focus()` would restart
309+
// ProseMirror's DOMObserver which reads the stale browser selection
310+
// (collapsed at the document start) and overwrites the PM state.
311+
// Using `view.focus()` (ProseMirror-aware) prevents this by writing
312+
// the PM selection to the DOM before restarting the observer. We also
313+
// save/restore as a safety net against async drift during clipboard reads.
314+
const savedFrom = view.state.selection.from;
315+
const savedTo = view.state.selection.to;
311316
view.focus();
312317
const { html, text } = await readClipboardRaw();
313318
// Restore selection after the async gap β€” ProseMirror's DOMObserver
@@ -323,20 +328,22 @@ export function getItems(context, customItems = [], includeDefaultItems = true)
323328
view.dispatch(tr.setSelection(SelectionType.create(doc, safeFrom, safeTo)));
324329
}
325330
}
326-
// When plain text is available, prefer view.pasteText β€” it preserves
327-
// whitespace correctly. Chromium wraps writeText() output in HTML,
328-
// which routes through handleHtmlPaste and strips leading/trailing spaces.
329-
// Still run handleClipboardPaste for URL detection (passes empty html
330-
// so it hits the 'plain-text' branch).
331-
if (text) {
332-
const urlHandled = handleClipboardPaste({ editor, view }, '', text);
333-
if (!urlHandled && typeof view.pasteText === 'function') {
334-
view.pasteText(text, createPasteEventShim({ html: '', text }));
331+
const handled = handleClipboardPaste({ editor, view }, html, text);
332+
if (!handled) {
333+
const pasteEvent = createPasteEventShim({ html, text });
334+
335+
if (html && typeof view.pasteHTML === 'function') {
336+
view.pasteHTML(html, pasteEvent);
337+
return;
338+
}
339+
340+
if (text && typeof view.pasteText === 'function') {
341+
view.pasteText(text, pasteEvent);
342+
return;
335343
}
336-
} else if (html) {
337-
const handled = handleClipboardPaste({ editor, view }, html, '');
338-
if (!handled && typeof view.pasteHTML === 'function') {
339-
view.pasteHTML(html, createPasteEventShim({ html, text: '' }));
344+
345+
if (text && editor.commands?.insertContent) {
346+
editor.commands.insertContent(text, { contentType: 'text' });
340347
}
341348
}
342349
},

β€Žpackages/super-editor/src/components/context-menu/tests/ContextMenu.test.jsβ€Ž

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,7 @@ describe('ContextMenu.vue', () => {
294294
expect(moveCursorToMouseEvent).not.toHaveBeenCalled();
295295
});
296296

297-
it('should not move cursor when right-click happens outside a range selection', async () => {
298-
// When a range selection exists, the context menu preserves it β€” coordinate-based
299-
// hit testing in presentation mode can misreport whether the click is inside the
300-
// selection, which would collapse it unexpectedly.
297+
it('should move cursor when right-click happens outside the active selection', async () => {
301298
mount(ContextMenu, { props: mockProps });
302299

303300
const { moveCursorToMouseEvent } = await import('../../cursor-helpers.js');
@@ -316,7 +313,7 @@ describe('ContextMenu.vue', () => {
316313

317314
await contextMenuHandler(rightClickEvent);
318315

319-
expect(moveCursorToMouseEvent).not.toHaveBeenCalled();
316+
expect(moveCursorToMouseEvent).toHaveBeenCalledWith(rightClickEvent, mockEditor);
320317
});
321318

322319
it('should allow native context menu when modifier is pressed', async () => {

β€Žpackages/super-editor/src/components/context-menu/tests/menuItems.test.jsβ€Ž

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -567,16 +567,12 @@ describe('menuItems.js', () => {
567567
});
568568

569569
describe('getItems - paste action behavior', () => {
570-
it('should prefer pasteText over pasteHTML when both html and text are available', async () => {
571-
// When plain text is available, the paste action prefers view.pasteText
572-
// because Chromium wraps writeText() output in HTML, which routes through
573-
// handleHtmlPaste and strips leading/trailing spaces.
570+
it('should not force plain-text insert when HTML paste is unhandled', async () => {
574571
const insertContent = vi.fn();
575572
mockEditor = createMockEditor({
576573
commands: { insertContent },
577574
});
578575
mockEditor.view.dom.focus = vi.fn();
579-
mockEditor.view.pasteText = vi.fn();
580576
mockEditor.view.pasteHTML = vi.fn();
581577
mockContext = createMockContext({
582578
editor: mockEditor,
@@ -597,14 +593,12 @@ describe('menuItems.js', () => {
597593
expect(pasteAction).toBeTypeOf('function');
598594
await pasteAction(mockEditor);
599595

600-
// Passes empty html to handleClipboardPaste for URL detection only
601596
expect(clipboardMocks.handleClipboardPaste).toHaveBeenCalledWith(
602597
{ editor: mockEditor, view: mockEditor.view },
603-
'',
598+
'<p>word html</p>',
604599
'word html',
605600
);
606-
expect(mockEditor.view.pasteText).toHaveBeenCalledWith('word html', expect.any(Object));
607-
expect(mockEditor.view.pasteHTML).not.toHaveBeenCalled();
601+
expect(mockEditor.view.pasteHTML).toHaveBeenCalledWith('<p>word html</p>', expect.any(Object));
608602
expect(insertContent).not.toHaveBeenCalled();
609603
});
610604

@@ -637,15 +631,11 @@ describe('menuItems.js', () => {
637631
expect(insertContent).not.toHaveBeenCalled();
638632
});
639633

640-
it('should not paste when view has no pasteText and handleClipboardPaste returns false', async () => {
641-
// When pasteText is unavailable and URL detection returns false,
642-
// the text branch has no further action. The code does not fall
643-
// back to insertContent for the text branch.
634+
it('should fall back to insertContent when view has no pasteHTML or pasteText', async () => {
644635
const insertContent = vi.fn();
645636
mockEditor = createMockEditor({
646637
commands: { insertContent },
647638
});
648-
mockEditor.view.dom.focus = vi.fn();
649639
// No pasteHTML or pasteText on view
650640
delete mockEditor.view.pasteHTML;
651641
delete mockEditor.view.pasteText;
@@ -667,13 +657,7 @@ describe('menuItems.js', () => {
667657

668658
await pasteAction(mockEditor);
669659

670-
expect(clipboardMocks.handleClipboardPaste).toHaveBeenCalledWith(
671-
{ editor: mockEditor, view: mockEditor.view },
672-
'',
673-
'fallback text',
674-
);
675-
// No pasteText available, URL detection returned false β€” nothing else happens
676-
expect(insertContent).not.toHaveBeenCalled();
660+
expect(insertContent).toHaveBeenCalledWith('fallback text', { contentType: 'text' });
677661
});
678662
});
679663

β€Žpackages/super-editor/src/core/InputRule.jsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ export function handleHtmlPaste(html, editor, source) {
348348
cleanedHtml.dataset.superdocImport = 'true';
349349
}
350350

351-
let doc = PMDOMParser.fromSchema(editor.schema).parse(cleanedHtml, { preserveWhitespace: true });
351+
let doc = PMDOMParser.fromSchema(editor.schema).parse(cleanedHtml);
352352
doc = mergeAdjacentTableFragments(doc);
353353

354354
doc = wrapTextsInRuns(doc);

0 commit comments

Comments
Β (0)