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

This commit is contained in:
bwyu 2015-03-03 17:10:08 -08:00
commit d7e502e27c
35 changed files with 1061 additions and 167 deletions

View File

@ -17,10 +17,10 @@ view's scope.) These additional properties are:
an argument to set.)
* `method`: Name of a method to invoke upon a selected object when
a control is activated, e.g. on a button click.
* `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.
* `exclusive`: Optional; true if this control should be considered
applicable only when all elements in the selection has
the associated property. Otherwise, only at least one member 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

View File

@ -76,7 +76,6 @@ define(
key = (representation || {}).key;
// Track the represented object
domainObject = representedObject;
// Ensure existing watches are released
destroy();
}

View File

@ -16,13 +16,13 @@ define(
* the current selection.
*
* @param structure toolbar structure, as provided by view definition
* @param {Array} selection the current selection state
* @param {Function} commit callback to invoke after changes
* @constructor
*/
function EditToolbar(structure, selection, commit) {
function EditToolbar(structure, commit) {
var toolbarStructure = Object.create(structure || {}),
toolbarState,
selection,
properties = [];
// Generate a new key for an item's property
@ -34,14 +34,20 @@ define(
// 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;
}
}
@ -49,6 +55,9 @@ define(
// 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
@ -108,7 +117,7 @@ define(
function isApplicable(item) {
var property = (item || {}).property,
method = (item || {}).method,
exclusive = !(item || {}).inclusive;
exclusive = !!(item || {}).exclusive;
// Check if a selected item defines this property
function hasProperty(selected) {
@ -150,40 +159,77 @@ define(
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
// Prepare a toolbar section
function convertSection(section) {
var converted = Object.create(section || {});
converted.items =
((section || {}).items || [])
.map(convertItem)
.filter(isApplicable);
.map(convertItem);
return converted;
}
toolbarStructure.sections =
((structure || {}).sections || [])
.map(convertSection)
.filter(nonEmpty);
// Show/hide controls in this section per applicability
function refreshSectionApplicability(section) {
var count = 0;
// Show/hide each item
(section.items || []).forEach(function (item) {
item.hidden = !isApplicable(item);
count += item.hidden ? 0 : 1;
});
// Hide this section if there are no applicable items
section.hidden = !count;
}
toolbarState = properties.map(initializeState);
// Show/hide controls if they are applicable
function refreshApplicability() {
toolbarStructure.sections.forEach(refreshSectionApplicability);
}
// Refresh toolbar state to match selection
function refreshState() {
toolbarState = properties.map(initializeState);
}
toolbarStructure.sections =
((structure || {}).sections || []).map(convertSection);
toolbarState = [];
return {
/**
*
* Set the current selection. Visisbility of sections
* and items in the toolbar will be updated to match this.
* @param {Array} s the new selection
*/
setSelection: function (s) {
selection = s;
refreshApplicability();
refreshState();
},
/**
* Get the structure of the toolbar, as appropriate to
* pass to `mct-toolbar`.
* @returns the toolbar structure
*/
getStructure: function () {
return toolbarStructure;
},
/**
* 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
*/
getState: function () {
return toolbarState;
},
updateState: function (key, value) {
updateProperties(properties[key], value);
/**
* 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
*/
updateState: function (index, value) {
return updateProperties(properties[index], value);
}
};
}

View File

@ -15,9 +15,7 @@ define(
* @constructor
*/
function EditToolbarRepresenter(scope, element, attrs) {
var definition,
unwatch,
toolbar,
var toolbar,
toolbarObject = {};
// Mark changes as ready to persist
@ -29,59 +27,83 @@ define(
// Handle changes to the current selection
function updateSelection(selection) {
// Make sure selection is array-like
selection = Array.isArray(selection) ?
selection :
(selection ? [selection] : []);
// Only update if there is a toolbar to update
if (toolbar) {
// Make sure selection is array-like
selection = Array.isArray(selection) ?
selection :
(selection ? [selection] : []);
// Instantiate a new toolbar...
toolbar = new EditToolbar(definition, selection, commit);
// Update the toolbar's selection
toolbar.setSelection(selection);
// ...and expose its structure/state
toolbarObject.structure = toolbar.getStructure();
toolbarObject.state = toolbar.getState();
// ...and expose its structure/state
toolbarObject.structure = toolbar.getStructure();
toolbarObject.state = toolbar.getState();
}
}
// Get state (to watch it)
function getState() {
return toolbarObject.state;
}
// Update selection models to match changed toolbar state
function updateState(state) {
// Update underlying state based on toolbar changes
state.forEach(function (value, index) {
toolbar.updateState(index, value);
});
// Commit the changes.
commit("Changes from toolbar.");
var changed = (state || []).map(function (value, index) {
return toolbar.updateState(index, value);
}).reduce(function (a, b) {
return a || b;
}, false);
// Only commit if something actually changed
if (changed) {
// Commit the changes.
commit("Changes from toolbar.");
}
}
// Initialize toolbar (expose object to parent scope)
function initialize(definition) {
// If we have been asked to expose toolbar state...
if (attrs.toolbar) {
// Initialize toolbar object
toolbar = new EditToolbar(definition, commit);
// 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);
// 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.
// Destroy; remove toolbar object from parent scope
function destroy() {
if (unwatch) {
unwatch();
unwatch = undefined;
// Clear exposed toolbar state (if any)
if (attrs.toolbar) {
delete scope.$parent[attrs.toolbar];
}
}
// If we have been asked to expose toolbar state...
// If this representation exposes a toolbar, set up watches
// to synchronize with it.
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
);
scope.$watchCollection(getState, updateState);
// Watch for changes in the current selection state
scope.$watchCollection("selection", updateSelection);
// Expose toolbar state under that name
scope.$parent[attrs.toolbar] = toolbarObject;
}
return {

View File

@ -35,7 +35,7 @@ define(
});
it("exposes toolbar state under a attr-defined name", function () {
// A strucutre/state object should have been added to the
// A structure/state object should have been added to the
// parent scope under the name provided in the "toolbar"
// attribute
expect(mockScope.$parent.testToolbar).toBeDefined();
@ -48,16 +48,22 @@ define(
});
it("watches for toolbar state changes", function () {
expect(mockScope.$parent.$watchCollection).toHaveBeenCalledWith(
"testToolbar.state",
representer.represent({});
expect(mockScope.$watchCollection).toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
expect(mockScope.$watchCollection.calls[0].args[0]())
.toBe(mockScope.$parent.testToolbar.state);
});
it("stops watching toolbar state when destroyed", function () {
expect(mockUnwatch).not.toHaveBeenCalled();
it("removes state from parent scope on destroy", function () {
// Verify precondition
expect(mockScope.$parent.testToolbar).toBeDefined();
// Destroy the represeter
representer.destroy();
expect(mockUnwatch).toHaveBeenCalled();
// Should have removed toolbar state from view
expect(mockScope.$parent.testToolbar).toBeUndefined();
});
// Verify a simple interaction between selection state and toolbar
@ -78,12 +84,39 @@ define(
// Update the state
mockScope.$parent.testToolbar.state[0] = 456;
mockScope.$parent.$watchCollection.mostRecentCall.args[1](
// Invoke the first watch (assumed to be for toolbar state)
mockScope.$watchCollection.calls[0].args[1](
mockScope.$parent.testToolbar.state
);
// Should have updated the original object
expect(testObject.k).toEqual(456);
// Should have committed the change
expect(mockScope.commit).toHaveBeenCalled();
});
it("does not commit if nothing changed", 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]);
// Invoke the first watch (assumed to be for toolbar state)
mockScope.$watchCollection.calls[0].args[1](
mockScope.$parent.testToolbar.state
);
// Should have committed the change
expect(mockScope.commit).not.toHaveBeenCalled();
});
});

View File

@ -6,34 +6,41 @@ define(
"use strict";
describe("An Edit mode toolbar", function () {
var testStructure,
var mockCommit,
testStructure,
testAB,
testABC,
testABC2,
testABCXYZ,
testABCYZ,
testM;
testM,
toolbar;
function getVisibility(obj) {
return !obj.hidden;
}
beforeEach(function () {
mockCommit = jasmine.createSpy('commit');
testStructure = {
sections: [
{
items: [
{ name: "A", property: "a" },
{ name: "B", property: "b" },
{ name: "C", property: "c" }
{ name: "A", property: "a", exclusive: true },
{ name: "B", property: "b", exclusive: true },
{ name: "C", property: "c", exclusive: true }
]
},
{
items: [
{ name: "X", property: "x", inclusive: true },
{ name: "Y", property: "y" },
{ name: "Z", property: "z" }
{ name: "X", property: "x" },
{ name: "Y", property: "y", exclusive: true },
{ name: "Z", property: "z", exclusive: true }
]
},
{
items: [
{ name: "M", method: "m" }
{ name: "M", method: "m", exclusive: true }
]
}
]
@ -44,6 +51,8 @@ define(
testABCXYZ = { a: 0, b: 1, c: 2, x: 'X!', y: 'Y!', z: 'Z!' };
testABCYZ = { a: 0, b: 1, c: 2, y: 'Y!', z: 'Z!' };
testM = { m: jasmine.createSpy("method") };
toolbar = new EditToolbar(testStructure, mockCommit);
});
it("provides properties from the original structure", function () {
@ -67,27 +76,25 @@ define(
).not.toBeUndefined();
});
it("prunes empty sections", function () {
it("marks empty sections as hidden", function () {
// Verify that all sections are included when applicable...
expect(
new EditToolbar(testStructure, [ testABCXYZ ])
.getStructure()
.sections
.length
).toEqual(2);
toolbar.setSelection([ testABCXYZ ]);
expect(toolbar.getStructure().sections.map(getVisibility))
.toEqual([ true, true, false ]);
// ...but omitted when only some are applicable
expect(
new EditToolbar(testStructure, [ testABC ])
.getStructure()
.sections
.length
).toEqual(1);
toolbar.setSelection([ testABC ]);
expect(toolbar.getStructure().sections.map(getVisibility))
.toEqual([ true, false, false ]);
});
it("reads properties from selections", function () {
var toolbar = new EditToolbar(testStructure, [ testABC ]),
structure = toolbar.getStructure(),
state = toolbar.getState();
var structure, state;
toolbar.setSelection([ testABC ]);
structure = toolbar.getStructure();
state = toolbar.getState();
expect(state[structure.sections[0].items[0].key])
.toEqual(testABC.a);
@ -98,11 +105,11 @@ define(
});
it("reads properties from getters", function () {
var toolbar, structure, state;
var structure, state;
testABC.a = function () { return "from a getter!"; };
toolbar = new EditToolbar(testStructure, [ testABC ]);
toolbar.setSelection([ testABC ]);
structure = toolbar.getStructure();
state = toolbar.getState();
@ -111,10 +118,9 @@ define(
});
it("sets properties on update", function () {
var toolbar = new EditToolbar(testStructure, [ testABC ]),
structure = toolbar.getStructure();
toolbar.setSelection([ testABC ]);
toolbar.updateState(
structure.sections[0].items[0].key,
toolbar.getStructure().sections[0].items[0].key,
"new value"
);
// Should have updated the underlying object
@ -122,11 +128,11 @@ define(
});
it("invokes setters on update", function () {
var toolbar, structure, state;
var structure, state;
testABC.a = jasmine.createSpy('a');
toolbar = new EditToolbar(testStructure, [ testABC ]);
toolbar.setSelection([ testABC ]);
structure = toolbar.getStructure();
toolbar.updateState(
@ -137,70 +143,58 @@ define(
expect(testABC.a).toHaveBeenCalledWith("new value");
});
it("provides a return value describing update status", function () {
// Should return true if actually updated, otherwise false
var key;
toolbar.setSelection([ testABC ]);
key = toolbar.getStructure().sections[0].items[0].key;
expect(toolbar.updateState(key, testABC.a)).toBeFalsy();
expect(toolbar.updateState(key, "new value")).toBeTruthy();
});
it("removes inapplicable items", function () {
// First, verify with all items
expect(
new EditToolbar(testStructure, [ testABC ])
.getStructure()
.sections[0]
.items
.length
).toEqual(3);
toolbar.setSelection([ testABC ]);
expect(toolbar.getStructure().sections[0].items.map(getVisibility))
.toEqual([ true, true, true ]);
// Then, try with some items omitted
expect(
new EditToolbar(testStructure, [ testABC, testAB ])
.getStructure()
.sections[0]
.items
.length
).toEqual(2);
toolbar.setSelection([ testABC, testAB ]);
expect(toolbar.getStructure().sections[0].items.map(getVisibility))
.toEqual([ true, true, false ]);
});
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);
toolbar.setSelection([ testABC, testABC2 ]);
expect(toolbar.getStructure().sections[0].items.map(getVisibility))
.toEqual([ false, true, true ]);
});
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);
toolbar.setSelection([ testABC, testABCXYZ ]);
expect(toolbar.getStructure().sections[1].items.map(getVisibility))
.toEqual([ true, false, false ]);
});
it("removes inclusive items when there are no matches", function () {
expect(
new EditToolbar(testStructure, [ testABCYZ ])
.getStructure()
.sections[1]
.items
.length
).toEqual(2);
toolbar.setSelection([ testABCYZ ]);
expect(toolbar.getStructure().sections[1].items.map(getVisibility))
.toEqual([ false, true, true ]);
});
it("adds click functions when a method is specified", function () {
var testCommit = jasmine.createSpy('commit'),
toolbar = new EditToolbar(testStructure, [ testM ], testCommit);
toolbar.setSelection([testM]);
// Verify precondition
expect(testM.m).not.toHaveBeenCalled();
// Click!
toolbar.getStructure().sections[0].items[0].click();
toolbar.getStructure().sections[2].items[0].click();
// Should have called the underlying function
expect(testM.m).toHaveBeenCalled();
// Should also have committed the change
expect(testCommit).toHaveBeenCalled();
expect(mockCommit).toHaveBeenCalled();
});
});
}

View File

@ -29,7 +29,6 @@
"glyph": "+",
"control": "menu-button",
"text": "Add",
"inclusive": true,
"options": [
{
"name": "Box",
@ -55,13 +54,93 @@
}
]
},
{
"items": [
{
"method": "order",
"glyph": "o",
"control": "menu-button",
"options": [
{
"name": "Move to Top",
"glyph": "^",
"key": "top"
},
{
"name": "Move Up",
"glyph": "^",
"key": "up"
},
{
"name": "Move Down",
"glyph": "v",
"key": "down"
},
{
"name": "Move to Bottom",
"glyph": "v",
"key": "bottom"
}
]
},
{
"property": "fill",
"glyph": "X",
"control": "color"
},
{
"property": "stroke",
"glyph": "-",
"control": "color"
},
{
"property": "color",
"glyph": "\u1D1B",
"mandatory": true,
"control": "color"
},
{
"property": "url",
"glyph": "\u2353",
"control": "dialog-button",
"title": "Image Properties",
"dialog": {
"control": "textfield",
"name": "Image URL",
"required": true
}
},
{
"property": "text",
"glyph": "p",
"control": "dialog-button",
"title": "Text Properties",
"dialog": {
"control": "textfield",
"name": "Text",
"required": true
}
},
{
"method": "showTitle",
"glyph": "+",
"control": "button",
"description": "Show telemetry element title."
},
{
"method": "hideTitle",
"glyph": "X",
"control": "button",
"description": "Hide telemetry element title."
}
]
},
{
"items": [
{
"method": "remove",
"control": "button",
"text": "Remove",
"inclusive": true
"glyph": "Z"
}
]
}

View File

@ -1,3 +1,3 @@
<div ng-style="{ background: ngModel.element.fill }"
<div ng-style="{ background: ngModel.fill(), border: '1px ' + ngModel.stroke() + ' solid' }"
style="width: 100%; height: 100%;">
</div>

View File

@ -1,3 +1,3 @@
<div ng-style="{ 'background-image': 'url(' + ngModel.element.url + ')'}"
<div ng-style="{ 'background-image': 'url(' + ngModel.element.url + ')', border: '1px solid ' + ngModel.stroke() }"
style="width: 100%; height: 100%; background-size: contain; background-repeat: no-repeat; background-position: center;">
</div>
</div>

View File

@ -4,7 +4,7 @@
ng-attr-y1="{{parameters.gridSize[1] * ngModel.y1() + 1}}"
ng-attr-x2="{{parameters.gridSize[0] * ngModel.x2() + 1}}"
ng-attr-y2="{{parameters.gridSize[1] * ngModel.y2() + 1}}"
stroke="lightgray"
ng-attr-stroke="{{ngModel.stroke()}}"
stroke-width="2">
</line>
</svg>

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 505 B

View File

@ -1,5 +1,7 @@
<div style="background: #444;">
<div style="position: absolute; left: 0px; top: 0px; bottom: 0px; width: 50%; overflow: hidden;">
<div ng-style="{ background: ngModel.fill(), border: '1px solid ' + ngModel.stroke(), color: ngModel.color() }"
style="width: 100%; height: 100%;">
<div style="position: absolute; left: 0px; top: 0px; bottom: 0px; width: 50%; overflow: hidden;"
ng-show="ngModel.element.titled">
{{ngModel.name}}
</div>
<div style="position: absolute; right: 0px; top: 0px; bottom: 0px; width: 50%; overflow: hidden;">

View File

@ -1,4 +1,4 @@
<div ng-style="{ background: ngModel.element.fill }"
<div ng-style="{ background: ngModel.fill(), border: '1px solid ' + ngModel.stroke(), color: ngModel.color() }"
style="width: 100%; height: 100%; overflow: hidden;">
{{ngModel.element.text}}
</div>

View File

@ -24,6 +24,8 @@ define(
subscription,
cellStyles = [],
elementProxies = [],
names = {}, // Cache names by ID
values = {}, // Cache values by ID
elementProxiesById = {},
selection;
@ -67,10 +69,12 @@ define(
var id = telemetryObject && telemetryObject.getId();
if (id) {
(elementProxiesById[id] || []).forEach(function (element) {
element.name = telemetryObject.getModel().name;
element.value = telemetryFormatter.formatRangeValue(
names[id] = telemetryObject.getModel().name;
values[id] = telemetryFormatter.formatRangeValue(
subscription.getRangeValue(telemetryObject)
);
element.name = names[id];
element.value = values[id];
});
}
}
@ -127,6 +131,9 @@ define(
elementProxies.forEach(function (elementProxy) {
var id = elementProxy.id;
if (elementProxy.element.type === 'fixed.telemetry') {
// Provide it a cached name/value to avoid flashing
elementProxy.name = names[id];
elementProxy.value = values[id];
elementProxiesById[id] = elementProxiesById[id] || [];
elementProxiesById[id].push(elementProxy);
}
@ -193,6 +200,9 @@ define(
x: Math.floor(position.x / gridSize[0]),
y: Math.floor(position.y / gridSize[1]),
id: id,
stroke: "transparent",
color: "#717171",
titled: true,
width: DEFAULT_DIMENSIONS[0],
height: DEFAULT_DIMENSIONS[1]
});

View File

@ -0,0 +1,40 @@
/*global define*/
define(
['./ElementProxy', './AccessorMutator'],
function (ElementProxy, AccessorMutator) {
'use strict';
/**
* Selection proxy for Box elements in a fixed position view.
* Also serves as a superclass for Text elements, since those
* elements have a superset of Box properties.
*
* Note that arguments here are meant to match those expected
* by `Array.prototype.map`
*
* @constructor
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
*/
function BoxProxy(element, index, elements) {
var proxy = new ElementProxy(element, index, elements);
/**
* Get/set this element's fill color. (Omitting the
* argument makes this act as a getter.)
* @method
* @memberof BoxProxy
* @param {string} fill the new fill color
* @returns {string} the fill color
*/
proxy.fill = new AccessorMutator(element, 'fill');
return proxy;
}
return BoxProxy;
}
);

View File

@ -6,20 +6,25 @@ define(
"use strict";
var INITIAL_STATES = {
"fixed.image": {},
"fixed.image": {
stroke: "transparent"
},
"fixed.box": {
fill: "#888",
border: "transparent"
fill: "#717171",
border: "transparent",
stroke: "transparent"
},
"fixed.line": {
x: 5,
y: 9,
x2: 6,
y2: 6
y2: 6,
stroke: "#717171"
},
"fixed.text": {
fill: "transparent",
border: "transparent"
stroke: "transparent",
color: "#717171"
}
},
DIALOGS = {

View File

@ -1,16 +1,16 @@
/*global define*/
define(
['./TelemetryProxy', './ElementProxy', './LineProxy'],
function (TelemetryProxy, ElementProxy, LineProxy) {
['./TelemetryProxy', './ImageProxy', './LineProxy', './BoxProxy', './TextProxy'],
function (TelemetryProxy, ImageProxy, LineProxy, BoxProxy, TextProxy) {
"use strict";
return {
"fixed.telemetry": TelemetryProxy,
"fixed.line": LineProxy,
"fixed.box": ElementProxy,
"fixed.image": ElementProxy,
"fixed.text": ElementProxy
"fixed.box": BoxProxy,
"fixed.image": ImageProxy,
"fixed.text": TextProxy
};
}
);

View File

@ -5,11 +5,23 @@ define(
function (AccessorMutator) {
"use strict";
// Index deltas for changes in order
var ORDERS = {
top: Number.POSITIVE_INFINITY,
up: 1,
down: -1,
bottom: Number.NEGATIVE_INFINITY
};
/**
* Abstract superclass for other classes which provide useful
* interfaces upon an elements in a fixed position view.
* This handles the generic operations (e.g. remove) so that
* subclasses only need to implement element-specific behaviors.
*
* Note that arguments here are meant to match those expected
* by `Array.prototype.map`
*
* @constructor
* @param element the fixed position element, as stored in its
* configuration
@ -37,11 +49,11 @@ define(
*/
y: new AccessorMutator(element, 'y'),
/**
* Get and/or set the z index of this element.
* @param {number} [z] the new z index (if setting)
* @returns {number} the z index
* Get and/or set the stroke color of this element.
* @param {string} [stroke] the new stroke color (if setting)
* @returns {string} the stroke color
*/
z: new AccessorMutator(element, 'z'),
stroke: new AccessorMutator(element, 'stroke'),
/**
* Get and/or set the width of this element.
* Units are in fixed position grid space.
@ -56,6 +68,28 @@ define(
* @returns {number} the height
*/
height: new AccessorMutator(element, 'height'),
/**
* Change the display order of this element.
* @param {string} o where to move this element;
* one of "top", "up", "down", or "bottom"
*/
order: function (o) {
var delta = ORDERS[o] || 0,
desired = Math.max(
Math.min(index + delta, elements.length - 1),
0
);
// Move to the desired index, if this is a change
if ((desired !== index) && (elements[index] === element)) {
// Splice out the current element
elements.splice(index, 1);
// Splice it back in at the correct index
elements.splice(desired, 0, element);
// Track change in index (proxy should be recreated
// anyway, but be consistent)
index = desired;
}
},
/**
* Remove this element from the fixed position view.
*/

View File

@ -0,0 +1,35 @@
/*global define*/
define(
['./ElementProxy', './AccessorMutator'],
function (ElementProxy, AccessorMutator) {
'use strict';
/**
* Selection proxy for Image elements in a fixed position view.
*
* Note that arguments here are meant to match those expected
* by `Array.prototype.map`
*
* @constructor
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
*/
function ImageProxy(element, index, elements) {
var proxy = new ElementProxy(element, index, elements);
/**
* Get and/or set the displayed text of this element.
* @param {string} [text] the new text (if setting)
* @returns {string} the text
*/
proxy.url = new AccessorMutator(element, 'url');
return proxy;
}
return ImageProxy;
}
);

View File

@ -1,18 +1,49 @@
/*global define*/
define(
['./ElementProxy'],
function (ElementProxy) {
['./TextProxy', './AccessorMutator'],
function (TextProxy, AccessorMutator) {
'use strict';
// Method names to expose from this proxy
var HIDE = 'hideTitle', SHOW = 'showTitle';
/**
* Selection proxy for telemetry elements in a fixed position view.
*
* Note that arguments here are meant to match those expected
* by `Array.prototype.map`
*
* @constructor
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
*/
function TelemetryProxy(element, index, elements) {
var proxy = new ElementProxy(element, index, elements);
var proxy = new TextProxy(element, index, elements);
// Toggle the visibility of the title
function toggle() {
// Toggle the state
element.titled = !element.titled;
// Change which method is exposed, to influence
// which button is shown in the toolbar
delete proxy[SHOW];
delete proxy[HIDE];
proxy[element.titled ? HIDE : SHOW] = toggle;
}
// Expose the domain object identifier
proxy.id = element.id;
// Expose initial toggle
proxy[element.titled ? HIDE : SHOW] = toggle;
// Don't expose text configuration
delete proxy.text;
return proxy;
}

View File

@ -0,0 +1,42 @@
/*global define*/
define(
['./BoxProxy', './AccessorMutator'],
function (BoxProxy, AccessorMutator) {
'use strict';
/**
* Selection proxy for Text elements in a fixed position view.
*
* Note that arguments here are meant to match those expected
* by `Array.prototype.map`
*
* @constructor
* @param element the fixed position element, as stored in its
* configuration
* @param index the element's index within its array
* @param {Array} elements the full array of elements
*/
function TextProxy(element, index, elements) {
var proxy = new BoxProxy(element, index, elements);
/**
* Get and/or set the text color of this element.
* @param {string} [color] the new text color (if setting)
* @returns {string} the text color
*/
proxy.color = new AccessorMutator(element, 'color');
/**
* Get and/or set the displayed text of this element.
* @param {string} [text] the new text (if setting)
* @returns {string} the text
*/
proxy.text = new AccessorMutator(element, 'text');
return proxy;
}
return TextProxy;
}
);

View File

@ -0,0 +1,36 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/BoxProxy'],
function (BoxProxy) {
"use strict";
describe("A fixed position box proxy", function () {
var testElement,
testElements,
proxy;
beforeEach(function () {
testElement = {
x: 1,
y: 2,
width: 42,
height: 24,
fill: "transparent"
};
testElements = [ {}, {}, testElement, {} ];
proxy = new BoxProxy(
testElement,
testElements.indexOf(testElement),
testElements
);
});
it("provides getter/setter for fill color", function () {
expect(proxy.fill()).toEqual('transparent');
expect(proxy.fill('#FFF')).toEqual('#FFF');
expect(proxy.fill()).toEqual('#FFF');
});
});
}
);

View File

@ -14,7 +14,7 @@ define(
testElement = {
x: 1,
y: 2,
z: 3,
stroke: '#717171',
width: 42,
height: 24
};
@ -36,6 +36,17 @@ define(
proxy.remove();
expect(testElements).toEqual([{}, {}, {}]);
});
it("allows order to be changed", function () {
proxy.order("down");
expect(testElements).toEqual([{}, testElement, {}, {}]);
proxy.order("up");
expect(testElements).toEqual([{}, {}, testElement, {}]);
proxy.order("bottom");
expect(testElements).toEqual([testElement, {}, {}, {}]);
proxy.order("top");
expect(testElements).toEqual([{}, {}, {}, testElement]);
});
});
}
);

View File

@ -0,0 +1,37 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/ImageProxy'],
function (ImageProxy) {
"use strict";
describe("A fixed position image proxy", function () {
var testElement,
testElements,
proxy;
beforeEach(function () {
testElement = {
x: 1,
y: 2,
width: 42,
height: 24,
url: "http://www.nasa.gov"
};
testElements = [ {}, {}, testElement, {} ];
proxy = new ImageProxy(
testElement,
testElements.indexOf(testElement),
testElements
);
});
it("provides getter/setter for image URL", function () {
expect(proxy.url()).toEqual("http://www.nasa.gov");
expect(proxy.url("http://www.nasa.gov/some.jpg"))
.toEqual("http://www.nasa.gov/some.jpg");
expect(proxy.url()).toEqual("http://www.nasa.gov/some.jpg");
});
});
}
);

View File

@ -30,6 +30,27 @@ define(
it("exposes the element's id", function () {
expect(proxy.id).toEqual('test-id');
});
it("allows title to be shown/hidden", function () {
// Initially, only showTitle and hideTitle are available
expect(proxy.hideTitle).toBeUndefined();
proxy.showTitle();
// Should have set titled state
expect(testElement.titled).toBeTruthy();
// Should also have changed methods available
expect(proxy.showTitle).toBeUndefined();
proxy.hideTitle();
// Should have cleared titled state
expect(testElement.titled).toBeFalsy();
// Available methods should have changed again
expect(proxy.hideTitle).toBeUndefined();
proxy.showTitle();
});
});
}
);

View File

@ -0,0 +1,36 @@
/*global define,describe,it,expect,beforeEach,jasmine*/
define(
['../../src/elements/TextProxy'],
function (TextProxy) {
"use strict";
describe("A fixed position text proxy", function () {
var testElement,
testElements,
proxy;
beforeEach(function () {
testElement = {
x: 1,
y: 2,
width: 42,
height: 24,
fill: "transparent"
};
testElements = [ {}, {}, testElement, {} ];
proxy = new TextProxy(
testElement,
testElements.indexOf(testElement),
testElements
);
});
it("provides getter/setter for fill color", function () {
expect(proxy.fill()).toEqual('transparent');
expect(proxy.fill('#FFF')).toEqual('#FFF');
expect(proxy.fill()).toEqual('#FFF');
});
});
}
);

View File

@ -5,9 +5,11 @@
"LayoutDrag",
"LayoutSelection",
"elements/AccessorMutator",
"elements/BoxProxy",
"elements/ElementFactory",
"elements/ElementProxies",
"elements/ElementProxy",
"elements/LineProxy",
"elements/TelemetryProxy"
"elements/TelemetryProxy",
"elements/TextProxy"
]

View File

@ -49,6 +49,10 @@
{
"key": "menu-button",
"templateUrl": "templates/controls/menu-button.html"
},
{
"key": "dialog-button",
"templateUrl": "templates/controls/dialog.html"
}
],
"controllers": [
@ -60,6 +64,15 @@
{
"key": "CompositeController",
"implementation": "controllers/CompositeController.js"
},
{
"key": "ColorController",
"implementation": "controllers/ColorController.js"
},
{
"key": "DialogButtonController",
"implementation": "controllers/DialogButtonController.js",
"depends": [ "$scope", "dialogService" ]
}
]
}

View File

@ -1,3 +1,36 @@
<input type="color"
name="mctControl"
ng-model="ngModel[field]">
<div class="s-btn s-icon-btn s-very-subtle btn-menu menu-element dropdown click-invoke"
ng-controller="ClickAwayController as toggle">
<span ng-click="toggle.toggle()">
<span class="ui-symbol icon">{{structure.glyph}}</span>
<span class="title-label" ng-if="structure.text">
{{structure.text}}
</span>
<span class='ui-symbol icon invoke-menu'
ng-if="!structure.text">
v
</span>
</span>
<div class="menu dropdown"
ng-controller="ColorController as colors"
ng-show="toggle.isActive()">
<div style="width: 12em; display: block;" ng-if="!structure.mandatory">
<div style="width: 1em; height: 1em; border: 1px gray solid; display: inline-block;"
ng-click="ngModel[field] = 'transparent'">
{{ngModel[field] === 'transparent' ? 'x' : '' }}
</div>
None
</div>
<div style="width: 12em; display: block;"
ng-repeat="group in colors.groups()">
<div ng-repeat="color in group"
style="width: 1em; height: 1em; border: 1px gray solid; display: inline-block;"
ng-style="{ background: color }"
ng-click="ngModel[field] = color">
{{ngModel[field] === color ? 'x' : '' }}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,5 @@
<span ng-controller="DialogButtonController as dialog">
<mct-control key="'button'"
structure="dialog.getButtonStructure()">
</mct-control>
</span>

View File

@ -3,10 +3,12 @@
<div class="form">
<span ng-repeat="section in structure.sections"
class="control-group coordinates"
ng-if="!section.hidden"
title="{{section.description}}">
<ng-form ng-repeat="item in section.items"
ng-class="{ 'input-labeled': item.name }"
ng-hide="item.hidden"
class="inline"
title="{{item.description}}"
name="mctFormInner">

View File

@ -0,0 +1,81 @@
/*global define*/
define(
[],
function () {
"use strict";
var BASE_COLORS = [
[ 136, 32, 32 ],
[ 224, 64, 64 ],
[ 240, 160, 72 ],
[ 255, 248, 96 ],
[ 128, 240, 72 ],
[ 128, 248, 248 ],
[ 88, 144, 224 ],
[ 0, 72, 240 ],
[ 136, 80, 240 ],
[ 224, 96, 248 ]
],
GRADIENTS = [0.75, 0.50, 0.25, -0.25, -0.50, -0.75],
GROUPS = [];
function toWebColor(triplet) {
return '#' + triplet.map(function (v) {
return (v < 16 ? '0' : '') + v.toString(16);
}).join('');
}
function toGradient(triplet, value) {
return triplet.map(function (v) {
return Math.round(value > 0 ?
(v + (255 - v) * value) :
(v * (1 + value))
);
});
}
function initializeGroups() {
var i, group;
// Ten grayscale colors
group = [];
while (group.length < 10) {
group.push(toWebColor([
Math.round(28.3333 * group.length),
Math.round(28.3333 * group.length),
Math.round(28.3333 * group.length)
]));
}
GROUPS.push(group);
// Ten basic colors
GROUPS.push(BASE_COLORS.map(toWebColor));
// ...and some gradients of those colors
group = [];
GRADIENTS.forEach(function (v) {
group = group.concat(BASE_COLORS.map(function (c) {
return toWebColor(toGradient(c, v));
}));
});
GROUPS.push(group);
}
function ColorController() {
if (GROUPS.length === 0) {
initializeGroups();
}
return {
groups: function () {
return GROUPS;
}
};
}
return ColorController;
}
);

View File

@ -0,0 +1,76 @@
/*global define*/
define(
[],
function () {
'use strict';
/**
* Controller for the `dialog-button` control type. Provides
* structure for a button (embedded via the template) which
* will show a dialog for editing a single property when clicked.
* @constructor
* @param $scope the control's Angular scope
* @param {DialogService} dialogService service to use to prompt
* for user input
*/
function DialogButtonController($scope, dialogService) {
var buttonStructure,
buttonForm,
field;
// Store the result of user input to the model
function storeResult(result) {
$scope.ngModel[$scope.field] = result[$scope.field];
}
// Prompt for user input
function showDialog() {
// Prepare initial state
var state = {};
state[$scope.field] = $scope.ngModel[$scope.field];
// Show dialog, then store user input (if any)
dialogService.getUserInput(buttonForm, state).then(storeResult);
}
// Refresh state based on structure for this control
function refreshStructure(structure) {
var row = Object.create(structure.dialog || {});
structure = structure || {};
// Add the key, to read back from that row
row.key = $scope.field;
// Prepare the structure for the button itself
buttonStructure = {};
buttonStructure.glyph = structure.glyph;
buttonStructure.name = structure.name;
buttonStructure.description = structure.description;
buttonStructure.click = showDialog;
// Prepare the form; a single row
buttonForm = {
name: structure.title,
sections: [ { rows: [ row ] } ]
};
}
$scope.$watch('structure', refreshStructure);
return {
/**
* Get the structure for an `mct-control` of type
* `button`; a dialog will be launched when this button
* is clicked.
* @returns dialog structure
*/
getButtonStructure: function () {
return buttonStructure;
}
};
}
return DialogButtonController;
}
);

View File

@ -0,0 +1,51 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/ColorController"],
function (ColorController) {
"use strict";
var COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
describe("The color picker's controller", function () {
var controller;
beforeEach(function () {
controller = new ColorController();
});
it("exposes groups of colors", function () {
var groups = controller.groups();
// Make sure that the groups array is non-empty
expect(Array.isArray(groups)).toBeTruthy();
expect(groups.length).not.toEqual(0);
groups.forEach(function (group) {
// Make sure each group is a non-empty array
expect(Array.isArray(group)).toBeTruthy();
expect(group.length).not.toEqual(0);
// Make sure they're valid web colors
group.forEach(function (color) {
expect(COLOR_REGEX.test(color)).toBeTruthy();
});
});
});
it("exposes unique colors", function () {
var count = 0, set = {};
// Count each color, and add them to the set
controller.groups().forEach(function (group) {
group.forEach(function (color) {
count += 1;
set[color] = true;
});
});
// Size of set should be number of colors if all were unique
expect(Object.keys(set).length).toEqual(count);
});
});
}
);

View File

@ -0,0 +1,116 @@
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../../src/controllers/DialogButtonController"],
function (DialogButtonController) {
"use strict";
describe("A dialog button controller", function () {
var mockScope,
mockDialogService,
mockPromise,
testStructure,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj(
'$scope',
[ '$watch' ]
);
mockDialogService = jasmine.createSpyObj(
'dialogService',
[ 'getUserInput' ]
);
mockPromise = jasmine.createSpyObj(
'promise',
[ 'then' ]
);
testStructure = {
name: "A Test",
glyph: "T",
description: "Test description",
control: "dialog-button",
title: "Test title",
dialog: {
"control": "textfield",
"name": "Inner control"
}
};
mockScope.field = "testKey";
mockScope.ngModel = { testKey: "initial test value" };
mockScope.structure = testStructure;
mockDialogService.getUserInput.andReturn(mockPromise);
controller = new DialogButtonController(
mockScope,
mockDialogService
);
});
it("provides a structure for a button control", function () {
var buttonStructure;
// Template is just a mct-control pointing to a button
// control, so this controller needs to set up all the
// logic for showing a dialog and collecting user input
// when that button gets clicked.
expect(mockScope.$watch).toHaveBeenCalledWith(
"structure", // As passed in via mct-control
jasmine.any(Function)
);
mockScope.$watch.mostRecentCall.args[1](testStructure);
buttonStructure = controller.getButtonStructure();
expect(buttonStructure.glyph).toEqual(testStructure.glyph);
expect(buttonStructure.description).toEqual(testStructure.description);
expect(buttonStructure.name).toEqual(testStructure.name);
expect(buttonStructure.click).toEqual(jasmine.any(Function));
});
it("shows a dialog when clicked", function () {
mockScope.$watch.mostRecentCall.args[1](testStructure);
// Verify precondition - no dialog shown
expect(mockDialogService.getUserInput).not.toHaveBeenCalled();
// Click!
controller.getButtonStructure().click();
// Should have shown a dialog
expect(mockDialogService.getUserInput).toHaveBeenCalled();
});
it("stores user input to the model", function () {
var key, input = {};
// Show dialog, click...
mockScope.$watch.mostRecentCall.args[1](testStructure);
controller.getButtonStructure().click();
// Should be listening to 'then'
expect(mockPromise.then)
.toHaveBeenCalledWith(jasmine.any(Function));
// Find the key that the dialog should return
key = mockDialogService.getUserInput.mostRecentCall
.args[0].sections[0].rows[0].key;
// Provide 'user input'
input[key] = "test user input";
// Resolve the promise with it
mockPromise.then.mostRecentCall.args[0](input);
// ... should have been placed into the model
expect(mockScope.ngModel.testKey).toEqual("test user input");
});
it("supplies initial model state to the dialog", function () {
var key, state;
mockScope.$watch.mostRecentCall.args[1](testStructure);
controller.getButtonStructure().click();
// Find the key that the dialog should return
key = mockDialogService.getUserInput.mostRecentCall
.args[0].sections[0].rows[0].key;
// Get the initial state provided to the dialog
state = mockDialogService.getUserInput.mostRecentCall.args[1];
// Should have had value from ngModel stored to that key
expect(state[key]).toEqual("initial test value");
});
});
}
);

View File

@ -1,7 +1,9 @@
[
"MCTControl",
"MCTForm",
"controllers/ColorController",
"controllers/CompositeController",
"controllers/DateTimeController",
"controllers/DialogButtonController",
"controllers/FormController"
]