2021-07-26 14:50:31 +01:00
import { _ } from '@joplin/lib/locale' ;
import { MarkupToHtml } from '@joplin/renderer' ;
2023-02-15 10:59:32 -03:00
import { TinyMceEditorEvents } from './types' ;
2025-01-27 10:34:58 -08:00
import { Editor } from 'tinymce' ;
import Setting from '@joplin/lib/models/Setting' ;
2024-04-01 15:34:22 +01:00
import { focus } from '@joplin/lib/utils/focusHandler' ;
2021-07-26 14:50:31 +01:00
const taboverride = require ( 'taboverride' ) ;
interface SourceInfo {
openCharacters : string ;
closeCharacters : string ;
content : string ;
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-07-26 14:50:31 +01:00
node : any ;
language : string ;
}
2025-01-27 10:34:58 -08:00
const createTextAreaKeyListeners = ( ) = > {
let hasListeners = true ;
// Selectively enable/disable taboverride based on settings -- remove taboverride
// when pressing tab if tab is expected to move focus.
const onKeyDown = ( event : KeyboardEvent ) = > {
if ( event . key === 'Tab' ) {
if ( Setting . value ( 'editor.tabMovesFocus' ) ) {
taboverride . utils . removeListeners ( event . currentTarget ) ;
hasListeners = false ;
} else {
// Prevent the default focus-changing behavior
event . preventDefault ( ) ;
requestAnimationFrame ( ( ) = > {
focus ( 'openEditDialog::dialogTextArea_keyDown' , event . target ) ;
} ) ;
}
}
} ;
const onKeyUp = ( event : KeyboardEvent ) = > {
if ( event . key === 'Tab' && ! hasListeners ) {
taboverride . utils . addListeners ( event . currentTarget ) ;
hasListeners = true ;
}
} ;
return { onKeyDown , onKeyUp } ;
} ;
interface TextAreaTabHandler {
remove ( ) : void ;
2021-07-26 14:50:31 +01:00
}
// Allows pressing tab in a textarea to input an actual tab (instead of changing focus)
// taboverride will take care of actually inserting the tab character, while the keydown
// event listener will override the default behaviour, which is to focus the next field.
2025-01-27 10:34:58 -08:00
function enableTextAreaTab ( document : Document ) : TextAreaTabHandler {
type RemoveCallback = ( ) = > void ;
const removeCallbacks : RemoveCallback [ ] = [ ] ;
2021-07-26 14:50:31 +01:00
2025-01-27 10:34:58 -08:00
const textAreas = document . querySelectorAll < HTMLTextAreaElement > ( '.tox-textarea' ) ;
for ( const textArea of textAreas ) {
const { onKeyDown , onKeyUp } = createTextAreaKeyListeners ( ) ;
textArea . addEventListener ( 'keydown' , onKeyDown ) ;
textArea . addEventListener ( 'keyup' , onKeyUp ) ;
// Enable/disable taboverride **after** the listeners above.
// The custom keyup/keydown need to have higher precedence.
taboverride . set ( textArea , true ) ;
removeCallbacks . push ( ( ) = > {
taboverride . set ( textArea , false ) ;
textArea . removeEventListener ( 'keyup' , onKeyUp ) ;
textArea . removeEventListener ( 'keydown' , onKeyDown ) ;
} ) ;
2021-07-26 14:50:31 +01:00
}
2025-01-27 10:34:58 -08:00
return {
remove : ( ) = > {
for ( const callback of removeCallbacks ) {
callback ( ) ;
}
} ,
} ;
2021-07-26 14:50:31 +01:00
}
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-07-26 14:50:31 +01:00
function findBlockSource ( node : any ) : SourceInfo {
const sources = node . getElementsByClassName ( 'joplin-source' ) ;
if ( ! sources . length ) throw new Error ( 'No source for node' ) ;
const source = sources [ 0 ] ;
return {
openCharacters : source.getAttribute ( 'data-joplin-source-open' ) ,
closeCharacters : source.getAttribute ( 'data-joplin-source-close' ) ,
content : source.textContent ,
node : source ,
language : source.getAttribute ( 'data-joplin-language' ) || '' ,
} ;
}
2023-06-30 09:11:26 +01:00
function newBlockSource ( language = '' , content = '' , previousSource : SourceInfo = null ) : SourceInfo {
2021-07-26 14:50:31 +01:00
let fence = '```' ;
if ( language === 'katex' ) {
if ( previousSource && previousSource . openCharacters === '$' ) {
fence = '$' ;
} else {
fence = '$$' ;
}
}
const fenceLanguage = language === 'katex' ? '' : language ;
return {
openCharacters : fence === '$' ? '$' : ` \ n ${ fence } ${ fenceLanguage } \ n ` ,
closeCharacters : fence === '$' ? '$' : ` \ n ${ fence } \ n ` ,
content : content ,
node : null ,
language : language ,
} ;
}
function editableInnerHtml ( html : string ) : string {
const temp = document . createElement ( 'div' ) ;
temp . innerHTML = html ;
const editable = temp . getElementsByClassName ( 'joplin-editable' ) ;
if ( ! editable . length ) throw new Error ( ` Invalid joplin-editable: ${ html } ` ) ;
return editable [ 0 ] . innerHTML ;
}
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
2025-01-27 10:34:58 -08:00
export default function openEditDialog ( editor : Editor , markupToHtml : any , dispatchDidUpdate : Function , editable : any ) {
2021-07-26 14:50:31 +01:00
const source = editable ? findBlockSource ( editable ) : newBlockSource ( ) ;
2025-01-27 10:34:58 -08:00
const containerDocument = editor . getContainer ( ) . ownerDocument ;
let tabHandler : TextAreaTabHandler | null = null ;
2021-07-26 14:50:31 +01:00
editor . windowManager . open ( {
title : _ ( 'Edit' ) ,
size : 'large' ,
initialData : {
codeTextArea : source.content ,
languageInput : source.language ,
} ,
2024-04-05 12:16:49 +01:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-07-26 14:50:31 +01:00
onSubmit : async ( dialogApi : any ) = > {
const newSource = newBlockSource ( dialogApi . getData ( ) . languageInput , dialogApi . getData ( ) . codeTextArea , source ) ;
2024-04-17 02:19:25 -07:00
const md = ` ${ newSource . openCharacters } ${ newSource . content } ${ newSource . closeCharacters } ` ;
2021-07-26 14:50:31 +01:00
const result = await markupToHtml . current ( MarkupToHtml . MARKUP_LANGUAGE_MARKDOWN , md , { bodyOnly : true } ) ;
// markupToHtml will return the complete editable HTML, but we only
// want to update the inner HTML, so as not to break additional props that
// are added by TinyMCE on the main node.
if ( editable ) {
editable . innerHTML = editableInnerHtml ( result . html ) ;
} else {
editor . insertContent ( result . html ) ;
}
dialogApi . close ( ) ;
2023-02-15 10:59:32 -03:00
editor . fire ( TinyMceEditorEvents . JoplinChange ) ;
2021-07-26 14:50:31 +01:00
dispatchDidUpdate ( editor ) ;
} ,
onClose : ( ) = > {
2025-01-27 10:34:58 -08:00
tabHandler ? . remove ( ) ;
2021-07-26 14:50:31 +01:00
} ,
body : {
type : 'panel' ,
items : [
{
type : 'input' ,
name : 'languageInput' ,
label : 'Language' ,
// Katex is a special case with special opening/closing tags
// and we don't currently handle switching the language in this case.
2025-01-27 10:34:58 -08:00
enabled : source.language !== 'katex' ,
2021-07-26 14:50:31 +01:00
} ,
{
type : 'textarea' ,
name : 'codeTextArea' ,
} ,
] ,
} ,
buttons : [
{
type : 'submit' ,
text : 'OK' ,
} ,
] ,
} ) ;
window . requestAnimationFrame ( ( ) = > {
2025-01-27 10:34:58 -08:00
tabHandler = enableTextAreaTab ( containerDocument ) ;
2021-07-26 14:50:31 +01:00
} ) ;
}