mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-19 20:31:46 +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/useMessageHandler.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/useScheduleSaveCallbacks.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/search.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/showNoteContentProperties.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/showSpellCheckerMenu.test.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/toggleLayoutMoveMode.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/JoplinViews.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/JoplinViewsMenus.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/utils/createViewHandle.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.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/useMessageHandler.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/useScheduleSaveCallbacks.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/search.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/showNoteContentProperties.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/showSpellCheckerMenu.test.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/toggleLayoutMoveMode.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/JoplinViews.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/JoplinViewsMenus.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/utils/createViewHandle.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.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
|
@ -637,6 +637,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
<NoteEditor
|
||||
windowId={defaultWindowId}
|
||||
key={key}
|
||||
startupPluginsLoaded={this.props.startupPluginsLoaded}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
|
@ -21,6 +21,7 @@ interface Props {
|
||||
newWindow: boolean;
|
||||
windowId: string;
|
||||
activeWindowId: string;
|
||||
startupPluginsLoaded: boolean;
|
||||
}
|
||||
|
||||
const emptyCallback = () => {};
|
||||
@ -45,6 +46,7 @@ const SecondaryWindow: React.FC<Props> = props => {
|
||||
<NoteEditor
|
||||
windowId={props.windowId}
|
||||
onTitleChange={onNoteTitleChange}
|
||||
startupPluginsLoaded={props.startupPluginsLoaded}
|
||||
/>
|
||||
</div>;
|
||||
|
||||
@ -121,5 +123,6 @@ export default connect((state: AppState, ownProps: ConnectProps) => {
|
||||
codeView: windowState?.editorCodeView ?? state.settings['editor.codeView'],
|
||||
legacyMarkdown: state.settings['editor.legacyMarkdown'],
|
||||
activeWindowId: stateUtils.activeWindowId(state),
|
||||
startupPluginsLoaded: state.startupPluginsLoaded,
|
||||
};
|
||||
})(SecondaryWindow);
|
||||
|
@ -3,11 +3,10 @@ import { ContextMenuParams, Event } from 'electron';
|
||||
import { useEffect, RefObject } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
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 CommandService from '@joplin/lib/services/CommandService';
|
||||
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 eventManager from '@joplin/lib/eventManager';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
|
@ -32,7 +32,6 @@ import { itemIsReadOnly } from '@joplin/lib/models/utils/readOnly';
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
||||
import NoteSearchBar from '../NoteSearchBar';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import NoteRevisionViewer from '../NoteRevisionViewer';
|
||||
@ -51,10 +50,20 @@ import { MarkupLanguage } from '@joplin/renderer';
|
||||
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions';
|
||||
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks';
|
||||
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 { 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 logger = Logger.create('NoteEditor');
|
||||
|
||||
const commands = [
|
||||
require('./commands/showRevisions'),
|
||||
];
|
||||
@ -64,6 +73,15 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
const onDragOver: React.DragEventHandler = event => event.preventDefault();
|
||||
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) {
|
||||
const [showRevisions, setShowRevisions] = useState(false);
|
||||
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
|
||||
@ -73,6 +91,9 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
const titleInputRef = useRef<HTMLInputElement>();
|
||||
const isMountedRef = useRef(true);
|
||||
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.
|
||||
const editorId = useMemo(() => {
|
||||
@ -94,6 +115,29 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
|
||||
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({
|
||||
noteId: effectiveNoteId,
|
||||
isProvisional: props.isProvisional,
|
||||
@ -101,6 +145,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
editorRef: editorRef,
|
||||
onBeforeLoad: formNote_beforeLoad,
|
||||
onAfterLoad: formNote_afterLoad,
|
||||
builtInEditorVisible,
|
||||
editorId,
|
||||
});
|
||||
setFormNoteRef.current = setFormNote;
|
||||
@ -186,7 +231,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
// trigger onChange events, for example the textarea might be cleared.
|
||||
// We need to ignore these events, otherwise the note is going to be saved
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -456,16 +501,18 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
|
||||
let editor = null;
|
||||
|
||||
if (props.bodyEditor === 'TinyMCE') {
|
||||
editor = <TinyMCE {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'PlainText') {
|
||||
editor = <PlainEditor {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror5') {
|
||||
editor = <CodeMirror5 {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror6') {
|
||||
editor = <CodeMirror6 {...editorProps}/>;
|
||||
} else {
|
||||
throw new Error(`Invalid editor: ${props.bodyEditor}`);
|
||||
if (builtInEditorVisible) {
|
||||
if (props.bodyEditor === 'TinyMCE') {
|
||||
editor = <TinyMCE {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'PlainText') {
|
||||
editor = <PlainEditor {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror5') {
|
||||
editor = <CodeMirror5 {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror6') {
|
||||
editor = <CodeMirror6 {...editorProps}/>;
|
||||
} else {
|
||||
throw new Error(`Invalid editor: ${props.bodyEditor}`);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return renderNoNotes(styles.root);
|
||||
}
|
||||
@ -616,6 +680,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
{renderSearchInfo()}
|
||||
<div style={{ display: 'flex', flex: 1, paddingLeft: theme.editorPaddingLeft, maxHeight: '100%', minHeight: '0' }}>
|
||||
{editor}
|
||||
{renderPluginEditor()}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
{renderSearchBar()}
|
||||
@ -667,6 +732,8 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
watchedResources: state.watchedResources,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
'plugins.shownEditorViewIds': state.settings['plugins.shownEditorViewIds'] || [],
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'historyBackward',
|
||||
'historyForward',
|
||||
|
@ -1,6 +1,6 @@
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
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 { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||
import { Dispatch } from 'redux';
|
||||
@ -55,9 +55,11 @@ export interface NoteEditorProps {
|
||||
shareCacheSetting: string;
|
||||
syncUserId: string;
|
||||
searchResults: ProcessResultsRow[];
|
||||
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
'plugins.shownEditorViewIds': string[];
|
||||
onTitleChange?: (title: string)=> void;
|
||||
bodyEditor: string;
|
||||
startupPluginsLoaded: boolean;
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorRef {
|
||||
|
@ -15,6 +15,7 @@ const defaultFormNoteProps: HookDependencies = {
|
||||
onBeforeLoad: () => { },
|
||||
onAfterLoad: () => { },
|
||||
editorId: 'editor',
|
||||
builtInEditorVisible: false,
|
||||
};
|
||||
|
||||
describe('useFormNote', () => {
|
||||
|
@ -31,6 +31,7 @@ export interface HookDependencies {
|
||||
editorRef: any;
|
||||
onBeforeLoad(event: OnLoadEvent): void;
|
||||
onAfterLoad(event: OnLoadEvent): void;
|
||||
builtInEditorVisible: boolean;
|
||||
}
|
||||
|
||||
type MapFormNoteCallback = (previousFormNote: FormNote)=> FormNote;
|
||||
@ -67,10 +68,11 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
||||
}
|
||||
|
||||
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
|
||||
// a new refresh.
|
||||
const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
const prevBuiltInEditorVisible = usePrevious<boolean>(builtInEditorVisible);
|
||||
|
||||
useQueuedAsyncEffect(async (event) => {
|
||||
if (formNoteRefreshScheduled <= 0) return;
|
||||
@ -107,6 +109,15 @@ const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId:
|
||||
setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1);
|
||||
}, [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(() => {
|
||||
if (!noteId) return ()=>{};
|
||||
|
||||
@ -134,7 +145,9 @@ const useRefreshFormNoteOnChange = (formNoteRef: RefObject<FormNote>, editorId:
|
||||
};
|
||||
|
||||
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 [isNewNote, setIsNewNote] = useState(false);
|
||||
@ -195,7 +208,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
return newFormNote;
|
||||
}, []);
|
||||
|
||||
useRefreshFormNoteOnChange(formNoteRef, editorId, noteId, initNoteState);
|
||||
useRefreshFormNoteOnChange(formNoteRef, editorId, noteId, initNoteState, builtInEditorVisible);
|
||||
|
||||
useEffect(() => {
|
||||
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 { buildStyle } from '@joplin/lib/theme';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||
import { AppState } from '../../app.reducer';
|
||||
|
||||
interface NoteToolbarProps {
|
||||
@ -49,13 +50,20 @@ interface ConnectProps {
|
||||
const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
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 {
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'showSpellCheckerMenu',
|
||||
'editAlarm',
|
||||
'toggleVisiblePanes',
|
||||
'showNoteProperties',
|
||||
].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'noteToolbar')), whenClauseContext),
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(commands
|
||||
.concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'noteToolbar')), whenClauseContext),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -28,6 +28,7 @@ import * as restoreNote from './restoreNote';
|
||||
import * as revealResourceFile from './revealResourceFile';
|
||||
import * as search from './search';
|
||||
import * as setTags from './setTags';
|
||||
import * as showEditorPlugin from './showEditorPlugin';
|
||||
import * as showModalMessage from './showModalMessage';
|
||||
import * as showNoteContentProperties from './showNoteContentProperties';
|
||||
import * as showNoteProperties from './showNoteProperties';
|
||||
@ -35,6 +36,7 @@ import * as showPrompt from './showPrompt';
|
||||
import * as showShareFolderDialog from './showShareFolderDialog';
|
||||
import * as showShareNoteDialog from './showShareNoteDialog';
|
||||
import * as showSpellCheckerMenu from './showSpellCheckerMenu';
|
||||
import * as toggleEditorPlugin from './toggleEditorPlugin';
|
||||
import * as toggleEditors from './toggleEditors';
|
||||
import * as toggleLayoutMoveMode from './toggleLayoutMoveMode';
|
||||
import * as toggleMenuBar from './toggleMenuBar';
|
||||
@ -76,6 +78,7 @@ const index: any[] = [
|
||||
revealResourceFile,
|
||||
search,
|
||||
setTags,
|
||||
showEditorPlugin,
|
||||
showModalMessage,
|
||||
showNoteContentProperties,
|
||||
showNoteProperties,
|
||||
@ -83,6 +86,7 @@ const index: any[] = [
|
||||
showShareFolderDialog,
|
||||
showShareNoteDialog,
|
||||
showSpellCheckerMenu,
|
||||
toggleEditorPlugin,
|
||||
toggleEditors,
|
||||
toggleLayoutMoveMode,
|
||||
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';
|
||||
|
||||
// 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);
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
|
@ -24,6 +24,10 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
> .toolbar-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.-has-title {
|
||||
width: auto;
|
||||
max-width: unset;
|
||||
|
@ -137,7 +137,13 @@ export class EventManager {
|
||||
// deep equality check to see if it's been changed. Normally the
|
||||
// filter objects should be relatively small so there shouldn't be
|
||||
// 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.
|
||||
if (newOutput === undefined) continue;
|
||||
|
@ -926,6 +926,12 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
||||
storage: SettingStorage.File,
|
||||
},
|
||||
|
||||
'plugins.shownEditorViewIds': {
|
||||
value: [] as string[],
|
||||
type: SettingItemType.Array,
|
||||
public: false,
|
||||
},
|
||||
|
||||
// Deprecated - use markdown.plugin.*
|
||||
'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] },
|
||||
|
@ -180,6 +180,10 @@ export default class Plugin {
|
||||
this.viewControllers_[v.handle] = v;
|
||||
}
|
||||
|
||||
public hasViewController(handle: ViewHandle) {
|
||||
return !!this.viewControllers_[handle];
|
||||
}
|
||||
|
||||
public viewController(handle: ViewHandle): ViewController {
|
||||
if (!this.viewControllers_[handle]) throw new Error(`View not found: ${handle}`);
|
||||
return this.viewControllers_[handle];
|
||||
|
@ -14,6 +14,7 @@ import isCompatible from './utils/isCompatible';
|
||||
import { AppType } from './api/types';
|
||||
import minVersionForPlatform from './utils/isCompatible/minVersionForPlatform';
|
||||
import { _ } from '../../locale';
|
||||
import ViewController from './ViewController';
|
||||
const uslug = require('@joplin/fork-uslug');
|
||||
|
||||
const logger = Logger.create('PluginService');
|
||||
@ -202,6 +203,13 @@ export default class PluginService extends BaseService {
|
||||
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 {
|
||||
const output = { ...settings };
|
||||
|
||||
|
@ -5,10 +5,15 @@ const { toSystemSlashes } = require('../../path-utils');
|
||||
import PostMessageService, { MessageParticipant } from '../PostMessageService';
|
||||
import { PluginViewState } from './reducer';
|
||||
import { defaultWindowId } from '../../reducer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import CommandService from '../CommandService';
|
||||
|
||||
const logger = Logger.create('WebviewController');
|
||||
|
||||
export enum ContainerType {
|
||||
Panel = 'panel',
|
||||
Dialog = 'dialog',
|
||||
Editor = 'editor',
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
@ -49,12 +54,15 @@ 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 closeResponse_: CloseResponse = null;
|
||||
private containerType_: ContainerType = 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) {
|
||||
super(handle, pluginId, store);
|
||||
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
||||
this.containerType_ = containerType;
|
||||
|
||||
const view: PluginViewState = {
|
||||
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): Promise<any> {
|
||||
|
||||
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() {
|
||||
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
|
||||
public onMessage(callback: any) {
|
||||
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
|
||||
// ---------------------------------------------
|
||||
@ -238,4 +266,27 @@ export default class WebviewController extends ViewController {
|
||||
public set fitToContent(fitToContent: boolean) {
|
||||
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 JoplinViewsPanels from './JoplinViewsPanels';
|
||||
import JoplinViewsNoteList from './JoplinViewsNoteList';
|
||||
import JoplinViewsEditors from './JoplinViewsEditor';
|
||||
|
||||
/**
|
||||
* This namespace provides access to view-related services.
|
||||
@ -25,6 +26,7 @@ export default class JoplinViews {
|
||||
private menus_: JoplinViewsMenus = null;
|
||||
private toolbarButtons_: JoplinViewsToolbarButtons = null;
|
||||
private dialogs_: JoplinViewsDialogs = null;
|
||||
private editors_: JoplinViewsEditors = null;
|
||||
private noteList_: JoplinViewsNoteList = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private implementation_: any = null;
|
||||
@ -46,6 +48,11 @@ export default class JoplinViews {
|
||||
return this.panels_;
|
||||
}
|
||||
|
||||
public get editors() {
|
||||
if (!this.editors_) this.editors_ = new JoplinViewsEditors(this.plugin, this.store);
|
||||
return this.editors_;
|
||||
}
|
||||
|
||||
public get menuItems() {
|
||||
if (!this.menuItems_) this.menuItems_ = new JoplinViewsMenuItems(this.plugin, this.store);
|
||||
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;
|
||||
}
|
||||
|
||||
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 { FolderEntity } from '../../database/types';
|
||||
import makeListener from '../utils/makeListener';
|
||||
import { Disposable, MenuItem } from './types';
|
||||
import { Disposable, EditContextMenuFilterObject, FilterHandler } from './types';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
@ -18,12 +18,6 @@ import Note from '../../../models/Note';
|
||||
*/
|
||||
import Folder from '../../../models/Folder';
|
||||
|
||||
export interface EditContextMenuFilterObject {
|
||||
items: MenuItem[];
|
||||
}
|
||||
|
||||
type FilterHandler<T> = (object: T)=> Promise<void>;
|
||||
|
||||
enum ItemChangeEventType {
|
||||
Create = 1,
|
||||
Update = 2,
|
||||
|
@ -384,6 +384,26 @@ export interface Rectangle {
|
||||
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
|
||||
// =================================================================
|
||||
|
@ -5,6 +5,10 @@ import { ButtonSpec } from './api/types';
|
||||
export interface PluginViewState {
|
||||
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;
|
||||
@ -28,7 +32,7 @@ interface PluginContentScriptStates {
|
||||
[type: string]: PluginContentScriptState[];
|
||||
}
|
||||
|
||||
interface PluginState {
|
||||
export interface PluginState {
|
||||
id: string;
|
||||
contentScripts: PluginContentScriptStates;
|
||||
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