You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-29 22:48:10 +02:00
This commit is contained in:
@@ -12,32 +12,81 @@ import WebviewController from './WebviewController';
|
||||
|
||||
const logger = Logger.create('EditorPluginHandler');
|
||||
|
||||
const makeNoteUpdateAction = (pluginService: PluginService, shownEditorViewIds: string[]) => {
|
||||
export interface UpdateEvent {
|
||||
noteId: string;
|
||||
newBody: string;
|
||||
}
|
||||
|
||||
interface EmitActivationCheckOptions {
|
||||
noteId: string;
|
||||
parentWindowId: string;
|
||||
}
|
||||
|
||||
interface SaveNoteEvent {
|
||||
id: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export type OnSaveNoteCallback = (updatedNote: SaveNoteEvent)=> void;
|
||||
|
||||
const makeNoteUpdateAction = (pluginService: PluginService, event: UpdateEvent, shownEditorViewIds: string[]) => {
|
||||
return async () => {
|
||||
for (const viewId of shownEditorViewIds) {
|
||||
const controller = pluginService.viewControllerByViewId(viewId) as WebviewController;
|
||||
if (controller) controller.emitUpdate();
|
||||
if (controller) {
|
||||
controller.emitUpdate({
|
||||
noteId: event.noteId,
|
||||
newBody: event.newBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default class {
|
||||
|
||||
private pluginService_: PluginService;
|
||||
private viewUpdateAsyncQueue_ = new AsyncActionQueue(100, IntervalType.Fixed);
|
||||
private lastNoteState_: UpdateEvent|null = null;
|
||||
private lastShownEditorViewIds_ = '';
|
||||
private lastEditorPluginShown_: string|null = null;
|
||||
|
||||
public constructor(pluginService: PluginService) {
|
||||
this.pluginService_ = pluginService;
|
||||
public constructor(
|
||||
private pluginService_: PluginService,
|
||||
private onSaveNote_: OnSaveNoteCallback,
|
||||
) {
|
||||
}
|
||||
|
||||
public emitUpdate(shownEditorViewIds: string[]) {
|
||||
logger.info('emitUpdate:', shownEditorViewIds);
|
||||
this.viewUpdateAsyncQueue_.push(makeNoteUpdateAction(this.pluginService_, shownEditorViewIds));
|
||||
public emitUpdate(event: UpdateEvent, shownEditorViewIds: string[]) {
|
||||
if (shownEditorViewIds.length === 0) return;
|
||||
|
||||
const isEventDifferentFrom = (other: UpdateEvent|null) => {
|
||||
if (!other) return true;
|
||||
return event.noteId !== other.noteId || event.newBody !== other.newBody;
|
||||
};
|
||||
|
||||
const shownEditorViewIdsString = shownEditorViewIds.join(',');
|
||||
const differentEditorViewsShown = shownEditorViewIdsString !== this.lastShownEditorViewIds_;
|
||||
|
||||
// lastNoteState_ often contains the last change saved by the editor. As a result,
|
||||
// if `event` matches `lastNoteState_`, the event was probably caused by the last save.
|
||||
// In this case, avoid sending an update event (which plugins often interpret as refreshing
|
||||
// the editor):
|
||||
const isDifferentFromSave = isEventDifferentFrom(this.lastNoteState_);
|
||||
|
||||
if (isDifferentFromSave || differentEditorViewsShown) {
|
||||
logger.info('emitUpdate:', shownEditorViewIds);
|
||||
this.viewUpdateAsyncQueue_.push(makeNoteUpdateAction(this.pluginService_, event, shownEditorViewIds));
|
||||
|
||||
this.lastNoteState_ = { ...event };
|
||||
this.lastShownEditorViewIds_ = shownEditorViewIdsString;
|
||||
}
|
||||
}
|
||||
|
||||
public async emitActivationCheck() {
|
||||
public async emitActivationCheck({ noteId, parentWindowId }: EmitActivationCheckOptions) {
|
||||
let filterObject: EditorActivationCheckFilterObject = {
|
||||
activatedEditors: [],
|
||||
effectiveNoteId: noteId,
|
||||
windowId: parentWindowId,
|
||||
};
|
||||
filterObject = await eventManager.filterEmit('editorActivationCheck', filterObject);
|
||||
|
||||
@@ -45,8 +94,34 @@ export default class {
|
||||
|
||||
for (const editor of filterObject.activatedEditors) {
|
||||
const controller = this.pluginService_.pluginById(editor.pluginId).viewController(editor.viewId) as WebviewController;
|
||||
controller.setActive(editor.isActive);
|
||||
if (controller.parentWindowId === parentWindowId) {
|
||||
controller.setActive(editor.isActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onEditorPluginShown(editorViewId: string) {
|
||||
// Don't double-register callbacks
|
||||
if (editorViewId === this.lastEditorPluginShown_) {
|
||||
return;
|
||||
}
|
||||
this.lastEditorPluginShown_ = editorViewId;
|
||||
|
||||
const controller = this.pluginService_.viewControllerByViewId(editorViewId) as WebviewController;
|
||||
controller?.onNoteSaveRequested(event => {
|
||||
this.scheduleSaveNote_(event.noteId, event.body);
|
||||
});
|
||||
}
|
||||
|
||||
private scheduleSaveNote_(noteId: string, noteBody: string) {
|
||||
this.lastNoteState_ = {
|
||||
noteId,
|
||||
newBody: noteBody,
|
||||
};
|
||||
|
||||
return this.onSaveNote_({
|
||||
id: noteId,
|
||||
body: noteBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,10 @@ export default class Plugin {
|
||||
this.viewControllers_[v.handle] = v;
|
||||
}
|
||||
|
||||
public removeViewController(v: ViewController) {
|
||||
delete this.viewControllers_[v.handle];
|
||||
}
|
||||
|
||||
public hasViewController(handle: ViewHandle) {
|
||||
return !!this.viewControllers_[handle];
|
||||
}
|
||||
@@ -229,6 +233,10 @@ export default class Plugin {
|
||||
this.onUnloadListeners_.push(callback);
|
||||
}
|
||||
|
||||
public removeOnUnloadListener(callback: OnUnloadListener) {
|
||||
this.onUnloadListeners_ = this.onUnloadListeners_.filter(other => other !== callback);
|
||||
}
|
||||
|
||||
public onUnload() {
|
||||
for (const callback of this.onUnloadListeners_) {
|
||||
callback();
|
||||
|
||||
@@ -3,10 +3,9 @@ import shim from '../../shim';
|
||||
import { ButtonSpec, DialogResult, ViewHandle } from './api/types';
|
||||
const { toSystemSlashes } = require('../../path-utils');
|
||||
import PostMessageService, { MessageParticipant } from '../PostMessageService';
|
||||
import { PluginViewState } from './reducer';
|
||||
import { PluginEditorViewState, PluginViewState } from './reducer';
|
||||
import { defaultWindowId } from '../../reducer';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import CommandService from '../CommandService';
|
||||
|
||||
const logger = Logger.create('WebviewController');
|
||||
|
||||
@@ -49,32 +48,48 @@ function findItemByKey(layout: any, key: string): any {
|
||||
return recurseFind(layout);
|
||||
}
|
||||
|
||||
interface EditorUpdateEvent {
|
||||
noteId: string;
|
||||
newBody: string;
|
||||
}
|
||||
type EditorUpdateListener = (event: EditorUpdateEvent)=> void;
|
||||
|
||||
interface SaveNoteEvent {
|
||||
noteId: string;
|
||||
body: string;
|
||||
}
|
||||
type OnSaveNoteCallback = (saveNoteEvent: SaveNoteEvent)=> void;
|
||||
|
||||
export default class WebviewController extends ViewController {
|
||||
|
||||
private baseDir_: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
private messageListener_: Function = null;
|
||||
private updateListener_: ()=> void = null;
|
||||
private updateListener_: EditorUpdateListener|null = null;
|
||||
private closeResponse_: CloseResponse = null;
|
||||
private containerType_: ContainerType = null;
|
||||
private saveNoteListener_: OnSaveNoteCallback|null = null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
|
||||
public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType, parentWindowId: string|null) {
|
||||
super(handle, pluginId, store);
|
||||
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
||||
this.containerType_ = containerType;
|
||||
|
||||
const view: PluginViewState = {
|
||||
id: this.handle,
|
||||
editorTypeId: '',
|
||||
type: this.type,
|
||||
containerType: containerType,
|
||||
html: '',
|
||||
scripts: [],
|
||||
buttons: null,
|
||||
fitToContent: true,
|
||||
// Opened is used for dialogs and mobile panels (which are shown
|
||||
// like dialogs):
|
||||
opened: containerType === ContainerType.Panel,
|
||||
buttons: null,
|
||||
fitToContent: true,
|
||||
active: false,
|
||||
parentWindowId,
|
||||
};
|
||||
|
||||
this.store.dispatch({
|
||||
@@ -84,10 +99,23 @@ export default class WebviewController extends ViewController {
|
||||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.store.dispatch({
|
||||
type: 'PLUGIN_VIEW_REMOVE',
|
||||
pluginId: this.pluginId,
|
||||
viewId: this.storeView.id,
|
||||
});
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
return 'webview';
|
||||
}
|
||||
|
||||
// Returns `null` if the view can be shown in any window.
|
||||
public get parentWindowId(): string {
|
||||
return this.storeView.parentWindowId;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private setStoreProp(name: string, value: any) {
|
||||
this.store.dispatch({
|
||||
@@ -127,7 +155,6 @@ export default class WebviewController extends ViewController {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public postMessage(message: any) {
|
||||
|
||||
const messageId = `plugin_${Date.now()}${Math.random()}`;
|
||||
|
||||
void PostMessageService.instance().postMessage({
|
||||
@@ -146,15 +173,10 @@ export default class WebviewController extends ViewController {
|
||||
public async emitMessage(event: EmitMessageEvent) {
|
||||
if (!this.messageListener_) return;
|
||||
|
||||
if (this.containerType_ === ContainerType.Editor && !this.isActive()) {
|
||||
logger.info('emitMessage: Not emitting message because editor is disabled:', this.pluginId, this.handle);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.messageListener_(event.message);
|
||||
}
|
||||
|
||||
public emitUpdate() {
|
||||
public emitUpdate(event: EditorUpdateEvent) {
|
||||
if (!this.updateListener_) return;
|
||||
|
||||
if (this.containerType_ === ContainerType.Editor && (!this.isActive() || !this.isVisible())) {
|
||||
@@ -162,7 +184,7 @@ export default class WebviewController extends ViewController {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateListener_();
|
||||
this.updateListener_(event);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -170,8 +192,7 @@ export default class WebviewController extends ViewController {
|
||||
this.messageListener_ = callback;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public onUpdate(callback: any) {
|
||||
public onUpdate(callback: EditorUpdateListener) {
|
||||
this.updateListener_ = callback;
|
||||
}
|
||||
|
||||
@@ -271,22 +292,37 @@ export default class WebviewController extends ViewController {
|
||||
// Specific to editors
|
||||
// ---------------------------------------------
|
||||
|
||||
public setEditorTypeId(id: string) {
|
||||
this.setStoreProp('editorTypeId', id);
|
||||
}
|
||||
|
||||
public setActive(active: boolean) {
|
||||
this.setStoreProp('opened', active);
|
||||
this.setStoreProp('active', active);
|
||||
}
|
||||
|
||||
public isActive(): boolean {
|
||||
return this.storeView.opened;
|
||||
const state = this.storeView as PluginEditorViewState;
|
||||
return state.active;
|
||||
}
|
||||
|
||||
public setOpened(visible: boolean) {
|
||||
this.setStoreProp('opened', visible);
|
||||
}
|
||||
|
||||
public isVisible(): boolean {
|
||||
if (!this.storeView.opened) return false;
|
||||
const shownEditorViewIds: string[] = this.store.getState().settings['plugins.shownEditorViewIds'];
|
||||
return shownEditorViewIds.includes(this.handle);
|
||||
const state = this.storeView as PluginEditorViewState;
|
||||
return state.active && state.opened;
|
||||
}
|
||||
|
||||
public async setVisible(visible: boolean) {
|
||||
await CommandService.instance().execute('showEditorPlugin', this.handle, visible);
|
||||
public async requestSaveNote(event: SaveNoteEvent) {
|
||||
if (!this.saveNoteListener_) {
|
||||
logger.warn('Note save requested, but no save handler was registered. View ID: ', this.storeView?.id);
|
||||
return;
|
||||
}
|
||||
this.saveNoteListener_(event);
|
||||
}
|
||||
|
||||
public onNoteSaveRequested(listener: OnSaveNoteCallback) {
|
||||
this.saveNoteListener_ = listener;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default class JoplinViewsDialogs {
|
||||
}
|
||||
|
||||
const handle = createViewHandle(this.plugin, id);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Dialog);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Dialog, null);
|
||||
this.plugin.addViewController(controller);
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
import eventManager from '../../../eventManager';
|
||||
import eventManager, { EventName, WindowCloseEvent, WindowOpenEvent } from '../../../eventManager';
|
||||
import Setting from '../../../models/Setting';
|
||||
import { defaultWindowId } from '../../../reducer';
|
||||
import Plugin from '../Plugin';
|
||||
import createViewHandle from '../utils/createViewHandle';
|
||||
import WebviewController, { ContainerType } from '../WebviewController';
|
||||
import { ActivationCheckCallback, EditorActivationCheckFilterObject, FilterHandler, ViewHandle, UpdateCallback } from './types';
|
||||
import { ActivationCheckCallback, EditorActivationCheckFilterObject, FilterHandler, ViewHandle, UpdateCallback, EditorPluginCallbacks } from './types';
|
||||
|
||||
interface SaveNoteOptions {
|
||||
/**
|
||||
* The ID of the note to save. This should match either:
|
||||
* - The ID of the note currently being edited
|
||||
* - The ID of a note that was very recently open in the editor.
|
||||
*
|
||||
* This property is present to ensure that the note editor doesn't write
|
||||
* to the wrong note just after switching notes.
|
||||
*/
|
||||
noteId: string;
|
||||
/** The note's new content. */
|
||||
body: string;
|
||||
}
|
||||
|
||||
type ActivationCheckSlice = Pick<EditorActivationCheckFilterObject, 'effectiveNoteId'|'windowId'|'activatedEditors'>;
|
||||
|
||||
/**
|
||||
* Allows creating alternative note editors. You can create a view to handle loading and saving the
|
||||
@@ -49,6 +67,7 @@ export default class JoplinViewsEditors {
|
||||
private store: any;
|
||||
private plugin: Plugin;
|
||||
private activationCheckHandlers_: Record<string, FilterHandler<EditorActivationCheckFilterObject>> = {};
|
||||
private unhandledActivationCheck_: Map<string, ActivationCheckSlice> = new Map();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public constructor(plugin: Plugin, store: any) {
|
||||
@@ -60,14 +79,106 @@ export default class JoplinViewsEditors {
|
||||
return this.plugin.viewController(handle) as WebviewController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new editor plugin. Joplin will call the provided callback to create new editor views
|
||||
* associated with the plugin as necessary (e.g. when a new editor is created in a new window).
|
||||
*/
|
||||
public async register(viewId: string, callbacks: EditorPluginCallbacks) {
|
||||
const initializeController = (handle: ViewHandle, windowId: string) => {
|
||||
const editorTypeId = `${this.plugin.id}-${viewId}`;
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Editor, windowId);
|
||||
controller.setEditorTypeId(editorTypeId);
|
||||
this.plugin.addViewController(controller);
|
||||
// Restore the last open/closed state for the editor
|
||||
controller.setOpened(Setting.value('plugins.shownEditorViewIds').includes(editorTypeId));
|
||||
|
||||
return () => {
|
||||
this.plugin.removeViewController(controller);
|
||||
controller.destroy();
|
||||
};
|
||||
};
|
||||
|
||||
// Register the activation check handler early to handle the case where the editorActivationCheck
|
||||
// event is fired **before** an activation check handler is registered through the API.
|
||||
const registerActivationCheckHandler = (handle: ViewHandle) => {
|
||||
const onActivationCheck: FilterHandler<EditorActivationCheckFilterObject> = async object => {
|
||||
if (this.activationCheckHandlers_[handle]) {
|
||||
return this.activationCheckHandlers_[handle](object);
|
||||
} else {
|
||||
this.unhandledActivationCheck_.set(handle, {
|
||||
...object,
|
||||
});
|
||||
return object;
|
||||
}
|
||||
};
|
||||
eventManager.filterOn('editorActivationCheck', onActivationCheck);
|
||||
const cleanup = () => {
|
||||
eventManager.filterOff('editorActivationCheck', onActivationCheck);
|
||||
this.unhandledActivationCheck_.delete(handle);
|
||||
};
|
||||
|
||||
return cleanup;
|
||||
};
|
||||
|
||||
const listenForWindowOrPluginClose = (windowId: string, onClose: ()=> void) => {
|
||||
const closeListener = (event: WindowCloseEvent|null) => {
|
||||
if (event && event.windowId !== windowId) return;
|
||||
|
||||
onClose();
|
||||
eventManager.off(EventName.WindowClose, closeListener);
|
||||
};
|
||||
eventManager.on(EventName.WindowClose, closeListener);
|
||||
|
||||
this.plugin.addOnUnloadListener(() => {
|
||||
closeListener(null);
|
||||
});
|
||||
};
|
||||
|
||||
const createEditorViewForWindow = async (windowId: string) => {
|
||||
const handle = createViewHandle(this.plugin, `${viewId}-${windowId}`);
|
||||
|
||||
const removeController = initializeController(handle, windowId);
|
||||
const removeActivationCheck = registerActivationCheckHandler(handle);
|
||||
|
||||
await callbacks.onSetup(handle);
|
||||
|
||||
// Register the activation check after calling onSetup to ensure that the editor
|
||||
// is fully set up before it can be marked as active.
|
||||
await this.onActivationCheck(handle, callbacks.onActivationCheck);
|
||||
|
||||
listenForWindowOrPluginClose(windowId, () => {
|
||||
// Save resources by closing resources associated with
|
||||
// closed windows:
|
||||
removeController();
|
||||
removeActivationCheck();
|
||||
});
|
||||
};
|
||||
|
||||
await createEditorViewForWindow(defaultWindowId);
|
||||
|
||||
const onWindowOpen = (event: WindowOpenEvent) => createEditorViewForWindow(event.windowId);
|
||||
eventManager.on(EventName.WindowOpen, onWindowOpen);
|
||||
this.plugin.addOnUnloadListener(() => {
|
||||
eventManager.off(EventName.WindowOpen, onWindowOpen);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new editor view
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public async create(id: string): Promise<ViewHandle> {
|
||||
const handle = createViewHandle(this.plugin, id);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Editor);
|
||||
this.plugin.addViewController(controller);
|
||||
return handle;
|
||||
return new Promise<ViewHandle>(resolve => {
|
||||
void this.register(id, {
|
||||
onSetup: async (handle) => {
|
||||
resolve(handle);
|
||||
},
|
||||
onActivationCheck: async () => {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,29 +203,52 @@ export default class JoplinViewsEditors {
|
||||
return this.controller(handle).onMessage(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the content of the editor, without calling `onUpdate` for editors in the same window.
|
||||
*/
|
||||
public async saveNote(handle: ViewHandle, props: SaveNoteOptions): Promise<void> {
|
||||
await this.controller(handle).requestSaveNote({
|
||||
noteId: props.noteId,
|
||||
body: props.body,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*
|
||||
* @deprecated - `onActivationCheck` should be provided when the editor is first created with
|
||||
* `editor.register`.
|
||||
*/
|
||||
public async onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise<void> {
|
||||
const handler: FilterHandler<EditorActivationCheckFilterObject> = async (object) => {
|
||||
const isActive = await callback();
|
||||
const isActive = async ({ windowId, effectiveNoteId }: ActivationCheckSlice) => {
|
||||
const isCorrectWindow = windowId === this.controller(handle).parentWindowId;
|
||||
const active = isCorrectWindow && await callback({
|
||||
handle,
|
||||
noteId: effectiveNoteId,
|
||||
});
|
||||
return active;
|
||||
};
|
||||
const handler = async (object: ActivationCheckSlice) => {
|
||||
object.activatedEditors.push({
|
||||
pluginId: this.plugin.id,
|
||||
viewId: handle,
|
||||
isActive: isActive,
|
||||
isActive: await isActive(object),
|
||||
});
|
||||
return object;
|
||||
};
|
||||
|
||||
this.activationCheckHandlers_[handle] = handler;
|
||||
|
||||
eventManager.filterOn('editorActivationCheck', this.activationCheckHandlers_[handle]);
|
||||
this.plugin.addOnUnloadListener(() => {
|
||||
eventManager.filterOff('editorActivationCheck', this.activationCheckHandlers_[handle]);
|
||||
});
|
||||
// Handle the case where the activation check was done before this onActivationCheck handler was registered.
|
||||
if (this.unhandledActivationCheck_.has(handle)) {
|
||||
const lastActivationCheckObject = this.unhandledActivationCheck_.get(handle);
|
||||
this.unhandledActivationCheck_.delete(handle);
|
||||
|
||||
this.controller(handle).setActive(await isActive(lastActivationCheckObject));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +271,7 @@ export default class JoplinViewsEditors {
|
||||
* Tells whether the editor is active or not.
|
||||
*/
|
||||
public async isActive(handle: ViewHandle): Promise<boolean> {
|
||||
return this.controller(handle).visible;
|
||||
return this.controller(handle).isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
import { defaultWindowId } from '../../../reducer';
|
||||
import Plugin from '../Plugin';
|
||||
import createViewHandle from '../utils/createViewHandle';
|
||||
import WebviewController, { ContainerType } from '../WebviewController';
|
||||
@@ -45,7 +46,7 @@ export default class JoplinViewsPanels {
|
||||
}
|
||||
|
||||
const handle = createViewHandle(this.plugin, id);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Panel);
|
||||
const controller = new WebviewController(handle, this.plugin.id, this.store, this.plugin.baseDir, ContainerType.Panel, defaultWindowId);
|
||||
this.plugin.addViewController(controller);
|
||||
return handle;
|
||||
}
|
||||
@@ -130,6 +131,10 @@ export default class JoplinViewsPanels {
|
||||
return this.controller(handle).visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assuming that the current panel is an editor plugin view, returns
|
||||
* whether the editor plugin view supports editing the current note.
|
||||
*/
|
||||
public async isActive(handle: ViewHandle): Promise<boolean> {
|
||||
return this.controller(handle).isActive();
|
||||
}
|
||||
|
||||
@@ -171,6 +171,8 @@ export default class JoplinWorkspace {
|
||||
|
||||
/**
|
||||
* Gets the currently selected note. Will be `null` if no note is selected.
|
||||
*
|
||||
* On desktop, this returns the selected note in the focused window.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async selectedNote(): Promise<any> {
|
||||
|
||||
@@ -397,9 +397,40 @@ export interface Rectangle {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type ActivationCheckCallback = ()=> Promise<boolean>;
|
||||
export interface EditorUpdateEvent {
|
||||
newBody: string;
|
||||
noteId: string;
|
||||
}
|
||||
export type UpdateCallback = (event: EditorUpdateEvent)=> Promise<void>;
|
||||
|
||||
export type UpdateCallback = ()=> Promise<void>;
|
||||
|
||||
export interface ActivationCheckEvent {
|
||||
handle: ViewHandle;
|
||||
noteId: string;
|
||||
}
|
||||
export type ActivationCheckCallback = (event: ActivationCheckEvent)=> Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Required callbacks for creating an editor plugin.
|
||||
*/
|
||||
export interface EditorPluginCallbacks {
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*/
|
||||
onActivationCheck: ActivationCheckCallback;
|
||||
|
||||
/**
|
||||
* Emitted when an editor view is created. This happens, for example, when a new window containing
|
||||
* a new editor is created.
|
||||
*
|
||||
* This callback should set the editor plugin's HTML using `editors.setHtml`, add scripts to the editor
|
||||
* with `editors.addScript`, and optionally listen for external changes using `editors.onUpdate`.
|
||||
*/
|
||||
onSetup: (handle: ViewHandle)=> Promise<void>;
|
||||
}
|
||||
|
||||
export type VisibleHandler = ()=> Promise<void>;
|
||||
|
||||
@@ -408,6 +439,8 @@ export interface EditContextMenuFilterObject {
|
||||
}
|
||||
|
||||
export interface EditorActivationCheckFilterObject {
|
||||
effectiveNoteId: string;
|
||||
windowId: string;
|
||||
activatedEditors: {
|
||||
pluginId: string;
|
||||
viewId: string;
|
||||
|
||||
@@ -2,23 +2,39 @@ import { Draft } from 'immer';
|
||||
import { ContainerType } from './WebviewController';
|
||||
import { ButtonSpec } from './api/types';
|
||||
|
||||
export interface PluginViewState {
|
||||
interface PluginViewStateBase {
|
||||
id: string;
|
||||
type: string;
|
||||
// Note that this property will mean different thing depending on the `containerType`. If it's a
|
||||
// dialog, it means that the dialog is opened. If it's a panel, it means it's visible/opened. If
|
||||
// it's an editor, it means the editor is currently active (but it may not be visible - see
|
||||
// JoplinViewsEditor).
|
||||
opened: boolean;
|
||||
buttons: ButtonSpec[];
|
||||
fitToContent?: boolean;
|
||||
scripts?: string[];
|
||||
html?: string;
|
||||
commandName?: string;
|
||||
location?: string;
|
||||
containerType: ContainerType;
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
export interface PluginEditorViewState extends PluginViewStateBase {
|
||||
containerType: ContainerType.Editor;
|
||||
|
||||
parentWindowId: string;
|
||||
active: boolean;
|
||||
|
||||
// A non-unique ID determined by the type of the editor. Unlike the id property,
|
||||
// this is the same for editor views of the same type opened in different windows.
|
||||
editorTypeId: string;
|
||||
}
|
||||
|
||||
interface PluginDialogViewState extends PluginViewStateBase {
|
||||
containerType: ContainerType.Dialog;
|
||||
}
|
||||
|
||||
interface PluginPanelViewState extends PluginViewStateBase {
|
||||
containerType: ContainerType.Panel;
|
||||
}
|
||||
|
||||
export type PluginViewState = PluginEditorViewState|PluginDialogViewState|PluginPanelViewState;
|
||||
|
||||
interface PluginViewStates {
|
||||
[key: string]: PluginViewState;
|
||||
}
|
||||
@@ -166,6 +182,10 @@ const reducer = (draftRoot: Draft<any>, action: any) => {
|
||||
draft.plugins[action.pluginId].views[action.view.id] = { ...action.view };
|
||||
break;
|
||||
|
||||
case 'PLUGIN_VIEW_REMOVE':
|
||||
delete draft.plugins[action.pluginId].views[action.viewId];
|
||||
break;
|
||||
|
||||
case 'PLUGIN_VIEW_PROP_SET':
|
||||
|
||||
if (action.name !== 'html') {
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { PluginState, PluginStates, PluginViewState } from '../reducer';
|
||||
import { ContainerType } from '../WebviewController';
|
||||
import { PluginStates } from '../reducer';
|
||||
import getActivePluginEditorViews from './getActivePluginEditorViews';
|
||||
|
||||
const logger = Logger.create('getActivePluginEditorView');
|
||||
|
||||
interface Output {
|
||||
editorPlugin: PluginState;
|
||||
editorView: PluginViewState;
|
||||
}
|
||||
export default (plugins: PluginStates, windowId: string) => {
|
||||
const allActiveViews = getActivePluginEditorViews(plugins, windowId);
|
||||
|
||||
export default (plugins: PluginStates) => {
|
||||
let output: Output = { editorPlugin: null, editorView: null };
|
||||
for (const [, pluginState] of Object.entries(plugins)) {
|
||||
for (const [, view] of Object.entries(pluginState.views)) {
|
||||
if (view.type === 'webview' && view.containerType === ContainerType.Editor && view.opened) {
|
||||
if (output.editorPlugin) {
|
||||
logger.warn(`More than one editor plugin are active for this note. Active plugin: ${output.editorPlugin.id}. Ignored plugin: ${pluginState.id}`);
|
||||
} else {
|
||||
output = { editorPlugin: pluginState, editorView: view };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allActiveViews.length === 0) {
|
||||
return { editorPlugin: null, editorView: null };
|
||||
}
|
||||
|
||||
return output;
|
||||
const result = allActiveViews[0];
|
||||
if (allActiveViews.length > 1) {
|
||||
const ignoredPluginIds = allActiveViews.slice(1).map(({ editorPlugin }) => editorPlugin.id);
|
||||
logger.warn(`More than one editor plugin are active for this note. Active plugin: ${result.editorPlugin.id}. Ignored plugins: ${ignoredPluginIds.join(',')}`);
|
||||
}
|
||||
|
||||
return allActiveViews[0];
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import { ContainerType } from '../WebviewController';
|
||||
|
||||
interface Options {
|
||||
mustBeVisible?: boolean;
|
||||
}
|
||||
|
||||
export default (plugins: PluginStates, windowId: string, { mustBeVisible = false }: Options = {}) => {
|
||||
const output = [];
|
||||
|
||||
for (const [, pluginState] of Object.entries(plugins)) {
|
||||
for (const [, view] of Object.entries(pluginState.views)) {
|
||||
if (view.type !== 'webview' || view.containerType !== ContainerType.Editor) continue;
|
||||
if (view.parentWindowId !== windowId || !view.active) continue;
|
||||
|
||||
output.push({ editorPlugin: pluginState, editorView: view });
|
||||
}
|
||||
}
|
||||
|
||||
if (mustBeVisible) {
|
||||
// Filter out views that haven't been shown:
|
||||
return output.filter(({ editorView }) => editorView.opened);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import getActivePluginEditorView from './getActivePluginEditorView';
|
||||
import getActivePluginEditorViews from './getActivePluginEditorViews';
|
||||
|
||||
export default (plugins: PluginStates, shownEditorViewIds: string[]) => {
|
||||
const { editorPlugin, editorView } = getActivePluginEditorView(plugins);
|
||||
if (editorView) {
|
||||
if (!shownEditorViewIds.includes(editorView.id)) return { editorPlugin: null, editorView: null };
|
||||
export default (plugins: PluginStates, windowId: string) => {
|
||||
const visibleViews = getActivePluginEditorViews(plugins, windowId, { mustBeVisible: true });
|
||||
if (!visibleViews.length) {
|
||||
return { editorView: null, editorPlugin: null };
|
||||
}
|
||||
return { editorPlugin, editorView };
|
||||
return visibleViews[0];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import getActivePluginEditorViews from './getActivePluginEditorViews';
|
||||
|
||||
export default (state: PluginStates, windowId: string) => {
|
||||
return getActivePluginEditorViews(
|
||||
state, windowId, { mustBeVisible: true },
|
||||
).map(({ editorView }) => editorView.id);
|
||||
};
|
||||
Reference in New Issue
Block a user