mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 00:13:09 +00:00
A frame slot `side-panel`: a plugin's panel right of the mail list and
reader, opened from a button in the panel rail (Mountain View's, or a slim
rail in the default layout while a plugin offers one). `ui.setSidePanel`
names its button; `ui.openPanel('side-panel')` opens it.
`ui.setSidePanel` takes `rail: false`: the rail draws no button for the
plugin's panel, which then opens only on the plugin's request
(openPanel / togglePanel('side-panel')), e.g. from a top bar action. A
rail left without buttons is not drawn, in the default layout and in
Mountain View's side panel alike.
68 lines
3.0 KiB
TypeScript
68 lines
3.0 KiB
TypeScript
// Plugins opening and closing the panel their slot sits in
|
|
// (api.ui.openPanel / closePanel / togglePanel): the composer's side panels,
|
|
// the reader's detail panel and the panel beside the mail (side-panel). A
|
|
// toolbar button, a shortcut or a right-click entry can then bring the
|
|
// plugin's panel up instead of a dialog. The panels subscribe here while they are on screen; a request
|
|
// nobody answers (no composer open, no message open, a phone) resolves to
|
|
// null, so the plugin can fall back to a dialog.
|
|
//
|
|
// When a panel opens on request, its slot gets a new `focusRequest` number
|
|
// in its props, so the plugin can put the focus where it belongs.
|
|
|
|
export const PANEL_SLOTS = ['composer-sidebar', 'composer-sidebar-right', 'email-detail-sidebar', 'side-panel'] as const;
|
|
export type PanelSlot = typeof PANEL_SLOTS[number];
|
|
export type PanelMode = 'open' | 'close' | 'toggle';
|
|
|
|
export interface PanelRequest {
|
|
pluginId: string;
|
|
slot: PanelSlot;
|
|
mode: PanelMode;
|
|
/** For a composer panel: the composer it belongs to (from the plugin's click context or slot props). */
|
|
composerId?: string;
|
|
}
|
|
|
|
/** Answers with the panel's state after the request (true = open), or null when the request is not for it. */
|
|
export type PanelRequestHandler = (request: PanelRequest) => boolean | null;
|
|
|
|
const handlers = new Set<PanelRequestHandler>();
|
|
|
|
export function onPanelRequest(handler: PanelRequestHandler): () => void {
|
|
handlers.add(handler);
|
|
return () => { handlers.delete(handler); };
|
|
}
|
|
|
|
export function isPanelSlot(value: unknown): value is PanelSlot {
|
|
return typeof value === 'string' && (PANEL_SLOTS as readonly string[]).includes(value);
|
|
}
|
|
|
|
/**
|
|
* Check a plugin's request and deliver it: the panel's state afterwards, or
|
|
* null when no such panel is on screen. Throws with a message the plugin sees.
|
|
*/
|
|
export function requestPanel(pluginId: string, slot: unknown, mode: unknown, opts: unknown): boolean | null {
|
|
const method = `ui.${mode === 'open' ? 'openPanel' : mode === 'close' ? 'closePanel' : 'togglePanel'}`;
|
|
if (!isPanelSlot(slot)) throw new Error(`${method}: "${String(slot)}" is not a panel (${PANEL_SLOTS.join(', ')})`);
|
|
if (mode !== 'open' && mode !== 'close' && mode !== 'toggle') throw new Error(`${method}: unknown mode`);
|
|
let composerId: string | undefined;
|
|
if (opts !== undefined && opts !== null) {
|
|
if (typeof opts !== 'object') throw new Error(`${method}: options must be an object`);
|
|
const value = (opts as { composerId?: unknown }).composerId;
|
|
if (value !== undefined) {
|
|
if (typeof value !== 'string' || !value) throw new Error(`${method}: options.composerId must be a string`);
|
|
composerId = value;
|
|
}
|
|
}
|
|
const request: PanelRequest = { pluginId, slot, mode, ...(composerId ? { composerId } : {}) };
|
|
for (const handler of handlers) {
|
|
let answer: boolean | null = null;
|
|
try { answer = handler(request); } catch { answer = null; }
|
|
if (answer !== null) return answer;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Test helper. */
|
|
export function resetPanelRequests(): void {
|
|
handlers.clear();
|
|
}
|