mirror of
https://github.com/nasa/openmct.git
synced 2025-03-23 04:25:27 +00:00
Merge remote-tracking branch 'origin/open881' into open-master
This commit is contained in:
commit
d7e502e27c
@ -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
|
||||
|
@ -76,7 +76,6 @@ define(
|
||||
key = (representation || {}).key;
|
||||
// Track the represented object
|
||||
domainObject = representedObject;
|
||||
|
||||
// Ensure existing watches are released
|
||||
destroy();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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 |
@ -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;">
|
||||
|
@ -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>
|
@ -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]
|
||||
});
|
||||
|
40
platform/features/layout/src/elements/BoxProxy.js
Normal file
40
platform/features/layout/src/elements/BoxProxy.js
Normal 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;
|
||||
}
|
||||
);
|
@ -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 = {
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
);
|
@ -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.
|
||||
*/
|
||||
|
35
platform/features/layout/src/elements/ImageProxy.js
Normal file
35
platform/features/layout/src/elements/ImageProxy.js
Normal 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;
|
||||
}
|
||||
);
|
@ -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;
|
||||
}
|
||||
|
||||
|
42
platform/features/layout/src/elements/TextProxy.js
Normal file
42
platform/features/layout/src/elements/TextProxy.js
Normal 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;
|
||||
}
|
||||
);
|
36
platform/features/layout/test/elements/BoxProxySpec.js
Normal file
36
platform/features/layout/test/elements/BoxProxySpec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
37
platform/features/layout/test/elements/ImageProxySpec.js
Normal file
37
platform/features/layout/test/elements/ImageProxySpec.js
Normal 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");
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
36
platform/features/layout/test/elements/TextProxySpec.js
Normal file
36
platform/features/layout/test/elements/TextProxySpec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -5,9 +5,11 @@
|
||||
"LayoutDrag",
|
||||
"LayoutSelection",
|
||||
"elements/AccessorMutator",
|
||||
"elements/BoxProxy",
|
||||
"elements/ElementFactory",
|
||||
"elements/ElementProxies",
|
||||
"elements/ElementProxy",
|
||||
"elements/LineProxy",
|
||||
"elements/TelemetryProxy"
|
||||
"elements/TelemetryProxy",
|
||||
"elements/TextProxy"
|
||||
]
|
@ -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" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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>
|
5
platform/forms/res/templates/controls/dialog.html
Normal file
5
platform/forms/res/templates/controls/dialog.html
Normal file
@ -0,0 +1,5 @@
|
||||
<span ng-controller="DialogButtonController as dialog">
|
||||
<mct-control key="'button'"
|
||||
structure="dialog.getButtonStructure()">
|
||||
</mct-control>
|
||||
</span>
|
@ -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">
|
||||
|
81
platform/forms/src/controllers/ColorController.js
Normal file
81
platform/forms/src/controllers/ColorController.js
Normal 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;
|
||||
}
|
||||
);
|
76
platform/forms/src/controllers/DialogButtonController.js
Normal file
76
platform/forms/src/controllers/DialogButtonController.js
Normal 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;
|
||||
}
|
||||
);
|
51
platform/forms/test/controllers/ColorControllerSpec.js
Normal file
51
platform/forms/test/controllers/ColorControllerSpec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
116
platform/forms/test/controllers/DialogButtonControllerSpec.js
Normal file
116
platform/forms/test/controllers/DialogButtonControllerSpec.js
Normal 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");
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -1,7 +1,9 @@
|
||||
[
|
||||
"MCTControl",
|
||||
"MCTForm",
|
||||
"controllers/ColorController",
|
||||
"controllers/CompositeController",
|
||||
"controllers/DateTimeController",
|
||||
"controllers/DialogButtonController",
|
||||
"controllers/FormController"
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user