1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-18 09:35:20 +02:00
joplin/ReactNativeClient/lib/services/CommandService.ts

299 lines
7.9 KiB
TypeScript

const BaseService = require('lib/services/BaseService');
const eventManager = require('lib/eventManager');
export interface CommandRuntime {
execute(props:any):void
isEnabled?(props:any):boolean
mapStateToProps?(state:any):any
// Used for the (optional) toolbar button title
title?(props:any):string,
props?:any
}
export interface CommandDeclaration {
name: string
// Used for the menu item label, and toolbar button tooltip
label?():string,
iconName?: string,
// Same as `role` key in Electron MenuItem:
// https://www.electronjs.org/docs/api/menu-item#new-menuitemoptions
// Note that due to a bug in Electron, menu items with a role cannot
// be disabled.
role?: string,
}
export interface Command {
declaration: CommandDeclaration,
runtime?: CommandRuntime,
}
interface Commands {
[key:string]: Command;
}
interface ReduxStore {
dispatch(action:any):void;
getState():any;
}
interface Utils {
store: ReduxStore;
}
export const utils:Utils = {
store: {
dispatch: () => {},
getState: () => {},
},
};
interface CommandByNameOptions {
mustExist?:boolean,
runtimeMustBeRegistered?:boolean,
}
interface CommandState {
title: string,
enabled: boolean,
}
interface CommandStates {
[key:string]: CommandState
}
export default class CommandService extends BaseService {
private static instance_:CommandService;
static instance():CommandService {
if (this.instance_) return this.instance_;
this.instance_ = new CommandService();
return this.instance_;
}
private commands_:Commands = {};
private commandPreviousStates_:CommandStates = {};
private mapStateToPropsIID_:any = null;
initialize(store:any) {
utils.store = store;
}
public on(eventName:string, callback:Function) {
eventManager.on(eventName, callback);
}
public off(eventName:string, callback:Function) {
eventManager.off(eventName, callback);
}
private propsHaveChanged(previous:any, next:any) {
if (!previous && next) return true;
for (const n in previous) {
if (previous[n] !== next[n]) return true;
}
return false;
}
scheduleMapStateToProps(state:any) {
if (this.mapStateToPropsIID_) clearTimeout(this.mapStateToPropsIID_);
this.mapStateToPropsIID_ = setTimeout(() => {
this.mapStateToProps(state);
}, 50);
}
private mapStateToProps(state:any) {
const newState = state;
const changedCommands:any = {};
for (const name in this.commands_) {
const command = this.commands_[name];
if (!command.runtime) continue;
if (!command.runtime.mapStateToProps) {
command.runtime.props = {};
continue;
}
const newProps = command.runtime.mapStateToProps(state);
const haveChanged = this.propsHaveChanged(command.runtime.props, newProps);
if (haveChanged) {
const previousState = this.commandPreviousStates_[name];
command.runtime.props = newProps;
const newState:CommandState = {
enabled: this.isEnabled(name),
title: this.title(name),
};
if (!previousState || previousState.title !== newState.title || previousState.enabled !== newState.enabled) {
changedCommands[name] = newState;
}
this.commandPreviousStates_[name] = newState;
}
}
if (Object.keys(changedCommands).length) {
eventManager.emit('commandsEnabledStateChange', { commands: changedCommands });
}
return newState;
}
private commandByName(name:string, options:CommandByNameOptions = null):Command {
options = {
mustExist: true,
runtimeMustBeRegistered: false,
};
const command = this.commands_[name];
if (!command) {
if (options.mustExist) throw new Error(`Command not found: ${name}. Make sure the declaration has been registered.`);
return null;
}
if (options.runtimeMustBeRegistered && !command.runtime) throw new Error(`Runtime is not registered for command ${name}`);
return command;
}
registerDeclaration(declaration:CommandDeclaration) {
// if (this.commands_[declaration.name]) throw new Error(`There is already a command with name ${declaration.name}`);
declaration = { ...declaration };
if (!declaration.label) declaration.label = () => '';
if (!declaration.iconName) declaration.iconName = '';
// In TypeScript it's not an issue, but in JavaScript it's easy to accidentally set the label
// to a string instead of a function, and it will cause strange errors that are hard to debug.
// So here check early that we have the right type.
if (typeof declaration.label !== 'function') throw new Error(`declaration.label must be a function: ${declaration.name}`);
this.commands_[declaration.name] = {
declaration: declaration,
};
delete this.commandPreviousStates_[declaration.name];
}
registerRuntime(commandName:string, runtime:CommandRuntime) {
if (typeof commandName !== 'string') throw new Error(`Command name must be a string. Got: ${JSON.stringify(commandName)}`);
const command = this.commandByName(commandName);
runtime = Object.assign({}, runtime);
if (!runtime.isEnabled) runtime.isEnabled = () => true;
if (!runtime.title) runtime.title = () => null;
command.runtime = runtime;
delete this.commandPreviousStates_[commandName];
}
componentRegisterCommands(component:any, commands:any[]) {
for (const command of commands) {
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(component));
}
}
componentUnregisterCommands(commands:any[]) {
for (const command of commands) {
CommandService.instance().unregisterRuntime(command.declaration.name);
}
}
unregisterRuntime(commandName:string) {
const command = this.commandByName(commandName, { mustExist: false });
if (!command || !command.runtime) return;
delete command.runtime;
delete this.commandPreviousStates_[commandName];
}
execute(commandName:string, args:any = null) {
console.info('CommandService::execute:', commandName, args);
const command = this.commandByName(commandName);
command.runtime.execute(args ? args : {});
}
scheduleExecute(commandName:string, args:any = null) {
setTimeout(() => {
this.execute(commandName, args);
}, 10);
}
isEnabled(commandName:string):boolean {
const command = this.commandByName(commandName);
if (!command || !command.runtime) return false;
if (!command.runtime.props) return false;
return command.runtime.isEnabled(command.runtime.props);
}
title(commandName:string):string {
const command = this.commandByName(commandName);
if (!command || !command.runtime || !command.runtime.props) return null;
return command.runtime.title(command.runtime.props);
}
private extractExecuteArgs(command:Command, executeArgs:any) {
if (executeArgs) return executeArgs;
if (!command.runtime) throw new Error(`Command: ${command.declaration.name}: Runtime is not defined - make sure it has been registered.`);
if (command.runtime.props) return command.runtime.props;
return {};
}
commandToToolbarButton(commandName:string, executeArgs:any = null) {
const command = this.commandByName(commandName, { runtimeMustBeRegistered: true });
return {
tooltip: command.declaration.label(),
iconName: command.declaration.iconName,
enabled: this.isEnabled(commandName),
onClick: () => {
this.execute(commandName, this.extractExecuteArgs(command, executeArgs));
},
title: this.title(commandName),
};
}
commandToMenuItem(commandName:string, accelerator:string = null, executeArgs:any = null) {
const command = this.commandByName(commandName);
const item:any = {
id: command.declaration.name,
label: command.declaration.label(),
click: () => {
this.execute(commandName, this.extractExecuteArgs(command, executeArgs));
},
};
if (accelerator) item.accelerator = accelerator;
if (command.declaration.role) item.role = command.declaration.role;
return item;
}
commandsEnabledState(previousState:any = null):any {
const output:any = {};
for (const name in this.commands_) {
const enabled = this.isEnabled(name);
if (!previousState || previousState[name] !== enabled) {
output[name] = enabled;
}
}
return output;
}
}