Add UI button registration API (registerButton, unregisterButton, getButtonsForLocation)
This commit is contained in:
141
dist/PluginRegistry.js
vendored
141
dist/PluginRegistry.js
vendored
@@ -11,6 +11,7 @@ export class PluginRegistry {
|
||||
this.beforeSendInterceptors = [];
|
||||
this.receiveInterceptors = [];
|
||||
this.renderers = new Map();
|
||||
this.buttons = new Map();
|
||||
this.settingsSchemas = new Map();
|
||||
this.pluginSettings = new Map();
|
||||
this.matrixHandlers = new Map();
|
||||
@@ -57,6 +58,10 @@ export class PluginRegistry {
|
||||
if (renderer.pluginId === id)
|
||||
this.renderers.delete(type);
|
||||
}
|
||||
for (const [buttonId, btn] of this.buttons.entries()) {
|
||||
if (btn.pluginId === id)
|
||||
this.buttons.delete(buttonId);
|
||||
}
|
||||
for (const [themeId, theme] of this.themes.entries()) {
|
||||
if (theme.pluginId === id) {
|
||||
this.onThemeUnregistered?.(themeId);
|
||||
@@ -93,11 +98,14 @@ export class PluginRegistry {
|
||||
this.commands.delete(name);
|
||||
}
|
||||
/** Execute a command by name, passing raw argument string. */
|
||||
async executeCommand(name, rawArgs) {
|
||||
async executeCommand(name, rawArgs, roomId) {
|
||||
const cmd = this.commands.get(name);
|
||||
if (!cmd)
|
||||
throw new Error(`Command /${name} not found`);
|
||||
const args = this.parseCommandArgs(cmd.command, rawArgs);
|
||||
if (roomId) {
|
||||
args.roomId = roomId;
|
||||
}
|
||||
const entry = this.plugins.get(cmd.pluginId);
|
||||
if (!entry)
|
||||
throw new Error(`Plugin ${cmd.pluginId} not loaded`);
|
||||
@@ -106,11 +114,25 @@ export class PluginRegistry {
|
||||
parseCommandArgs(command, rawArgs) {
|
||||
if (!command.args || command.args.length === 0)
|
||||
return {};
|
||||
const parts = rawArgs.trim().split(/\s+/);
|
||||
const trimmedArgs = rawArgs.trim();
|
||||
if (!trimmedArgs)
|
||||
return {};
|
||||
const argNames = command.args.map(arg => typeof arg === 'string' ? arg : arg.name);
|
||||
// If only one arg (that's not roomId), give it all the text
|
||||
const nonRoomIdArgs = argNames.filter(name => name !== 'roomId');
|
||||
if (nonRoomIdArgs.length === 1) {
|
||||
const result = {};
|
||||
result[nonRoomIdArgs[0]] = trimmedArgs;
|
||||
return result;
|
||||
}
|
||||
// Otherwise, split by whitespace
|
||||
const parts = trimmedArgs.split(/\s+/);
|
||||
const result = {};
|
||||
command.args.forEach((arg, index) => {
|
||||
const argName = typeof arg === 'string' ? arg : arg.name;
|
||||
result[argName] = parts[index];
|
||||
if (argName !== 'roomId') {
|
||||
result[argName] = parts[index];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -171,6 +193,119 @@ export class PluginRegistry {
|
||||
return this.renderers.get(type)?.renderer;
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI Buttons
|
||||
// ---------------------------------------------------------------------------
|
||||
/** Register a UI button contributed by a plugin. */
|
||||
registerButton(pluginId, button) {
|
||||
const fullId = `${pluginId}:${button.id}`;
|
||||
if (this.buttons.has(fullId)) {
|
||||
console.warn(`[PluginRegistry] Button ${fullId} already registered`);
|
||||
return;
|
||||
}
|
||||
this.buttons.set(fullId, { pluginId, button: { ...button, id: fullId } });
|
||||
}
|
||||
/** Remove a registered button by ID. */
|
||||
unregisterButton(pluginId, buttonId) {
|
||||
const fullId = `${pluginId}:${buttonId}`;
|
||||
this.buttons.delete(fullId);
|
||||
}
|
||||
/**
|
||||
* Get all buttons for a specific UI location, sorted by position configuration.
|
||||
* Returns buttons in the order they should be rendered.
|
||||
*/
|
||||
getButtonsForLocation(location) {
|
||||
const buttons = Array.from(this.buttons.values())
|
||||
.filter(({ button }) => button.location === location)
|
||||
.map(({ button }) => button);
|
||||
return this.sortButtonsByPosition(buttons);
|
||||
}
|
||||
/**
|
||||
* Sort buttons according to their position configuration.
|
||||
* - Buttons with position.before/after are positioned relative to existing buttons
|
||||
* - Buttons in groups are kept together
|
||||
* - Within groups, buttons are sorted by order property
|
||||
*/
|
||||
sortButtonsByPosition(buttons) {
|
||||
if (buttons.length === 0)
|
||||
return [];
|
||||
const grouped = new Map();
|
||||
const ungrouped = [];
|
||||
const positionedButtons = new Map();
|
||||
for (const button of buttons) {
|
||||
positionedButtons.set(button.id, button);
|
||||
if (button.position?.group) {
|
||||
if (!grouped.has(button.position.group)) {
|
||||
grouped.set(button.position.group, []);
|
||||
}
|
||||
grouped.get(button.position.group).push(button);
|
||||
}
|
||||
else {
|
||||
ungrouped.push(button);
|
||||
}
|
||||
}
|
||||
for (const [_group, groupButtons] of grouped) {
|
||||
groupButtons.sort((a, b) => {
|
||||
const orderA = a.position?.order ?? 0;
|
||||
const orderB = b.position?.order ?? 0;
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
const result = [];
|
||||
const inserted = new Set();
|
||||
const insertButton = (button) => {
|
||||
if (inserted.has(button.id))
|
||||
return;
|
||||
const group = button.position?.group ? grouped.get(button.position.group) : [button];
|
||||
if (!group)
|
||||
return;
|
||||
for (const btn of group) {
|
||||
if (!inserted.has(btn.id)) {
|
||||
result.push(btn);
|
||||
inserted.add(btn.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const button of [...ungrouped, ...Array.from(grouped.values()).flat()]) {
|
||||
if (inserted.has(button.id))
|
||||
continue;
|
||||
if (button.position?.before) {
|
||||
const beforeButton = positionedButtons.get(button.position.before);
|
||||
if (beforeButton) {
|
||||
const idx = result.findIndex(b => b.id === beforeButton.id);
|
||||
if (idx !== -1) {
|
||||
const group = button.position.group ? grouped.get(button.position.group) : [button];
|
||||
if (group) {
|
||||
for (let i = group.length - 1; i >= 0; i--) {
|
||||
if (!inserted.has(group[i].id)) {
|
||||
result.splice(idx, 0, group[i]);
|
||||
inserted.add(group[i].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (button.position?.after) {
|
||||
const afterButton = positionedButtons.get(button.position.after);
|
||||
if (afterButton) {
|
||||
const idx = result.findIndex(b => b.id === afterButton.id);
|
||||
if (idx !== -1) {
|
||||
insertButton(button);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
insertButton(button);
|
||||
}
|
||||
for (const button of buttons) {
|
||||
if (!inserted.has(button.id)) {
|
||||
result.push(button);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
/** Define the settings schema for a plugin and load persisted values. */
|
||||
|
||||
Reference in New Issue
Block a user