/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* @module bajaux/commands/Command
*/
define([ 'lex!',
'jquery',
'Promise',
'bajaux/events' ], function (
lex,
$,
Promise,
events) {
'use strict';
var idCounter = 0,
COMMAND_CHANGE_EVENT = events.command.CHANGE_EVENT,
COMMAND_INVOKE_EVENT = events.command.INVOKE_EVENT,
COMMAND_FAIL_EVENT = events.command.FAIL_EVENT,
noop = function () {},
defaultToggleFunc = function () {
// By default, a toggle command just toggles its state.
this.toggle();
};
function parseAccelerator(acc) {
var a = null,
codes, c, i, res;
if (acc) {
if (acc && typeof acc === "object") {
a = acc;
} else {
a = {
keyCode: acc.toLowerCase()
};
}
// If we have a String then convert the character code.
if (typeof a.keyCode === "string") {
codes = a.keyCode.split("+");
for (i = 0; i < codes.length; ++i) {
c = codes[i];
if (c.length === 1) {
// Process keycode
a.keyCode = c.charCodeAt(0);
} else {
// Process modifiers
if (c === "alt") {
a.alt = true;
} else if (c === "shift") {
a.shift = true;
} else if (c === "ctrl") {
a.ctrl = true;
} else if (c === "meta") {
a.meta = true;
} else if (c === "backspace") { // Process other key codes
a.keyCode = 8;
} else if (c === "tab") {
a.keyCode = 9;
} else if (c === "space") {
a.keyCode = 32;
} else if (c === "pageup") {
a.keyCode = 33;
} else if (c === "pagedown") {
a.keyCode = 34;
} else if (c === "left") {
a.keyCode = 37;
} else if (c === "up") {
a.keyCode = 38;
} else if (c === "right") {
a.keyCode = 39;
} else if (c === "down") {
a.keyCode = 40;
} else if (c === "delete") {
a.keyCode = 46; // 127 in BAccelerator
} else if (c === "end") {
a.keyCode = 35;
} else if (c === "esc") {
a.keyCode = 27;
} else if (c === "home") {
a.keyCode = 36;
} else if (c === "insert") {
a.keyCode = 45; // 155 in BAccelerator;
} else if (c.charAt(0) === "f") {
// Process function keys
res = /f([1-9]+)/.exec(c);
if (res) {
a.keyCode = 112 + (parseInt(res[1], 10) - 1);
}
}
}
}
}
}
return a;
}
/**
* A Command is essentially an asynchronous function with a nicely formatted
* display name attached.
*
* @class
* @alias module:bajaux/commands/Command
*
* @param {String|Object} params An Object Literal or the display name for
* the Command.
*
* @param {String} params.displayName The display name format
* for the Command. This format will be used in
* `toDisplayName`. This can also be the first argument, in which
* case `func` must be the second.
*
* @param {Function} params.func The function this command will execute. The
* function can return a promise if it's going to be invoked asynchronously.
*
* @param {String} [params.description] A description of the
* Command. This format will be used in `toDescription`.
*
* @param {Boolean} [params.enabled] The enabled state of the Command.
* Defaults to `true`.
*
* @param {Number} [params.flags] The Command flags. Defaults to
* `Command.flags.ALL`.
*
* @param {String} [params.icon] The Command Icon. This can also be a
* String encoding an icon will be created from.
*
* @param {Function} [func] the function to invoke, if using the two-argument
* constructor
*
* @example
* new Command("baja.Format compatible display name", function () {
* alert("I'm a command!");
* });
*
* new Command("Format compatible display name", function () {
* return new Promise(function (resolve, reject) {
* setTimeout(function () {
* wobble.foo();
* resolve;
* }, 1000);
* });
* });
*
* new Command({
* displayName: "I'll be converted to a Format: %lexicon(baja:january)%",
* description: "I'll be converted to a Format too: %lexicon(baja:true)%",
* func: function () {
* alert("I'm a command!");
* }
* });
*
* new Command({
* module: "myModule", // Create a Command that gets its displayName, description
* lex: "myCommand", // and icon from a module's lexicon.
* func: function () {
* alert("I'm a command!");
* }
* });
*/
var Command = function Command(params, func) {
var that = this,
loadingPromise;
params = params && params.constructor === Object ? params
: { displayName: params, func: func };
// Assign variables
that.$displayName = params.displayName || "";
that.$description = params.description || "";
that.$func = params.func || (that.isToggleCommand() ? defaultToggleFunc : noop);
that.$enabled = params.enabled === undefined ? true : params.enabled;
that.$flags = params.flags === undefined ? Command.flags.ALL : params.flags;
that.setIcon(params.icon || "");
that.$id = idCounter++;
that.$accelerator = parseAccelerator(params.accelerator);
that.$jq = params.jq || null;
// If a module and lexicon is specified then resolve the lexicon
// and get the value. This asynchronously updates the Command. When
// the Command is updated, an change event will be fired to update
// any user interfaces.
var pending = true;
if (params.module && params.lex) {
// Always update asynchronously
loadingPromise = lex.module(params.module)
.then(function (moduleLex) {
var res;
try {
res = moduleLex.get(params.lex + ".displayName");
if (res) {
that.setDisplayNameFormat(res);
}
res = moduleLex.get(params.lex + ".description");
if (res) {
that.setDescriptionFormat(res);
}
res = moduleLex.get(params.lex + ".icon");
if (res) {
that.setIcon(res);
}
res = moduleLex.get(params.lex + ".accelerator");
if (res) {
that.setAccelerator(res);
}
} catch (ignore) {
} finally {
pending = false;
}
});
} else {
pending = false;
loadingPromise = Promise.resolve();
}
loadingPromise.isPending = () => pending;
that.$loading = loadingPromise;
};
/**
* Namespace containing numbers for comparing flag bits. C&P'ed directly
* from MgrController in workbench module.
*/
Command.flags = {};
/** Match all flags. */
Command.flags.ALL = 0xFFFF;
/** Makes the command be available in the main menu. */
Command.flags.MENU_BAR = 0x0001;
/** Makes the command be available in the main toolbar. */
Command.flags.TOOL_BAR = 0x0002;
/** Match no flags */
Command.flags.NONE = 0x0000;
/**
* Return true if the Command is still loading.
*
* @returns {Boolean} true if still loading.
*/
Command.prototype.isLoading = function isLoading() {
return this.$loading.isPending();
};
/**
* Return the loading promise for the Command.
*
* The returned promise will be resolved once the Command
* has finished loading.
*
* @returns {Promise} The promise used for loading a Command.
*/
Command.prototype.loading = function loading() {
return this.$loading;
};
/**
* Return the format display name of the command.
*
* @returns {String}
*/
Command.prototype.getDisplayNameFormat = function getDisplayNameFormat() {
return this.$displayName;
};
/**
* Set the display name format of the command. Triggers a
* `bajaux:changecommand` event.
*
* @param {String} displayName display name - supports baja Format syntax
*/
Command.prototype.setDisplayNameFormat = function setDisplayNameFormat(displayName) {
this.$displayName = displayName;
this.trigger(COMMAND_CHANGE_EVENT);
};
/**
* Access the Command's display name.
*
* In order to access the display name, a promise will be returned
* that will be resolved once the command has been loaded and
* the display name has been found.
*
* @returns {Promise} Promise to be resolved with the display name
*/
Command.prototype.toDisplayName = function toDisplayName() {
var that = this;
return that.$loading
.then(function () { return lex.format(that.$displayName); });
};
/**
* Get the unformatted description of the command.
*
* @returns {String}
*/
Command.prototype.getDescriptionFormat = function getDescriptionFormat() {
return this.$description;
};
/**
* Set the description format of the command. Triggers a
* `bajaux:changecommand` event.
*
* @param {String} description the command description - supports baja Format
* syntax
*/
Command.prototype.setDescriptionFormat = function setDescriptionFormat(description) {
this.$description = description;
this.trigger(COMMAND_CHANGE_EVENT);
};
/**
* Access the Command's description.
*
* In order to access the description, a promise will be returned
* that will be resolved once the command has been loaded and
* the description has been found.
*
* @returns {Promise} Promise to be resolved with the description
*/
Command.prototype.toDescription = function toDescription() {
var that = this;
return that.$loading
.then(function () { return lex.format(that.$description); });
};
/**
* Return the Command's icon URI
*
* @returns {String}
*/
Command.prototype.getIcon = function getIcon() {
return this.$icon;
};
/**
* Sets the icon for this Command. Triggers a `bajaux:changecommand` event.
*
* @param {String} icon The Command's icon (either a URI or a module:// ORD string)
*/
Command.prototype.setIcon = function setIcon(icon) {
this.$icon = icon.replace(/^module:\/\//, "/module/");
this.trigger(COMMAND_CHANGE_EVENT);
};
/**
* Gets this command's enabled status.
*
* @returns {Boolean}
*/
Command.prototype.isEnabled = function isEnabled() {
return this.$enabled;
};
/**
* Sets this command's enabled status. Triggers a
* `bajaux:changecommand` event.
*
* @param {Boolean} enabled
*/
Command.prototype.setEnabled = function setEnabled(enabled) {
this.$enabled = enabled;
this.trigger(COMMAND_CHANGE_EVENT);
};
/**
* Get this command's flags.
*
* @returns {Number}
*/
Command.prototype.getFlags = function getFlags() {
return this.$flags;
};
/**
* Set this command's flags.
*
* @param {Number} flags
*/
Command.prototype.setFlags = function setFlags(flags) {
this.$flags = flags;
this.trigger(COMMAND_CHANGE_EVENT);
};
/**
* Check to see if this command's flags match any of the bits of the
* input flags.
*
* @param {Number} flags The flags to check against
* @returns {Boolean}
*/
Command.prototype.hasFlags = function hasFlags(flags) {
return !!(this.$flags & flags);
};
/**
* Return the raw function associated with this command.
*
* @returns {Function}
*/
Command.prototype.getFunction = function getFunction() {
return this.$func;
};
/**
* Set the Command's function handler.
*
* @param {Function} func The new function handler for the command.
*/
Command.prototype.setFunction = function setFunction(func) {
this.$func = func;
this.trigger(COMMAND_CHANGE_EVENT);
};
/**
* Invoke the Command. Triggers a `bajaux:invokecommand` or
* `bajaux:failcommand` event, as appropriate.
*
* Arguments can be passed into `invoke()` that will be passed into the
* function's Command Handler.
*
* @returns {Promise} A promise object that will be resolved (or rejected)
* once the Command's function handler has finished invoking.
*/
Command.prototype.invoke = function invoke() {
var that = this,
args = arguments;
// eslint-disable-next-line promise/avoid-new
return Promise.try(function () {
return that.$func.apply(that, args);
})
.then(function (result) {
var args = Array.prototype.slice.call(arguments);
that.trigger.apply(that, [ COMMAND_INVOKE_EVENT ].concat(args));
return result;
}, function (err) {
var args = Array.prototype.slice.call(arguments);
that.trigger.apply(that, [ COMMAND_FAIL_EVENT ].concat(args));
throw err;
});
};
/**
* Always returns true.
*/
Command.prototype.isCommand = function isCommand() {
return true;
};
/**
* Always returns false.
*/
Command.prototype.isToggleCommand = function isToggleCommand() {
return false;
};
/**
* Return a unique numerical id for the Command.
*
* This is id unique to every Command object created.
*/
Command.prototype.getId = function getId() {
return this.$id;
};
/**
* Return the accelerator for the Command or null if
* nothing is defined.
*
* @see module:bajaux/commands/Command#setAccelerator
*
* @returns {Object} The accelerator or null if nothing is defined.
*/
Command.prototype.getAccelerator = function getAccelerator() {
return this.$accelerator;
};
/**
* Set the accelerator information for the Command.
*
* @param {Object|String|Number|null|undefined} acc The accelerator keyboard information. This can
* be a keyCode number, a character (i.e. 'a') or an Object that contains the accelerator information.
* If no accelerator should be used the null/undefined should be specified.
* @param {String|Number} acc.keyCode The key code of the accelerator. This can be a character
* code number or a character (i.e. 'a').
* @param {Boolean} [acc.ctrl] `true` if the control key needs to be pressed.
* @param {Boolean} [acc.shift] `true` if the shift key needs to be pressed.
* @param {Boolean} [acc.alt] `true` if the alt key needs to be pressed.
* @param {Boolean} [acc.meta] `true` if a meta key needs to be pressed.
*
* @see module:bajaux/commands/Command#getAccelerator
*/
Command.prototype.setAccelerator = function setAccelerator(acc) {
this.$accelerator = parseAccelerator(acc);
// Trigger a change event
this.trigger(COMMAND_CHANGE_EVENT);
};
/**
* Visit this Command with the specified function.
*
* @param {Function} func Will be invoked with this
* Command passed in as an argument.
*/
Command.prototype.visit = function visit(func) {
return func(this);
};
/**
* Triggers an event from this Command.
*
* @param {String} name
*/
Command.prototype.trigger = function trigger(name) {
var that = this,
passedArgs = Array.prototype.slice.call(arguments, 1),
args = [ that ].concat(passedArgs);
if (that.$jq) {
that.$jq.trigger(name, args);
}
if (that.$eh) {
that.$eh.trigger(name, args);
}
};
/**
* If a jQuery DOM argument is specified, this will set the DOM.
* If not specified then no DOM will be set.
* This method will always return the jQuery DOM associated with this Command.
*
* @param {JQuery} [jqDom] If specified, this will set the jQuery DOM.
* @returns {JQuery} A jQuery DOM object for firing events on.
*/
Command.prototype.jq = function jq(jqDom) {
if (jqDom !== undefined) {
this.$jq = jqDom;
}
return this.$jq || null;
};
/**
* Register a function callback handler for the specified event.
*
* @param {String} event The event id to register the function for.
* @param {Function} handler The event handler to be called when the event is fired.
*/
Command.prototype.on = function (event, handler) {
var that = this;
that.$eh = that.$eh || $("<div></div>");
that.$eh.on.apply(that.$eh, arguments);
};
/**
* Unregister a function callback handler for the specified event.
*
* @param {String} [event] The name of the event to unregister.
* If name isn't specified, all events for the Command will be unregistered.
* @param {Function} [handler] The function to unregister. If
* not specified, all handlers for the event will be unregistered.
*/
Command.prototype.off = function (event, handler) {
var that = this;
if (that.$eh) {
that.$eh.off.apply(that.$eh, arguments);
}
};
/**
* Attempt to merge this command with another command, and return a new
* Command that does both tasks. If the two commands are mutually
* incompatible, return a falsy value.
*
* @param {module:bajaux/commands/Command} cmd
* @returns {module:bajaux/commands/Command}
*
* @example
* <caption>
* Here is an example to show the basic concept. Commands that simply
* add two numbers together can easily be merged together thanks to the
* associative property.
* </caption>
*
* var AddCommand = function AddCommand(inc) {
* this.$inc = inc;
* Command.call(this, {
* displayName: 'Add ' + inc + ' to the given number',
* func: function (num) { return num + inc; }
* });
* };
* AddCommand.prototype = Object.create(Command.prototype);
*
* AddCommand.prototype.merge = function (cmd) {
* if (cmd instanceof AddCommand) {
* return new AddCommand(this.$inc + cmd.$inc);
* }
* };
*
* var addOneCommand = new AddCommand(1),
* addFiveCommand = new AddCommand(5),
* addSixCommand = addOneCommand.merge(addFiveCommand);
* addSixCommand.invoke(10)
* .then(function (result) {
* console.log('is 16? ', result === 16);
* });
*/
Command.prototype.merge = function (cmd) {
return null;
};
return Command;
});