/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* Defines {@link baja.Component}.
* @module baja/comp/Component
*/
define([ "bajaPromises",
"bajaScript/nav",
"bajaScript/baja/comp/Complex",
"bajaScript/baja/comp/LinkCheck",
"bajaScript/baja/comp/DynamicProperty",
"bajaScript/baja/comp/Flags",
"bajaScript/baja/comp/PropertyAction",
"bajaScript/baja/comp/PropertyTopic",
"bajaScript/baja/comp/compUtil",
"bajaScript/baja/comm/Callback",
"bajaScript/baja/tag/ComponentTags",
"bajaScript/baja/tag/SmartTags",
"bajaScript/baja/tag/ComponentRelations",
"bajaScript/baja/tag/SmartRelations",
"lex!",
"nmodule/js/rc/asyncUtils/promiseMux" ], function (
Promise,
baja,
Complex,
LinkCheck,
DynamicProperty,
Flags,
PropertyAction,
PropertyTopic,
compUtil,
Callback,
ComponentTags,
SmartTags,
ComponentRelations,
SmartRelations,
lexjs,
promiseMux) {
"use strict";
var subclass = baja.subclass,
callSuper = baja.callSuper,
bajaError = baja.error,
bajaDef = baja.def,
strictArg = baja.strictArg,
objectify = baja.objectify,
setContextInOkCallback = compUtil.setContextInOkCallback,
setContextInFailCallback = compUtil.setContextInFailCallback,
unlease = compUtil.unlease,
// Internal Component Event flags
CHANGED = 0,
ADDED = 1,
REMOVED = 2,
RENAMED = 3,
REORDERED = 4,
//PARENTED = 5, //never used
//UNPARENTED = 6, //never used
//ACTION_INVOKED = 7, //never used
TOPIC_FIRED = 8,
FLAGS_CHANGED = 9,
FACETS_CHANGED = 10,
//RECATEGORIZED = 11, //never used
KNOB_ADDED = 12,
KNOB_REMOVED = 13,
SUBSCRIBED = 14,
UNSUBSCRIBED = 15,
RELATION_KNOB_ADDED = 16,
RELATION_KNOB_REMOVED = 17,
invalidActionArgErrMsg = "Invalid Action Argument: ";
const { HIDDEN, READONLY, OPERATOR } = Flags;
const DISPLAY_NAMES_FLAGS = HIDDEN | READONLY | OPERATOR;
/**
* Represents a `baja:Component` in BajaScript.
*
* `Component` is the required base class for all
* Baja component classes.
*
* Just like Niagara, `baja:Component` contains a lot of the core
* functionality of the framework. Unlike `baja:Struct`, a `Component` can
* contain both frozen and dynamic `Slot`s. Frozen `Slot`s are defined at
* compile time (typically hard coded in Java) and Dynamic `Slot`s can be
* added at runtime (i.e. when the Station is running). There are three
* different types of `Slot`s that a `Component` can contain (`Property`,
* `Action` and `Topic`).
*
* @see baja.Struct
* @see baja.Property
* @see baja.Action
* @see baja.Topic
*
* @class
* @alias baja.Component
* @extends baja.Complex
*/
var Component = function Component() {
callSuper(Component, this, arguments);
this.$space = null;
this.$handle = null;
this.$bPropsLoaded = false;
this.$subs = [];
this.$lease = false;
this.$leaseTicket = baja.clock.expiredTicket;
this.$knobs = null;
this.$permissionsStr = null;
this.$permissions = null;
this.$nc = true;
this.$muxed = null;
};
subclass(Component, Complex);
// This is a generic component event handling function
// that can route events to Component or Subscriber event handlers
function handleComponentEvent(component, handlers, id, slot, obj, str, cx) {
if (id === CHANGED) {
handlers.fireHandlers("changed", bajaError, component, slot, cx);
} else if (id === ADDED) {
handlers.fireHandlers("added", bajaError, component, slot, cx);
} else if (id === REMOVED) {
handlers.fireHandlers("removed", bajaError, component, slot, obj, cx);
} else if (id === RENAMED) {
handlers.fireHandlers("renamed", bajaError, component, slot, str, cx);
} else if (id === REORDERED) {
handlers.fireHandlers("reordered", bajaError, component, cx);
} else if (id === TOPIC_FIRED) {
handlers.fireHandlers("topicFired", bajaError, component, slot, obj, cx);
} else if (id === FLAGS_CHANGED) {
handlers.fireHandlers("flagsChanged", bajaError, component, slot, cx);
} else if (id === FACETS_CHANGED) {
handlers.fireHandlers("facetsChanged", bajaError, component, slot, cx);
} else if (id === SUBSCRIBED) {
handlers.fireHandlers("subscribed", bajaError, component, cx);
} else if (id === UNSUBSCRIBED) {
handlers.fireHandlers("unsubscribed", bajaError, component, cx);
} else if (id === KNOB_ADDED) {
handlers.fireHandlers("addKnob", bajaError, component, slot, obj, cx);
} else if (id === KNOB_REMOVED) {
handlers.fireHandlers("removeKnob", bajaError, component, slot, obj, cx);
} else if (id === RELATION_KNOB_ADDED) {
handlers.fireHandlers("addRelationKnob", bajaError, component, obj, cx);
} else if (id === RELATION_KNOB_REMOVED) {
handlers.fireHandlers("removeRelationKnob", bajaError, component, obj, cx);
}
}
// Handle Component child events
function handleComponentChildEvent(component, handlers, id, str, cx) {
if (id === RENAMED) {
handlers.fireHandlers("componentRenamed", bajaError, component, str, cx);
} else if (id === FLAGS_CHANGED) {
handlers.fireHandlers("componentFlagsChanged", bajaError, component, cx);
} else if (id === FACETS_CHANGED) {
handlers.fireHandlers("componentFacetsChanged", bajaError, component, cx);
}
}
// Handler Reorder Component Child Events
function handleReorderComponentChildEvent(component, handlers, cx) {
handlers.fireHandlers("componentReordered", bajaError, component, cx);
}
function fwCompEvent(comp, id, slot, obj, str, cx) {
var targetVal = null,
i,
x,
component;
if (comp.isSubscribed() || id === UNSUBSCRIBED) {
// First support framework callback
// TODO: Commented out for now for improved performance. Are these really needed?
/*
try {
if (id === CHANGED) {
comp.$fw("changed", slot, cx);
}
else if (id === ADDED) {
comp.$fw("added", slot, cx);
}
else if (id === REMOVED) {
comp.$fw("removed", slot, obj, cx);
}
else if (id === RENAMED) {
comp.$fw("renamed", slot, str, cx);
}
else if (id === REORDERED) {
comp.$fw("reordered", cx);
}
else if (id === TOPIC_FIRED) {
comp.$fw("fired", slot, obj, cx);
}
else if (id === SUBSCRIBED) {
comp.$fw("subscribed", cx);
}
else if (id === UNSUBSCRIBED) {
comp.$fw("unsubscribed", cx);
}
else if (id === KNOB_ADDED) {
comp.$fw("knobAdded", obj, cx);
}
else if (id === KNOB_REMOVED) {
comp.$fw("knobRemoved", obj, cx);
}
}
catch (e) {
error(e);
}
*/
// Route to event handlers on the Component
if (comp.hasHandlers()) {
handleComponentEvent(comp, comp, id, slot, obj, str, cx);
}
// Route to Subscribers if there are any registered
if (comp.$subs.length > 0) {
// Route to all registered Subscribers
for (i = 0; i < comp.$subs.length; ++i) {
// Route to event handlers on the Subscriber
if (comp.$subs[i].hasHandlers()) {
handleComponentEvent(comp, comp.$subs[i], id, slot, obj, str, cx);
}
}
}
}
if (id === RENAMED) {
if (slot && slot.isProperty()) {
targetVal = comp.get(slot);
}
} else if (id === FLAGS_CHANGED ||
id === FACETS_CHANGED) {
if (slot && slot.isProperty()) {
targetVal = comp.get(slot);
}
}
// Route to child Component
if (targetVal !== null && targetVal.getType().isComponent() && targetVal.isSubscribed()) {
// Route to event handlers on the Component
handleComponentChildEvent(targetVal, targetVal, id, str, cx);
// Route to Subscribers if there are any registered
if (targetVal.$subs.length > 0) {
// Route to all registered Subscribers
for (x = 0; x < targetVal.$subs.length; ++x) {
// Route to event handlers on the Subscriber
handleComponentChildEvent(targetVal, targetVal.$subs[x], id, str, cx);
}
}
}
// Special case for child reordered Component events. We need to route to all child Components on the target Component
if (id === REORDERED) {
comp.getSlots(function (slot) {
return slot.isProperty() && slot.getType().isComponent() && this.get(slot).isSubscribed();
}).each(function (slot) {
// Route reordered event
component = this.get(slot);
handleReorderComponentChildEvent(component, component, cx);
if (component.$subs.length > 0) {
for (i = 0; i < component.$subs.length; ++i) {
// Route to event handlers on the Subscriber
handleReorderComponentChildEvent(component, component.$subs[i], cx);
}
}
});
}
// NavEvents
if (baja.nav.hasHandlers() &&
comp.isMounted() &&
comp.isNavChild() &&
((id === ADDED && slot.getType().isComponent()) ||
(id === REMOVED && obj.getType().isComponent()) ||
(id === RENAMED && slot.getType().isComponent()) ||
id === REORDERED)) {
if (id === ADDED) {
baja.nav.fireHandlers("added", bajaError, baja.nav, comp.getNavOrd(), comp.get(slot), cx);
} else if (id === REMOVED) {
baja.nav.fireHandlers("removed", bajaError, baja.nav, comp.getNavOrd(), slot.getName(), obj, cx);
} else if (id === RENAMED) {
baja.nav.fireHandlers("renamed", bajaError, baja.nav, comp.getNavOrd(), comp.get(slot), str, cx);
} else if (id === REORDERED) {
baja.nav.fireHandlers("reordered", bajaError, baja.nav, comp.getNavOrd(), cx);
}
}
}
const hasOperatorFlag = (slot) => !!(slot.getFlags() & baja.Flags.OPERATOR);
const hasReadonlyFlag = (slot) => !!(slot.getFlags() & baja.Flags.READONLY);
/**
* Set a Slot's flags.
*
* If the `Complex` is mounted, this will **asynchronously** set the Slot
* Flags on the server.
*
* For callbacks, the `this` keyword is set to the `Component` instance.
*
* @param {Object} obj the object literal for the method's arguments.
* @param {baja.Slot|String} obj.slot the Slot or Slot name.
* @param {Number} obj.flags the new flags for the Slot.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok function
* callback. Called once the method has succeeded.
* @param {Function} [obj.fail] the fail function callback. Called if this
* method has an error.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @param [obj.cx] the Context (used internally by BajaScript).
* @returns {Promise} a promise that will be resolved once the flags have been
* set.
*
* @example
* <caption>
* An object literal is used to specify the method's arguments.
* </caption>
*
* myObj.setFlags({
* slot: 'outsideAirTemp',
* flags: baja.Flags.SUMMARY,
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('flags were set');
* })
* .catch(function (err) {
* baja.error('error setting flags: ' + err);
* });
*/
Component.prototype.setFlags = function (obj) {
obj = objectify(obj);
var slot = obj.slot,
flags = obj.flags,
cx = obj.cx,
serverDecode = cx && cx.serverDecode,
commit = cx && cx.commit,
cb = new Callback(obj.ok, obj.fail, obj.batch);
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, cb);
try {
slot = this.getSlot(slot);
// Short circuit some of this if this is called from a Server decode
if (!serverDecode) {
// Validate arguments
strictArg(slot, baja.Slot);
strictArg(flags, Number);
if (slot === null) {
throw new Error("Could not find Slot: " + obj.slot);
}
// Subclass check
if (typeof this.checkSetFlags === "function") {
this.checkSetFlags(slot, flags, cx);
}
}
// Check if this is a proxy. If so then trap it...
if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
this.$space.getCallbacks().setFlags(this, slot, flags, cb);
return cb.promise();
}
// Set the flags for the Slot
slot.$setFlags(flags);
if (cx) {
if (typeof cx.displayName === "string") {
slot.$setDisplayName(cx.displayName);
}
if (typeof cx.display === "string") {
slot.$setDisplay(cx.display);
}
}
// Fire Component Event
fwCompEvent(this, FLAGS_CHANGED, slot, null, null, cx);
cb.ok();
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Set a dynamic Slot's facets.
*
* If the `Complex` is mounted, this will **asynchronously* change the
* facets on the server.
*
* For callbacks, the `this` keyword is set to the Component instance.
*
* @param {Object} obj the object literal for the method's arguments.
* @param {baja.Slot|String} obj.slot the Slot of Slot name.
* @param {baja.Facets} obj.facets the new facets for the dynamic Slot.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok function
* callback. Called once the method has succeeded.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail function
* callback. Called if this method has an error.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @param [obj.cx] the Context (used internally by BajaScript).
* @returns {Promise} a promise that will be resolved once the Facets have
* been set.
*
* @example
* <caption>
* An object literal is used to specify the method's arguments.
* </caption>
*
* myObj.setFacets({
* slot: 'outsideAirTemp',
* facets: baja.Facets.make({ foo: 'boo' }),
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('facets have been set');
* })
* .catch(function (err) {
* baja.error('error setting facets: ' + err);
* });
*/
Component.prototype.setFacets = function (obj) {
obj = objectify(obj);
var cb = new Callback(obj.ok, obj.fail, obj.batch),
slot = obj.slot,
facets = obj.facets,
cx = obj.cx,
serverDecode = cx && cx.serverDecode,
commit = cx && cx.commit;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, cb);
try {
slot = this.getSlot(slot);
if (facets === null) {
facets = baja.Facets.DEFAULT;
}
// Short circuit some of this if this is the result of a Server Decode
if (!serverDecode) {
// Validate arguments
strictArg(slot, baja.Slot);
strictArg(facets, baja.Facets);
if (slot === null) {
throw new Error("Could not find Slot: " + obj.slot);
}
if (slot.isFrozen()) {
throw new Error("Cannot set facets of frozen Slot: " + slot.getName());
}
// Subclass check
if (typeof this.checkSetFacets === "function") {
this.checkSetFacets(slot, facets, cx);
}
}
// Check if this is a proxy. If so then trap it...
if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
this.$space.getCallbacks().setFacets(this, slot, facets, cb);
return cb.promise();
}
// Set the flags for the Slot
slot.$setFacets(facets);
if (cx) {
if (typeof cx.displayName === "string") {
slot.$setDisplayName(cx.displayName);
}
if (typeof cx.display === "string") {
slot.$setDisplay(cx.display);
}
}
// Fire Component Event
fwCompEvent(this, FACETS_CHANGED, slot, facets, null, cx);
cb.ok();
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Add a dynamic `Property` to a Component.
*
* If the value extends `baja:Action`, the new slot is also an Action.
* If the value extends `baja:Topic`, the new slot is also a Topic.
*
* If the `Complex` is mounted, this will **asynchronously** add
* the `Property` to the `Component` on the server.
*
* For callbacks, the 'this' keyword is set to the Component instance.
*
* @see baja.Facets
* @see baja.Flags
* @see baja.Component#getUniqueName
*
* @param {Object} obj the object literal for the method's arguments.
* @param {String} obj.slot the Slot name the unique name to use as the String
* key for the slot. If null is passed, then a unique name will automatically
* be generated. If the name ends with the '?' character a unique name will
* automatically be generated by appending numbers to the specified name. The
* name must meet the "name" production in the SlotPath BNF grammar.
* Informally this means that the name must start with an ASCII letter and
* contain only ASCII letters, ASCII digits, or '_'. Escape sequences can be
* specified using the '$' char. Use `baja.SlotPath.escape()` to escape
* illegal characters.
* @param {baja.Value} obj.value the value to be added
* @param {Number} [obj.flags] optional Slot flags.
* @param {baja.Facets} [obj.facets] optional Slot Facets.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
* function is called once the Property has been added to the Server. The
* function is passed the new `Property` that has just been added.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* This function is called if the Property fails to add. Any error information
* is passed into this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @param [obj.cx] the Context (used internally by BajaScript).
* @returns {Promise.<baja.Property>} a promise that will be resolved with the
* newly added Property.
*
* @example
* <caption>
* An object literal is used to specify the method's arguments.
* </caption>
*
* myObj.add({
* slot: 'foo',
* value: 'slot value',
* facets: baja.Facets.make({ doo: 'boo'), // Optional
* flags: baja.Flags.SUMMARY, // Optional
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (prop) {
* baja.outln('added a new Property named "' + prop + '"');
* })
* .catch(function (err) {
* baja.error('error adding Property: ' + err);
* });
*/
Component.prototype.add = function (obj) {
obj = objectify(obj, "value");
var cb = new Callback(obj.ok, obj.fail, obj.batch),
slotName = obj.slot,
val = obj.value,
flags = obj.flags,
facets = obj.facets,
cx = obj.cx,
serverDecode,
commit,
displayName,
display,
p;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, cb);
try {
slotName = bajaDef(slotName, null);
flags = bajaDef(flags, 0);
facets = bajaDef(facets, baja.Facets.DEFAULT);
cx = bajaDef(cx, null);
serverDecode = cx && cx.serverDecode;
commit = cx && cx.commit;
// Short-circuit some of this if this is the result of a Server Decode
if (!serverDecode) {
// Validate arguments
strictArg(slotName, String);
strictArg(val);
strictArg(flags, Number);
strictArg(facets, baja.Facets);
strictArg(cx);
if (!baja.hasType(val)) {
throw new Error("Can only add BValue Types as Component Properties");
}
if (val.getType().isAbstract()) {
throw new Error("Cannot add Abstract Type to Component: " + val.getType());
}
if (!val.getType().isValue()) {
throw new Error("Cannot add non Value Types as Properties to a Component");
}
if (val === this) {
throw new Error("Illegal argument value === this");
}
// Custom check add
if (typeof this.checkAdd === "function") {
this.checkAdd(slotName, val, flags, facets, cx);
}
if (val.getType().isComponent()) {
if (typeof val.isParentLegal === "function") {
if (!this.getType().is("baja:UnrestrictedFolder")) {
if (!val.isParentLegal(this)) {
throw new Error("Illegal parent: " + this.getType() + " for child " + val.getType());
}
}
}
if (typeof this.isChildLegal === "function") {
if (!this.isChildLegal(val)) {
throw new Error("Illegal child: " + val.getType() + " for parent " + this.getType());
}
}
}
}
// Check if this is a proxy. If so then trap it...
if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
this.$space.getCallbacks().add(this,
slotName,
val,
flags,
facets,
cb);
return cb.promise();
}
if (!serverDecode) {
if (slotName === null) {
slotName = this.getUniqueName(val.getType().getTypeName()); // TODO: Need extra argument checking before this is reached
} else if (slotName.substring(slotName.length - 1, slotName.length) === "?") {
slotName = this.getUniqueName(slotName.substring(0, slotName.length - 1));
}
baja.SlotPath.verifyValidName(slotName);
}
// Check for duplicate Slot
if (this.$map.get(slotName) !== null) {
throw new Error("Duplicate Slot: " + slotName);
}
if (val.getType().isComplex() && val.getParent() !== null) {
throw new Error("Complex already parented: " + val.getType());
}
displayName = baja.SlotPath.unescape(slotName);
display = "";
if (cx) {
if (typeof cx.displayName === "string") {
displayName = cx.displayName;
}
if (typeof cx.display === "string") {
display = cx.display;
}
}
if (val.getType().isAction()) {
p = new PropertyAction(slotName, displayName, display, flags, facets, val);
} else if (val.getType().isTopic()) {
p = new PropertyTopic(slotName, displayName, display, flags, facets, val);
} else {
p = new DynamicProperty(slotName, displayName, display, flags, facets, val);
}
// Add the Slot to the map
this.$map.put(slotName, p);
// Set up any parenting if needed
if (val.getType().isComplex()) {
val.$parent = this;
val.$propInParent = p;
// If we have a Component then attempt to mount it
if (val.getType().isComponent() && this.isMounted()) {
this.$space.$fw("mount", val);
}
}
// Fire Component Event
fwCompEvent(this, ADDED, p, null, null, cx);
cb.ok(p);
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Return a unique name for a potential new Slot in this `Component`.
*
* Please note, this method inspects the current Slots this Component has loaded
* to find a unique name. Therefore, if this Component is a Proxy, it must be
* fully loaded and subscribed. Also please refrain from using this method in a
* batch operation since it's likely the other operations in the batch will influence
* a Slot name's uniqueness.
*
* @param {String} slotName the initial Slot name used to ensure uniqueness. This must be
* a valid Slot name.
* @returns {String} a unique name.
*/
Component.prototype.getUniqueName = function (slotName) {
baja.SlotPath.verifyValidName(slotName);
let slotNum = slotName.match(/\d*$/)[0] || 0;
let baseName = slotName.replace(/\d*$/, '');
let uniqueName = slotName;
for (let i = 0; i < slotNum.length - 1; i++) {
const char = slotNum.substring(i, i + 1);
if (char !== '0') {
break;
}
baseName = baseName + char;
}
while (this.getSlot(uniqueName) !== null) {
slotNum++;
uniqueName = baseName + slotNum;
}
return uniqueName;
};
function removeUnmountEvent(component, handlers, cx) {
handlers.fireHandlers("unmount", bajaError, component, cx);
}
function removePropagateUnmountEvent(component, cx, hasNavEventHandlers) {
// If the Component is subscribed then trigger the unmount events
if (component.isSubscribed()) {
removeUnmountEvent(component, component, cx);
if (component.$subs.length > 0) {
// Route to all registered Subscribers
var i;
for (i = 0; i < component.$subs.length; ++i) {
// Route to event handlers on the Subscriber
removeUnmountEvent(component, component.$subs[i], cx);
}
}
}
// Fire Nav events
if (hasNavEventHandlers && component.isNavChild()) {
baja.nav.fireHandlers("unmount", bajaError, component, cx);
}
// Search all child Components and trigger the unmount event
component.getSlots(function (slot) {
return slot.isProperty() && slot.getType().isComponent();
}).each(function (slot) {
removePropagateUnmountEvent(this.get(slot), cx, hasNavEventHandlers);
});
}
/**
* Remove the dynamic `Slot` by the specified name.
*
* If the `Complex` is mounted, this will **asynchronously** remove
* the Property from the `Component` on the server.
*
* For callbacks, the 'this' keyword is set to the Component instance.
*
* @param {baja.Slot|String|Object} obj the Slot, Slot name, `Complex`
* instance or an object literal.
* @param {String} obj.slot the Slot, Slot name or `Complex` instance to
* remove.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
* function is called once the Property has been removed from the Server.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* This function is called if the Property fails to remove. Any error
* information is passed into this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @param [obj.cx] the Context (used internally by BajaScript).
* @returns {Promise} a promise that will be resolved once the Property has
* been removed.
*
* @example
* <caption>
* The Slot, Slot name, a Complex or an object literal can be used for
* the method's arguments.
* </caption>
*
* myObj.remove("foo");
*
* //...or via the Slot itself...
*
* myObj.remove(theFooSlot);
*
* //...or remove the Complex instance from the parent...
*
* myObj.remove(aComplexInstance);
*
* //... of if more arguments are needed then via object literal notation...
*
* myObj.remove({
* slot: 'foo',
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('Property has been removed');
* })
* .catch(function (err) {
* baja.error('error removing Property: ' + err);
* });
*/
Component.prototype.remove = function (obj) {
obj = objectify(obj, "slot");
var slot = obj.slot,
cx = obj.cx,
serverDecode = cx && cx.serverDecode,
commit = cx && cx.commit,
cb = new Callback(obj.ok, obj.fail, obj.batch),
val = null;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, cb);
try {
strictArg(slot);
if (baja.hasType(slot) && slot.getType().isComplex()) {
val = slot;
slot = slot.getPropertyInParent();
} else {
slot = this.getSlot(slot);
}
// Short circuit some of this on a Server decode
if (!serverDecode) {
if (slot === null) {
throw new Error("Invalid slot for Component remove");
}
if (!slot.isProperty() || slot.isFrozen()) {
throw new Error("Cannot remove Slot that isn't a dynamic Property: " + slot.getName());
}
// Subclass check
if (typeof this.checkRemove === "function") {
this.checkRemove(slot, cx);
}
}
// Check if this is a proxy. If so then trap it...
if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
this.$space.getCallbacks().remove(this, slot, cb);
return cb.promise();
}
// TODO: Remove links?
if (val === null) {
val = this.get(slot);
}
// Unparent
if (val.getType().isComplex()) {
// Unmount from Component Space
if (val.getType().isComponent() && val.isNavChild()) {
// Trigger unmount event to this component and all child Components just before it's properly removed
removePropagateUnmountEvent(val, cx, baja.nav.hasHandlers("unmount"));
// If we have a Component then attempt to unmount it
// (includes unregistering from subscription)
if (this.isMounted()) {
this.$space.$fw("unmount", val);
}
}
val.$parent = null;
val.$propInParent = null;
}
// Remove the Component from the Slot Map
this.$map.remove(slot.getName());
// Fire Component Event
fwCompEvent(this, REMOVED, slot, val, null, cx);
cb.ok();
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Rename the specified dynamic Slot.
*
* If the `Component` is mounted, this will **asynchronously** rename
* the Slot in the Component on the server.
*
* For callbacks, the `this` keyword is set to the Component instance.
*
* @param {Object} obj the object literal used for the method's arguments.
* @param {baja.Slot|String} obj.slot the dynamic Slot or dynamic Slot name
* that will be renamed
* @param {String} obj.newName the new name of the Slot. The name must meet
* the "name" production in the `SlotPath` BNF grammar. Informally this means
* that the name must start with an ASCII letter, and contain only ASCII
* letters, ASCII digits, or '_'. Escape sequences can be specified using the
* '$' char. Use `baja.SlotPath.escape()` to escape illegal characters.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
* function is called once the Slot has been renamed.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* This function is called if the Slot fails to rename. Any error information
* is passed into this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @param [obj.cx] the Context (used internally by BajaScript).
* @returns {Promise} a promise that will be resolved once the slot has been
* renamed.
*
* @example
* <caption>
* An object literal is used for the method's arguments.
* </caption>
*
* myObj.rename({
* slot: 'foo',
* newName: 'boo',
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('slot has been renamed');
* })
* .catch(function (err) {
* baja.error('error renaming slot: ' + err);
* });
*/
Component.prototype.rename = function (obj) {
obj = objectify(obj);
var slot = obj.slot,
newName = obj.newName,
cx = obj.cx,
serverDecode = cx && cx.serverDecode,
commit = cx && cx.commit,
cb = new Callback(obj.ok, obj.fail, obj.batch),
s,
oldName;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, cb);
try {
s = this.getSlot(slot);
if (s === null) {
throw new Error("Cannot rename. Slot doesn't exist: " + slot + " -> " + newName);
}
// Short circuit some of these checks on a Server decode
if (!serverDecode) {
strictArg(s, baja.Slot);
strictArg(newName, String);
baja.SlotPath.verifyValidName(newName);
if (s.isFrozen()) {
throw new Error("Cannot rename frozen Slot: " + slot + " -> " + newName);
}
if (this.getSlot(newName) !== null) {
throw new Error("Cannot rename. Slot name already used: " + slot + " -> " + newName);
}
// Subclass check
if (typeof this.checkRename === "function") {
this.checkRename(s, newName, cx);
}
}
// Record the old name
oldName = s.getName();
// Check if this is a proxy. If so then trap it...
if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
this.$space.getCallbacks().rename(this, oldName, newName, cb);
return cb.promise();
}
// Rename the Component from the Slot Map
if (!this.$map.rename(oldName, newName)) {
throw new Error("Cannot rename: " + oldName + " -> " + newName);
}
s.$slotName = newName;
if (cx) {
if (typeof cx.displayName === "string") {
s.$setDisplayName(cx.displayName);
}
if (typeof cx.display === "string") {
s.$setDisplay(cx.display);
}
}
// Fire Component Event
fwCompEvent(this, RENAMED, s, null, oldName, cx);
cb.ok();
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Sets display names of one or more slots of this component.
* If an empty string is given for obj.newDisplayName, the display name will
* be removed.
* In addition to firing "added" or "changed" component events on this
* component, a synthetic "rename" event is also fired on each of the slot
* whose display name is changed.
*
* @param {Object} obj
* @param {baja.Slot|Array.<baja.Slot>|string} obj.slot
* @param {Array.<string>|string} obj.newDisplayName
* @returns {Promise}
* @since Niagara 4.13
*/
Component.prototype.setDisplayName = function (obj) {
const slot = obj.slot,
newDisplayName = obj.newDisplayName,
cx = obj.cx;
const slots = Array.isArray(slot) ? slot : [ slot ],
newDisplayNames = Array.isArray(newDisplayName) ? newDisplayName : [ newDisplayName ];
let arg = {};
slots.forEach((slot, i) => {
if (newDisplayNames[i]) {
arg[slot] = newDisplayNames[i];
}
});
return this.$setDisplayNameMuxed({ slots, arg, cx });
};
/**
* Uses promise muxing to work with multiple add/set calls to the
* "displayNames" slot of this component.
* @private
* @param {Object} obj
* @returns {Promise}
*/
Component.prototype.$setDisplayNameMuxed = function (obj) {
const that = this;
if (!this.$muxed) {
this.$muxed = promiseMux({
exec: function (paramsArray) {
const proms = Array(paramsArray.length).fill(Promise.resolve());
let slots, cxs = [];
const displayNames = that.get('displayNames');
let newArg = (displayNames && displayNames.toObject()) || {};
// If array length is only 1 then treat it as a single op and just set the displayName
if (paramsArray.length === 1) {
// Merge the displayNames map
const first = paramsArray[0];
const { arg, slots: s } = first;
slots = s;
slots.forEach((slot, i) => {
if (arg[slot]) {
newArg[slot] = arg[slot];
} else {
delete newArg[slot];
}
});
} else {
slots = paramsArray.map((params) => params.slots).flat();
cxs = paramsArray.map((params) => params.cx).flat();
const args = paramsArray
.map((params) => params.arg)
.reduce((p, c) => {
return Object.assign({}, p, c);
}, {});
newArg = Object.assign(newArg, args);
}
proms[paramsArray.length - 1] = that[that.has('displayNames') ? 'set' : 'add']({
slot: 'displayNames',
value: baja.NameMap.make(newArg),
flags: DISPLAY_NAMES_FLAGS
});
return Promise.all(proms)
.then((result) => {
slots.forEach((slot, i) => {
// Fire synthetic "renamed" component and nav event
slot && that.$syntheticSetDisplayName(slot, cxs[i]);
});
return result;
});
},
coalesce: false
});
}
return this.$muxed(obj);
};
/**
* Fire a "renamed" component event after its display name has been set.
* Note that this operation is "synthetic" in the sense that an actual
* component rename itself has not occurred.
* All the component subscribers and nav event listeners will get this event
* and handle any UI updates similar to a rename operation.
*
* @private
* @param {baja.Slot|string} slot
* @param {object} [cx]
* @since Niagara 4.13
*/
Component.prototype.$syntheticSetDisplayName = function (slot, cx) {
slot = this.getSlot(slot); //get the actual slot ("slot" can be a string)
const oldName = slot.getName();
fwCompEvent(this, RENAMED, slot, null, oldName, cx);
};
function getKeyIndex(k, keys) {
var i;
for (i = 0; i < keys.length; ++i) {
if (k === keys[i]) {
return i;
}
}
throw new Error("Could not find Property in reorder: " + k);
}
function getNewPropsIndex(k, dynamicProperties) {
var i;
for (i = 0; i < dynamicProperties.length; ++i) {
if (k === dynamicProperties[i].getName()) {
return i;
}
}
throw new Error("Could not find dynamic Property in reorder: " + k);
}
/**
* Reorder the `Component`'s dynamic Properties.
*
* If the `Component` is mounted, this will **asynchronously** reorder
* the dynamic Properties in the Component on the Server.
*
* For callbacks, the `this` keyword is set to the `Component` instance.
*
* @param {Array.<baja.Property|String>|Object} obj the array of Properties
* or Property names, or an object literal used for the method's arguments.
* @param {Array.<baja.Property|String>} obj.dynamicProperties an array of
* Properties or Property names for the slot order.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
* function is called once the dynamic Properties have been reordered.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* This function is called if the dynamic Properties fail to reorder. Any
* error information is passed into this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be batched into this object.
* @param [obj.cx] the Context (used internally by BajaScript).
* @returns {Promise} a promise that will be resolved once the dynamic
* properties have been reordered.
*
* @example
* <caption>
* A Property array or an object literal can used for the method's arguments.
* </caption>
*
* // Order via an array of Properties...
* myObj.reorder([booProp, fooProp, dooProp]);
*
* // ...or order via an array of Property names...
* myObj.reorder(["boo", "foo", "doo"]);
*
* // ...or for more arguments, use an object literal...
* myObj.reorder({
* dynamicProperties: [booProp, fooProp, dooProp], // Can also be a Property name array!
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('slots have been reordered');
* })
* .catch(function (err) {
* baja.outln('error reordering slots: ' + err);
* });
*/
Component.prototype.reorder = function (obj) {
obj = objectify(obj, "dynamicProperties");
var that = this,
dynamicProperties = obj.dynamicProperties,
cx = obj.cx,
serverDecode = cx && cx.serverDecode,
commit = cx && cx.commit,
cb = new Callback(obj.ok, obj.fail, obj.batch),
i,
preGetSlot,
currentDynProps,
dynPropCount,
keys;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
// If this is a commit and the component is mounted then only process if the component is subscribed and fully loaded.
// Otherwise it probably won't work
if (commit && that.isMounted() && !(that.isSubscribed() && that.$bPropsLoaded)) {
cb.ok();
return cb.promise();
}
try {
// Verify the array contents
if (!serverDecode) {
strictArg(dynamicProperties, Array);
}
for (i = 0; i < dynamicProperties.length; ++i) {
preGetSlot = dynamicProperties[i];
dynamicProperties[i] = that.getSlot(dynamicProperties[i]);
if (!dynamicProperties[i]) {
throw new Error("Could not find dynamic Property to reorder: " + preGetSlot + " in " + this.toPathString());
}
if (dynamicProperties[i].isFrozen()) {
throw new Error("Cannot reorder frozen Properties");
}
}
currentDynProps = that.getSlots().dynamic().properties();
dynPropCount = 0;
while (currentDynProps.next()) {
++dynPropCount;
}
if (dynPropCount === 0) {
throw new Error("Cannot reorder. No dynamic Props!");
}
if (dynPropCount !== dynamicProperties.length) {
throw new Error("Cannot reorder. Actual count: " + dynPropCount + " != " + dynamicProperties.length);
}
// Subclass check
if (!serverDecode && typeof that.checkReorder === "function") {
that.checkReorder(dynamicProperties, cx);
}
// Check if this is a proxy. If so then trap it...
if (!commit && that.isMounted() && that.$space.hasCallbacks()) {
that.$space.getCallbacks().reorder(that, dynamicProperties, cb);
return cb.promise();
}
// Get a copy of the keys used in the SlotMap
keys = that.$map.getKeys();
// Sort the map accordingly
that.$map.sort(function (a, b) {
var as = that.$map.get(a),
bs = that.$map.get(b);
// If both Properties are frozen then just compare against the current indexes
if (as.isFrozen() && bs.isFrozen()) {
return getKeyIndex(a, keys) < getKeyIndex(b, keys) ? -1 : 1;
}
if (as.isFrozen()) {
return -1;
}
if (bs.isFrozen()) {
return 1;
}
// If both Properties are dynamic then we can order as per the new array
return getNewPropsIndex(a, dynamicProperties) < getNewPropsIndex(b, dynamicProperties) ? -1 : 1;
});
// Fire Component Event
fwCompEvent(that, REORDERED, null, null, null, cx);
cb.ok();
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Invoke an Action.
*
* If the `Component` is mounted, this will **asynchronously** invoke
* the Action on the Component in the Server.
*
* For callbacks, the `this` keyword is set to the `Component` instance.
*
* @param {baja.Action|String|Object} obj the Action, Action name or object
* literal for the method's arguments.
* @param {baja.Action|String} obj.slot the Action or Action name.
* @param [obj.value] the Action's argument.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
* function is called once Action has been invoked. If the Action has a
* returned argument, this will be passed to this function.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* This function is called if the Action fails to invoke. Any error
* information is passed into this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @param [obj.cx] the Context (used internally by BajaScript).
* @returns {Promise.<baja.Value|null>} a promise that will be resolved once
* the Action has been invoked.
*
* @example
* <caption>
* A Slot, Slot name or an object literal can used for the method's arguments.
* </caption>
*
* // Invoke the Action via its Action Slot...
* myObj.invoke(fooAction);
*
* // ...or via the Action's Slot name...
* myObj.invoke("foo");
*
* // ...or for more arguments, use an object literal...
* myObj.invoke({
* slot: actionSlot, // Can also be an Action Slot name
* value: 'the Action argument',
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (returnValue) {
* baja.outln('action successfully invoked and returned: ' + returnValue);
* })
* .catch(function (err) {
* baja.error('error invoking action: ' + err);
* });
*
* @example
* <caption>
* Please note that auto-generated convenience methods are created and
* added to a Component for invoking frozen Actions. If the name of the
* auto-generated Action method is already used, BajaScript will attach
* a number to the end of the method name so it becomes unique. For
* example, the 'set' Action on a NumericWritable would be called 'set1'
* because Component already has a 'set' method.
* </caption>
*
* // Invoke an Action called 'override'. Pass in an argument
* myPoint.override(overrideVal);
*
* // ...or via an object literal for more arguments...
* myPoint.override({
* value: overrideVal
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (returnValue) {
* baja.outln('action successfully invoked and returned: ' + returnValue);
* })
* .catch(function (err) {
* baja.error('error invoking action: ' + err);
* });
*/
Component.prototype.invoke = function (obj) {
obj = objectify(obj, "slot");
var that = this,
action = obj.slot,
arg = bajaDef(obj.value, null),
cx = obj.cx,
retVal = null,
cb = new Callback(obj.ok, obj.fail, obj.batch),
flags = 0,
paramType;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
try {
action = that.getSlot(action);
if (action === null || !action.isAction()) {
throw new Error("Action does not exist: " + obj.slot);
}
// Get Slot flags so we can test for 'async'
flags = that.getFlags(action);
paramType = action.getParamType();
// Check we have a valid argument for this Action
if (paramType) {
if (baja.hasType(arg)) {
if (arg.getType().isNumber() && paramType.isNumber() && !arg.getType().equals(paramType)) {
// Recreate the number with the correct boxed type if the type spec differs
arg = paramType.getInstance().constructor.make(arg.valueOf());
} else if (!arg.getType().is(paramType)) {
throw new Error(invalidActionArgErrMsg + arg);
}
} else {
throw new Error(invalidActionArgErrMsg + arg);
}
}
} catch (err) {
// Notify fail
cb.fail(err);
// We should ALWAYS bail after calling a callback fail!
return cb.promise();
}
function inv() {
try {
if (that.isMounted() && that.$space.hasCallbacks()) {
// If mounted then make a network call for the Action invocation
that.$space.getCallbacks().invokeAction(that, action, arg, cb);
return;
}
if (!action.isProperty()) {
// Invoke do method of Action
var s = "do" + action.getName().capitalizeFirstLetter();
if (typeof that[s] === "function") {
// Invoke but ensure null is returned if the function returns undefined
retVal = bajaDef(that[s](arg, cx), null);
} else {
throw new Error("Could not find do method for Action: " + action.getName());
}
} else {
// If the Action is also a Property then forward its invocation on to the value
retVal = bajaDef(that.get(action).invoke(that, arg, cx), null);
}
cb.ok(retVal);
} catch (err) {
cb.fail(err);
}
}
if ((flags & Flags.ASYNC) !== Flags.ASYNC) {
inv();
} else {
baja.runAsync(inv);
}
return cb.promise();
};
/**
* Fire a Topic.
*
* If the Component is mounted, this will **asynchronously** fire
* the Topic on the Component in the Server.
*
* For callbacks, the `this` keyword is set to the Component instance.
*
* @param {baja.Action|String|Object} obj the Topic, Topic name or object
* literal for the method's arguments.
* @param {baja.Action|String} obj.slot the Topic or Topic name.
* @param [obj.value] the Topic's event.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. This
* function is called once the Topic has been fired.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* This function is called if the Topic fails to fire. Any error information
* is passed into this function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @param [obj.cx] the Context (used internally by BajaScript).
* @returns {Promise} a promise that will be resolved once the Topic has been
* fired.
*
* @example
* <caption>
* A Slot, Slot name or an object literal can used for the method's arguments.
* </caption>
*
* // Fire the Topic via its Topic Slot...
* myObj.fire(fooTopic);
*
* // ...or via the Topic's Slot name...
* myObj.fire("foo");
*
* // ...or for more arguments, use an object literal...
* myObj.fire({
* slot: topicSlot, // Can also be a Topic Slot name
* value: "the Topic event argument",
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('topic has been fired');
* })
* .catch(function (err) {
* baja.error('error firing topic: ' + err);
* });
*
* @example
* <caption>
* Please note that auto-generated convenience methods are created and
* added to a Component for firing frozen Topics. If the name of the
* auto-generated Topic method is already used, BajaScript will attach a
* number to the end of the method name so it becomes unique.
* </caption>
*
* // Fire a Topic called 'foo'
* myObj.fireFoo();
*
* // Fire a Topic called foo with an event argument...
* myObj.fireFoo("the Topic event argument");
*
* // ...or via an object literal for more arguments...
* myObj.fireFoo({
* value: "the Topic event argument",
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('topic has been fired');
* })
* .catch(function (err) {
* baja.error('error firing topic: ' + err);
* });
*/
Component.prototype.fire = function (obj) {
obj = objectify(obj, "slot");
var topic = obj.slot,
event = obj.value,
cx = obj.cx,
serverDecode = cx && cx.serverDecode,
commit = cx && cx.commit,
cb = new Callback(obj.ok, obj.fail, obj.batch);
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, cb);
try {
// Ensure we have a Topic Slot
topic = this.getSlot(topic);
if (!topic.isTopic()) {
throw new Error("Slot is not a Topic: " + topic.getName());
}
// Ensure event is not undefined
event = bajaDef(event, null);
// Short circuit some of this on a Server decode
if (!serverDecode) {
// Validate the event
if (event !== null) {
if (!baja.hasType(event)) {
throw new Error("Topic event is not a BValue");
}
if (event.getType().isAbstract()) {
throw new Error("Topic event is has abstract Type: " + event.getType());
}
if (!event.getType().isValue()) {
throw new Error("Topic event is not a BValue: " + event.getType());
}
}
}
// Check if this is a proxy. If so then trap it...
if (!commit && this.isMounted() && this.$space.hasCallbacks()) {
this.$space.getCallbacks().fire(this, topic, event, cb);
return cb.promise();
}
// If the Topic is a Property then fire on the Property
if (topic.isProperty()) {
this.get(topic).fire(this, event, cx);
}
// Fire component event for Topic
fwCompEvent(this, TOPIC_FIRED, topic, event, null, cx);
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Internal Framework Method.
*
* @private
*
* @see baja.Complex#$fw
*/
Component.prototype.$fw = function (x, a, b, c, d) {
var k, that = this;
if (x === "modified") {
// Fire a Component modified event for Property changed
fwCompEvent(that, CHANGED, a, null, null, b);
return;
}
if (x === "fwSubscribed") {
fwCompEvent(that, SUBSCRIBED, null, null, null, b);
return;
}
if (x === "fwUnsubscribed") {
fwCompEvent(that, UNSUBSCRIBED, null, null, null, b);
return;
}
if (x === "modifyTrap") {
// Check if this is a proxy. If so then trap any modifications
if (that.isMounted() && that.$space.hasCallbacks() && !(d && d.commit)) {
that.$space.getCallbacks().set(that,
a, // propertyPath
b, // value
c); // callback
return true;
}
return false;
}
if (x === "installKnob") {
// Add a knob to the Component
that.$knobs = that.$knobs || {};
that.$knobCount = that.$knobCount || 0;
that.$knobs[a.getId()] = a;
a.getSourceComponent = function () { return that; };
++that.$knobCount;
fwCompEvent(that, KNOB_ADDED, that.getSlot(a.getSourceSlotName()), /*a=Knob*/a, null, /*b=Context*/b);
return;
}
if (x === "uninstallKnob") {
// Remove the knob from the Component
if (this.$knobs && this.$knobs.hasOwnProperty(a)) {
k = this.$knobs[a];
delete this.$knobs[a];
--this.$knobCount;
fwCompEvent(this, KNOB_REMOVED, /*b=Slot name*/this.getSlot(b), k, null, /*c=Context*/c);
}
return;
}
if (x === "installRelationKnob") {
// Add a relation knob to the Component
this.$rknobs = this.$rknobs || {};
this.$rknobCount = this.$rknobCount || 0;
this.$rknobs[a.getId()] = a;
a.getEndpointComponent = function () { return that; };
++this.$rknobCount;
fwCompEvent(this, RELATION_KNOB_ADDED, null, /*a=RelationKnob*/a, null, /*b=Context*/b);
return;
}
if (x === "uninstallRelationKnob") {
// Remove the relation knob from the Component
if (this.$rknobs && this.$rknobs.hasOwnProperty(a)) {
k = this.$rknobs[a];
delete this.$rknobs[a];
--this.$rknobCount;
fwCompEvent(this, RELATION_KNOB_REMOVED, null, k, null, /*b=Context*/b);
}
return;
}
if (x === "setPermissions") {
// Set the permissions on the Component
this.$permissionsStr = a;
// Nullify any decoded permissions
this.$permissions = null;
//no return - fall through
}
return callSuper("$fw", Component, this, arguments);
};
/**
* Return true if the `Component` is mounted inside a Space.
*
* @returns {Boolean}
*/
Component.prototype.isMounted = function () {
return this.$space !== null;
};
/**
* Return the Component Space.
*
* @returns the Component Space for this `Component` (if mounted) otherwise return null.
*/
Component.prototype.getComponentSpace = function () {
return this.$space;
};
/**
* Return the `Component`'s handle.
*
* @returns {String|null} handle for this Component (if mounted), otherwise
* null.
*/
Component.prototype.getHandle = function () {
return this.$handle;
};
/**
* Return the ORD in session for this `Component`.
*
* @returns {baja.Ord} ORD in Session for this `Component` (or null if not mounted).
*/
Component.prototype.getOrdInSession = function () {
return this.getHandle() === null ? null : baja.Ord.make("station:|h:" + this.getHandle());
};
/**
* Subscribe a number of `Component`s for a period of time.
*
* The default period of time is 10 seconds.
*
* Please note that a {@link baja.Subscriber} can also be used to put a
* `Component` into and out of subscription.
*
* If the `Component` is mounted and it can be subscribed, this will result in
* an **asynchronous** network call.
*
* If any of the the `Component`s are already leased, the lease timer will
* just be renewed.
*
* For callbacks, the `this` keyword is set to whatever the `comps` argument
* was originally set to.
*
* @see baja.Subscriber
* @see baja.Component#lease
*
* @param {Object} obj an object literal for the method's arguments.
* @param {Array.<baja.Component>|baja.Component} obj.comps the Components
* to be subscribed.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the Component has been subscribed.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* Called if the Component fails to subscribe. Any errors will be passed to
* this function.
* @param {Number|baja.RelTime} [obj.time] the number of milliseconds or RelTime
* for the lease. Defaults to 10,000 milliseconds.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will
* be batched into this object. The timer will only be started once the
* batch has fully committed.
* @returns {Promise} a promise that will be resolved once the component(s)
* have been subscribed.
*
* @example
* <caption>
* A time (Number or baja.RelTime) or an object literal can be used to
* specify the method's arguments.
* </caption>
*
* // Lease an array of Components for the default time period
* myComp.lease([comp1, comp2, comp3]);
*
* // ...or lease for 2 and half minutes...
* myComp.lease({
* time: baja.RelTime.make({minutes: 2, seconds: 30}),
* comps: [comp1, comp2, comp3]
* })
* .then(function () {
* baja.outln('components have been subscribed');
* })
* .catch(function (err) {
* baja.outln('components failed to subscribe: ' + err);
* });
*/
Component.lease = function (obj) {
obj = objectify(obj, "comps");
var cb = new Callback(obj.ok, obj.fail, obj.batch),
comps = obj.comps,
time = bajaDef(obj.time, /*10 seconds*/10000),
space = null,
i,
handles = [],
compsToSub = [],
promises = [],
j;
function scheduleUnlease(comp, time) {
// Cancel the current lease ticket
comp.$leaseTicket.cancel();
// Schedule to expire the Subscription in 60 seconds
comp.$leaseTicket = baja.clock.schedule(function () {
unlease(comp);
}, time);
}
try {
if (!comps) {
throw new Error("Must specify Components for lease");
}
// Ensure 'comps' is used as the context in the callback...
setContextInOkCallback(comps, cb);
setContextInFailCallback(comps, cb);
// If a rel time then get the number of milliseconds from it
if (baja.hasType(time) && time.getType().is("baja:RelTime")) {
time = time.getMillis();
}
strictArg(time, Number);
if (time < 1) {
throw new Error("Invalid lease time (time must be > 0 ms): " + time);
}
if (!(comps instanceof Array)) {
comps = [ comps ];
}
if (comps.length === 0) {
cb.ok();
return cb.promise();
}
for (i = 0; i < comps.length; ++i) {
// Check all Components are valid
strictArg(comps[i], Component);
if (!comps[i].isMounted()) {
throw new Error("Cannot subscribe unmounted Component!");
}
if (!space) {
space = comps[i].getComponentSpace();
}
if (space !== comps[i].getComponentSpace()) {
throw new Error("All Components must belong to the same Component Space!");
}
}
cb.addOk(function (ok, fail) {
var x;
for (x = 0; x < comps.length; ++x) {
scheduleUnlease(comps[x], time);
}
ok();
});
// Build handles we want to subscribe
for (j = 0; j < comps.length; ++j) {
// See if we need to make any network calls.
if (!comps[j].$subDf || Promise.isRejected(comps[j].$subDf.promise())) {
handles.push("h:" + comps[j].getHandle());
compsToSub.push({
comp: comps[j],
df: (comps[j].$subDf = Promise.deferred())
});
}
comps[j].$lease = true;
promises.push(comps[j].$subDf.promise());
}
// When all of these promises are resolved then we're done.
cb.addOk(function (ok, fail) {
Promise.all(promises).then(ok, fail);
});
// If there's currently a lease active then renew it by calling ok on the callback
if (handles.length === 0) {
cb.ok();
return cb.promise();
}
// Signal that each Component has been subscribed
cb.addOk(function (ok, fail) {
var i;
for (i = 0; i < compsToSub.length; ++i) {
try {
compsToSub[i].comp.$fw("fwSubscribed");
} catch (err0) {
bajaError(err0);
} finally {
try {
compsToSub[i].df.resolve();
} catch (err1) {
bajaError(err1);
}
}
}
ok();
});
cb.addFail(function (ok, fail, err) {
var i;
for (i = 0; i < compsToSub.length; ++i) {
try {
compsToSub[i].df.reject(err);
} catch (err0) {
bajaError(err0);
}
}
fail(err);
});
// Make network call for subscription
space.getCallbacks().subscribe(handles, cb, obj.importAsync);
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Subscribe a `Component` for a period of time.
*
* The default lease time is 10 seconds.
*
* Please note that a {@link baja.Subscriber} can also be used to put a
* `Component` into and out of subscription.
*
* If the `Component` is mounted and it can be subscribed, this will result in
* an **asynchronous** network call.
*
* If `lease` is called while the `Component` is already leased, the timer
* will just be renewed.
*
* For callbacks, the `this` keyword is set to the `Component` instance.
*
* @see baja.Subscriber
* @see baja.Component.lease
*
* @param {Number|baja.RelTime|Object} [obj] the number of milliseconds,
* RelTime or an object literal for the method's arguments.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the Component has been subscribed.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* Called if the Component fails to subscribe. Any errors will be passed to
* this function.
* @param {Number|baja.RelTime} [obj.time] the number of milliseconds or
* RelTime for the lease.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object. The timer will only be started once the batch has
* fully committed.
* @returns {Promise} a promise that will be resolved once the component has
* been subscribed.
*
* @example
* <caption>
* A time (Number or baja.RelTime) or an object literal can be used to
* specify the method's arguments.
* </caption>
*
* // Lease for 15 seconds
* myComp.lease(15000);
*
* // ...or lease for 2 and half minutes...
* myComp.lease(baja.RelTime.make({minutes: 2, seconds: 30}));
*
* // ...or lease using an object literal for more arguments...
* myComp.lease({
* time: 1000, // in milliseconds. Can also be a RelTime.
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('component has been leased');
* })
* .catch(function (err) {
* baja.error('failed to lease component: ' + err);
* });
*/
Component.prototype.lease = function (obj) {
obj = objectify(obj, "time");
obj.comps = this;
return Component.lease(obj);
};
/**
* Is the component subscribed?
*
* @returns {Boolean}
*/
Component.prototype.isSubscribed = function () {
// Component is subscribed if is leased or a Subscriber is registered on it
return this.$lease || this.$subs.length > 0;
};
/**
* Load all of the Slots on the `Component`.
*
* If the `Component` is mounted and it can be loaded, this will result in
* an **asynchronous** network call.
*
* For callbacks, the `this` keyword is set to the `Component` instance.
*
* @param {object} [obj]
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the Component has been loaded.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* Called if the Component fails to load. Any errors will be passed to this
* function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @returns {Promise} a promise that will be resolved once the slots have
* been loaded.
*
* @example
* <caption>
* An optional object literal can be used to specify the method's arguments.
* </caption>
*
* myComp.loadSlots({
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function () {
* baja.outln('slots have been loaded');
* })
* .catch(function (err) {
* baja.error('failed to load slots: ' + err);
* });
*/
Component.prototype.loadSlots = function (obj) {
obj = objectify(obj);
let loadSlotsPromise = this.$loadSlotsPromise;
if (!loadSlotsPromise) {
const cb = new Callback(baja.ok, baja.fail, obj.batch);
if (!this.$bPropsLoaded && this.isMounted() && this.$space.hasCallbacks()) {
this.$space.getCallbacks().loadSlots("h:" + this.getHandle(), 0, cb);
} else {
cb.ok();
}
loadSlotsPromise = this.$loadSlotsPromise = cb.promise();
}
const cb = obj.cb || new Callback(obj.ok, obj.fail);
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, cb);
loadSlotsPromise.then((result) => cb.ok(result), (err) => cb.fail(err));
return cb.promise();
};
/**
* Make a Server Side Call.
*
* Sometimes it's useful to invoke a method on the server from BajaScript.
* A Server Side Call is how this is achieved.
*
* This will result in an **asynchronous** network call.
*
* In order to make a Server Side Call, the developer needs to first create a
* Niagara (Server Side) class that implements the
* `box:javax.baja.box.BIServerSideCallHandler` interface.
* The implementation should also declare itself as an Agent on the target
* Component Type (more information in Java interface docs).
* For callbacks, the 'this' keyword is set to the Component instance.
*
* @param {Object} obj the object literal for the method's arguments.
* @param {String} obj.typeSpec the type specification of the Server Side Call
* Handler (`moduleName:typeName`).
* @param {String} obj.methodName the name of the method to invoke in the
* Server Side Call Handler
* @param obj.value the value for the server side method argument (must be a
* BajaScript Type)
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the Server Side Call has been invoked. Any return value is
* passed to this function.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* Called if the Component fails to load. Any errors will be passed to this
* function.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @returns {Promise.<baja.Value|null>} a promise that will be resolved once
* the server side call has completed.
*
* @example
* <caption>
* Here's an example of how a method implemented by this handler can be invoked.
* </caption>
*
* // A resolved and mounted Component...
* myComp.serverSideCall({
* typeSpec: "foo:MyServerSideCallHandler", // The TypeSpec (moduleName:typeName) of the Server Side Call Handler
* methodName: "bar", // The name of the public method we wish to invoke in the handler
* value: "the argument for the method", // The argument to pass into the method (this can be any Baja Object/Component structure).
* It will be deserialized automatically by Niagara.
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (returnValue) {
* baja.outln('server side call has completed with: ' + returnValue);
* })
* .catch(function (err) {
* baja.error('server side call failed: ' + err);
* });
*/
Component.prototype.serverSideCall = function (obj) {
obj = objectify(obj);
var typeSpec = obj.typeSpec,
methodName = obj.methodName,
val = bajaDef(obj.value, null),
cb = new Callback(obj.ok, obj.fail, obj.batch);
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(this, cb);
setContextInFailCallback(this, cb);
try {
// Check arguments
strictArg(typeSpec, String);
strictArg(methodName, String);
strictArg(val);
// Can only make this call on proper mounted Components that have Space Callbacks
if (this.isMounted() && this.$space.hasCallbacks()) {
this.$space.getCallbacks().serverSideCall(this, typeSpec, methodName, val, cb);
} else {
throw new Error("Unable to make serverSideCall on non-proxy Component Space");
}
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Return the Slot Path of the `Component`.
*
* @returns {baja.SlotPath} the Slot Path or null if not mounted.
*/
Component.prototype.getSlotPath = function () {
if (!this.isMounted()) {
return null;
}
var slotNames = [],
b;
function getParNames(comp) {
slotNames.push(comp.getName());
var p = comp.getParent();
if (p !== null) {
getParNames(p);
}
}
getParNames(this);
b = slotNames.reverse().join("/");
if (b.length === 0) {
b = "/";
}
return new baja.SlotPath(b);
};
/**
* Return the path string of the `Component`.
*
* @returns {String} the Path String or null if not mounted.
*/
Component.prototype.toPathString = function () {
if (!this.isMounted()) {
return null;
}
return this.getSlotPath().getBody();
};
/**
* Return the Nav ORD for the `Component`.
*
* @param {object} [params]
* @param {boolean} [params.sessionAware]
* @returns {baja.Ord} the Nav ORD or null if it's not mounted.
* @see baja.NavContainer#getNavOrd
*/
Component.prototype.getNavOrd = function (params) {
if (!this.isMounted()) {
return null;
}
var spaceOrd = this.$space.getAbsoluteOrd(params);
if (spaceOrd === null) {
return null;
}
return baja.Ord.make(spaceOrd + "|" + this.getSlotPath());
};
/**
* Return the Nav Name for the `Component`.
*
* @returns {String}
*/
Component.prototype.getNavName = function () {
var name = this.getName(),
space;
if (name !== null) {
return name;
}
space = this.getComponentSpace();
if (space && space.getRootComponent() !== null) {
return space.getNavName();
}
return null;
};
/**
* Return the type if the object the nav node navigates too.
*
* For a Component, this is just a string version of its own type spec.
*
* @returns {String} The nav node type spec.
*/
Component.prototype.getNavTypeSpec = function () {
return this.getType().toString();
};
/**
* Return the Nav Display Name for the `Component`.
*
* @returns {String}
*/
Component.prototype.getNavDisplayName = function () {
return this.getDisplayName();
};
/**
* Return the Nav Parent for the `Component`.
*
* @returns parent Nav Node
*/
Component.prototype.getNavParent = function () {
var parent = this.getParent();
return parent || this.getComponentSpace();
};
/**
* Access the Nav Children for the `Component`.
*
* @see baja.NavContainer#getNavChildren
*/
Component.prototype.getNavChildren = function (obj) {
obj = objectify(obj, "ok");
var cb = new Callback(obj.ok, obj.fail, obj.batch),
that = this,
kids;
if (that.isMounted() && that.$space.hasCallbacks()) {
// If we're mounted then make a network call to get the NavChildren since this
// is always implemented Server Side
that.$space.getCallbacks().getNavChildren(that.getHandle(), cb);
} else {
kids = [];
that.getSlots().properties().isComponent().each(function (slot) {
if ((that.getFlags(slot) & baja.Flags.HIDDEN) === 0) {
kids.push(that.get(slot));
}
});
cb.ok(kids);
}
return cb.promise();
};
/**
* Return the Icon for the `Component`
*
* @returns {baja.Icon}
*/
Component.prototype.getIcon = function () {
var icon = this.get('icon');
if (baja.hasType(icon, 'baja:Icon')) {
return icon;
}
return this.getType().getIcon();
};
/**
* Return the Nav Icon for the `Component`
*
* @returns {baja.Icon}
*/
Component.prototype.getNavIcon = function () {
return this.getIcon();
};
/**
* Return the Nav Description for the `Component`.
*
* @returns {String}
*/
Component.prototype.getNavDescription = function () {
return this.getType().toString();
};
// Mix-in the event handlers for baja.Component
baja.event.mixin(Component.prototype);
// These comments are added for the benefit of JsDoc Toolkit...
/**
* Attach an Event Handler to this `Component` instance.
*
* When an instance of `Component` is subscribed to a `Component` running
* in the Station, BajaScript can be used to listen for Component Events.
*
* An event handler consists of a name and a function. When the
* function is called, `this` will map to the target `Component` the handler
* is attached to.
*
* For a list of all the event handlers and some of this method's more advanced
* features, please see {@link baja.Subscriber#attach}.
*
* @function baja.Component#attach
*
* @see baja.Subscriber
* @see baja.Component#detach
* @see baja.Component#getHandlers
* @see baja.Component#hasHandlers
*
* @param {String} event handler name
* @param {Function} func the event handler function
*
* @example
* <caption>
* A common event to attach to would be a Property changed event.
* </caption>
*
* // myPoint is a mounted and subscribed Component...
* myPoint.attach("changed", function (prop, cx) {
* if (prop.getName() === "out") {
* baja.outln("The output of the point is: " + this.getOutDisplay());
* }
* });
*/
/**
* Detach an Event Handler from the `Component`.
*
* If no arguments are used with this method then all events are removed.
*
* For some of this method's more advanced features, please see
* {@link baja.Subscriber#detach}.
*
* For a list of all the event handlers, please see {@link baja.Subscriber#attach}.
*
* @function baja.Component#detach
*
* @see baja.Subscriber
* @see baja.Component#attach
* @see baja.Component#getHandlers
* @see baja.Component#hasHandlers
*
* @param {String} [hName] the name of the handler to detach from the Component.
* @param {Function} [func] the function to remove from the Subscriber. It's recommended to supply this just in case
* other scripts have added event handlers.
*/
/**
* Return an array of event handlers.
*
* For a list of all the event handlers, please see
* {@link baja.Subscriber#attach}.
*
* To access multiple handlers, insert a space between the handler names.
*
* @function baja.Component#getHandlers
*
* @see baja.Subscriber
* @see baja.Component#detach
* @see baja.Component#attach
* @see baja.Component#hasHandlers
*
* @param {String} hName the name of the handler
* @returns {Array.<Function>}
*/
/**
* Return true if there any handlers registered for the given handler name.
*
* If no handler name is specified then test to see if there are any
* handlers registered at all.
*
* Multiple handlers can be tested for by using a space character between the names.
*
* For a list of all the event handlers, please see
* {@link baja.Subscriber#attach}.
*
* @function baja.Component#hasHandlers
*
* @see baja.Subscriber
* @see baja.Component#detach
* @see baja.Component#attach
* @see baja.Component#getHandlers
*
* @param {String} [hName] the name of the handler. If undefined, then see if there are any
* handlers registered at all.
* @returns {Boolean}
*/
/**
* Returns the default parameter for an Action.
*
* For unmounted Components, by default it calls
* {@link baja.ActionProperty#getParamDefault}. If mounted in a Proxy
* Component Space, this will result in an asynchronous network call.
*
* For callbacks, the `this` keyword is set to the `Component` instance.
*
* @param {Object} obj the object literal for the method's arguments.
* @param {baja.Action|String} obj.slot the Action or Action name.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback.
* Called once the Action Parameter Default has been received. Any return
* value is passed to this function (could be null if no Action parameter is
* defined).
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @returns {Promise.<baja.Value|null>} a promise that will be resolved once
* the callbacks have been invoked.
*
* @example
* // A resolved and mounted Component...
* myComp.getActionParameterDefault({
* slot: 'myAction',
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (param) {
* if (param === null) {
* baja.outln('action takes no parameters');
* } else {
* baja.outln('action parameter is: ' + param);
* }
* })
* .catch(function (err) {
* baja.error('could not retrieve action parameter: ' + err);
* });
*/
Component.prototype.getActionParameterDefault = function (obj) {
obj = objectify(obj, "slot");
var that = this,
cb = new Callback(obj.ok, obj.fail, obj.batch),
slot = that.getSlot(obj.slot),
def;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
try {
// Check arguments
if (slot === null) {
throw new Error("Unable to find Action: " + obj.slot);
}
if (!slot.isAction()) {
throw new Error("Slot is not an Action: " + obj.slot);
}
if (that.isMounted() && that.$space.hasCallbacks()) {
// If mounted then make a network call for the Action invocation
that.$space.getCallbacks().getActionParameterDefault(that, slot, cb);
} else {
// If not mounted then just return it from the Slot
def = slot.getParamDefault();
cb.ok(def);
}
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Return the knob count for the `Component`.
*
* @returns {Number} the number of knobs installed on the `Component`
*/
Component.prototype.getKnobCount = function () {
return this.$knobCount || 0;
};
/**
* Return the Knobs for a particular Slot or the whole `Component`.
*
* If no slot is passed in all the knobs for the `Component` will be returned.
*
* @param {baja.Slot|String} [slot] the Slot or Slot name.
*
* @returns {Array.<Object>} array of knobs
*/
Component.prototype.getKnobs = function (slot) {
if (!this.$knobCount) {
return [];
}
var p,
knob,
k = [],
mySlot;
if (arguments.length > 0) {
// Find Knobs for a particular Slot
mySlot = this.getSlot(slot);
if (!mySlot) {
throw new Error("Invalid Slot: " + slot);
}
}
// Build up knobs array
for (p in this.$knobs) {
if (this.$knobs.hasOwnProperty(p)) {
knob = this.$knobs[p];
if (mySlot) {
if (knob.getSourceSlotName() === mySlot.getName()) {
k.push(knob);
}
} else {
k.push(knob);
}
}
}
return k;
};
/**
* Return the relation knob count for the component.
*
* @returns {Number} The number of relation knobs installed on the component.
*/
Component.prototype.getRelationKnobCount = function () {
return this.$rknobCount || 0;
};
/**
* Return all of the relation knobs for the component.
*
* @returns {Array.<Object>} An array of relation knobs.
*/
Component.prototype.getRelationKnobs = function () {
var that = this,
rknobs = [],
p;
if (that.$rknobCount && that.$rknobs) {
for (p in that.$rknobs) {
if (that.$rknobs.hasOwnProperty(p)) {
rknobs.push(that.$rknobs[p]);
}
}
}
return rknobs;
};
/**
* Return the relation knob for the specified id.
*
* @param {String} id The id.
*
* @returns {Object|null} The relation knob or null if nothing can be found.
*/
Component.prototype.getRelationKnob = function (id) {
var rknobs = this.getRelationKnobs(),
i;
for (i = 0; i < rknobs.length; ++i) {
if (rknobs[i].getRelationId() === id) {
return rknobs[i];
}
}
return null;
};
/**
* Return the Links for a particular Slot or the whole `Component`.
*
* If no slot is passed in all the links for the `Component` will be returned.
*
* @param {baja.Slot|String} [slot] the `Slot` or Slot name.
*
* @returns {Array.<baja.Struct>} array of links.
*/
Component.prototype.getLinks = function (slot) {
var links = [],
mySlot;
if (arguments.length === 0) {
// If no arguments then return all the knobs for this component
this.getSlots(function (s) {
return s.isProperty() && s.getType().isLink();
}).each(function (s) {
links.push(this.get(s));
});
} else {
// Find Links for a particular Slot
mySlot = this.getSlot(slot);
if (mySlot === null) {
throw new Error("Invalid Slot: " + slot);
}
this.getSlots(function (s) {
return s.isProperty() && s.getType().isLink() && this.get(s).getTargetSlotName() === mySlot.getName();
}).each(function (s) {
links.push(this.get(s));
});
}
return links;
};
/**
* Create an instance of a Link to link from the specified source component
* to this one.
*
* For unmounted `Component`s, by default this method resolves a plain
* `baja:Link` instance. If mounted in a Proxy Component Space, this will
* result in an asynchronous network call.
*
* For callbacks, the `this` keyword is set to the `Component` instance.
*
* @param {baja.LinkCheck~LinkCreateInfo} obj the object literal for the
* method's arguments. The `target` property will be set to this instance on
* which you're calling `makeLink()`.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
* Link will be passed as an argument to this function.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @returns {Promise.<baja.Struct>} a promise that will be resolved with the
* newly added Link.
*
* @example
* component.makeLink({
* source: sourceComponent,
* sourceSlot: 'sourceSlot',
* targetSlot: 'myTargetSlot',
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (link) {
* baja.outln('link created: ' + link);
* })
* .catch(function (err) {
* baja.error('failed to create link: ' + err);
* });
*/
Component.prototype.makeLink = function (obj) {
obj = objectify(obj);
var that = this,
cb = new Callback(obj.ok, obj.fail, obj.batch),
source = obj.source,
sourceSlot = obj.sourceSlot,
targetSlot = obj.targetSlot,
link;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
try {
strictArg(source, Component);
sourceSlot = source.getSlot(sourceSlot);
targetSlot = that.getSlot(targetSlot);
if (!targetSlot) {
throw new Error(fromBajaLex('makeLink.invalidTarget', 'Invalid Target Slot.'));
}
if (!sourceSlot) {
throw new Error(fromBajaLex('makeLink.invalidSource', 'Invalid Source Slot.'));
}
if (that.isMounted() && that.$space.hasCallbacks()) {
// If mounted then make a network call to get the link
that.$space.getCallbacks().makeLink(source, sourceSlot, that, targetSlot, cb);
} else {
// If not mounted then just return it from the Slot
link = baja.$("baja:Link");
cb.ok(link);
}
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* Check the validity of a link from the specified source `Component` to this
* one.
*
* The target and source components must be mounted, leased / subscribed,
* otherwise this function will fail.
*
* For callbacks, the `this` keyword is set to the `Component` instance.
*
* @param {baja.LinkCheck~LinkCreateInfo} obj the object literal for the
* method's arguments. The `target` property will be set to this instance on
* which you're calling `checkLink()`.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
* Link will be passed as an argument to this function.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
* @returns {Promise.<baja.LinkCheck>} a promise that will be resolved with the
* LinkCheck object.
*
* @example
* component.checkLink({
* source: sourceComponent,
* sourceSlot: 'sourceSlot',
* targetSlot: 'myTargetSlot',
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (linkCheck) {
* baja.outln('link checked: ' + linkCheck.isValid());
* })
* .catch(function (err) {
* baja.error('failed to check link: ' + err);
* });
*/
Component.prototype.checkLink = function (obj) {
obj = objectify(obj);
var that = this,
cb = new Callback(obj.ok, obj.fail, obj.batch),
source = obj.source,
sourceSlot = obj.sourceSlot,
targetSlot = obj.targetSlot;
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
strictArg(source, Component);
try {
// Verify target and source are subscribed.
if (!that.isSubscribed() || !source.isSubscribed()) {
throw new Error(fromBajaLex('linkcheck.notSubscribed', 'Component.checkLink requires target source to be subscribed.'));
}
sourceSlot = source.getSlot(sourceSlot);
targetSlot = that.getSlot(targetSlot);
if (!targetSlot) {
cb.ok(LinkCheck.makeInvalid(fromBajaLex('linkcheck.invalidTarget', 'Invalid link target.')));
} else if (!sourceSlot) {
cb.ok(LinkCheck.makeInvalid(fromBajaLex('linkcheck.invalidSource', 'Invalid link source.')));
} else if (that.isMounted() && that.$space.hasCallbacks()) {
var linkCheck = that.doCheckLink(obj);
if (linkCheck instanceof LinkCheck) {
cb.ok(linkCheck);
return cb.promise();
} else {
cb.addOk(function (ok, fail, serverLinkCheck) {
if (serverLinkCheck.isValid()) {
Promise.resolve(linkCheck)
.then(function (linkCheck) {
ok(linkCheck || serverLinkCheck);
})
.catch(fail);
} else {
ok(serverLinkCheck);
}
});
// If mounted then make a network call to get the link check.
// This ensures that BComponent.doCheckLink is called.
that.$space.getCallbacks().checkLink(source, sourceSlot, that, targetSlot, cb);
}
} else {
throw new Error(fromBajaLex('linkcheck.onlyMounted', 'Component.checkLink only supports mounted online components'));
}
} catch (err) {
cb.fail(err);
}
return cb.promise();
};
/**
* This is an override point to specify additional link checking
* between the specified source and the target slot. The default
* implementation is to return undefined, delegating the link checking to the
* station. If you know the link is valid without checking the station,
* return or resolve LinkCheck.makeValid(). If you know the link would result
* in an error condition then return or resolve to an invalid LinkCheck
* with the appropriate reason.
*
* @param {baja.LinkCheck~LinkCreateInfo} obj the object literal for the
* method's arguments. The `target` property will be set to this instance on
* which you're calling `doCheckLink()`.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
* LinkCheck will be passed as an argument to this function.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
*
* @returns {null|LinkCheck|Promise.<LinkCheck>} a falsy value to delegate
* the decision to the server. Otherwise, return a LinkCheck object, or a
* promise resolving to one.
*/
Component.prototype.doCheckLink = function (obj) {
};
/**
* Return the permissions for this `Component`.
*
* @returns {baja.Permissions}
*/
Component.prototype.getPermissions = function () {
var p = this.$permissions;
if (!p) {
if (typeof this.$permissionsStr === "string") {
p = this.$permissions = baja.Permissions.make(this.$permissionsStr);
} else {
p = baja.Permissions.all;
}
}
return p;
};
/**
* Return true if this Component is a Nav Child.
*
* @returns {Boolean} true if this Component is a Nav Child.
*/
Component.prototype.isNavChild = function () {
return this.$nc;
};
/**
* Returns a promise that resolves to the Component's tags. If the Component
* is mounted under a Proxy Component Space, a network call will be made
* for the Component's implied tags to include into the result.
*
* @param {Object} obj the object literal for the method's arguments.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
* collection of tags will be passed as an argument to this function.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
*
* @returns {Promise.<module:baja/tag/ComponentTags>} a promise that resolves
* to the Component's tags.
*
* @example
* component.tags({
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (tags) {
* baja.outln(tags.getAll().map(function (tag) {
* return tag.getId() + ' = ' + tag.getValue();
* }).join());
* })
* .catch(function (err) {
* baja.error('failed to retrieve tags: ' + err);
* });
*/
Component.prototype.tags = function (obj) {
obj = obj || {};
var that = this,
cb = new Callback(obj.ok, obj.fail, obj.batch);
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
if (that.isMounted() && that.$space.hasCallbacks()) {
cb.addOk(function (ok, fail, tags) {
ok(new SmartTags(new ComponentTags(that), tags));
});
that.$space.getCallbacks().getImpliedTags(that.getHandle(), cb);
} else {
cb.ok(new ComponentTags(that));
}
return cb.promise();
};
/**
* Returns a promise that resolves to the Component's relations. If the
* Component is mounted under a Proxy Component Space, a network call will be
* made for the Component's implied relations to include into the result.
*
* @param {Object} [obj] the object literal for the method's arguments.
* @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback. A
* link will be passed as an argument to this function.
* @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback.
* @param {baja.comm.Batch} [obj.batch] if defined, any network calls will be
* batched into this object.
*
* @returns {Promise.<baja/tag/ComponentRelations>} a promise that resolves
* to the Component's relations.
*
* @example
* component.relations({
* batch // if defined, any network calls will be batched into this object (optional)
* })
* .then(function (relations) {
* baja.outln(relations.getAll().map(function (relation) {
* return relation.getId() + ' = ' + relation.getEndpointOrd();
* }).join());
* })
* .catch(function (err) {
* baja.error('failed to retrieve relations: ' + err);
* });
*/
Component.prototype.relations = function (obj) {
obj = obj || {};
var that = this,
cb = new Callback(obj.ok, obj.fail, obj.batch);
// Ensure 'this' is Component in callbacks...
setContextInOkCallback(that, cb);
setContextInFailCallback(that, cb);
if (that.isMounted() && that.$space.hasCallbacks()) {
cb.addOk(function (ok, fail, relations) {
ok(new SmartRelations(new ComponentRelations(that), relations));
});
that.$space.getCallbacks().getImpliedRelations(that.getHandle(), cb);
} else {
cb.ok(new ComponentRelations(that));
}
return cb.promise();
};
/**
* Invokes a Niagara RPC call on the Component running in the Station.
* The method must implement the 'NiagaraRpc' annotation.
*
* Any extra arguments passed in will be encoded in raw JSON and passed up
* to the Server. If one of those arguments is a 'baja.comm.Batch', the
* call will be batched accordingly.
*
* @param {String} methodName The method name of the RPC call to invoke on
* the Server.
* @returns {Promise.<baja.Value|null>} A promise that is resolved once the
* RPC call has completed. If the Station RPC call returns a value then it
* will be encoded and returned as a value in the promise.
*/
Component.prototype.rpc = function (methodName) {
var args = Array.prototype.slice.call(arguments);
return baja.rpc.apply(baja.comm, [ this.getOrdInSession() ].concat(args));
};
/**
* Returns a promise that resolves to the agent list for this component.
*
* @see baja.registry.getAgents
*
* @param {Array<String>} [is] An optional array of filters to add to the
* agent query.
* @param {baja.comm.Batch} batch An optional object used to batch network
* calls together.
* @returns {Promise.<Array.<Object>>} A promise that will resolve with the
* Agent Info.
*/
Component.prototype.getAgents = function (is, batch) {
var that = this;
return baja.registry.getAgents(that.isMounted() ?
that.getOrdInSession() : "type:" + that.getType(), is, batch);
};
/**
* If the target is to a slot within this component
* then use the read permission based on the slot's
* operator flag. If the target is the component itself
* return if operator read is enabled.
*
* @since Niagara 4.12
* @param {module:baja/ord/OrdTarget} ordTarget
* @returns {boolean} true if the component has required permissions to be read
*/
Component.prototype.canRead = function (ordTarget) {
ordTargetMustBeToThisComponent(this, ordTarget);
const slot = ordTarget.getSlotInComponent();
return this.$canRead(slot && this.getSlot(slot));
};
/**
* @private
* @param {baja.Slot|string} [slot] the slot to check if I can read. If no slot specified, check
* to see if this component itself is readable - only operator read required. If I do not have
* the given slot, I can't read it.
* @returns {boolean}
* @see BComponent#canRead
*/
Component.prototype.$canRead = function (slot) {
const permissions = this.getPermissions();
if (!slot) {
return permissions.hasOperatorRead();
}
slot = this.getSlot(slot);
if (!slot) {
return false;
}
return permissions.hasAdminRead() || (hasOperatorFlag(slot) && permissions.hasOperatorRead());
};
/**
* If the target is to a slot within this component
* then use the write permission based on the slot's
* readonly and operator flags. If the target is the
* component itself return if operator write is enabled.
*
* @since Niagara 4.10
* @param {module:baja/ord/OrdTarget} ordTarget
* @returns {boolean} true if the component has required permissions to be written into
*/
Component.prototype.canWrite = function (ordTarget) {
ordTargetMustBeToThisComponent(this, ordTarget);
const slot = ordTarget.getSlotInComponent();
return this.$canWrite(slot && this.getSlot(slot));
};
/**
* @private
* @param {baja.Slot|string} [slot] the slot to check if I can write. If no slot specified, check
* to see if this component itself is writable - only operator writable required. If I do not have
* the given slot, I can't write to it.
* @returns {boolean}
* @see BComponent#canWrite
*/
Component.prototype.$canWrite = function (slot) {
const permissions = this.getPermissions();
if (!slot) {
return permissions.hasOperatorWrite();
}
slot = this.getSlot(slot);
if (!slot || hasReadonlyFlag(slot)) {
return false;
}
return permissions.hasAdminWrite() || (hasOperatorFlag(slot) && permissions.hasOperatorWrite());
};
/**
* If the target is to a slot within this component
* then use the invoke permission based on the slot's
* operator flag. If the target is the component itself
* return if operator invoke is enabled.
*
* @since Niagara 4.12
* @param {module:baja/ord/OrdTarget} ordTarget
* @returns {boolean} true if the component has required permissions to be invoked
*/
Component.prototype.canInvoke = function (ordTarget) {
ordTargetMustBeToThisComponent(this, ordTarget);
const slot = ordTarget.getSlotInComponent();
return this.$canInvoke(slot && this.getSlot(slot));
};
/**
* @private
* @param {baja.Slot|string} [slot] the slot to check if I can invoke. If no slot specified, check
* to see if this component itself is invokable - only operator invoke required. If I do not have
* the given slot, I can't invoke it.
* @returns {boolean}
* @see BComponent#canInvoke
*/
Component.prototype.$canInvoke = function (slot) {
const permissions = this.getPermissions();
if (!slot) {
return permissions.hasOperatorInvoke();
}
slot = this.getSlot(slot);
if (!slot) {
return false;
}
return permissions.hasAdminInvoke() || (hasOperatorFlag(slot) && permissions.hasOperatorInvoke());
};
/**
* @param {baja.Component} comp
* @param {module:baja/ord/OrdTarget} ordTarget
*/
function ordTargetMustBeToThisComponent(comp, ordTarget) {
if (ordTarget.getComponent() !== comp) {
throw new Error('OrdTarget is not to this component');
}
}
function fromBajaLex(key, def) {
return lexjs.$getSync({ module: 'baja', key, def });
}
return Component;
});