1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-29 22:48:10 +02:00

Desktop: Resolves #11687: Plugins: Allow editor plugins to support multiple windows (#12041)

This commit is contained in:
Henry Heino
2025-06-06 02:00:47 -07:00
committed by GitHub
parent 291ba88224
commit 608dbab453
46 changed files with 1022 additions and 195 deletions

View File

@@ -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,
});
}
}

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
/**

View File

@@ -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();
}

View File

@@ -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> {

View File

@@ -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;

View File

@@ -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') {

View File

@@ -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];
};

View File

@@ -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;
};

View File

@@ -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];
};

View File

@@ -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);
};