diff --git a/services/web/frontend/js/features/source-editor/extensions/context-menu.ts b/services/web/frontend/js/features/source-editor/extensions/context-menu.ts index 834484372b..b4a0aa66e9 100644 --- a/services/web/frontend/js/features/source-editor/extensions/context-menu.ts +++ b/services/web/frontend/js/features/source-editor/extensions/context-menu.ts @@ -17,6 +17,9 @@ import { } from '@codemirror/state' import { closeAllContextMenusEffect } from '../utils/close-all-context-menus-effect' import { isContextMenuMouseEvent } from '../utils/context-menu-mouse-event' +import { isMobileDevice } from '../utils/isMobileDevice' + +const isMobile = isMobileDevice() export const openContextMenuEffect = StateEffect.define<{ pos: number @@ -28,11 +31,6 @@ export const closeContextMenuEffect = StateEffect.define() export const openContextMenuAnnotation = Annotation.define() -const isTouchOnlyInput = - typeof window.matchMedia === 'function' && - window.matchMedia('(pointer: coarse)').matches && - window.matchMedia('(hover: none)').matches - type ContextMenuState = { tooltip: Tooltip | null mousePosition: { x: number; y: number } | null @@ -236,7 +234,7 @@ function isClickOnGutter(target: HTMLElement): boolean { // Gutter context menu plugin const gutterContextMenuPlugin = (): Extension => EditorView.updateListener.of(update => { - if (isTouchOnlyInput || !update.view.dom.parentElement) { + if (!update.view.dom.parentElement) { return } @@ -275,12 +273,8 @@ const gutterContextMenuPlugin = (): Extension => // Handle right-click on ol-cm-filler (empty line widget) // domEventHandlers doesn't fire for contenteditable="false" elements, so we use a direct DOM listener -const emptyLineFillerContextMenuPlugin = (): Extension => { - if (isTouchOnlyInput) { - return [] - } - - return ViewPlugin.define(view => { +const emptyLineFillerContextMenuPlugin = (): Extension => + ViewPlugin.define(view => { const contentDOM = view.contentDOM const handleContextMenu = (event: Event) => { @@ -313,16 +307,11 @@ const emptyLineFillerContextMenuPlugin = (): Extension => { }, } }) -} // Editor view context menu handlers const editorContextMenuHandlers = (): Extension => EditorView.domEventHandlers({ contextmenu(event: MouseEvent, view: EditorView) { - if (isTouchOnlyInput) { - return false - } - const pos = view.posAtCoords({ x: event.clientX, y: event.clientY }) if (pos === null) { return false @@ -361,8 +350,7 @@ const editorContextMenuHandlers = (): Extension => } // Prevent default on right-click to preserve selection - // But not on touch devices - they need native selection behavior - if (isRightClick && !isTouchOnlyInput) { + if (isRightClick) { event.preventDefault() return true } @@ -394,7 +382,7 @@ const contextMenuKeymap = (): Extension => ) export const contextMenu = (enabled: boolean): Extension => - enabled + enabled && !isMobile ? [ contextMenuContainerTheme, contextMenuStateField, diff --git a/services/web/frontend/js/features/source-editor/utils/isMobileDevice.ts b/services/web/frontend/js/features/source-editor/utils/isMobileDevice.ts new file mode 100644 index 0000000000..d7e8753339 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/utils/isMobileDevice.ts @@ -0,0 +1,15 @@ +import browser from '../extensions/browser' + +export function isMobileDevice(): boolean { + const ua = typeof navigator !== 'undefined' ? navigator.userAgent : '' + const isMobileUserAgent = /Android|iPhone|iPad|Mobile/i.test(ua) + + // Input-capability fallback. + const isTouchOnlyInput = + typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia('(pointer: coarse)').matches && + window.matchMedia('(hover: none)').matches + + return browser.ios || browser.android || isMobileUserAgent || isTouchOnlyInput +}