Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugins: Add support for editor plugins #11296

Merged
merged 22 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/app-desktop/gui/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ class MainScreenComponent extends React.Component<Props, State> {
<NoteEditor
windowId={defaultWindowId}
key={key}
startupPluginsLoaded={this.props.startupPluginsLoaded}
/>
</div>;
},
Expand Down
3 changes: 3 additions & 0 deletions packages/app-desktop/gui/NoteEditor/EditorWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface Props {
newWindow: boolean;
windowId: string;
activeWindowId: string;
startupPluginsLoaded: boolean;
}

const emptyCallback = () => {};
Expand All @@ -45,6 +46,7 @@ const SecondaryWindow: React.FC<Props> = props => {
<NoteEditor
windowId={props.windowId}
onTitleChange={onNoteTitleChange}
startupPluginsLoaded={props.startupPluginsLoaded}
/>
</div>;

Expand Down Expand Up @@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
91 changes: 79 additions & 12 deletions packages/app-desktop/gui/NoteEditor/NoteEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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'),
];
Expand All @@ -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);
Expand All @@ -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(() => {
Expand All @@ -94,13 +115,37 @@ 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,
titleInputRef: titleInputRef,
editorRef: editorRef,
onBeforeLoad: formNote_beforeLoad,
onAfterLoad: formNote_afterLoad,
builtInEditorVisible,
editorId,
});
setFormNoteRef.current = setFormNote;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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()}
Expand Down Expand Up @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions packages/app-desktop/gui/NoteEditor/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { FormNote } from './types';

import HtmlToMd from '@joplin/lib/HtmlToMd';
import HtmlToMd, { ParseOptions } from '@joplin/lib/HtmlToMd';
import Note from '@joplin/lib/models/Note';
import { NoteEntity } from '@joplin/lib/services/database/types';
const { MarkupToHtml } = require('@joplin/renderer');

export async function htmlToMarkdown(markupLanguage: number, html: string, originalCss: string): Promise<string> {
export async function htmlToMarkdown(markupLanguage: number, html: string, originalCss: string, parseOptions: ParseOptions = null): Promise<string> {
let newBody = '';

if (markupLanguage === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN) {
Expand All @@ -14,6 +14,7 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
preserveImageTagsWithSize: true,
preserveNestedTables: true,
preserveColorStyles: true,
...parseOptions,
});
newBody = await Note.replaceResourceExternalToInternalLinks(newBody, { useAbsolutePaths: true });
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export async function processPastedHtml(html: string, htmlToMd: HtmlToMarkdownHa
// TinyMCE, but lost once the note is saved. So here we convert the HTML to Markdown then back
// to HTML to ensure that the content we paste will be handled correctly by the app.
if (htmlToMd && mdToHtml) {
const md = await htmlToMd(MarkupLanguage.Markdown, html, '');
const md = await htmlToMd(MarkupLanguage.Markdown, html, '', { preserveColorStyles: Setting.value('editor.pastePreserveColors') });
html = (await mdToHtml(MarkupLanguage.Markdown, md, markupRenderOptions({ bodyOnly: true }))).html;

// When plugins that add to the end of rendered content are installed, bodyOnly can
Expand Down
9 changes: 6 additions & 3 deletions packages/app-desktop/gui/NoteEditor/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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';
import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';
import { DropHandler } from './useDropHandler';
import { SearchMarkers } from './useSearchMarkers';
import { ParseOptions } from '@joplin/lib/HtmlToMd';

export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
Expand Down Expand Up @@ -54,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 {
Expand Down Expand Up @@ -85,7 +88,7 @@ export interface MarkupToHtmlOptions {
}

export type MarkupToHtmlHandler = (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
export type HtmlToMarkdownHandler = (markupLanguage: number, html: string, originalCss: string)=> Promise<string>;
export type HtmlToMarkdownHandler = (markupLanguage: number, html: string, originalCss: string, parseOptions?: ParseOptions)=> Promise<string>;

export interface NoteBodyEditorProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const defaultFormNoteProps: HookDependencies = {
onBeforeLoad: () => { },
onAfterLoad: () => { },
editorId: 'editor',
builtInEditorVisible: false,
};

describe('useFormNote', () => {
Expand Down
Loading
Loading