You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-03 23:50:33 +02:00
This commit is contained in:
@ -297,6 +297,7 @@ packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useConnectToEditorPlugin.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
@ -1117,12 +1118,13 @@ packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/plugins/usePlugin.js
|
||||
packages/lib/hooks/plugins/useVisiblePluginEditorViewIds.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
|
||||
packages/lib/hooks/useQueuedAsyncEffect.js
|
||||
@ -1363,12 +1365,14 @@ packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorViewIds.js
|
||||
packages/lib/services/plugins/utils/isCompatible/getDefaultPlatforms.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.test.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.js
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -271,6 +271,7 @@ packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/resourceHandling.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useConnectToEditorPlugin.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
@ -1091,12 +1092,13 @@ packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/plugins/usePlugin.js
|
||||
packages/lib/hooks/plugins/useVisiblePluginEditorViewIds.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
|
||||
packages/lib/hooks/useQueuedAsyncEffect.js
|
||||
@ -1337,12 +1339,14 @@ packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getActivePluginEditorViews.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorView.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorViewIds.js
|
||||
packages/lib/services/plugins/utils/isCompatible/getDefaultPlatforms.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.test.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.js
|
||||
|
@ -52,10 +52,10 @@ import Logger from '@joplin/utils/Logger';
|
||||
import usePluginEditorView from './utils/usePluginEditorView';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import EditorPluginHandler from '@joplin/lib/services/plugins/EditorPluginHandler';
|
||||
import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
||||
import StatusBar from './StatusBar';
|
||||
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
|
||||
import useConnectToEditorPlugin from './utils/useConnectToEditorPlugin';
|
||||
|
||||
const debounce = require('debounce');
|
||||
|
||||
@ -80,12 +80,6 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
const isMountedRef = useRef(true);
|
||||
const noteSearchBarRef = useRef(null);
|
||||
|
||||
const editorPluginHandler = useMemo(() => {
|
||||
return new EditorPluginHandler(PluginService.instance());
|
||||
}, []);
|
||||
|
||||
const shownEditorViewIds = props['plugins.shownEditorViewIds'];
|
||||
|
||||
// Should be constant and unique to this instance of the editor.
|
||||
const editorId = useMemo(() => {
|
||||
return `editor-${editorIdCounter++}`;
|
||||
@ -105,18 +99,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
}, []);
|
||||
|
||||
const effectiveNoteId = useEffectiveNoteId(props);
|
||||
|
||||
useAsyncEffect(async (_event) => {
|
||||
if (!props.startupPluginsLoaded) return;
|
||||
await editorPluginHandler.emitActivationCheck();
|
||||
}, [effectiveNoteId, editorPluginHandler, props.startupPluginsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.startupPluginsLoaded) return;
|
||||
editorPluginHandler.emitUpdate(shownEditorViewIds);
|
||||
}, [effectiveNoteId, editorPluginHandler, shownEditorViewIds, props.startupPluginsLoaded]);
|
||||
|
||||
const { editorPlugin, editorView } = usePluginEditorView(props.plugins, shownEditorViewIds);
|
||||
const { editorPlugin, editorView } = usePluginEditorView(props.plugins);
|
||||
const builtInEditorVisible = !editorPlugin;
|
||||
|
||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||
@ -135,6 +118,19 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
|
||||
const formNoteFolder = useFolder({ folderId: formNote.parent_id });
|
||||
|
||||
const windowId = useContext(WindowIdContext);
|
||||
const shownEditorViewIds = useVisiblePluginEditorViewIds(props.plugins, windowId);
|
||||
useConnectToEditorPlugin({
|
||||
startupPluginsLoaded: props.startupPluginsLoaded,
|
||||
setFormNote,
|
||||
scheduleSaveNote,
|
||||
formNote,
|
||||
effectiveNoteId,
|
||||
shownEditorViewIds,
|
||||
activeEditorView: editorView,
|
||||
plugins: props.plugins,
|
||||
});
|
||||
|
||||
const {
|
||||
localSearch,
|
||||
onChange: localSearch_change,
|
||||
@ -336,7 +332,6 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
lastEditorScrollPercents: props.lastEditorScrollPercents,
|
||||
editorRef,
|
||||
});
|
||||
const windowId = useContext(WindowIdContext);
|
||||
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
|
||||
useResourceUnwatcher({ noteId: formNote.id, windowId });
|
||||
@ -706,7 +701,6 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
'plugins.shownEditorViewIds': state.settings['plugins.shownEditorViewIds'] || [],
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'historyBackward',
|
||||
'historyForward',
|
||||
|
@ -63,7 +63,6 @@ export interface NoteEditorProps {
|
||||
syncUserId: string;
|
||||
searchResults: ProcessResultsRow[];
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
'plugins.shownEditorViewIds': string[];
|
||||
onTitleChange?: (title: string)=> void;
|
||||
bodyEditor: string;
|
||||
startupPluginsLoaded: boolean;
|
||||
|
@ -0,0 +1,106 @@
|
||||
import EditorPluginHandler, { OnSaveNoteCallback } from '@joplin/lib/services/plugins/EditorPluginHandler';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { FormNote } from './types';
|
||||
import { WindowIdContext } from '../../NewWindowOrIFrame';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { PluginEditorViewState, PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
|
||||
import { OnSetFormNote } from './useFormNote';
|
||||
|
||||
const logger = Logger.create('useEditorPlugin');
|
||||
|
||||
type OnScheduleSaveNote = (formNote: FormNote)=> Promise<void>;
|
||||
|
||||
interface Props {
|
||||
plugins: PluginStates;
|
||||
startupPluginsLoaded: boolean;
|
||||
formNote: FormNote;
|
||||
activeEditorView: PluginEditorViewState;
|
||||
setFormNote: OnSetFormNote;
|
||||
scheduleSaveNote: OnScheduleSaveNote;
|
||||
effectiveNoteId: string;
|
||||
shownEditorViewIds: string[];
|
||||
}
|
||||
|
||||
const useEditorPluginHandler = (formNote: FormNote, setFormNote: OnSetFormNote, scheduleSaveNote: OnScheduleSaveNote) => {
|
||||
const formNoteRef = useRef(formNote);
|
||||
formNoteRef.current = formNote;
|
||||
const lastEditorPluginSaveRef = useRef<FormNote|null>(null);
|
||||
|
||||
return useMemo(() => {
|
||||
const onSave: OnSaveNoteCallback = async (newContent) => {
|
||||
const changed = newContent.body !== formNoteRef.current.body || newContent.id !== formNoteRef.current.id;
|
||||
if (!changed) return;
|
||||
|
||||
const differentId = newContent.id !== formNoteRef.current.id;
|
||||
const sameIdAsLastSave = newContent.id === lastEditorPluginSaveRef.current?.id;
|
||||
// Ensure that the note is being saved with the correct parent_id, title, etc.
|
||||
const sourceFormNote = differentId && sameIdAsLastSave ? lastEditorPluginSaveRef.current : formNoteRef.current;
|
||||
const newFormNote = {
|
||||
...sourceFormNote,
|
||||
id: newContent.id,
|
||||
body: newContent.body,
|
||||
hasChanged: true,
|
||||
};
|
||||
|
||||
lastEditorPluginSaveRef.current = newFormNote;
|
||||
setFormNote(newFormNote);
|
||||
return scheduleSaveNote(newFormNote);
|
||||
};
|
||||
return new EditorPluginHandler(PluginService.instance(), onSave);
|
||||
}, [setFormNote, scheduleSaveNote]);
|
||||
};
|
||||
|
||||
const useLoadedViewIdsCacheKey = (windowId: string, plugins: PluginStates) => {
|
||||
return useMemo(() => {
|
||||
const viewIds = [];
|
||||
for (const plugin of Object.values(plugins)) {
|
||||
for (const view of Object.values(plugin.views)) {
|
||||
if (view.containerType === ContainerType.Editor && view.parentWindowId === windowId) {
|
||||
viewIds.push(view.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create a string that can be easily checked for changes as an effect dependency
|
||||
return JSON.stringify(viewIds);
|
||||
}, [windowId, plugins]);
|
||||
};
|
||||
|
||||
// Connects editor plugins to the current editor (handles editor plugin saving, loading).
|
||||
const useConnectToEditorPlugin = ({
|
||||
plugins, startupPluginsLoaded, setFormNote, formNote, scheduleSaveNote, effectiveNoteId, activeEditorView, shownEditorViewIds,
|
||||
}: Props) => {
|
||||
const windowId = useContext(WindowIdContext);
|
||||
const loadedViewIdCacheKey = useLoadedViewIdsCacheKey(windowId, plugins);
|
||||
const editorPluginHandler = useEditorPluginHandler(formNote, setFormNote, scheduleSaveNote);
|
||||
|
||||
useQueuedAsyncEffect(async () => {
|
||||
if (!startupPluginsLoaded) return;
|
||||
logger.debug('Emitting activation check for views:', loadedViewIdCacheKey);
|
||||
|
||||
await editorPluginHandler.emitActivationCheck({
|
||||
parentWindowId: windowId,
|
||||
noteId: effectiveNoteId,
|
||||
});
|
||||
// It's important to re-run the activation check when the loaded view IDs change.
|
||||
// As such, `loadedViewIds` needs to be in the dependencies list:
|
||||
}, [loadedViewIdCacheKey, windowId, effectiveNoteId, editorPluginHandler, startupPluginsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeEditorView) {
|
||||
editorPluginHandler.onEditorPluginShown(activeEditorView.id);
|
||||
}
|
||||
}, [activeEditorView, editorPluginHandler]);
|
||||
|
||||
const formNoteBody = formNote.body;
|
||||
useEffect(() => {
|
||||
editorPluginHandler.emitUpdate({
|
||||
noteId: effectiveNoteId,
|
||||
newBody: formNoteBody,
|
||||
}, shownEditorViewIds);
|
||||
}, [effectiveNoteId, formNoteBody, editorPluginHandler, shownEditorViewIds]);
|
||||
};
|
||||
|
||||
export default useConnectToEditorPlugin;
|
@ -1,16 +1,20 @@
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import usePluginEditorView from './usePluginEditorView';
|
||||
import { PluginStates, PluginViewState } from '@joplin/lib/services/plugins/reducer';
|
||||
import { PluginEditorViewState, PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
|
||||
const sampleView = (): PluginViewState => {
|
||||
const sampleView = (): PluginEditorViewState => {
|
||||
return {
|
||||
buttons: [],
|
||||
containerType: ContainerType.Editor,
|
||||
id: 'view-1',
|
||||
opened: true,
|
||||
editorTypeId: 'view-1',
|
||||
type: 'webview',
|
||||
opened: true,
|
||||
active: true,
|
||||
parentWindowId: defaultWindowId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -24,7 +28,7 @@ describe('usePluginEditorView', () => {
|
||||
const pluginStates: PluginStates = {
|
||||
'0': {
|
||||
contentScripts: {},
|
||||
id: '1',
|
||||
id: '0',
|
||||
views: {
|
||||
'view-0': {
|
||||
...sampleView(),
|
||||
@ -43,7 +47,7 @@ describe('usePluginEditorView', () => {
|
||||
};
|
||||
|
||||
{
|
||||
const test = renderHook(() => usePluginEditorView(pluginStates, ['view-1']));
|
||||
const test = renderHook(() => usePluginEditorView(pluginStates));
|
||||
expect(test.result.current.editorPlugin.id).toBe('1');
|
||||
expect(test.result.current.editorView.id).toBe('view-1');
|
||||
test.unmount();
|
||||
@ -51,7 +55,7 @@ describe('usePluginEditorView', () => {
|
||||
|
||||
{
|
||||
pluginStates['1'].views['view-1'].opened = false;
|
||||
const test = renderHook(() => usePluginEditorView(pluginStates, ['view-1']));
|
||||
const test = renderHook(() => usePluginEditorView(pluginStates));
|
||||
expect(test.result.current.editorPlugin).toBeFalsy();
|
||||
test.unmount();
|
||||
}
|
||||
@ -79,7 +83,7 @@ describe('usePluginEditorView', () => {
|
||||
};
|
||||
|
||||
{
|
||||
const test = renderHook(() => usePluginEditorView(pluginStates, ['view-1']));
|
||||
const test = renderHook(() => usePluginEditorView(pluginStates));
|
||||
expect(test.result.current.editorPlugin.id).toBe('1');
|
||||
expect(test.result.current.editorView.id).toBe('view-1');
|
||||
test.unmount();
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import getShownPluginEditorView from '@joplin/lib/services/plugins/utils/getShownPluginEditorView';
|
||||
import { WindowIdContext } from '../../NewWindowOrIFrame';
|
||||
|
||||
// If a plugin editor should be shown for the current note, this function will return the plugin and
|
||||
// associated view.
|
||||
export default (plugins: PluginStates, shownEditorViewIds: string[]) => {
|
||||
export default (plugins: PluginStates) => {
|
||||
const windowId = useContext(WindowIdContext);
|
||||
return useMemo(() => {
|
||||
return getShownPluginEditorView(plugins, shownEditorViewIds);
|
||||
}, [plugins, shownEditorViewIds]);
|
||||
return getShownPluginEditorView(plugins, windowId);
|
||||
}, [plugins, windowId]);
|
||||
};
|
||||
|
@ -51,7 +51,7 @@ interface ConnectProps {
|
||||
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state, { windowId: ownProps.windowId });
|
||||
|
||||
const { editorPlugin } = getActivePluginEditorView(state.pluginService.plugins);
|
||||
const { editorPlugin } = getActivePluginEditorView(state.pluginService.plugins, ownProps.windowId);
|
||||
|
||||
const commands = [
|
||||
'showSpellCheckerMenu',
|
||||
|
@ -6,6 +6,7 @@ import EditorCodeDialog from './EditorCodeDialog';
|
||||
export default class NoteEditorPage {
|
||||
public readonly codeMirrorEditor: Locator;
|
||||
public readonly noteViewerContainer: Locator;
|
||||
public readonly editorPluginFrame: Locator;
|
||||
public readonly richTextEditor: Locator;
|
||||
public readonly noteTitleInput: Locator;
|
||||
|
||||
@ -16,6 +17,7 @@ export default class NoteEditorPage {
|
||||
public readonly toggleEditorsButton: Locator;
|
||||
public readonly toggleEditorLayoutButton: Locator;
|
||||
private readonly disableTabNavigationButton: Locator;
|
||||
public readonly toggleEditorPluginButton: Locator;
|
||||
|
||||
public readonly editorSearchInput: Locator;
|
||||
public readonly viewerSearchInput: Locator;
|
||||
@ -26,6 +28,7 @@ export default class NoteEditorPage {
|
||||
this.containerLocator = page.locator('.rli-editor');
|
||||
this.codeMirrorEditor = this.containerLocator.locator('.cm-editor');
|
||||
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
|
||||
this.editorPluginFrame = this.containerLocator.locator('iframe[id^="plugin-view-"]');
|
||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
||||
this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' });
|
||||
this.toggleCodeBlockButton = this.containerLocator.getByRole('button', { name: 'Code Block' });
|
||||
@ -36,6 +39,7 @@ export default class NoteEditorPage {
|
||||
this.editorSearchInput = this.containerLocator.getByPlaceholder('Find');
|
||||
this.viewerSearchInput = this.containerLocator.getByPlaceholder('Search...');
|
||||
this.disableTabNavigationButton = this.containerLocator.getByRole('button', { name: 'Tab moves focus' });
|
||||
this.toggleEditorPluginButton = this.containerLocator.getByRole('button', { name: 'Toggle editor plugin' });
|
||||
|
||||
this.richTextCodeEditor = new EditorCodeDialog(page);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import { msleep, Second } from '@joplin/utils/time';
|
||||
|
||||
test.describe('pluginApi', () => {
|
||||
test('the editor.setText command should update the current note (use RTE: false)', async ({ startAppWithPlugins }) => {
|
||||
@ -60,5 +61,66 @@ test.describe('pluginApi', () => {
|
||||
await mainScreen.goToAnything.runCommand(app, 'testShowToastNotification');
|
||||
await expect(notificationLocator).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should be possible to switch between multiple editor plugins', async ({ startAppWithPlugins }) => {
|
||||
const { mainWindow } = await startAppWithPlugins(['resources/test-plugins/multipleEditorPlugins.js']);
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
|
||||
await mainScreen.createNewNote('Test note');
|
||||
const toggleButton = mainScreen.noteEditor.toggleEditorPluginButton;
|
||||
|
||||
// Initially, the toggle button should be visible, and so should the default editor.
|
||||
await expect(mainScreen.noteEditor.codeMirrorEditor).toBeAttached();
|
||||
await toggleButton.click();
|
||||
|
||||
const pluginFrame = mainScreen.noteEditor.editorPluginFrame;
|
||||
await expect(pluginFrame).toBeAttached();
|
||||
|
||||
// Should describe the frame
|
||||
const frameViewIdLabel = pluginFrame.contentFrame().locator('#view-id-base');
|
||||
await expect(frameViewIdLabel).toHaveText('test-editor-plugin-1');
|
||||
|
||||
// Clicking toggle again should cycle to the next editor plugin
|
||||
await toggleButton.click();
|
||||
await expect(frameViewIdLabel).toHaveText('test-editor-plugin-2');
|
||||
|
||||
// Clicking toggle again should dismiss the editor plugins
|
||||
await toggleButton.click();
|
||||
await expect(mainScreen.noteEditor.codeMirrorEditor).toBeAttached();
|
||||
await expect(pluginFrame).not.toBeAttached();
|
||||
});
|
||||
|
||||
test('should be possible to save changes to a note using the editor plugin API', async ({ startAppWithPlugins }) => {
|
||||
const { mainWindow, app } = await startAppWithPlugins(['resources/test-plugins/editorPluginSaving.js']);
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.createNewNote('Test note');
|
||||
|
||||
const noteEditor = mainScreen.noteEditor;
|
||||
await noteEditor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.type('Initial content.');
|
||||
|
||||
const toggleButton = noteEditor.toggleEditorPluginButton;
|
||||
await toggleButton.click();
|
||||
|
||||
// Should switch to the editor
|
||||
const pluginFrame = noteEditor.editorPluginFrame;
|
||||
await expect(pluginFrame).toBeAttached();
|
||||
const pluginFrameContent = pluginFrame.contentFrame();
|
||||
await expect(pluginFrameContent.getByText('Loaded!')).toBeAttached();
|
||||
|
||||
// Editor plugin tests should pass
|
||||
await mainScreen.goToAnything.runCommand(app, 'testEditorPluginSave-test-editor-plugin');
|
||||
|
||||
// Should have saved
|
||||
await toggleButton.click();
|
||||
const expectedUpdatedText = 'Changed by test-editor-plugin';
|
||||
await expect(noteEditor.codeMirrorEditor).toHaveText(expectedUpdatedText);
|
||||
|
||||
// Regression test: Historically the editor's content would very briefly be correct, then
|
||||
// almost immediately be replaced with the old content. Doing another check after a brief
|
||||
// delay should cause the test to fail if this bug returns:
|
||||
await msleep(Second);
|
||||
await expect(noteEditor.codeMirrorEditor).toHaveText(expectedUpdatedText);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,66 @@
|
||||
// Allows referencing the Joplin global:
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// Allows the `joplin-manifest` block comment:
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "org.joplinapp.plugins.example.editorPlugin",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "3.1",
|
||||
"name": "JS Bundle test",
|
||||
"description": "JS Bundle Test plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "",
|
||||
"homepage_url": "https://joplinapp.org"
|
||||
}
|
||||
*/
|
||||
|
||||
const registerEditorPlugin = async (editorViewId) => {
|
||||
const editors = joplin.views.editors;
|
||||
const saveCallbacks = [];
|
||||
|
||||
await editors.register(editorViewId, {
|
||||
onSetup: async (viewHandle) => {
|
||||
await editors.setHtml(
|
||||
viewHandle,
|
||||
'<code>Loaded!</code>',
|
||||
);
|
||||
|
||||
let noteId;
|
||||
await editors.onUpdate(viewHandle, event => {
|
||||
noteId = event.noteId;
|
||||
});
|
||||
|
||||
saveCallbacks.push(() => {
|
||||
void editors.saveNote(viewHandle, {
|
||||
noteId,
|
||||
body: `Changed by ${editorViewId}`,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onActivationCheck: async _event => {
|
||||
// Always enable
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
await joplin.commands.register({
|
||||
name: `testEditorPluginSave-${editorViewId}`,
|
||||
label: `Test editor plugin save for ${editorViewId}`,
|
||||
iconName: 'fas fa-music',
|
||||
execute: async () => {
|
||||
for (const saveCallback of saveCallbacks) {
|
||||
saveCallback();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await registerEditorPlugin('test-editor-plugin');
|
||||
},
|
||||
});
|
@ -0,0 +1,46 @@
|
||||
// Allows referencing the Joplin global:
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// Allows the `joplin-manifest` block comment:
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "org.joplinapp.plugins.example.editorPlugin",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "3.1",
|
||||
"name": "JS Bundle test",
|
||||
"description": "JS Bundle Test plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "",
|
||||
"homepage_url": "https://joplinapp.org"
|
||||
}
|
||||
*/
|
||||
|
||||
const registerEditorPlugin = async (editorViewId) => {
|
||||
const editors = joplin.views.editors;
|
||||
await editors.register(editorViewId, {
|
||||
async onSetup(view) {
|
||||
await editors.setHtml(
|
||||
view,
|
||||
`<div id="frame-summary">
|
||||
Editor plugin:
|
||||
<code id="view-id-base">${editorViewId}</code>
|
||||
for view handle: <code>${encodeURI(view)}</code>
|
||||
</div>`,
|
||||
);
|
||||
},
|
||||
async onActivationCheck(_event) {
|
||||
// Always enable
|
||||
return true;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
// Register two different editor plugins:
|
||||
await registerEditorPlugin('test-editor-plugin-1');
|
||||
await registerEditorPlugin('test-editor-plugin-2');
|
||||
},
|
||||
});
|
@ -70,17 +70,20 @@ export const test = base.extend<JoplinFixtures>({
|
||||
// See https://github.com/microsoft/playwright/issues/8798
|
||||
//
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
profileDirectory: async ({ }, use) => {
|
||||
profileDirectory: async ({ }, use, testInfo) => {
|
||||
const profilePath = resolve(join(testDir, 'test-profile'));
|
||||
const profileSubdir = join(profilePath, uuid.createNano());
|
||||
await mkdirp(profileSubdir);
|
||||
|
||||
await use(profileSubdir);
|
||||
|
||||
// For debugging purposes, attach the Joplin log file to the test:
|
||||
await attachJoplinLog(profileSubdir, testInfo);
|
||||
|
||||
await remove(profileSubdir);
|
||||
},
|
||||
|
||||
electronApp: async ({ profileDirectory }, use, testInfo) => {
|
||||
electronApp: async ({ profileDirectory }, use) => {
|
||||
const startupArgs = createStartupArgs(profileDirectory);
|
||||
const electronApp = await electron.launch({ args: startupArgs });
|
||||
const startupPromise = waitForAppLoaded(electronApp);
|
||||
@ -89,9 +92,6 @@ export const test = base.extend<JoplinFixtures>({
|
||||
|
||||
await use(electronApp);
|
||||
|
||||
// For debugging purposes, attach the Joplin log file to the test:
|
||||
await attachJoplinLog(profileDirectory, testInfo);
|
||||
|
||||
await electronApp.firstWindow();
|
||||
await electronApp.close();
|
||||
},
|
||||
|
@ -25,9 +25,13 @@ export default function(webviewRef: RefObject<HTMLIFrameElement>, isReady: boole
|
||||
}
|
||||
|
||||
if (event.data.target === 'postMessageService.registerViewMessageHandler') {
|
||||
PostMessageService.instance().registerViewMessageHandler(ResponderComponentType.UserWebview, viewId, (message: MessageResponse) => {
|
||||
PostMessageService.instance().registerViewMessageHandler(
|
||||
ResponderComponentType.UserWebview,
|
||||
viewId,
|
||||
(message: MessageResponse) => {
|
||||
postMessage('postMessageService.plugin_message', { message });
|
||||
});
|
||||
},
|
||||
);
|
||||
} else if (event.data.target === 'postMessageService.message') {
|
||||
void PostMessageService.instance().postMessage({
|
||||
pluginId,
|
||||
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import { StyleSheet, View, useWindowDimensions } from 'react-native';
|
||||
import usePlugin from '@joplin/lib/hooks/usePlugin';
|
||||
import usePlugin from '@joplin/lib/hooks/plugins/usePlugin';
|
||||
import { DialogContentSize, DialogWebViewApi } from '../types';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
@ -4,7 +4,7 @@ import { PluginHtmlContents, ViewInfo } from '@joplin/lib/services/plugins/reduc
|
||||
import ExtendedWebView from '../../ExtendedWebView';
|
||||
import { WebViewControl } from '../../ExtendedWebView/types';
|
||||
import { ViewStyle } from 'react-native';
|
||||
import usePlugin from '@joplin/lib/hooks/usePlugin';
|
||||
import usePlugin from '@joplin/lib/hooks/plugins/usePlugin';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import useDialogMessenger from './hooks/useDialogMessenger';
|
||||
import useWebViewSetup from './hooks/useWebViewSetup';
|
||||
|
@ -2,7 +2,7 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import usePlugin from '@joplin/lib/hooks/usePlugin';
|
||||
import usePlugin from '@joplin/lib/hooks/plugins/usePlugin';
|
||||
|
||||
// initialItem is used when the plugin is not installed. For example, if the plugin item is being
|
||||
// created from search results.
|
||||
|
@ -68,6 +68,8 @@ import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getAct
|
||||
import EditorPluginHandler from '@joplin/lib/services/plugins/EditorPluginHandler';
|
||||
import AudioRecordingBanner from '../../voiceTyping/AudioRecordingBanner';
|
||||
import SpeechToTextBanner from '../../voiceTyping/SpeechToTextBanner';
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const emptyArray: any[] = [];
|
||||
@ -86,6 +88,7 @@ interface NoteNavigation {
|
||||
}
|
||||
|
||||
interface Props extends BaseProps {
|
||||
windowId: string;
|
||||
provisionalNoteIds: string[];
|
||||
navigation: NoteNavigation;
|
||||
dispatch: Dispatch;
|
||||
@ -102,13 +105,13 @@ interface Props extends BaseProps {
|
||||
highlightedWords: string[];
|
||||
noteHash: string;
|
||||
toolbarEnabled: boolean;
|
||||
'plugins.shownEditorViewIds': string[];
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
editorNoteReloadTimeRequest: number;
|
||||
}
|
||||
|
||||
interface ComponentProps extends Props {
|
||||
dialogs: DialogControl;
|
||||
visibleEditorPluginIds: string[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -170,7 +173,9 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public dialogbox: any;
|
||||
private commandRegistration_: RegisteredRuntime|null = null;
|
||||
private editorPluginHandler_ = new EditorPluginHandler(PluginService.instance());
|
||||
private editorPluginHandler_ = new EditorPluginHandler(PluginService.instance(), saveEvent => {
|
||||
return shared.noteComponent_change(this, 'body', saveEvent.body);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public static navigationOptions(): any {
|
||||
@ -561,10 +566,13 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}, 100);
|
||||
}
|
||||
|
||||
await this.editorPluginHandler_.emitActivationCheck();
|
||||
await this.editorPluginHandler_.emitActivationCheck({
|
||||
noteId: this.props.noteId,
|
||||
parentWindowId: defaultWindowId,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.editorPluginHandler_.emitUpdate(this.props['plugins.shownEditorViewIds']);
|
||||
this.emitEditorPluginUpdate_();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@ -578,7 +586,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
await ResourceFetcher.instance().markForDownload(resourceIds);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
public componentDidUpdate(prevProps: ComponentProps, prevState: State) {
|
||||
if (this.doFocusUpdate_) {
|
||||
this.doFocusUpdate_ = false;
|
||||
this.scheduleFocusUpdate();
|
||||
@ -626,15 +634,22 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props['plugins.shownEditorViewIds'] !== prevProps['plugins.shownEditorViewIds']) {
|
||||
const { editorPlugin } = getShownPluginEditorView(this.props.plugins, this.props['plugins.shownEditorViewIds']);
|
||||
if (this.props.visibleEditorPluginIds !== prevProps.visibleEditorPluginIds) {
|
||||
const { editorPlugin } = getShownPluginEditorView(this.props.plugins, this.props.windowId);
|
||||
if (!editorPlugin && this.props.editorNoteReloadTimeRequest > this.state.noteLastLoadTime) {
|
||||
void shared.reloadNote(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevProps.noteId && this.props.noteId && prevProps.noteId !== this.props.noteId) {
|
||||
void this.editorPluginHandler_.emitActivationCheck();
|
||||
void this.editorPluginHandler_.emitActivationCheck({
|
||||
noteId: this.props.noteId,
|
||||
parentWindowId: defaultWindowId,
|
||||
});
|
||||
}
|
||||
|
||||
if (prevState.note.body !== this.state.note.body) {
|
||||
this.emitEditorPluginUpdate_();
|
||||
}
|
||||
}
|
||||
|
||||
@ -659,6 +674,13 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
this.setState({ newAndNoTitleChangeNoteId: null });
|
||||
}
|
||||
|
||||
private emitEditorPluginUpdate_() {
|
||||
this.editorPluginHandler_.emitUpdate({
|
||||
noteId: this.props.noteId,
|
||||
newBody: this.state.note.body,
|
||||
}, this.props.visibleEditorPluginIds);
|
||||
}
|
||||
|
||||
private onPlainEditorTextChange = (text: string) => {
|
||||
if (!this.undoRedoService_.canUndo) {
|
||||
this.undoRedoService_.push(this.undoState());
|
||||
@ -1463,7 +1485,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
// multiple times.
|
||||
this.registerCommands();
|
||||
|
||||
const { editorPlugin, editorView } = getShownPluginEditorView(this.props.plugins, this.props['plugins.shownEditorViewIds']);
|
||||
const { editorPlugin, editorView } = getShownPluginEditorView(this.props.plugins, this.props.windowId);
|
||||
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
@ -1494,6 +1516,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}
|
||||
|
||||
const renderPluginEditor = () => {
|
||||
this.editorPluginHandler_.onEditorPluginShown(editorView.id);
|
||||
return <PluginUserWebView
|
||||
viewInfo={{ plugin: editorPlugin, view: editorView }}
|
||||
themeId={this.props.themeId}
|
||||
@ -1604,6 +1627,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
const voiceTypingDialogShown = this.state.showSpeechToTextDialog || this.state.showAudioRecorder;
|
||||
const renderActionButton = () => {
|
||||
if (voiceTypingDialogShown) return null;
|
||||
if (editorView) return null;
|
||||
if (!this.state.note || !!this.state.note.deleted_time) return null;
|
||||
|
||||
const editButton = {
|
||||
@ -1670,7 +1694,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
return result;
|
||||
};
|
||||
|
||||
const { editorPlugin: activeEditorPlugin } = getActivePluginEditorView(this.props.plugins);
|
||||
const { editorPlugin: activeEditorPlugin } = getActivePluginEditorView(this.props.plugins, this.props.windowId);
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
@ -1709,13 +1733,16 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
// how the new note should be rendered
|
||||
const NoteScreenWrapper = (props: Props) => {
|
||||
const dialogs = useContext(DialogContext);
|
||||
const visibleEditorPluginIds = useVisiblePluginEditorViewIds(props.plugins, props.windowId);
|
||||
|
||||
return (
|
||||
<NoteScreenComponent key={props.noteId} dialogs={dialogs} {...props} />
|
||||
<NoteScreenComponent key={props.noteId} dialogs={dialogs} visibleEditorPluginIds={visibleEditorPluginIds} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
const NoteScreen = connect((state: AppState) => {
|
||||
return {
|
||||
windowId: state.windowId,
|
||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
noteHash: state.selectedNoteHash,
|
||||
itemType: state.selectedItemType,
|
||||
@ -1732,7 +1759,6 @@ const NoteScreen = connect((state: AppState) => {
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
'plugins.shownEditorViewIds': state.settings['plugins.shownEditorViewIds'] || [],
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
editorNoteReloadTimeRequest: state.editorNoteReloadTimeRequest,
|
||||
|
||||
|
@ -14,7 +14,7 @@ import Plugin from '../Plugin';
|
||||
* now, are not well documented. You can find the list directly on GitHub
|
||||
* though at the following locations:
|
||||
*
|
||||
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands)
|
||||
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/WindowCommandsAndDialogs/commands)
|
||||
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands)
|
||||
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts)
|
||||
*
|
||||
@ -25,8 +25,13 @@ import Plugin from '../Plugin';
|
||||
* commands can be found in these places:
|
||||
*
|
||||
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/commands)
|
||||
* * [Note screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/components/screens/Note/commands)
|
||||
* * [Editor commands](https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/components/NoteEditor/commandDeclarations.ts)
|
||||
*
|
||||
* Additionally, certain global commands have the same implementation on both platforms:
|
||||
*
|
||||
* * [Shared global commands](https://github.com/laurent22/joplin/tree/dev/packages/lib/commands)
|
||||
*
|
||||
* ## Executing editor commands
|
||||
*
|
||||
* There might be a situation where you want to invoke editor commands
|
||||
|
@ -42,9 +42,11 @@ export default class JoplinSettings {
|
||||
*/
|
||||
values(keys: string[] | string): Promise<Record<string, unknown>>;
|
||||
/**
|
||||
* @deprecated Use joplin.settings.values()
|
||||
* Gets a setting value (only applies to setting you registered from your plugin).
|
||||
*
|
||||
* Gets a setting value (only applies to setting you registered from your plugin)
|
||||
* Note: If you want to retrieve all your plugin settings, for example when the plugin starts,
|
||||
* it is recommended to use the `values()` function instead - it will be much faster than
|
||||
* calling `value()` multiple times.
|
||||
*/
|
||||
value(key: string): Promise<any>;
|
||||
/**
|
||||
|
@ -9,8 +9,17 @@ import JoplinViewsEditors from './JoplinViewsEditor';
|
||||
/**
|
||||
* This namespace provides access to view-related services.
|
||||
*
|
||||
* All view services provide a `create()` method which you would use to create the view object, whether it's a dialog, a toolbar button or a menu item.
|
||||
* In some cases, the `create()` method will return a [[ViewHandle]], which you would use to act on the view, for example to set certain properties or call some methods.
|
||||
* ## Creating a view
|
||||
*
|
||||
* All view services provide a `create()` method which you would use to create the view object,
|
||||
* whether it's a dialog, a toolbar button or a menu item. In some cases, the `create()` method will
|
||||
* return a [[ViewHandle]], which you would use to act on the view, for example to set certain
|
||||
* properties or call some methods.
|
||||
*
|
||||
* ## The `webviewApi` object
|
||||
*
|
||||
* Within a view, you can use the global object `webviewApi` for various utility functions, such as
|
||||
* sending messages or displaying context menu. Refer to [[WebviewApi]] for the full documentation.
|
||||
*/
|
||||
export default class JoplinViews {
|
||||
private store;
|
||||
|
@ -1,5 +1,18 @@
|
||||
import Plugin from '../Plugin';
|
||||
import { ActivationCheckCallback, ViewHandle, UpdateCallback } from './types';
|
||||
import { ActivationCheckCallback, ViewHandle, UpdateCallback, EditorPluginCallbacks } from './types';
|
||||
interface SaveNoteOptions {
|
||||
/**
|
||||
* The ID of the note to save. This should match either:
|
||||
* - The ID of the note currently being edited
|
||||
* - The ID of a note that was very recently open in the editor.
|
||||
*
|
||||
* This property is present to ensure that the note editor doesn't write
|
||||
* to the wrong note just after switching notes.
|
||||
*/
|
||||
noteId: string;
|
||||
/** The note's new content. */
|
||||
body: string;
|
||||
}
|
||||
/**
|
||||
* Allows creating alternative note editors. You can create a view to handle loading and saving the
|
||||
* note, and do your own rendering.
|
||||
@ -41,10 +54,18 @@ export default class JoplinViewsEditors {
|
||||
private store;
|
||||
private plugin;
|
||||
private activationCheckHandlers_;
|
||||
private unhandledActivationCheck_;
|
||||
constructor(plugin: Plugin, store: any);
|
||||
private controller;
|
||||
/**
|
||||
* Registers a new editor plugin. Joplin will call the provided callback to create new editor views
|
||||
* associated with the plugin as necessary (e.g. when a new editor is created in a new window).
|
||||
*/
|
||||
register(viewId: string, callbacks: EditorPluginCallbacks): Promise<void>;
|
||||
/**
|
||||
* Creates a new editor view
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
create(id: string): Promise<ViewHandle>;
|
||||
/**
|
||||
@ -59,11 +80,18 @@ export default class JoplinViewsEditors {
|
||||
* See [[JoplinViewPanels]]
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Saves the content of the editor, without calling `onUpdate` for editors in the same window.
|
||||
*/
|
||||
saveNote(handle: ViewHandle, props: SaveNoteOptions): Promise<void>;
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*
|
||||
* @deprecated - `onActivationCheck` should be provided when the editor is first created with
|
||||
* `editor.register`.
|
||||
*/
|
||||
onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise<void>;
|
||||
/**
|
||||
@ -86,3 +114,4 @@ export default class JoplinViewsEditors {
|
||||
*/
|
||||
isVisible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
export {};
|
||||
|
@ -80,5 +80,9 @@ export default class JoplinViewsPanels {
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
/**
|
||||
* Assuming that the current panel is an editor plugin view, returns
|
||||
* whether the editor plugin view supports editing the current note.
|
||||
*/
|
||||
isActive(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
@ -80,6 +80,8 @@ export default class JoplinWorkspace {
|
||||
filterEditorContextMenu(handler: FilterHandler<EditContextMenuFilterObject>): void;
|
||||
/**
|
||||
* Gets the currently selected note. Will be `null` if no note is selected.
|
||||
*
|
||||
* On desktop, this returns the selected note in the focused window.
|
||||
*/
|
||||
selectedNote(): Promise<any>;
|
||||
/**
|
||||
@ -93,5 +95,12 @@ export default class JoplinWorkspace {
|
||||
* Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes.
|
||||
*/
|
||||
selectedNoteIds(): Promise<string[]>;
|
||||
/**
|
||||
* Gets the last hash (note section ID) from cross-note link targeting specific section.
|
||||
* New hash is available after `onNoteSelectionChange()` is triggered.
|
||||
* Example of cross-note link where `hello-world` is a hash: [Other Note Title](:/9bc9a5cb83f04554bf3fd3e41b4bb415#hello-world).
|
||||
* Method returns empty value when a note was navigated with method other than cross-note link containing valid hash.
|
||||
*/
|
||||
selectedNoteHash(): Promise<string>;
|
||||
}
|
||||
export {};
|
||||
|
@ -397,9 +397,40 @@ export interface Rectangle {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type ActivationCheckCallback = ()=> Promise<boolean>;
|
||||
export interface EditorUpdateEvent {
|
||||
newBody: string;
|
||||
noteId: string;
|
||||
}
|
||||
export type UpdateCallback = (event: EditorUpdateEvent)=> Promise<void>;
|
||||
|
||||
export type UpdateCallback = ()=> Promise<void>;
|
||||
|
||||
export interface ActivationCheckEvent {
|
||||
handle: ViewHandle;
|
||||
noteId: string;
|
||||
}
|
||||
export type ActivationCheckCallback = (event: ActivationCheckEvent)=> Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Required callbacks for creating an editor plugin.
|
||||
*/
|
||||
export interface EditorPluginCallbacks {
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*/
|
||||
onActivationCheck: ActivationCheckCallback;
|
||||
|
||||
/**
|
||||
* Emitted when an editor view is created. This happens, for example, when a new window containing
|
||||
* a new editor is created.
|
||||
*
|
||||
* This callback should set the editor plugin's HTML using `editors.setHtml`, add scripts to the editor
|
||||
* with `editors.addScript`, and optionally listen for external changes using `editors.onUpdate`.
|
||||
*/
|
||||
onSetup: (handle: ViewHandle)=> Promise<void>;
|
||||
}
|
||||
|
||||
export type VisibleHandler = ()=> Promise<void>;
|
||||
|
||||
@ -408,6 +439,8 @@ export interface EditContextMenuFilterObject {
|
||||
}
|
||||
|
||||
export interface EditorActivationCheckFilterObject {
|
||||
effectiveNoteId: string;
|
||||
windowId: string;
|
||||
activatedEditors: {
|
||||
pluginId: string;
|
||||
viewId: string;
|
||||
@ -417,6 +450,20 @@ export interface EditorActivationCheckFilterObject {
|
||||
|
||||
export type FilterHandler<T> = (object: T)=> Promise<T>;
|
||||
|
||||
export type CommandArgument = string|number|object|boolean|null;
|
||||
|
||||
export interface MenuTemplateItem {
|
||||
label?: string;
|
||||
command?: string;
|
||||
commandArgs?: CommandArgument[];
|
||||
}
|
||||
|
||||
export interface WebviewApi {
|
||||
postMessage: (message: object)=> unknown;
|
||||
onMessage: (message: object)=> void;
|
||||
menuPopupFromTemplate: (template: MenuTemplateItem[])=> void;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Settings types
|
||||
// =================================================================
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from '../services/CommandService';
|
||||
import Setting from '../models/Setting';
|
||||
import getActivePluginEditorView from '../services/plugins/utils/getActivePluginEditorView';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import getActivePluginEditorViews from '../services/plugins/utils/getActivePluginEditorViews';
|
||||
import PluginService from '../services/plugins/PluginService';
|
||||
import WebviewController from '../services/plugins/WebviewController';
|
||||
import Setting from '../models/Setting';
|
||||
|
||||
const logger = Logger.create('showEditorPlugin');
|
||||
|
||||
@ -16,10 +19,10 @@ export const runtime = (): CommandRuntime => {
|
||||
execute: async (context: CommandContext, editorViewId = '', show = true) => {
|
||||
logger.info('View:', editorViewId, 'Show:', show);
|
||||
|
||||
const shownEditorViewIds = Setting.value('plugins.shownEditorViewIds');
|
||||
|
||||
const pluginStates = context.state.pluginService.plugins;
|
||||
const windowId = context.state.windowId;
|
||||
if (!editorViewId) {
|
||||
const { editorPlugin, editorView } = getActivePluginEditorView(context.state.pluginService.plugins);
|
||||
const { editorPlugin, editorView } = getActivePluginEditorView(pluginStates, windowId);
|
||||
|
||||
if (!editorPlugin) {
|
||||
logger.warn('No editor plugin to toggle to');
|
||||
@ -29,27 +32,43 @@ export const runtime = (): CommandRuntime => {
|
||||
editorViewId = editorView.id;
|
||||
}
|
||||
|
||||
const idx = shownEditorViewIds.indexOf(editorViewId);
|
||||
|
||||
if (show) {
|
||||
if (idx >= 0) {
|
||||
logger.info(`Editor is already visible: ${editorViewId}`);
|
||||
const activePlugins = getActivePluginEditorViews(pluginStates, windowId);
|
||||
const editorPluginData = activePlugins.find(({ editorView }) => editorView.id === editorViewId);
|
||||
if (!editorPluginData) {
|
||||
logger.warn(`No editor view with ID ${editorViewId} is active.`);
|
||||
return;
|
||||
}
|
||||
const { editorView } = editorPluginData;
|
||||
const controller = PluginService.instance().viewControllerByViewId(editorView.id) as WebviewController;
|
||||
if (!controller) {
|
||||
throw new Error(`No controller registered for editor view ${editorView.id}`);
|
||||
}
|
||||
|
||||
shownEditorViewIds.push(editorViewId);
|
||||
} else {
|
||||
if (idx < 0) {
|
||||
const previousVisible = editorView.parentWindowId === windowId && controller.isVisible();
|
||||
|
||||
if (show && previousVisible) {
|
||||
logger.info(`Editor is already visible: ${editorViewId}`);
|
||||
return;
|
||||
} else if (!show && !previousVisible) {
|
||||
logger.info(`Editor is already hidden: ${editorViewId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
shownEditorViewIds.splice(idx, 1);
|
||||
const getUpdatedShownViewIds = () => {
|
||||
let newShownViewTypeIds = [...Setting.value('plugins.shownEditorViewIds')];
|
||||
// Always filter out the current view, even if show is false. This prevents
|
||||
// the view ID from being present multiple times.
|
||||
const viewIdsWithoutCurrent = newShownViewTypeIds.filter(id => id !== editorView.editorTypeId);
|
||||
newShownViewTypeIds = viewIdsWithoutCurrent;
|
||||
|
||||
if (show) {
|
||||
newShownViewTypeIds.push(editorView.editorTypeId);
|
||||
}
|
||||
return newShownViewTypeIds;
|
||||
};
|
||||
Setting.setValue('plugins.shownEditorViewIds', getUpdatedShownViewIds());
|
||||
|
||||
logger.info('Shown editor IDs:', shownEditorViewIds);
|
||||
|
||||
Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds);
|
||||
controller.setOpened(show);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from '../services/CommandService';
|
||||
import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from '../services/CommandService';
|
||||
import { _ } from '../locale';
|
||||
import Setting from '../models/Setting';
|
||||
import getActivePluginEditorView from '../services/plugins/utils/getActivePluginEditorView';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import getActivePluginEditorViews from '../services/plugins/utils/getActivePluginEditorViews';
|
||||
import getShownPluginEditorView from '../services/plugins/utils/getShownPluginEditorView';
|
||||
|
||||
const logger = Logger.create('toggleEditorPlugin');
|
||||
|
||||
@ -15,29 +15,34 @@ export const declaration: CommandDeclaration = {
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext) => {
|
||||
const shownEditorViewIds = Setting.value('plugins.shownEditorViewIds');
|
||||
const { editorPlugin, editorView } = getActivePluginEditorView(context.state.pluginService.plugins);
|
||||
const activeWindowId = context.state.windowId;
|
||||
const activePluginStates = getActivePluginEditorViews(context.state.pluginService.plugins, activeWindowId);
|
||||
|
||||
if (!editorPlugin) {
|
||||
if (activePluginStates.length === 0) {
|
||||
logger.warn('No editor plugin to toggle to');
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = shownEditorViewIds.indexOf(editorView.id);
|
||||
let hasBeenHidden = false;
|
||||
let showedView = false;
|
||||
const setEditorPluginVisible = async (viewId: string, visible: boolean) => {
|
||||
await CommandService.instance().execute('showEditorPlugin', viewId, visible);
|
||||
showedView ||= visible;
|
||||
};
|
||||
|
||||
if (idx < 0) {
|
||||
shownEditorViewIds.push(editorView.id);
|
||||
} else {
|
||||
shownEditorViewIds.splice(idx, 1);
|
||||
hasBeenHidden = true;
|
||||
const { editorView: visibleView } = getShownPluginEditorView(context.state.pluginService.plugins, activeWindowId);
|
||||
// Hide the visible view
|
||||
if (visibleView) {
|
||||
await setEditorPluginVisible(visibleView.id, false);
|
||||
}
|
||||
|
||||
logger.info('New shown editor views: ', shownEditorViewIds);
|
||||
// Show the next view
|
||||
const visibleViewIndex = activePluginStates.findIndex(state => state.editorView.id === visibleView?.id);
|
||||
const nextIndex = visibleViewIndex + 1;
|
||||
if (nextIndex < activePluginStates.length) {
|
||||
await setEditorPluginVisible(activePluginStates[nextIndex].editorView.id, true);
|
||||
}
|
||||
|
||||
Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds);
|
||||
|
||||
if (hasBeenHidden) {
|
||||
if (!showedView) {
|
||||
// When the plugin editor goes from visible to hidden, we need to reload the note
|
||||
// because it may have been changed via the data API.
|
||||
context.dispatch({
|
||||
|
@ -5,7 +5,7 @@ import Note from '../../models/Note';
|
||||
import { reg } from '../../registry';
|
||||
import ResourceFetcher from '../../services/ResourceFetcher';
|
||||
import DecryptionWorker from '../../services/DecryptionWorker';
|
||||
import eventManager from '../../eventManager';
|
||||
import eventManager, { EventName } from '../../eventManager';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import shim from '../../shim';
|
||||
import { Dispatch } from 'redux';
|
||||
@ -142,4 +142,14 @@ export default async (store: any, _next: any, action: any, dispatch: Dispatch) =
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === 'WINDOW_OPEN') {
|
||||
eventManager.emit(EventName.WindowOpen, {
|
||||
windowId: action.windowId,
|
||||
});
|
||||
} else if (action.type === 'WINDOW_CLOSE') {
|
||||
eventManager.emit(EventName.WindowClose, {
|
||||
windowId: action.windowId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -19,6 +19,8 @@ export enum EventName {
|
||||
NoteContentChange = 'noteContentChange',
|
||||
OcrServiceResourcesProcessed = 'ocrServiceResourcesProcessed',
|
||||
NoteResourceIndexed = 'noteResourceIndexed',
|
||||
WindowOpen = 'windowOpen',
|
||||
WindowClose = 'windowClose',
|
||||
}
|
||||
|
||||
interface ItemChangeEvent {
|
||||
@ -56,6 +58,14 @@ interface AlarmChangeEvent {
|
||||
note: NoteEntity;
|
||||
}
|
||||
|
||||
export interface WindowOpenEvent {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
export interface WindowCloseEvent {
|
||||
windowId: string;
|
||||
}
|
||||
|
||||
type EventArgs = {
|
||||
[EventName.ResourceCreate]: [];
|
||||
[EventName.ResourceChange]: [ResourceChangeEvent];
|
||||
@ -71,6 +81,8 @@ type EventArgs = {
|
||||
[EventName.NoteContentChange]: [NoteContentChangeEvent];
|
||||
[EventName.OcrServiceResourcesProcessed]: [];
|
||||
[EventName.NoteResourceIndexed]: [];
|
||||
[EventName.WindowOpen]: [WindowOpenEvent];
|
||||
[EventName.WindowClose]: [WindowCloseEvent];
|
||||
};
|
||||
|
||||
type EventListenerCallbacks = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import PluginService from '../services/plugins/PluginService';
|
||||
import PluginService from '../../services/plugins/PluginService';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import shim from '../shim';
|
||||
import shim from '../../shim';
|
||||
|
||||
const logger = Logger.create('usePlugin');
|
||||
|
13
packages/lib/hooks/plugins/useVisiblePluginEditorViewIds.ts
Normal file
13
packages/lib/hooks/plugins/useVisiblePluginEditorViewIds.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { PluginStates } from '../../services/plugins/reducer';
|
||||
import getActivePluginEditorViews from '../../services/plugins/utils/getActivePluginEditorViews';
|
||||
import shim from '../../shim';
|
||||
const { useMemo } = shim.react();
|
||||
|
||||
const useVisiblePluginEditorViewIds = (plugins: PluginStates, windowId: string) => {
|
||||
return useMemo(() => {
|
||||
const visibleViews = getActivePluginEditorViews(plugins, windowId, { mustBeVisible: true });
|
||||
return visibleViews.flatMap(({ editorView }) => editorView.id);
|
||||
}, [plugins, windowId]);
|
||||
};
|
||||
|
||||
export default useVisiblePluginEditorViewIds;
|
@ -63,7 +63,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
|
||||
const commandFolderId = state.notesParentType === 'Folder' ? (options.commandFolderId || windowState.selectedFolderId) : '';
|
||||
const commandFolder: FolderEntity = commandFolderId ? BaseModel.byId(state.folders, commandFolderId) : null;
|
||||
|
||||
const { editorPlugin } = state.pluginService ? getActivePluginEditorView(state.pluginService.plugins) : { editorPlugin: null };
|
||||
const { editorPlugin } = state.pluginService ? getActivePluginEditorView(state.pluginService.plugins, windowState.windowId) : { editorPlugin: null };
|
||||
|
||||
const settings = state.settings || {};
|
||||
|
||||
|
@ -12,32 +12,81 @@ import WebviewController from './WebviewController';
|
||||
|
||||
const logger = Logger.create('EditorPluginHandler');
|
||||
|
||||
const makeNoteUpdateAction = (pluginService: PluginService, shownEditorViewIds: string[]) => {
|
||||
export interface UpdateEvent {
|
||||
noteId: string;
|
||||
newBody: string;
|
||||
}
|
||||
|
||||
interface EmitActivationCheckOptions {
|
||||
noteId: string;
|
||||
parentWindowId: string;
|
||||
}
|
||||
|
||||
interface SaveNoteEvent {
|
||||
id: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export type OnSaveNoteCallback = (updatedNote: SaveNoteEvent)=> void;
|
||||
|
||||
const makeNoteUpdateAction = (pluginService: PluginService, event: UpdateEvent, shownEditorViewIds: string[]) => {
|
||||
return async () => {
|
||||
for (const viewId of shownEditorViewIds) {
|
||||
const controller = pluginService.viewControllerByViewId(viewId) as WebviewController;
|
||||
if (controller) controller.emitUpdate();
|
||||
if (controller) {
|
||||
controller.emitUpdate({
|
||||
noteId: event.noteId,
|
||||
newBody: event.newBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default class {
|
||||
|
||||
private pluginService_: PluginService;
|
||||
private viewUpdateAsyncQueue_ = new AsyncActionQueue(100, IntervalType.Fixed);
|
||||
private lastNoteState_: UpdateEvent|null = null;
|
||||
private lastShownEditorViewIds_ = '';
|
||||
private lastEditorPluginShown_: string|null = null;
|
||||
|
||||
public constructor(pluginService: PluginService) {
|
||||
this.pluginService_ = pluginService;
|
||||
public constructor(
|
||||
private pluginService_: PluginService,
|
||||
private onSaveNote_: OnSaveNoteCallback,
|
||||
) {
|
||||
}
|
||||
|
||||
public emitUpdate(shownEditorViewIds: string[]) {
|
||||
public emitUpdate(event: UpdateEvent, shownEditorViewIds: string[]) {
|
||||
if (shownEditorViewIds.length === 0) return;
|
||||
|
||||
const isEventDifferentFrom = (other: UpdateEvent|null) => {
|
||||
if (!other) return true;
|
||||
return event.noteId !== other.noteId || event.newBody !== other.newBody;
|
||||
};
|
||||
|
||||
const shownEditorViewIdsString = shownEditorViewIds.join(',');
|
||||
const differentEditorViewsShown = shownEditorViewIdsString !== this.lastShownEditorViewIds_;
|
||||
|
||||
// lastNoteState_ often contains the last change saved by the editor. As a result,
|
||||
// if `event` matches `lastNoteState_`, the event was probably caused by the last save.
|
||||
// In this case, avoid sending an update event (which plugins often interpret as refreshing
|
||||
// the editor):
|
||||
const isDifferentFromSave = isEventDifferentFrom(this.lastNoteState_);
|
||||
|
||||
if (isDifferentFromSave || differentEditorViewsShown) {
|
||||
logger.info('emitUpdate:', shownEditorViewIds);
|
||||
this.viewUpdateAsyncQueue_.push(makeNoteUpdateAction(this.pluginService_, shownEditorViewIds));
|
||||
this.viewUpdateAsyncQueue_.push(makeNoteUpdateAction(this.pluginService_, event, shownEditorViewIds));
|
||||
|
||||
this.lastNoteState_ = { ...event };
|
||||
this.lastShownEditorViewIds_ = shownEditorViewIdsString;
|
||||
}
|
||||
}
|
||||
|
||||
public async emitActivationCheck() {
|
||||
public async emitActivationCheck({ noteId, parentWindowId }: EmitActivationCheckOptions) {
|
||||
let filterObject: EditorActivationCheckFilterObject = {
|
||||
activatedEditors: [],
|
||||
effectiveNoteId: noteId,
|
||||
windowId: parentWindowId,
|
||||
};
|
||||
filterObject = await eventManager.filterEmit('editorActivationCheck', filterObject);
|
||||
|
||||
@ -45,8 +94,34 @@ export default class {
|
||||
|
||||
for (const editor of filterObject.activatedEditors) {
|
||||
const controller = this.pluginService_.pluginById(editor.pluginId).viewController(editor.viewId) as WebviewController;
|
||||
if (controller.parentWindowId === parentWindowId) {
|
||||
controller.setActive(editor.isActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onEditorPluginShown(editorViewId: string) {
|
||||
// Don't double-register callbacks
|
||||
if (editorViewId === this.lastEditorPluginShown_) {
|
||||
return;
|
||||
}
|
||||
this.lastEditorPluginShown_ = editorViewId;
|
||||
|
||||
const controller = this.pluginService_.viewControllerByViewId(editorViewId) as WebviewController;
|
||||
controller?.onNoteSaveRequested(event => {
|
||||
this.scheduleSaveNote_(event.noteId, event.body);
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleSaveNote_(noteId: string, noteBody: string) {
|
||||
this.lastNoteState_ = {
|
||||
noteId,
|
||||
newBody: noteBody,
|
||||
};
|
||||
|
||||
return this.onSaveNote_({
|
||||
id: noteId,
|
||||
body: noteBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -180,6 +180,10 @@ export default class Plugin {
|
||||
this.viewControllers_[v.handle] = v;
|
||||
}
|
||||
|
||||
public removeViewController(v: ViewController) {
|
||||
delete this.viewControllers_[v.handle];
|
||||
}
|
||||
|
||||
public hasViewController(handle: ViewHandle) {
|
||||
return !!this.viewControllers_[handle];
|
||||
}
|
||||
@ -229,6 +233,10 @@ export default class Plugin {
|
||||
this.onUnloadListeners_.push(callback);
|
||||
}
|
||||
|
||||
public removeOnUnloadListener(callback: OnUnloadListener) {
|
||||
this.onUnloadListeners_ = this.onUnloadListeners_.filter(other => other !== callback);
|
||||
}
|
||||
|
||||
public onUnload() {
|
||||
for (const callback of this.onUnloadListeners_) {
|
||||
callback();
|
||||
|
@ -3,10 +3,9 @@ import shim from '../../shim';
|
||||
import { ButtonSpec, DialogResult, ViewHandle } from './api/types';
|
||||
const { toSystemSlashes } = require('../../path-utils');
|
||||
import PostMessageService, { MessageParticipant } from '../PostMessageService';
|
||||
import { PluginViewState } from './reducer';
|
||||
import { PluginEditorViewState, PluginViewState } from './reducer';
|
||||
import { defaultWindowId } from '../../reducer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import CommandService from '../CommandService';
|
||||
|
||||
const logger = Logger.create('WebviewController');
|
||||
|
||||
@ -49,32 +48,48 @@ function findItemByKey(layout: any, key: string): any {
|
||||
return recurseFind(layout);
|
||||
}
|
||||
|
||||
interface EditorUpdateEvent {
|
||||
noteId: string;
|
||||
newBody: string;
|
||||
}
|
||||
type EditorUpdateListener = (event: EditorUpdateEvent)=> void;
|
||||
|
||||
interface SaveNoteEvent {
|
||||
noteId: string;
|
||||
body: string;
|
||||
}
|
||||
type OnSaveNoteCallback = (saveNoteEvent: SaveNoteEvent)=> void;
|
||||
|
||||
export default class WebviewController extends ViewController {
|
||||
|
||||
private baseDir_: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
private messageListener_: Function = null;
|
||||
private updateListener_: ()=> void = null;
|
||||
private updateListener_: EditorUpdateListener|null = null;
|
||||
private closeResponse_: CloseResponse = null;
|
||||
private containerType_: ContainerType = null;
|
||||
private saveNoteListener_: OnSaveNoteCallback|null = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
|
||||
public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType, parentWindowId: string|null) {
|
||||
super(handle, pluginId, store);
|
||||
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
||||
this.containerType_ = containerType;
|
||||
|
||||
const view: PluginViewState = {
|
||||
id: this.handle,
|
||||
editorTypeId: '',
|
||||
type: this.type,
|
||||
containerType: containerType,
|
||||
html: '',
|
||||
scripts: [],
|
||||
buttons: null,
|
||||
fitToContent: true,
|
||||
// Opened is used for dialogs and mobile panels (which are shown
|
||||
// like dialogs):
|
||||
opened: containerType === ContainerType.Panel,
|
||||
buttons: null,
|
||||
fitToContent: true,
|
||||
active: false,
|
||||
parentWindowId,
|
||||
};
|
||||
|
||||
this.store.dispatch({
|
||||
@ -84,10 +99,23 @@ export default class WebviewController extends ViewController {
|
||||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.store.dispatch({
|
||||
type: 'PLUGIN_VIEW_REMOVE',
|
||||
pluginId: this.pluginId,
|
||||
viewId: this.storeView.id,
|
||||
});
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
return 'webview';
|
||||
}
|
||||
|
||||
// Returns `null` if the view can be shown in any window.
|
||||
public get parentWindowId(): string {
|
||||
return this.storeView.parentWindowId;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private setStoreProp(name: string, value: any) {
|
||||
this.store.dispatch({
|
||||
@ -127,7 +155,6 @@ export default class WebviewController extends ViewController {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public postMessage(message: any) {
|
||||
|
||||
const messageId = `plugin_${Date.now()}${Math.random()}`;
|
||||
|
||||
void PostMessageService.instance().postMessage({
|
||||
@ -146,15 +173,10 @@ export default class WebviewController extends ViewController {
|
||||
public async emitMessage(event: EmitMessageEvent) {
|
||||
if (!this.messageListener_) return;
|
||||
|
||||
if (this.containerType_ === ContainerType.Editor && !this.isActive()) {
|
||||
logger.info('emitMessage: Not emitting message because editor is disabled:', this.pluginId, this.handle);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.messageListener_(event.message);
|
||||
}
|
||||
|
||||
public emitUpdate() {
|
||||
public emitUpdate(event: EditorUpdateEvent) {
|
||||
if (!this.updateListener_) return;
|
||||
|
||||
if (this.containerType_ === ContainerType.Editor && (!this.isActive() || !this.isVisible())) {
|
||||
@ -162,7 +184,7 @@ export default class WebviewController extends ViewController {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateListener_();
|
||||
this.updateListener_(event);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@ -170,8 +192,7 @@ export default class WebviewController extends ViewController {
|
||||
this.messageListener_ = callback;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public onUpdate(callback: any) {
|
||||
public onUpdate(callback: EditorUpdateListener) {
|
||||
this.updateListener_ = callback;
|
||||
}
|
||||
|
||||
@ -271,22 +292,37 @@ export default class WebviewController extends ViewController {
|
||||
// Specific to editors
|
||||
// ---------------------------------------------
|
||||
|
||||
public setEditorTypeId(id: string) {
|
||||
this.setStoreProp('editorTypeId', id);
|
||||
}
|
||||
|
||||
public setActive(active: boolean) {
|
||||
this.setStoreProp('opened', active);
|
||||
this.setStoreProp('active', active);
|
||||
}
|
||||
|
||||
public isActive(): boolean {
|
||||
return this.storeView.opened;
|
||||
const state = this.storeView as PluginEditorViewState;
|
||||
return state.active;
|
||||
}
|
||||
|
||||
public setOpened(visible: boolean) {
|
||||
this.setStoreProp('opened', visible);
|
||||
}
|
||||
|
||||
public isVisible(): boolean {
|
||||
if (!this.storeView.opened) return false;
|
||||
const shownEditorViewIds: string[] = this.store.getState().settings['plugins.shownEditorViewIds'];
|
||||
return shownEditorViewIds.includes(this.handle);
|
||||
const state = this.storeView as PluginEditorViewState;
|
||||
return state.active && state.opened;
|
||||
}
|
||||
|
||||
public async setVisible(visible: boolean) {
|
||||
await CommandService.instance().execute('showEditorPlugin', this.handle, visible);
|
||||
public async requestSaveNote(event: SaveNoteEvent) {
|
||||
if (!this.saveNoteListener_) {
|
||||
logger.warn('Note save requested, but no save handler was registered. View ID: ', this.storeView?.id);
|
||||
return;
|
||||
}
|
||||
this.saveNoteListener_(event);
|
||||
}
|
||||
|
||||
public onNoteSaveRequested(listener: OnSaveNoteCallback) {
|
||||
this.saveNoteListener_ = listener;
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export default class JoplinViewsDialogs {
|
||||
}
|
||||
|
||||
const handle = createViewHandle(this.plugin, id);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Dialog);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Dialog, null);
|
||||
this.plugin.addViewController(controller);
|
||||
return handle;
|
||||
}
|
||||
|
@ -1,10 +1,28 @@
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
import eventManager from '../../../eventManager';
|
||||
import eventManager, { EventName, WindowCloseEvent, WindowOpenEvent } from '../../../eventManager';
|
||||
import Setting from '../../../models/Setting';
|
||||
import { defaultWindowId } from '../../../reducer';
|
||||
import Plugin from '../Plugin';
|
||||
import createViewHandle from '../utils/createViewHandle';
|
||||
import WebviewController, { ContainerType } from '../WebviewController';
|
||||
import { ActivationCheckCallback, EditorActivationCheckFilterObject, FilterHandler, ViewHandle, UpdateCallback } from './types';
|
||||
import { ActivationCheckCallback, EditorActivationCheckFilterObject, FilterHandler, ViewHandle, UpdateCallback, EditorPluginCallbacks } from './types';
|
||||
|
||||
interface SaveNoteOptions {
|
||||
/**
|
||||
* The ID of the note to save. This should match either:
|
||||
* - The ID of the note currently being edited
|
||||
* - The ID of a note that was very recently open in the editor.
|
||||
*
|
||||
* This property is present to ensure that the note editor doesn't write
|
||||
* to the wrong note just after switching notes.
|
||||
*/
|
||||
noteId: string;
|
||||
/** The note's new content. */
|
||||
body: string;
|
||||
}
|
||||
|
||||
type ActivationCheckSlice = Pick<EditorActivationCheckFilterObject, 'effectiveNoteId'|'windowId'|'activatedEditors'>;
|
||||
|
||||
/**
|
||||
* Allows creating alternative note editors. You can create a view to handle loading and saving the
|
||||
@ -49,6 +67,7 @@ export default class JoplinViewsEditors {
|
||||
private store: any;
|
||||
private plugin: Plugin;
|
||||
private activationCheckHandlers_: Record<string, FilterHandler<EditorActivationCheckFilterObject>> = {};
|
||||
private unhandledActivationCheck_: Map<string, ActivationCheckSlice> = new Map();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public constructor(plugin: Plugin, store: any) {
|
||||
@ -60,14 +79,106 @@ export default class JoplinViewsEditors {
|
||||
return this.plugin.viewController(handle) as WebviewController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new editor plugin. Joplin will call the provided callback to create new editor views
|
||||
* associated with the plugin as necessary (e.g. when a new editor is created in a new window).
|
||||
*/
|
||||
public async register(viewId: string, callbacks: EditorPluginCallbacks) {
|
||||
const initializeController = (handle: ViewHandle, windowId: string) => {
|
||||
const editorTypeId = `${this.plugin.id}-${viewId}`;
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Editor, windowId);
|
||||
controller.setEditorTypeId(editorTypeId);
|
||||
this.plugin.addViewController(controller);
|
||||
// Restore the last open/closed state for the editor
|
||||
controller.setOpened(Setting.value('plugins.shownEditorViewIds').includes(editorTypeId));
|
||||
|
||||
return () => {
|
||||
this.plugin.removeViewController(controller);
|
||||
controller.destroy();
|
||||
};
|
||||
};
|
||||
|
||||
// Register the activation check handler early to handle the case where the editorActivationCheck
|
||||
// event is fired **before** an activation check handler is registered through the API.
|
||||
const registerActivationCheckHandler = (handle: ViewHandle) => {
|
||||
const onActivationCheck: FilterHandler<EditorActivationCheckFilterObject> = async object => {
|
||||
if (this.activationCheckHandlers_[handle]) {
|
||||
return this.activationCheckHandlers_[handle](object);
|
||||
} else {
|
||||
this.unhandledActivationCheck_.set(handle, {
|
||||
...object,
|
||||
});
|
||||
return object;
|
||||
}
|
||||
};
|
||||
eventManager.filterOn('editorActivationCheck', onActivationCheck);
|
||||
const cleanup = () => {
|
||||
eventManager.filterOff('editorActivationCheck', onActivationCheck);
|
||||
this.unhandledActivationCheck_.delete(handle);
|
||||
};
|
||||
|
||||
return cleanup;
|
||||
};
|
||||
|
||||
const listenForWindowOrPluginClose = (windowId: string, onClose: ()=> void) => {
|
||||
const closeListener = (event: WindowCloseEvent|null) => {
|
||||
if (event && event.windowId !== windowId) return;
|
||||
|
||||
onClose();
|
||||
eventManager.off(EventName.WindowClose, closeListener);
|
||||
};
|
||||
eventManager.on(EventName.WindowClose, closeListener);
|
||||
|
||||
this.plugin.addOnUnloadListener(() => {
|
||||
closeListener(null);
|
||||
});
|
||||
};
|
||||
|
||||
const createEditorViewForWindow = async (windowId: string) => {
|
||||
const handle = createViewHandle(this.plugin, `${viewId}-${windowId}`);
|
||||
|
||||
const removeController = initializeController(handle, windowId);
|
||||
const removeActivationCheck = registerActivationCheckHandler(handle);
|
||||
|
||||
await callbacks.onSetup(handle);
|
||||
|
||||
// Register the activation check after calling onSetup to ensure that the editor
|
||||
// is fully set up before it can be marked as active.
|
||||
await this.onActivationCheck(handle, callbacks.onActivationCheck);
|
||||
|
||||
listenForWindowOrPluginClose(windowId, () => {
|
||||
// Save resources by closing resources associated with
|
||||
// closed windows:
|
||||
removeController();
|
||||
removeActivationCheck();
|
||||
});
|
||||
};
|
||||
|
||||
await createEditorViewForWindow(defaultWindowId);
|
||||
|
||||
const onWindowOpen = (event: WindowOpenEvent) => createEditorViewForWindow(event.windowId);
|
||||
eventManager.on(EventName.WindowOpen, onWindowOpen);
|
||||
this.plugin.addOnUnloadListener(() => {
|
||||
eventManager.off(EventName.WindowOpen, onWindowOpen);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new editor view
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public async create(id: string): Promise<ViewHandle> {
|
||||
const handle = createViewHandle(this.plugin, id);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Editor);
|
||||
this.plugin.addViewController(controller);
|
||||
return handle;
|
||||
return new Promise<ViewHandle>(resolve => {
|
||||
void this.register(id, {
|
||||
onSetup: async (handle) => {
|
||||
resolve(handle);
|
||||
},
|
||||
onActivationCheck: async () => {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,29 +203,52 @@ export default class JoplinViewsEditors {
|
||||
return this.controller(handle).onMessage(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the content of the editor, without calling `onUpdate` for editors in the same window.
|
||||
*/
|
||||
public async saveNote(handle: ViewHandle, props: SaveNoteOptions): Promise<void> {
|
||||
await this.controller(handle).requestSaveNote({
|
||||
noteId: props.noteId,
|
||||
body: props.body,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*
|
||||
* @deprecated - `onActivationCheck` should be provided when the editor is first created with
|
||||
* `editor.register`.
|
||||
*/
|
||||
public async onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise<void> {
|
||||
const handler: FilterHandler<EditorActivationCheckFilterObject> = async (object) => {
|
||||
const isActive = await callback();
|
||||
const isActive = async ({ windowId, effectiveNoteId }: ActivationCheckSlice) => {
|
||||
const isCorrectWindow = windowId === this.controller(handle).parentWindowId;
|
||||
const active = isCorrectWindow && await callback({
|
||||
handle,
|
||||
noteId: effectiveNoteId,
|
||||
});
|
||||
return active;
|
||||
};
|
||||
const handler = async (object: ActivationCheckSlice) => {
|
||||
object.activatedEditors.push({
|
||||
pluginId: this.plugin.id,
|
||||
viewId: handle,
|
||||
isActive: isActive,
|
||||
isActive: await isActive(object),
|
||||
});
|
||||
return object;
|
||||
};
|
||||
|
||||
this.activationCheckHandlers_[handle] = handler;
|
||||
|
||||
eventManager.filterOn('editorActivationCheck', this.activationCheckHandlers_[handle]);
|
||||
this.plugin.addOnUnloadListener(() => {
|
||||
eventManager.filterOff('editorActivationCheck', this.activationCheckHandlers_[handle]);
|
||||
});
|
||||
// Handle the case where the activation check was done before this onActivationCheck handler was registered.
|
||||
if (this.unhandledActivationCheck_.has(handle)) {
|
||||
const lastActivationCheckObject = this.unhandledActivationCheck_.get(handle);
|
||||
this.unhandledActivationCheck_.delete(handle);
|
||||
|
||||
this.controller(handle).setActive(await isActive(lastActivationCheckObject));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -137,7 +271,7 @@ export default class JoplinViewsEditors {
|
||||
* Tells whether the editor is active or not.
|
||||
*/
|
||||
public async isActive(handle: ViewHandle): Promise<boolean> {
|
||||
return this.controller(handle).visible;
|
||||
return this.controller(handle).isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
import { defaultWindowId } from '../../../reducer';
|
||||
import Plugin from '../Plugin';
|
||||
import createViewHandle from '../utils/createViewHandle';
|
||||
import WebviewController, { ContainerType } from '../WebviewController';
|
||||
@ -45,7 +46,7 @@ export default class JoplinViewsPanels {
|
||||
}
|
||||
|
||||
const handle = createViewHandle(this.plugin, id);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Panel);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Panel, defaultWindowId);
|
||||
this.plugin.addViewController(controller);
|
||||
return handle;
|
||||
}
|
||||
@ -130,6 +131,10 @@ export default class JoplinViewsPanels {
|
||||
return this.controller(handle).visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assuming that the current panel is an editor plugin view, returns
|
||||
* whether the editor plugin view supports editing the current note.
|
||||
*/
|
||||
public async isActive(handle: ViewHandle): Promise<boolean> {
|
||||
return this.controller(handle).isActive();
|
||||
}
|
||||
|
@ -171,6 +171,8 @@ export default class JoplinWorkspace {
|
||||
|
||||
/**
|
||||
* Gets the currently selected note. Will be `null` if no note is selected.
|
||||
*
|
||||
* On desktop, this returns the selected note in the focused window.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async selectedNote(): Promise<any> {
|
||||
|
@ -397,9 +397,40 @@ export interface Rectangle {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type ActivationCheckCallback = ()=> Promise<boolean>;
|
||||
export interface EditorUpdateEvent {
|
||||
newBody: string;
|
||||
noteId: string;
|
||||
}
|
||||
export type UpdateCallback = (event: EditorUpdateEvent)=> Promise<void>;
|
||||
|
||||
export type UpdateCallback = ()=> Promise<void>;
|
||||
|
||||
export interface ActivationCheckEvent {
|
||||
handle: ViewHandle;
|
||||
noteId: string;
|
||||
}
|
||||
export type ActivationCheckCallback = (event: ActivationCheckEvent)=> Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Required callbacks for creating an editor plugin.
|
||||
*/
|
||||
export interface EditorPluginCallbacks {
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*/
|
||||
onActivationCheck: ActivationCheckCallback;
|
||||
|
||||
/**
|
||||
* Emitted when an editor view is created. This happens, for example, when a new window containing
|
||||
* a new editor is created.
|
||||
*
|
||||
* This callback should set the editor plugin's HTML using `editors.setHtml`, add scripts to the editor
|
||||
* with `editors.addScript`, and optionally listen for external changes using `editors.onUpdate`.
|
||||
*/
|
||||
onSetup: (handle: ViewHandle)=> Promise<void>;
|
||||
}
|
||||
|
||||
export type VisibleHandler = ()=> Promise<void>;
|
||||
|
||||
@ -408,6 +439,8 @@ export interface EditContextMenuFilterObject {
|
||||
}
|
||||
|
||||
export interface EditorActivationCheckFilterObject {
|
||||
effectiveNoteId: string;
|
||||
windowId: string;
|
||||
activatedEditors: {
|
||||
pluginId: string;
|
||||
viewId: string;
|
||||
|
@ -2,23 +2,39 @@ import { Draft } from 'immer';
|
||||
import { ContainerType } from './WebviewController';
|
||||
import { ButtonSpec } from './api/types';
|
||||
|
||||
export interface PluginViewState {
|
||||
interface PluginViewStateBase {
|
||||
id: string;
|
||||
type: string;
|
||||
// Note that this property will mean different thing depending on the `containerType`. If it's a
|
||||
// dialog, it means that the dialog is opened. If it's a panel, it means it's visible/opened. If
|
||||
// it's an editor, it means the editor is currently active (but it may not be visible - see
|
||||
// JoplinViewsEditor).
|
||||
opened: boolean;
|
||||
buttons: ButtonSpec[];
|
||||
fitToContent?: boolean;
|
||||
scripts?: string[];
|
||||
html?: string;
|
||||
commandName?: string;
|
||||
location?: string;
|
||||
containerType: ContainerType;
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
export interface PluginEditorViewState extends PluginViewStateBase {
|
||||
containerType: ContainerType.Editor;
|
||||
|
||||
parentWindowId: string;
|
||||
active: boolean;
|
||||
|
||||
// A non-unique ID determined by the type of the editor. Unlike the id property,
|
||||
// this is the same for editor views of the same type opened in different windows.
|
||||
editorTypeId: string;
|
||||
}
|
||||
|
||||
interface PluginDialogViewState extends PluginViewStateBase {
|
||||
containerType: ContainerType.Dialog;
|
||||
}
|
||||
|
||||
interface PluginPanelViewState extends PluginViewStateBase {
|
||||
containerType: ContainerType.Panel;
|
||||
}
|
||||
|
||||
export type PluginViewState = PluginEditorViewState|PluginDialogViewState|PluginPanelViewState;
|
||||
|
||||
interface PluginViewStates {
|
||||
[key: string]: PluginViewState;
|
||||
}
|
||||
@ -166,6 +182,10 @@ const reducer = (draftRoot: Draft<any>, action: any) => {
|
||||
draft.plugins[action.pluginId].views[action.view.id] = { ...action.view };
|
||||
break;
|
||||
|
||||
case 'PLUGIN_VIEW_REMOVE':
|
||||
delete draft.plugins[action.pluginId].views[action.viewId];
|
||||
break;
|
||||
|
||||
case 'PLUGIN_VIEW_PROP_SET':
|
||||
|
||||
if (action.name !== 'html') {
|
||||
|
@ -1,28 +1,22 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { PluginState, PluginStates, PluginViewState } from '../reducer';
|
||||
import { ContainerType } from '../WebviewController';
|
||||
import { PluginStates } from '../reducer';
|
||||
import getActivePluginEditorViews from './getActivePluginEditorViews';
|
||||
|
||||
const logger = Logger.create('getActivePluginEditorView');
|
||||
|
||||
interface Output {
|
||||
editorPlugin: PluginState;
|
||||
editorView: PluginViewState;
|
||||
}
|
||||
export default (plugins: PluginStates, windowId: string) => {
|
||||
const allActiveViews = getActivePluginEditorViews(plugins, windowId);
|
||||
|
||||
export default (plugins: PluginStates) => {
|
||||
let output: Output = { editorPlugin: null, editorView: null };
|
||||
for (const [, pluginState] of Object.entries(plugins)) {
|
||||
for (const [, view] of Object.entries(pluginState.views)) {
|
||||
if (view.type === 'webview' && view.containerType === ContainerType.Editor && view.opened) {
|
||||
if (output.editorPlugin) {
|
||||
logger.warn(`More than one editor plugin are active for this note. Active plugin: ${output.editorPlugin.id}. Ignored plugin: ${pluginState.id}`);
|
||||
} else {
|
||||
output = { editorPlugin: pluginState, editorView: view };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allActiveViews.length === 0) {
|
||||
return { editorPlugin: null, editorView: null };
|
||||
}
|
||||
|
||||
return output;
|
||||
const result = allActiveViews[0];
|
||||
if (allActiveViews.length > 1) {
|
||||
const ignoredPluginIds = allActiveViews.slice(1).map(({ editorPlugin }) => editorPlugin.id);
|
||||
logger.warn(`More than one editor plugin are active for this note. Active plugin: ${result.editorPlugin.id}. Ignored plugins: ${ignoredPluginIds.join(',')}`);
|
||||
}
|
||||
|
||||
return allActiveViews[0];
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import { ContainerType } from '../WebviewController';
|
||||
|
||||
interface Options {
|
||||
mustBeVisible?: boolean;
|
||||
}
|
||||
|
||||
export default (plugins: PluginStates, windowId: string, { mustBeVisible = false }: Options = {}) => {
|
||||
const output = [];
|
||||
|
||||
for (const [, pluginState] of Object.entries(plugins)) {
|
||||
for (const [, view] of Object.entries(pluginState.views)) {
|
||||
if (view.type !== 'webview' || view.containerType !== ContainerType.Editor) continue;
|
||||
if (view.parentWindowId !== windowId || !view.active) continue;
|
||||
|
||||
output.push({ editorPlugin: pluginState, editorView: view });
|
||||
}
|
||||
}
|
||||
|
||||
if (mustBeVisible) {
|
||||
// Filter out views that haven't been shown:
|
||||
return output.filter(({ editorView }) => editorView.opened);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import getActivePluginEditorView from './getActivePluginEditorView';
|
||||
import getActivePluginEditorViews from './getActivePluginEditorViews';
|
||||
|
||||
export default (plugins: PluginStates, shownEditorViewIds: string[]) => {
|
||||
const { editorPlugin, editorView } = getActivePluginEditorView(plugins);
|
||||
if (editorView) {
|
||||
if (!shownEditorViewIds.includes(editorView.id)) return { editorPlugin: null, editorView: null };
|
||||
export default (plugins: PluginStates, windowId: string) => {
|
||||
const visibleViews = getActivePluginEditorViews(plugins, windowId, { mustBeVisible: true });
|
||||
if (!visibleViews.length) {
|
||||
return { editorView: null, editorPlugin: null };
|
||||
}
|
||||
return { editorPlugin, editorView };
|
||||
return visibleViews[0];
|
||||
};
|
||||
|
@ -0,0 +1,8 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import getActivePluginEditorViews from './getActivePluginEditorViews';
|
||||
|
||||
export default (state: PluginStates, windowId: string) => {
|
||||
return getActivePluginEditorViews(
|
||||
state, windowId, { mustBeVisible: true },
|
||||
).map(({ editorView }) => editorView.id);
|
||||
};
|
@ -2,7 +2,7 @@
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "../app-desktop/commands/emptyTrash.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
|
Reference in New Issue
Block a user