Merge pull request #30495 from overleaf/mg-context-menu-paste

[web] Support paste with formatting in context menu

GitOrigin-RevId: 551ed1d49ca423395bd9bfc756e10e8d59d71ecd
This commit is contained in:
Malik Glossop
2026-01-15 14:19:03 +00:00
committed by Copybot
parent f087d125c1
commit a0fc14b367
8 changed files with 390 additions and 114 deletions
@@ -81,6 +81,27 @@ function isPositionInsideSelection(pos: number, from: number, to: number) {
return from !== to && pos >= from && pos <= to
}
function isPositionInsideAnyRangeOrCursor(view: EditorView, pos: number) {
for (const range of view.state.selection.ranges) {
// If it's a cursor, treat a right-click anywhere on the same line as "inside".
// This avoids collapsing multi-cursor selections when right-clicking on blank lines
// or to the right of the caret.
if (range.from === range.to) {
const clickedLine = view.state.doc.lineAt(pos)
const cursorLine = view.state.doc.lineAt(range.from)
if (clickedLine.number === cursorLine.number) {
return true
}
continue
}
if (isPositionInsideSelection(pos, range.from, range.to)) {
return true
}
}
return false
}
function selectEntireLine(
view: EditorView,
pos: number
@@ -171,8 +192,7 @@ const editorContextMenuHandlers = (): Extension =>
return false
}
const { from, to } = view.state.selection.main
const clickedInsideSelection = isPositionInsideSelection(pos, from, to)
const clickedInsideSelection = isPositionInsideAnyRangeOrCursor(view, pos)
// Set cursor to clicked position if outside selection
let selection: TransactionSpec['selection'] = { anchor: pos }
@@ -45,35 +45,17 @@ export const pasteHtml = [
return false
}
// convert the HTML to LaTeX
try {
const parser = new DOMParser()
const { documentElement } = parser.parseFromString(html, 'text/html')
const latex = convertHtmlStringToLatex(
html,
clipboardData.files.length
)
// fall back to creating a figure when there's an image on the clipoard,
// unless the HTML indicates that it came from an Office application
// (which also puts an image on the clipboard)
if (
clipboardData.files.length > 0 &&
!hasProgId(documentElement) &&
!isOnlyTable(documentElement)
) {
// if there's no latex conversion, use plain text version
if (latex === null) {
return false
}
const bodyElement = documentElement.querySelector('body')
// DOMParser should always create a body element, so this is mostly for TypeScript
if (!bodyElement) {
return false
}
// if the only content is in a code block, use the plain text version
if (onlyCode(bodyElement)) {
return false
}
const latex = htmlToLaTeX(bodyElement)
// if there's no formatting, use the plain text version
if (latex === text && clipboardData.files.length === 0) {
return false
@@ -95,6 +77,37 @@ export const pasteHtml = [
pastedContent,
]
export function convertHtmlStringToLatex(
html: string,
filesLength: number
): string | null {
const parser = new DOMParser()
const { documentElement } = parser.parseFromString(html, 'text/html')
// Do not process HTML as LaTeX when the clipboard contains files (e.g. images),
// unless the HTML is from an Office application or is a table-only selection.
if (
filesLength > 0 &&
!hasProgId(documentElement) &&
!isOnlyTable(documentElement)
) {
return null
}
const bodyElement = documentElement.querySelector('body')
// DOMParser should always create a body element, so this is mostly for TypeScript
if (!bodyElement) {
return null
}
// If the only content is a code block, skip latex conversion
if (onlyCode(bodyElement)) {
return null
}
return htmlToLaTeX(bodyElement)
}
const removeUnwantedElements = (
documentElement: HTMLElement,
selector: string