You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
Desktop: Multiple window support (#11181)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
116
.eslintignore
116
.eslintignore
@@ -156,6 +156,7 @@ packages/app-desktop/commands/exportFolders.js
|
||||
packages/app-desktop/commands/exportNotes.js
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
packages/app-desktop/commands/index.js
|
||||
packages/app-desktop/commands/openNoteInNewWindow.js
|
||||
packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/renderMarkup.test.js
|
||||
packages/app-desktop/commands/renderMarkup.js
|
||||
@@ -209,60 +210,14 @@ packages/app-desktop/gui/KeymapConfig/styles/index.js
|
||||
packages/app-desktop/gui/KeymapConfig/utils/getLabel.js
|
||||
packages/app-desktop/gui/KeymapConfig/utils/useCommandStatus.js
|
||||
packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js
|
||||
packages/app-desktop/gui/MainScreen/MainScreen.js
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
||||
packages/app-desktop/gui/MainScreen/commands/deleteFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/duplicateNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/editAlarm.js
|
||||
packages/app-desktop/gui/MainScreen/commands/exportPdf.js
|
||||
packages/app-desktop/gui/MainScreen/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/MainScreen/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/MainScreen/commands/index.js
|
||||
packages/app-desktop/gui/MainScreen/commands/leaveSharedFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/newFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/newNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/newSubFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/newTodo.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openFolderDialog.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openItem.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/print.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/resetLayout.js
|
||||
packages/app-desktop/gui/MainScreen/commands/restoreFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/restoreNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/search.js
|
||||
packages/app-desktop/gui/MainScreen/commands/setTags.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showModalMessage.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showNoteContentProperties.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showPrompt.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.test.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleMenuBar.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleNoteList.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleNoteType.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderField.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderReverse.js
|
||||
packages/app-desktop/gui/MainScreen/commands/togglePerFolderSortOrder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleSideBar.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/MainScreen.js
|
||||
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
|
||||
packages/app-desktop/gui/MenuBar.js
|
||||
packages/app-desktop/gui/MultiNoteActions.js
|
||||
packages/app-desktop/gui/Navigator.js
|
||||
packages/app-desktop/gui/NewWindowOrIFrame.js
|
||||
packages/app-desktop/gui/NoteContentPropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/EditorWindow.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.js
|
||||
@@ -323,6 +278,7 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
||||
@@ -455,7 +411,66 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/commandPalette.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newSubFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newTodo.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolderDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openItem.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openTag.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/print.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameTag.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPrompt.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteList.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteType.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderField.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderReverse.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useDocument.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useElementHeight.js
|
||||
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
|
||||
@@ -544,6 +559,7 @@ packages/app-desktop/utils/isSafeToOpen.test.js
|
||||
packages/app-desktop/utils/isSafeToOpen.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-desktop/utils/window/types.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/index.js
|
||||
packages/app-mobile/commands/newNote.test.js
|
||||
@@ -994,6 +1010,8 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/useNowEffect.test.js
|
||||
packages/lib/hooks/useNowEffect.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.test.js
|
||||
|
||||
116
.gitignore
vendored
116
.gitignore
vendored
@@ -133,6 +133,7 @@ packages/app-desktop/commands/exportFolders.js
|
||||
packages/app-desktop/commands/exportNotes.js
|
||||
packages/app-desktop/commands/focusElement.js
|
||||
packages/app-desktop/commands/index.js
|
||||
packages/app-desktop/commands/openNoteInNewWindow.js
|
||||
packages/app-desktop/commands/openProfileDirectory.js
|
||||
packages/app-desktop/commands/renderMarkup.test.js
|
||||
packages/app-desktop/commands/renderMarkup.js
|
||||
@@ -186,60 +187,14 @@ packages/app-desktop/gui/KeymapConfig/styles/index.js
|
||||
packages/app-desktop/gui/KeymapConfig/utils/getLabel.js
|
||||
packages/app-desktop/gui/KeymapConfig/utils/useCommandStatus.js
|
||||
packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js
|
||||
packages/app-desktop/gui/MainScreen/MainScreen.js
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
||||
packages/app-desktop/gui/MainScreen/commands/deleteFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/duplicateNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/editAlarm.js
|
||||
packages/app-desktop/gui/MainScreen/commands/exportPdf.js
|
||||
packages/app-desktop/gui/MainScreen/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/MainScreen/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/MainScreen/commands/index.js
|
||||
packages/app-desktop/gui/MainScreen/commands/leaveSharedFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/newFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/newNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/newSubFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/newTodo.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openFolderDialog.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openItem.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/print.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/resetLayout.js
|
||||
packages/app-desktop/gui/MainScreen/commands/restoreFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/restoreNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/search.js
|
||||
packages/app-desktop/gui/MainScreen/commands/setTags.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showModalMessage.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showNoteContentProperties.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showPrompt.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.test.js
|
||||
packages/app-desktop/gui/MainScreen/commands/showSpellCheckerMenu.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleEditors.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleLayoutMoveMode.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleMenuBar.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleNoteList.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleNoteType.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderField.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleNotesSortOrderReverse.js
|
||||
packages/app-desktop/gui/MainScreen/commands/togglePerFolderSortOrder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleSideBar.js
|
||||
packages/app-desktop/gui/MainScreen/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/MainScreen.js
|
||||
packages/app-desktop/gui/MasterPasswordDialog/Dialog.js
|
||||
packages/app-desktop/gui/MenuBar.js
|
||||
packages/app-desktop/gui/MultiNoteActions.js
|
||||
packages/app-desktop/gui/Navigator.js
|
||||
packages/app-desktop/gui/NewWindowOrIFrame.js
|
||||
packages/app-desktop/gui/NoteContentPropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/EditorWindow.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.js
|
||||
@@ -300,6 +255,7 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
||||
@@ -432,7 +388,66 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/commandPalette.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newSubFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newTodo.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolderDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openItem.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openTag.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/print.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameTag.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreFolder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPrompt.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteList.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteType.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderField.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderReverse.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useDocument.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useElementHeight.js
|
||||
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
|
||||
@@ -521,6 +536,7 @@ packages/app-desktop/utils/isSafeToOpen.test.js
|
||||
packages/app-desktop/utils/isSafeToOpen.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||
packages/app-desktop/utils/window/types.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/commands/index.js
|
||||
packages/app-mobile/commands/newNote.test.js
|
||||
@@ -971,6 +987,8 @@ packages/lib/geolocation-node.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/useNowEffect.test.js
|
||||
packages/lib/hooks/useNowEffect.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.test.js
|
||||
|
||||
@@ -17,6 +17,8 @@ import { _ } from '@joplin/lib/locale';
|
||||
import restartInSafeModeFromMain from './utils/restartInSafeModeFromMain';
|
||||
import handleCustomProtocols, { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
|
||||
import { clearTimeout, setTimeout } from 'timers';
|
||||
import { resolve } from 'path';
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
|
||||
interface RendererProcessQuitReply {
|
||||
canClose: boolean;
|
||||
@@ -27,21 +29,30 @@ interface PluginWindows {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default class ElectronAppWrapper {
|
||||
type SecondaryWindowId = string;
|
||||
interface SecondaryWindowData {
|
||||
electronId: number;
|
||||
}
|
||||
|
||||
export default class ElectronAppWrapper {
|
||||
private logger_: Logger = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private electronApp_: any;
|
||||
private env_: string;
|
||||
private isDebugMode_: boolean;
|
||||
private profilePath_: string;
|
||||
|
||||
private win_: BrowserWindow = null;
|
||||
private mainWindowHidden_ = true;
|
||||
private pluginWindows_: PluginWindows = {};
|
||||
private secondaryWindows_: Map<SecondaryWindowId, SecondaryWindowData> = new Map();
|
||||
|
||||
private willQuitApp_ = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private tray_: any = null;
|
||||
private buildDir_: string = null;
|
||||
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
|
||||
private pluginWindows_: PluginWindows = {};
|
||||
|
||||
private initialCallbackUrl_: string = null;
|
||||
private updaterService_: AutoUpdaterService = null;
|
||||
private customProtocolHandler_: CustomProtocolHandler = null;
|
||||
@@ -68,10 +79,26 @@ export default class ElectronAppWrapper {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public window() {
|
||||
public mainWindow() {
|
||||
return this.win_;
|
||||
}
|
||||
|
||||
public activeWindow() {
|
||||
return BrowserWindow.getFocusedWindow() ?? this.win_;
|
||||
}
|
||||
|
||||
public windowById(joplinId: string) {
|
||||
if (joplinId === defaultWindowId) {
|
||||
return this.mainWindow();
|
||||
}
|
||||
|
||||
const windowData = this.secondaryWindows_.get(joplinId);
|
||||
if (windowData !== undefined) {
|
||||
return BrowserWindow.fromId(windowData.electronId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public env() {
|
||||
return this.env_;
|
||||
}
|
||||
@@ -210,6 +237,15 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
});
|
||||
|
||||
this.mainWindowHidden_ = !windowOptions.show;
|
||||
this.win_.on('hide', () => {
|
||||
this.mainWindowHidden_ = true;
|
||||
});
|
||||
|
||||
this.win_.on('show', () => {
|
||||
this.mainWindowHidden_ = false;
|
||||
});
|
||||
|
||||
void this.win_.loadURL(url.format({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
protocol: 'file:',
|
||||
@@ -249,6 +285,11 @@ export default class ElectronAppWrapper {
|
||||
// Script-controlled pages: Used for opening notes in new windows
|
||||
return {
|
||||
action: 'allow',
|
||||
overrideBrowserWindowOptions: {
|
||||
webPreferences: {
|
||||
preload: resolve(__dirname, './utils/window/secondaryWindowPreload.js'),
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (event.url.match(/^https?:\/\//)) {
|
||||
void bridge().openExternal(event.url);
|
||||
@@ -281,7 +322,8 @@ export default class ElectronAppWrapper {
|
||||
this.hide();
|
||||
}
|
||||
} else {
|
||||
if (this.trayShown() && !this.willQuitApp_) {
|
||||
const hasBackgroundWindows = this.secondaryWindows_.size > 0;
|
||||
if ((hasBackgroundWindows || this.trayShown()) && !this.willQuitApp_) {
|
||||
event.preventDefault();
|
||||
this.win_.hide();
|
||||
} else {
|
||||
@@ -311,6 +353,23 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('secondary-window-added', (event, windowId: string) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
const electronWindowId = window?.id;
|
||||
this.secondaryWindows_.set(windowId, { electronId: electronWindowId });
|
||||
|
||||
window.once('close', () => {
|
||||
this.secondaryWindows_.delete(windowId);
|
||||
|
||||
const allSecondaryWindowsClosed = this.secondaryWindows_.size === 0;
|
||||
const mainWindowVisuallyClosed = this.mainWindowHidden_;
|
||||
if (allSecondaryWindowsClosed && mainWindowVisuallyClosed && !this.trayShown()) {
|
||||
// Gracefully quit the app if the user has closed all windows
|
||||
this.win_.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
ipcMain.on('asynchronous-message', (_event: any, message: string, args: any) => {
|
||||
if (message === 'appCloseReply') {
|
||||
@@ -442,11 +501,11 @@ export default class ElectronAppWrapper {
|
||||
this.tray_.setContextMenu(contextMenu);
|
||||
|
||||
this.tray_.on('click', () => {
|
||||
if (!this.window()) {
|
||||
if (!this.mainWindow()) {
|
||||
console.warn('The window object was not available during the click event from tray icon');
|
||||
return;
|
||||
}
|
||||
this.window().show();
|
||||
this.mainWindow().show();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Cannot create tray', error);
|
||||
@@ -473,7 +532,7 @@ export default class ElectronAppWrapper {
|
||||
// Someone tried to open a second instance - focus our window instead
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
this.electronApp_.on('second-instance', (_e: any, argv: string[]) => {
|
||||
const win = this.window();
|
||||
const win = this.mainWindow();
|
||||
if (!win) return;
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.show();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppState } from './app.reducer';
|
||||
import { AppState, createAppDefaultWindowState } from './app.reducer';
|
||||
import appReducer, { createAppDefaultState } from './app.reducer';
|
||||
|
||||
describe('app.reducer', () => {
|
||||
@@ -47,4 +47,28 @@ describe('app.reducer', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('showing a dialog in one window should hide dialogs with the same ID in background windows', () => {
|
||||
const state: AppState = {
|
||||
...createAppDefaultState({}, {}),
|
||||
backgroundWindows: {
|
||||
testWindow: {
|
||||
...createAppDefaultWindowState(),
|
||||
windowId: 'testWindow',
|
||||
|
||||
visibleDialogs: {
|
||||
testDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const newState = appReducer(state, {
|
||||
type: 'VISIBLE_DIALOGS_ADD',
|
||||
name: 'testDialog',
|
||||
});
|
||||
|
||||
expect(newState.backgroundWindows.testWindow.visibleDialogs).toEqual({});
|
||||
expect(newState.visibleDialogs).toEqual({ testDialog: true });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import produce from 'immer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { defaultState, State } from '@joplin/lib/reducer';
|
||||
import { defaultState, defaultWindowState, State, WindowState } from '@joplin/lib/reducer';
|
||||
import iterateItems from './gui/ResizableLayout/utils/iterateItems';
|
||||
import { LayoutItem } from './gui/ResizableLayout/utils/types';
|
||||
import validateLayout from './gui/ResizableLayout/utils/validateLayout';
|
||||
@@ -30,56 +30,89 @@ export interface EditorScrollPercents {
|
||||
[noteId: string]: number;
|
||||
}
|
||||
|
||||
export interface AppState extends State {
|
||||
export interface VisibleDialogs {
|
||||
[dialogKey: string]: boolean;
|
||||
}
|
||||
|
||||
export interface AppWindowState extends WindowState {
|
||||
noteVisiblePanes: string[];
|
||||
editorCodeView: boolean;
|
||||
visibleDialogs: VisibleDialogs;
|
||||
dialogs: AppStateDialog[];
|
||||
devToolsVisible: boolean;
|
||||
}
|
||||
|
||||
interface BackgroundWindowStates {
|
||||
[windowId: string]: AppWindowState;
|
||||
}
|
||||
|
||||
export interface AppState extends State, AppWindowState {
|
||||
backgroundWindows: BackgroundWindowStates;
|
||||
|
||||
route: AppStateRoute;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
navHistory: any[];
|
||||
noteVisiblePanes: string[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
windowContentSize: any;
|
||||
watchedNoteFiles: string[];
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
devToolsVisible: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
visibleDialogs: any; // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: string;
|
||||
layoutMoveMode: boolean;
|
||||
startupPluginsLoaded: boolean;
|
||||
modalOverlayMessage: string|null;
|
||||
|
||||
// Extra reducer keys go here
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
watchedResources: any;
|
||||
mainLayout: LayoutItem;
|
||||
dialogs: AppStateDialog[];
|
||||
isResettingLayout: boolean;
|
||||
}
|
||||
|
||||
export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
return {
|
||||
...defaultWindowState,
|
||||
visibleDialogs: {},
|
||||
dialogs: [],
|
||||
noteVisiblePanes: ['editor', 'viewer'],
|
||||
editorCodeView: true,
|
||||
devToolsVisible: false,
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export function createAppDefaultState(windowContentSize: any, resourceEditWatcherDefaultState: any): AppState {
|
||||
return {
|
||||
...defaultState,
|
||||
...createAppDefaultWindowState(),
|
||||
route: {
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
props: {},
|
||||
},
|
||||
navHistory: [],
|
||||
noteVisiblePanes: ['editor', 'viewer'],
|
||||
windowContentSize, // bridge().windowContentSize(),
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
devToolsVisible: false,
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: null,
|
||||
layoutMoveMode: false,
|
||||
mainLayout: null,
|
||||
startupPluginsLoaded: false,
|
||||
dialogs: [],
|
||||
isResettingLayout: false,
|
||||
modalOverlayMessage: null,
|
||||
...resourceEditWatcherDefaultState,
|
||||
};
|
||||
}
|
||||
|
||||
const hideBackgroundDialogsWithId = produce((state: AppState, id: string) => {
|
||||
for (const windowId of Object.keys(state.backgroundWindows)) {
|
||||
const win = state.backgroundWindows[windowId];
|
||||
if (id in win.visibleDialogs) {
|
||||
delete win.visibleDialogs[id];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export default function(state: AppState, action: any) {
|
||||
let newState = state;
|
||||
@@ -171,9 +204,17 @@ export default function(state: AppState, action: any) {
|
||||
break;
|
||||
|
||||
case 'NOTE_VISIBLE_PANES_SET':
|
||||
newState = {
|
||||
...state,
|
||||
noteVisiblePanes: action.panes,
|
||||
};
|
||||
break;
|
||||
|
||||
newState = { ...state };
|
||||
newState.noteVisiblePanes = action.panes;
|
||||
case 'EDITOR_CODE_VIEW_CHANGE':
|
||||
newState = {
|
||||
...state,
|
||||
editorCodeView: action.value,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'MAIN_LAYOUT_SET':
|
||||
@@ -217,6 +258,14 @@ export default function(state: AppState, action: any) {
|
||||
|
||||
break;
|
||||
|
||||
case 'SHOW_MODAL_MESSAGE':
|
||||
newState = { ...newState, modalOverlayMessage: action.message };
|
||||
break;
|
||||
|
||||
case 'HIDE_MODAL_MESSAGE':
|
||||
newState = { ...newState, modalOverlayMessage: null };
|
||||
break;
|
||||
|
||||
case 'NOTE_FILE_WATCHER_ADD':
|
||||
|
||||
if (newState.watchedNoteFiles.indexOf(action.id) < 0) {
|
||||
@@ -272,12 +321,14 @@ export default function(state: AppState, action: any) {
|
||||
newState = { ...state };
|
||||
newState.visibleDialogs = { ...newState.visibleDialogs };
|
||||
newState.visibleDialogs[action.name] = true;
|
||||
newState = hideBackgroundDialogsWithId(newState, action.name);
|
||||
break;
|
||||
|
||||
case 'VISIBLE_DIALOGS_REMOVE':
|
||||
newState = { ...state };
|
||||
newState.visibleDialogs = { ...newState.visibleDialogs };
|
||||
delete newState.visibleDialogs[action.name];
|
||||
newState = hideBackgroundDialogsWithId(newState, action.name);
|
||||
break;
|
||||
|
||||
case 'FOCUS_SET':
|
||||
|
||||
@@ -34,8 +34,8 @@ const Menu = bridge().Menu;
|
||||
const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
import RevisionService from '@joplin/lib/services/RevisionService';
|
||||
import MigrationService from '@joplin/lib/services/MigrationService';
|
||||
import { loadCustomCss, injectCustomStyles } from '@joplin/lib/CssUtils';
|
||||
import mainScreenCommands from './gui/MainScreen/commands/index';
|
||||
import { loadCustomCss } from '@joplin/lib/CssUtils';
|
||||
import mainScreenCommands from './gui/WindowCommandsAndDialogs/commands/index';
|
||||
import noteEditorCommands from './gui/NoteEditor/commands/index';
|
||||
import noteListCommands from './gui/NoteList/commands/index';
|
||||
import noteListControlsCommands from './gui/NoteListControls/commands/index';
|
||||
@@ -151,10 +151,6 @@ class Application extends BaseApplication {
|
||||
void this.setupOcrService();
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'style.editor.fontFamily' || action.type === 'SETTING_UPDATE_ALL') {
|
||||
this.updateEditorFont();
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'windowContentZoomFactor' || action.type === 'SETTING_UPDATE_ALL') {
|
||||
webFrame.setZoomFactor(Setting.value('windowContentZoomFactor') / 100);
|
||||
}
|
||||
@@ -218,7 +214,7 @@ class Application extends BaseApplication {
|
||||
app.destroyTray();
|
||||
} else {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: _('Open %s', app.electronApp().name), click: () => { app.window().show(); } },
|
||||
{ label: _('Open %s', app.electronApp().name), click: () => { app.mainWindow().show(); } },
|
||||
{ type: 'separator' },
|
||||
{ label: _('Quit'), click: () => { void app.quit(); } },
|
||||
]);
|
||||
@@ -226,23 +222,6 @@ class Application extends BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
public updateEditorFont() {
|
||||
const fontFamilies = [];
|
||||
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
|
||||
fontFamilies.push('\'Avenir Next\', Avenir, Arial, sans-serif');
|
||||
|
||||
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
//
|
||||
// Note: Be careful about the specificity here. Incorrect specificity can break monospaced fonts in tables.
|
||||
|
||||
const css = `.CodeMirror5 *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.type = 'text/css';
|
||||
styleTag.appendChild(document.createTextNode(css));
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
|
||||
public setupContextMenu() {
|
||||
// bridge().setupContextMenu((misspelledWord: string, dictionarySuggestions: string[]) => {
|
||||
// let output = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions);
|
||||
@@ -430,6 +409,23 @@ class Application extends BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
private async setupCustomCss() {
|
||||
const chromeCssPath = Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP);
|
||||
if (await shim.fsDriver().exists(chromeCssPath)) {
|
||||
this.store().dispatch({
|
||||
// Main window custom CSS
|
||||
type: 'CUSTOM_CHROME_CSS_ADD',
|
||||
filePath: chromeCssPath,
|
||||
});
|
||||
}
|
||||
|
||||
this.store().dispatch({
|
||||
// Markdown preview pane
|
||||
type: 'CUSTOM_VIEWER_CSS_APPEND',
|
||||
css: await loadCustomCss(Setting.customCssFilePath(Setting.customCssFilenames.RENDERED_MARKDOWN)),
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async start(argv: string[], startOptions: StartOptions = null): Promise<any> {
|
||||
// If running inside a package, the command line, instead of being "node.exe <path> <flags>" is "joplin.exe <flags>" so
|
||||
@@ -444,7 +440,7 @@ class Application extends BaseApplication {
|
||||
|
||||
if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) {
|
||||
reg.logger().info('app.start: doing upgradeSyncTarget action');
|
||||
bridge().window().show();
|
||||
bridge().mainWindow().show();
|
||||
return { action: 'upgradeSyncTarget' };
|
||||
}
|
||||
|
||||
@@ -462,9 +458,6 @@ class Application extends BaseApplication {
|
||||
syncDebugLog.info(`Profile dir: ${dir}`);
|
||||
}
|
||||
|
||||
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
|
||||
await injectCustomStyles('appStyles', Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP));
|
||||
|
||||
this.setupAutoUpdaterService();
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
@@ -541,6 +534,8 @@ class Application extends BaseApplication {
|
||||
items: tags,
|
||||
});
|
||||
|
||||
await this.setupCustomCss();
|
||||
|
||||
// const masterKeys = await MasterKey.all();
|
||||
|
||||
// this.dispatch({
|
||||
@@ -583,13 +578,6 @@ class Application extends BaseApplication {
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
});
|
||||
|
||||
// Loads custom Markdown preview styles
|
||||
const cssString = await loadCustomCss(Setting.customCssFilePath(Setting.customCssFilenames.RENDERED_MARKDOWN));
|
||||
this.store().dispatch({
|
||||
type: 'CUSTOM_CSS_APPEND',
|
||||
css: cssString,
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_DEVTOOLS_SET',
|
||||
value: Setting.value('flagOpenDevTools'),
|
||||
@@ -602,7 +590,7 @@ class Application extends BaseApplication {
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
const runAutoUpdateCheck = () => {
|
||||
if (Setting.value('autoUpdateEnabled')) {
|
||||
void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
void checkForUpdates(true, bridge().mainWindow(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -623,9 +611,9 @@ class Application extends BaseApplication {
|
||||
}, 1000 * 60 * 60);
|
||||
|
||||
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
|
||||
bridge().window().hide();
|
||||
bridge().mainWindow().hide();
|
||||
} else {
|
||||
bridge().window().show();
|
||||
bridge().mainWindow().show();
|
||||
}
|
||||
|
||||
void ShareService.instance().maintenance();
|
||||
@@ -698,6 +686,15 @@ class Application extends BaseApplication {
|
||||
Setting.setValue('linking.extraAllowedExtensions', newExtensions);
|
||||
});
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
const currentWindowId = this.store().getState().windowId;
|
||||
this.dispatch({
|
||||
type: 'WINDOW_FOCUS',
|
||||
windowId: 'default',
|
||||
lastWindowId: currentWindowId,
|
||||
});
|
||||
});
|
||||
|
||||
await this.initPluginService();
|
||||
|
||||
this.setupContextMenu();
|
||||
|
||||
@@ -14,6 +14,7 @@ import { extname, normalize } from 'path';
|
||||
import isSafeToOpen from './utils/isSafeToOpen';
|
||||
import { closeSync, openSync, readSync, statSync } from 'fs';
|
||||
import { KB } from '@joplin/utils/bytes';
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
|
||||
interface LastSelectedPath {
|
||||
file: string;
|
||||
@@ -234,7 +235,7 @@ export class Bridge {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
public setupContextMenu(_spellCheckerMenuItemsHandler: Function) {
|
||||
require('electron-context-menu')({
|
||||
allWindows: [this.window()],
|
||||
allWindows: [this.mainWindow()],
|
||||
|
||||
electronApp: this.electronApp(),
|
||||
|
||||
@@ -259,8 +260,29 @@ export class Bridge {
|
||||
});
|
||||
}
|
||||
|
||||
public window() {
|
||||
return this.electronWrapper_.window();
|
||||
public mainWindow() {
|
||||
return this.electronWrapper_.mainWindow();
|
||||
}
|
||||
|
||||
public activeWindow() {
|
||||
return this.electronWrapper_.activeWindow();
|
||||
}
|
||||
|
||||
public windowById(id: string) {
|
||||
return this.electronWrapper_.windowById(id);
|
||||
}
|
||||
|
||||
// Switches to the window with the given ID, but only if that window was not the
|
||||
// last focused window
|
||||
public switchToWindow(windowId: string) {
|
||||
const targetWindow = this.windowById(windowId);
|
||||
if (this.activeWindow() !== this.windowById(windowId)) {
|
||||
targetWindow.show();
|
||||
}
|
||||
}
|
||||
|
||||
public switchToMainWindow() {
|
||||
this.switchToWindow(defaultWindowId);
|
||||
}
|
||||
|
||||
public showItemInFolder(fullPath: string) {
|
||||
@@ -272,36 +294,31 @@ export class Bridge {
|
||||
return new BrowserWindow(options);
|
||||
}
|
||||
|
||||
// Note: This provides the size of the main window. Prefer CSS where possible.
|
||||
public windowContentSize() {
|
||||
if (!this.window()) return { width: 0, height: 0 };
|
||||
const s = this.window().getContentSize();
|
||||
return { width: s[0], height: s[1] };
|
||||
}
|
||||
|
||||
public windowSize() {
|
||||
if (!this.window()) return { width: 0, height: 0 };
|
||||
const s = this.window().getSize();
|
||||
if (!this.mainWindow()) return { width: 0, height: 0 };
|
||||
const s = this.mainWindow().getContentSize();
|
||||
return { width: s[0], height: s[1] };
|
||||
}
|
||||
|
||||
public windowSetSize(width: number, height: number) {
|
||||
if (!this.window()) return;
|
||||
return this.window().setSize(width, height);
|
||||
if (!this.mainWindow()) return;
|
||||
return this.mainWindow().setSize(width, height);
|
||||
}
|
||||
|
||||
public openDevTools() {
|
||||
return this.window().webContents.openDevTools();
|
||||
return this.activeWindow().webContents.openDevTools();
|
||||
}
|
||||
|
||||
public closeDevTools() {
|
||||
return this.window().webContents.closeDevTools();
|
||||
return this.activeWindow().webContents.closeDevTools();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async showSaveDialog(options: any) {
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file;
|
||||
const { filePath } = await dialog.showSaveDialog(this.window(), options);
|
||||
const { filePath } = await dialog.showSaveDialog(this.activeWindow(), options);
|
||||
if (filePath) {
|
||||
this.lastSelectedPaths_.file = filePath;
|
||||
}
|
||||
@@ -316,7 +333,7 @@ export class Bridge {
|
||||
if (!('defaultPath' in options) && (this.lastSelectedPaths_ as any)[fileType]) options.defaultPath = (this.lastSelectedPaths_ as any)[fileType];
|
||||
if (!('createDirectory' in options)) options.createDirectory = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const { filePaths } = await dialog.showOpenDialog(this.window(), options as any);
|
||||
const { filePaths } = await dialog.showOpenDialog(this.activeWindow(), options as any);
|
||||
if (filePaths && filePaths.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
(this.lastSelectedPaths_ as any)[fileType] = dirname(filePaths[0]);
|
||||
@@ -327,7 +344,7 @@ export class Bridge {
|
||||
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private showMessageBox_(window: any, options: MessageDialogOptions): number {
|
||||
if (!window) window = this.window();
|
||||
if (!window) window = this.activeWindow();
|
||||
return dialog.showMessageBoxSync(window, { message: '', ...options });
|
||||
}
|
||||
|
||||
@@ -337,7 +354,7 @@ export class Bridge {
|
||||
...options,
|
||||
};
|
||||
|
||||
return this.showMessageBox_(this.window(), {
|
||||
return this.showMessageBox_(this.activeWindow(), {
|
||||
type: 'error',
|
||||
message: message,
|
||||
buttons: options.buttons,
|
||||
@@ -350,7 +367,7 @@ export class Bridge {
|
||||
...options,
|
||||
};
|
||||
|
||||
const result = this.showMessageBox_(this.window(), { type: 'question',
|
||||
const result = this.showMessageBox_(this.activeWindow(), { type: 'question',
|
||||
message: message,
|
||||
cancelId: 1,
|
||||
buttons: options.buttons, ...options });
|
||||
@@ -360,7 +377,7 @@ export class Bridge {
|
||||
|
||||
/* returns the index of the clicked button */
|
||||
public showMessageBox(message: string, options: MessageDialogOptions = {}) {
|
||||
const result = this.showMessageBox_(this.window(), { type: 'question',
|
||||
const result = this.showMessageBox_(this.activeWindow(), { type: 'question',
|
||||
message: message,
|
||||
buttons: [_('OK'), _('Cancel')], ...options });
|
||||
|
||||
@@ -369,7 +386,7 @@ export class Bridge {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public showInfoMessageBox(message: string, options: any = {}) {
|
||||
const result = this.showMessageBox_(this.window(), { type: 'info',
|
||||
const result = this.showMessageBox_(this.activeWindow(), { type: 'info',
|
||||
message: message,
|
||||
buttons: [_('OK')], ...options });
|
||||
return result === 0;
|
||||
@@ -413,7 +430,7 @@ export class Bridge {
|
||||
const allowOpenId = 2;
|
||||
const learnMoreId = 1;
|
||||
const fileExtensionDescription = JSON.stringify(fileExtension);
|
||||
const result = await dialog.showMessageBox(this.window(), {
|
||||
const result = await dialog.showMessageBox(this.activeWindow(), {
|
||||
title: _('Unknown file type'),
|
||||
message:
|
||||
_('Joplin doesn\'t recognise the %s extension. Opening this file could be dangerous. What would you like to do?', fileExtensionDescription),
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as exportDeletionLog from './exportDeletionLog';
|
||||
import * as exportFolders from './exportFolders';
|
||||
import * as exportNotes from './exportNotes';
|
||||
import * as focusElement from './focusElement';
|
||||
import * as openNoteInNewWindow from './openNoteInNewWindow';
|
||||
import * as openProfileDirectory from './openProfileDirectory';
|
||||
import * as renderMarkup from './renderMarkup';
|
||||
import * as replaceMisspelling from './replaceMisspelling';
|
||||
@@ -27,6 +28,7 @@ const index: any[] = [
|
||||
exportFolders,
|
||||
exportNotes,
|
||||
focusElement,
|
||||
openNoteInNewWindow,
|
||||
openProfileDirectory,
|
||||
renderMarkup,
|
||||
replaceMisspelling,
|
||||
|
||||
36
packages/app-desktop/commands/openNoteInNewWindow.ts
Normal file
36
packages/app-desktop/commands/openNoteInNewWindow.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { createAppDefaultWindowState } from '../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openNoteInNewWindow',
|
||||
label: () => _('Edit in new window'),
|
||||
iconName: 'icon-share',
|
||||
};
|
||||
|
||||
let idCounter = 0;
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteId: string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
const note = await Note.load(noteId, { fields: ['parent_id'] });
|
||||
context.dispatch({
|
||||
type: 'WINDOW_OPEN',
|
||||
noteId,
|
||||
folderId: note.parent_id,
|
||||
windowId: `window-${noteId}-${idCounter++}`,
|
||||
defaultAppWindowState: {
|
||||
...createAppDefaultWindowState(),
|
||||
noteVisiblePanes: Setting.value('noteVisiblePanes'),
|
||||
editorCodeView: Setting.value('editor.codeView'),
|
||||
},
|
||||
});
|
||||
},
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
};
|
||||
};
|
||||
@@ -22,7 +22,7 @@ export const runtime = (): CommandRuntime => {
|
||||
if (!modalDialogVisible && (isInsideContainer(activeElement, 'codeMirrorEditor') || isInsideContainer(activeElement, 'tox-edit-area__iframe'))) {
|
||||
await CommandService.instance().execute('replaceSelection', suggestion);
|
||||
} else {
|
||||
bridge().window().webContents.replaceMisspelling(suggestion);
|
||||
bridge().activeWindow().webContents.replaceMisspelling(suggestion);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -229,7 +229,7 @@ export default function(props: Props) {
|
||||
];
|
||||
|
||||
const menu = bridge().Menu.buildFromTemplate(template);
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().mainWindow() });
|
||||
}, [onInstall, onBrowsePlugins]);
|
||||
|
||||
const onSearchQueryChange = useCallback((event: OnChangeEvent) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { blur, focus } from '@joplin/lib/utils/focusHandler';
|
||||
import useDocument from './hooks/useDocument';
|
||||
|
||||
type OnCancelListener = ()=> void;
|
||||
|
||||
@@ -9,10 +10,14 @@ interface Props {
|
||||
className?: string;
|
||||
onCancel?: OnCancelListener;
|
||||
contentStyle?: React.CSSProperties;
|
||||
contentFillsScreen?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Dialog: React.FC<Props> = props => {
|
||||
const [containerElement, setContainerElement] = useState<HTMLDivElement|null>(null);
|
||||
const containerDocument = useDocument(containerElement);
|
||||
|
||||
// For correct focus handling, the dialog element needs to be managed separately from React. In particular,
|
||||
// just after creating the dialog, we need to call .showModal() and just **before** closing the dialog, we
|
||||
// need to call .close(). This second requirement is particularly difficult, as this needs to happen even
|
||||
@@ -21,7 +26,7 @@ const Dialog: React.FC<Props> = props => {
|
||||
// Because useEffect cleanup can happen after an element is removed from the HTML DOM, the dialog is managed
|
||||
// using native HTML APIs. This allows us to call .close() while the dialog is still attached to the DOM, which
|
||||
// allows the browser to restore the focus from before the dialog was opened.
|
||||
const dialogElement = useDialogElement(props.onCancel);
|
||||
const dialogElement = useDialogElement(containerDocument, props.onCancel);
|
||||
useDialogClassNames(dialogElement, props.className);
|
||||
|
||||
const [contentRendered, setContentRendered] = useState(false);
|
||||
@@ -34,6 +39,16 @@ const Dialog: React.FC<Props> = props => {
|
||||
}
|
||||
}, [dialogElement, contentRendered]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialogElement) return;
|
||||
|
||||
if (props.contentFillsScreen) {
|
||||
dialogElement.classList.add('-fullscreen');
|
||||
} else {
|
||||
dialogElement.classList.remove('-fullscreen');
|
||||
}
|
||||
}, [props.contentFillsScreen, dialogElement]);
|
||||
|
||||
if (dialogElement && !contentRendered) {
|
||||
setContentRendered(true);
|
||||
}
|
||||
@@ -43,19 +58,21 @@ const Dialog: React.FC<Props> = props => {
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
return <>
|
||||
{dialogElement && createPortal(content, dialogElement)}
|
||||
</>;
|
||||
return <div ref={setContainerElement} className='dialog-anchor-node'>
|
||||
{dialogElement && createPortal(content, dialogElement) as ReactNode}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const useDialogElement = (onCancel: undefined|OnCancelListener) => {
|
||||
const useDialogElement = (containerDocument: Document, onCancel: undefined|OnCancelListener) => {
|
||||
const [dialogElement, setDialogElement] = useState<HTMLDialogElement|null>(null);
|
||||
|
||||
const onCancelRef = useRef(onCancel);
|
||||
onCancelRef.current = onCancel;
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = document.createElement('dialog');
|
||||
if (!containerDocument) return () => {};
|
||||
|
||||
const dialog = containerDocument.createElement('dialog');
|
||||
dialog.addEventListener('click', event => {
|
||||
const onCancel = onCancelRef.current;
|
||||
const isBackgroundClick = event.target === dialog;
|
||||
@@ -84,13 +101,13 @@ const useDialogElement = (onCancel: undefined|OnCancelListener) => {
|
||||
// Work around what seems to be an Electron bug -- if an input or contenteditable region is refocused after
|
||||
// dismissing a dialog, it won't be editable.
|
||||
// Note: While this addresses the issue in the note title input, it does not address the issue in the Rich Text Editor.
|
||||
if (document.activeElement?.tagName === 'INPUT') {
|
||||
const element = document.activeElement as HTMLElement;
|
||||
if (containerDocument.activeElement?.tagName === 'INPUT') {
|
||||
const element = containerDocument.activeElement as HTMLElement;
|
||||
blur('Dialog', element);
|
||||
focus('Dialog', element);
|
||||
}
|
||||
});
|
||||
document.body.appendChild(dialog);
|
||||
containerDocument.body.appendChild(dialog);
|
||||
|
||||
setDialogElement(dialog);
|
||||
|
||||
@@ -102,7 +119,7 @@ const useDialogElement = (onCancel: undefined|OnCancelListener) => {
|
||||
}
|
||||
dialog.remove();
|
||||
};
|
||||
}, []);
|
||||
}, [containerDocument]);
|
||||
|
||||
return dialogElement;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export const IconSelector = (props: Props) => {
|
||||
attrs: {
|
||||
type: 'module',
|
||||
},
|
||||
});
|
||||
}, document);
|
||||
|
||||
if (event.cancelled) return;
|
||||
|
||||
@@ -45,7 +45,7 @@ export const IconSelector = (props: Props) => {
|
||||
attrs: {
|
||||
type: 'module',
|
||||
},
|
||||
});
|
||||
}, document);
|
||||
|
||||
if (event.cancelled) return;
|
||||
|
||||
|
||||
@@ -1,63 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import ResizableLayout from '../ResizableLayout/ResizableLayout';
|
||||
import findItemByKey from '../ResizableLayout/utils/findItemByKey';
|
||||
import { MoveButtonClickEvent } from '../ResizableLayout/MoveButtons';
|
||||
import { move } from '../ResizableLayout/utils/movements';
|
||||
import { LayoutItem } from '../ResizableLayout/utils/types';
|
||||
import NoteEditor from '../NoteEditor/NoteEditor';
|
||||
import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog';
|
||||
import ShareNoteDialog from '../ShareNoteDialog';
|
||||
import ResizableLayout from './ResizableLayout/ResizableLayout';
|
||||
import findItemByKey from './ResizableLayout/utils/findItemByKey';
|
||||
import { MoveButtonClickEvent } from './ResizableLayout/MoveButtons';
|
||||
import { move } from './ResizableLayout/utils/movements';
|
||||
import { LayoutItem } from './ResizableLayout/utils/types';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { PluginHtmlContents, PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import Sidebar from '../Sidebar/Sidebar';
|
||||
import UserWebview from '../../services/plugins/UserWebview';
|
||||
import UserWebviewDialog from '../../services/plugins/UserWebviewDialog';
|
||||
import Sidebar from './Sidebar/Sidebar';
|
||||
import UserWebview from '../services/plugins/UserWebview';
|
||||
import UserWebviewDialog from '../services/plugins/UserWebviewDialog';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import { StateLastDeletion, stateUtils } from '@joplin/lib/reducer';
|
||||
import InteropServiceHelper from '../../InteropServiceHelper';
|
||||
import { defaultWindowId, StateLastDeletion, stateUtils } from '@joplin/lib/reducer';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import NoteListWrapper from '../NoteListWrapper/NoteListWrapper';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { saveLayout, loadLayout } from '../ResizableLayout/utils/persist';
|
||||
import NoteListWrapper from './NoteListWrapper/NoteListWrapper';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { saveLayout, loadLayout } from './ResizableLayout/utils/persist';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||
import produce from 'immer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import bridge from '../../services/bridge';
|
||||
import time from '@joplin/lib/time';
|
||||
import bridge from '../services/bridge';
|
||||
import styled from 'styled-components';
|
||||
import { themeStyle, ThemeStyle } from '@joplin/lib/theme';
|
||||
import validateLayout from '../ResizableLayout/utils/validateLayout';
|
||||
import iterateItems from '../ResizableLayout/utils/iterateItems';
|
||||
import removeItem from '../ResizableLayout/utils/removeItem';
|
||||
import validateLayout from './ResizableLayout/utils/validateLayout';
|
||||
import iterateItems from './ResizableLayout/utils/iterateItems';
|
||||
import removeItem from './ResizableLayout/utils/removeItem';
|
||||
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
import ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog';
|
||||
import { ShareInvitation } from '@joplin/lib/services/share/reducer';
|
||||
import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems';
|
||||
import removeKeylessItems from './ResizableLayout/utils/removeKeylessItems';
|
||||
import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import ElectronAppWrapper from '../../ElectronAppWrapper';
|
||||
import ElectronAppWrapper from '../ElectronAppWrapper';
|
||||
import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils';
|
||||
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import commands from './commands/index';
|
||||
import invitationRespond from '@joplin/lib/services/share/invitationRespond';
|
||||
import restart from '../../services/restart';
|
||||
const { connect } = require('react-redux');
|
||||
import PromptDialog from '../PromptDialog';
|
||||
import NotePropertiesDialog from '../NotePropertiesDialog';
|
||||
import restart from '../services/restart';
|
||||
import { connect } from 'react-redux';
|
||||
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
|
||||
import validateColumns from '../NoteListHeader/utils/validateColumns';
|
||||
import TrashNotification from '../TrashNotification/TrashNotification';
|
||||
import UpdateNotification from '../UpdateNotification/UpdateNotification';
|
||||
import validateColumns from './NoteListHeader/utils/validateColumns';
|
||||
import TrashNotification from './TrashNotification/TrashNotification';
|
||||
import UpdateNotification from './UpdateNotification/UpdateNotification';
|
||||
import NoteEditor from './NoteEditor/NoteEditor';
|
||||
|
||||
const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
interface LayerModalState {
|
||||
visible: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
plugins: PluginStates;
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
@@ -69,9 +55,6 @@ interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
layoutMoveMode: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
editorNoteStatuses: any;
|
||||
customCss: string;
|
||||
shouldUpgradeSyncTarget: boolean;
|
||||
hasDisabledSyncItems: boolean;
|
||||
hasDisabledEncryptionItems: boolean;
|
||||
@@ -80,9 +63,6 @@ interface Props {
|
||||
showNeedUpgradingMasterKeyMessage: boolean;
|
||||
showShouldReencryptMessage: boolean;
|
||||
themeId: number;
|
||||
settingEditorCodeView: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
pluginsLegacy: any;
|
||||
startupPluginsLoaded: boolean;
|
||||
shareInvitations: ShareInvitation[];
|
||||
isSafeMode: boolean;
|
||||
@@ -109,7 +89,6 @@ interface ShareFolderDialogOptions {
|
||||
interface State {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
promptOptions: any;
|
||||
modalLayer: LayerModalState;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
notePropertiesDialogOptions: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -143,22 +122,15 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private waitForNotesSavedIID_: any;
|
||||
private isPrinting_: boolean;
|
||||
private styleKey_: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private styles_: any;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
private promptOnClose_: Function;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
promptOptions: null,
|
||||
modalLayer: {
|
||||
visible: false,
|
||||
message: '',
|
||||
},
|
||||
notePropertiesDialogOptions: {},
|
||||
noteContentPropertiesDialogOptions: {},
|
||||
shareNoteDialogOptions: {},
|
||||
@@ -170,14 +142,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
this.updateMainLayout(this.buildLayout(props.plugins));
|
||||
|
||||
this.registerCommands();
|
||||
|
||||
this.setupAppCloseHandling();
|
||||
|
||||
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
|
||||
this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this);
|
||||
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
|
||||
this.shareFolderDialog_close = this.shareFolderDialog_close.bind(this);
|
||||
this.resizableLayout_resize = this.resizableLayout_resize.bind(this);
|
||||
this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this);
|
||||
this.resizableLayout_moveButtonClick = this.resizableLayout_moveButtonClick.bind(this);
|
||||
@@ -318,22 +284,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
private notePropertiesDialog_close() {
|
||||
this.setState({ notePropertiesDialogOptions: {} });
|
||||
}
|
||||
|
||||
private noteContentPropertiesDialog_close() {
|
||||
this.setState({ noteContentPropertiesDialogOptions: {} });
|
||||
}
|
||||
|
||||
private shareNoteDialog_close() {
|
||||
this.setState({ shareNoteDialogOptions: {} });
|
||||
}
|
||||
|
||||
private shareFolderDialog_close() {
|
||||
this.setState({ shareFolderDialogOptions: { visible: false, folderId: '' } });
|
||||
}
|
||||
|
||||
public updateMainLayout(layout: LayoutItem) {
|
||||
this.props.dispatch({
|
||||
type: 'MAIN_LAYOUT_SET',
|
||||
@@ -363,34 +313,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
// this.setState({ layout: this.buildLayout(this.props.plugins) });
|
||||
}
|
||||
|
||||
if (this.state.notePropertiesDialogOptions !== prevState.notePropertiesDialogOptions) {
|
||||
this.props.dispatch({
|
||||
type: this.state.notePropertiesDialogOptions && this.state.notePropertiesDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE',
|
||||
name: 'noteProperties',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.noteContentPropertiesDialogOptions !== prevState.noteContentPropertiesDialogOptions) {
|
||||
this.props.dispatch({
|
||||
type: this.state.noteContentPropertiesDialogOptions && this.state.noteContentPropertiesDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE',
|
||||
name: 'noteContentProperties',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.shareNoteDialogOptions !== prevState.shareNoteDialogOptions) {
|
||||
this.props.dispatch({
|
||||
type: this.state.shareNoteDialogOptions && this.state.shareNoteDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE',
|
||||
name: 'shareNote',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.shareFolderDialogOptions !== prevState.shareFolderDialogOptions) {
|
||||
this.props.dispatch({
|
||||
type: this.state.shareFolderDialogOptions && this.state.shareFolderDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE',
|
||||
name: 'shareFolder',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.mainLayout !== prevProps.mainLayout) {
|
||||
const toSave = saveLayout(this.props.mainLayout);
|
||||
Setting.setValue('ui.layout', toSave);
|
||||
@@ -425,62 +347,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.unregisterCommands();
|
||||
|
||||
window.removeEventListener('resize', this.window_resize);
|
||||
window.removeEventListener('keydown', this.layoutModeListenerKeyDown);
|
||||
}
|
||||
|
||||
public async waitForNoteToSaved(noteId: string) {
|
||||
while (noteId && this.props.editorNoteStatuses[noteId] === 'saving') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Waiting for note to be saved...', this.props.editorNoteStatuses);
|
||||
await time.msleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async printTo_(target: string, options: any) {
|
||||
// Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion
|
||||
if (this.isPrinting_) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(`Printing ${options.path} to ${target} disallowed, already printing.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPrinting_ = true;
|
||||
|
||||
// Need to wait for save because the interop service reloads the note from the database
|
||||
await this.waitForNoteToSaved(options.noteId);
|
||||
|
||||
if (target === 'pdf') {
|
||||
try {
|
||||
const pdfData = await InteropServiceHelper.exportNoteToPdf(options.noteId, {
|
||||
printBackground: true,
|
||||
pageSize: Setting.value('export.pdfPageSize'),
|
||||
landscape: Setting.value('export.pdfPageOrientation') === 'landscape',
|
||||
customCss: this.props.customCss,
|
||||
plugins: this.props.plugins,
|
||||
});
|
||||
await shim.fsDriver().writeFile(options.path, pdfData, 'buffer');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
} else if (target === 'printer') {
|
||||
try {
|
||||
await InteropServiceHelper.printNote(options.noteId, {
|
||||
printBackground: true,
|
||||
customCss: this.props.customCss,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
}
|
||||
this.isPrinting_ = false;
|
||||
}
|
||||
|
||||
public rootLayoutSize() {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
@@ -533,15 +403,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
height: height,
|
||||
};
|
||||
|
||||
this.styles_.modalLayer = { ...theme.textStyle, zIndex: 10000,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
width: width - 20,
|
||||
height: height - 20,
|
||||
padding: 10 };
|
||||
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
@@ -724,18 +585,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
props.showInvalidJoplinCloudCredential;
|
||||
}
|
||||
|
||||
public registerCommands() {
|
||||
for (const command of commands) {
|
||||
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(this));
|
||||
}
|
||||
}
|
||||
|
||||
public unregisterCommands() {
|
||||
for (const command of commands) {
|
||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private resizableLayout_resize(event: any) {
|
||||
this.updateMainLayout(event.layout);
|
||||
@@ -784,14 +633,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
},
|
||||
|
||||
editor: () => {
|
||||
let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror6' : 'TinyMCE';
|
||||
|
||||
if (this.props.isSafeMode) {
|
||||
bodyEditor = 'PlainText';
|
||||
} else if (this.props.settingEditorCodeView && this.props.enableLegacyMarkdownEditor) {
|
||||
bodyEditor = 'CodeMirror5';
|
||||
}
|
||||
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
|
||||
return <NoteEditor
|
||||
windowId={defaultWindowId}
|
||||
key={key}
|
||||
/>;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -884,28 +729,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
...this.props.style,
|
||||
};
|
||||
const promptOptions = this.state.promptOptions;
|
||||
const styles = this.styles(this.props.themeId, style.width, style.height, this.messageBoxVisible());
|
||||
|
||||
if (!this.promptOnClose_) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
this.promptOnClose_ = (answer: any, buttonType: any) => {
|
||||
return this.state.promptOptions.onClose(answer, buttonType);
|
||||
};
|
||||
}
|
||||
|
||||
const messageComp = this.renderNotification(theme, styles);
|
||||
|
||||
const dialogInfo = PluginManager.instance().pluginDialogToShow(this.props.pluginsLegacy);
|
||||
const pluginDialog = !dialogInfo ? null : <dialogInfo.Dialog {...dialogInfo.props} />;
|
||||
|
||||
const modalLayerStyle = { ...styles.modalLayer, display: this.state.modalLayer.visible ? 'block' : 'none' };
|
||||
|
||||
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
|
||||
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
|
||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||
const shareFolderDialogOptions = this.state.shareFolderDialogOptions;
|
||||
|
||||
const layoutComp = this.props.mainLayout ? (
|
||||
<ResizableLayout
|
||||
height={styles.rowHeight}
|
||||
@@ -920,15 +747,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
|
||||
{this.renderPluginDialogs()}
|
||||
{noteContentPropertiesDialogOptions.visible && <NoteContentPropertiesDialog markupLanguage={noteContentPropertiesDialogOptions.markupLanguage} themeId={this.props.themeId} onClose={this.noteContentPropertiesDialog_close} text={noteContentPropertiesDialogOptions.text}/>}
|
||||
{notePropertiesDialogOptions.visible && <NotePropertiesDialog themeId={this.props.themeId} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
|
||||
{shareNoteDialogOptions.visible && <ShareNoteDialog themeId={this.props.themeId} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
|
||||
{shareFolderDialogOptions.visible && <ShareFolderDialog themeId={this.props.themeId} folderId={shareFolderDialogOptions.folderId} onClose={this.shareFolderDialog_close} />}
|
||||
|
||||
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} themeId={this.props.themeId} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
|
||||
|
||||
<TrashNotification
|
||||
lastDeletion={this.props.lastDeletion}
|
||||
lastDeletionNotificationTime={this.props.lastDeletionNotificationTime}
|
||||
@@ -939,7 +757,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
<UpdateNotification themeId={this.props.themeId} />
|
||||
{messageComp}
|
||||
{layoutComp}
|
||||
{pluginDialog}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -948,10 +765,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const syncInfo = localSyncInfoFromState(state);
|
||||
const showNeedUpgradingEnabledMasterKeyMessage = !!EncryptionService.instance().masterKeysThatNeedUpgrading(syncInfo.masterKeys.filter((k) => !!k.enabled)).length;
|
||||
const windowState = stateUtils.windowStateById(state, defaultWindowId);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
settingEditorCodeView: state.settings['editor.codeView'],
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
hasDisabledEncryptionItems: state.hasDisabledEncryptionItems,
|
||||
showMissingMasterKeyMessage: showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys),
|
||||
@@ -959,11 +776,8 @@ const mapStateToProps = (state: AppState) => {
|
||||
showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
|
||||
hasMissingSyncCredentials: shouldShowMissingPasswordWarning(state.settings['sync.target'], state.settings),
|
||||
pluginsLegacy: state.pluginsLegacy,
|
||||
plugins: state.pluginService.plugins,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
customCss: state.customCss,
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
|
||||
layoutMoveMode: state.layoutMoveMode,
|
||||
mainLayout: state.mainLayout,
|
||||
@@ -977,7 +791,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
listRendererId: state.settings['notes.listRendererId'],
|
||||
lastDeletion: state.lastDeletion,
|
||||
lastDeletionNotificationTime: state.lastDeletionNotificationTime,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedFolderId: windowState.selectedFolderId,
|
||||
mustUpgradeAppMessage: state.mustUpgradeAppMessage,
|
||||
notesSortOrderField: state.settings['notes.sortOrder.field'],
|
||||
notesSortOrderReverse: state.settings['notes.sortOrder.reverse'],
|
||||
@@ -1,14 +0,0 @@
|
||||
import { CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'hideModalMessage',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
return {
|
||||
execute: async () => {
|
||||
comp.setState({ modalLayer: { visible: false, message: '' } });
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'showModalMessage',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext, message: string) => {
|
||||
let brIndex = 1;
|
||||
const lines = message.split('\n').map((line: string) => {
|
||||
if (!line.trim()) return <br key={`${brIndex++}`}/>;
|
||||
return <div key={line} className="text">{line}</div>;
|
||||
});
|
||||
|
||||
comp.setState({
|
||||
modalLayer: {
|
||||
visible: true,
|
||||
message:
|
||||
<div className="modal-message">
|
||||
<div id="loading-animation" />
|
||||
<div className="text">
|
||||
{lines}
|
||||
</div>
|
||||
</div>,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import { AppState } from '../app.reducer';
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { defaultWindowId, stateUtils } from '@joplin/lib/reducer';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import KeymapService from '@joplin/lib/services/KeymapService';
|
||||
@@ -19,7 +19,7 @@ import menuCommandNames from './menuCommandNames';
|
||||
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
|
||||
import bridge from '../services/bridge';
|
||||
import checkForUpdates from '../checkForUpdates';
|
||||
const { connect } = require('react-redux');
|
||||
import { connect } from 'react-redux';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
@@ -27,6 +27,11 @@ import { getListRendererById, getListRendererIds } from '@joplin/lib/services/no
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { EventName } from '@joplin/lib/eventManager';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('MenuBar');
|
||||
|
||||
const packageInfo: PackageInfo = require('../packageInfo.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Menu = bridge().Menu;
|
||||
@@ -150,7 +155,7 @@ interface Props {
|
||||
dispatch: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
menuItemProps: any;
|
||||
routeName: string;
|
||||
mainScreenVisible: boolean;
|
||||
selectedFolderId: string;
|
||||
layoutButtonSequence: number;
|
||||
['notes.sortOrder.field']: string;
|
||||
@@ -173,6 +178,8 @@ interface Props {
|
||||
pluginSettings: PluginSettings;
|
||||
noteListRendererIds: string[];
|
||||
noteListRendererId: string;
|
||||
windowId: string;
|
||||
secondaryWindowFocused: boolean;
|
||||
showMenuBar: boolean;
|
||||
}
|
||||
|
||||
@@ -192,11 +199,11 @@ function menuItemSetEnabled(id: string, enabled: boolean) {
|
||||
menuItem.enabled = enabled;
|
||||
}
|
||||
|
||||
const applyMenuBarVisibility = (showMenuBar: boolean) => {
|
||||
const applyMenuBarVisibility = (windowId: string, showMenuBar: boolean) => {
|
||||
// The menu bar cannot be hidden on macOS
|
||||
if (shim.isMac()) return;
|
||||
|
||||
const window = bridge().window();
|
||||
const window = bridge().windowById(windowId) ?? bridge().mainWindow();
|
||||
window.setAutoHideMenuBar(!showMenuBar);
|
||||
window.setMenuBarVisibility(showMenuBar);
|
||||
};
|
||||
@@ -402,6 +409,17 @@ function useMenu(props: Props) {
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
const navigateTo = (routeName: string) => {
|
||||
void NavService.go(routeName);
|
||||
|
||||
// NavService.go opens in the main window -- switch to it to show the screen:
|
||||
const isBackgroundWindow = props.windowId !== defaultWindowId;
|
||||
if (isBackgroundWindow) {
|
||||
logger.info('Focusing the main window');
|
||||
bridge().mainWindow().show();
|
||||
}
|
||||
};
|
||||
|
||||
const quitMenuItem = {
|
||||
label: _('Quit'),
|
||||
accelerator: keymapService.getAccelerator('quit'),
|
||||
@@ -515,10 +533,7 @@ function useMenu(props: Props) {
|
||||
const syncStatusItem = {
|
||||
label: _('Synchronisation Status'),
|
||||
click: () => {
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Status',
|
||||
});
|
||||
navigateTo('Status');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -548,10 +563,7 @@ function useMenu(props: Props) {
|
||||
label: _('Options'),
|
||||
accelerator: keymapService.getAccelerator('config'),
|
||||
click: () => {
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Config',
|
||||
});
|
||||
navigateTo('Config');
|
||||
},
|
||||
},
|
||||
separator(),
|
||||
@@ -561,10 +573,7 @@ function useMenu(props: Props) {
|
||||
const toolsItemsAll = [{
|
||||
label: _('Note attachments...'),
|
||||
click: () => {
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Resources',
|
||||
});
|
||||
navigateTo('Resources');
|
||||
},
|
||||
}];
|
||||
|
||||
@@ -579,7 +588,7 @@ function useMenu(props: Props) {
|
||||
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
||||
ipcRenderer.send('check-for-updates');
|
||||
} else {
|
||||
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
void checkForUpdates(false, bridge().mainWindow(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -619,10 +628,7 @@ function useMenu(props: Props) {
|
||||
visible: !!shim.isMac(),
|
||||
accelerator: shim.isMac() && keymapService.getAccelerator('config'),
|
||||
click: () => {
|
||||
props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Config',
|
||||
});
|
||||
navigateTo('Config');
|
||||
},
|
||||
}, {
|
||||
label: _('Check for updates...'),
|
||||
@@ -1020,7 +1026,7 @@ function useMenu(props: Props) {
|
||||
rootMenus.help,
|
||||
].filter(item => item !== null);
|
||||
|
||||
if (props.routeName !== 'Main') {
|
||||
if (!props.mainScreenVisible) {
|
||||
setMenu(Menu.buildFromTemplate([
|
||||
{
|
||||
label: _('&File'),
|
||||
@@ -1050,7 +1056,8 @@ function useMenu(props: Props) {
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [
|
||||
props.routeName,
|
||||
props.windowId,
|
||||
props.mainScreenVisible,
|
||||
props.pluginMenuItems,
|
||||
props.pluginMenus,
|
||||
keymapLastChangeTime,
|
||||
@@ -1100,18 +1107,36 @@ function useMenu(props: Props) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function MenuBar(props: Props): any {
|
||||
const menu = useMenu(props);
|
||||
if (menu) Menu.setApplicationMenu(menu);
|
||||
applyMenuBarVisibility(props.showMenuBar);
|
||||
|
||||
useEffect(() => {
|
||||
// Currently, this sets the menu for all windows. Although it's possible to set the menu
|
||||
// for individual windows with BrowserWindow.setMenu, it causes issues with updating the
|
||||
// state of existing menu items (and doesn't work with MacOS/Playwright).
|
||||
if (menu) {
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
}, [menu]);
|
||||
|
||||
useEffect(() => {
|
||||
applyMenuBarVisibility(props.windowId, props.showMenuBar);
|
||||
}, [props.showMenuBar, props.windowId]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
|
||||
const mapStateToProps = (state: AppState): Partial<Props> => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
|
||||
const secondaryWindowFocused = state.windowId !== defaultWindowId;
|
||||
|
||||
return {
|
||||
windowId: state.windowId,
|
||||
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(getPluginCommandNames(state.pluginService.plugins)), whenClauseContext),
|
||||
locale: state.settings.locale,
|
||||
routeName: state.route.routeName,
|
||||
// Secondary windows can only show the main screen
|
||||
mainScreenVisible: state.route.routeName === 'Main' || secondaryWindowFocused,
|
||||
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
layoutButtonSequence: state.settings.layoutButtonSequence,
|
||||
['notes.sortOrder.field']: state.settings['notes.sortOrder.field'],
|
||||
@@ -1127,7 +1152,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
['spellChecker.languages']: state.settings['spellChecker.languages'],
|
||||
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
customCss: state.customViewerCss,
|
||||
profileConfig: state.profileConfig,
|
||||
noteListRendererIds: state.noteListRendererIds,
|
||||
noteListRendererId: state.settings['notes.listRendererId'],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dispatch } from 'redux';
|
||||
import { ThemeStyle } from '@joplin/lib/theme';
|
||||
|
||||
import { buildStyle } from '@joplin/lib/theme';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import bridge from '../services/bridge';
|
||||
|
||||
interface MultiNoteActionsProps {
|
||||
themeId: number;
|
||||
@@ -46,7 +46,7 @@ export default function MultiNoteActions(props: MultiNoteActionsProps) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const multiNotesButton_click = (item: any) => {
|
||||
if (item.submenu) {
|
||||
item.submenu.popup({ window: bridge().window() });
|
||||
item.submenu.popup({ window: bridge().activeWindow() });
|
||||
} else {
|
||||
item.click();
|
||||
}
|
||||
|
||||
@@ -1,55 +1,67 @@
|
||||
const React = require('react');
|
||||
import * as React from 'react';
|
||||
const { connect } = require('react-redux');
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { AppState } from '../app.reducer';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import { AppState, AppStateRoute } from '../app.reducer';
|
||||
import bridge from '../services/bridge';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import { WindowIdContext } from './NewWindowOrIFrame';
|
||||
|
||||
interface Props {
|
||||
route: AppStateRoute;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
route: any;
|
||||
screens: Record<string, any>;
|
||||
|
||||
style: React.CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class NavigatorComponent extends React.Component<Props> {
|
||||
public UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
if (newProps.route) {
|
||||
const screenInfo = this.props.screens[newProps.route.routeName];
|
||||
const NavigatorComponent: React.FC<Props> = props => {
|
||||
const windowId = useContext(WindowIdContext);
|
||||
|
||||
const route = props.route;
|
||||
const screenInfo = props.screens[route?.routeName];
|
||||
|
||||
const screensRef = useRef(props.screens);
|
||||
screensRef.current = props.screens;
|
||||
|
||||
const prevRoute = useRef<AppStateRoute|null>(null);
|
||||
useEffect(() => {
|
||||
const routeName = route?.routeName;
|
||||
if (route) {
|
||||
const devMarker = Setting.value('env') === 'dev' ? ` (DEV - ${Setting.value('profileDir')})` : '';
|
||||
const windowTitle = [`Joplin${devMarker}`];
|
||||
if (screenInfo.title) {
|
||||
windowTitle.push(screenInfo.title());
|
||||
}
|
||||
this.updateWindowTitle(windowTitle.join(' - '));
|
||||
bridge().windowById(windowId)?.setTitle(windowTitle.join(' - '));
|
||||
}
|
||||
}
|
||||
|
||||
public updateWindowTitle(title: string) {
|
||||
try {
|
||||
if (bridge().window()) bridge().window().setTitle(title);
|
||||
} catch (error) {
|
||||
console.warn('updateWindowTitle', error);
|
||||
// When a navigation happens in an unfocused window, show the window to the user.
|
||||
// This might happen if, for example, a secondary window triggers a navigation in
|
||||
// the main window.
|
||||
if (routeName && routeName !== prevRoute.current?.routeName) {
|
||||
bridge().switchToWindow(windowId);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.props.route) throw new Error('Route must not be null');
|
||||
prevRoute.current = route;
|
||||
}, [route, screenInfo, windowId]);
|
||||
|
||||
const route = this.props.route;
|
||||
const screenProps = route.props ? route.props : {};
|
||||
const screenInfo = this.props.screens[route.routeName];
|
||||
const Screen = screenInfo.screen;
|
||||
if (!route) throw new Error('Route must not be null');
|
||||
|
||||
const screenStyle = {
|
||||
width: this.props.style.width,
|
||||
height: this.props.style.height,
|
||||
};
|
||||
const screenProps = route.props ? route.props : {};
|
||||
const Screen = screenInfo.screen;
|
||||
|
||||
return (
|
||||
<div style={this.props.style} className={this.props.className}>
|
||||
<Screen style={screenStyle} {...screenProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
const screenStyle = {
|
||||
width: props.style.width,
|
||||
height: props.style.height,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={props.style} className={props.className}>
|
||||
<Screen style={screenStyle} {...screenProps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Navigator = connect((state: AppState) => {
|
||||
return {
|
||||
|
||||
172
packages/app-desktop/gui/NewWindowOrIFrame.tsx
Normal file
172
packages/app-desktop/gui/NewWindowOrIFrame.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, createContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { SecondaryWindowApi } from '../utils/window/types';
|
||||
|
||||
// This component uses react-dom's Portals to render its children in a different HTML
|
||||
// document. As children are rendered in a different Window/Document, they should avoid
|
||||
// referencing the `window` and `document` globals. Instead, HTMLElement.ownerDocument
|
||||
// and refs can be used to access the child component's DOM.
|
||||
|
||||
export const WindowIdContext = createContext(defaultWindowId);
|
||||
|
||||
type OnCloseCallback = ()=> void;
|
||||
type OnFocusCallback = ()=> void;
|
||||
|
||||
export enum WindowMode {
|
||||
Iframe, NewWindow,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
// Note: children will be rendered in a different DOM from this node. Avoid using document.* methods
|
||||
// in child components.
|
||||
children: React.ReactNode[]|React.ReactNode;
|
||||
title: string;
|
||||
mode: WindowMode;
|
||||
windowId: string;
|
||||
onClose: OnCloseCallback;
|
||||
onFocus?: OnFocusCallback;
|
||||
}
|
||||
|
||||
const useDocument = (
|
||||
mode: WindowMode,
|
||||
iframeElement: HTMLIFrameElement|null,
|
||||
onClose: OnCloseCallback,
|
||||
) => {
|
||||
const [doc, setDoc] = useState<Document>(null);
|
||||
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
useEffect(() => {
|
||||
let openedWindow: Window|null = null;
|
||||
const unmounted = false;
|
||||
if (iframeElement) {
|
||||
setDoc(iframeElement?.contentWindow?.document);
|
||||
} else if (mode === WindowMode.NewWindow) {
|
||||
openedWindow = window.open('about:blank');
|
||||
setDoc(openedWindow.document);
|
||||
|
||||
// .onbeforeunload and .onclose events don't seem to fire when closed by a user -- rely on polling
|
||||
// instead:
|
||||
void (async () => {
|
||||
while (!unmounted) {
|
||||
await new Promise<void>(resolve => {
|
||||
shim.setTimeout(() => resolve(), 2000);
|
||||
});
|
||||
|
||||
if (openedWindow?.closed) {
|
||||
onCloseRef.current?.();
|
||||
openedWindow = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Delay: Closing immediately causes Electron to crash
|
||||
setTimeout(() => {
|
||||
if (!openedWindow?.closed) {
|
||||
openedWindow?.close();
|
||||
onCloseRef.current?.();
|
||||
openedWindow = null;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
if (iframeElement && !openedWindow) {
|
||||
onCloseRef.current?.();
|
||||
}
|
||||
};
|
||||
}, [iframeElement, mode]);
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
type OnSetLoaded = (loaded: boolean)=> void;
|
||||
const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded, onFocus?: OnFocusCallback) => {
|
||||
const onFocusRef = useRef(onFocus);
|
||||
onFocusRef.current = onFocus;
|
||||
|
||||
useEffect(() => {
|
||||
if (!doc) return;
|
||||
|
||||
doc.open();
|
||||
doc.write('<!DOCTYPE html><html><head></head><body></body></html>');
|
||||
doc.close();
|
||||
|
||||
const cssUrls = [
|
||||
'style.min.css',
|
||||
];
|
||||
|
||||
for (const url of cssUrls) {
|
||||
const style = doc.createElement('link');
|
||||
style.rel = 'stylesheet';
|
||||
style.href = url;
|
||||
doc.head.appendChild(style);
|
||||
}
|
||||
|
||||
const jsUrls = [
|
||||
'vendor/lib/smalltalk/dist/smalltalk.min.js',
|
||||
'./utils/window/eventHandlerOverrides.js',
|
||||
];
|
||||
for (const url of jsUrls) {
|
||||
const script = doc.createElement('script');
|
||||
script.src = url;
|
||||
doc.head.appendChild(script);
|
||||
}
|
||||
|
||||
doc.body.style.height = '100vh';
|
||||
|
||||
const containerWindow = doc.defaultView;
|
||||
containerWindow.addEventListener('focus', () => {
|
||||
onFocusRef.current?.();
|
||||
});
|
||||
if (doc.hasFocus()) {
|
||||
onFocusRef.current?.();
|
||||
}
|
||||
|
||||
setLoaded(true);
|
||||
}, [doc, setLoaded]);
|
||||
};
|
||||
|
||||
const NewWindowOrIFrame: React.FC<Props> = props => {
|
||||
const [iframeRef, setIframeRef] = useState<HTMLIFrameElement|null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const doc = useDocument(props.mode, iframeRef, props.onClose);
|
||||
useDocumentSetup(doc, setLoaded, props.onFocus);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doc) return;
|
||||
doc.title = props.title;
|
||||
}, [doc, props.title]);
|
||||
|
||||
useEffect(() => {
|
||||
const win = doc?.defaultView;
|
||||
if (win && 'electronWindow' in win && typeof win.electronWindow === 'object') {
|
||||
const electronWindow = win.electronWindow as SecondaryWindowApi;
|
||||
electronWindow.onSetWindowId(props.windowId);
|
||||
}
|
||||
}, [doc, props.windowId]);
|
||||
|
||||
const parentNode = loaded ? doc?.body : null;
|
||||
const wrappedChildren = <WindowIdContext.Provider value={props.windowId}>{props.children}</WindowIdContext.Provider>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM
|
||||
const contentPortal = parentNode && createPortal(wrappedChildren, parentNode) as any;
|
||||
if (props.mode === WindowMode.NewWindow) {
|
||||
return <div style={{ display: 'none' }}>{contentPortal}</div>;
|
||||
} else {
|
||||
return <iframe
|
||||
ref={setIframeRef}
|
||||
style={{ flexGrow: 1, width: '100%', height: '100%', border: 'none' }}
|
||||
>
|
||||
{contentPortal}
|
||||
</iframe>;
|
||||
}
|
||||
};
|
||||
|
||||
export default NewWindowOrIFrame;
|
||||
125
packages/app-desktop/gui/NoteEditor/EditorWindow.tsx
Normal file
125
packages/app-desktop/gui/NoteEditor/EditorWindow.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import NoteEditor from './NoteEditor';
|
||||
import StyleSheetContainer from '../StyleSheets/StyleSheetContainer';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { Dispatch } from 'redux';
|
||||
import NewWindowOrIFrame, { WindowMode } from '../NewWindowOrIFrame';
|
||||
import WindowCommandsAndDialogs from '../WindowCommandsAndDialogs/WindowCommandsAndDialogs';
|
||||
|
||||
const { StyleSheetManager } = require('styled-components');
|
||||
// Note: Transitive dependencies used only by react-select. Remove if react-select is removed.
|
||||
import createCache from '@emotion/cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
themeId: number;
|
||||
|
||||
newWindow: boolean;
|
||||
windowId: string;
|
||||
activeWindowId: string;
|
||||
}
|
||||
|
||||
const emptyCallback = () => {};
|
||||
const useWindowTitle = (isNewWindow: boolean) => {
|
||||
const [title, setTitle] = useState('Untitled');
|
||||
|
||||
if (!isNewWindow) {
|
||||
return {
|
||||
windowTitle: 'Editor',
|
||||
onNoteTitleChange: emptyCallback,
|
||||
};
|
||||
}
|
||||
|
||||
return { windowTitle: `Joplin - ${title}`, onNoteTitleChange: setTitle };
|
||||
};
|
||||
|
||||
const SecondaryWindow: React.FC<Props> = props => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { windowTitle, onNoteTitleChange } = useWindowTitle(props.newWindow);
|
||||
const editor = <div className='note-editor-wrapper' ref={containerRef}>
|
||||
<NoteEditor
|
||||
windowId={props.windowId}
|
||||
onTitleChange={onNoteTitleChange}
|
||||
/>
|
||||
</div>;
|
||||
|
||||
const newWindow = props.newWindow;
|
||||
const onWindowClose = useCallback(() => {
|
||||
if (newWindow) {
|
||||
props.dispatch({ type: 'WINDOW_CLOSE', windowId: props.windowId });
|
||||
}
|
||||
}, [props.dispatch, props.windowId, newWindow]);
|
||||
|
||||
const onWindowFocus = useCallback(() => {
|
||||
// Verify that the window still has focus (e.g. to handle the case where the event was delayed).
|
||||
if (containerRef.current?.ownerDocument.hasFocus()) {
|
||||
props.dispatch({
|
||||
type: 'WINDOW_FOCUS',
|
||||
windowId: props.windowId,
|
||||
lastWindowId: props.activeWindowId,
|
||||
});
|
||||
}
|
||||
}, [props.dispatch, props.windowId, props.activeWindowId]);
|
||||
|
||||
return <NewWindowOrIFrame
|
||||
mode={newWindow ? WindowMode.NewWindow : WindowMode.Iframe}
|
||||
windowId={props.windowId}
|
||||
onClose={onWindowClose}
|
||||
onFocus={onWindowFocus}
|
||||
title={windowTitle}
|
||||
>
|
||||
<LibraryStyleRoot>
|
||||
<WindowCommandsAndDialogs windowId={props.windowId} />
|
||||
{editor}
|
||||
</LibraryStyleRoot>
|
||||
<StyleSheetContainer />
|
||||
</NewWindowOrIFrame>;
|
||||
};
|
||||
|
||||
interface StyleProviderProps {
|
||||
children: React.ReactNode[]|React.ReactNode;
|
||||
}
|
||||
|
||||
// Sets the root style container for libraries. At present, this is needed by react-select (which uses @emotion/...)
|
||||
// and styled-components.
|
||||
// See: https://github.com/JedWatson/react-select/issues/3680 and https://github.com/styled-components/styled-components/issues/659
|
||||
const LibraryStyleRoot: React.FC<StyleProviderProps> = props => {
|
||||
const [dependencyStyleContainer, setDependencyStyleContainer] = useState<HTMLDivElement|null>(null);
|
||||
const cache = useMemo(() => {
|
||||
return createCache({
|
||||
key: 'new-window-cache',
|
||||
container: dependencyStyleContainer,
|
||||
});
|
||||
}, [dependencyStyleContainer]);
|
||||
|
||||
return <>
|
||||
<div ref={setDependencyStyleContainer}></div>
|
||||
<StyleSheetManager target={dependencyStyleContainer}>
|
||||
<CacheProvider value={cache}>
|
||||
{props.children}
|
||||
</CacheProvider>
|
||||
</StyleSheetManager>
|
||||
</>;
|
||||
};
|
||||
|
||||
interface ConnectProps {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
export default connect((state: AppState, ownProps: ConnectProps) => {
|
||||
// May be undefined if the window hasn't opened
|
||||
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
isSafeMode: state.settings.isSafeMode,
|
||||
codeView: windowState?.editorCodeView ?? state.settings['editor.codeView'],
|
||||
legacyMarkdown: state.settings['editor.legacyMarkdown'],
|
||||
activeWindowId: stateUtils.activeWindowId(state),
|
||||
};
|
||||
})(SecondaryWindow);
|
||||
@@ -40,8 +40,12 @@ function Toolbar(props: ToolbarProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
interface ConnectProps {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState, connectProps: ConnectProps) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state, { windowId: connectProps.windowId });
|
||||
|
||||
const commandNames = [
|
||||
'historyBackward',
|
||||
|
||||
@@ -25,6 +25,7 @@ interface ContextMenuProps {
|
||||
editorPaste: ()=> void;
|
||||
editorRef: RefObject<CodeMirrorControl>;
|
||||
editorClassName: string;
|
||||
containerRef: RefObject<HTMLDivElement|null>;
|
||||
}
|
||||
|
||||
const useContextMenu = (props: ContextMenuProps) => {
|
||||
@@ -51,12 +52,13 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
|
||||
function pointerInsideEditor(params: ContextMenuParams) {
|
||||
const x = params.x, y = params.y, isEditable = params.isEditable;
|
||||
const elements = document.getElementsByClassName(props.editorClassName);
|
||||
const containerDoc = props.containerRef.current?.ownerDocument;
|
||||
const elements = containerDoc?.getElementsByClassName(props.editorClassName);
|
||||
|
||||
// Note: We can't check inputFieldType here. When spellcheck is enabled,
|
||||
// params.inputFieldType is "none". When spellcheck is disabled,
|
||||
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
|
||||
if (!elements.length || !isEditable) return false;
|
||||
if (!elements?.length || !isEditable) return false;
|
||||
|
||||
// Checks whether the element the pointer clicked on is inside the editor.
|
||||
// This logic will need to be changed if the editor is eventually wrapped
|
||||
@@ -65,7 +67,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
const zoom = Setting.value('windowContentZoomFactor');
|
||||
const xScreen = convertFromScreenCoordinates(zoom, x);
|
||||
const yScreen = convertFromScreenCoordinates(zoom, y);
|
||||
const intersectingElement = document.elementFromPoint(xScreen, yScreen);
|
||||
const intersectingElement = containerDoc.elementFromPoint(xScreen, yScreen);
|
||||
return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement);
|
||||
}
|
||||
|
||||
@@ -150,18 +152,21 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
menu.append(new MenuItem(item));
|
||||
});
|
||||
|
||||
menu.popup();
|
||||
menu.popup({ window: bridge().activeWindow() });
|
||||
}
|
||||
|
||||
// Prepend the event listener so that it gets called before
|
||||
// the listener that shows the default menu.
|
||||
bridge().window().webContents.prependListener('context-menu', onContextMenu);
|
||||
const targetWindow = bridge().activeWindow();
|
||||
targetWindow.webContents.prependListener('context-menu', onContextMenu);
|
||||
|
||||
return () => {
|
||||
bridge().window().webContents.off('context-menu', onContextMenu);
|
||||
if (!targetWindow.isDestroyed()) {
|
||||
targetWindow.webContents.off('context-menu', onContextMenu);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
props.plugins, props.editorClassName, editorRef,
|
||||
props.plugins, props.editorClassName, editorRef, props.containerRef,
|
||||
props.editorCutText, props.editorCopyText, props.editorPaste,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef, useContext } from 'react';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditorRef } from '../../../utils/types';
|
||||
@@ -33,6 +33,7 @@ import useContextMenu from '../utils/useContextMenu';
|
||||
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
|
||||
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
|
||||
return { ...override };
|
||||
@@ -728,6 +729,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
editorCutText, editorCopyText, editorPaste,
|
||||
editorRef,
|
||||
editorClassName: 'codeMirrorEditor',
|
||||
containerRef: rootRef,
|
||||
});
|
||||
|
||||
function renderEditor() {
|
||||
@@ -773,11 +775,13 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
);
|
||||
}
|
||||
|
||||
const windowId = useContext(WindowIdContext);
|
||||
|
||||
return (
|
||||
<ErrorBoundary message="The text editor encountered a fatal error and could not continue. The error might be due to a plugin, so please try to disable some of them and try again.">
|
||||
<div style={styles.root} ref={rootRef}>
|
||||
<div style={styles.rowToolbar}>
|
||||
<Toolbar themeId={props.themeId}/>
|
||||
<Toolbar themeId={props.themeId} windowId={windowId}/>
|
||||
{props.noteToolbar}
|
||||
</div>
|
||||
<div style={styles.rowEditorViewer}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef } from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo, ForwardedRef, useContext } from 'react';
|
||||
|
||||
import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditorRef, OnChangeEvent } from '../../../utils/types';
|
||||
import { getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
|
||||
@@ -29,6 +29,7 @@ import Toolbar from '../Toolbar';
|
||||
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange';
|
||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
|
||||
const logger = Logger.create('CodeMirror6');
|
||||
const logDebug = (message: string) => logger.debug(message);
|
||||
@@ -336,6 +337,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
editorCutText, editorCopyText, editorPaste,
|
||||
editorRef,
|
||||
editorClassName: 'cm-editor',
|
||||
containerRef: rootRef,
|
||||
});
|
||||
|
||||
const lastSearchState = useRef<SearchState|null>(null);
|
||||
@@ -437,11 +439,13 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
);
|
||||
};
|
||||
|
||||
const windowId = useContext(WindowIdContext);
|
||||
|
||||
return (
|
||||
<ErrorBoundary message="The text editor encountered a fatal error and could not continue. The error might be due to a plugin, so please try to disable some of them and try again.">
|
||||
<div style={styles.root} ref={rootRef}>
|
||||
<div style={styles.rowToolbar}>
|
||||
<Toolbar themeId={props.themeId}/>
|
||||
<Toolbar themeId={props.themeId} windowId={windowId}/>
|
||||
{props.noteToolbar}
|
||||
</div>
|
||||
<div style={styles.rowEditorViewer}>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { RefObject, useRef, useEffect } from 'react';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import NoteTextViewer from '../../../../../NoteTextViewer';
|
||||
import { NoteViewerControl } from '../../../../../NoteTextViewer';
|
||||
|
||||
interface Props {
|
||||
editorRef: RefObject<CodeMirrorControl>;
|
||||
webviewRef: RefObject<NoteTextViewer>;
|
||||
webviewRef: RefObject<NoteViewerControl>;
|
||||
visiblePanes: string[];
|
||||
}
|
||||
|
||||
const useRefocusOnVisiblePaneChange = ({ editorRef, webviewRef, visiblePanes }: Props) => {
|
||||
const lastVisiblePanes = useRef(visiblePanes);
|
||||
useEffect(() => {
|
||||
const editorHasFocus = editorRef.current?.cm6?.dom?.contains(document.activeElement);
|
||||
const cm6Dom = editorRef.current?.cm6?.dom;
|
||||
const doc = cm6Dom?.getRootNode() as Document|null;
|
||||
const editorHasFocus = cm6Dom?.contains(doc?.activeElement);
|
||||
const viewerHasFocus = webviewRef.current?.hasFocus();
|
||||
|
||||
const lastHadViewer = lastVisiblePanes.current.includes('viewer');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle, useMemo } from 'react';
|
||||
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos, HtmlToMarkdownHandler } from '../../utils/types';
|
||||
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml } from '../../utils/resourceHandling';
|
||||
import attachedResources from '@joplin/lib/utils/attachedResources';
|
||||
@@ -41,6 +41,7 @@ const supportedLocales = require('./supportedLocales');
|
||||
import { hasProtocol } from '@joplin/utils/url';
|
||||
import useTabIndenter from './utils/useTabIndenter';
|
||||
import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
|
||||
import useDocument from '../../../hooks/useDocument';
|
||||
|
||||
const logger = Logger.create('TinyMCE');
|
||||
|
||||
@@ -99,6 +100,8 @@ let changeId_ = 1;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
const [editorContainer, setEditorContainer] = useState<HTMLDivElement|null>(null);
|
||||
const editorContainerDom = useDocument(editorContainer);
|
||||
const [editor, setEditor] = useState<Editor|null>(null);
|
||||
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
@@ -119,9 +122,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
contentKey: null,
|
||||
});
|
||||
|
||||
const rootIdRef = useRef<string>(`tinymce-${Date.now()}${Math.round(Math.random() * 10000)}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const editorRef = useRef<any>(null);
|
||||
const editorRef = useRef<Editor>(null);
|
||||
editorRef.current = editor;
|
||||
|
||||
const styles = styles_(props);
|
||||
@@ -333,6 +334,8 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorContainerDom) return () => {};
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function loadScripts() {
|
||||
@@ -351,7 +354,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
];
|
||||
|
||||
for (const s of scriptsToLoad) {
|
||||
if (document.getElementById(s.id)) {
|
||||
if (editorContainerDom.getElementById(s.id)) {
|
||||
s.loaded = true;
|
||||
continue;
|
||||
}
|
||||
@@ -359,7 +362,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Loading script', s.src);
|
||||
|
||||
await loadScript(s);
|
||||
await loadScript(s, editorContainerDom);
|
||||
if (cancelled) return;
|
||||
|
||||
s.loaded = true;
|
||||
@@ -373,19 +376,20 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [editorContainerDom]);
|
||||
|
||||
useWebViewApi(editor);
|
||||
useWebViewApi(editor, editorContainerDom?.defaultView);
|
||||
const { resetModifiedTitles: resetLinkTooltips } = useLinkTooltips(editor);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorContainerDom) return () => {};
|
||||
const theme = themeStyle(props.themeId);
|
||||
const backgroundColor = props.whiteBackgroundNoteRendering ? lightTheme.backgroundColor : theme.backgroundColor;
|
||||
|
||||
const element = document.createElement('style');
|
||||
const element = editorContainerDom.createElement('style');
|
||||
element.setAttribute('id', 'tinyMceStyle');
|
||||
document.head.appendChild(element);
|
||||
element.appendChild(document.createTextNode(`
|
||||
editorContainerDom.head.appendChild(element);
|
||||
element.appendChild(editorContainerDom.createTextNode(`
|
||||
.joplin-tinymce .tox-editor-header {
|
||||
padding-left: ${styles.leftExtraToolbarContainer.width + styles.leftExtraToolbarContainer.padding * 2}px;
|
||||
padding-right: ${styles.rightExtraToolbarContainer.width + styles.rightExtraToolbarContainer.padding * 2}px;
|
||||
@@ -582,7 +586,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
`));
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(element);
|
||||
editorContainerDom.head.removeChild(element);
|
||||
};
|
||||
// editorReady is here because TinyMCE starts by initializing a blank iframe, which needs to be
|
||||
// styled by us, otherwise users in dark mode get a bright white flash. During initialization
|
||||
@@ -594,7 +598,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
//
|
||||
// tl;dr: editorReady is used here because the css needs to be re-applied after TinyMCE init
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [editorReady, props.themeId, lightTheme, props.whiteBackgroundNoteRendering, props.watchedNoteFiles]);
|
||||
}, [editorReady, editorContainerDom, props.themeId, lightTheme, props.whiteBackgroundNoteRendering, props.watchedNoteFiles]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Enable or disable the editor
|
||||
@@ -611,6 +615,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!scriptLoaded) return;
|
||||
if (!editorContainer) return;
|
||||
|
||||
const loadEditor = async () => {
|
||||
const language = closestSupportedLocale(props.locale, true, supportedLocales);
|
||||
@@ -645,8 +650,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const editors = await (window as any).tinymce.init({
|
||||
selector: `#${rootIdRef.current}`,
|
||||
const containerWindow = editorContainerDom.defaultView as any;
|
||||
const editors = await containerWindow.tinymce.init({
|
||||
selector: `#${editorContainer.id}`,
|
||||
width: '100%',
|
||||
body_class: 'jop-tinymce',
|
||||
height: '100%',
|
||||
@@ -831,7 +837,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
|
||||
void loadEditor();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [scriptLoaded]);
|
||||
}, [scriptLoaded, editorContainer]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Set the initial content and load the plugin CSS and JS files
|
||||
@@ -1421,12 +1427,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
);
|
||||
}
|
||||
|
||||
const containerId = useMemo(() => {
|
||||
return `tinymce-container-${Math.ceil(Math.random() * 1000)}-${Date.now()}`;
|
||||
}, []);
|
||||
return (
|
||||
<div style={styles.rootStyle} className="joplin-tinymce">
|
||||
{renderDisabledOverlay()}
|
||||
{renderLeftExtraToolbarButtons()}
|
||||
{renderRightExtraToolbarButtons()}
|
||||
<div style={{ width: '100%', height: '100%' }} id={rootIdRef.current}/>
|
||||
<div style={{ width: '100%', height: '100%' }} id={containerId} ref={setEditorContainer}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,21 +8,25 @@ import { menuItems } from '../../../utils/contextMenu';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import type { Event as ElectronEvent } from 'electron';
|
||||
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { TinyMceEditorEvents } from './types';
|
||||
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from '../../../utils/types';
|
||||
import { Editor } from 'tinymce';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function contextMenuElement(editor: any, x: number, y: number) {
|
||||
function contextMenuElement(editor: Editor, x: number, y: number) {
|
||||
if (!editor || !editor.getDoc()) return null;
|
||||
|
||||
const iframes = document.getElementsByClassName('tox-edit-area__iframe');
|
||||
const containerDoc = editor.getContainer().ownerDocument;
|
||||
const iframes = containerDoc.getElementsByClassName('tox-edit-area__iframe');
|
||||
if (!iframes.length) return null;
|
||||
|
||||
const zoom = Setting.value('windowContentZoomFactor') / 100;
|
||||
@@ -31,7 +35,7 @@ function contextMenuElement(editor: any, x: number, y: number) {
|
||||
|
||||
// We use .elementFromPoint to handle the case where a dialog is covering
|
||||
// part of the editor.
|
||||
const targetElement = document.elementFromPoint(xScreen, yScreen);
|
||||
const targetElement = containerDoc.elementFromPoint(xScreen, yScreen);
|
||||
if (targetElement !== iframes[0]) {
|
||||
return null;
|
||||
}
|
||||
@@ -49,26 +53,31 @@ interface ContextMenuActionOptions {
|
||||
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
|
||||
|
||||
// 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
|
||||
export default function(editor: any, plugins: PluginStates, dispatch: Function, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) {
|
||||
export default function(editor: Editor, plugins: PluginStates, dispatch: Function, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) {
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
const contextMenuItems = menuItems(dispatch, htmlToMd, mdToHtml);
|
||||
const targetWindow = bridge().activeWindow();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function onContextMenu(_event: any, params: any) {
|
||||
function onContextMenu(event: ElectronEvent, params: any) {
|
||||
const element = contextMenuElement(editor, params.x, params.y);
|
||||
if (!element) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const menu = new Menu();
|
||||
|
||||
let itemType: ContextMenuItemType = ContextMenuItemType.None;
|
||||
let resourceId = '';
|
||||
let linkToCopy = null;
|
||||
|
||||
if (element.nodeName === 'IMG') {
|
||||
itemType = ContextMenuItemType.Image;
|
||||
resourceId = Resource.pathToId(element.src);
|
||||
resourceId = Resource.pathToId((element as HTMLImageElement).src);
|
||||
} else if (element.nodeName === 'A') {
|
||||
resourceId = Resource.pathToId(element.href);
|
||||
resourceId = Resource.pathToId((element as HTMLAnchorElement).href);
|
||||
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
|
||||
linkToCopy = element.getAttribute('href') || '';
|
||||
} else {
|
||||
@@ -94,38 +103,37 @@ export default function(editor: any, plugins: PluginStates, dispatch: Function,
|
||||
mdToHtml,
|
||||
};
|
||||
|
||||
let template = [];
|
||||
|
||||
for (const itemName in contextMenuItems) {
|
||||
const item = contextMenuItems[itemName];
|
||||
|
||||
if (!item.isActive(itemType, contextMenuActionOptions.current)) continue;
|
||||
|
||||
template.push({
|
||||
menu.append(new MenuItem({
|
||||
label: item.label,
|
||||
click: () => {
|
||||
item.onAction(contextMenuActionOptions.current);
|
||||
},
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
||||
|
||||
for (const item of spellCheckerMenuItems) {
|
||||
template.push(item);
|
||||
menu.append(new MenuItem(item));
|
||||
}
|
||||
|
||||
template = template.concat(menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu));
|
||||
for (const item of menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)) {
|
||||
menu.append(new MenuItem(item));
|
||||
}
|
||||
|
||||
const menu = bridge().Menu.buildFromTemplate(template);
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: targetWindow });
|
||||
}
|
||||
|
||||
bridge().window().webContents.on('context-menu', onContextMenu);
|
||||
targetWindow.webContents.prependListener('context-menu', onContextMenu);
|
||||
|
||||
return () => {
|
||||
if (bridge().window()?.webContents?.off) {
|
||||
bridge().window().webContents.off('context-menu', onContextMenu);
|
||||
if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) {
|
||||
targetWindow.webContents.off('context-menu', onContextMenu);
|
||||
}
|
||||
};
|
||||
}, [editor, plugins, dispatch, htmlToMd, mdToHtml]);
|
||||
|
||||
@@ -2,13 +2,14 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { useEffect } from 'react';
|
||||
import { Editor } from 'tinymce';
|
||||
|
||||
const useWebViewApi = (editor: Editor) => {
|
||||
const useWebViewApi = (editor: Editor, window: Window) => {
|
||||
useEffect(() => {
|
||||
if (!editor) return ()=>{};
|
||||
if (!window) return ()=>{};
|
||||
|
||||
const scriptElement = document.createElement('script');
|
||||
const scriptElement = window.document.createElement('script');
|
||||
const channelId = `plugin-post-message-${Math.random()}`;
|
||||
scriptElement.appendChild(document.createTextNode(`
|
||||
scriptElement.appendChild(window.document.createTextNode(`
|
||||
window.webviewApi = {
|
||||
postMessage: (contentScriptId, message) => {
|
||||
const channelId = ${JSON.stringify(channelId)};
|
||||
@@ -66,7 +67,7 @@ const useWebViewApi = (editor: Editor) => {
|
||||
window.removeEventListener('message', onMessageHandler);
|
||||
scriptElement.remove();
|
||||
};
|
||||
}, [editor]);
|
||||
}, [editor, window]);
|
||||
};
|
||||
|
||||
export default useWebViewApi;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo, useContext } from 'react';
|
||||
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
||||
import { connect } from 'react-redux';
|
||||
import MultiNoteActions from '../MultiNoteActions';
|
||||
@@ -51,6 +51,8 @@ import { MarkupLanguage } from '@joplin/renderer';
|
||||
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions';
|
||||
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks';
|
||||
import WarningBanner from './WarningBanner/WarningBanner';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||
const debounce = require('debounce');
|
||||
|
||||
const commands = [
|
||||
@@ -59,7 +61,10 @@ const commands = [
|
||||
|
||||
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
|
||||
function NoteEditor(props: NoteEditorProps) {
|
||||
const onDragOver: React.DragEventHandler = event => event.preventDefault();
|
||||
let editorIdCounter = 0;
|
||||
|
||||
function NoteEditorContent(props: NoteEditorProps) {
|
||||
const [showRevisions, setShowRevisions] = useState(false);
|
||||
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(false);
|
||||
@@ -69,9 +74,14 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
const isMountedRef = useRef(true);
|
||||
const noteSearchBarRef = useRef(null);
|
||||
|
||||
// Should be constant and unique to this instance of the editor.
|
||||
const editorId = useMemo(() => {
|
||||
return `editor-${editorIdCounter++}`;
|
||||
}, []);
|
||||
|
||||
const setFormNoteRef = useRef<OnSetFormNote>();
|
||||
const { saveNoteIfWillChange, scheduleSaveNote } = useScheduleSaveCallbacks({
|
||||
setFormNote: setFormNoteRef, dispatch: props.dispatch, editorRef,
|
||||
setFormNote: setFormNoteRef, dispatch: props.dispatch, editorRef, editorId,
|
||||
});
|
||||
const formNote_beforeLoad = useCallback(async (event: OnLoadEvent) => {
|
||||
await saveNoteIfWillChange(event.formNote);
|
||||
@@ -85,14 +95,13 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
const effectiveNoteId = useEffectiveNoteId(props);
|
||||
|
||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||
syncStarted: props.syncStarted,
|
||||
decryptionStarted: props.decryptionStarted,
|
||||
noteId: effectiveNoteId,
|
||||
isProvisional: props.isProvisional,
|
||||
titleInputRef: titleInputRef,
|
||||
editorRef: editorRef,
|
||||
onBeforeLoad: formNote_beforeLoad,
|
||||
onAfterLoad: formNote_afterLoad,
|
||||
editorId,
|
||||
});
|
||||
setFormNoteRef.current = setFormNote;
|
||||
const formNoteRef = useRef<FormNote>();
|
||||
@@ -166,6 +175,10 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
}, 100);
|
||||
}, [props.dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
props.onTitleChange?.(formNote.title);
|
||||
}, [formNote.title, props.onTitleChange]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onFieldChange = useCallback(async (field: string, value: any, changeId = 0) => {
|
||||
if (!isMountedRef.current) {
|
||||
@@ -225,6 +238,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
// 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]);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
useWindowCommandHandler({
|
||||
dispatch: props.dispatch,
|
||||
setShowLocalSearch,
|
||||
@@ -232,6 +246,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
editorRef,
|
||||
titleInputRef,
|
||||
onBodyChange,
|
||||
containerRef,
|
||||
});
|
||||
|
||||
// const onTitleKeydown = useCallback((event:any) => {
|
||||
@@ -295,7 +310,8 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
lastEditorScrollPercents: props.lastEditorScrollPercents,
|
||||
editorRef,
|
||||
});
|
||||
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
const windowId = useContext(WindowIdContext);
|
||||
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const externalEditWatcher_noteChange = useCallback((event: any) => {
|
||||
@@ -340,12 +356,19 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
useEffect(() => {
|
||||
const dependencies = {
|
||||
setShowRevisions,
|
||||
isInFocusedDocument: () => {
|
||||
return containerRef.current?.ownerDocument?.hasFocus();
|
||||
},
|
||||
};
|
||||
|
||||
CommandService.instance().componentRegisterCommands(dependencies, commands);
|
||||
const registeredCommands = CommandService.instance().componentRegisterCommands(
|
||||
dependencies,
|
||||
commands,
|
||||
true,
|
||||
);
|
||||
|
||||
return () => {
|
||||
CommandService.instance().componentUnregisterCommands(commands);
|
||||
registeredCommands.deregister();
|
||||
};
|
||||
}, [setShowRevisions]);
|
||||
|
||||
@@ -366,7 +389,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
opacity: 0.1,
|
||||
...rootStyle,
|
||||
};
|
||||
return <div style={emptyDivStyle}></div>;
|
||||
return <div style={emptyDivStyle} ref={containerRef}></div>;
|
||||
}
|
||||
|
||||
function renderTagButton() {
|
||||
@@ -464,10 +487,11 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
padding: theme.margin,
|
||||
verticalAlign: 'top',
|
||||
boxSizing: 'border-box',
|
||||
flex: 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={revStyle}>
|
||||
<div style={revStyle} ref={containerRef}>
|
||||
<NoteRevisionViewer customCss={props.customCss} noteId={formNote.id} onBack={noteRevisionViewer_onBack} />
|
||||
</div>
|
||||
);
|
||||
@@ -575,7 +599,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
return (
|
||||
<div style={styles.root} onDrop={onDrop}>
|
||||
<div style={styles.root} onDragOver={onDragOver} onDrop={onDrop} ref={containerRef}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
{renderResourceWatchingNotification()}
|
||||
{renderResourceInSearchResultsNotification()}
|
||||
@@ -606,33 +630,40 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
NoteEditor as NoteEditorComponent,
|
||||
};
|
||||
interface ConnectProps {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null;
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
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';
|
||||
}
|
||||
|
||||
return {
|
||||
noteId: noteId,
|
||||
notes: state.notes,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
noteId,
|
||||
bodyEditor,
|
||||
isProvisional: state.provisionalNoteIds.includes(noteId),
|
||||
notes: windowState.notes,
|
||||
selectedNoteIds: windowState.selectedNoteIds,
|
||||
selectedFolderId: windowState.selectedFolderId,
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
syncStarted: state.syncStarted,
|
||||
decryptionStarted: state.decryptionWorker?.state !== 'idle',
|
||||
themeId: state.settings.theme,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
notesParentType: state.notesParentType,
|
||||
selectedNoteTags: state.selectedNoteTags,
|
||||
notesParentType: windowState.notesParentType,
|
||||
selectedNoteTags: windowState.selectedNoteTags,
|
||||
lastEditorScrollPercents: state.lastEditorScrollPercents,
|
||||
selectedNoteHash: state.selectedNoteHash,
|
||||
selectedNoteHash: windowState.selectedNoteHash,
|
||||
searches: state.searches,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
customCss: state.customCss,
|
||||
noteVisiblePanes: state.noteVisiblePanes,
|
||||
selectedSearchId: windowState.selectedSearchId,
|
||||
customCss: state.customViewerCss,
|
||||
noteVisiblePanes: windowState.noteVisiblePanes,
|
||||
watchedResources: state.watchedResources,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
@@ -654,4 +685,4 @@ const mapStateToProps = (state: AppState) => {
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(NoteEditor);
|
||||
export default connect(mapStateToProps)(NoteEditorContent);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { ChangeEvent, useCallback, useRef } from 'react';
|
||||
import { ChangeEvent, useCallback, useContext, useRef } from 'react';
|
||||
import NoteToolbar from '../../NoteToolbar/NoteToolbar';
|
||||
import { buildStyle } from '@joplin/lib/theme';
|
||||
import time from '@joplin/lib/time';
|
||||
import { WindowIdContext } from '../../NewWindowOrIFrame';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -97,11 +98,14 @@ export default function NoteTitleBar(props: Props) {
|
||||
return <span className="updated-time-label" style={styles.titleDate}>{time.formatMsToLocal(props.noteUserUpdatedTime)}</span>;
|
||||
}
|
||||
|
||||
const windowId = useContext(WindowIdContext);
|
||||
|
||||
function renderNoteToolbar() {
|
||||
return <NoteToolbar
|
||||
themeId={props.themeId}
|
||||
style={styles.toolbarStyle}
|
||||
disabled={props.disabled}
|
||||
windowId={windowId}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,5 +10,8 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
execute: async () => {
|
||||
comp.setShowRevisions(true);
|
||||
},
|
||||
getPriority: () => {
|
||||
return comp.isInFocusedDocument() ? 1 : 0;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
@use "./styles/warning-banner-link.scss";
|
||||
@use "./styles/note-title-info-group.scss";
|
||||
@use "./styles/note-title-wrapper.scss";
|
||||
@use "./styles/note-editor-wrapper.scss";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
.note-editor-wrapper {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { RefObject } from 'react';
|
||||
|
||||
const getWindowCommandPriority = <T extends HTMLElement> (contentContainer: RefObject<T>) => {
|
||||
if (!contentContainer.current) return 0;
|
||||
const containerDocument = contentContainer.current.getRootNode() as Document;
|
||||
if (!containerDocument || !containerDocument.hasFocus()) return 0;
|
||||
|
||||
if (contentContainer.current.contains(containerDocument.activeElement)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Container document has focus, but not this editor.
|
||||
return 1;
|
||||
};
|
||||
export default getWindowCommandPriority;
|
||||
@@ -30,9 +30,6 @@ export interface NoteEditorProps {
|
||||
isProvisional: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
editorNoteStatuses: any;
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
bodyEditor: string;
|
||||
notesParentType: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
selectedNoteTags: any[];
|
||||
@@ -57,6 +54,9 @@ export interface NoteEditorProps {
|
||||
shareCacheSetting: string;
|
||||
syncUserId: string;
|
||||
searchResults: ProcessResultsRow[];
|
||||
|
||||
onTitleChange?: (title: string)=> void;
|
||||
bodyEditor: string;
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorRef {
|
||||
|
||||
@@ -8,14 +8,13 @@ import { join } from 'path';
|
||||
import { formNoteToNote } from '.';
|
||||
|
||||
const defaultFormNoteProps: HookDependencies = {
|
||||
syncStarted: false,
|
||||
decryptionStarted: false,
|
||||
noteId: '',
|
||||
isProvisional: false,
|
||||
titleInputRef: null,
|
||||
editorRef: null,
|
||||
onBeforeLoad: ()=>{},
|
||||
onAfterLoad: ()=>{},
|
||||
onBeforeLoad: () => { },
|
||||
onAfterLoad: () => { },
|
||||
editorId: 'editor',
|
||||
};
|
||||
|
||||
describe('useFormNote', () => {
|
||||
@@ -27,59 +26,58 @@ describe('useFormNote', () => {
|
||||
it('should update note when decryption completes', async () => {
|
||||
const testNote = await Note.save({ title: 'Test Note!' });
|
||||
|
||||
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
|
||||
const makeFormNoteProps = (): HookDependencies => {
|
||||
return {
|
||||
...defaultFormNoteProps,
|
||||
syncStarted,
|
||||
decryptionStarted,
|
||||
noteId: testNote.id,
|
||||
};
|
||||
};
|
||||
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps(true, false),
|
||||
initialProps: makeFormNoteProps(),
|
||||
});
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
title: testNote.title,
|
||||
});
|
||||
// id is falsy until after the first load of the form note.
|
||||
expect(formNote.result.current.formNote.id).not.toBeFalsy();
|
||||
});
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
title: testNote.title,
|
||||
});
|
||||
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_cipher_text: 'cipher_text',
|
||||
encryption_applied: 1,
|
||||
});
|
||||
|
||||
// Sync starting should cause a re-render
|
||||
formNote.rerender(makeFormNoteProps(false, false));
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
await act(async () => {
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_cipher_text: 'cipher_text',
|
||||
encryption_applied: 1,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
formNote.rerender(makeFormNoteProps(false, true));
|
||||
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_applied: 0,
|
||||
title: 'Test Note!',
|
||||
// Changing encryption_applied should cause a re-render
|
||||
await act(async () => {
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Ending decryption should also cause a re-render
|
||||
formNote.rerender(makeFormNoteProps(false, false));
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
await act(async () => {
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_applied: 0,
|
||||
title: 'Test Note!',
|
||||
});
|
||||
});
|
||||
|
||||
// Ending decryption should also cause a re-render
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
});
|
||||
// A larger-than-default timeout is needed to prevent CI failures:
|
||||
}, { timeout: 5_000 });
|
||||
|
||||
formNote.unmount();
|
||||
});
|
||||
|
||||
@@ -116,37 +114,33 @@ describe('useFormNote', () => {
|
||||
formNote.unmount();
|
||||
});
|
||||
|
||||
// It seems this test is crashing the worker on CI (out of memory), so disabling it for now.
|
||||
it('should reload the note when it is changed outside of the editor', async () => {
|
||||
const note = await Note.save({ title: 'Test Note!', body: '...' });
|
||||
|
||||
// it('should reload the note when it is changed outside of the editor', async () => {
|
||||
// const note = await Note.save({ title: 'Test Note!' });
|
||||
const props = {
|
||||
...defaultFormNoteProps,
|
||||
noteId: note.id,
|
||||
};
|
||||
|
||||
// const makeFormNoteProps = (dbNote: DbNote): HookDependencies => {
|
||||
// return {
|
||||
// ...defaultFormNoteProps,
|
||||
// noteId: note.id,
|
||||
// dbNote,
|
||||
// };
|
||||
// };
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
// const formNote = renderHook(props => useFormNote(props), {
|
||||
// initialProps: makeFormNoteProps({ id: note.id, updated_time: note.updated_time }),
|
||||
// });
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote.title).toBe('Test Note!');
|
||||
});
|
||||
|
||||
// await formNote.waitFor(() => {
|
||||
// expect(formNote.result.current.formNote.title).toBe('Test Note!');
|
||||
// });
|
||||
// Simulate the note being modified outside the editor
|
||||
await act(async () => {
|
||||
await Note.save({ id: note.id, title: 'Modified' });
|
||||
});
|
||||
|
||||
// // Simulate the note being modified outside the editor
|
||||
// const modifiedNote = await Note.save({ id: note.id, title: 'Modified' });
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote.title).toBe('Modified');
|
||||
});
|
||||
|
||||
// // NoteEditor then would update `dbNote`
|
||||
// formNote.rerender(makeFormNoteProps({ id: note.id, updated_time: modifiedNote.updated_time }));
|
||||
|
||||
// await formNote.waitFor(() => {
|
||||
// expect(formNote.result.current.formNote.title).toBe('Modified');
|
||||
// });
|
||||
// });
|
||||
formNote.unmount();
|
||||
});
|
||||
|
||||
test('should refresh resource infos when changed outside the editor', async () => {
|
||||
let note = await Note.save({});
|
||||
@@ -154,17 +148,15 @@ describe('useFormNote', () => {
|
||||
const resourceIds = Note.linkedItemIds(note.body);
|
||||
const resource = await Resource.load(resourceIds[0]);
|
||||
|
||||
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
|
||||
const makeFormNoteProps = (): HookDependencies => {
|
||||
return {
|
||||
...defaultFormNoteProps,
|
||||
syncStarted,
|
||||
decryptionStarted,
|
||||
noteId: note.id,
|
||||
};
|
||||
};
|
||||
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps(true, false),
|
||||
initialProps: makeFormNoteProps(),
|
||||
});
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
|
||||
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
|
||||
|
||||
const logger = Logger.create('useFormNote');
|
||||
|
||||
@@ -22,9 +23,8 @@ export interface OnLoadEvent {
|
||||
}
|
||||
|
||||
export interface HookDependencies {
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
noteId: string;
|
||||
editorId: string;
|
||||
isProvisional: boolean;
|
||||
titleInputRef: RefObject<HTMLInputElement>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -66,26 +66,85 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
type InitNoteStateCallback = (note: NoteEntity, isNew: boolean)=> Promise<FormNote>;
|
||||
const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId: string, noteId: string, initNoteState: InitNoteStateCallback) => {
|
||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||
// a new refresh.
|
||||
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
useQueuedAsyncEffect(async (event) => {
|
||||
if (formNoteRefreshScheduled <= 0) return;
|
||||
if (formNoteRef.current.hasChanged) {
|
||||
logger.info('Form note changed between scheduling a refresh and the refresh itself. Cancelling the refresh.');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
const loadNote = async () => {
|
||||
const n = await Note.load(noteId);
|
||||
if (event.cancelled || formNoteRef.current.hasChanged) return;
|
||||
|
||||
// Normally should not happened because if the note has been deleted via sync
|
||||
// it would not have been loaded in the editor (due to note selection changing
|
||||
// on delete)
|
||||
if (!n) {
|
||||
logger.warn('Trying to reload note that has been deleted:', noteId);
|
||||
return;
|
||||
}
|
||||
|
||||
await initNoteState(n, false);
|
||||
if (event.cancelled) return;
|
||||
setFormNoteRefreshScheduled(0);
|
||||
};
|
||||
|
||||
await loadNote();
|
||||
}, [formNoteRefreshScheduled, noteId, editorId, initNoteState]);
|
||||
|
||||
const refreshFormNote = useCallback(() => {
|
||||
// Increase the counter to cancel any ongoing refresh attempts
|
||||
// and start a new one.
|
||||
setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1);
|
||||
}, [formNoteRefreshScheduled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteId) return ()=>{};
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
type ChangeEventSlice = { itemId: string; changeId: string };
|
||||
const listener = ({ itemId, changeId }: ChangeEventSlice) => {
|
||||
// If this change came from the current editor, it should already be
|
||||
// handled by calls to `setFormNote`. If events from the current editor
|
||||
// aren't ignored, most user-activated note changes (e.g. a keypress)
|
||||
// cause the note to refresh. (Undesired refreshes can cause the cursor to jump).
|
||||
const isExternalChange = !(changeId ?? 'unknown').endsWith(editorId);
|
||||
if (itemId === noteId && !cancelled && isExternalChange) {
|
||||
if (formNoteRef.current.hasChanged) return;
|
||||
refreshFormNote();
|
||||
}
|
||||
};
|
||||
eventManager.on(EventName.ItemChange, listener);
|
||||
|
||||
return () => {
|
||||
eventManager.off(EventName.ItemChange, listener);
|
||||
cancelled = true;
|
||||
};
|
||||
}, [formNoteRef, noteId, editorId, refreshFormNote]);
|
||||
};
|
||||
|
||||
export default function useFormNote(dependencies: HookDependencies) {
|
||||
const {
|
||||
syncStarted, decryptionStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad,
|
||||
} = dependencies;
|
||||
const { noteId, editorId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad } = dependencies;
|
||||
|
||||
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
||||
const [isNewNote, setIsNewNote] = useState(false);
|
||||
const prevSyncStarted = usePrevious(syncStarted);
|
||||
const prevDecryptionStarted = usePrevious(decryptionStarted);
|
||||
const previousNoteId = usePrevious(formNote.id);
|
||||
const [resourceInfos, setResourceInfos] = useState<ResourceInfos>({});
|
||||
|
||||
const formNoteRef = useRef(formNote);
|
||||
formNoteRef.current = formNote;
|
||||
|
||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||
// a new refresh.
|
||||
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
const initNoteState = useCallback(async (n: NoteEntity, isNewNote: boolean) => {
|
||||
const initNoteState: InitNoteStateCallback = useCallback(async (n, isNewNote) => {
|
||||
let originalCss = '';
|
||||
|
||||
if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
|
||||
@@ -125,9 +184,9 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
logger.info('Cancelled note refresh -- form note changed while loading attached resources.');
|
||||
return null;
|
||||
}
|
||||
|
||||
setResourceInfos(resources);
|
||||
setFormNote(newFormNote);
|
||||
formNoteRef.current = newFormNote;
|
||||
|
||||
logger.debug('Resource info and form note set.');
|
||||
|
||||
@@ -136,69 +195,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
return newFormNote;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (formNoteRefreshScheduled <= 0) return () => {};
|
||||
if (formNoteRef.current.hasChanged) {
|
||||
logger.info('Form note changed between scheduling a refresh and the refresh itself. Cancelling the refresh.');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
logger.info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadNote = async () => {
|
||||
const n = await Note.load(noteId);
|
||||
if (cancelled) return;
|
||||
|
||||
// Normally should not happened because if the note has been deleted via sync
|
||||
// it would not have been loaded in the editor (due to note selection changing
|
||||
// on delete)
|
||||
if (!n) {
|
||||
logger.warn('Trying to reload note that has been deleted:', noteId);
|
||||
return;
|
||||
}
|
||||
|
||||
await initNoteState(n, false);
|
||||
|
||||
setFormNoteRefreshScheduled(0);
|
||||
};
|
||||
|
||||
void loadNote();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [formNoteRefreshScheduled, noteId, initNoteState]);
|
||||
|
||||
const refreshFormNote = useCallback(() => {
|
||||
// Increase the counter to cancel any ongoing refresh attempts
|
||||
// and start a new one.
|
||||
setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1);
|
||||
}, [formNoteRefreshScheduled]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check that synchronisation has just finished - and
|
||||
// if the note has never been changed, we reload it.
|
||||
// If the note has already been changed, it's a conflict
|
||||
// that's already been handled by the synchronizer.
|
||||
const decryptionJustEnded = prevDecryptionStarted && !decryptionStarted;
|
||||
const syncJustEnded = prevSyncStarted && !syncStarted;
|
||||
|
||||
if (!decryptionJustEnded && !syncJustEnded) return;
|
||||
if (formNoteRef.current.hasChanged) return;
|
||||
|
||||
logger.debug('Sync or decryption finished with an unchanged formNote.');
|
||||
|
||||
// Refresh the form note.
|
||||
// This is kept separate from the above logic so that when prevSyncStarted is changed
|
||||
// from true to false, it doesn't cancel the note from loading.
|
||||
refreshFormNote();
|
||||
}, [
|
||||
prevSyncStarted, syncStarted,
|
||||
prevDecryptionStarted, decryptionStarted,
|
||||
refreshFormNote,
|
||||
]);
|
||||
useRefreshFormNoteOnChange(formNoteRef, editorId, noteId, initNoteState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteId) {
|
||||
@@ -296,14 +293,14 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
// changes, with no delay during which async code can run. Even a small delay (e.g. that introduced
|
||||
// by a setState -> useEffect) can lead to a race condition. See https://github.com/laurent22/joplin/issues/8960.
|
||||
const onSetFormNote: OnSetFormNote = useCallback(newFormNote => {
|
||||
let newNote;
|
||||
if (typeof newFormNote === 'function') {
|
||||
const newNote = newFormNote(formNoteRef.current);
|
||||
formNoteRef.current = newNote;
|
||||
setFormNote(newNote);
|
||||
newNote = newFormNote(formNoteRef.current);
|
||||
} else {
|
||||
formNoteRef.current = newFormNote;
|
||||
setFormNote(newFormNote);
|
||||
newNote = newFormNote;
|
||||
}
|
||||
formNoteRef.current = newNote;
|
||||
setFormNote(newNote);
|
||||
}, [setFormNote]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,10 +5,22 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import PostMessageService from '@joplin/lib/services/PostMessageService';
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
// 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
|
||||
export default function useMessageHandler(scrollWhenReady: ScrollOptions|null, clearScrollWhenReady: ()=> void, editorRef: any, setLocalSearchResultCount: Function, dispatch: Function, formNote: FormNote, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) {
|
||||
export default function useMessageHandler(
|
||||
scrollWhenReady: ScrollOptions|null,
|
||||
clearScrollWhenReady: ()=> void,
|
||||
windowId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
editorRef: any,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
setLocalSearchResultCount: Function,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function,
|
||||
formNote: FormNote,
|
||||
htmlToMd: HtmlToMarkdownHandler,
|
||||
mdToHtml: MarkupToHtmlHandler,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return useCallback(async (event: any) => {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
@@ -49,7 +61,7 @@ export default function useMessageHandler(scrollWhenReady: ScrollOptions|null, c
|
||||
mdToHtml,
|
||||
}, dispatch);
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().activeWindow() });
|
||||
} else if (msg.indexOf('#') === 0) {
|
||||
// This is an internal anchor, which is handled by the WebView so skip this case
|
||||
} else if (msg === 'contentScriptExecuteCommand') {
|
||||
@@ -57,7 +69,7 @@ export default function useMessageHandler(scrollWhenReady: ScrollOptions|null, c
|
||||
const commandArgs = arg0.args || [];
|
||||
void CommandService.instance().execute(commandName, ...commandArgs);
|
||||
} else if (msg === 'postMessageService.message') {
|
||||
void PostMessageService.instance().postMessage(arg0);
|
||||
void PostMessageService.instance().postMessage({ ...arg0, windowId });
|
||||
} else if (msg === 'openPdfViewer') {
|
||||
await CommandService.instance().execute('openPdfViewer', arg0.resourceId, arg0.pageNo);
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ const logger = Logger.create('useScheduleSaveCallbacks');
|
||||
|
||||
interface Props {
|
||||
setFormNote: RefObject<OnSetFormNote>;
|
||||
editorId: string;
|
||||
dispatch: Dispatch;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
}
|
||||
@@ -26,7 +27,7 @@ const useScheduleSaveCallbacks = (props: Props) => {
|
||||
return async function() {
|
||||
const note = await formNoteToNote(formNote);
|
||||
logger.debug('Saving note...', note);
|
||||
const savedNote = await Note.save(note);
|
||||
const savedNote = await Note.save(note, { changeId: `editorChange-${props.editorId}` });
|
||||
|
||||
props.setFormNote.current((prev: FormNote) => {
|
||||
return { ...prev, user_updated_time: savedNote.user_updated_time, hasChanged: false };
|
||||
@@ -45,7 +46,7 @@ const useScheduleSaveCallbacks = (props: Props) => {
|
||||
|
||||
formNote.saveActionQueue.push(makeAction(formNote));
|
||||
return formNote.saveActionQueue.waitForAllDone();
|
||||
}, [props.dispatch, props.setFormNote]);
|
||||
}, [props.dispatch, props.editorId, props.setFormNote]);
|
||||
|
||||
const saveNoteIfWillChange = useCallback(async (formNote: FormNote) => {
|
||||
if (!formNote.id || !formNote.bodyWillChangeId) return;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
import { NoteBodyEditorRef, OnChangeEvent, ScrollOptionTypes } from './types';
|
||||
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
|
||||
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext, RegisteredRuntime } from '@joplin/lib/services/CommandService';
|
||||
import time from '@joplin/lib/time';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import getWindowCommandPriority from './getWindowCommandPriority';
|
||||
|
||||
const commandsWithDependencies = [
|
||||
require('../commands/showLocalSearch'),
|
||||
@@ -24,6 +25,7 @@ interface HookDependencies {
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
titleInputRef: RefObject<HTMLInputElement>;
|
||||
onBodyChange: OnBodyChange;
|
||||
containerRef: RefObject<HTMLDivElement|null>;
|
||||
}
|
||||
|
||||
function editorCommandRuntime(
|
||||
@@ -76,11 +78,19 @@ function editorCommandRuntime(
|
||||
}
|
||||
|
||||
export default function useWindowCommandHandler(dependencies: HookDependencies) {
|
||||
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, onBodyChange } = dependencies;
|
||||
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, onBodyChange, containerRef } = dependencies;
|
||||
|
||||
useEffect(() => {
|
||||
const getRuntimePriority = () => getWindowCommandPriority(containerRef);
|
||||
|
||||
const deregisterCallbacks: RegisteredRuntime[] = [];
|
||||
for (const declaration of editorCommandDeclarations) {
|
||||
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, onBodyChange));
|
||||
const runtime = editorCommandRuntime(declaration, editorRef, onBodyChange);
|
||||
deregisterCallbacks.push(CommandService.instance().registerRuntime(
|
||||
declaration.name,
|
||||
{ ...runtime, getPriority: getRuntimePriority },
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
@@ -91,17 +101,18 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
|
||||
};
|
||||
|
||||
for (const command of commandsWithDependencies) {
|
||||
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(dependencies));
|
||||
const runtime = command.runtime(dependencies);
|
||||
deregisterCallbacks.push(CommandService.instance().registerRuntime(
|
||||
command.declaration.name,
|
||||
{ ...runtime, getPriority: getRuntimePriority },
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const declaration of editorCommandDeclarations) {
|
||||
CommandService.instance().unregisterRuntime(declaration.name);
|
||||
}
|
||||
|
||||
for (const command of commandsWithDependencies) {
|
||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||
for (const runtime of deregisterCallbacks) {
|
||||
runtime.deregister();
|
||||
}
|
||||
};
|
||||
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, onBodyChange]);
|
||||
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, onBodyChange, containerRef]);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ import { _ } from '@joplin/lib/locale';
|
||||
import useActiveDescendantId from './utils/useActiveDescendantId';
|
||||
import getNoteElementIdFromJoplinId from '../NoteListItem/utils/getNoteElementIdFromJoplinId';
|
||||
import useFocusVisible from './utils/useFocusVisible';
|
||||
const { connect } = require('react-redux');
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const commands = {
|
||||
focusElementNoteList,
|
||||
@@ -311,19 +312,24 @@ const NoteList = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
interface ConnectProps {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? Folder.byId(state.folders, state.selectedFolderId) : null;
|
||||
const userId = state.settings['sync.userId'];
|
||||
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
|
||||
|
||||
return {
|
||||
notes: state.notes,
|
||||
notes: windowState.notes,
|
||||
folders: state.folders,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedNoteIds: windowState.selectedNoteIds,
|
||||
selectedFolderId: windowState.selectedFolderId,
|
||||
themeId: state.settings.theme,
|
||||
notesParentType: state.notesParentType,
|
||||
searches: state.searches,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
selectedSearchId: windowState.selectedSearchId,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
isInsertingNotes: state.isInsertingNotes,
|
||||
@@ -332,7 +338,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
customCss: state.customViewerCss,
|
||||
focusedField: state.focusedField,
|
||||
parentFolderIsReadOnly: state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync(ModelType.Folder, ItemChange.SOURCE_UNSPECIFIED, selectedFolder as ItemSlice, userId, state.shareService) : false,
|
||||
selectedFolderInTrash: itemIsInTrash(selectedFolder),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { FocusNote } from '../utils/useFocusNote';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'focusElementNoteList',
|
||||
@@ -14,6 +15,10 @@ export const runtime = (focusNote: FocusNote): CommandRuntime => {
|
||||
execute: async (context: CommandContext, noteId: string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
focusNote(noteId);
|
||||
|
||||
// The sidebar is only present in the main window. If a different window
|
||||
// is active, the main window needs to be shown.
|
||||
bridge().switchToMainWindow();
|
||||
},
|
||||
enabledCondition: 'noteListHasNotes',
|
||||
};
|
||||
|
||||
@@ -8,11 +8,12 @@ import { runtime as focusSearchRuntime } from './commands/focusSearch';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { notesSortOrderNextField } from '../../services/sortOrder/notesSortOrderUtils';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const { connect } = require('react-redux');
|
||||
import { connect } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
||||
import { Breakpoints } from '../NoteList/utils/types';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
|
||||
interface Props {
|
||||
showNewNoteButtons: boolean;
|
||||
@@ -274,17 +275,22 @@ function NoteListControls(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
interface ConnectProps {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state, { windowId: ownProps.windowId });
|
||||
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
|
||||
|
||||
return {
|
||||
showNewNoteButtons: state.selectedFolderId !== getTrashFolderId(),
|
||||
showNewNoteButtons: windowState.selectedFolderId !== getTrashFolderId(),
|
||||
newNoteButtonEnabled: CommandService.instance().isEnabled('newNote', whenClauseContext),
|
||||
newTodoButtonEnabled: CommandService.instance().isEnabled('newTodo', whenClauseContext),
|
||||
sortOrderButtonsVisible: state.settings['notes.sortOrder.buttonsVisible'],
|
||||
sortOrderField: state.settings['notes.sortOrder.field'],
|
||||
sortOrderReverse: state.settings['notes.sortOrder.reverse'],
|
||||
notesParentType: state.notesParentType,
|
||||
notesParentType: windowState.notesParentType,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,6 @@ export default (columns: NoteListColumns) => {
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuItems);
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().mainWindow() });
|
||||
}, [columns]);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ const useOnContextMenu = (
|
||||
customCss: customCss,
|
||||
});
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().mainWindow() });
|
||||
}, [selectedNoteIds, notes, dispatch, watchedNoteFiles, plugins, selectedFolderId, customCss]);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import { useMemo, useState, useEffect, useCallback, useContext } from 'react';
|
||||
import NoteList2 from '../NoteList/NoteList2';
|
||||
import NoteListControls from '../NoteListControls/NoteListControls';
|
||||
import { Size } from '../ResizableLayout/utils/types';
|
||||
@@ -17,6 +17,7 @@ import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
|
||||
import depNameToNoteProp from '@joplin/lib/services/noteList/depNameToNoteProp';
|
||||
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||
|
||||
const logger = Logger.create('NoteListWrapper');
|
||||
|
||||
@@ -163,9 +164,11 @@ export default function NoteListWrapper(props: Props) {
|
||||
/>;
|
||||
};
|
||||
|
||||
const windowId = useContext(WindowIdContext);
|
||||
const renderNoteList = () => {
|
||||
if (!listRenderer) return null;
|
||||
return <NoteList2
|
||||
windowId={windowId}
|
||||
listRenderer={listRenderer}
|
||||
resizableLayoutEventEmitter={props.resizableLayoutEventEmitter}
|
||||
size={noteListSize}
|
||||
@@ -186,6 +189,7 @@ export default function NoteListWrapper(props: Props) {
|
||||
buttonSize={noteListControlsButtonSize}
|
||||
padding={noteListControlsPadding}
|
||||
buttonVerticalGap={noteListControlsButtonVerticalGap}
|
||||
windowId={windowId}
|
||||
/>
|
||||
{renderHeader()}
|
||||
{renderNoteList()}
|
||||
|
||||
@@ -3,6 +3,9 @@ import * as React from 'react';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import bridge from '../services/bridge';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import { ForwardedRef, forwardRef, RefObject, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { WindowIdContext } from './NewWindowOrIFrame';
|
||||
import useDocument from './hooks/useDocument';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
@@ -15,230 +18,215 @@ interface Props {
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
type RemovePluginAssetsCallback = ()=> void;
|
||||
|
||||
interface SetHtmlOptions {
|
||||
pluginAssets: { path: string }[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export default class NoteTextViewerComponent extends React.Component<Props, any> {
|
||||
export interface NoteViewerControl {
|
||||
domReady(): boolean;
|
||||
setHtml(html: string, options: SetHtmlOptions): void;
|
||||
send(channel: string, arg0?: unknown, arg1?: unknown): void;
|
||||
focus(): void;
|
||||
hasFocus(): boolean;
|
||||
}
|
||||
|
||||
private initialized_ = false;
|
||||
private domReady_ = false;
|
||||
private webviewRef_: React.RefObject<HTMLIFrameElement>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private webviewListeners_: any = null;
|
||||
const usePluginMessageResponder = (webviewRef: RefObject<HTMLIFrameElement>) => {
|
||||
const windowId = useContext(WindowIdContext);
|
||||
|
||||
private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.webviewRef_ = React.createRef();
|
||||
|
||||
PostMessageService.instance().registerResponder(ResponderComponentType.NoteTextViewer, '', (message: MessageResponse) => {
|
||||
if (!this.webviewRef_?.current?.contentWindow) {
|
||||
useEffect(() => {
|
||||
PostMessageService.instance().registerResponder(ResponderComponentType.NoteTextViewer, '', windowId, (message: MessageResponse) => {
|
||||
if (!webviewRef?.current?.contentWindow) {
|
||||
reg.logger().warn('Cannot respond to message because target is gone', message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.webviewRef_.current.contentWindow.postMessage({
|
||||
webviewRef.current.contentWindow.postMessage({
|
||||
target: 'webview',
|
||||
name: 'postMessageService.response',
|
||||
data: message,
|
||||
}, '*');
|
||||
});
|
||||
|
||||
this.webview_domReady = this.webview_domReady.bind(this);
|
||||
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
|
||||
this.webview_load = this.webview_load.bind(this);
|
||||
this.webview_message = this.webview_message.bind(this);
|
||||
}
|
||||
return () => {
|
||||
PostMessageService.instance().unregisterResponder(ResponderComponentType.NoteTextViewer, '', windowId);
|
||||
};
|
||||
}, [webviewRef, windowId]);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private webview_domReady(event: any) {
|
||||
this.domReady_ = true;
|
||||
if (this.props.onDomReady) this.props.onDomReady(event);
|
||||
}
|
||||
const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerControl>) => {
|
||||
const [webview, setWebview] = useState<HTMLIFrameElement|null>(null);
|
||||
const webviewRef = useRef<HTMLIFrameElement|null>(null);
|
||||
webviewRef.current = webview;
|
||||
usePluginMessageResponder(webviewRef);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private webview_ipcMessage(event: any) {
|
||||
if (this.props.onIpcMessage) this.props.onIpcMessage(event);
|
||||
}
|
||||
const domReadyRef = useRef(false);
|
||||
type RemovePluginAssetsCallback = ()=> void;
|
||||
const removePluginAssetsCallbackRef = useRef<RemovePluginAssetsCallback|null>(null);
|
||||
|
||||
private webview_load() {
|
||||
this.webview_domReady({});
|
||||
}
|
||||
const parentDoc = useDocument(webview);
|
||||
const containerWindow = parentDoc?.defaultView;
|
||||
|
||||
private webview_message(event: MessageEvent) {
|
||||
if (event.source !== this.webviewRef_.current?.contentWindow) return;
|
||||
useImperativeHandle(ref, () => {
|
||||
const result: NoteViewerControl = {
|
||||
domReady: () => domReadyRef.current,
|
||||
setHtml: (html: string, options: SetHtmlOptions) => {
|
||||
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
|
||||
|
||||
// Grant & remove asset access.
|
||||
if (options.pluginAssets) {
|
||||
removePluginAssetsCallbackRef.current?.();
|
||||
|
||||
const pluginAssetPaths: string[] = options.pluginAssets.map((asset) => asset.path);
|
||||
const assetAccesses = pluginAssetPaths.map(
|
||||
path => protocolHandler.allowReadAccessToFile(path),
|
||||
);
|
||||
|
||||
removePluginAssetsCallbackRef.current = () => {
|
||||
for (const accessControl of assetAccesses) {
|
||||
accessControl.remove();
|
||||
}
|
||||
|
||||
removePluginAssetsCallbackRef.current = null;
|
||||
};
|
||||
}
|
||||
|
||||
result.send('setHtml', html, {
|
||||
...options,
|
||||
mediaAccessKey: protocolHandler.getMediaAccessKey(),
|
||||
});
|
||||
},
|
||||
send: (channel: string, arg0: unknown = null, arg1: unknown = null) => {
|
||||
const win = webviewRef.current?.contentWindow;
|
||||
|
||||
// Window may already be closed
|
||||
if (!win) return;
|
||||
|
||||
if (channel === 'focus') {
|
||||
win.postMessage({ target: 'webview', name: 'focus', data: {} }, '*');
|
||||
}
|
||||
|
||||
// External code should use .setHtml (rather than send('setHtml', ...))
|
||||
if (channel === 'setHtml') {
|
||||
win.postMessage({ target: 'webview', name: 'setHtml', data: { html: arg0, options: arg1 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'scrollToHash') {
|
||||
win.postMessage({ target: 'webview', name: 'scrollToHash', data: { hash: arg0 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'setPercentScroll') {
|
||||
win.postMessage({ target: 'webview', name: 'setPercentScroll', data: { percent: arg0 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'setMarkers') {
|
||||
win.postMessage({ target: 'webview', name: 'setMarkers', data: { keywords: arg0, options: arg1 } }, '*');
|
||||
}
|
||||
},
|
||||
focus: () => {
|
||||
if (webviewRef.current) {
|
||||
// Calling focus on webviewRef seems to be necessary when NoteTextViewer.focus
|
||||
// is called outside of a user event (e.g. in a setTimeout) or during automated
|
||||
// tests:
|
||||
focus('NoteTextViewer::focus', webviewRef.current);
|
||||
|
||||
// Calling .focus on this.webviewRef.current isn't sufficient.
|
||||
// To allow arrow-key scrolling, focus must also be set within the iframe:
|
||||
result.send('focus');
|
||||
}
|
||||
},
|
||||
hasFocus: () => {
|
||||
return webviewRef.current?.contains(parentDoc.activeElement);
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}, [parentDoc]);
|
||||
|
||||
const webview_domReadyRef = useRef<EventListener>();
|
||||
webview_domReadyRef.current = (event: Event) => {
|
||||
domReadyRef.current = true;
|
||||
if (props.onDomReady) props.onDomReady(event);
|
||||
};
|
||||
|
||||
const webview_ipcMessageRef = useRef<EventListener>();
|
||||
webview_ipcMessageRef.current = (event: Event) => {
|
||||
if (props.onIpcMessage) props.onIpcMessage(event);
|
||||
};
|
||||
|
||||
const webview_loadRef = useRef<EventListener>();
|
||||
webview_loadRef.current = (event: Event) => {
|
||||
webview_domReadyRef.current(event);
|
||||
};
|
||||
|
||||
type MessageEventListener = (event: MessageEvent)=> void;
|
||||
const webview_messageRef = useRef<MessageEventListener>();
|
||||
webview_messageRef.current = (event: MessageEvent) => {
|
||||
if (event.source !== webviewRef.current?.contentWindow) return;
|
||||
if (!event.data || event.data.target !== 'main') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
const args = event.data.args;
|
||||
|
||||
if (this.props.onIpcMessage) {
|
||||
this.props.onIpcMessage({
|
||||
if (props.onIpcMessage) {
|
||||
props.onIpcMessage({
|
||||
channel: callName,
|
||||
args: args,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public domReady() {
|
||||
return this.domReady_;
|
||||
}
|
||||
useEffect(() => {
|
||||
const wv = webviewRef.current;
|
||||
if (!wv || !containerWindow) return () => {};
|
||||
|
||||
public initWebview() {
|
||||
const wv = this.webviewRef_.current;
|
||||
const webviewListeners: Record<string, EventListener> = {
|
||||
'dom-ready': (event) => webview_domReadyRef.current(event),
|
||||
'ipc-message': (event) => webview_ipcMessageRef.current(event),
|
||||
'load': (event) => webview_loadRef.current(event),
|
||||
};
|
||||
|
||||
if (!this.webviewListeners_) {
|
||||
this.webviewListeners_ = {
|
||||
'dom-ready': this.webview_domReady.bind(this),
|
||||
'ipc-message': this.webview_ipcMessage.bind(this),
|
||||
'load': this.webview_load.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
for (const n in this.webviewListeners_) {
|
||||
if (!this.webviewListeners_.hasOwnProperty(n)) continue;
|
||||
const fn = this.webviewListeners_[n];
|
||||
for (const n in webviewListeners) {
|
||||
if (!webviewListeners.hasOwnProperty(n)) continue;
|
||||
const fn = webviewListeners[n];
|
||||
wv.addEventListener(n, fn);
|
||||
}
|
||||
|
||||
window.addEventListener('message', this.webview_message);
|
||||
}
|
||||
const messageListener: MessageEventListener = event => webview_messageRef.current(event);
|
||||
containerWindow.addEventListener('message', messageListener);
|
||||
|
||||
private destroyWebview() {
|
||||
const wv = this.webviewRef_.current;
|
||||
if (!wv || !this.initialized_) return;
|
||||
return () => {
|
||||
domReadyRef.current = false;
|
||||
|
||||
for (const n in this.webviewListeners_) {
|
||||
if (!this.webviewListeners_.hasOwnProperty(n)) continue;
|
||||
const fn = this.webviewListeners_[n];
|
||||
wv.removeEventListener(n, fn);
|
||||
}
|
||||
const wv = webviewRef.current;
|
||||
if (!wv) return;
|
||||
|
||||
window.removeEventListener('message', this.webview_message);
|
||||
for (const n in webviewListeners) {
|
||||
if (!webviewListeners.hasOwnProperty(n)) continue;
|
||||
const fn = webviewListeners[n];
|
||||
wv.removeEventListener(n, fn);
|
||||
}
|
||||
|
||||
this.initialized_ = false;
|
||||
this.domReady_ = false;
|
||||
containerWindow?.removeEventListener('message', messageListener);
|
||||
|
||||
this.removePluginAssetsCallback_?.();
|
||||
}
|
||||
removePluginAssetsCallbackRef.current?.();
|
||||
};
|
||||
}, [containerWindow]);
|
||||
|
||||
public focus() {
|
||||
if (this.webviewRef_.current) {
|
||||
// Calling focus on webviewRef_ seems to be necessary when NoteTextViewer.focus
|
||||
// is called outside of a user event (e.g. in a setTimeout) or during automated
|
||||
// tests:
|
||||
focus('NoteTextViewer::focus', this.webviewRef_.current);
|
||||
const viewerStyle = useMemo(() => {
|
||||
return { border: 'none', ...props.viewerStyle };
|
||||
}, [props.viewerStyle]);
|
||||
|
||||
// Calling .focus on this.webviewRef.current isn't sufficient.
|
||||
// To allow arrow-key scrolling, focus must also be set within the iframe:
|
||||
this.send('focus');
|
||||
}
|
||||
}
|
||||
// allow=fullscreen: Required to allow the user to fullscreen videos.
|
||||
return (
|
||||
<iframe
|
||||
className="noteTextViewer"
|
||||
ref={setWebview}
|
||||
style={viewerStyle}
|
||||
allow='clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=(self) encrypted-media=(self)'
|
||||
allowFullScreen={true}
|
||||
src={`joplin-content://note-viewer/${__dirname}/note-viewer/index.html`}
|
||||
></iframe>
|
||||
);
|
||||
});
|
||||
|
||||
public hasFocus() {
|
||||
return this.webviewRef_.current?.contains(document.activeElement);
|
||||
}
|
||||
|
||||
public tryInit() {
|
||||
if (!this.initialized_ && this.webviewRef_.current) {
|
||||
this.initWebview();
|
||||
this.initialized_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.tryInit();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.tryInit();
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.destroyWebview();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Wrap WebView functions
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public send(channel: string, arg0: any = null, arg1: any = null) {
|
||||
const win = this.webviewRef_.current.contentWindow;
|
||||
|
||||
if (channel === 'focus') {
|
||||
win.postMessage({ target: 'webview', name: 'focus', data: {} }, '*');
|
||||
}
|
||||
|
||||
// External code should use .setHtml (rather than send('setHtml', ...))
|
||||
if (channel === 'setHtml') {
|
||||
win.postMessage({ target: 'webview', name: 'setHtml', data: { html: arg0, options: arg1 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'scrollToHash') {
|
||||
win.postMessage({ target: 'webview', name: 'scrollToHash', data: { hash: arg0 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'setPercentScroll') {
|
||||
win.postMessage({ target: 'webview', name: 'setPercentScroll', data: { percent: arg0 } }, '*');
|
||||
}
|
||||
|
||||
if (channel === 'setMarkers') {
|
||||
win.postMessage({ target: 'webview', name: 'setMarkers', data: { keywords: arg0, options: arg1 } }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
public setHtml(html: string, options: SetHtmlOptions) {
|
||||
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
|
||||
|
||||
// Grant & remove asset access.
|
||||
if (options.pluginAssets) {
|
||||
this.removePluginAssetsCallback_?.();
|
||||
|
||||
const pluginAssetPaths: string[] = options.pluginAssets.map((asset) => asset.path);
|
||||
const assetAccesses = pluginAssetPaths.map(
|
||||
path => protocolHandler.allowReadAccessToFile(path),
|
||||
);
|
||||
|
||||
this.removePluginAssetsCallback_ = () => {
|
||||
for (const accessControl of assetAccesses) {
|
||||
accessControl.remove();
|
||||
}
|
||||
|
||||
this.removePluginAssetsCallback_ = null;
|
||||
};
|
||||
}
|
||||
|
||||
this.send('setHtml', html, {
|
||||
...options,
|
||||
mediaAccessKey: protocolHandler.getMediaAccessKey(),
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Wrap WebView functions (END)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
public render() {
|
||||
const viewerStyle = { border: 'none', ...this.props.viewerStyle };
|
||||
|
||||
// allow=fullscreen: Required to allow the user to fullscreen videos.
|
||||
return (
|
||||
<iframe
|
||||
className="noteTextViewer"
|
||||
ref={this.webviewRef_}
|
||||
style={viewerStyle}
|
||||
allow='clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=(self) encrypted-media=(self)'
|
||||
allowFullScreen={true}
|
||||
src={`joplin-content://note-viewer/${__dirname}/note-viewer/index.html`}
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default NoteTextViewer;
|
||||
|
||||
@@ -4,9 +4,10 @@ import ToolbarBase from '../ToolbarBase';
|
||||
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
const { connect } = require('react-redux');
|
||||
import { connect } from 'react-redux';
|
||||
import { buildStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { AppState } from '../../app.reducer';
|
||||
|
||||
interface NoteToolbarProps {
|
||||
themeId: number;
|
||||
@@ -42,9 +43,11 @@ function NoteToolbar(props: NoteToolbarProps) {
|
||||
|
||||
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const mapStateToProps = (state: any) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
interface ConnectProps {
|
||||
windowId: string;
|
||||
}
|
||||
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state, { windowId: ownProps.windowId });
|
||||
|
||||
return {
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function PdfViewer(props: Props) {
|
||||
mdToHtml: async (_a, b, _c) => { return { html: b, pluginAssets: [], cssStrings: [] }; },
|
||||
} as ContextMenuOptions, props.dispatch);
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().activeWindow() });
|
||||
}, [props.dispatch]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
||||
@@ -15,8 +15,6 @@ interface Props {
|
||||
defaultValue: any;
|
||||
visible: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
buttons: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClose: Function;
|
||||
@@ -82,8 +80,8 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
this.focusInput_ = false;
|
||||
}
|
||||
|
||||
public styles(themeId: number, width: number, height: number, visible: boolean) {
|
||||
const styleKey = `${themeId}_${width}_${height}_${visible}`;
|
||||
public styles(themeId: number, visible: boolean) {
|
||||
const styleKey = `${themeId}_${visible}`;
|
||||
if (styleKey === this.styleKey_) return this.styles_;
|
||||
|
||||
const theme = themeStyle(themeId);
|
||||
@@ -111,7 +109,7 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
};
|
||||
|
||||
this.styles_.input = {
|
||||
width: 0.5 * width,
|
||||
width: 'calc(0.5 * var(--prompt-width))',
|
||||
maxWidth: 400,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
@@ -123,8 +121,8 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
control: (provided: any) => {
|
||||
return { ...provided,
|
||||
minWidth: width * 0.2,
|
||||
maxWidth: width * 0.5,
|
||||
minWidth: 'calc(var(--prompt-width) * 0.2)',
|
||||
maxWidth: 'calc(var(--prompt-width) * 0.5)',
|
||||
fontFamily: theme.fontFamily,
|
||||
};
|
||||
},
|
||||
@@ -191,19 +189,16 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
|
||||
this.styles_.desc = { ...theme.textStyle, marginTop: 10 };
|
||||
|
||||
this.styles_.dialog = { maxWidth: width };
|
||||
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.state.visible) return null;
|
||||
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
|
||||
|
||||
const styles = this.styles(this.props.themeId, style.width, style.height, this.state.visible);
|
||||
const styles = this.styles(this.props.themeId, this.state.visible);
|
||||
|
||||
const onClose = (accept: boolean, buttonType: string = null) => {
|
||||
if (this.props.onClose) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import app from '../app';
|
||||
import { AppState, AppStateDialog } from '../app.reducer';
|
||||
import MainScreen from './MainScreen/MainScreen';
|
||||
import MainScreen from './MainScreen';
|
||||
import ConfigScreen from './ConfigScreen/ConfigScreen';
|
||||
import StatusScreen from './StatusScreen/StatusScreen';
|
||||
import OneDriveLoginScreen from './OneDriveLoginScreen';
|
||||
@@ -19,18 +19,17 @@ import ClipperServer from '@joplin/lib/ClipperServer';
|
||||
import DialogTitle from './DialogTitle';
|
||||
import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow';
|
||||
import Dialog from './Dialog';
|
||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
||||
import EditFolderDialog from './EditFolderDialog/Dialog';
|
||||
import PdfViewer from './PdfViewer';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
import ImportScreen from './ImportScreen';
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
import Navigator from './Navigator';
|
||||
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
||||
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
|
||||
import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsAndDialogs';
|
||||
import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer';
|
||||
import bridge from '../services/bridge';
|
||||
import EditorWindow from './NoteEditor/EditorWindow';
|
||||
const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components');
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -41,6 +40,7 @@ interface Props {
|
||||
zoomFactor: number;
|
||||
needApiAuth: boolean;
|
||||
dialogs: AppStateDialog[];
|
||||
secondaryWindowStates: WindowState[];
|
||||
}
|
||||
|
||||
interface ModalDialogProps {
|
||||
@@ -50,46 +50,6 @@ interface ModalDialogProps {
|
||||
onClick: ClickEventHandler;
|
||||
}
|
||||
|
||||
interface RegisteredDialogProps {
|
||||
themeId: number;
|
||||
key: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
}
|
||||
|
||||
interface RegisteredDialog {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
render: (props: RegisteredDialogProps, customProps: any)=> any;
|
||||
}
|
||||
|
||||
const registeredDialogs: Record<string, RegisteredDialog> = {
|
||||
syncWizard: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
|
||||
masterPassword: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
|
||||
editFolder: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
pdfViewer: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||
return <PdfViewer key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
* {
|
||||
@@ -101,7 +61,7 @@ const GlobalStyle = createGlobalStyle`
|
||||
let wcsTimeoutId_: any = null;
|
||||
|
||||
async function initialize() {
|
||||
bridge().window().on('resize', () => {
|
||||
bridge().activeWindow().on('resize', () => {
|
||||
if (wcsTimeoutId_) shim.clearTimeout(wcsTimeoutId_);
|
||||
|
||||
wcsTimeoutId_ = shim.setTimeout(() => {
|
||||
@@ -122,6 +82,11 @@ async function initialize() {
|
||||
size: bridge().windowContentSize(),
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
type: 'EDITOR_CODE_VIEW_CHANGE',
|
||||
value: Setting.value('editor.codeView'),
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
type: 'NOTE_VISIBLE_PANES_SET',
|
||||
panes: Setting.value('noteVisiblePanes'),
|
||||
@@ -196,23 +161,14 @@ class RootComponent extends React.Component<Props, any> {
|
||||
};
|
||||
}
|
||||
|
||||
private renderDialogs() {
|
||||
const props: Props = this.props;
|
||||
|
||||
if (!props.dialogs.length) return null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const output: any[] = [];
|
||||
for (const dialog of props.dialogs) {
|
||||
const md = registeredDialogs[dialog.name];
|
||||
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
|
||||
output.push(md.render({
|
||||
key: dialog.name,
|
||||
themeId: props.themeId,
|
||||
dispatch: props.dispatch,
|
||||
}, dialog.props));
|
||||
}
|
||||
return output;
|
||||
private renderSecondaryWindows() {
|
||||
return this.props.secondaryWindowStates.map((windowState: WindowState) => {
|
||||
return <EditorWindow
|
||||
key={`new-window-note-${windowState.windowId}`}
|
||||
windowId={windowState.windowId}
|
||||
newWindow={true}
|
||||
/>;
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
@@ -237,12 +193,13 @@ class RootComponent extends React.Component<Props, any> {
|
||||
return (
|
||||
<StyleSheetManager disableVendorPrefixes>
|
||||
<ThemeProvider theme={theme}>
|
||||
<StyleSheetContainer themeId={this.props.themeId}></StyleSheetContainer>
|
||||
<StyleSheetContainer/>
|
||||
<MenuBar/>
|
||||
<GlobalStyle/>
|
||||
<WindowCommandsAndDialogs windowId={defaultWindowId} />
|
||||
<Navigator style={navigatorStyle} screens={screens} className={`profile-${this.props.profileConfigCurrentProfileId}`} />
|
||||
{this.renderSecondaryWindows()}
|
||||
{this.renderModalMessage(this.modalDialogProps())}
|
||||
{this.renderDialogs()}
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
);
|
||||
@@ -258,6 +215,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
needApiAuth: state.needApiAuth,
|
||||
dialogs: state.dialogs,
|
||||
profileConfigCurrentProfileId: state.profileConfig.currentProfileId,
|
||||
secondaryWindowStates: stateUtils.secondaryWindowStates(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import useFocusHandler from './hooks/useFocusHandler';
|
||||
import useOnRenderItem from './hooks/useOnRenderItem';
|
||||
import { ListItem } from './types';
|
||||
import useSidebarCommandHandler from './hooks/useSidebarCommandHandler';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import useOnRenderListWrapper from './hooks/useOnRenderListWrapper';
|
||||
|
||||
interface Props {
|
||||
@@ -94,15 +95,17 @@ const FolderAndTagList: React.FC<Props> = props => {
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const mainWindowState = stateUtils.mainWindowState(state);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
tags: state.tags,
|
||||
folders: state.folders,
|
||||
notesParentType: state.notesParentType,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedTagId: state.selectedTagId,
|
||||
notesParentType: mainWindowState.notesParentType,
|
||||
selectedFolderId: mainWindowState.selectedFolderId,
|
||||
selectedTagId: mainWindowState.selectedTagId,
|
||||
collapsedFolderIds: state.collapsedFolderIds,
|
||||
selectedSmartFilterId: state.selectedSmartFilterId,
|
||||
selectedSmartFilterId: mainWindowState.selectedSmartFilterId,
|
||||
plugins: state.pluginService.plugins,
|
||||
tagHeaderIsExpanded: state.settings.tagHeaderIsExpanded,
|
||||
folderHeaderIsExpanded: state.settings.folderHeaderIsExpanded,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp';
|
||||
import { AppState } from '../../../app.reducer';
|
||||
import { SidebarCommandRuntimeProps } from '../types';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'focusElementSideBar',
|
||||
@@ -17,6 +18,8 @@ export const runtime = (props: SidebarCommandRuntimeProps): CommandRuntime => {
|
||||
|
||||
if (sidebarVisible) {
|
||||
props.focusSidebar();
|
||||
// The sidebar is only present in the main window:
|
||||
bridge().switchToMainWindow();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ const useOnRenderItem = (props: Props) => {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('emptyTrash')),
|
||||
);
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().activeWindow() });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ const useOnRenderItem = (props: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().activeWindow() });
|
||||
}, [props.dispatch, pluginsRef]);
|
||||
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ const AllNotesItem: React.FC<Props> = props => {
|
||||
}));
|
||||
}
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().activeWindow() });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -40,7 +40,7 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder')),
|
||||
);
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
menu.popup({ window: bridge().activeWindow() });
|
||||
}
|
||||
}, [itemId]);
|
||||
|
||||
|
||||
@@ -8,36 +8,116 @@
|
||||
// unmount is handled properly. There should only be one such component on the
|
||||
// page.
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import useDocument from '../hooks/useDocument';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../app.reducer';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
themeId: any;
|
||||
themeId: number;
|
||||
editorFontSetting: string;
|
||||
customChromeCssPaths: string[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export default function(props: Props): any {
|
||||
const [styleSheetContent, setStyleSheetContent] = useState('');
|
||||
const editorFontFromSettings = (settingValue: string) => {
|
||||
const fontFamilies = [];
|
||||
if (settingValue) fontFamilies.push(`"${settingValue}"`);
|
||||
fontFamilies.push('\'Avenir Next\', Avenir, Arial, sans-serif');
|
||||
|
||||
return fontFamilies;
|
||||
};
|
||||
|
||||
const useThemeCss = (themeId: number) => {
|
||||
const [themeCss, setThemeCss] = useState('');
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const theme = themeStyle(themeId);
|
||||
const themeCss = themeToCss(theme);
|
||||
if (event.cancelled) return;
|
||||
setStyleSheetContent(themeCss);
|
||||
}, [props.themeId]);
|
||||
setThemeCss(themeCss);
|
||||
}, [themeId]);
|
||||
|
||||
return themeCss;
|
||||
};
|
||||
|
||||
const useEditorCss = (editorFontSetting: string) => {
|
||||
return useMemo(() => {
|
||||
const fontFamilies = editorFontFromSettings(editorFontSetting);
|
||||
return `
|
||||
/* The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
|
||||
https://github.com/laurent22/joplin/issues/155
|
||||
|
||||
Note: Be careful about the specificity here. Incorrect specificity can break monospaced fonts in tables. */
|
||||
.CodeMirror5 *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }
|
||||
`;
|
||||
}, [editorFontSetting]);
|
||||
};
|
||||
|
||||
const useLinkedCss = (doc: Document|null, cssPaths: string[]) => {
|
||||
useEffect(() => {
|
||||
const element = document.createElement('style');
|
||||
element.setAttribute('id', 'main-theme-stylesheet-container');
|
||||
document.head.appendChild(element);
|
||||
element.appendChild(document.createTextNode(styleSheetContent));
|
||||
return () => {
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
}, [styleSheetContent]);
|
||||
if (!doc) return () => {};
|
||||
|
||||
return <div style={{ display: 'none' }}></div>;
|
||||
}
|
||||
const elements: HTMLElement[] = [];
|
||||
for (const path of cssPaths) {
|
||||
const element = doc.createElement('link');
|
||||
element.rel = 'stylesheet';
|
||||
element.href = path;
|
||||
element.classList.add('dynamic-linked-stylesheet');
|
||||
doc.head.appendChild(element);
|
||||
|
||||
elements.push(element);
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const element of elements) {
|
||||
element.remove();
|
||||
}
|
||||
};
|
||||
}, [doc, cssPaths]);
|
||||
};
|
||||
|
||||
const useAppliedCss = (doc: Document|null, css: string) => {
|
||||
useEffect(() => {
|
||||
if (!doc) return () => {};
|
||||
|
||||
const element = doc.createElement('style');
|
||||
element.setAttribute('id', 'main-theme-stylesheet-container');
|
||||
doc.head.appendChild(element);
|
||||
element.appendChild(document.createTextNode(css));
|
||||
return () => {
|
||||
doc.head.removeChild(element);
|
||||
};
|
||||
}, [css, doc]);
|
||||
};
|
||||
|
||||
const StyleSheetContainer: React.FC<Props> = props => {
|
||||
const [elementRef, setElementRef] = useState<HTMLElement|null>(null);
|
||||
const doc = useDocument(elementRef);
|
||||
|
||||
const themeCss = useThemeCss(props.themeId);
|
||||
const editorCss = useEditorCss(props.editorFontSetting);
|
||||
|
||||
useAppliedCss(doc, `
|
||||
/* Theme CSS */
|
||||
${themeCss}
|
||||
|
||||
/* Editor font CSS */
|
||||
${editorCss}
|
||||
`);
|
||||
useLinkedCss(doc, props.customChromeCssPaths);
|
||||
|
||||
return <div ref={setElementRef} style={{ display: 'none' }}></div>;
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
editorFontSetting: state.settings['style.editor.fontFamily'] as string,
|
||||
customChromeCssPaths: state.customChromeCssPaths,
|
||||
};
|
||||
})(StyleSheetContainer);
|
||||
|
||||
@@ -106,7 +106,8 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||
return allItems.filter(isFocusable);
|
||||
}, [allItems]);
|
||||
const containerRef = useRef<HTMLDivElement|null>(null);
|
||||
const containerHasFocus = !!containerRef.current?.contains(document.activeElement);
|
||||
const doc = containerRef.current?.ownerDocument;
|
||||
const containerHasFocus = !!containerRef.current?.contains(doc?.activeElement);
|
||||
|
||||
let keyCounter = 0;
|
||||
const renderItem = (o: ToolbarItemInfo, indexInFocusable: number) => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { AppStateDialog } from '../../app.reducer';
|
||||
import appDialogs from './utils/appDialogs';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Dispatch;
|
||||
appDialogStates: AppStateDialog[];
|
||||
}
|
||||
|
||||
const AppDialogs: React.FC<Props> = props => {
|
||||
if (!props.appDialogStates.length) return null;
|
||||
|
||||
const output: React.ReactNode[] = [];
|
||||
for (const dialog of props.appDialogStates) {
|
||||
const md = appDialogs[dialog.name];
|
||||
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
|
||||
output.push(md.render({
|
||||
key: dialog.name,
|
||||
themeId: props.themeId,
|
||||
dispatch: props.dispatch,
|
||||
}, dialog.props));
|
||||
}
|
||||
return <>{output}</>;
|
||||
};
|
||||
|
||||
export default AppDialogs;
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import Dialog from '../Dialog';
|
||||
|
||||
interface Props {
|
||||
message: string;
|
||||
}
|
||||
|
||||
const ModalMessageOverlay: React.FC<Props> = ({ message }) => {
|
||||
let brIndex = 1;
|
||||
const lines = message.split('\n').map((line: string) => {
|
||||
if (!line.trim()) return <br key={`${brIndex++}`}/>;
|
||||
return <div key={line} className="text">{line}</div>;
|
||||
});
|
||||
|
||||
return <Dialog contentFillsScreen={true}>
|
||||
<div className="modal-message">
|
||||
<div id="loading-animation" />
|
||||
<div className="text" role="status">
|
||||
{lines}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>;
|
||||
};
|
||||
|
||||
export default ModalMessageOverlay;
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import UserWebviewDialog from '../../services/plugins/UserWebviewDialog';
|
||||
import { PluginHtmlContents, PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import { VisibleDialogs } from '../../app.reducer';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
visibleDialogs: VisibleDialogs;
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
plugins: PluginStates;
|
||||
}
|
||||
|
||||
const PluginDialogs: React.FC<Props> = props => {
|
||||
const output = [];
|
||||
const infos = pluginUtils.viewInfosByType(props.plugins, 'webview');
|
||||
|
||||
for (const info of infos) {
|
||||
const { plugin, view } = info;
|
||||
if (view.containerType !== ContainerType.Dialog) continue;
|
||||
if (!props.visibleDialogs[view.id]) continue;
|
||||
const html = props.pluginHtmlContents[plugin.id]?.[view.id] ?? '';
|
||||
|
||||
output.push(<UserWebviewDialog
|
||||
key={view.id}
|
||||
viewId={view.id}
|
||||
themeId={props.themeId}
|
||||
html={html}
|
||||
scripts={view.scripts}
|
||||
pluginId={plugin.id}
|
||||
buttons={view.buttons}
|
||||
fitToContent={view.fitToContent}
|
||||
/>);
|
||||
}
|
||||
|
||||
if (!output.length) return null;
|
||||
|
||||
return (
|
||||
<div className='user-webview-dialog-container'>
|
||||
{output}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginDialogs;
|
||||
@@ -0,0 +1,197 @@
|
||||
import * as React from 'react';
|
||||
import PromptDialog from '../PromptDialog';
|
||||
import ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog';
|
||||
import NotePropertiesDialog from '../NotePropertiesDialog';
|
||||
import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog';
|
||||
import ShareNoteDialog from '../ShareNoteDialog';
|
||||
import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DialogState } from './types';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState, AppStateDialog, VisibleDialogs } from '../../app.reducer';
|
||||
import { Dispatch } from 'redux';
|
||||
import ModalMessageOverlay from './ModalMessageOverlay';
|
||||
import { EditorNoteStatuses, stateUtils } from '@joplin/lib/reducer';
|
||||
import dialogs from '../dialogs';
|
||||
import useDocument from '../hooks/useDocument';
|
||||
import useWindowCommands from './utils/useWindowCommands';
|
||||
import PluginDialogs from './PluginDialogs';
|
||||
import useSyncDialogState from './utils/useSyncDialogState';
|
||||
import AppDialogs from './AppDialogs';
|
||||
|
||||
const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
themeId: number;
|
||||
plugins: PluginStates;
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
visibleDialogs: VisibleDialogs;
|
||||
appDialogStates: AppStateDialog[];
|
||||
pluginsLegacy: unknown;
|
||||
modalMessage: string|null;
|
||||
|
||||
customCss: string;
|
||||
editorNoteStatuses: EditorNoteStatuses;
|
||||
}
|
||||
|
||||
const defaultDialogState: DialogState = {
|
||||
noteContentPropertiesDialogOptions: {
|
||||
visible: false,
|
||||
},
|
||||
shareNoteDialogOptions: {
|
||||
visible: false,
|
||||
},
|
||||
notePropertiesDialogOptions: {
|
||||
visible: false,
|
||||
},
|
||||
shareFolderDialogOptions: {
|
||||
visible: false,
|
||||
},
|
||||
promptOptions: null,
|
||||
};
|
||||
|
||||
// Certain dialog libraries need a reference to the active window:
|
||||
const useSyncActiveWindow = (containerWindow: Window|null) => {
|
||||
useEffect(() => {
|
||||
if (!containerWindow) return () => {};
|
||||
|
||||
const onFocusCallback = () => {
|
||||
dialogs.setActiveWindow(containerWindow);
|
||||
};
|
||||
if (containerWindow.document.hasFocus()) {
|
||||
onFocusCallback();
|
||||
}
|
||||
|
||||
containerWindow.addEventListener('focus', onFocusCallback);
|
||||
|
||||
return () => {
|
||||
containerWindow.removeEventListener('focus', onFocusCallback);
|
||||
};
|
||||
}, [containerWindow]);
|
||||
};
|
||||
|
||||
const WindowCommandsAndDialogs: React.FC<Props> = props => {
|
||||
const [referenceElement, setReferenceElement] = useState(null);
|
||||
const containerDocument = useDocument(referenceElement);
|
||||
|
||||
const documentRef = useRef<Document|null>(null);
|
||||
documentRef.current = containerDocument;
|
||||
|
||||
const [dialogState, setDialogState] = useState<DialogState>(defaultDialogState);
|
||||
|
||||
useSyncDialogState(dialogState, props.dispatch);
|
||||
useWindowCommands({
|
||||
documentRef,
|
||||
customCss: props.customCss,
|
||||
plugins: props.plugins,
|
||||
editorNoteStatuses: props.editorNoteStatuses,
|
||||
setDialogState,
|
||||
});
|
||||
useSyncActiveWindow(containerDocument?.defaultView);
|
||||
|
||||
const onDialogHideCallbacks = useMemo(() => {
|
||||
type OnHideCallbacks = Partial<Record<keyof DialogState, ()=> void>>;
|
||||
const result: OnHideCallbacks = {};
|
||||
for (const key of Object.keys(defaultDialogState)) {
|
||||
result[key as keyof DialogState] = () => {
|
||||
setDialogState(dialogState => {
|
||||
return {
|
||||
...dialogState,
|
||||
[key]: { visible: false },
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
const promptOnClose = useCallback((answer: unknown, buttonType: unknown) => {
|
||||
dialogState.promptOptions.onClose(answer, buttonType);
|
||||
}, [dialogState.promptOptions]);
|
||||
|
||||
const dialogInfo = PluginManager.instance().pluginDialogToShow(props.pluginsLegacy);
|
||||
const pluginDialog = !dialogInfo ? null : <dialogInfo.Dialog {...dialogInfo.props} />;
|
||||
|
||||
const { noteContentPropertiesDialogOptions, notePropertiesDialogOptions, shareNoteDialogOptions, shareFolderDialogOptions, promptOptions } = dialogState;
|
||||
|
||||
|
||||
return <>
|
||||
<div ref={setReferenceElement}/>
|
||||
{pluginDialog}
|
||||
{props.modalMessage !== null ? <ModalMessageOverlay message={props.modalMessage}/> : null}
|
||||
<PluginDialogs
|
||||
themeId={props.themeId}
|
||||
visibleDialogs={props.visibleDialogs}
|
||||
pluginHtmlContents={props.pluginHtmlContents}
|
||||
plugins={props.plugins}
|
||||
/>
|
||||
<AppDialogs
|
||||
appDialogStates={props.appDialogStates}
|
||||
themeId={props.themeId}
|
||||
dispatch={props.dispatch}
|
||||
/>
|
||||
{noteContentPropertiesDialogOptions.visible && (
|
||||
<NoteContentPropertiesDialog
|
||||
markupLanguage={noteContentPropertiesDialogOptions.markupLanguage}
|
||||
themeId={props.themeId}
|
||||
onClose={onDialogHideCallbacks.noteContentPropertiesDialogOptions}
|
||||
text={noteContentPropertiesDialogOptions.text}
|
||||
/>
|
||||
)}
|
||||
{notePropertiesDialogOptions.visible && (
|
||||
<NotePropertiesDialog
|
||||
themeId={props.themeId}
|
||||
noteId={notePropertiesDialogOptions.noteId}
|
||||
onClose={onDialogHideCallbacks.notePropertiesDialogOptions}
|
||||
onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick}
|
||||
/>
|
||||
)}
|
||||
{shareNoteDialogOptions.visible && (
|
||||
<ShareNoteDialog
|
||||
themeId={props.themeId}
|
||||
noteIds={shareNoteDialogOptions.noteIds}
|
||||
onClose={onDialogHideCallbacks.shareNoteDialogOptions}
|
||||
/>
|
||||
)}
|
||||
{shareFolderDialogOptions.visible && (
|
||||
<ShareFolderDialog
|
||||
themeId={props.themeId}
|
||||
folderId={shareFolderDialogOptions.folderId}
|
||||
onClose={onDialogHideCallbacks.shareFolderDialogOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PromptDialog
|
||||
autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null}
|
||||
defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''}
|
||||
themeId={props.themeId}
|
||||
onClose={promptOnClose}
|
||||
label={promptOptions ? promptOptions.label : ''}
|
||||
description={promptOptions ? promptOptions.description : null}
|
||||
visible={!!promptOptions}
|
||||
buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null}
|
||||
inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null}
|
||||
/>
|
||||
</>;
|
||||
};
|
||||
|
||||
interface ConnectProps {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
export default connect((state: AppState, ownProps: ConnectProps) => {
|
||||
const windowState = stateUtils.windowStateById(state, ownProps.windowId);
|
||||
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
plugins: state.pluginService.plugins,
|
||||
visibleDialogs: windowState.visibleDialogs,
|
||||
appDialogStates: windowState.dialogs,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
customCss: state.customViewerCss,
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
pluginsLegacy: state.pluginsLegacy,
|
||||
modalMessage: state.modalOverlayMessage,
|
||||
};
|
||||
})(WindowCommandsAndDialogs);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'hideModalMessage',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext) => {
|
||||
context.dispatch({ type: 'HIDE_MODAL_MESSAGE' });
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { WindowControl } from '../utils/useWindowControl';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
@@ -8,15 +9,14 @@ export const declaration: CommandDeclaration = {
|
||||
iconName: 'fa-file',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
export const runtime = (comp: WindowControl): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteIds: string[] = null) => {
|
||||
noteIds = noteIds || context.state.selectedNoteIds;
|
||||
|
||||
try {
|
||||
if (noteIds.length !== 1) throw new Error(_('Only one note can be printed at a time.'));
|
||||
await comp.printTo_('printer', { noteId: noteIds[0] });
|
||||
await comp.printTo('printer', { noteId: noteIds[0] });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'showModalMessage',
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, message: string) => {
|
||||
context.dispatch({
|
||||
type: 'SHOW_MODAL_MESSAGE',
|
||||
message,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user