mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Plugins: Add support for editor plugins (#11296)
This commit is contained in:
parent
49e86d116f
commit
f091c32992
@ -292,6 +292,8 @@ packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
||||||
@ -445,6 +447,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
||||||
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
||||||
@ -453,6 +456,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.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.test.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
||||||
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditorPlugin.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
||||||
@ -1226,6 +1230,7 @@ packages/lib/services/plugins/api/JoplinPlugins.js
|
|||||||
packages/lib/services/plugins/api/JoplinSettings.js
|
packages/lib/services/plugins/api/JoplinSettings.js
|
||||||
packages/lib/services/plugins/api/JoplinViews.js
|
packages/lib/services/plugins/api/JoplinViews.js
|
||||||
packages/lib/services/plugins/api/JoplinViewsDialogs.js
|
packages/lib/services/plugins/api/JoplinViewsDialogs.js
|
||||||
|
packages/lib/services/plugins/api/JoplinViewsEditor.js
|
||||||
packages/lib/services/plugins/api/JoplinViewsMenuItems.js
|
packages/lib/services/plugins/api/JoplinViewsMenuItems.js
|
||||||
packages/lib/services/plugins/api/JoplinViewsMenus.js
|
packages/lib/services/plugins/api/JoplinViewsMenus.js
|
||||||
packages/lib/services/plugins/api/JoplinViewsNoteList.js
|
packages/lib/services/plugins/api/JoplinViewsNoteList.js
|
||||||
@ -1244,6 +1249,7 @@ packages/lib/services/plugins/testing/MockPlatformImplementation.js
|
|||||||
packages/lib/services/plugins/testing/MockPluginRunner.js
|
packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||||
packages/lib/services/plugins/utils/createViewHandle.js
|
packages/lib/services/plugins/utils/createViewHandle.js
|
||||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||||
|
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -269,6 +269,8 @@ packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
|||||||
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
|
||||||
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
|
||||||
@ -422,6 +424,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
||||||
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
||||||
@ -430,6 +433,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.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.test.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
||||||
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditorPlugin.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
||||||
@ -1203,6 +1207,7 @@ packages/lib/services/plugins/api/JoplinPlugins.js
|
|||||||
packages/lib/services/plugins/api/JoplinSettings.js
|
packages/lib/services/plugins/api/JoplinSettings.js
|
||||||
packages/lib/services/plugins/api/JoplinViews.js
|
packages/lib/services/plugins/api/JoplinViews.js
|
||||||
packages/lib/services/plugins/api/JoplinViewsDialogs.js
|
packages/lib/services/plugins/api/JoplinViewsDialogs.js
|
||||||
|
packages/lib/services/plugins/api/JoplinViewsEditor.js
|
||||||
packages/lib/services/plugins/api/JoplinViewsMenuItems.js
|
packages/lib/services/plugins/api/JoplinViewsMenuItems.js
|
||||||
packages/lib/services/plugins/api/JoplinViewsMenus.js
|
packages/lib/services/plugins/api/JoplinViewsMenus.js
|
||||||
packages/lib/services/plugins/api/JoplinViewsNoteList.js
|
packages/lib/services/plugins/api/JoplinViewsNoteList.js
|
||||||
@ -1221,6 +1226,7 @@ packages/lib/services/plugins/testing/MockPlatformImplementation.js
|
|||||||
packages/lib/services/plugins/testing/MockPluginRunner.js
|
packages/lib/services/plugins/testing/MockPluginRunner.js
|
||||||
packages/lib/services/plugins/utils/createViewHandle.js
|
packages/lib/services/plugins/utils/createViewHandle.js
|
||||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||||
|
packages/lib/services/plugins/utils/getActivePluginEditorView.js
|
||||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||||
|
@ -637,6 +637,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
|||||||
<NoteEditor
|
<NoteEditor
|
||||||
windowId={defaultWindowId}
|
windowId={defaultWindowId}
|
||||||
key={key}
|
key={key}
|
||||||
|
startupPluginsLoaded={this.props.startupPluginsLoaded}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -21,6 +21,7 @@ interface Props {
|
|||||||
newWindow: boolean;
|
newWindow: boolean;
|
||||||
windowId: string;
|
windowId: string;
|
||||||
activeWindowId: string;
|
activeWindowId: string;
|
||||||
|
startupPluginsLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyCallback = () => {};
|
const emptyCallback = () => {};
|
||||||
@ -45,6 +46,7 @@ const SecondaryWindow: React.FC<Props> = props => {
|
|||||||
<NoteEditor
|
<NoteEditor
|
||||||
windowId={props.windowId}
|
windowId={props.windowId}
|
||||||
onTitleChange={onNoteTitleChange}
|
onTitleChange={onNoteTitleChange}
|
||||||
|
startupPluginsLoaded={props.startupPluginsLoaded}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
@ -121,5 +123,6 @@ export default connect((state: AppState, ownProps: ConnectProps) => {
|
|||||||
codeView: windowState?.editorCodeView ?? state.settings['editor.codeView'],
|
codeView: windowState?.editorCodeView ?? state.settings['editor.codeView'],
|
||||||
legacyMarkdown: state.settings['editor.legacyMarkdown'],
|
legacyMarkdown: state.settings['editor.legacyMarkdown'],
|
||||||
activeWindowId: stateUtils.activeWindowId(state),
|
activeWindowId: stateUtils.activeWindowId(state),
|
||||||
|
startupPluginsLoaded: state.startupPluginsLoaded,
|
||||||
};
|
};
|
||||||
})(SecondaryWindow);
|
})(SecondaryWindow);
|
||||||
|
@ -3,11 +3,10 @@ import { ContextMenuParams, Event } from 'electron';
|
|||||||
import { useEffect, RefObject } from 'react';
|
import { useEffect, RefObject } from 'react';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
import { EditContextMenuFilterObject, MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||||
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
|
|
||||||
import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||||
import eventManager from '@joplin/lib/eventManager';
|
import eventManager from '@joplin/lib/eventManager';
|
||||||
import bridge from '../../../../../services/bridge';
|
import bridge from '../../../../../services/bridge';
|
||||||
|
@ -32,7 +32,6 @@ import { itemIsReadOnly } from '@joplin/lib/models/utils/readOnly';
|
|||||||
const { themeStyle } = require('@joplin/lib/theme');
|
const { themeStyle } = require('@joplin/lib/theme');
|
||||||
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
||||||
import NoteSearchBar from '../NoteSearchBar';
|
import NoteSearchBar from '../NoteSearchBar';
|
||||||
import { reg } from '@joplin/lib/registry';
|
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import NoteRevisionViewer from '../NoteRevisionViewer';
|
import NoteRevisionViewer from '../NoteRevisionViewer';
|
||||||
@ -51,10 +50,20 @@ import { MarkupLanguage } from '@joplin/renderer';
|
|||||||
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions';
|
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions';
|
||||||
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks';
|
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks';
|
||||||
import WarningBanner from './WarningBanner/WarningBanner';
|
import WarningBanner from './WarningBanner/WarningBanner';
|
||||||
|
import UserWebview from '../../services/plugins/UserWebview';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import usePluginEditorView from './utils/usePluginEditorView';
|
||||||
import { stateUtils } from '@joplin/lib/reducer';
|
import { stateUtils } from '@joplin/lib/reducer';
|
||||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||||
|
import { EditorActivationCheckFilterObject } from '@joplin/lib/services/plugins/api/types';
|
||||||
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
||||||
|
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
|
||||||
|
|
||||||
const debounce = require('debounce');
|
const debounce = require('debounce');
|
||||||
|
|
||||||
|
const logger = Logger.create('NoteEditor');
|
||||||
|
|
||||||
const commands = [
|
const commands = [
|
||||||
require('./commands/showRevisions'),
|
require('./commands/showRevisions'),
|
||||||
];
|
];
|
||||||
@ -64,6 +73,15 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
|||||||
const onDragOver: React.DragEventHandler = event => event.preventDefault();
|
const onDragOver: React.DragEventHandler = event => event.preventDefault();
|
||||||
let editorIdCounter = 0;
|
let editorIdCounter = 0;
|
||||||
|
|
||||||
|
const makeNoteUpdateAction = (shownEditorViewIds: string[]) => {
|
||||||
|
return async () => {
|
||||||
|
for (const viewId of shownEditorViewIds) {
|
||||||
|
const controller = PluginService.instance().viewControllerByViewId(viewId) as WebviewController;
|
||||||
|
if (controller) controller.emitUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
function NoteEditorContent(props: NoteEditorProps) {
|
function NoteEditorContent(props: NoteEditorProps) {
|
||||||
const [showRevisions, setShowRevisions] = useState(false);
|
const [showRevisions, setShowRevisions] = useState(false);
|
||||||
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
|
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
|
||||||
@ -73,6 +91,9 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
const titleInputRef = useRef<HTMLInputElement>();
|
const titleInputRef = useRef<HTMLInputElement>();
|
||||||
const isMountedRef = useRef(true);
|
const isMountedRef = useRef(true);
|
||||||
const noteSearchBarRef = useRef(null);
|
const noteSearchBarRef = useRef(null);
|
||||||
|
const viewUpdateAsyncQueue_ = useRef<AsyncActionQueue>(new AsyncActionQueue(100, IntervalType.Fixed));
|
||||||
|
|
||||||
|
const shownEditorViewIds = props['plugins.shownEditorViewIds'];
|
||||||
|
|
||||||
// Should be constant and unique to this instance of the editor.
|
// Should be constant and unique to this instance of the editor.
|
||||||
const editorId = useMemo(() => {
|
const editorId = useMemo(() => {
|
||||||
@ -94,6 +115,29 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
|
|
||||||
const effectiveNoteId = useEffectiveNoteId(props);
|
const effectiveNoteId = useEffectiveNoteId(props);
|
||||||
|
|
||||||
|
useAsyncEffect(async (event) => {
|
||||||
|
if (!props.startupPluginsLoaded) return;
|
||||||
|
|
||||||
|
let filterObject: EditorActivationCheckFilterObject = {
|
||||||
|
activatedEditors: [],
|
||||||
|
};
|
||||||
|
filterObject = await eventManager.filterEmit('editorActivationCheck', filterObject);
|
||||||
|
if (event.cancelled) return;
|
||||||
|
|
||||||
|
for (const editor of filterObject.activatedEditors) {
|
||||||
|
const controller = PluginService.instance().pluginById(editor.pluginId).viewController(editor.viewId) as WebviewController;
|
||||||
|
controller.setActive(editor.isActive);
|
||||||
|
}
|
||||||
|
}, [effectiveNoteId, props.startupPluginsLoaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.startupPluginsLoaded) return;
|
||||||
|
viewUpdateAsyncQueue_.current.push(makeNoteUpdateAction(shownEditorViewIds));
|
||||||
|
}, [effectiveNoteId, shownEditorViewIds, props.startupPluginsLoaded]);
|
||||||
|
|
||||||
|
const { editorPlugin, editorView } = usePluginEditorView(props.plugins, shownEditorViewIds);
|
||||||
|
const builtInEditorVisible = !editorPlugin;
|
||||||
|
|
||||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||||
noteId: effectiveNoteId,
|
noteId: effectiveNoteId,
|
||||||
isProvisional: props.isProvisional,
|
isProvisional: props.isProvisional,
|
||||||
@ -101,6 +145,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
editorRef: editorRef,
|
editorRef: editorRef,
|
||||||
onBeforeLoad: formNote_beforeLoad,
|
onBeforeLoad: formNote_beforeLoad,
|
||||||
onAfterLoad: formNote_afterLoad,
|
onAfterLoad: formNote_afterLoad,
|
||||||
|
builtInEditorVisible,
|
||||||
editorId,
|
editorId,
|
||||||
});
|
});
|
||||||
setFormNoteRef.current = setFormNote;
|
setFormNoteRef.current = setFormNote;
|
||||||
@ -186,7 +231,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
// trigger onChange events, for example the textarea might be cleared.
|
// trigger onChange events, for example the textarea might be cleared.
|
||||||
// We need to ignore these events, otherwise the note is going to be saved
|
// We need to ignore these events, otherwise the note is going to be saved
|
||||||
// with an invalid body.
|
// with an invalid body.
|
||||||
reg.logger().debug('Skipping change event because the component is unmounted');
|
logger.debug('Skipping change event because the component is unmounted');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,16 +501,18 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
|
|
||||||
let editor = null;
|
let editor = null;
|
||||||
|
|
||||||
if (props.bodyEditor === 'TinyMCE') {
|
if (builtInEditorVisible) {
|
||||||
editor = <TinyMCE {...editorProps}/>;
|
if (props.bodyEditor === 'TinyMCE') {
|
||||||
} else if (props.bodyEditor === 'PlainText') {
|
editor = <TinyMCE {...editorProps}/>;
|
||||||
editor = <PlainEditor {...editorProps}/>;
|
} else if (props.bodyEditor === 'PlainText') {
|
||||||
} else if (props.bodyEditor === 'CodeMirror5') {
|
editor = <PlainEditor {...editorProps}/>;
|
||||||
editor = <CodeMirror5 {...editorProps}/>;
|
} else if (props.bodyEditor === 'CodeMirror5') {
|
||||||
} else if (props.bodyEditor === 'CodeMirror6') {
|
editor = <CodeMirror5 {...editorProps}/>;
|
||||||
editor = <CodeMirror6 {...editorProps}/>;
|
} else if (props.bodyEditor === 'CodeMirror6') {
|
||||||
} else {
|
editor = <CodeMirror6 {...editorProps}/>;
|
||||||
throw new Error(`Invalid editor: ${props.bodyEditor}`);
|
} else {
|
||||||
|
throw new Error(`Invalid editor: ${props.bodyEditor}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteRevisionViewer_onBack = useCallback(() => {
|
const noteRevisionViewer_onBack = useCallback(() => {
|
||||||
@ -592,6 +639,23 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderPluginEditor = () => {
|
||||||
|
if (!editorPlugin) return null;
|
||||||
|
|
||||||
|
const html = props.pluginHtmlContents[editorPlugin.id]?.[editorView.id] ?? '';
|
||||||
|
|
||||||
|
return <UserWebview
|
||||||
|
key={editorView.id}
|
||||||
|
viewId={editorView.id}
|
||||||
|
themeId={props.themeId}
|
||||||
|
html={html}
|
||||||
|
scripts={editorView.scripts}
|
||||||
|
pluginId={editorPlugin.id}
|
||||||
|
borderBottom={true}
|
||||||
|
fitToContent={false}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
if (formNote.encryption_applied || !formNote.id || !effectiveNoteId) {
|
if (formNote.encryption_applied || !formNote.id || !effectiveNoteId) {
|
||||||
return renderNoNotes(styles.root);
|
return renderNoNotes(styles.root);
|
||||||
}
|
}
|
||||||
@ -616,6 +680,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
{renderSearchInfo()}
|
{renderSearchInfo()}
|
||||||
<div style={{ display: 'flex', flex: 1, paddingLeft: theme.editorPaddingLeft, maxHeight: '100%', minHeight: '0' }}>
|
<div style={{ display: 'flex', flex: 1, paddingLeft: theme.editorPaddingLeft, maxHeight: '100%', minHeight: '0' }}>
|
||||||
{editor}
|
{editor}
|
||||||
|
{renderPluginEditor()}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
{renderSearchBar()}
|
{renderSearchBar()}
|
||||||
@ -667,6 +732,8 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
|||||||
watchedResources: state.watchedResources,
|
watchedResources: state.watchedResources,
|
||||||
highlightedWords: state.highlightedWords,
|
highlightedWords: state.highlightedWords,
|
||||||
plugins: state.pluginService.plugins,
|
plugins: state.pluginService.plugins,
|
||||||
|
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||||
|
'plugins.shownEditorViewIds': state.settings['plugins.shownEditorViewIds'] || [],
|
||||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||||
'historyBackward',
|
'historyBackward',
|
||||||
'historyForward',
|
'historyForward',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
import { MarkupLanguage } from '@joplin/renderer';
|
import { MarkupLanguage } from '@joplin/renderer';
|
||||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
|
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
@ -55,9 +55,11 @@ export interface NoteEditorProps {
|
|||||||
shareCacheSetting: string;
|
shareCacheSetting: string;
|
||||||
syncUserId: string;
|
syncUserId: string;
|
||||||
searchResults: ProcessResultsRow[];
|
searchResults: ProcessResultsRow[];
|
||||||
|
pluginHtmlContents: PluginHtmlContents;
|
||||||
|
'plugins.shownEditorViewIds': string[];
|
||||||
onTitleChange?: (title: string)=> void;
|
onTitleChange?: (title: string)=> void;
|
||||||
bodyEditor: string;
|
bodyEditor: string;
|
||||||
|
startupPluginsLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NoteBodyEditorRef {
|
export interface NoteBodyEditorRef {
|
||||||
|
@ -15,6 +15,7 @@ const defaultFormNoteProps: HookDependencies = {
|
|||||||
onBeforeLoad: () => { },
|
onBeforeLoad: () => { },
|
||||||
onAfterLoad: () => { },
|
onAfterLoad: () => { },
|
||||||
editorId: 'editor',
|
editorId: 'editor',
|
||||||
|
builtInEditorVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('useFormNote', () => {
|
describe('useFormNote', () => {
|
||||||
|
@ -31,6 +31,7 @@ export interface HookDependencies {
|
|||||||
editorRef: any;
|
editorRef: any;
|
||||||
onBeforeLoad(event: OnLoadEvent): void;
|
onBeforeLoad(event: OnLoadEvent): void;
|
||||||
onAfterLoad(event: OnLoadEvent): void;
|
onAfterLoad(event: OnLoadEvent): void;
|
||||||
|
builtInEditorVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MapFormNoteCallback = (previousFormNote: FormNote)=> FormNote;
|
type MapFormNoteCallback = (previousFormNote: FormNote)=> FormNote;
|
||||||
@ -67,10 +68,11 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InitNoteStateCallback = (note: NoteEntity, isNew: boolean)=> Promise<FormNote>;
|
type InitNoteStateCallback = (note: NoteEntity, isNew: boolean)=> Promise<FormNote>;
|
||||||
const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId: string, noteId: string, initNoteState: InitNoteStateCallback) => {
|
const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId: string, noteId: string, initNoteState: InitNoteStateCallback, builtInEditorVisible: boolean) => {
|
||||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||||
// a new refresh.
|
// a new refresh.
|
||||||
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||||
|
const prevBuiltInEditorVisible = usePrevious<boolean>(builtInEditorVisible);
|
||||||
|
|
||||||
useQueuedAsyncEffect(async (event) => {
|
useQueuedAsyncEffect(async (event) => {
|
||||||
if (formNoteRefreshScheduled <= 0) return;
|
if (formNoteRefreshScheduled <= 0) return;
|
||||||
@ -107,6 +109,15 @@ const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId:
|
|||||||
setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1);
|
setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1);
|
||||||
}, [formNoteRefreshScheduled]);
|
}, [formNoteRefreshScheduled]);
|
||||||
|
|
||||||
|
// When switching from the plugin editor to the built-in editor, we refresh the note since the
|
||||||
|
// plugin may have modified it via the data API.
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevBuiltInEditorVisible !== builtInEditorVisible && builtInEditorVisible) {
|
||||||
|
refreshFormNote();
|
||||||
|
}
|
||||||
|
}, [builtInEditorVisible, prevBuiltInEditorVisible, refreshFormNote]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!noteId) return ()=>{};
|
if (!noteId) return ()=>{};
|
||||||
|
|
||||||
@ -134,7 +145,9 @@ const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId:
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function useFormNote(dependencies: HookDependencies) {
|
export default function useFormNote(dependencies: HookDependencies) {
|
||||||
const { noteId, editorId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad } = dependencies;
|
const {
|
||||||
|
noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad, builtInEditorVisible, editorId,
|
||||||
|
} = dependencies;
|
||||||
|
|
||||||
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
||||||
const [isNewNote, setIsNewNote] = useState(false);
|
const [isNewNote, setIsNewNote] = useState(false);
|
||||||
@ -195,7 +208,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
|||||||
return newFormNote;
|
return newFormNote;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useRefreshFormNoteOnChange(formNoteRef, editorId, noteId, initNoteState);
|
useRefreshFormNoteOnChange(formNoteRef, editorId, noteId, initNoteState, builtInEditorVisible);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
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 { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||||
|
|
||||||
|
const sampleView = (): PluginViewState => {
|
||||||
|
return {
|
||||||
|
buttons: [],
|
||||||
|
containerType: ContainerType.Editor,
|
||||||
|
id: 'view-1',
|
||||||
|
opened: true,
|
||||||
|
type: 'webview',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('usePluginEditorView', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupDatabaseAndSynchronizer(1);
|
||||||
|
await switchClient(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the plugin editor view if is opened', async () => {
|
||||||
|
const pluginStates: PluginStates = {
|
||||||
|
'0': {
|
||||||
|
contentScripts: {},
|
||||||
|
id: '1',
|
||||||
|
views: {
|
||||||
|
'view-0': {
|
||||||
|
...sampleView(),
|
||||||
|
id: 'view-0',
|
||||||
|
containerType: ContainerType.Panel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'1': {
|
||||||
|
contentScripts: {},
|
||||||
|
id: '1',
|
||||||
|
views: {
|
||||||
|
'view-1': sampleView(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const test = renderHook(() => usePluginEditorView(pluginStates, ['view-1']));
|
||||||
|
expect(test.result.current.editorPlugin.id).toBe('1');
|
||||||
|
expect(test.result.current.editorView.id).toBe('view-1');
|
||||||
|
test.unmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
pluginStates['1'].views['view-1'].opened = false;
|
||||||
|
const test = renderHook(() => usePluginEditorView(pluginStates, ['view-1']));
|
||||||
|
expect(test.result.current.editorPlugin).toBeFalsy();
|
||||||
|
test.unmount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a plugin editor view even if multiple editors are conflicting', async () => {
|
||||||
|
const pluginStates: PluginStates = {
|
||||||
|
'1': {
|
||||||
|
contentScripts: {},
|
||||||
|
id: '1',
|
||||||
|
views: {
|
||||||
|
'view-1': sampleView(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'2': {
|
||||||
|
contentScripts: {},
|
||||||
|
id: '2',
|
||||||
|
views: {
|
||||||
|
'view-2': {
|
||||||
|
...sampleView(),
|
||||||
|
id: 'view-2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const test = renderHook(() => usePluginEditorView(pluginStates, ['view-1']));
|
||||||
|
expect(test.result.current.editorPlugin.id).toBe('1');
|
||||||
|
expect(test.result.current.editorView.id).toBe('view-1');
|
||||||
|
test.unmount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||||
|
|
||||||
|
// 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[]) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
const { editorPlugin, editorView } = getActivePluginEditorView(plugins);
|
||||||
|
if (editorView) {
|
||||||
|
if (!shownEditorViewIds.includes(editorView.id)) return { editorPlugin: null, editorView: null };
|
||||||
|
}
|
||||||
|
return { editorPlugin, editorView };
|
||||||
|
}, [plugins, shownEditorViewIds]);
|
||||||
|
};
|
@ -7,6 +7,7 @@ import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseC
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { buildStyle } from '@joplin/lib/theme';
|
import { buildStyle } from '@joplin/lib/theme';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
|
|
||||||
interface NoteToolbarProps {
|
interface NoteToolbarProps {
|
||||||
@ -49,13 +50,20 @@ interface ConnectProps {
|
|||||||
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||||
const whenClauseContext = stateToWhenClauseContext(state, { windowId: ownProps.windowId });
|
const whenClauseContext = stateToWhenClauseContext(state, { windowId: ownProps.windowId });
|
||||||
|
|
||||||
|
const { editorPlugin } = getActivePluginEditorView(state.pluginService.plugins);
|
||||||
|
|
||||||
|
const commands = [
|
||||||
|
'showSpellCheckerMenu',
|
||||||
|
'editAlarm',
|
||||||
|
'toggleVisiblePanes',
|
||||||
|
'showNoteProperties',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (editorPlugin) commands.push('toggleEditorPlugin');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(commands
|
||||||
'showSpellCheckerMenu',
|
.concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'noteToolbar')), whenClauseContext),
|
||||||
'editAlarm',
|
|
||||||
'toggleVisiblePanes',
|
|
||||||
'showNoteProperties',
|
|
||||||
].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'noteToolbar')), whenClauseContext),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import * as restoreNote from './restoreNote';
|
|||||||
import * as revealResourceFile from './revealResourceFile';
|
import * as revealResourceFile from './revealResourceFile';
|
||||||
import * as search from './search';
|
import * as search from './search';
|
||||||
import * as setTags from './setTags';
|
import * as setTags from './setTags';
|
||||||
|
import * as showEditorPlugin from './showEditorPlugin';
|
||||||
import * as showModalMessage from './showModalMessage';
|
import * as showModalMessage from './showModalMessage';
|
||||||
import * as showNoteContentProperties from './showNoteContentProperties';
|
import * as showNoteContentProperties from './showNoteContentProperties';
|
||||||
import * as showNoteProperties from './showNoteProperties';
|
import * as showNoteProperties from './showNoteProperties';
|
||||||
@ -35,6 +36,7 @@ import * as showPrompt from './showPrompt';
|
|||||||
import * as showShareFolderDialog from './showShareFolderDialog';
|
import * as showShareFolderDialog from './showShareFolderDialog';
|
||||||
import * as showShareNoteDialog from './showShareNoteDialog';
|
import * as showShareNoteDialog from './showShareNoteDialog';
|
||||||
import * as showSpellCheckerMenu from './showSpellCheckerMenu';
|
import * as showSpellCheckerMenu from './showSpellCheckerMenu';
|
||||||
|
import * as toggleEditorPlugin from './toggleEditorPlugin';
|
||||||
import * as toggleEditors from './toggleEditors';
|
import * as toggleEditors from './toggleEditors';
|
||||||
import * as toggleLayoutMoveMode from './toggleLayoutMoveMode';
|
import * as toggleLayoutMoveMode from './toggleLayoutMoveMode';
|
||||||
import * as toggleMenuBar from './toggleMenuBar';
|
import * as toggleMenuBar from './toggleMenuBar';
|
||||||
@ -76,6 +78,7 @@ const index: any[] = [
|
|||||||
revealResourceFile,
|
revealResourceFile,
|
||||||
search,
|
search,
|
||||||
setTags,
|
setTags,
|
||||||
|
showEditorPlugin,
|
||||||
showModalMessage,
|
showModalMessage,
|
||||||
showNoteContentProperties,
|
showNoteContentProperties,
|
||||||
showNoteProperties,
|
showNoteProperties,
|
||||||
@ -83,6 +86,7 @@ const index: any[] = [
|
|||||||
showShareFolderDialog,
|
showShareFolderDialog,
|
||||||
showShareNoteDialog,
|
showShareNoteDialog,
|
||||||
showSpellCheckerMenu,
|
showSpellCheckerMenu,
|
||||||
|
toggleEditorPlugin,
|
||||||
toggleEditors,
|
toggleEditors,
|
||||||
toggleLayoutMoveMode,
|
toggleLayoutMoveMode,
|
||||||
toggleMenuBar,
|
toggleMenuBar,
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
|
||||||
|
const logger = Logger.create('showEditorPlugin');
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'showEditorPlugin',
|
||||||
|
label: () => 'Show editor plugin',
|
||||||
|
iconName: 'fas fa-eye',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (context: CommandContext, editorViewId = '', show = true) => {
|
||||||
|
logger.info('View:', editorViewId, 'Show:', show);
|
||||||
|
|
||||||
|
const shownEditorViewIds = Setting.value('plugins.shownEditorViewIds');
|
||||||
|
|
||||||
|
if (!editorViewId) {
|
||||||
|
const { editorPlugin, editorView } = getActivePluginEditorView(context.state.pluginService.plugins);
|
||||||
|
|
||||||
|
if (!editorPlugin) {
|
||||||
|
logger.warn('No editor plugin to toggle to');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editorViewId = editorView.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = shownEditorViewIds.indexOf(editorViewId);
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
if (idx >= 0) {
|
||||||
|
logger.info(`Editor is already visible: ${editorViewId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shownEditorViewIds.push(editorViewId);
|
||||||
|
} else {
|
||||||
|
if (idx < 0) {
|
||||||
|
logger.info(`Editor is already hidden: ${editorViewId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shownEditorViewIds.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
|
||||||
|
const logger = Logger.create('toggleEditorPlugin');
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'toggleEditorPlugin',
|
||||||
|
label: () => _('Toggle editor plugin'),
|
||||||
|
iconName: 'fas fa-eye',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (context: CommandContext) => {
|
||||||
|
const shownEditorViewIds = Setting.value('plugins.shownEditorViewIds');
|
||||||
|
const { editorPlugin, editorView } = getActivePluginEditorView(context.state.pluginService.plugins);
|
||||||
|
|
||||||
|
if (!editorPlugin) {
|
||||||
|
logger.warn('No editor plugin to toggle to');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = shownEditorViewIds.indexOf(editorView.id);
|
||||||
|
|
||||||
|
if (idx < 0) {
|
||||||
|
shownEditorViewIds.push(editorView.id);
|
||||||
|
} else {
|
||||||
|
shownEditorViewIds.splice(idx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
export default function usePrevious(value: any, initialValue: any = null): any {
|
export default function usePrevious<T>(value: T, initialValue: T = null): T {
|
||||||
const ref = useRef(initialValue);
|
const ref = useRef(initialValue);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ref.current = value;
|
ref.current = value;
|
||||||
|
@ -24,6 +24,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
> .toolbar-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
&.-has-title {
|
&.-has-title {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
|
@ -137,7 +137,13 @@ export class EventManager {
|
|||||||
// deep equality check to see if it's been changed. Normally the
|
// deep equality check to see if it's been changed. Normally the
|
||||||
// filter objects should be relatively small so there shouldn't be
|
// filter objects should be relatively small so there shouldn't be
|
||||||
// much of a performance hit.
|
// much of a performance hit.
|
||||||
const newOutput = await listener(output);
|
let newOutput = null;
|
||||||
|
try {
|
||||||
|
newOutput = await listener(output);
|
||||||
|
} catch (error) {
|
||||||
|
error.message = `Error in listener when calling: ${filterName}: ${error.message}`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// Plugin didn't return anything - so we leave the object as it is.
|
// Plugin didn't return anything - so we leave the object as it is.
|
||||||
if (newOutput === undefined) continue;
|
if (newOutput === undefined) continue;
|
||||||
|
@ -926,6 +926,12 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
|||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'plugins.shownEditorViewIds': {
|
||||||
|
value: [] as string[],
|
||||||
|
type: SettingItemType.Array,
|
||||||
|
public: false,
|
||||||
|
},
|
||||||
|
|
||||||
// Deprecated - use markdown.plugin.*
|
// Deprecated - use markdown.plugin.*
|
||||||
'markdown.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
'markdown.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
||||||
'markdown.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
'markdown.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
||||||
|
@ -180,6 +180,10 @@ export default class Plugin {
|
|||||||
this.viewControllers_[v.handle] = v;
|
this.viewControllers_[v.handle] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasViewController(handle: ViewHandle) {
|
||||||
|
return !!this.viewControllers_[handle];
|
||||||
|
}
|
||||||
|
|
||||||
public viewController(handle: ViewHandle): ViewController {
|
public viewController(handle: ViewHandle): ViewController {
|
||||||
if (!this.viewControllers_[handle]) throw new Error(`View not found: ${handle}`);
|
if (!this.viewControllers_[handle]) throw new Error(`View not found: ${handle}`);
|
||||||
return this.viewControllers_[handle];
|
return this.viewControllers_[handle];
|
||||||
|
@ -14,6 +14,7 @@ import isCompatible from './utils/isCompatible';
|
|||||||
import { AppType } from './api/types';
|
import { AppType } from './api/types';
|
||||||
import minVersionForPlatform from './utils/isCompatible/minVersionForPlatform';
|
import minVersionForPlatform from './utils/isCompatible/minVersionForPlatform';
|
||||||
import { _ } from '../../locale';
|
import { _ } from '../../locale';
|
||||||
|
import ViewController from './ViewController';
|
||||||
const uslug = require('@joplin/fork-uslug');
|
const uslug = require('@joplin/fork-uslug');
|
||||||
|
|
||||||
const logger = Logger.create('PluginService');
|
const logger = Logger.create('PluginService');
|
||||||
@ -202,6 +203,13 @@ export default class PluginService extends BaseService {
|
|||||||
return this.plugins_[id];
|
return this.plugins_[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public viewControllerByViewId(id: string): ViewController|null {
|
||||||
|
for (const [, plugin] of Object.entries(this.plugins_)) {
|
||||||
|
if (plugin.hasViewController(id)) return plugin.viewController(id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public unserializePluginSettings(settings: SerializedPluginSettings): PluginSettings {
|
public unserializePluginSettings(settings: SerializedPluginSettings): PluginSettings {
|
||||||
const output = { ...settings };
|
const output = { ...settings };
|
||||||
|
|
||||||
|
@ -5,10 +5,15 @@ const { toSystemSlashes } = require('../../path-utils');
|
|||||||
import PostMessageService, { MessageParticipant } from '../PostMessageService';
|
import PostMessageService, { MessageParticipant } from '../PostMessageService';
|
||||||
import { PluginViewState } from './reducer';
|
import { PluginViewState } from './reducer';
|
||||||
import { defaultWindowId } from '../../reducer';
|
import { defaultWindowId } from '../../reducer';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import CommandService from '../CommandService';
|
||||||
|
|
||||||
|
const logger = Logger.create('WebviewController');
|
||||||
|
|
||||||
export enum ContainerType {
|
export enum ContainerType {
|
||||||
Panel = 'panel',
|
Panel = 'panel',
|
||||||
Dialog = 'dialog',
|
Dialog = 'dialog',
|
||||||
|
Editor = 'editor',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
@ -49,12 +54,15 @@ export default class WebviewController extends ViewController {
|
|||||||
private baseDir_: string;
|
private baseDir_: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
private messageListener_: Function = null;
|
private messageListener_: Function = null;
|
||||||
|
private updateListener_: ()=> void = null;
|
||||||
private closeResponse_: CloseResponse = null;
|
private closeResponse_: CloseResponse = null;
|
||||||
|
private containerType_: ContainerType = null;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// 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) {
|
||||||
super(handle, pluginId, store);
|
super(handle, pluginId, store);
|
||||||
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
||||||
|
this.containerType_ = containerType;
|
||||||
|
|
||||||
const view: PluginViewState = {
|
const view: PluginViewState = {
|
||||||
id: this.handle,
|
id: this.handle,
|
||||||
@ -135,18 +143,38 @@ export default class WebviewController extends ViewController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
public async emitMessage(event: EmitMessageEvent) {
|
||||||
public async emitMessage(event: EmitMessageEvent): Promise<any> {
|
|
||||||
|
|
||||||
if (!this.messageListener_) return;
|
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);
|
return this.messageListener_(event.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public emitUpdate() {
|
||||||
|
if (!this.updateListener_) return;
|
||||||
|
|
||||||
|
if (this.containerType_ === ContainerType.Editor && (!this.isActive() || !this.isVisible())) {
|
||||||
|
logger.info('emitMessage: Not emitting update because editor is disabled or hidden:', this.pluginId, this.handle, this.isActive(), this.isVisible());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateListener_();
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
public onMessage(callback: any) {
|
public onMessage(callback: any) {
|
||||||
this.messageListener_ = callback;
|
this.messageListener_ = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
public onUpdate(callback: any) {
|
||||||
|
this.updateListener_ = callback;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// Specific to panels
|
// Specific to panels
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
@ -238,4 +266,27 @@ export default class WebviewController extends ViewController {
|
|||||||
public set fitToContent(fitToContent: boolean) {
|
public set fitToContent(fitToContent: boolean) {
|
||||||
this.setStoreProp('fitToContent', fitToContent);
|
this.setStoreProp('fitToContent', fitToContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------
|
||||||
|
// Specific to editors
|
||||||
|
// ---------------------------------------------
|
||||||
|
|
||||||
|
public setActive(active: boolean) {
|
||||||
|
this.setStoreProp('opened', active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActive(): boolean {
|
||||||
|
return this.storeView.opened;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isVisible(): Promise<boolean> {
|
||||||
|
if (!this.storeView.opened) return false;
|
||||||
|
const shownEditorViewIds: string[] = this.store.getState().settings['plugins.shownEditorViewIds'];
|
||||||
|
return shownEditorViewIds.includes(this.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setVisible(visible: boolean) {
|
||||||
|
await CommandService.instance().execute('showEditorPlugin', this.handle, visible);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import JoplinViewsMenus from './JoplinViewsMenus';
|
|||||||
import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons';
|
import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons';
|
||||||
import JoplinViewsPanels from './JoplinViewsPanels';
|
import JoplinViewsPanels from './JoplinViewsPanels';
|
||||||
import JoplinViewsNoteList from './JoplinViewsNoteList';
|
import JoplinViewsNoteList from './JoplinViewsNoteList';
|
||||||
|
import JoplinViewsEditors from './JoplinViewsEditor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This namespace provides access to view-related services.
|
* This namespace provides access to view-related services.
|
||||||
@ -25,6 +26,7 @@ export default class JoplinViews {
|
|||||||
private menus_: JoplinViewsMenus = null;
|
private menus_: JoplinViewsMenus = null;
|
||||||
private toolbarButtons_: JoplinViewsToolbarButtons = null;
|
private toolbarButtons_: JoplinViewsToolbarButtons = null;
|
||||||
private dialogs_: JoplinViewsDialogs = null;
|
private dialogs_: JoplinViewsDialogs = null;
|
||||||
|
private editors_: JoplinViewsEditors = null;
|
||||||
private noteList_: JoplinViewsNoteList = null;
|
private noteList_: JoplinViewsNoteList = null;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
private implementation_: any = null;
|
private implementation_: any = null;
|
||||||
@ -46,6 +48,11 @@ export default class JoplinViews {
|
|||||||
return this.panels_;
|
return this.panels_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get editors() {
|
||||||
|
if (!this.editors_) this.editors_ = new JoplinViewsEditors(this.plugin, this.store);
|
||||||
|
return this.editors_;
|
||||||
|
}
|
||||||
|
|
||||||
public get menuItems() {
|
public get menuItems() {
|
||||||
if (!this.menuItems_) this.menuItems_ = new JoplinViewsMenuItems(this.plugin, this.store);
|
if (!this.menuItems_) this.menuItems_ = new JoplinViewsMenuItems(this.plugin, this.store);
|
||||||
return this.menuItems_;
|
return this.menuItems_;
|
||||||
|
152
packages/lib/services/plugins/api/JoplinViewsEditor.ts
Normal file
152
packages/lib/services/plugins/api/JoplinViewsEditor.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/* eslint-disable multiline-comment-style */
|
||||||
|
|
||||||
|
import eventManager from '../../../eventManager';
|
||||||
|
import Plugin from '../Plugin';
|
||||||
|
import createViewHandle from '../utils/createViewHandle';
|
||||||
|
import WebviewController, { ContainerType } from '../WebviewController';
|
||||||
|
import { ActivationCheckCallback, EditorActivationCheckFilterObject, FilterHandler, ViewHandle, UpdateCallback } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows creating alternative note editors. You can create a view to handle loading and saving the
|
||||||
|
* note, and do your own rendering.
|
||||||
|
*
|
||||||
|
* Although it may be used to implement an alternative text editor, the more common use case may be
|
||||||
|
* to render the note in a different, graphical way - for example displaying a graph, and
|
||||||
|
* saving/loading the graph data in the associated note. In that case, you would detect whether the
|
||||||
|
* current note contains graph data and, in this case, you'd display your viewer.
|
||||||
|
*
|
||||||
|
* Terminology: An editor is **active** when it can be used to edit the current note. Note that it
|
||||||
|
* doesn't necessarily mean that your editor is visible - it just means that the user has the option
|
||||||
|
* to switch to it (via the "toggle editor" button). A **visible** editor is active and is currently
|
||||||
|
* being displayed.
|
||||||
|
*
|
||||||
|
* To implement an editor you need to listen to two events:
|
||||||
|
*
|
||||||
|
* - `onActivationCheck`: This is a way for the app to know whether your editor should be active or
|
||||||
|
* not. Return `true` from this handler to activate your editor.
|
||||||
|
*
|
||||||
|
* - `onUpdate`: When this is called you should update your editor based on the current note
|
||||||
|
* content. Call `joplin.workspace.selectedNote()` to get the current note.
|
||||||
|
*
|
||||||
|
* - `showEditorPlugin` and `toggleEditorPlugin` commands. Additionally you can use these commands
|
||||||
|
* to display your editor via `joplin.commands.execute('showEditorPlugin')`. This is not always
|
||||||
|
* necessary since the user can switch to your editor using the "toggle editor" button, however
|
||||||
|
* you may want to programmatically display the editor in some cases - for example when creating a
|
||||||
|
* new note specific to your editor.
|
||||||
|
*
|
||||||
|
* Note that only one editor view can be active at a time. This is why it is important not to
|
||||||
|
* activate your view if it's not relevant to the current note. If more than one is active, it is
|
||||||
|
* undefined which editor is going to be used to display the note.
|
||||||
|
*
|
||||||
|
* For an example of editor plugin, see the [YesYouKan
|
||||||
|
* plugin](https://github.com/joplin/plugin-yesyoukan/blob/master/src/index.ts). In particular,
|
||||||
|
* check the logic around `onActivationCheck` and `onUpdate` since this is the entry points for
|
||||||
|
* using this API.
|
||||||
|
*/
|
||||||
|
export default class JoplinViewsEditors {
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
private store: any;
|
||||||
|
private plugin: Plugin;
|
||||||
|
private activationCheckHandlers_: Record<string, FilterHandler<EditorActivationCheckFilterObject>> = {};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
public constructor(plugin: Plugin, store: any) {
|
||||||
|
this.store = store;
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private controller(handle: ViewHandle): WebviewController {
|
||||||
|
return this.plugin.viewController(handle) as WebviewController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new editor view
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the editor HTML content
|
||||||
|
*/
|
||||||
|
public async setHtml(handle: ViewHandle, html: string): Promise<string> {
|
||||||
|
return this.controller(handle).html = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds and loads a new JS or CSS file into the panel.
|
||||||
|
*/
|
||||||
|
public async addScript(handle: ViewHandle, scriptPath: string): Promise<void> {
|
||||||
|
return this.controller(handle).addScript(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [[JoplinViewPanels]]
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
|
public async onMessage(handle: ViewHandle, callback: Function): Promise<void> {
|
||||||
|
return this.controller(handle).onMessage(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the editor can potentially be activated - this for example when the current note
|
||||||
|
* is changed, or when the application is opened. At that point should can check the current
|
||||||
|
* note and decide whether your editor should be activated or not. If it should return `true`,
|
||||||
|
* otherwise return `false`.
|
||||||
|
*/
|
||||||
|
public async onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise<void> {
|
||||||
|
const handler: FilterHandler<EditorActivationCheckFilterObject> = async (object) => {
|
||||||
|
const isActive = await callback();
|
||||||
|
object.activatedEditors.push({
|
||||||
|
pluginId: this.plugin.id,
|
||||||
|
viewId: handle,
|
||||||
|
isActive: isActive,
|
||||||
|
});
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.activationCheckHandlers_[handle] = handler;
|
||||||
|
|
||||||
|
eventManager.filterOn('editorActivationCheck', this.activationCheckHandlers_[handle]);
|
||||||
|
this.plugin.addOnUnloadListener(() => {
|
||||||
|
eventManager.filterOff('editorActivationCheck', this.activationCheckHandlers_[handle]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the editor content should be updated. This for example when the currently
|
||||||
|
* selected note changes, or when the user makes the editor visible.
|
||||||
|
*/
|
||||||
|
public async onUpdate(handle: ViewHandle, callback: UpdateCallback): Promise<void> {
|
||||||
|
this.controller(handle).onUpdate(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [[JoplinViewPanels]]
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
public postMessage(handle: ViewHandle, message: any): void {
|
||||||
|
return this.controller(handle).postMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether the editor is active or not.
|
||||||
|
*/
|
||||||
|
public async isActive(handle: ViewHandle): Promise<boolean> {
|
||||||
|
return this.controller(handle).visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether the editor is effectively visible or not. If the editor is inactive, this will
|
||||||
|
* return `false`. If the editor is active and the user has switched to it, it will return
|
||||||
|
* `true`. Otherwise it will return `false`.
|
||||||
|
*/
|
||||||
|
public async isVisible(handle: ViewHandle): Promise<boolean> {
|
||||||
|
return this.controller(handle).isVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -130,4 +130,8 @@ export default class JoplinViewsPanels {
|
|||||||
return this.controller(handle).visible;
|
return this.controller(handle).visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async isActive(handle: ViewHandle): Promise<boolean> {
|
||||||
|
return this.controller(handle).isActive();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import eventManager, { EventName } from '../../../eventManager';
|
|||||||
import Setting from '../../../models/Setting';
|
import Setting from '../../../models/Setting';
|
||||||
import { FolderEntity } from '../../database/types';
|
import { FolderEntity } from '../../database/types';
|
||||||
import makeListener from '../utils/makeListener';
|
import makeListener from '../utils/makeListener';
|
||||||
import { Disposable, MenuItem } from './types';
|
import { Disposable, EditContextMenuFilterObject, FilterHandler } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* @ignore
|
||||||
@ -18,12 +18,6 @@ import Note from '../../../models/Note';
|
|||||||
*/
|
*/
|
||||||
import Folder from '../../../models/Folder';
|
import Folder from '../../../models/Folder';
|
||||||
|
|
||||||
export interface EditContextMenuFilterObject {
|
|
||||||
items: MenuItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterHandler<T> = (object: T)=> Promise<void>;
|
|
||||||
|
|
||||||
enum ItemChangeEventType {
|
enum ItemChangeEventType {
|
||||||
Create = 1,
|
Create = 1,
|
||||||
Update = 2,
|
Update = 2,
|
||||||
|
@ -384,6 +384,26 @@ export interface Rectangle {
|
|||||||
height?: number;
|
height?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ActivationCheckCallback = ()=> Promise<boolean>;
|
||||||
|
|
||||||
|
export type UpdateCallback = ()=> Promise<void>;
|
||||||
|
|
||||||
|
export type VisibleHandler = ()=> Promise<void>;
|
||||||
|
|
||||||
|
export interface EditContextMenuFilterObject {
|
||||||
|
items: MenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorActivationCheckFilterObject {
|
||||||
|
activatedEditors: {
|
||||||
|
pluginId: string;
|
||||||
|
viewId: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterHandler<T> = (object: T)=> Promise<T>;
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// Settings types
|
// Settings types
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
@ -5,6 +5,10 @@ import { ButtonSpec } from './api/types';
|
|||||||
export interface PluginViewState {
|
export interface PluginViewState {
|
||||||
id: string;
|
id: string;
|
||||||
type: 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;
|
opened: boolean;
|
||||||
buttons: ButtonSpec[];
|
buttons: ButtonSpec[];
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
@ -28,7 +32,7 @@ interface PluginContentScriptStates {
|
|||||||
[type: string]: PluginContentScriptState[];
|
[type: string]: PluginContentScriptState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PluginState {
|
export interface PluginState {
|
||||||
id: string;
|
id: string;
|
||||||
contentScripts: PluginContentScriptStates;
|
contentScripts: PluginContentScriptStates;
|
||||||
views: PluginViewStates;
|
views: PluginViewStates;
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import { PluginState, PluginStates, PluginViewState } from '../reducer';
|
||||||
|
import { ContainerType } from '../WebviewController';
|
||||||
|
|
||||||
|
const logger = Logger.create('getActivePluginEditorView');
|
||||||
|
|
||||||
|
interface Output {
|
||||||
|
editorPlugin: PluginState;
|
||||||
|
editorView: PluginViewState;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user