2020-11-14 00:02:17 +00:00
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types' ;
import { PluginStates } from '@joplin/lib/services/plugins/reducer' ;
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService' ;
import { useEffect } from 'react' ;
import bridge from '../../../../../services/bridge' ;
2022-03-28 21:40:29 +05:30
import { ContextMenuOptions , ContextMenuItemType } from '../../../utils/contextMenuUtils' ;
import { menuItems } from '../../../utils/contextMenu' ;
2020-11-14 00:02:17 +00:00
import MenuUtils from '@joplin/lib/services/commands/MenuUtils' ;
import CommandService from '@joplin/lib/services/CommandService' ;
2021-01-07 21:44:31 +00:00
import Setting from '@joplin/lib/models/Setting' ;
2024-11-08 07:32:05 -08:00
import type { Event as ElectronEvent } from 'electron' ;
2020-11-14 00:02:17 +00:00
2021-01-22 17:41:11 +00:00
import Resource from '@joplin/lib/models/Resource' ;
2023-02-15 10:59:32 -03:00
import { TinyMceEditorEvents } from './types' ;
2024-01-26 19:11:05 +00:00
import { HtmlToMarkdownHandler , MarkupToHtmlHandler } from '../../../utils/types' ;
2024-11-08 07:32:05 -08:00
import { Editor } from 'tinymce' ;
2020-11-14 00:02:17 +00:00
2024-11-08 07:32:05 -08:00
const Menu = bridge ( ) . Menu ;
const MenuItem = bridge ( ) . MenuItem ;
2020-11-14 00:02:17 +00:00
const menuUtils = new MenuUtils ( CommandService . instance ( ) ) ;
// x and y are the absolute coordinates, as returned by the context-menu event
// handler on the webContent. This function will return null if the point is
// not within the TinyMCE editor.
2024-11-08 07:32:05 -08:00
function contextMenuElement ( editor : Editor , x : number , y : number ) {
2020-11-14 00:02:17 +00:00
if ( ! editor || ! editor . getDoc ( ) ) return null ;
2024-11-08 07:32:05 -08:00
const containerDoc = editor . getContainer ( ) . ownerDocument ;
const iframes = containerDoc . getElementsByClassName ( 'tox-edit-area__iframe' ) ;
2020-11-14 00:02:17 +00:00
if ( ! iframes . length ) return null ;
2024-02-19 02:04:54 -08:00
const zoom = Setting . value ( 'windowContentZoomFactor' ) / 100 ;
const xScreen = x / zoom ;
const yScreen = y / zoom ;
2020-11-14 00:02:17 +00:00
2024-02-19 02:04:54 -08:00
// We use .elementFromPoint to handle the case where a dialog is covering
// part of the editor.
2024-11-08 07:32:05 -08:00
const targetElement = containerDoc . elementFromPoint ( xScreen , yScreen ) ;
2024-02-19 02:04:54 -08:00
if ( targetElement !== iframes [ 0 ] ) {
return null ;
2020-11-14 00:02:17 +00:00
}
2024-02-19 02:04:54 -08:00
const iframeRect = iframes [ 0 ] . getBoundingClientRect ( ) ;
const relativeX = xScreen - iframeRect . left ;
const relativeY = yScreen - iframeRect . top ;
return editor . getDoc ( ) . elementFromPoint ( relativeX , relativeY ) ;
2020-11-14 00:02:17 +00:00
}
interface ContextMenuActionOptions {
current : ContextMenuOptions ;
}
const contextMenuActionOptions : ContextMenuActionOptions = { current : null } ;
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
2024-11-08 07:32:05 -08:00
export default function ( editor : Editor , plugins : PluginStates , dispatch : Function , htmlToMd : HtmlToMarkdownHandler , mdToHtml : MarkupToHtmlHandler ) {
2020-11-14 00:02:17 +00:00
useEffect ( ( ) = > {
if ( ! editor ) return ( ) = > { } ;
2024-01-26 19:11:05 +00:00
const contextMenuItems = menuItems ( dispatch , htmlToMd , mdToHtml ) ;
2024-11-08 07:32:05 -08:00
const targetWindow = bridge ( ) . activeWindow ( ) ;
2020-11-14 00:02:17 +00:00
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2024-11-08 07:32:05 -08:00
function onContextMenu ( event : ElectronEvent , params : any ) {
2020-11-14 00:02:17 +00:00
const element = contextMenuElement ( editor , params . x , params . y ) ;
if ( ! element ) return ;
2024-11-08 07:32:05 -08:00
event . preventDefault ( ) ;
const menu = new Menu ( ) ;
2020-11-14 00:02:17 +00:00
let itemType : ContextMenuItemType = ContextMenuItemType . None ;
let resourceId = '' ;
let linkToCopy = null ;
if ( element . nodeName === 'IMG' ) {
itemType = ContextMenuItemType . Image ;
2024-11-08 07:32:05 -08:00
resourceId = Resource . pathToId ( ( element as HTMLImageElement ) . src ) ;
2020-11-14 00:02:17 +00:00
} else if ( element . nodeName === 'A' ) {
2024-11-08 07:32:05 -08:00
resourceId = Resource . pathToId ( ( element as HTMLAnchorElement ) . href ) ;
2020-11-14 00:02:17 +00:00
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link ;
linkToCopy = element . getAttribute ( 'href' ) || '' ;
} else {
itemType = ContextMenuItemType . Text ;
}
contextMenuActionOptions . current = {
itemType ,
resourceId ,
2022-03-28 21:40:29 +05:30
filename : null ,
mime : null ,
2020-11-14 00:02:17 +00:00
linkToCopy ,
textToCopy : null ,
htmlToCopy : editor.selection ? editor . selection . getContent ( ) : '' ,
insertContent : ( content : string ) = > {
editor . insertContent ( content ) ;
} ,
isReadOnly : false ,
2023-02-15 10:59:32 -03:00
fireEditorEvent : ( event : TinyMceEditorEvents ) = > {
editor . fire ( event ) ;
} ,
2024-01-26 19:11:05 +00:00
htmlToMd ,
mdToHtml ,
2020-11-14 00:02:17 +00:00
} ;
for ( const itemName in contextMenuItems ) {
const item = contextMenuItems [ itemName ] ;
if ( ! item . isActive ( itemType , contextMenuActionOptions . current ) ) continue ;
2024-11-08 07:32:05 -08:00
menu . append ( new MenuItem ( {
2020-11-14 00:02:17 +00:00
label : item.label ,
click : ( ) = > {
item . onAction ( contextMenuActionOptions . current ) ;
} ,
2024-11-08 07:32:05 -08:00
} ) ) ;
2020-11-14 00:02:17 +00:00
}
const spellCheckerMenuItems = SpellCheckerService . instance ( ) . contextMenuItems ( params . misspelledWord , params . dictionarySuggestions ) ;
for ( const item of spellCheckerMenuItems ) {
2024-11-08 07:32:05 -08:00
menu . append ( new MenuItem ( item ) ) ;
2020-11-14 00:02:17 +00:00
}
2024-11-08 07:32:05 -08:00
for ( const item of menuUtils . pluginContextMenuItems ( plugins , MenuItemLocation . EditorContextMenu ) ) {
menu . append ( new MenuItem ( item ) ) ;
}
2020-11-14 00:02:17 +00:00
2024-11-08 07:32:05 -08:00
menu . popup ( { window : targetWindow } ) ;
2020-11-14 00:02:17 +00:00
}
2024-11-08 07:32:05 -08:00
targetWindow . webContents . prependListener ( 'context-menu' , onContextMenu ) ;
2020-11-14 00:02:17 +00:00
return ( ) = > {
2024-11-08 07:32:05 -08:00
if ( ! targetWindow . isDestroyed ( ) && targetWindow ? . webContents ? . off ) {
targetWindow . webContents . off ( 'context-menu' , onContextMenu ) ;
2020-11-14 00:02:17 +00:00
}
} ;
2024-01-26 19:11:05 +00:00
} , [ editor , plugins , dispatch , htmlToMd , mdToHtml ] ) ;
2020-11-14 00:02:17 +00:00
}