/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Logan Byam and Gareth Johnson
*/
/**
* @module bajaux/commands/CommandGroup
*/
define([ 'lex!',
'Promise',
'bajaux/commands/Command',
'bajaux/events' ], function (
lex,
Promise,
Command,
events) {
"use strict";
var COMMANDGROUP_CHANGE_EVENT = events.command.GROUP_CHANGE_EVENT;
/**
* A CommandGroup is a container for Commands and other CommandGroups.
*
* @class
* @alias module:bajaux/commands/CommandGroup
* @param {object} params parameters - or just pass the `displayName` directly
* @param {String} params.displayName A format display name that will be used
* when `toDisplayName` is called.
* @param {String} [params.icon] The Command Group Icon.
* @param {Array.<module:bajaux/commands/CommandGroup|module:bajaux/commands/Command>} [params.commands] specify
* the initial set of children of this command group
*/
var CommandGroup = function CommandGroup(params) {
var that = this;
params = params && params.constructor === Object ? params : { displayName: params };
that.$kids = params.commands || [];
that.$displayName = params.displayName || "";
that.$flags = params.flags === undefined ? Command.flags.ALL : params.flags;
that.$jq = params.jq || null;
that.setIcon(params.icon || "");
};
/**
* Add a command to this group. Triggers a
* `bajaux:changecommandgroup` event. Multiple arguments can be specified to
* add many commands at once.
*
* @param {module:bajaux/commands/Command} command The command to add.
* @returns {module:bajaux/commands/CommandGroup} Return the CommandGroup instance.
*/
CommandGroup.prototype.add = function add(command) {
//TODO: support objects / arrays and convert to Commands/CommandGroups
var that = this,
jq = that.$jq,
visit = function (c) {
c.jq(jq);
},
kids = that.$kids,
i;
for (i = 0; i < arguments.length; ++i) {
if (!contains(kids, arguments[i])) {
kids.push(arguments[i]);
}
}
if (jq) {
for (i = 0; i < arguments.length; ++i) {
arguments[i].visit(visit);
}
}
that.trigger(COMMANDGROUP_CHANGE_EVENT);
return that;
};
/**
* Remove a command from this group. Triggers a
* `bajaux:changecommandgroup` event.
* Multiple arguments
* can be specified so multiple commands can be removed at once.
* @example
* // Passing an id/index (Number)
* commandGroup.remove(0); // Removes the 0 index kid from the group
* // Passing a command (module:bajaux/commands/Command)
* commandGroup.remove(command); // Removes the matching 'command' from the group
* // Passing multiple commands (module:bajaux/commands/Command)
* commandGroup.remove(command1, command2); // Removes the matching commands from the group
*
* @param {module:bajaux/commands/Command|Number} command The command or index
* of the Command to remove.
* @returns {module:bajaux/commands/CommandGroup} Return the CommandGroup instance.
*/
CommandGroup.prototype.remove = function remove(command) {
var that = this,
kids = that.$kids,
indicesToRemove = [],
i, j;
// Mark elements we want to remove
for (i = 0; i < arguments.length; ++i) {
if (typeof arguments[i] === "number" && kids[arguments[i]]) {
indicesToRemove.push(arguments[i]);
} else {
for (j = 0; j < kids.length; ++j) {
if (kids[j] === arguments[i]) {
indicesToRemove.push(j);
break;
}
// If the command is in a sub-group try there
if (!kids[j].isCommand()) {
kids[j].remove.apply(kids[j], arguments);
}
}
}
}
if (indicesToRemove.length) {
indicesToRemove.sort(function (a, b) { return a - b; });
// Remove marked elements
for (i = indicesToRemove.length; i >= 0; --i) {
if (kids[indicesToRemove[i]]) {
kids.splice(indicesToRemove[i], 1);
}
}
that.trigger(COMMANDGROUP_CHANGE_EVENT);
}
return that;
};
/**
* Remove all children of this command group that match the input flag,
* optionally emptying out child groups as well. Triggers a
* `bajaux:changecommandgroup` event.
*
* @param {Object} options An options object
* @param {Number} [options.flags] Flags to use to filter commands. Only
* children that match one of these flags will be removed.
* If omitted, defaults to `Command.flags.ALL` meaning all
* children will be removed.
* @param {Boolean} [options.recurse] `true` if should also empty out any
* sub-groups.
*
* @example
* cmdGroup.removeAll();
* cmdGroup.removeAll(Command.flags.MENU_BAR);
* cmdGroup.removeAll({ flags: Command.flags.MENU_BAR, recurse: true });
*/
CommandGroup.prototype.removeAll = function removeAll(options) {
options = options && options.constructor === Object ? options : { flags: options };
var that = this,
kids = that.$kids,
flags = options.flags === undefined ? Command.flags.ALL : options.flags,
recurse = options.recurse,
kid,
i = 0;
while (i < kids.length) {
kid = kids[i];
if (recurse && !kid.isCommand()) {
kid.removeAll(options);
}
if (kid.hasFlags(flags)) {
kids.splice(i, 1);
} else {
i++;
}
}
that.trigger(COMMANDGROUP_CHANGE_EVENT);
};
/**
* Sorts this command group. Typically will not be overridden; override
* `doSort` instead. Triggers a `bajaux:changecommandgroup` event.
*
* @returns {Promise} A promise that will be invoked once the CommandGroup
* has been sorted.
*/
CommandGroup.prototype.sort = function sort() {
var that = this;
return Promise.resolve(that.doSort(that.$kids.slice()))
.then(function (sorted) {
that.$kids = sorted;
that.trigger(COMMANDGROUP_CHANGE_EVENT);
return sorted;
});
};
/**
* Does the work of sorting this command group. By default, does nothing.
*
* When overriding this method, be sure to return the sorted array as a
* parameter to `deferred.resolve`.
*
* @param {Array} kids An array of the command group's children - this may
* include commands or sub-CommandGroups.
* @returns {Array|Promise} an array containing the sorted children, or
* a promise to be resolved with same
*/
CommandGroup.prototype.doSort = function doSort(kids) {
return kids;
};
/**
* Return a listing of the commands (not command groups) contained within
* this group. Can optionally recursively visit down through any
* sub-CommandGroups contained in this group (depth first) to return all
* commands. Can optionally filter commands by flags as well.
*
* @param {Object} options An options object.
* @param {Number} [options.flags] flags Used to filter commands. If omitted,
* includes all commands regardless of flags.
* @param {Boolean} [options.recurse] `true` if should recurse down through any
* sub-groups.
* @returns {Array} An array of Commands.
*
* @example
* cmdGroup.flatten(); // all child commands
* cmdGroup.flatten(Command.flags.MENU); // only children with MENU flag
* cmdGroup.flatten({ flags: Command.flags.MENU, recurse: true });
*/
CommandGroup.prototype.flatten = function flatten(options) {
options = options && options.constructor === Object ? options : { flags: options };
var flags = options.flags,
recurse = options.recurse,
commands = [];
this.$kids.forEach(function (kid) {
if (kid instanceof Command && flagsMatch(kid, flags)) {
commands.push(kid);
} else if (kid instanceof CommandGroup && recurse && flagsMatch(kid, flags)) {
commands = commands.concat(kid.flatten(options));
}
});
return commands;
};
function flagsMatch(command, flags) {
if (typeof flags === 'number') {
return command.hasFlags(flags);
}
return true;
}
/**
* Return promise that will be resolved once all the Commands in this
* group have been loaded.
*
* @returns {Promise} A promise that will be resolved once all of the
* child Commands have loaded.
*/
CommandGroup.prototype.loading = function loading() {
var promises = this.$kids.map(function (kid) { return kid.loading(); });
return Promise.all(promises);
};
/**
* @returns {boolean} true if any of the Commands are still loading.
*/
CommandGroup.prototype.isLoading = function isLoading() {
var kids = this.$kids,
i;
for (i = 0; i < kids.length; ++i) {
if (kids[i].isLoading()) {
return true;
}
}
return false;
};
/**
* Filters the commands in this command group based on the results of
* a test function, and returns a new, filtered CommandGroup instance.
*
* @param {Object} options An options object literal
* @param {Function} [options.include] A function that will receive a
* command object and return true or false depending on whether to include
* that command in the filtered results. If omitted, defaults to always
* return true.
* @param {Number} [options.flags] A number containing flag bits to test on
* each command. If omitted, defaults to Command.flags.ALL.
* @returns {module:bajaux/commands/CommandGroup} A filtered command group.
*/
CommandGroup.prototype.filter = function filter(options) {
options = options && options.constructor === Object ? options : { include: options };
var that = this,
newKids = [],
include = options.include === undefined ? function () { return true; } : options.include,
flags = options.flags === undefined ? Command.flags.ALL : options.flags,
newGroup = new CommandGroup(that.$displayName);
newGroup.jq(that.$jq);
that.$kids.forEach(function (kid) {
if (kid instanceof Command && kid.hasFlags(flags) && include(kid)) {
newKids.push(kid);
} else if (kid instanceof CommandGroup && kid.hasFlags(flags)) {
newKids.push(kid.filter(options));
}
});
newGroup.$kids = newKids;
newGroup.$flags = that.$flags;
return newGroup;
};
/**
* Visit through all of the Commands and CommandGroups.
*
* The function passed it will be called for every Command and CommandGroup
* found (including this CommandGroup).
*
* @param func Called for every CommandGroup and Command found. Each
* Command/CommandGroup is passed in as the first argument to the function.
* If the function returns a false value then iteration stops.
*/
CommandGroup.prototype.visit = function visit(func) {
if (func(this) === false) {
return false;
}
this.$kids.forEach(function (kid) {
return kid.visit(func);
});
};
/**
* Find a Command in this CommandGroup (or sub CommandGroup) via its id and return it.
*
* @param {Number|Function} id an id, filter function, or Command constructor. A filter
* function receives a Command instance and returns a boolean result - the first Command passing
* the filter will be returned. If passing a Command constructor, the first Command that is an
* instance of that constructor will be returned.
*
* @example
* group.findCommand(DeleteCommand); // returns the first instance of DeleteCommand in the group if present.
*
* @returns {module:bajaux/commands/Command|null} return the found Command (or null if nothing found).
*/
CommandGroup.prototype.findCommand = function findCommand(id) {
var c = null;
const isFunction = typeof id === 'function';
if (isFunction && id.prototype instanceof Command) {
const ctor = id;
id = (cmd) => cmd instanceof ctor;
}
function filterFunc(cmd) {
var result = isFunction ? id(cmd) : cmd.isCommand() && cmd.getId() === id;
if (result) {
c = cmd;
return false;
}
}
this.visit(filterFunc);
return c;
};
/**
* Merges an input command group with this one, and returns a new
* command group with the merged results.
*
* @param {module:bajaux/commands/CommandGroup} group The group to merge with
* this one
* @param {object} [params]
* @param {boolean} [params.mergeCommands=true] set to false to cause commands
* to be merged by a simple instanceof check, and all child commands of this
* group will be included; otherwise, only commands whose `merge()` function
* returns a new Command will be included
* @returns {module:bajaux/commands/CommandGroup|null} the merged group, or
* null if the merge could not be completed
*/
CommandGroup.prototype.merge = function merge(group, params) {
if (!(group instanceof CommandGroup)) { return null; }
var that = this,
mergeCommands = (params && params.mergeCommands) !== false,
newGroup = new CommandGroup(that.$displayName),
newKids,
otherKids = group.$kids;
if (mergeCommands) {
newKids = [];
that.$kids.forEach(function (myKid) {
for (var i = 0; i < otherKids.length; ++i) {
var merged = myKid.merge(otherKids[i]);
if (merged) {
return newKids.push(merged);
}
}
});
if (!newKids.length) {
return null;
}
} else {
newKids = that.$kids.slice();
otherKids.forEach(function (otherKid) {
if (!contains(newKids, otherKid)) {
newKids.push(otherKid);
}
});
}
newGroup.$kids = newKids;
newGroup.$flags = that.$flags;
newGroup.visit(function (c) {
c.jq(that.$jq);
});
return newGroup;
};
/**
* Returns a defensive copy of this group's array of children.
*
* @returns {Array}
*/
CommandGroup.prototype.getChildren = function getChildren() {
return this.$kids.slice();
};
/**
* Sets this group's array of commands/groups wholesale. Triggers a
* `bajaux:changecommandgroup` event.
*
* @param {Array} children
*/
CommandGroup.prototype.setChildren = function setChildren(children) {
var that = this;
that.$kids = children;
that.visit(function (c) {
c.jq(that.$jq);
});
that.trigger(COMMANDGROUP_CHANGE_EVENT);
};
/**
* Return a Command/CommandGroup based upon the index or
* null if nothing can be found.
*
* @param {Number} index
* @returns {module:bajaux/commands/Command|module:bajaux/commands/CommandGroup|null}
*/
CommandGroup.prototype.get = function get(index) {
return this.$kids[index] || null;
};
/**
* Return the number of children the CommandGroup has
* (this covers both Commands and CommandGroups).
*
* @returns {Number}
*/
CommandGroup.prototype.size = function size() {
return this.$kids.length;
};
/**
* Return true if this CommandGroup doesn't contain any children.
*
* @returns true if empty.
*/
CommandGroup.prototype.isEmpty = function isEmpty() {
return this.$kids.length === 0;
};
/**
* Return the format display name of this command group.
*
* @returns {String}
*/
CommandGroup.prototype.getDisplayNameFormat = function getDisplayNameFormat() {
return this.$displayName;
};
/**
* Set the display name format of the command group. Triggers a
* `bajaux:changecommandgroup` event.
*
* @param {String} displayName display name - supports baja Format syntax
*/
CommandGroup.prototype.setDisplayNameFormat = function setDisplayNameFormat(displayName) {
this.$displayName = displayName;
this.trigger(COMMANDGROUP_CHANGE_EVENT);
};
/**
* Formats the command's display name.
*
* @returns {Promise.<String>} Promise to be resolved with the display name
*/
CommandGroup.prototype.toDisplayName = function toDisplayName() {
var that = this;
return lex.format(that.$displayName)
.then(function (s) {
// If the format doesn't resolve then use the default Commands display name
return s || lex.format("%lexicon(bajaux:commands)%");
});
};
/**
* Returns this group's flags.
*
* @returns {Number}
*/
CommandGroup.prototype.getFlags = function getFlags() {
return this.$flags;
};
/**
* Sets this group's flags. Triggers a `bajaux:changecommandgroup` event.
*
* @param {Number} flags
*/
CommandGroup.prototype.setFlags = function setFlags(flags) {
this.$flags = flags;
this.trigger(COMMANDGROUP_CHANGE_EVENT);
};
/**
* Check to see if this group's flags match any of the bits of the
* input flags.
*
* @param {Number} flags The flags to check against
*/
CommandGroup.prototype.hasFlags = function hasFlags(flags) {
return (this.$flags & flags) > 0;
};
/**
* Always returns false.
*/
CommandGroup.prototype.isCommand = function isCommand() {
return false;
};
/**
* Always returns false.
*/
CommandGroup.prototype.isToggleCommand = function isToggleCommand() {
return false;
};
/**
* Triggers an event from this CommandGroup.
*
* @function
* @param {String} name
*/
CommandGroup.prototype.trigger = Command.prototype.trigger;
/**
* 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 CommandGroup.
*
* @function
* @param {JQuery} [jqDom] If specified, this will set the jQuery DOM.
* @returns {JQuery} A jQuery DOM object for firing events on.
*/
CommandGroup.prototype.jq = Command.prototype.jq;
/**
* 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.
* @since Niagara 4.12
*/
CommandGroup.prototype.on = Command.prototype.on;
/**
* 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.
* @since Niagara 4.12
*/
CommandGroup.prototype.off = Command.prototype.off;
/**
* Return the CommandGroup's icon URI
* @since Niagara 4.12
*
* @returns {String}
*/
CommandGroup.prototype.getIcon = function getIcon() {
return this.$icon;
};
/**
* Sets the icon for this CommandGroup. Triggers a `bajaux:changecommandgroup` event.
*
* @param {String} icon The CommandButton's icon (either a URI or a module:// ORD string)
* @since Niagara 4.12
*/
CommandGroup.prototype.setIcon = function setIcon(icon) {
this.$icon = icon.replace(/^module:\/\//, "/module/");
this.trigger(COMMANDGROUP_CHANGE_EVENT);
};
function contains(arr, obj) {
for (var i = 0; i < arr.length; ++i) {
if (arr[i] === obj) { return true; }
}
}
return CommandGroup;
});