/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Logan Byam
*/
/**
* @module nmodule/webEditors/rc/fe/baja/BaseEditor
*/
define([
'baja!',
'log!nmodule.webEditors.rc.fe.baja.BaseEditor',
'jquery',
'Promise',
'underscore',
'nmodule/webEditors/rc/fe/BaseWidget',
'nmodule/webEditors/rc/fe/baja/util/compUtils',
'nmodule/webEditors/rc/fe/baja/util/facetsUtils',
'nmodule/webEditors/rc/fe/baja/util/typeUtils',
'nmodule/webEditors/rc/wb/mixin/mixinUtils',
'css!nmodule/webEditors/rc/fe/webEditors-structure' ], function (
baja,
log,
$,
Promise,
_,
BaseWidget,
compUtils,
facetsUtils,
typeUtils,
mixinUtils) {
'use strict';
const TYPE_CLASS_PREFIX = 'type-';
const TYPE_CLASS_REGEX = /^type-/;
const MIXIN_NAME = 'typeClasses';
const { getMergedSlotFacets } = compUtils;
const { applyFacets } = facetsUtils;
const { applyMixin, hasMixin } = mixinUtils;
const { getSuperTypeChain } = typeUtils;
const logWarning = log.warning.bind(log);
////////////////////////////////////////////////////////////////
// Support functions
////////////////////////////////////////////////////////////////
function isTypeClass(cls) {
return !!cls.match(TYPE_CLASS_REGEX);
}
function removeAllTypeClasses(dom) {
if (!dom) { return; }
dom.removeClass(function (i, classList) {
var allClasses = classList.split(' '),
typeClasses = _.filter(allClasses, isTypeClass);
return typeClasses.join(' ');
});
}
////////////////////////////////////////////////////////////////
// BaseEditor definition
////////////////////////////////////////////////////////////////
/**
* Base class for all `webEditors` editors for Baja values. This editor
* incorporates all the `Widget` sugar from `BaseWidget` and adds more
* Baja-specific features on top. Most Niagara field editors should extend
* from this class.
*
* @class
* @extends module:nmodule/webEditors/rc/fe/BaseWidget
* @alias module:nmodule/webEditors/rc/fe/baja/BaseEditor
* @param {Object} [params] Same parameters as
* {@link module:nmodule/webEditors/rc/fe/BaseWidget BaseWidget}
* @param {baja.Facets|Object} [params.facets] (Deprecated - use properties)
* facets to use to customize this editor instance. These facets will
* typically come from the slot facets of a Baja value being loaded into this
* BaseEditor. They will be converted into transient bajaux `Properties` and
* will override any other specified `Properties` with the same name. This can
* be either a `Facets` instance or an object literal.
*/
var BaseEditor = function BaseEditor(params) {
params = params || {};
var that = this,
data = params.data;
BaseWidget.apply(that, arguments);
that.$base = data && data.ordBase;
if (params.facets) {
logWarning(new Error('passing facets param to BaseEditor constructor is' +
' deprecated - use properties instead'));
that.setFacets(params.facets);
}
};
BaseEditor.prototype = Object.create(BaseWidget.prototype);
BaseEditor.prototype.constructor = BaseEditor;
//copy across just in case
BaseEditor.VALUE_READY_EVENT = BaseWidget.VALUE_READY_EVENT;
BaseEditor.$MIXIN_NAME = MIXIN_NAME;
/**
* Convert a BajaScript Type to a corresponding CSS class.
*
* @param {String|Type} type spec
* @returns {String}
* @example
* expect(BaseEditor.typeToClass('baja:String')).toBe('type-baja-String');
*/
BaseEditor.typeToClass = function (type) {
return TYPE_CLASS_PREFIX + String(type).replace(':', '-');
};
/**
* Convert the given Facets into hidden, transient `bajaux Properties` and
* apply them to this editor. In most cases you'll want to use
* `properties().setValue()` directly, but this method is useful when
* applying `Complex` slot facets.
*
* @param {baja.Facets|Object} facets (a `baja.Facets` instance or an object
* literal to be converted to `baja.Facets`)
*/
BaseEditor.prototype.setFacets = function (facets) {
applyFacets(facets, this);
};
function getTypeClasses(type) {
return _.map(getSuperTypeChain(type), BaseEditor.typeToClass);
}
/**
* Every `BaseEditor` will apply a number of CSS classes to a DOM element
* when a value is loaded into it:
*
* - `editor`
* - If the loaded value is a Baja value, a number of CSS classes
* corresponding to the value's Type and all superTypes. Classes will be
* determined using {@link module:nmodule/webEditors/rc/fe/baja/BaseEditor.typeToClass|typeToClass()}.
*
* It will also emit a `loaded` tinyevent.
*
* @param {baja.Value|*} value
* @param {Object} [params]
* @returns {Promise} call to {@link module:bajaux/Widget#load}
*/
BaseEditor.prototype.load = function (value, params) {
var that = this;
return BaseWidget.prototype.load.apply(that, arguments)
.then(function (result) {
var dom = that.jq();
//TODO: switchboard notWhile
if (!dom) {
throw new Error('already destroyed');
}
that.$updateTypeClasses(value, dom);
return result;
});
};
/**
* @private
* @param {*} value
* @param {JQuery} dom
*/
BaseEditor.prototype.$updateTypeClasses = function (value, dom) {
removeAllTypeClasses(dom);
if (baja.hasType(value)) {
dom.addClass(getTypeClasses(value.getType()).join(' '));
}
};
/**
* Removes all classes added during a call to {@link #load}. Emits a
* `destroyed` tinyevent.
*
* @returns {Promise} call to {@link module:bajaux/Widget#destroy}
*/
BaseEditor.prototype.destroy = function () {
const jq = this.jq();
return BaseWidget.prototype.destroy.apply(this, arguments)
.then(() => {
removeAllTypeClasses(jq);
});
};
/**
* Applies the legacy behavior of applying the "type-moduleName-TypeName" CSS classes when a
* value is loaded.
* @private
* @param {module:bajaux/Widget} widget
*/
BaseEditor.$mixinTypeClasses = function (widget) {
if (widget instanceof BaseEditor || hasMixin(widget, MIXIN_NAME)) {
return;
}
applyMixin(widget, MIXIN_NAME);
const { load, destroy } = widget;
widget.$updateTypeClasses = widget.$updateTypeClasses || BaseEditor.prototype.$updateTypeClasses;
widget.load = function (value) {
return load.apply(this, arguments)
.then((result) => {
const dom = this.jq();
//TODO: switchboard notWhile
if (!dom) {
throw new Error('already destroyed');
}
this.$updateTypeClasses(value, dom);
dom.addClass('editor');
return result;
});
};
widget.destroy = function () {
const dom = this.jq();
return destroy.apply(this, arguments)
.then(() => {
if (dom) {
dom.removeClass('editor');
removeAllTypeClasses(dom);
}
});
};
};
/**
* There are two main cases for using an editor.
*
* In one case, you are editing a standalone value. For example, you are
* prompting the user for a `baja:String`, and the read value will simply be
* passed into another function for handling - nothing will necessarily be
* saved up to the station.
*
* In the second case, you are editing a slot on a Complex. In this case,
* when the editor is saved, you want the saved value to be written back to
* a particular slot on the Complex.
*
* This function indicates which case is currently in play. When an editor
* is a complex slot editor, the load/read semantics are the same, but saving
* the editor should cause the read value to be written back to the edited
* Slot.
*
* To create a complex slot editor, see `ComplexSlotEditor.make`.
*
* @private
* @returns {boolean}
* @see module:nmodule/webEditors/rc/fe/baja/ComplexSlotEditor
*/
BaseEditor.prototype.isComplexSlotEditor = function () {
return false;
};
/**
* If this editor is a complex slot editor, then when it is saved, the read
* value will be written back to this Complex.
*
* This returns `undefined` by default. It should return a Complex if and only
* if it is a complex slot editor (`isComplexSlotEditor()` returns true).
*
* @private
* @returns {undefined|baja.Complex} the Complex that saved changes will
* be written back to
*/
BaseEditor.prototype.getComplex = function () {
return undefined;
};
/**
* If this editor is a complex slot editor, then when it is saved, the read
* value will be written to the backing Complex at this slot.
*
* This returns `undefined` by default. It should return a `baja.Slot` if and
* only if it is a complex slot editor (`isComplexSlotEditor()` returns
* true).
*
* @private
* @returns {undefined|baja.Slot} the slot on the backing Complex to write
* saved changes to
*/
BaseEditor.prototype.getSlot = function () {
return undefined;
};
//TODO: this is exactly the kind of TODO that was called for on makeChildFor.
//come back and replace ordBase shuffling.
/**
* Sometimes, an editor may need to resolve ORDs in order to operate; for
* instance, resolving the RoleService for a list of roles, the SearchService
* to perform a search, etc. ORD resolution when BajaScript is in offline mode
* will often require a mounted base component to resolve the ORD against.
*
* This base object may be given as an `ordBase` bajaux `Property`.
*
* See: `spaceUtils.resolveService`, etc.
*
* @private
* @returns {Promise.<baja.Component|undefined>} promise to be resolved with a
* Component to use as a base when resolving ORDs, or `undefined` if not
* present
*/
BaseEditor.prototype.getOrdBase = function () {
return Promise.resolve(
this.properties().getValue('ordBase') || this.getComplex());
};
/**
* When this editor is a complex slot editor, the facets for that slot will
* be translated to hidden properties on this editor. By default, the facets
* will be retrieved directly from the slot (`getSlot()`) on the complex
* (`getComplex()`). By overriding this function, you can define the way the
* slot facets are retrieved.
*
* Why would you want to override this function? In the Niagara Framework,
* the `getSlotFacets` method is very often overridden on `BComplex`
* subclasses, most commonly to pull down facets from a parent component.
* BajaScript does not necessarily know about those overridden
* slot facets. By overriding this function instead, you can bring that
* functionality into your field editors even if it does not yet exist in
* BajaScript.
*
* This should not typically be called directly (hence the `$`) but will be
* called automatically by the framework when editing a slot on a complex.
*
* As of 4.3, this was marked private. BajaScript Type Extensions allow for
* the overriding of getSlotFacets() in the browser, rendering this function
* mostly unnecessary.
*
* @private
* @returns {Promise} promise to be resolved with the facets for the
* configured Complex and Slot, or `baja.Facets.DEFAULT` if not present
*/
BaseEditor.prototype.$getSlotFacets = function () {
var complex = this.getComplex(),
slot = this.getSlot();
return Promise.resolve((complex && slot && getMergedSlotFacets(complex, slot)) ||
baja.Facets.DEFAULT);
};
/**
* Currently, only `ComplexSlotEditor` will use this function. It functions
* as an override point; there is no real reason to call it directly.
* PropertySheet saves have always called this method.
* Starting in Niagara 4.14, `Manager` based Dialogs started calling this method.
* Note that the New and Add Commands on a Manager will call this method, but with an unmounted
* complex. You can detect this scenario by checking if `this.getComplex().isMounted()` is false.
* Any behavior in this function that is needed for a mounted Complex should
* be replicated in your subclass of MgrModel#addInstances.
* @see module:nmodule/webEditors/rc/wb/mgr/model/MgrModel#addInstances
*
* In certain rare use cases, you may want finer control over how the
* value is saved back to the `Complex`: changing slot flags, for instance,
* or delegating to a separate service. In this case, override this function
* to perform the work you need, then return a truthy value to indicate
* that the work was done.
*
* @private
* @param {baja.Value|module:nmodule/webEditors/rc/fe/baja/util/ComplexDiff} readValue
* the Baja value to save to `this.getSlot()` on `this.getComplex()`
* @param {Object} [params] same parameters that may be passed to `save()`
* @param {baja.comm.Batch} [params.batch] optional batch to use
* @param {function} [params.progressCallback] if present, `saveToComplex` implementations
* *must* call it with `batchSaveMixin.COMMIT_READY` after adding any network calls to
* `params.batch`.
* @returns {undefined|Promise|*} undefined by default, to indicate
* that no work was done. When overriding, return a truthy value to
* short-circuit default `ComplexSlotEditor` save behavior.
*/
BaseEditor.prototype.saveToComplex = function (readValue, params) {
return undefined;
};
/**
* Same as `getChildWidgets`, but is limited to instances of `BaseEditor`.
* @deprecated use `getChildWidgets` instead.
* @param {Object} [params]
*/
BaseEditor.prototype.getChildEditors = function (params) {
params = !params ? {} : params instanceof $ ? { dom: params } : params;
return this.getChildWidgets(_.extend({
filter: (ed) => ed instanceof BaseEditor || isHonoraryBaseEditor(ed)
}, params));
};
/**
* NCCB-28133: this goes away
* @param {module:bajaux/Widget} ed
* @returns {boolean}
*/
function isHonoraryBaseEditor(ed) {
return ed && ed.$isHonoraryBaseEditor;
}
// copy over for API compatibility
BaseEditor.SHOULD_VALIDATE = BaseWidget.SHOULD_VALIDATE;
BaseEditor.VALIDATE_ON_SAVE = BaseWidget.VALIDATE_ON_SAVE;
BaseEditor.VALIDATE_ON_READ = BaseWidget.VALIDATE_ON_READ;
return (BaseEditor);
});