2020-05-02 17:41:07 +02:00
import * as React from 'react' ;
2024-11-08 17:32:05 +02:00
import { useState , useEffect , useCallback , useRef , useMemo , useContext } from 'react' ;
2020-05-02 17:41:07 +02:00
import TinyMCE from './NoteBody/TinyMCE/TinyMCE' ;
import { connect } from 'react-redux' ;
import MultiNoteActions from '../MultiNoteActions' ;
2024-05-21 02:28:19 +02:00
import { htmlToMarkdown } from './utils' ;
2020-05-02 17:41:07 +02:00
import useSearchMarkers from './utils/useSearchMarkers' ;
import useNoteSearchBar from './utils/useNoteSearchBar' ;
import useMessageHandler from './utils/useMessageHandler' ;
import useWindowCommandHandler from './utils/useWindowCommandHandler' ;
import useDropHandler from './utils/useDropHandler' ;
import useMarkupToHtml from './utils/useMarkupToHtml' ;
2024-05-22 15:57:17 +02:00
import useFormNote , { OnLoadEvent , OnSetFormNote } from './utils/useFormNote' ;
2022-11-14 19:25:41 +02:00
import useEffectiveNoteId from './utils/useEffectiveNoteId' ;
2020-09-15 15:01:07 +02:00
import useFolder from './utils/useFolder' ;
2020-05-02 17:41:07 +02:00
import styles_ from './styles' ;
2024-05-21 02:28:19 +02:00
import { NoteEditorProps , FormNote , OnChangeEvent , NoteBodyEditorProps , AllAssetsOptions , NoteBodyEditorRef } from './utils/types' ;
2020-11-07 17:59:37 +02:00
import CommandService from '@joplin/lib/services/CommandService' ;
2020-09-15 15:01:07 +02:00
import ToolbarButton from '../ToolbarButton/ToolbarButton' ;
import Button , { ButtonLevel } from '../Button/Button' ;
2023-12-13 21:24:58 +02:00
import eventManager , { EventName } from '@joplin/lib/eventManager' ;
2021-09-04 19:11:29 +02:00
import { AppState } from '../../app.reducer' ;
2024-12-11 14:31:05 +02:00
import ToolbarButtonUtils , { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils' ;
2023-12-13 21:24:58 +02:00
import { _ , _n } from '@joplin/lib/locale' ;
2020-10-20 00:24:40 +02:00
import TagList from '../TagList' ;
2020-11-05 18:58:23 +02:00
import NoteTitleBar from './NoteTitle/NoteTitleBar' ;
2024-10-27 23:19:38 +02:00
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils' ;
2020-11-07 17:59:37 +02:00
import Setting from '@joplin/lib/models/Setting' ;
2020-11-13 19:09:28 +02:00
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext' ;
2020-11-16 13:03:44 +02:00
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher' ;
2023-07-16 18:42:42 +02:00
import { itemIsReadOnly } from '@joplin/lib/models/utils/readOnly' ;
2020-11-07 17:59:37 +02:00
const { themeStyle } = require ( '@joplin/lib/theme' ) ;
const { substrWithEllipsis } = require ( '@joplin/lib/string-utils' ) ;
2023-01-19 19:19:06 +02:00
import NoteSearchBar from '../NoteSearchBar' ;
2021-01-22 19:41:11 +02:00
import Note from '@joplin/lib/models/Note' ;
import Folder from '@joplin/lib/models/Folder' ;
2023-01-19 19:19:06 +02:00
import NoteRevisionViewer from '../NoteRevisionViewer' ;
2023-08-14 19:33:48 +02:00
import { parseShareCache } from '@joplin/lib/services/share/reducer' ;
2023-07-16 18:42:42 +02:00
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect' ;
import { ModelType } from '@joplin/lib/BaseModel' ;
import BaseItem from '@joplin/lib/models/BaseItem' ;
import { ErrorCode } from '@joplin/lib/errors' ;
import ItemChange from '@joplin/lib/models/ItemChange' ;
2023-10-22 13:00:19 +02:00
import PlainEditor from './NoteBody/PlainEditor/PlainEditor' ;
2023-09-21 10:12:40 +02:00
import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror' ;
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror' ;
2023-12-13 21:24:58 +02:00
import { openItemById } from './utils/contextMenu' ;
2024-03-25 14:50:33 +02:00
import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginSettingValue' ;
2023-12-29 18:08:09 +02:00
import { MarkupLanguage } from '@joplin/renderer' ;
2024-05-21 02:28:19 +02:00
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions' ;
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks' ;
2024-08-02 15:47:26 +02:00
import WarningBanner from './WarningBanner/WarningBanner' ;
2024-11-10 16:04:46 +02:00
import UserWebview from '../../services/plugins/UserWebview' ;
import Logger from '@joplin/utils/Logger' ;
import usePluginEditorView from './utils/usePluginEditorView' ;
2024-11-08 17:32:05 +02:00
import { stateUtils } from '@joplin/lib/reducer' ;
import { WindowIdContext } from '../NewWindowOrIFrame' ;
2024-11-10 16:04:46 +02:00
import { EditorActivationCheckFilterObject } from '@joplin/lib/services/plugins/api/types' ;
import PluginService from '@joplin/lib/services/plugins/PluginService' ;
import WebviewController from '@joplin/lib/services/plugins/WebviewController' ;
import AsyncActionQueue , { IntervalType } from '@joplin/lib/AsyncActionQueue' ;
2024-05-30 09:40:32 +02:00
const debounce = require ( 'debounce' ) ;
2020-05-02 17:41:07 +02:00
2024-11-10 16:04:46 +02:00
const logger = Logger . create ( 'NoteEditor' ) ;
2020-07-03 23:32:39 +02:00
const commands = [
require ( './commands/showRevisions' ) ,
] ;
2020-10-09 19:35:46 +02:00
const toolbarButtonUtils = new ToolbarButtonUtils ( CommandService . instance ( ) ) ;
2024-11-08 17:32:05 +02:00
const onDragOver : React.DragEventHandler = event = > event . preventDefault ( ) ;
let editorIdCounter = 0 ;
2024-11-10 16:04:46 +02:00
const makeNoteUpdateAction = ( shownEditorViewIds : string [ ] ) = > {
return async ( ) = > {
for ( const viewId of shownEditorViewIds ) {
const controller = PluginService . instance ( ) . viewControllerByViewId ( viewId ) as WebviewController ;
if ( controller ) controller . emitUpdate ( ) ;
}
} ;
} ;
2024-11-08 17:32:05 +02:00
function NoteEditorContent ( props : NoteEditorProps ) {
2020-05-02 17:41:07 +02:00
const [ showRevisions , setShowRevisions ] = useState ( false ) ;
const [ titleHasBeenManuallyChanged , setTitleHasBeenManuallyChanged ] = useState ( false ) ;
2023-07-16 18:42:42 +02:00
const [ isReadOnly , setIsReadOnly ] = useState < boolean > ( false ) ;
2020-05-02 17:41:07 +02:00
2023-10-22 13:00:19 +02:00
const editorRef = useRef < NoteBodyEditorRef > ( ) ;
2024-05-21 02:28:19 +02:00
const titleInputRef = useRef < HTMLInputElement > ( ) ;
2020-05-02 17:41:07 +02:00
const isMountedRef = useRef ( true ) ;
const noteSearchBarRef = useRef ( null ) ;
2024-11-10 16:04:46 +02:00
const viewUpdateAsyncQueue_ = useRef < AsyncActionQueue > ( new AsyncActionQueue ( 100 , IntervalType . Fixed ) ) ;
const shownEditorViewIds = props [ 'plugins.shownEditorViewIds' ] ;
2020-05-02 17:41:07 +02:00
2024-11-08 17:32:05 +02:00
// Should be constant and unique to this instance of the editor.
const editorId = useMemo ( ( ) = > {
return ` editor- ${ editorIdCounter ++ } ` ;
} , [ ] ) ;
2024-05-22 15:57:17 +02:00
const setFormNoteRef = useRef < OnSetFormNote > ( ) ;
2024-05-21 02:28:19 +02:00
const { saveNoteIfWillChange , scheduleSaveNote } = useScheduleSaveCallbacks ( {
2024-11-08 17:32:05 +02:00
setFormNote : setFormNoteRef , dispatch : props.dispatch , editorRef , editorId ,
2024-05-21 02:28:19 +02:00
} ) ;
2020-11-12 21:13:28 +02:00
const formNote_beforeLoad = useCallback ( async ( event : OnLoadEvent ) = > {
2020-05-02 17:41:07 +02:00
await saveNoteIfWillChange ( event . formNote ) ;
setShowRevisions ( false ) ;
2024-05-21 02:28:19 +02:00
} , [ saveNoteIfWillChange ] ) ;
2020-05-02 17:41:07 +02:00
const formNote_afterLoad = useCallback ( async ( ) = > {
setTitleHasBeenManuallyChanged ( false ) ;
} , [ ] ) ;
2022-11-14 19:25:41 +02:00
const effectiveNoteId = useEffectiveNoteId ( props ) ;
2024-11-10 16:04:46 +02:00
useAsyncEffect ( async ( event ) = > {
if ( ! props . startupPluginsLoaded ) return ;
let filterObject : EditorActivationCheckFilterObject = {
activatedEditors : [ ] ,
} ;
filterObject = await eventManager . filterEmit ( 'editorActivationCheck' , filterObject ) ;
if ( event . cancelled ) return ;
for ( const editor of filterObject . activatedEditors ) {
const controller = PluginService . instance ( ) . pluginById ( editor . pluginId ) . viewController ( editor . viewId ) as WebviewController ;
controller . setActive ( editor . isActive ) ;
}
} , [ effectiveNoteId , props . startupPluginsLoaded ] ) ;
useEffect ( ( ) = > {
if ( ! props . startupPluginsLoaded ) return ;
viewUpdateAsyncQueue_ . current . push ( makeNoteUpdateAction ( shownEditorViewIds ) ) ;
} , [ effectiveNoteId , shownEditorViewIds , props . startupPluginsLoaded ] ) ;
const { editorPlugin , editorView } = usePluginEditorView ( props . plugins , shownEditorViewIds ) ;
const builtInEditorVisible = ! editorPlugin ;
2020-05-02 17:41:07 +02:00
const { formNote , setFormNote , isNewNote , resourceInfos } = useFormNote ( {
2022-11-14 19:25:41 +02:00
noteId : effectiveNoteId ,
2020-05-02 17:41:07 +02:00
isProvisional : props.isProvisional ,
titleInputRef : titleInputRef ,
editorRef : editorRef ,
onBeforeLoad : formNote_beforeLoad ,
onAfterLoad : formNote_afterLoad ,
2024-11-10 16:04:46 +02:00
builtInEditorVisible ,
2024-11-08 17:32:05 +02:00
editorId ,
2020-05-02 17:41:07 +02:00
} ) ;
2024-05-21 02:28:19 +02:00
setFormNoteRef . current = setFormNote ;
2020-05-02 17:41:07 +02:00
const formNoteRef = useRef < FormNote > ( ) ;
formNoteRef . current = { . . . formNote } ;
2020-09-15 15:01:07 +02:00
const formNoteFolder = useFolder ( { folderId : formNote.parent_id } ) ;
2020-05-02 17:41:07 +02:00
const {
localSearch ,
onChange : localSearch_change ,
onNext : localSearch_next ,
onPrevious : localSearch_previous ,
onClose : localSearch_close ,
setResultCount : setLocalSearchResultCount ,
showLocalSearch ,
setShowLocalSearch ,
searchMarkers : localSearchMarkerOptions ,
2022-12-31 01:54:04 +02:00
} = useNoteSearchBar ( { noteSearchBarRef } ) ;
2020-05-02 17:41:07 +02:00
// If the note has been modified in another editor, wait for it to be saved
// before loading it in this editor.
// const waitingToSaveNote = props.noteId && formNote.id !== props.noteId && props.editorNoteStatuses[props.noteId] === 'saving';
const styles = styles_ ( props ) ;
2023-12-29 18:08:09 +02:00
const whiteBackgroundNoteRendering = formNote . markup_language === MarkupLanguage . Html ;
2020-10-21 01:23:55 +02:00
const markupToHtml = useMarkupToHtml ( {
themeId : props.themeId ,
2023-12-29 18:08:09 +02:00
whiteBackgroundNoteRendering ,
2020-10-21 01:23:55 +02:00
customCss : props.customCss ,
plugins : props.plugins ,
2024-03-25 14:50:33 +02:00
settingValue : getPluginSettingValue ,
2020-10-21 01:23:55 +02:00
} ) ;
2020-05-02 17:41:07 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-09-19 13:03:16 +02:00
const allAssets = useCallback ( async ( markupLanguage : number , options : AllAssetsOptions = null ) : Promise < any [ ] > = > {
2021-09-19 19:54:14 +02:00
options = {
contentMaxWidthTarget : '' ,
. . . options ,
} ;
2023-12-29 18:08:09 +02:00
const theme = themeStyle ( options . themeId ? options.themeId : props.themeId ) ;
2020-05-02 17:41:07 +02:00
2020-12-19 19:42:18 +02:00
const markupToHtml = markupLanguageUtils . newMarkupToHtml ( { } , {
2024-07-26 13:22:49 +02:00
resourceBaseUrl : ` joplin-content://note-viewer/ ${ Setting . value ( 'resourceDir' ) } / ` ,
2021-05-19 15:00:16 +02:00
customCss : props.customCss ,
2020-05-02 17:41:07 +02:00
} ) ;
2021-09-19 13:03:16 +02:00
return markupToHtml . allAssets ( markupLanguage , theme , {
contentMaxWidth : props.contentMaxWidth ,
contentMaxWidthTarget : options.contentMaxWidthTarget ,
2023-12-29 18:08:09 +02:00
whiteBackgroundNoteRendering : options.whiteBackgroundNoteRendering ,
2021-09-19 13:03:16 +02:00
} ) ;
2021-08-14 13:19:53 +02:00
} , [ props . themeId , props . customCss , props . contentMaxWidth ] ) ;
2020-05-02 17:41:07 +02:00
const handleProvisionalFlag = useCallback ( ( ) = > {
if ( props . isProvisional ) {
props . dispatch ( {
type : 'NOTE_PROVISIONAL_FLAG_CLEAR' ,
id : formNote.id ,
} ) ;
}
2024-05-21 02:28:19 +02:00
} , [ props . isProvisional , formNote . id , props . dispatch ] ) ;
2020-05-02 17:41:07 +02:00
2024-05-30 09:40:32 +02:00
const scheduleNoteListResort = useMemo ( ( ) = > {
return debounce ( ( ) = > {
// Although the note list will update automatically, it may take some time. This
// forces an immediate update.
props . dispatch ( { type : 'NOTE_SORT' } ) ;
} , 100 ) ;
} , [ props . dispatch ] ) ;
2020-05-02 17:41:07 +02:00
2024-11-08 17:32:05 +02:00
useEffect ( ( ) = > {
props . onTitleChange ? . ( formNote . title ) ;
} , [ formNote . title , props . onTitleChange ] ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2024-05-30 09:40:32 +02:00
const onFieldChange = useCallback ( async ( field : string , value : any , changeId = 0 ) = > {
2020-05-02 17:41:07 +02:00
if ( ! isMountedRef . current ) {
// When the component is unmounted, various actions can happen which can
// trigger onChange events, for example the textarea might be cleared.
// We need to ignore these events, otherwise the note is going to be saved
// with an invalid body.
2024-11-10 16:04:46 +02:00
logger . debug ( 'Skipping change event because the component is unmounted' ) ;
2020-05-02 17:41:07 +02:00
return ;
}
handleProvisionalFlag ( ) ;
const change = field === 'body' ? {
body : value ,
} : {
title : value ,
} ;
const newNote = {
. . . formNote ,
. . . change ,
bodyWillChangeId : 0 ,
bodyChangeId : 0 ,
hasChanged : true ,
} ;
if ( field === 'title' ) {
setTitleHasBeenManuallyChanged ( true ) ;
}
if ( isNewNote && ! titleHasBeenManuallyChanged && field === 'body' ) {
// TODO: Handle HTML/Markdown format
newNote . title = Note . defaultTitle ( value ) ;
}
if ( changeId !== null && field === 'body' && formNote . bodyWillChangeId !== changeId ) {
// Note was changed, but another note was loaded before save - skipping
// The previously loaded note, that was modified, will be saved via saveNoteIfWillChange()
} else {
setFormNote ( newNote ) ;
2024-05-30 09:40:32 +02:00
await scheduleSaveNote ( newNote ) ;
}
if ( field === 'title' ) {
// Scheduling a resort needs to be:
// - called after scheduleSaveNote so that the new note title is used for sorting
// - debounced because many calls to scheduleSaveNote can resolve at once
scheduleNoteListResort ( ) ;
2020-05-02 17:41:07 +02:00
}
2024-05-30 09:40:32 +02:00
} , [ handleProvisionalFlag , formNote , setFormNote , isNewNote , titleHasBeenManuallyChanged , scheduleNoteListResort , scheduleSaveNote ] ) ;
2020-05-02 17:41:07 +02:00
2024-09-26 12:35:32 +02:00
const onDrop = useDropHandler ( { editorRef } ) ;
const onBodyChange = useCallback ( ( event : OnChangeEvent ) = > onFieldChange ( 'body' , event . content , event . changeId ) , [ onFieldChange ] ) ;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onTitleChange = useCallback ( ( event : any ) = > onFieldChange ( 'title' , event . target . value ) , [ onFieldChange ] ) ;
2024-11-08 17:32:05 +02:00
const containerRef = useRef < HTMLDivElement > ( null ) ;
2020-11-14 02:02:17 +02:00
useWindowCommandHandler ( {
dispatch : props.dispatch ,
setShowLocalSearch ,
noteSearchBarRef ,
editorRef ,
titleInputRef ,
2024-09-26 12:35:32 +02:00
onBodyChange ,
2024-11-08 17:32:05 +02:00
containerRef ,
2020-11-14 02:02:17 +02:00
} ) ;
2020-05-02 17:41:07 +02:00
2020-11-05 18:58:23 +02:00
// const onTitleKeydown = useCallback((event:any) => {
// const keyCode = event.keyCode;
2020-05-02 17:41:07 +02:00
2020-11-05 18:58:23 +02:00
// if (keyCode === 9) {
// // TAB
// event.preventDefault();
2020-05-02 17:41:07 +02:00
2020-11-05 18:58:23 +02:00
// if (event.shiftKey) {
// CommandService.instance().execute('focusElement', 'noteList');
// } else {
// CommandService.instance().execute('focusElement', 'noteBody');
// }
// }
// }, [props.dispatch]);
2020-05-02 17:41:07 +02:00
2023-08-14 19:33:48 +02:00
const shareCache = useMemo ( ( ) = > {
return parseShareCache ( props . shareCacheSetting ) ;
} , [ props . shareCacheSetting ] ) ;
2023-07-16 18:42:42 +02:00
useAsyncEffect ( async event = > {
if ( ! formNote . id ) return ;
try {
2023-08-14 19:33:48 +02:00
const result = await itemIsReadOnly ( BaseItem , ModelType . Note , ItemChange . SOURCE_UNSPECIFIED , formNote . id , props . syncUserId , shareCache ) ;
2023-07-16 18:42:42 +02:00
if ( event . cancelled ) return ;
setIsReadOnly ( result ) ;
} catch ( error ) {
if ( error . code === ErrorCode . NotFound ) {
// Can happen if the note has been deleted but a render is
// triggered anyway. It can be ignored.
} else {
throw error ;
}
}
2023-08-14 19:33:48 +02:00
} , [ formNote . id , props . syncUserId , shareCache ] ) ;
2023-07-16 18:42:42 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-05-02 17:41:07 +02:00
const onBodyWillChange = useCallback ( ( event : any ) = > {
handleProvisionalFlag ( ) ;
2020-05-21 10:14:33 +02:00
setFormNote ( prev = > {
2020-05-02 17:41:07 +02:00
return {
. . . prev ,
bodyWillChangeId : event.changeId ,
hasChanged : true ,
} ;
} ) ;
props . dispatch ( {
type : 'EDITOR_NOTE_STATUS_SET' ,
id : formNote.id ,
status : 'saving' ,
} ) ;
2024-05-21 02:28:19 +02:00
} , [ formNote , setFormNote , handleProvisionalFlag , props . dispatch ] ) ;
2020-05-02 17:41:07 +02:00
2024-05-21 02:28:19 +02:00
const { scrollWhenReady , clearScrollWhenReady } = useScrollWhenReadyOptions ( {
noteId : formNote.id ,
selectedNoteHash : props.selectedNoteHash ,
lastEditorScrollPercents : props.lastEditorScrollPercents ,
editorRef ,
} ) ;
2024-11-08 17:32:05 +02:00
const windowId = useContext ( WindowIdContext ) ;
const onMessage = useMessageHandler ( scrollWhenReady , clearScrollWhenReady , windowId , editorRef , setLocalSearchResultCount , props . dispatch , formNote , htmlToMarkdown , markupToHtml ) ;
2020-05-02 17:41:07 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-08-02 12:24:54 +02:00
const externalEditWatcher_noteChange = useCallback ( ( event : any ) = > {
2020-05-02 17:41:07 +02:00
if ( event . id === formNote . id ) {
const newFormNote = {
. . . formNote ,
title : event.note.title ,
body : event.note.body ,
} ;
setFormNote ( newFormNote ) ;
}
2024-05-21 02:28:19 +02:00
} , [ formNote , setFormNote ] ) ;
2020-05-02 17:41:07 +02:00
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-08-02 12:24:54 +02:00
const onNotePropertyChange = useCallback ( ( event : any ) = > {
2020-05-21 10:14:33 +02:00
setFormNote ( formNote = > {
2020-05-02 17:41:07 +02:00
if ( formNote . id !== event . note . id ) return formNote ;
const newFormNote : FormNote = { . . . formNote } ;
for ( const key in event . note ) {
if ( key === 'id' ) continue ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-05-02 17:41:07 +02:00
( newFormNote as any ) [ key ] = event . note [ key ] ;
}
return newFormNote ;
} ) ;
2024-05-21 02:28:19 +02:00
} , [ setFormNote ] ) ;
2020-05-02 17:41:07 +02:00
useEffect ( ( ) = > {
2023-12-13 21:24:58 +02:00
eventManager . on ( EventName . AlarmChange , onNotePropertyChange ) ;
2020-05-02 17:41:07 +02:00
ExternalEditWatcher . instance ( ) . on ( 'noteChange' , externalEditWatcher_noteChange ) ;
return ( ) = > {
2023-12-13 21:24:58 +02:00
eventManager . off ( EventName . AlarmChange , onNotePropertyChange ) ;
2020-05-02 17:41:07 +02:00
ExternalEditWatcher . instance ( ) . off ( 'noteChange' , externalEditWatcher_noteChange ) ;
} ;
} , [ externalEditWatcher_noteChange , onNotePropertyChange ] ) ;
2020-07-03 23:32:39 +02:00
useEffect ( ( ) = > {
const dependencies = {
setShowRevisions ,
2024-11-08 17:32:05 +02:00
isInFocusedDocument : ( ) = > {
return containerRef . current ? . ownerDocument ? . hasFocus ( ) ;
} ,
2020-05-02 17:41:07 +02:00
} ;
2024-11-08 17:32:05 +02:00
const registeredCommands = CommandService . instance ( ) . componentRegisterCommands (
dependencies ,
commands ,
true ,
) ;
2020-05-02 17:41:07 +02:00
2020-07-03 23:32:39 +02:00
return ( ) = > {
2024-11-08 17:32:05 +02:00
registeredCommands . deregister ( ) ;
2020-07-03 23:32:39 +02:00
} ;
} , [ setShowRevisions ] ) ;
2020-05-02 17:41:07 +02:00
2023-09-21 10:12:40 +02:00
const onScroll = useCallback ( ( event : { percent : number } ) = > {
2020-05-02 17:41:07 +02:00
props . dispatch ( {
type : 'EDITOR_SCROLL_PERCENT_SET' ,
2021-12-15 20:03:20 +02:00
// In callbacks of setTimeout()/setInterval(), props/state cannot be used
// to refer the current value, since they would be one or more generations old.
// For the purpose, useRef value should be used.
noteId : formNoteRef.current.id ,
2020-05-02 17:41:07 +02:00
percent : event.percent ,
} ) ;
2023-10-24 11:47:19 +02:00
} , [ props . dispatch ] ) ;
2020-05-02 17:41:07 +02:00
2024-05-21 02:28:19 +02:00
function renderNoNotes ( rootStyle : React.CSSProperties ) {
2023-06-01 13:02:36 +02:00
const emptyDivStyle = {
backgroundColor : 'black' ,
opacity : 0.1 ,
. . . rootStyle ,
} ;
2024-11-08 17:32:05 +02:00
return < div style = { emptyDivStyle } ref = { containerRef } > < / div > ;
2020-05-02 17:41:07 +02:00
}
2020-09-15 15:01:07 +02:00
function renderTagButton() {
return < ToolbarButton
themeId = { props . themeId }
2020-10-09 19:35:46 +02:00
toolbarButtonInfo = { props . setTagsToolbarButtonInfo }
2020-09-15 15:01:07 +02:00
/ > ;
}
2020-05-17 15:01:42 +02:00
function renderTagBar() {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( props . themeId ) ;
2020-09-29 12:33:22 +02:00
const noteIds = [ formNote . id ] ;
2021-01-25 12:20:56 +02:00
const instructions = < span onClick = { ( ) = > { void CommandService . instance ( ) . execute ( 'setTags' , noteIds ) ; } } style = { { . . . theme . clickableTextStyle , whiteSpace : 'nowrap' } } > { _ ( 'Click to add tags...' ) } < / span > ;
2020-09-29 12:33:22 +02:00
const tagList = props . selectedNoteTags . length ? < TagList items = { props . selectedNoteTags } / > : null ;
2020-09-15 15:01:07 +02:00
return (
2020-09-29 12:33:22 +02:00
< div style = { { paddingLeft : 8 , display : 'flex' , flexDirection : 'row' , alignItems : 'center' } } > { tagList } { instructions } < / div >
2020-09-15 15:01:07 +02:00
) ;
2020-05-17 15:01:42 +02:00
}
2020-09-06 14:07:00 +02:00
const searchMarkers = useSearchMarkers ( showLocalSearch , localSearchMarkerOptions , props . searches , props . selectedSearchId , props . highlightedWords ) ;
2020-05-02 17:41:07 +02:00
2020-11-12 21:13:28 +02:00
const editorProps : NoteBodyEditorProps = {
2020-05-02 17:41:07 +02:00
ref : editorRef ,
contentKey : formNote.id ,
style : styles.tinyMCE ,
2023-12-29 18:08:09 +02:00
whiteBackgroundNoteRendering ,
2020-05-02 17:41:07 +02:00
onChange : onBodyChange ,
onWillChange : onBodyWillChange ,
onMessage : onMessage ,
content : formNote.body ,
contentMarkupLanguage : formNote.markup_language ,
contentOriginalCss : formNote.originalCss ,
resourceInfos : resourceInfos ,
2023-12-06 21:17:16 +02:00
resourceDirectory : Setting.value ( 'resourceDir' ) ,
2020-05-02 17:41:07 +02:00
htmlToMarkdown : htmlToMarkdown ,
markupToHtml : markupToHtml ,
allAssets : allAssets ,
2023-07-16 18:42:42 +02:00
disabled : isReadOnly ,
2020-09-15 15:01:07 +02:00
themeId : props.themeId ,
2020-05-02 17:41:07 +02:00
dispatch : props.dispatch ,
2020-10-09 19:35:46 +02:00
noteToolbar : null ,
2020-05-02 17:41:07 +02:00
onScroll : onScroll ,
2020-07-23 00:13:23 +02:00
setLocalSearchResultCount : setLocalSearchResultCount ,
2024-08-15 17:01:52 +02:00
setLocalSearch : localSearch_change ,
setShowLocalSearch ,
useLocalSearch : showLocalSearch ,
2020-05-02 17:41:07 +02:00
searchMarkers : searchMarkers ,
visiblePanes : props.noteVisiblePanes || [ 'editor' , 'viewer' ] ,
keyboardMode : Setting.value ( 'editor.keyboardMode' ) ,
2020-05-04 01:55:41 +02:00
locale : Setting.value ( 'locale' ) ,
2020-05-10 17:28:22 +02:00
onDrop : onDrop ,
2020-10-09 19:35:46 +02:00
noteToolbarButtonInfos : props.toolbarButtonInfos ,
plugins : props.plugins ,
2021-01-29 20:45:11 +02:00
fontSize : Setting.value ( 'style.editor.fontSize' ) ,
2021-08-14 13:19:53 +02:00
contentMaxWidth : props.contentMaxWidth ,
2021-10-30 18:51:19 +02:00
isSafeMode : props.isSafeMode ,
2023-01-17 14:08:22 +02:00
useCustomPdfViewer : props.useCustomPdfViewer ,
2022-08-27 14:32:20 +02:00
// We need it to identify the context for which media is rendered.
2024-02-26 12:16:23 +02:00
// It is currently used to remember pdf scroll position for each attachments of each note uniquely.
2022-08-27 14:32:20 +02:00
noteId : props.noteId ,
2024-04-03 19:29:22 +02:00
watchedNoteFiles : props.watchedNoteFiles ,
2020-05-02 17:41:07 +02:00
} ;
let editor = null ;
2024-11-10 16:04:46 +02:00
if ( builtInEditorVisible ) {
if ( props . bodyEditor === 'TinyMCE' ) {
editor = < TinyMCE { ...editorProps } / > ;
} else if ( props . bodyEditor === 'PlainText' ) {
editor = < PlainEditor { ...editorProps } / > ;
} else if ( props . bodyEditor === 'CodeMirror5' ) {
editor = < CodeMirror5 { ...editorProps } / > ;
} else if ( props . bodyEditor === 'CodeMirror6' ) {
editor = < CodeMirror6 { ...editorProps } / > ;
} else {
throw new Error ( ` Invalid editor: ${ props . bodyEditor } ` ) ;
}
2020-05-02 17:41:07 +02:00
}
const noteRevisionViewer_onBack = useCallback ( ( ) = > {
setShowRevisions ( false ) ;
} , [ ] ) ;
2023-12-13 21:24:58 +02:00
const onBannerResourceClick = useCallback ( async ( event : React.MouseEvent < HTMLAnchorElement > ) = > {
event . preventDefault ( ) ;
const resourceId = event . currentTarget . getAttribute ( 'data-resource-id' ) ;
await openItemById ( resourceId , props . dispatch ) ;
} , [ props . dispatch ] ) ;
2020-05-02 17:41:07 +02:00
if ( showRevisions ) {
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( props . themeId ) ;
2020-05-02 17:41:07 +02:00
2024-05-21 02:28:19 +02:00
const revStyle : React.CSSProperties = {
2020-09-15 15:01:07 +02:00
// ...props.style,
2020-05-02 17:41:07 +02:00
display : 'inline-flex' ,
padding : theme.margin ,
verticalAlign : 'top' ,
boxSizing : 'border-box' ,
2024-11-08 17:32:05 +02:00
flex : 1 ,
2020-05-02 17:41:07 +02:00
} ;
return (
2024-11-08 17:32:05 +02:00
< div style = { revStyle } ref = { containerRef } >
2020-05-02 17:41:07 +02:00
< NoteRevisionViewer customCss = { props . customCss } noteId = { formNote . id } onBack = { noteRevisionViewer_onBack } / >
< / div >
) ;
}
if ( props . selectedNoteIds . length > 1 ) {
return < MultiNoteActions
2020-09-15 15:01:07 +02:00
themeId = { props . themeId }
2020-05-02 17:41:07 +02:00
selectedNoteIds = { props . selectedNoteIds }
notes = { props . notes }
dispatch = { props . dispatch }
watchedNoteFiles = { props . watchedNoteFiles }
2020-10-09 19:35:46 +02:00
plugins = { props . plugins }
2020-11-17 13:50:46 +02:00
inConflictFolder = { props . selectedFolderId === Folder . conflictFolderId ( ) }
2021-05-19 15:00:16 +02:00
customCss = { props . customCss }
2020-05-02 17:41:07 +02:00
/ > ;
}
function renderSearchBar() {
if ( ! showLocalSearch ) return false ;
2020-09-15 15:01:07 +02:00
const theme = themeStyle ( props . themeId ) ;
2020-05-02 17:41:07 +02:00
return (
< NoteSearchBar
ref = { noteSearchBarRef }
2022-11-14 18:48:41 +02:00
themeId = { props . themeId }
2020-05-02 17:41:07 +02:00
style = { {
display : 'flex' ,
height : 35 ,
borderTop : ` 1px solid ${ theme . dividerColor } ` ,
} }
query = { localSearch . query }
searching = { localSearch . searching }
resultCount = { localSearch . resultCount }
selectedIndex = { localSearch . selectedIndex }
onChange = { localSearch_change }
onNext = { localSearch_next }
onPrevious = { localSearch_previous }
onClose = { localSearch_close }
2020-09-06 17:28:23 +02:00
visiblePanes = { props . noteVisiblePanes }
2024-08-15 17:01:52 +02:00
editorType = { props . bodyEditor }
2020-05-02 17:41:07 +02:00
/ >
) ;
}
2020-07-22 20:03:31 +02:00
function renderResourceWatchingNotification() {
if ( ! Object . keys ( props . watchedResources ) . length ) return null ;
const resourceTitles = Object . keys ( props . watchedResources ) . map ( id = > props . watchedResources [ id ] . title ) ;
return (
< div style = { styles . resourceWatchBanner } >
< p style = { styles . resourceWatchBannerLine } > { _ ( 'The following attachments are being watched for changes:' ) } < strong > { resourceTitles . join ( ', ' ) } < / strong > < / p >
< p style = { { . . . styles . resourceWatchBannerLine , marginBottom : 0 } } > { _ ( 'The attachments will no longer be watched when you switch to a different note.' ) } < / p >
< / div >
) ;
}
2023-12-13 21:24:58 +02:00
const renderResourceInSearchResultsNotification = ( ) = > {
const resourceResults = props . searchResults . filter ( r = > r . id === props . noteId && r . item_type === ModelType . Resource ) ;
if ( ! resourceResults . length ) return null ;
const renderResource = ( id : string , title : string ) = > {
return < li key = { id } > < a data - resource - id = { id } onClick = { onBannerResourceClick } href = "#" > { title } < / a > < / li > ;
} ;
return (
< div style = { styles . resourceWatchBanner } >
< p style = { styles . resourceWatchBannerLine } > { _n ( 'The following attachment matches your search query:' , 'The following attachments match your search query:' , resourceResults . length ) } < / p >
< ul >
{ resourceResults . map ( r = > renderResource ( r . item_id , r . title ) ) }
< / ul >
< / div >
) ;
} ;
2020-09-15 15:01:07 +02:00
function renderSearchInfo() {
2021-01-02 19:27:37 +02:00
const theme = themeStyle ( props . themeId ) ;
2020-09-15 15:01:07 +02:00
if ( formNoteFolder && [ 'Search' , 'Tag' , 'SmartFilter' ] . includes ( props . notesParentType ) ) {
return (
2021-01-02 19:27:37 +02:00
< div style = { { paddingTop : 10 , paddingBottom : 10 , paddingLeft : theme.editorPaddingLeft } } >
2020-09-15 15:01:07 +02:00
< Button
iconName = "icon-notebooks"
level = { ButtonLevel . Primary }
title = { _ ( 'In: %s' , substrWithEllipsis ( formNoteFolder . title , 0 , 100 ) ) }
onClick = { ( ) = > {
props . dispatch ( {
type : 'FOLDER_AND_NOTE_SELECT' ,
folderId : formNoteFolder.id ,
noteId : formNote.id ,
} ) ;
} }
/ >
< div style = { { flex : 1 } } > < / div >
< / div >
) ;
} else {
return null ;
}
}
2024-11-10 16:04:46 +02:00
const renderPluginEditor = ( ) = > {
if ( ! editorPlugin ) return null ;
const html = props . pluginHtmlContents [ editorPlugin . id ] ? . [ editorView . id ] ? ? '' ;
return < UserWebview
key = { editorView . id }
viewId = { editorView . id }
themeId = { props . themeId }
html = { html }
scripts = { editorView . scripts }
pluginId = { editorPlugin . id }
borderBottom = { true }
fitToContent = { false }
/ > ;
} ;
2022-11-14 19:25:41 +02:00
if ( formNote . encryption_applied || ! formNote . id || ! effectiveNoteId ) {
2020-05-02 17:41:07 +02:00
return renderNoNotes ( styles . root ) ;
}
2021-01-02 19:27:37 +02:00
const theme = themeStyle ( props . themeId ) ;
2020-05-02 17:41:07 +02:00
return (
2024-11-08 17:32:05 +02:00
< div style = { styles . root } onDragOver = { onDragOver } onDrop = { onDrop } ref = { containerRef } >
2020-05-02 17:41:07 +02:00
< div style = { { display : 'flex' , flexDirection : 'column' , height : '100%' } } >
2020-07-22 20:03:31 +02:00
{ renderResourceWatchingNotification ( ) }
2023-12-13 21:24:58 +02:00
{ renderResourceInSearchResultsNotification ( ) }
2020-11-05 18:58:23 +02:00
< NoteTitleBar
titleInputRef = { titleInputRef }
themeId = { props . themeId }
isProvisional = { props . isProvisional }
noteIsTodo = { formNote . is_todo }
noteTitle = { formNote . title }
noteUserUpdatedTime = { formNote . user_updated_time }
onTitleChange = { onTitleChange }
2023-07-16 18:42:42 +02:00
disabled = { isReadOnly }
2020-11-05 18:58:23 +02:00
/ >
2020-09-15 15:01:07 +02:00
{ renderSearchInfo ( ) }
2023-09-21 10:12:40 +02:00
< div style = { { display : 'flex' , flex : 1 , paddingLeft : theme.editorPaddingLeft , maxHeight : '100%' , minHeight : '0' } } >
2020-05-02 17:41:07 +02:00
{ editor }
2024-11-10 16:04:46 +02:00
{ renderPluginEditor ( ) }
2020-05-02 17:41:07 +02:00
< / div >
< div style = { { display : 'flex' , flexDirection : 'row' , alignItems : 'center' } } >
{ renderSearchBar ( ) }
< / div >
2021-05-17 20:33:44 +02:00
< div className = "tag-bar" style = { { paddingLeft : theme.editorPaddingLeft , display : 'flex' , flexDirection : 'row' , alignItems : 'center' , height : 40 } } >
2020-09-15 15:01:07 +02:00
{ renderTagButton ( ) }
{ renderTagBar ( ) }
< / div >
2024-08-02 15:47:26 +02:00
< WarningBanner bodyEditor = { props . bodyEditor } / >
2020-05-02 17:41:07 +02:00
< / div >
< / div >
) ;
}
2024-11-08 17:32:05 +02:00
interface ConnectProps {
windowId : string ;
}
2020-05-02 17:41:07 +02:00
2024-11-08 17:32:05 +02:00
const mapStateToProps = ( state : AppState , ownProps : ConnectProps ) = > {
const whenClauseContext = stateToWhenClauseContext ( state , { windowId : ownProps.windowId } ) ;
const windowState = stateUtils . windowStateById ( state , ownProps . windowId ) ;
const noteId = stateUtils . selectedNoteId ( windowState ) ;
let bodyEditor = windowState . editorCodeView ? 'CodeMirror6' : 'TinyMCE' ;
if ( state . settings . isSafeMode ) {
bodyEditor = 'PlainText' ;
} else if ( windowState . editorCodeView && state . settings [ 'editor.legacyMarkdown' ] ) {
bodyEditor = 'CodeMirror5' ;
}
2020-05-02 17:41:07 +02:00
return {
2024-11-08 17:32:05 +02:00
noteId ,
bodyEditor ,
2020-05-02 17:41:07 +02:00
isProvisional : state.provisionalNoteIds.includes ( noteId ) ,
2024-11-08 17:32:05 +02:00
notes : windowState.notes ,
selectedNoteIds : windowState.selectedNoteIds ,
selectedFolderId : windowState.selectedFolderId ,
2020-05-02 17:41:07 +02:00
editorNoteStatuses : state.editorNoteStatuses ,
2020-09-15 15:01:07 +02:00
themeId : state.settings.theme ,
2020-05-02 17:41:07 +02:00
watchedNoteFiles : state.watchedNoteFiles ,
2024-11-08 17:32:05 +02:00
notesParentType : windowState.notesParentType ,
selectedNoteTags : windowState.selectedNoteTags ,
2020-05-02 17:41:07 +02:00
lastEditorScrollPercents : state.lastEditorScrollPercents ,
2024-11-08 17:32:05 +02:00
selectedNoteHash : windowState.selectedNoteHash ,
2020-05-02 17:41:07 +02:00
searches : state.searches ,
2024-11-08 17:32:05 +02:00
selectedSearchId : windowState.selectedSearchId ,
customCss : state.customViewerCss ,
noteVisiblePanes : windowState.noteVisiblePanes ,
2020-07-22 20:03:31 +02:00
watchedResources : state.watchedResources ,
2020-09-06 14:07:00 +02:00
highlightedWords : state.highlightedWords ,
2020-10-09 19:35:46 +02:00
plugins : state.pluginService.plugins ,
2024-11-10 16:04:46 +02:00
pluginHtmlContents : state.pluginService.pluginHtmlContents ,
'plugins.shownEditorViewIds' : state . settings [ 'plugins.shownEditorViewIds' ] || [ ] ,
2020-10-18 22:52:10 +02:00
toolbarButtonInfos : toolbarButtonUtils.commandsToToolbarButtons ( [
2020-10-09 19:35:46 +02:00
'historyBackward' ,
'historyForward' ,
'toggleEditors' ,
2020-10-10 14:32:30 +02:00
'toggleExternalEditing' ,
2020-10-18 22:52:10 +02:00
] , whenClauseContext ) ,
setTagsToolbarButtonInfo : toolbarButtonUtils.commandsToToolbarButtons ( [
2020-10-09 19:35:46 +02:00
'setTags' ,
2024-12-11 14:31:05 +02:00
] , whenClauseContext ) [ 0 ] as ToolbarButtonInfo ,
2021-08-14 13:19:53 +02:00
contentMaxWidth : state.settings [ 'style.editor.contentMaxWidth' ] ,
2021-10-30 18:51:19 +02:00
isSafeMode : state.settings.isSafeMode ,
2023-07-16 18:42:42 +02:00
useCustomPdfViewer : false ,
syncUserId : state.settings [ 'sync.userId' ] ,
2023-08-14 19:33:48 +02:00
shareCacheSetting : state.settings [ 'sync.shareCache' ] ,
2023-12-13 21:24:58 +02:00
searchResults : state.searchResults ,
2020-05-02 17:41:07 +02:00
} ;
} ;
2024-11-08 17:32:05 +02:00
export default connect ( mapStateToProps ) ( NoteEditorContent ) ;