+ mct-object="domainObject">
diff --git a/platform/commonUI/edit/src/representers/EditToolbar.js b/platform/commonUI/edit/src/representers/EditToolbar.js
new file mode 100644
index 0000000000..591a312aa3
--- /dev/null
+++ b/platform/commonUI/edit/src/representers/EditToolbar.js
@@ -0,0 +1,170 @@
+/*global define*/
+define(
+ [],
+ function () {
+ "use strict";
+
+ // Utility functions for reducing truth arrays
+ function and(a, b) { return a && b; }
+ function or(a, b) { return a || b; }
+
+
+ /**
+ * Provides initial structure and state (as suitable for provision
+ * to the `mct-toolbar` directive) for a view's tool bar, based on
+ * that view's declaration of what belongs in its tool bar and on
+ * the current selection.
+ *
+ * @param structure toolbar structure, as provided by view definition
+ * @param {Array} selection the current selection state
+ * @constructor
+ */
+ function EditToolbar(structure, selection) {
+ var toolbarStructure = Object.create(structure || {}),
+ toolbarState,
+ properties = [];
+
+ // Generate a new key for an item's property
+ function addKey(property) {
+ properties.push(property);
+ return 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) {
+ // 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') {
+ selected[property](value);
+ } else {
+ selected[property] = value;
+ }
+ }
+ }
+
+ // Update property in all selected elements
+ selection.forEach(updateProperty);
+ }
+
+ // 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,
+ exclusive = !(item || {}).inclusive;
+
+ // Check if a selected item defines this property
+ function hasProperty(selected) {
+ return selected[property] !== undefined;
+ }
+
+ return property && selection.map(hasProperty).reduce(
+ exclusive ? and : or,
+ exclusive
+ ) && isConsistent(property);
+ }
+
+ // Prepare a toolbar item based on current selection
+ function convertItem(item) {
+ var converted = Object.create(item || {});
+ converted.key = addKey(item.property);
+ return converted;
+ }
+
+ // Used to filter out sections that have become empty
+ function nonEmpty(section) {
+ return section && section.items && section.items.length > 0;
+ }
+
+ // Prepare a toolbar section based on current selection
+ function convertSection(section) {
+ var converted = Object.create(section || {});
+ converted.items =
+ ((section || {}).items || [])
+ .map(convertItem)
+ .filter(isApplicable);
+ return converted;
+ }
+
+ toolbarStructure.sections =
+ ((structure || {}).sections || [])
+ .map(convertSection)
+ .filter(nonEmpty);
+
+ toolbarState = properties.map(initializeState);
+
+ return {
+ /**
+ *
+ */
+ getStructure: function () {
+ return toolbarStructure;
+ },
+ getState: function () {
+ return toolbarState;
+ },
+ updateState: function (key, value) {
+ updateProperties(properties[key], value);
+ }
+ };
+ }
+
+ return EditToolbar;
+ }
+);
+
diff --git a/platform/commonUI/edit/src/representers/EditToolbarRepresenter.js b/platform/commonUI/edit/src/representers/EditToolbarRepresenter.js
new file mode 100644
index 0000000000..feeb5bcd53
--- /dev/null
+++ b/platform/commonUI/edit/src/representers/EditToolbarRepresenter.js
@@ -0,0 +1,97 @@
+/*global define*/
+
+define(
+ ['./EditToolbar'],
+ function (EditToolbar) {
+ "use strict";
+
+ // No operation
+ function noop() {}
+
+ /**
+ * The EditToolbarRepresenter populates the toolbar in Edit mode
+ * based on a view's definition.
+ * @param {Scope} scope the Angular scope of the representation
+ * @constructor
+ */
+ function EditToolbarRepresenter(scope, element, attrs) {
+ var definition,
+ unwatch,
+ toolbar,
+ toolbarObject = {};
+
+ // Handle changes to the current selection
+ function updateSelection(selection) {
+ // Make sure selection is array-like
+ selection = Array.isArray(selection) ?
+ selection :
+ (selection ? [selection] : []);
+
+ // Instantiate a new toolbar...
+ toolbar = new EditToolbar(definition, selection);
+
+ // ...and expose its structure/state
+ toolbarObject.structure = toolbar.getStructure();
+ toolbarObject.state = toolbar.getState();
+ }
+
+ // Update selection models to match changed toolbar state
+ function updateState(state) {
+ state.forEach(function (value, index) {
+ toolbar.updateState(index, value);
+ });
+ }
+
+ // Represent a domain object using this definition
+ function represent(representation) {
+ // Clear any existing selection
+ scope.selection = [];
+ // Get the newest toolbar definition from the view
+ definition = (representation || {}).toolbar || {};
+ // Initialize toolbar to an empty selection
+ updateSelection([]);
+ }
+
+ // Destroy; stop watching the parent for changes in
+ // toolbar state.
+ function destroy() {
+ if (unwatch) {
+ unwatch();
+ unwatch = undefined;
+ }
+ }
+
+ // If we have been asked to expose toolbar state...
+ if (attrs.toolbar) {
+ // Expose toolbar state under that name
+ scope.$parent[attrs.toolbar] = toolbarObject;
+ // Detect and handle changes to state from the toolbar
+ unwatch = scope.$parent.$watchCollection(
+ attrs.toolbar + ".state",
+ updateState
+ );
+ // Watch for changes in the current selection state
+ scope.$watchCollection("selection", updateSelection);
+ }
+
+ 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
+ */
+ represent: (attrs || {}).toolbar ? represent : noop,
+ /**
+ * Release any resources associated with this representer.
+ */
+ destroy: (attrs || {}).toolbar ? destroy : noop
+ };
+ }
+
+ return EditToolbarRepresenter;
+ }
+);
\ No newline at end of file
diff --git a/platform/commonUI/edit/test/representers/EditToolbarRepresenterSpec.js b/platform/commonUI/edit/test/representers/EditToolbarRepresenterSpec.js
new file mode 100644
index 0000000000..134f5c0261
--- /dev/null
+++ b/platform/commonUI/edit/test/representers/EditToolbarRepresenterSpec.js
@@ -0,0 +1,91 @@
+/*global define,describe,it,expect,beforeEach,jasmine*/
+
+define(
+ ["../../src/representers/EditToolbarRepresenter"],
+ function (EditToolbarRepresenter) {
+ "use strict";
+
+ describe("The Edit mode toolbar representer", function () {
+ var mockScope,
+ mockElement,
+ testAttrs,
+ mockUnwatch,
+ representer;
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj(
+ '$scope',
+ [ '$on', '$watch', '$watchCollection' ]
+ );
+ mockElement = {};
+ testAttrs = { toolbar: 'testToolbar' };
+ mockScope.$parent = jasmine.createSpyObj(
+ '$parent',
+ [ '$watch', '$watchCollection' ]
+ );
+ mockUnwatch = jasmine.createSpy('unwatch');
+
+ mockScope.$parent.$watchCollection.andReturn(mockUnwatch);
+
+ representer = new EditToolbarRepresenter(
+ mockScope,
+ mockElement,
+ testAttrs
+ );
+ });
+
+ it("exposes toolbar state under a attr-defined name", function () {
+ // A strucutre/state object should have been added to the
+ // parent scope under the name provided in the "toolbar"
+ // attribute
+ expect(mockScope.$parent.testToolbar).toBeDefined();
+ });
+
+ it("is robust against lack of a toolbar definition", function () {
+ expect(function () {
+ representer.represent({});
+ }).not.toThrow();
+ });
+
+ it("watches for toolbar state changes", function () {
+ expect(mockScope.$parent.$watchCollection).toHaveBeenCalledWith(
+ "testToolbar.state",
+ jasmine.any(Function)
+ );
+ });
+
+ it("stops watching toolbar state when destroyed", function () {
+ expect(mockUnwatch).not.toHaveBeenCalled();
+ representer.destroy();
+ expect(mockUnwatch).toHaveBeenCalled();
+ });
+
+ // Verify a simple interaction between selection state and toolbar
+ // state; more complicated interactions are tested in EditToolbar.
+ it("conveys state changes", function () {
+ var testObject = { k: 123 };
+
+ // Provide a view which has a toolbar
+ representer.represent({
+ toolbar: { sections: [ { items: [ { property: 'k' } ] } ] }
+ });
+
+ // Update the selection
+ mockScope.selection.push(testObject);
+ expect(mockScope.$watchCollection.mostRecentCall.args[0])
+ .toEqual('selection'); // Make sure we're using right watch
+ mockScope.$watchCollection.mostRecentCall.args[1]([testObject]);
+
+ // Update the state
+ mockScope.$parent.testToolbar.state[0] = 456;
+ mockScope.$parent.$watchCollection.mostRecentCall.args[1](
+ mockScope.$parent.testToolbar.state
+ );
+
+ // Should have updated the original object
+ expect(testObject.k).toEqual(456);
+ });
+
+ });
+ }
+);
\ No newline at end of file
diff --git a/platform/commonUI/edit/test/representers/EditToolbarSpec.js b/platform/commonUI/edit/test/representers/EditToolbarSpec.js
new file mode 100644
index 0000000000..de22fe03e4
--- /dev/null
+++ b/platform/commonUI/edit/test/representers/EditToolbarSpec.js
@@ -0,0 +1,189 @@
+/*global define,describe,it,expect,beforeEach,jasmine*/
+
+define(
+ ['../../src/representers/EditToolbar'],
+ function (EditToolbar) {
+ "use strict";
+
+ describe("An Edit mode toolbar", function () {
+ var testStructure,
+ testAB,
+ testABC,
+ testABC2,
+ testABCXYZ,
+ testABCYZ;
+
+ beforeEach(function () {
+ testStructure = {
+ sections: [
+ {
+ items: [
+ { name: "A", property: "a" },
+ { name: "B", property: "b" },
+ { name: "C", property: "c" }
+ ]
+ },
+ {
+ items: [
+ { name: "X", property: "x", inclusive: true },
+ { name: "Y", property: "y" },
+ { name: "Z", property: "z" }
+ ]
+ }
+ ]
+ };
+ testAB = { a: 0, b: 1 };
+ testABC = { a: 0, b: 1, c: 2 };
+ testABC2 = { a: 4, b: 1, c: 2 }; // For inconsistent-state checking
+ testABCXYZ = { a: 0, b: 1, c: 2, x: 'X!', y: 'Y!', z: 'Z!' };
+ testABCYZ = { a: 0, b: 1, c: 2, y: 'Y!', z: 'Z!' };
+ });
+
+ it("provides properties from the original structure", function () {
+ expect(
+ new EditToolbar(testStructure, [ testABC ])
+ .getStructure()
+ .sections[0]
+ .items[1]
+ .name
+ ).toEqual("B");
+ });
+
+ // This is needed by mct-toolbar
+ it("adds keys to form structure", function () {
+ expect(
+ new EditToolbar(testStructure, [ testABC ])
+ .getStructure()
+ .sections[0]
+ .items[1]
+ .key
+ ).not.toBeUndefined();
+ });
+
+ it("prunes empty sections", function () {
+ // Verify that all sections are included when applicable...
+ expect(
+ new EditToolbar(testStructure, [ testABCXYZ ])
+ .getStructure()
+ .sections
+ .length
+ ).toEqual(2);
+ // ...but omitted when only some are applicable
+ expect(
+ new EditToolbar(testStructure, [ testABC ])
+ .getStructure()
+ .sections
+ .length
+ ).toEqual(1);
+ });
+
+ it("reads properties from selections", function () {
+ var toolbar = new EditToolbar(testStructure, [ testABC ]),
+ structure = toolbar.getStructure(),
+ state = toolbar.getState();
+
+ expect(state[structure.sections[0].items[0].key])
+ .toEqual(testABC.a);
+ expect(state[structure.sections[0].items[1].key])
+ .toEqual(testABC.b);
+ expect(state[structure.sections[0].items[2].key])
+ .toEqual(testABC.c);
+ });
+
+ it("reads properties from getters", function () {
+ var toolbar, structure, state;
+
+ testABC.a = function () { return "from a getter!"; };
+
+ toolbar = new EditToolbar(testStructure, [ testABC ]);
+ structure = toolbar.getStructure();
+ state = toolbar.getState();
+
+ expect(state[structure.sections[0].items[0].key])
+ .toEqual("from a getter!");
+ });
+
+ it("sets properties on update", function () {
+ var toolbar = new EditToolbar(testStructure, [ testABC ]),
+ structure = toolbar.getStructure();
+ toolbar.updateState(
+ structure.sections[0].items[0].key,
+ "new value"
+ );
+ // Should have updated the underlying object
+ expect(testABC.a).toEqual("new value");
+ });
+
+ it("invokes setters on update", function () {
+ var toolbar, structure, state;
+
+ testABC.a = jasmine.createSpy('a');
+
+ toolbar = new EditToolbar(testStructure, [ testABC ]);
+ structure = toolbar.getStructure();
+
+ toolbar.updateState(
+ structure.sections[0].items[0].key,
+ "new value"
+ );
+ // Should have updated the underlying object
+ expect(testABC.a).toHaveBeenCalledWith("new value");
+ });
+
+ it("removes inapplicable items", function () {
+ // First, verify with all items
+ expect(
+ new EditToolbar(testStructure, [ testABC ])
+ .getStructure()
+ .sections[0]
+ .items
+ .length
+ ).toEqual(3);
+ // Then, try with some items omitted
+ expect(
+ new EditToolbar(testStructure, [ testABC, testAB ])
+ .getStructure()
+ .sections[0]
+ .items
+ .length
+ ).toEqual(2);
+ });
+
+ it("removes inconsistent states", function () {
+ // Only two of three values match among these selections
+ expect(
+ new EditToolbar(testStructure, [ testABC, testABC2 ])
+ .getStructure()
+ .sections[0]
+ .items
+ .length
+ ).toEqual(2);
+ });
+
+ it("allows inclusive items", function () {
+ // One inclusive item is in the set, property 'x' of the
+ // second section; make sure items are pruned down
+ // when only some of the selection has x,y,z properties
+ expect(
+ new EditToolbar(testStructure, [ testABC, testABCXYZ ])
+ .getStructure()
+ .sections[1]
+ .items
+ .length
+ ).toEqual(1);
+ });
+
+ it("removes inclusive items when there are no matches", function () {
+ expect(
+ new EditToolbar(testStructure, [ testABCYZ ])
+ .getStructure()
+ .sections[1]
+ .items
+ .length
+ ).toEqual(2);
+ });
+ });
+ }
+);
+
+
diff --git a/platform/commonUI/edit/test/suite.json b/platform/commonUI/edit/test/suite.json
index e50fb236d4..5744ff8ea8 100644
--- a/platform/commonUI/edit/test/suite.json
+++ b/platform/commonUI/edit/test/suite.json
@@ -15,5 +15,7 @@
"capabilities/EditorCapability",
"objects/EditableDomainObject",
"objects/EditableDomainObjectCache",
- "objects/EditableModelCache"
+ "objects/EditableModelCache",
+ "representers/EditToolbar",
+ "representers/EditToolbarRepresenter"
]
\ No newline at end of file