[Code Style] Use prototypes in Edit bundle

WTD-1482.
This commit is contained in:
Victor Woeltjen 2015-08-10 16:38:13 -07:00
parent efc42aa8f2
commit be5cad212a
25 changed files with 766 additions and 790 deletions

@ -31,9 +31,24 @@ define(
* capabilities to persist the changes that have been made. * capabilities to persist the changes that have been made.
* @constructor * @constructor
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {Action}
*/ */
function CancelAction($location, urlService, context) { function CancelAction($location, urlService, context) {
var domainObject = context.domainObject; this.domainObject = context.domainObject;
this.$location = $location;
this.urlService = urlService;
}
/**
* Cancel editing.
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
*/
CancelAction.prototype.perform = function () {
var domainObject = this.domainObject,
$location = this.$location,
urlService = this.urlService;
// Look up the object's "editor.completion" capability; // Look up the object's "editor.completion" capability;
// this is introduced by EditableDomainObject which is // this is introduced by EditableDomainObject which is
@ -58,26 +73,15 @@ define(
))); )));
} }
return { return doCancel(getEditorCapability())
/** .then(returnToBrowse);
* Cancel editing. };
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
* @memberof platform/commonUI/edit.CancelAction#
*/
perform: function () {
return doCancel(getEditorCapability())
.then(returnToBrowse);
}
};
}
/** /**
* Check if this action is applicable in a given context. * Check if this action is applicable in a given context.
* This will ensure that a domain object is present in the context, * This will ensure that a domain object is present in the context,
* and that this domain object is in Edit mode. * and that this domain object is in Edit mode.
* @returns true if applicable * @returns {boolean} true if applicable
*/ */
CancelAction.appliesTo = function (context) { CancelAction.appliesTo = function (context) {
var domainObject = (context || {}).domainObject; var domainObject = (context || {}).domainObject;

@ -44,6 +44,7 @@ define(
* route) * route)
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @constructor * @constructor
* @implements {Action}
*/ */
function EditAction($location, navigationService, $log, context) { function EditAction($location, navigationService, $log, context) {
var domainObject = (context || {}).domainObject; var domainObject = (context || {}).domainObject;
@ -61,18 +62,19 @@ define(
return NULL_ACTION; return NULL_ACTION;
} }
return { this.domainObject = domainObject;
/** this.$location = $location;
* Enter edit mode. this.navigationService = navigationService;
* @memberof platform/commonUI/edit.EditAction#
*/
perform: function () {
navigationService.setNavigation(domainObject);
$location.path("/edit");
}
};
} }
/**
* Enter edit mode.
*/
EditAction.prototype.perform = function () {
this.navigationService.setNavigation(this.domainObject);
this.$location.path("/edit");
};
/** /**
* Check for applicability; verify that a domain object is present * Check for applicability; verify that a domain object is present
* for this action to be performed upon. * for this action to be performed upon.

@ -31,42 +31,40 @@ define(
* Add one domain object to another's composition. * Add one domain object to another's composition.
* @constructor * @constructor
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {Action}
*/ */
function LinkAction(context) { function LinkAction(context) {
var domainObject = (context || {}).domainObject, this.domainObject = (context || {}).domainObject;
selectedObject = (context || {}).selectedObject, this.selectedObject = (context || {}).selectedObject;
selectedId = selectedObject && selectedObject.getId(); this.selectedId = this.selectedObject && this.selectedObject.getId();
}
LinkAction.prototype.perform = function () {
var self = this;
// Add this domain object's identifier // Add this domain object's identifier
function addId(model) { function addId(model) {
if (Array.isArray(model.composition) && if (Array.isArray(model.composition) &&
model.composition.indexOf(selectedId) < 0) { model.composition.indexOf(self.selectedId) < 0) {
model.composition.push(selectedId); model.composition.push(self.selectedId);
} }
} }
// Persist changes to the domain object // Persist changes to the domain object
function doPersist() { function doPersist() {
var persistence = domainObject.getCapability('persistence'); var persistence =
self.domainObject.getCapability('persistence');
return persistence.persist(); return persistence.persist();
} }
// Link these objects // Link these objects
function doLink() { function doLink() {
return domainObject.useCapability("mutation", addId) return self.domainObject.useCapability("mutation", addId)
.then(doPersist); .then(doPersist);
} }
return { return this.selectedId && doLink();
/** };
* Perform this action.
* @memberof platform/commonUI/edit.LinkAction#
*/
perform: function () {
return selectedId && doLink();
}
};
}
return LinkAction; return LinkAction;
} }

@ -32,60 +32,58 @@ define(
'use strict'; 'use strict';
/** /**
* Construct an action which will allow an object's metadata to be * Implements the "Edit Properties" action, which prompts the user
* edited. * to modify a domain object's properties.
* *
* @param {DialogService} dialogService a service which will show the dialog * @param {DialogService} dialogService a service which will show the dialog
* @param {DomainObject} object the object to be edited * @param {DomainObject} object the object to be edited
* @param {ActionContext} context the context in which this action is performed * @param {ActionContext} context the context in which this action is performed
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {Action}
* @constructor * @constructor
*/ */
function PropertiesAction(dialogService, context) { function PropertiesAction(dialogService, context) {
var object = context.domainObject; this.domainObject = (context || {}).domainObject;
this.dialogService = dialogService;
}
PropertiesAction.prototype.perform = function () {
var type = this.domainObject.getCapability('type'),
domainObject = this.domainObject,
dialogService = this.dialogService;
// Persist modifications to this domain object // Persist modifications to this domain object
function doPersist() { function doPersist() {
var persistence = object.getCapability('persistence'); var persistence = domainObject.getCapability('persistence');
return persistence && persistence.persist(); return persistence && persistence.persist();
} }
// Update the domain object model based on user input // Update the domain object model based on user input
function updateModel(userInput, dialog) { function updateModel(userInput, dialog) {
return object.useCapability('mutation', function (model) { return domainObject.useCapability('mutation', function (model) {
dialog.updateModel(model, userInput); dialog.updateModel(model, userInput);
}); });
} }
function showDialog(type) { function showDialog(type) {
// Create a dialog object to generate the form structure, etc. // Create a dialog object to generate the form structure, etc.
var dialog = new PropertiesDialog(type, object.getModel()); var dialog =
new PropertiesDialog(type, domainObject.getModel());
// Show the dialog // Show the dialog
return dialogService.getUserInput( return dialogService.getUserInput(
dialog.getFormStructure(), dialog.getFormStructure(),
dialog.getInitialFormValue() dialog.getInitialFormValue()
).then(function (userInput) { ).then(function (userInput) {
// Update the model, if user input was provided // Update the model, if user input was provided
return userInput && updateModel(userInput, dialog); return userInput && updateModel(userInput, dialog);
}).then(function (result) { }).then(function (result) {
return result && doPersist(); return result && doPersist();
}); });
} }
return { return type && showDialog(type);
/** };
* Perform this action.
* @return {Promise} a promise which will be
* fulfilled when the action has completed.
* @memberof platform/commonUI/edit.PropertiesAction#
*/
perform: function () {
var type = object.getCapability('type');
return type && showDialog(type);
}
};
}
/** /**
* Filter this action for applicability against a given context. * Filter this action for applicability against a given context.

@ -35,57 +35,56 @@ define(
* @constructor * @constructor
*/ */
function PropertiesDialog(type, model) { function PropertiesDialog(type, model) {
var properties = type.getProperties(); this.type = type;
this.model = model;
return { this.properties = type.getProperties();
/**
* Get sections provided by this dialog.
* @return {FormStructure} the structure of this form
* @memberof platform/commonUI/edit.PropertiesDialog#
*/
getFormStructure: function () {
return {
name: "Edit " + model.name,
sections: [{
name: "Properties",
rows: properties.map(function (property, index) {
// Property definition is same as form row definition
var row = Object.create(property.getDefinition());
row.key = index;
return row;
})
}]
};
},
/**
* Get the initial state of the form shown by this dialog
* (based on the object model)
* @returns {object} initial state of the form
* @memberof platform/commonUI/edit.PropertiesDialog#
*/
getInitialFormValue: function () {
// Start with initial values for properties
// Note that index needs to correlate to row.key
// from getFormStructure
return properties.map(function (property) {
return property.getValue(model);
});
},
/**
* Update a domain object model based on the value of a form.
* @memberof platform/commonUI/edit.PropertiesDialog#
*/
updateModel: function (model, formValue) {
// Update all properties
properties.forEach(function (property, index) {
property.setValue(model, formValue[index]);
});
}
};
} }
/**
* Get sections provided by this dialog.
* @return {FormStructure} the structure of this form
*/
PropertiesDialog.prototype.getFormStructure = function () {
return {
name: "Edit " + this.model.name,
sections: [{
name: "Properties",
rows: this.properties.map(function (property, index) {
// Property definition is same as form row definition
var row = Object.create(property.getDefinition());
row.key = index;
return row;
})
}]
};
};
/**
* Get the initial state of the form shown by this dialog
* (based on the object model)
* @returns {object} initial state of the form
*/
PropertiesDialog.prototype.getInitialFormValue = function () {
var model = this.model;
// Start with initial values for properties
// Note that index needs to correlate to row.key
// from getFormStructure
return this.properties.map(function (property) {
return property.getValue(model);
});
};
/**
* Update a domain object model based on the value of a form.
*/
PropertiesDialog.prototype.updateModel = function (model, formValue) {
// Update all properties
this.properties.forEach(function (property, index) {
property.setValue(model, formValue[index]);
});
};
return PropertiesDialog; return PropertiesDialog;
} }
); );

@ -39,68 +39,64 @@ define(
* @param {ActionContext} context the context in which this action is performed * @param {ActionContext} context the context in which this action is performed
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @constructor * @constructor
* @implements {Action}
*/ */
function RemoveAction($q, context) { function RemoveAction($q, context) {
var object = (context || {}).domainObject; this.domainObject = (context || {}).domainObject;
this.$q = $q;
}
/** /**
* Perform this action.
* @return {Promise} a promise which will be
* fulfilled when the action has completed.
*/
RemoveAction.prototype.perform = function () {
var $q = this.$q,
domainObject = this.domainObject;
/*
* Check whether an object ID matches the ID of the object being * Check whether an object ID matches the ID of the object being
* removed (used to filter a parent's composition to handle the * removed (used to filter a parent's composition to handle the
* removal.) * removal.)
* @memberof platform/commonUI/edit.RemoveAction#
*/ */
function isNotObject(otherObjectId) { function isNotObject(otherObjectId) {
return otherObjectId !== object.getId(); return otherObjectId !== domainObject.getId();
} }
/** /*
* Mutate a parent object such that it no longer contains the object * Mutate a parent object such that it no longer contains the object
* which is being removed. * which is being removed.
* @memberof platform/commonUI/edit.RemoveAction#
*/ */
function doMutate(model) { function doMutate(model) {
model.composition = model.composition.filter(isNotObject); model.composition = model.composition.filter(isNotObject);
} }
/** /*
* Invoke persistence on a domain object. This will be called upon * Invoke persistence on a domain object. This will be called upon
* the removed object's parent (as its composition will have changed.) * the removed object's parent (as its composition will have changed.)
* @memberof platform/commonUI/edit.RemoveAction#
*/ */
function doPersist(domainObject) { function doPersist(domainObject) {
var persistence = domainObject.getCapability('persistence'); var persistence = domainObject.getCapability('persistence');
return persistence && persistence.persist(); return persistence && persistence.persist();
} }
/** /*
* Remove the object from its parent, as identified by its context * Remove the object from its parent, as identified by its context
* capability. * capability.
* @param {ContextCapability} contextCapability the "context" capability
* of the domain object being removed.
* @memberof platform/commonUI/edit.RemoveAction#
*/ */
function removeFromContext(contextCapability) { function removeFromContext(contextCapability) {
var parent = contextCapability.getParent(); var parent = contextCapability.getParent();
$q.when( return $q.when(
parent.useCapability('mutation', doMutate) parent.useCapability('mutation', doMutate)
).then(function () { ).then(function () {
return doPersist(parent); return doPersist(parent);
}); });
} }
return { return $q.when(this.domainObject.getCapability('context'))
/** .then(removeFromContext);
* Perform this action. };
* @return {module:core/promises.Promise} a promise which will be
* fulfilled when the action has completed.
* @memberof platform/commonUI/edit.RemoveAction#
*/
perform: function () {
return $q.when(object.getCapability('context'))
.then(removeFromContext);
}
};
}
// Object needs to have a parent for Remove to be applicable // Object needs to have a parent for Remove to be applicable
RemoveAction.appliesTo = function (context) { RemoveAction.appliesTo = function (context) {

@ -31,10 +31,26 @@ define(
* Edit Mode. Exits the editing user interface and invokes object * Edit Mode. Exits the editing user interface and invokes object
* capabilities to persist the changes that have been made. * capabilities to persist the changes that have been made.
* @constructor * @constructor
* @implements {Action}
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
*/ */
function SaveAction($location, urlService, context) { function SaveAction($location, urlService, context) {
var domainObject = context.domainObject; this.domainObject = (context || {}).domainObject;
this.$location = $location;
this.urlService = urlService;
}
/**
* Save changes and conclude editing.
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
* @memberof platform/commonUI/edit.SaveAction#
*/
SaveAction.prototype.perform = function () {
var domainObject = this.domainObject,
$location = this.$location,
urlService = this.urlService;
// Invoke any save behavior introduced by the editor capability; // Invoke any save behavior introduced by the editor capability;
// this is introduced by EditableDomainObject which is // this is introduced by EditableDomainObject which is
@ -53,19 +69,8 @@ define(
)); ));
} }
return { return doSave().then(returnToBrowse);
/** };
* Save changes and conclude editing.
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
* @memberof platform/commonUI/edit.SaveAction#
*/
perform: function () {
return doSave().then(returnToBrowse);
}
};
}
/** /**
* Check if this action is applicable in a given context. * Check if this action is applicable in a given context.

@ -37,6 +37,7 @@ define(
* to a pattern used there and may contain unused arguments. * to a pattern used there and may contain unused arguments.
* @constructor * @constructor
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {CompositionCapability}
*/ */
return function EditableCompositionCapability( return function EditableCompositionCapability(
contextCapability, contextCapability,

@ -37,6 +37,7 @@ define(
* to a pattern used there and may contain unused arguments. * to a pattern used there and may contain unused arguments.
* @constructor * @constructor
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {ContextCapability}
*/ */
return function EditableContextCapability( return function EditableContextCapability(
contextCapability, contextCapability,

@ -37,6 +37,7 @@ define(
* to a pattern used there and may contain unused arguments. * to a pattern used there and may contain unused arguments.
* @constructor * @constructor
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {PersistenceCapability}
*/ */
function EditablePersistenceCapability( function EditablePersistenceCapability(
persistenceCapability, persistenceCapability,

@ -37,6 +37,7 @@ define(
* to a pattern used there and may contain unused arguments. * to a pattern used there and may contain unused arguments.
* @constructor * @constructor
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {RelationshipCapability}
*/ */
return function EditableRelationshipCapability( return function EditableRelationshipCapability(
relationshipCapability, relationshipCapability,

@ -42,26 +42,45 @@ define(
* @constructor * @constructor
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
*/ */
return function EditorCapability( function EditorCapability(
persistenceCapability, persistenceCapability,
editableObject, editableObject,
domainObject, domainObject,
cache cache
) { ) {
this.editableObject = editableObject;
this.domainObject = domainObject;
this.cache = cache;
}
// Simulate Promise.resolve (or $q.when); the former // Simulate Promise.resolve (or $q.when); the former
// causes a delayed reaction from Angular (since it // causes a delayed reaction from Angular (since it
// does not trigger a digest) and the latter is not // does not trigger a digest) and the latter is not
// readily accessible, since we're a few classes // readily accessible, since we're a few classes
// removed from the layer which gets dependency // removed from the layer which gets dependency
// injection. // injection.
function resolvePromise(value) { function resolvePromise(value) {
return (value && value.then) ? value : { return (value && value.then) ? value : {
then: function (callback) { then: function (callback) {
return resolvePromise(callback(value)); return resolvePromise(callback(value));
} }
}; };
} }
/**
* Save any changes that have been made to this domain object
* (as well as to others that might have been retrieved and
* modified during the editing session)
* @param {boolean} nonrecursive if true, save only this
* object (and not other objects with associated changes)
* @returns {Promise} a promise that will be fulfilled after
* persistence has completed.
* @memberof platform/commonUI/edit.EditorCapability#
*/
EditorCapability.prototype.save = function (nonrecursive) {
var domainObject = this.domainObject,
editableObject = this.editableObject,
cache = this.cache;
// Update the underlying, "real" domain object's model // Update the underlying, "real" domain object's model
// with changes made to the copy used for editing. // with changes made to the copy used for editing.
@ -76,42 +95,32 @@ define(
return domainObject.getCapability('persistence').persist(); return domainObject.getCapability('persistence').persist();
} }
return { return nonrecursive ?
/** resolvePromise(doMutate()).then(doPersist) :
* Save any changes that have been made to this domain object resolvePromise(cache.saveAll());
* (as well as to others that might have been retrieved and
* modified during the editing session)
* @param {boolean} nonrecursive if true, save only this
* object (and not other objects with associated changes)
* @returns {Promise} a promise that will be fulfilled after
* persistence has completed.
* @memberof platform/commonUI/edit.EditorCapability#
*/
save: function (nonrecursive) {
return nonrecursive ?
resolvePromise(doMutate()).then(doPersist) :
resolvePromise(cache.saveAll());
},
/**
* Cancel editing; Discard any changes that have been made to
* this domain object (as well as to others that might have
* been retrieved and modified during the editing session)
* @returns {Promise} a promise that will be fulfilled after
* cancellation has completed.
* @memberof platform/commonUI/edit.EditorCapability#
*/
cancel: function () {
return resolvePromise(undefined);
},
/**
* Check if there are any unsaved changes.
* @returns {boolean} true if there are unsaved changes
* @memberof platform/commonUI/edit.EditorCapability#
*/
dirty: function () {
return cache.dirty();
}
};
}; };
/**
* Cancel editing; Discard any changes that have been made to
* this domain object (as well as to others that might have
* been retrieved and modified during the editing session)
* @returns {Promise} a promise that will be fulfilled after
* cancellation has completed.
* @memberof platform/commonUI/edit.EditorCapability#
*/
EditorCapability.prototype.cancel = function () {
return resolvePromise(undefined);
};
/**
* Check if there are any unsaved changes.
* @returns {boolean} true if there are unsaved changes
* @memberof platform/commonUI/edit.EditorCapability#
*/
EditorCapability.prototype.dirty = function () {
return cache.dirty();
};
return EditorCapability;
} }
); );

@ -38,12 +38,12 @@ define(
* @constructor * @constructor
*/ */
function EditController($scope, $q, navigationService) { function EditController($scope, $q, navigationService) {
var navigatedObject; var self = this;
function setNavigation(domainObject) { function setNavigation(domainObject) {
// Wrap the domain object such that all mutation is // Wrap the domain object such that all mutation is
// confined to edit mode (until Save) // confined to edit mode (until Save)
navigatedObject = self.navigatedDomainObject =
domainObject && new EditableDomainObject(domainObject, $q); domainObject && new EditableDomainObject(domainObject, $q);
} }
@ -52,35 +52,33 @@ define(
$scope.$on("$destroy", function () { $scope.$on("$destroy", function () {
navigationService.removeListener(setNavigation); navigationService.removeListener(setNavigation);
}); });
return {
/**
* Get the domain object which is navigated-to.
* @returns {DomainObject} the domain object that is navigated-to
* @memberof platform/commonUI/edit.EditController#
*/
navigatedObject: function () {
return navigatedObject;
},
/**
* Get the warning to show if the user attempts to navigate
* away from Edit mode while unsaved changes are present.
* @returns {string} the warning to show, or undefined if
* there are no unsaved changes
* @memberof platform/commonUI/edit.EditController#
*/
getUnloadWarning: function () {
var editorCapability = navigatedObject &&
navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty();
return hasChanges ?
"Unsaved changes will be lost if you leave this page." :
undefined;
}
};
} }
/**
* Get the domain object which is navigated-to.
* @returns {DomainObject} the domain object that is navigated-to
*/
EditController.prototype.navigatedObject = function () {
return this.navigatedDomainObject;
};
/**
* Get the warning to show if the user attempts to navigate
* away from Edit mode while unsaved changes are present.
* @returns {string} the warning to show, or undefined if
* there are no unsaved changes
*/
EditController.prototype.getUnloadWarning = function () {
var navigatedObject = this.navigatedDomainObject,
editorCapability = navigatedObject &&
navigatedObject.getCapability("editor"),
hasChanges = editorCapability && editorCapability.dirty();
return hasChanges ?
"Unsaved changes will be lost if you leave this page." :
undefined;
};
return EditController; return EditController;
} }
); );

@ -32,12 +32,13 @@ define(
* @constructor * @constructor
*/ */
function EditPanesController($scope) { function EditPanesController($scope) {
var root; var self = this;
// Update root object based on represented object // Update root object based on represented object
function updateRoot(domainObject) { function updateRoot(domainObject) {
var context = domainObject && var root = self.rootDomainObject,
domainObject.getCapability('context'), context = domainObject &&
domainObject.getCapability('context'),
newRoot = context && context.getTrueRoot(), newRoot = context && context.getTrueRoot(),
oldId = root && root.getId(), oldId = root && root.getId(),
newId = newRoot && newRoot.getId(); newId = newRoot && newRoot.getId();
@ -45,25 +46,21 @@ define(
// Only update if this has actually changed, // Only update if this has actually changed,
// to avoid excessive refreshing. // to avoid excessive refreshing.
if (oldId !== newId) { if (oldId !== newId) {
root = newRoot; self.rootDomainObject = newRoot;
} }
} }
// Update root when represented object changes // Update root when represented object changes
$scope.$watch('domainObject', updateRoot); $scope.$watch('domainObject', updateRoot);
return {
/**
* Get the root-level domain object, as reported by the
* represented domain object.
* @returns {DomainObject} the root object
* @memberof platform/commonUI/edit.EditPanesController#
*/
getRoot: function () {
return root;
}
};
} }
/**
* Get the root-level domain object, as reported by the
* represented domain object.
* @returns {DomainObject} the root object
*/
EditPanesController.prototype.getRoot = function () {
return this.rootDomainObject;
};
return EditPanesController; return EditPanesController;
} }

@ -70,6 +70,7 @@ define(
* model to allow changes to be easily cancelled. * model to allow changes to be easily cancelled.
* @constructor * @constructor
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {DomainObject}
*/ */
function EditableDomainObject(domainObject, $q) { function EditableDomainObject(domainObject, $q) {
// The cache will hold all domain objects reached from // The cache will hold all domain objects reached from
@ -94,10 +95,10 @@ define(
this, this,
delegateArguments delegateArguments
), ),
factory = capabilityFactories[name]; Factory = capabilityFactories[name];
return (factory && capability) ? return (Factory && capability) ?
factory(capability, editableObject, domainObject, cache) : new Factory(capability, editableObject, domainObject, cache) :
capability; capability;
}; };

@ -44,7 +44,7 @@ define(
* of objects retrieved via composition or context capabilities as * of objects retrieved via composition or context capabilities as
* editable domain objects. * editable domain objects.
* *
* @param {Constructor<EditableDomainObject>} EditableDomainObject a * @param {Constructor<DomainObject>} EditableDomainObject a
* constructor function which takes a regular domain object as * constructor function which takes a regular domain object as
* an argument, and returns an editable domain object as its * an argument, and returns an editable domain object as its
* result. * result.
@ -53,104 +53,108 @@ define(
* @constructor * @constructor
*/ */
function EditableDomainObjectCache(EditableDomainObject, $q) { function EditableDomainObjectCache(EditableDomainObject, $q) {
var cache = new EditableModelCache(), this.cache = new EditableModelCache();
dirty = {}, this.dirtyObjects = {};
root; this.root = undefined;
this.$q = $q;
return { this.EditableDomainObject = EditableDomainObject;
/**
* Wrap this domain object in an editable form, or pull such
* an object from the cache if one already exists.
*
* @param {DomainObject} domainObject the regular domain object
* @returns {DomainObject} the domain object in an editable form
* @memberof platform/commonUI/edit.EditableDomainObjectCache#
*/
getEditableObject: function (domainObject) {
var type = domainObject.getCapability('type');
// Track the top-level domain object; this will have
// some special behavior for its context capability.
root = root || domainObject;
// Avoid double-wrapping (WTD-1017)
if (domainObject.hasCapability('editor')) {
return domainObject;
}
// Don't bother wrapping non-editable objects
if (!type || !type.hasFeature('creation')) {
return domainObject;
}
// Provide an editable form of the object
return new EditableDomainObject(
domainObject,
cache.getCachedModel(domainObject)
);
},
/**
* Check if a domain object is (effectively) the top-level
* object in this editable subgraph.
* @returns {boolean} true if it is the root
* @memberof platform/commonUI/edit.EditableDomainObjectCache#
*/
isRoot: function (domainObject) {
return domainObject === root;
},
/**
* Mark an editable domain object (presumably already cached)
* as having received modifications during editing; it should be
* included in the bulk save invoked when editing completes.
*
* @param {DomainObject} domainObject the domain object
* @memberof platform/commonUI/edit.EditableDomainObjectCache#
*/
markDirty: function (domainObject) {
dirty[domainObject.getId()] = domainObject;
},
/**
* Mark an object (presumably already cached) as having had its
* changes saved (and thus no longer needing to be subject to a
* save operation.)
*
* @param {DomainObject} domainObject the domain object
* @memberof platform/commonUI/edit.EditableDomainObjectCache#
*/
markClean: function (domainObject) {
delete dirty[domainObject.getId()];
},
/**
* Initiate a save on all objects that have been cached.
* @memberof platform/commonUI/edit.EditableDomainObjectCache#
*/
saveAll: function () {
// Get a list of all dirty objects
var objects = Object.keys(dirty).map(function (k) {
return dirty[k];
});
// Clear dirty set, since we're about to save.
dirty = {};
// Most save logic is handled by the "editor.completion"
// capability, so that is delegated here.
return $q.all(objects.map(function (object) {
// Save; pass a nonrecursive flag to avoid looping
return object.getCapability('editor').save(true);
}));
},
/**
* Check if any objects have been marked dirty in this cache.
* @returns {boolean} true if objects are dirty
* @memberof platform/commonUI/edit.EditableDomainObjectCache#
*/
dirty: function () {
return Object.keys(dirty).length > 0;
}
};
} }
/**
* Wrap this domain object in an editable form, or pull such
* an object from the cache if one already exists.
*
* @param {DomainObject} domainObject the regular domain object
* @returns {DomainObject} the domain object in an editable form
*/
EditableDomainObjectCache.prototype.getEditableObject = function (domainObject) {
var type = domainObject.getCapability('type'),
EditableDomainObject = this.EditableDomainObject;
// Track the top-level domain object; this will have
// some special behavior for its context capability.
this.root = this.root || domainObject;
// Avoid double-wrapping (WTD-1017)
if (domainObject.hasCapability('editor')) {
return domainObject;
}
// Don't bother wrapping non-editable objects
if (!type || !type.hasFeature('creation')) {
return domainObject;
}
// Provide an editable form of the object
return new EditableDomainObject(
domainObject,
this.cache.getCachedModel(domainObject)
);
};
/**
* Check if a domain object is (effectively) the top-level
* object in this editable subgraph.
* @returns {boolean} true if it is the root
*/
EditableDomainObjectCache.prototype.isRoot = function (domainObject) {
return domainObject === this.root;
};
/**
* Mark an editable domain object (presumably already cached)
* as having received modifications during editing; it should be
* included in the bulk save invoked when editing completes.
*
* @param {DomainObject} domainObject the domain object
* @memberof platform/commonUI/edit.EditableDomainObjectCache#
*/
EditableDomainObjectCache.prototype.markDirty = function (domainObject) {
this.dirtyObjects[domainObject.getId()] = domainObject;
};
/**
* Mark an object (presumably already cached) as having had its
* changes saved (and thus no longer needing to be subject to a
* save operation.)
*
* @param {DomainObject} domainObject the domain object
*/
EditableDomainObjectCache.prototype.markClean = function (domainObject) {
delete this.dirtyObjects[domainObject.getId()];
};
/**
* Initiate a save on all objects that have been cached.
* @return {Promise} A promise which will resolve when all objects are
* persisted.
*/
EditableDomainObjectCache.prototype.saveAll = function () {
// Get a list of all dirty objects
var dirty = this.dirtyObjects,
objects = Object.keys(dirty).map(function (k) {
return dirty[k];
});
// Clear dirty set, since we're about to save.
this.dirtyObjects = {};
// Most save logic is handled by the "editor.completion"
// capability, so that is delegated here.
return this.$q.all(objects.map(function (object) {
// Save; pass a nonrecursive flag to avoid looping
return object.getCapability('editor').save(true);
}));
};
/**
* Check if any objects have been marked dirty in this cache.
* @returns {boolean} true if objects are dirty
*/
EditableDomainObjectCache.prototype.dirty = function () {
return Object.keys(this.dirtyObjects).length > 0;
};
return EditableDomainObjectCache; return EditableDomainObjectCache;
} }
); );

@ -35,31 +35,28 @@ define(
* @constructor * @constructor
*/ */
function EditableModelCache() { function EditableModelCache() {
var cache = {}; this.cache = {};
// Deep-copy a model. Models are JSONifiable, so this can be
// done by stringification then destringification
function clone(model) {
return JSON.parse(JSON.stringify(model));
}
return {
/**
* Get this domain object's model from the cache (or
* place it in the cache if it isn't in the cache yet)
* @returns a clone of the domain object's model
* @memberof platform/commonUI/edit.EditableModelCache#
*/
getCachedModel: function (domainObject) {
var id = domainObject.getId();
return (cache[id] =
cache[id] || clone(domainObject.getModel()));
}
};
} }
// Deep-copy a model. Models are JSONifiable, so this can be
// done by stringification then destringification
function clone(model) {
return JSON.parse(JSON.stringify(model));
}
/**
* Get this domain object's model from the cache (or
* place it in the cache if it isn't in the cache yet)
* @returns a clone of the domain object's model
*/
EditableModelCache.prototype.getCachedModel = function (domainObject) {
var id = domainObject.getId(),
cache = this.cache;
return (cache[id] =
cache[id] || clone(domainObject.getModel()));
};
return EditableModelCache; return EditableModelCache;
} }
); );

@ -32,52 +32,44 @@ define(
* (shown as buttons in the top-right of browse mode.) * (shown as buttons in the top-right of browse mode.)
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @constructor * @constructor
* @implements {Policy.<Action, ActionContext>}
*/ */
function EditActionPolicy() { function EditActionPolicy() {
// Get a count of views which are not flagged as non-editable. }
function countEditableViews(context) {
var domainObject = (context || {}).domainObject,
views = domainObject && domainObject.useCapability('view'),
count = 0;
// A view is editable unless explicitly flagged as not // Get a count of views which are not flagged as non-editable.
(views || []).forEach(function (view) { function countEditableViews(context) {
count += (view.editable !== false) ? 1 : 0; var domainObject = (context || {}).domainObject,
}); views = domainObject && domainObject.useCapability('view'),
count = 0;
return count; // A view is editable unless explicitly flagged as not
(views || []).forEach(function (view) {
count += (view.editable !== false) ? 1 : 0;
});
return count;
}
EditActionPolicy.prototype.allow = function (action, context) {
var key = action.getMetadata().key,
category = (context || {}).category;
// Only worry about actions in the view-control category
if (category === 'view-control') {
// Restrict 'edit' to cases where there are editable
// views (similarly, restrict 'properties' to when
// the converse is true)
if (key === 'edit') {
return countEditableViews(context) > 0;
} else if (key === 'properties') {
return countEditableViews(context) < 1;
}
} }
return { // Like all policies, allow by default.
/** return true;
* Check whether or not a given action is allowed by this };
* policy.
* @param {Action} action the action
* @param context the context
* @returns {boolean} true if not disallowed
* @memberof platform/commonUI/edit.EditActionPolicy#
*/
allow: function (action, context) {
var key = action.getMetadata().key,
category = (context || {}).category;
// Only worry about actions in the view-control category
if (category === 'view-control') {
// Restrict 'edit' to cases where there are editable
// views (similarly, restrict 'properties' to when
// the converse is true)
if (key === 'edit') {
return countEditableViews(context) > 0;
} else if (key === 'properties') {
return countEditableViews(context) < 1;
}
}
// Like all policies, allow by default.
return true;
}
};
}
return EditActionPolicy; return EditActionPolicy;
} }

@ -30,30 +30,22 @@ define(
* Policy controlling which views should be visible in Edit mode. * Policy controlling which views should be visible in Edit mode.
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @constructor * @constructor
* @implements {Policy.<View, DomainObject>}
*/ */
function EditableViewPolicy() { function EditableViewPolicy() {
return {
/**
* Check whether or not a given action is allowed by this
* policy.
* @param {Action} action the action
* @param domainObject the domain object which will be viewed
* @returns {boolean} true if not disallowed
* @memberof platform/commonUI/edit.EditableViewPolicy#
*/
allow: function (view, domainObject) {
// If a view is flagged as non-editable, only allow it
// while we're not in Edit mode.
if ((view || {}).editable === false) {
return !domainObject.hasCapability('editor');
}
// Like all policies, allow by default.
return true;
}
};
} }
EditableViewPolicy.prototype.allow = function (view, domainObject) {
// If a view is flagged as non-editable, only allow it
// while we're not in Edit mode.
if ((view || {}).editable === false) {
return !domainObject.hasCapability('editor');
}
// Like all policies, allow by default.
return true;
};
return EditableViewPolicy; return EditableViewPolicy;
} }
); );

@ -42,14 +42,16 @@ define(
* representations resulting from changes there. * representations resulting from changes there.
* *
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @implements {Representer}
* @constructor * @constructor
*/ */
function EditRepresenter($q, $log, scope) { function EditRepresenter($q, $log, scope) {
var domainObject, var self = this;
key;
// Mutate and persist a new version of a domain object's model. // Mutate and persist a new version of a domain object's model.
function doPersist(model) { function doPersist(model) {
var domainObject = self.domainObject;
// First, mutate; then, persist. // First, mutate; then, persist.
return $q.when(domainObject.useCapability("mutation", function () { return $q.when(domainObject.useCapability("mutation", function () {
return model; return model;
@ -65,7 +67,8 @@ define(
// Look up from scope; these will have been populated by // Look up from scope; these will have been populated by
// mct-representation. // mct-representation.
var model = scope.model, var model = scope.model,
configuration = scope.configuration; configuration = scope.configuration,
domainObject = self.domainObject;
// Log the commit message // Log the commit message
$log.debug([ $log.debug([
@ -79,52 +82,33 @@ define(
if (domainObject && domainObject.hasCapability("persistence")) { if (domainObject && domainObject.hasCapability("persistence")) {
// Configurations for specific views are stored by // Configurations for specific views are stored by
// key in the "configuration" field of the model. // key in the "configuration" field of the model.
if (key && configuration) { if (self.key && configuration) {
model.configuration = model.configuration || {}; model.configuration = model.configuration || {};
model.configuration[key] = configuration; model.configuration[self.key] = configuration;
} }
doPersist(model); doPersist(model);
} }
} }
// Respond to the destruction of the current representation.
function destroy() {
// Nothing to clean up
}
// Handle a specific representation of a specific domain object
function represent(representation, representedObject) {
// Track the key, to know which view configuration to save to.
key = (representation || {}).key;
// Track the represented object
domainObject = representedObject;
// Ensure existing watches are released
destroy();
}
// Place the "commit" method in the scope // Place the "commit" method in the scope
scope.commit = commit; scope.commit = commit;
return {
/**
* Set the current representation in use, and the domain
* object being represented.
*
* @param {RepresentationDefinition} representation the
* definition of the representation in use
* @param {DomainObject} domainObject the domain object
* being represented
* @memberof platform/commonUI/edit.EditRepresenter#
*/
represent: represent,
/**
* Release any resources associated with this representer.
* @memberof platform/commonUI/edit.EditRepresenter#
*/
destroy: destroy
};
} }
// Handle a specific representation of a specific domain object
EditRepresenter.prototype.represent = function represent(representation, representedObject) {
// Track the key, to know which view configuration to save to.
this.key = (representation || {}).key;
// Track the represented object
this.domainObject = representedObject;
// Ensure existing watches are released
this.destroy();
};
// Respond to the destruction of the current representation.
EditRepresenter.prototype.destroy = function destroy() {
// Nothing to clean up
};
return EditRepresenter; return EditRepresenter;
} }
); );

@ -42,122 +42,19 @@ define(
* @constructor * @constructor
*/ */
function EditToolbar(structure, commit) { function EditToolbar(structure, commit) {
var toolbarStructure = Object.create(structure || {}), var self = this;
toolbarState,
selection,
properties = [];
// Generate a new key for an item's property // Generate a new key for an item's property
function addKey(property) { function addKey(property) {
properties.push(property); self.properties.push(property);
return properties.length - 1; // Return index of property return self.properties.length - 1; // Return index of property
}
// Update value for this property in all elements of the
// selection which have this property.
function updateProperties(property, value) {
var changed = false;
// Update property in a selected element
function updateProperty(selected) {
// Ignore selected elements which don't have this property
if (selected[property] !== undefined) {
// Check if this is a setter, or just assignable
if (typeof selected[property] === 'function') {
changed =
changed || (selected[property]() !== value);
selected[property](value);
} else {
changed =
changed || (selected[property] !== value);
selected[property] = value;
}
}
}
// Update property in all selected elements
selection.forEach(updateProperty);
// Return whether or not anything changed
return changed;
}
// Look up the current value associated with a property
// in selection i
function lookupState(property, selected) {
var value = selected[property];
return (typeof value === 'function') ? value() : value;
}
// Get initial value for a given property
function initializeState(property) {
var result;
// Look through all selections for this property;
// values should all match by the time we perform
// this lookup anyway.
selection.forEach(function (selected) {
result = (selected[property] !== undefined) ?
lookupState(property, selected) :
result;
});
return result;
}
// Check if all elements of the selection which have this
// property have the same value for this property.
function isConsistent(property) {
var consistent = true,
observed = false,
state;
// Check if a given element of the selection is consistent
// with previously-observed elements for this property.
function checkConsistency(selected) {
var next;
// Ignore selections which don't have this property
if (selected[property] !== undefined) {
// Look up state of this element in the selection
next = lookupState(property, selected);
// Detect inconsistency
if (observed) {
consistent = consistent && (next === state);
}
// Track state for next iteration
state = next;
observed = true;
}
}
// Iterate through selections
selection.forEach(checkConsistency);
return consistent;
}
// Used to filter out items which are applicable (or not)
// to the current selection.
function isApplicable(item) {
var property = (item || {}).property,
method = (item || {}).method,
exclusive = !!(item || {}).exclusive;
// Check if a selected item defines this property
function hasProperty(selected) {
return (property && (selected[property] !== undefined)) ||
(method && (typeof selected[method] === 'function'));
}
return selection.map(hasProperty).reduce(
exclusive ? and : or,
exclusive
) && isConsistent(property);
} }
// Invoke all functions in selections with the given name // Invoke all functions in selections with the given name
function invoke(method, value) { function invoke(method, value) {
if (method) { if (method) {
// Make the change in the selection // Make the change in the selection
selection.forEach(function (selected) { self.selection.forEach(function (selected) {
if (typeof selected[method] === 'function') { if (typeof selected[method] === 'function') {
selected[method](value); selected[method](value);
} }
@ -190,75 +87,169 @@ define(
return converted; return converted;
} }
this.toolbarState = [];
this.selection = undefined;
this.properties = [];
this.toolbarStructure = Object.create(structure || {});
this.toolbarStructure.sections =
((structure || {}).sections || []).map(convertSection);
}
// Check if all elements of the selection which have this
// property have the same value for this property.
EditToolbar.prototype.isConsistent = function (property) {
var self = this,
consistent = true,
observed = false,
state;
// Check if a given element of the selection is consistent
// with previously-observed elements for this property.
function checkConsistency(selected) {
var next;
// Ignore selections which don't have this property
if (selected[property] !== undefined) {
// Look up state of this element in the selection
next = self.lookupState(property, selected);
// Detect inconsistency
if (observed) {
consistent = consistent && (next === state);
}
// Track state for next iteration
state = next;
observed = true;
}
}
// Iterate through selections
self.selection.forEach(checkConsistency);
return consistent;
};
// Used to filter out items which are applicable (or not)
// to the current selection.
EditToolbar.prototype.isApplicable = function (item) {
var property = (item || {}).property,
method = (item || {}).method,
exclusive = !!(item || {}).exclusive;
// Check if a selected item defines this property
function hasProperty(selected) {
return (property && (selected[property] !== undefined)) ||
(method && (typeof selected[method] === 'function'));
}
return this.selection.map(hasProperty).reduce(
exclusive ? and : or,
exclusive
) && this.isConsistent(property);
};
// Look up the current value associated with a property
EditToolbar.prototype.lookupState = function (property, selected) {
var value = selected[property];
return (typeof value === 'function') ? value() : value;
};
/**
* Set the current selection. Visibility of sections
* and items in the toolbar will be updated to match this.
* @param {Array} s the new selection
*/
EditToolbar.prototype.setSelection = function (s) {
var self = this;
// Show/hide controls in this section per applicability // Show/hide controls in this section per applicability
function refreshSectionApplicability(section) { function refreshSectionApplicability(section) {
var count = 0; var count = 0;
// Show/hide each item // Show/hide each item
(section.items || []).forEach(function (item) { (section.items || []).forEach(function (item) {
item.hidden = !isApplicable(item); item.hidden = !self.isApplicable(item);
count += item.hidden ? 0 : 1; count += item.hidden ? 0 : 1;
}); });
// Hide this section if there are no applicable items // Hide this section if there are no applicable items
section.hidden = !count; section.hidden = !count;
} }
// Show/hide controls if they are applicable // Get initial value for a given property
function refreshApplicability() { function initializeState(property) {
toolbarStructure.sections.forEach(refreshSectionApplicability); var result;
// Look through all selections for this property;
// values should all match by the time we perform
// this lookup anyway.
self.selection.forEach(function (selected) {
result = (selected[property] !== undefined) ?
self.lookupState(property, selected) :
result;
});
return result;
} }
// Refresh toolbar state to match selection this.selection = s;
function refreshState() { this.toolbarStructure.sections.forEach(refreshSectionApplicability);
toolbarState = properties.map(initializeState); this.toolbarState = this.properties.map(initializeState);
} };
toolbarStructure.sections = /**
((structure || {}).sections || []).map(convertSection); * Get the structure of the toolbar, as appropriate to
* pass to `mct-toolbar`.
* @returns the toolbar structure
*/
EditToolbar.prototype.getStructure = function () {
return this.toolbarStructure;
};
toolbarState = []; /**
* Get the current state of the toolbar, as appropriate
* to two-way bind to the state handled by `mct-toolbar`.
* @returns {Array} state of the toolbar
*/
EditToolbar.prototype.getState = function () {
return this.toolbarState;
};
return { /**
/** * Update state within the current selection.
* Set the current selection. Visisbility of sections * @param {number} index the index of the corresponding
* and items in the toolbar will be updated to match this. * element in the state array
* @param {Array} s the new selection * @param value the new value to convey to the selection
* @memberof platform/commonUI/edit.EditToolbar# */
*/ EditToolbar.prototype.updateState = function (index, value) {
setSelection: function (s) { var self = this;
selection = s;
refreshApplicability(); // Update value for this property in all elements of the
refreshState(); // selection which have this property.
}, function updateProperties(property, value) {
/** var changed = false;
* Get the structure of the toolbar, as appropriate to
* pass to `mct-toolbar`. // Update property in a selected element
* @returns the toolbar structure function updateProperty(selected) {
* @memberof platform/commonUI/edit.EditToolbar# // Ignore selected elements which don't have this property
*/ if (selected[property] !== undefined) {
getStructure: function () { // Check if this is a setter, or just assignable
return toolbarStructure; if (typeof selected[property] === 'function') {
}, changed =
/** changed || (selected[property]() !== value);
* Get the current state of the toolbar, as appropriate selected[property](value);
* to two-way bind to the state handled by `mct-toolbar`. } else {
* @returns {Array} state of the toolbar changed =
* @memberof platform/commonUI/edit.EditToolbar# changed || (selected[property] !== value);
*/ selected[property] = value;
getState: function () { }
return toolbarState; }
},
/**
* Update state within the current selection.
* @param {number} index the index of the corresponding
* element in the state array
* @param value the new value to convey to the selection
* @memberof platform/commonUI/edit.EditToolbar#
*/
updateState: function (index, value) {
return updateProperties(properties[index], value);
} }
};
} // Update property in all selected elements
self.selection.forEach(updateProperty);
// Return whether or not anything changed
return changed;
}
return updateProperties(this.properties[index], value);
};
return EditToolbar; return EditToolbar;
} }

@ -27,7 +27,10 @@ define(
"use strict"; "use strict";
// No operation // No operation
function noop() {} var NOOP_REPRESENTER = {
represent: function () {},
destroy: function () {}
};
/** /**
* The EditToolbarRepresenter populates the toolbar in Edit mode * The EditToolbarRepresenter populates the toolbar in Edit mode
@ -35,10 +38,10 @@ define(
* @param {Scope} scope the Angular scope of the representation * @param {Scope} scope the Angular scope of the representation
* @memberof platform/commonUI/edit * @memberof platform/commonUI/edit
* @constructor * @constructor
* @implements {Representer}
*/ */
function EditToolbarRepresenter(scope, element, attrs) { function EditToolbarRepresenter(scope, element, attrs) {
var toolbar, var self = this;
toolbarObject = {};
// Mark changes as ready to persist // Mark changes as ready to persist
function commit(message) { function commit(message) {
@ -50,31 +53,33 @@ define(
// Handle changes to the current selection // Handle changes to the current selection
function updateSelection(selection) { function updateSelection(selection) {
// Only update if there is a toolbar to update // Only update if there is a toolbar to update
if (toolbar) { if (self.toolbar) {
// Make sure selection is array-like // Make sure selection is array-like
selection = Array.isArray(selection) ? selection = Array.isArray(selection) ?
selection : selection :
(selection ? [selection] : []); (selection ? [selection] : []);
// Update the toolbar's selection // Update the toolbar's selection
toolbar.setSelection(selection); self.toolbar.setSelection(selection);
// ...and expose its structure/state // ...and expose its structure/state
toolbarObject.structure = toolbar.getStructure(); self.toolbarObject.structure =
toolbarObject.state = toolbar.getState(); self.toolbar.getStructure();
self.toolbarObject.state =
self.toolbar.getState();
} }
} }
// Get state (to watch it) // Get state (to watch it)
function getState() { function getState() {
return toolbarObject.state; return self.toolbarObject.state;
} }
// Update selection models to match changed toolbar state // Update selection models to match changed toolbar state
function updateState(state) { function updateState(state) {
// Update underlying state based on toolbar changes // Update underlying state based on toolbar changes
var changed = (state || []).map(function (value, index) { var changed = (state || []).map(function (value, index) {
return toolbar.updateState(index, value); return self.toolbar.updateState(index, value);
}).reduce(function (a, b) { }).reduce(function (a, b) {
return a || b; return a || b;
}, false); }, false);
@ -86,68 +91,62 @@ define(
} }
} }
// Initialize toolbar (expose object to parent scope) this.commit = commit;
function initialize(definition) { this.scope = scope;
// If we have been asked to expose toolbar state... this.attrs = attrs;
if (attrs.toolbar) { this.updateSelection = updateSelection;
// Initialize toolbar object this.toolbar = undefined;
toolbar = new EditToolbar(definition, commit); this.toolbarObject = {};
// Ensure toolbar state is exposed
scope.$parent[attrs.toolbar] = toolbarObject;
}
}
// Represent a domain object using this definition
function represent(representation) {
// Get the newest toolbar definition from the view
var definition = (representation || {}).toolbar || {};
// Expose the toolbar object to the parent scope
initialize(definition);
// Create a selection scope
scope.selection = new EditToolbarSelection();
// Initialize toolbar to an empty selection
updateSelection([]);
}
// Destroy; remove toolbar object from parent scope
function destroy() {
// Clear exposed toolbar state (if any)
if (attrs.toolbar) {
delete scope.$parent[attrs.toolbar];
}
}
// If this representation exposes a toolbar, set up watches // If this representation exposes a toolbar, set up watches
// to synchronize with it. // to synchronize with it.
if (attrs.toolbar) { if (attrs && attrs.toolbar) {
// Detect and handle changes to state from the toolbar // Detect and handle changes to state from the toolbar
scope.$watchCollection(getState, updateState); scope.$watchCollection(getState, updateState);
// Watch for changes in the current selection state // Watch for changes in the current selection state
scope.$watchCollection("selection.all()", updateSelection); scope.$watchCollection("selection.all()", updateSelection);
// Expose toolbar state under that name // Expose toolbar state under that name
scope.$parent[attrs.toolbar] = toolbarObject; scope.$parent[attrs.toolbar] = this.toolbarObject;
} else {
// No toolbar declared, so do nothing.
return NOOP_REPRESENTER;
} }
return {
/**
* Set the current representation in use, and the domain
* object being represented.
*
* @param {RepresentationDefinition} representation the
* definition of the representation in use
* @param {DomainObject} domainObject the domain object
* being represented
* @memberof platform/commonUI/edit.EditToolbarRepresenter#
*/
represent: (attrs || {}).toolbar ? represent : noop,
/**
* Release any resources associated with this representer.
* @memberof platform/commonUI/edit.EditToolbarRepresenter#
*/
destroy: (attrs || {}).toolbar ? destroy : noop
};
} }
// Represent a domain object using this definition
EditToolbarRepresenter.prototype.represent = function (representation) {
// Get the newest toolbar definition from the view
var definition = (representation || {}).toolbar || {},
self = this;
// Initialize toolbar (expose object to parent scope)
function initialize(definition) {
// If we have been asked to expose toolbar state...
if (self.attrs.toolbar) {
// Initialize toolbar object
self.toolbar = new EditToolbar(definition, self.commit);
// Ensure toolbar state is exposed
self.scope.$parent[self.attrs.toolbar] = self.toolbarObject;
}
}
// Expose the toolbar object to the parent scope
initialize(definition);
// Create a selection scope
this.scope.selection = new EditToolbarSelection();
// Initialize toolbar to an empty selection
this.updateSelection([]);
};
// Destroy; remove toolbar object from parent scope
EditToolbarRepresenter.prototype.destroy = function () {
// Clear exposed toolbar state (if any)
if (this.attrs.toolbar) {
delete this.scope.$parent[this.attrs.toolbar];
}
};
return EditToolbarRepresenter; return EditToolbarRepresenter;
} }
); );

@ -41,112 +41,91 @@ define(
* @constructor * @constructor
*/ */
function EditToolbarSelection() { function EditToolbarSelection() {
var selection = [ {} ], this.selection = [{}];
selecting = false, this.selecting = false;
selected; this.selectedObj = undefined;
}
// Remove the currently-selected object /**
function deselect() { * Check if an object is currently selected.
// Nothing to do if we don't have a selected object * @param {*} obj the object to check for selection
if (selecting) { * @returns {boolean} true if selected, otherwise false
// Clear state tracking */
selecting = false; EditToolbarSelection.prototype.selected = function (obj) {
selected = undefined; return (obj === this.selectedObj) || (obj === this.selection[0]);
};
// Remove the selection /**
selection.pop(); * Select an object.
* @param obj the object to select
return true; * @returns {boolean} true if selection changed
} */
EditToolbarSelection.prototype.select = function (obj) {
// Proxy is always selected
if (obj === this.selection[0]) {
return false; return false;
} }
// Select an object // Clear any existing selection
function select(obj) { this.deselect();
// Proxy is always selected
if (obj === selection[0]) {
return false;
}
// Clear any existing selection // Note the current selection state
deselect(); this.selectedObj = obj;
this.selecting = true;
// Note the current selection state // Add the selection
selected = obj; this.selection.push(obj);
selecting = true; };
// Add the selection /**
selection.push(obj); * Clear the current selection.
* @returns {boolean} true if selection changed
*/
EditToolbarSelection.prototype.deselect = function () {
// Nothing to do if we don't have a selected object
if (this.selecting) {
// Clear state tracking
this.selecting = false;
this.selectedObj = undefined;
// Remove the selection
this.selection.pop();
return true;
} }
return false;
};
/**
* Get the currently-selected object.
* @returns the currently selected object
*/
EditToolbarSelection.prototype.get = function () {
return this.selectedObj;
};
// Check if an object is selected /**
function isSelected(obj) { * Get/set the view proxy (for toolbar actions taken upon
return (obj === selected) || (obj === selection[0]); * the view itself.)
* @param [proxy] the view proxy (if setting)
* @returns the current view proxy
*/
EditToolbarSelection.prototype.proxy = function (p) {
if (arguments.length > 0) {
this.selection[0] = p;
} }
return this.selection[0];
};
// Getter for current selection /**
function get() { * Get an array containing all selections, including the
return selected; * selection proxy. It is generally not advisable to
} * mutate this array directly.
* @returns {Array} all selections
// Getter/setter for view proxy */
function proxy(p) { EditToolbarSelection.prototype.all = function () {
if (arguments.length > 0) { return this.selection;
selection[0] = p; };
}
return selection[0];
}
// Getter for the full array of selected objects (incl. view proxy)
function all() {
return selection;
}
return {
/**
* Check if an object is currently selected.
* @returns true if selected, otherwise false
* @memberof platform/commonUI/edit.EditToolbarSelection#
*/
selected: isSelected,
/**
* Select an object.
* @param obj the object to select
* @returns {boolean} true if selection changed
* @memberof platform/commonUI/edit.EditToolbarSelection#
*/
select: select,
/**
* Clear the current selection.
* @returns {boolean} true if selection changed
* @memberof platform/commonUI/edit.EditToolbarSelection#
*/
deselect: deselect,
/**
* Get the currently-selected object.
* @returns the currently selected object
* @memberof platform/commonUI/edit.EditToolbarSelection#
*/
get: get,
/**
* Get/set the view proxy (for toolbar actions taken upon
* the view itself.)
* @param [proxy] the view proxy (if setting)
* @returns the current view proxy
* @memberof platform/commonUI/edit.EditToolbarSelection#
*/
proxy: proxy,
/**
* Get an array containing all selections, including the
* selection proxy. It is generally not advisable to
* mutate this array directly.
* @returns {Array} all selections
* @memberof platform/commonUI/edit.EditToolbarSelection#
*/
all: all
};
}
return EditToolbarSelection; return EditToolbarSelection;
} }

@ -112,7 +112,9 @@ define(
}); });
it("saves objects that have been marked dirty", function () { it("saves objects that have been marked dirty", function () {
var objects = ['a', 'b', 'c'].map(TestObject).map(cache.getEditableObject); var objects = ['a', 'b', 'c'].map(TestObject).map(function (domainObject) {
return cache.getEditableObject(domainObject);
});
cache.markDirty(objects[0]); cache.markDirty(objects[0]);
cache.markDirty(objects[2]); cache.markDirty(objects[2]);
@ -123,7 +125,9 @@ define(
}); });
it("does not save objects that have been marked clean", function () { it("does not save objects that have been marked clean", function () {
var objects = ['a', 'b', 'c'].map(TestObject).map(cache.getEditableObject); var objects = ['a', 'b', 'c'].map(TestObject).map(function (domainObject) {
return cache.getEditableObject(domainObject);
});
cache.markDirty(objects[0]); cache.markDirty(objects[0]);
cache.markDirty(objects[2]); cache.markDirty(objects[2]);

@ -30,6 +30,29 @@ define(
function () { function () {
"use strict"; "use strict";
/**
* A policy is a participant in decision-making policies. Policies
* are divided into categories (identified symbolically by strings);
* within a given category, every given policy-driven decision will
* occur by consulting all available policies and requiring their
* collective consent (that is, every individual policy has the
* power to reject the decision entirely.)
*
* @interface Policy
* @template C, X
*/
/**
* Check if this policy allows the described decision. The types
* of the arguments expected here vary depending on policy category.
*
* @method Policy#allow
* @template C, X
* @param {C} candidate the thing to allow or disallow
* @param {X} context the context in which the decision occurs
* @returns {boolean} false if disallowed; otherwise, true
*/
/** /**
* Provides an implementation of `policyService` which consults * Provides an implementation of `policyService` which consults
* various policy extensions to determine whether or not a specific * various policy extensions to determine whether or not a specific