Merge remote-tracking branch 'origin/open878' into open-master

Conflicts:
	platform/commonUI/edit/res/templates/edit-object.html
This commit is contained in:
bwyu 2015-02-25 11:02:11 -08:00
commit a3685d0c6b
8 changed files with 596 additions and 7 deletions

View File

@ -1 +1,41 @@
Contains sources and resources associated with Edit mode.
Contains sources and resources associated with Edit mode.
# Toolbars
Views may specify the contents of a toolbar through a `toolbar`
property in their bundle definition. This should appear as the
structure one would provide to the `mct-toolbar` directive,
except additional properties are recognized to support the
mediation between toolbar contents, user interaction, and the
current selection (as read from the `selection` property of the
view's scope.) These additional properties are:
* `property`: Name of the property within a selected object. If,
for any given object in the selection, that field is a function,
then that function is assumed to be an accessor-mutator function
(that is, it will be called with no arguments to get, and with
an argument to set.)
* `inclusive`: Optional; true if this control should be considered
applicable whenever at least one element in the selection has
the associated property. Otherwise, all members of the current
selection must have this property for the control to be shown.
Controls in the toolbar are shown based on applicability to the
current selection. Applicability for a given member of the selection
is determined by the presence of absence of the named `property`
field. As a consequence of this, if `undefined` is a valid value for
that property, an accessor-mutator function must be used. Likewise,
if toolbar properties are meant to be view-global (as opposed to
per-selection) then the view must include some object to act as its
proxy in the current selection (in addition to whatever objects the
user will conceive of as part of the current selection), typically
with `inclusive` set to `true`.
## Selection
The `selection` property of a view's scope in Edit mode will be
initialized to an empty array. This array's contents may be modified
to implicitly change the contents of the toolbar based on the rules
described above. Care should be taken to modify this array in-place
instead of shadowing it (as the selection will typically
be a few scopes up the hierarchy from the view's actual scope.)

View File

@ -94,6 +94,9 @@
{
"implementation": "EditRepresenter.js",
"depends": [ "$q", "$log" ]
},
{
"implementation": "representers/EditToolbarRepresenter.js"
}
]
}

View File

@ -2,20 +2,17 @@
mct-object="domainObject"
ng-model="representation">
</mct-representation>
<div class="holder edit-area outline abs"
ng-init="toolbar = {}">
<div class="holder edit-area outline abs">
<mct-toolbar name="mctToolbar"
structure="toolbar.structure"
ng-model="toolbar.state">
</mct-toolbar>
<div class='split-layout vertical contents abs work-area'>
<div class='abs pane left edit-main'>
<div class='holder abs object-holder'>
<mct-representation key="representation.selected.key"
toolbar="toolbar"
mct-object="representation.selected.key && domainObject">
mct-object="domainObject">
</mct-representation>
</div>
</div>

View File

@ -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;
}
);

View File

@ -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;
}
);

View File

@ -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);
});
});
}
);

View File

@ -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);
});
});
}
);

View File

@ -15,5 +15,7 @@
"capabilities/EditorCapability",
"objects/EditableDomainObject",
"objects/EditableDomainObjectCache",
"objects/EditableModelCache"
"objects/EditableModelCache",
"representers/EditToolbar",
"representers/EditToolbarRepresenter"
]