You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +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