1
0
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:
Henry Heino
2024-11-08 07:32:05 -08:00
committed by GitHub
parent cbef725cc8
commit 4a88d6ff7a
163 changed files with 3303 additions and 1475 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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();

View File

@@ -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 });
});
});

View File

@@ -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':

View File

@@ -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();

View File

@@ -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),

View File

@@ -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,

View 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',
};
};

View File

@@ -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);
}
},
};

View File

@@ -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) => {

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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'],

View File

@@ -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: '' } });
},
};
};

View File

@@ -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>,
},
});
},
};
};

View File

@@ -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'],

View File

@@ -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();
}

View File

@@ -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 {

View 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;

View 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);

View File

@@ -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',

View File

@@ -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,
]);
};

View File

@@ -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}>

View File

@@ -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}>

View File

@@ -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');

View File

@@ -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>
);
};

View File

@@ -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]);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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}
/>;
}

View File

@@ -10,5 +10,8 @@ export const runtime = (comp: any): CommandRuntime => {
execute: async () => {
comp.setShowRevisions(true);
},
getPriority: () => {
return comp.isInFocusedDocument() ? 1 : 0;
},
};
};

View File

@@ -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";

View File

@@ -0,0 +1,8 @@
.note-editor-wrapper {
display: flex;
flex-grow: 1;
flex-shrink: 1;
width: 100%;
height: 100%;
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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(() => {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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]);
}

View File

@@ -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),

View File

@@ -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',
};

View File

@@ -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,
};
};

View File

@@ -46,6 +46,6 @@ export default (columns: NoteListColumns) => {
const menu = Menu.buildFromTemplate(menuItems);
menu.popup({ window: bridge().window() });
menu.popup({ window: bridge().mainWindow() });
}, [columns]);
};

View File

@@ -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]);
};

View File

@@ -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()}

View File

@@ -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;

View File

@@ -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([

View File

@@ -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

View File

@@ -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) {

View File

@@ -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),
};
};

View File

@@ -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,

View File

@@ -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();
}
},

View File

@@ -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]);

View File

@@ -45,7 +45,7 @@ const AllNotesItem: React.FC<Props> = props => {
}));
}
menu.popup({ window: bridge().window() });
menu.popup({ window: bridge().activeWindow() });
}, []);
return (

View File

@@ -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]);

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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' });
},
};
};

View File

@@ -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);
}

View File

@@ -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