Compare commits

..

32 Commits

Author SHA1 Message Date
Henry
7ff2b507e3 Added examples 2016-07-01 11:03:53 -07:00
Henry
dde7de01e2 Object events prototype 2016-07-01 11:03:52 -07:00
Andrew Henry
b55668426d Merge pull request #1062 from nasa/tc-redux
[Time Conductor] V2 Public API
2016-07-01 10:22:16 -07:00
Henry
5b656faa9d Added tests 2016-07-01 10:22:44 -07:00
Pete Richards
8d2c489fa9 [TimeConductor] Set bounds on timeSystem Change
Always set bounds on timeSystem change as not having valid bounds would
put views in inconsistent states.
2016-07-01 10:22:44 -07:00
Henry
4366b0870d [Time Conductor] API redesign. Initial commit of V2 public API. Addresses #933 2016-07-01 10:22:44 -07:00
Victor Woeltjen
47a543beb7 Merge pull request #1067 from nasa/api-css
[API] Remove stylesheet from example
2016-07-01 10:17:36 -07:00
Victor Woeltjen
06f87c1472 Merge pull request #1029 from nasa/api-toolbar-add-only
[API Prototype] Add toolbar
2016-07-01 10:13:29 -07:00
Victor Woeltjen
c9c41cdcc8 Merge remote-tracking branch 'origin/api-tutorials' into api-toolbar-add-only
Conflicts:
	src/MCT.js
2016-07-01 10:10:33 -07:00
Victor Woeltjen
14a56ea17e Merge pull request #1028 from nasa/api-view
[API Prototype] Support imperative view registration
2016-07-01 10:09:29 -07:00
Victor Woeltjen
b2e7db71cc Merge remote-tracking branch 'origin/api-tutorials' into api-view
Conflicts:
	src/MCT.js
	src/api/api.js
2016-07-01 10:08:05 -07:00
Victor Woeltjen
d51e6bfd92 Merge pull request #1030 from nasa/api-tutorial/objects
Api tutorial/objects
2016-07-01 10:03:42 -07:00
Pete Richards
d475d767d5 add grootprovider 2016-06-17 17:05:05 -07:00
Pete Richards
a63e053399 [ObjectAPI] Draft new Object API
Rought prototype of new object API.
2016-06-17 16:59:35 -07:00
Victor Woeltjen
370b515c23 [API] Synchronize view to model 2016-06-17 14:21:37 -07:00
Victor Woeltjen
4a50f325cb [API] Allow tasks to be added 2016-06-17 14:18:51 -07:00
Victor Woeltjen
dbe6a4efc1 [API] Title dialog 2016-06-17 14:05:00 -07:00
Victor Woeltjen
13920d8802 [API] Resolve/reject from dialog 2016-06-17 14:00:45 -07:00
Victor Woeltjen
b6a8c514aa [API] Show dialog from toolbar 2016-06-17 13:51:15 -07:00
Victor Woeltjen
e4a4704baa [API] Listen to add/remove buttons 2016-06-17 13:41:59 -07:00
Victor Woeltjen
be0029e59a [API] Get todo toolbar to look right 2016-06-17 13:38:54 -07:00
Victor Woeltjen
9cb273ef0a [API] Get registered toolbar to appear 2016-06-17 13:30:08 -07:00
Victor Woeltjen
eec9b1cf4c [API] Support distinct region registration 2016-06-17 13:10:24 -07:00
Victor Woeltjen
1f96e84542 [API] Override template to allow toolbar injection 2016-06-17 12:16:46 -07:00
Victor Woeltjen
c289a27305 [API] Begin adding toolbar 2016-06-17 11:43:49 -07:00
Victor Woeltjen
c944080790 [API] Remove stylesheet from example
No need to provide custom API for this.
2016-06-17 11:27:04 -07:00
Victor Woeltjen
96316de6e4 [API] Update view API 2016-06-17 11:16:08 -07:00
Victor Woeltjen
2240a87ddc [API] Move view off of type 2016-06-17 10:53:56 -07:00
Victor Woeltjen
d891affe48 [API] Move view off of type 2016-06-17 10:21:00 -07:00
Victor Woeltjen
21a618d1ce Merge branch 'api-type-proto' into api-view 2016-06-17 10:19:44 -07:00
Victor Woeltjen
5de7a96ccc Merge pull request #1010 from nasa/api-type-proto
[API Prototype] Type registration
2016-06-17 10:18:42 -07:00
Victor Woeltjen
09a833f524 Merge branch 'api-tutorials' into api-type-proto 2016-06-10 13:28:09 -07:00
29 changed files with 1253 additions and 244 deletions

View File

@@ -19,6 +19,7 @@
"comma-separated-values": "^3.6.4",
"FileSaver.js": "^0.0.2",
"zepto": "^1.1.6",
"eventemitter3": "^1.2.0"
"eventemitter3": "^1.2.0",
"lodash": "3.10.1"
}
}

View File

@@ -31,12 +31,13 @@
<script type="text/javascript">
require(['main'], function (mct) {
require([
'./tutorials/grootprovider/groots',
'./tutorials/todo/todo',
'./tutorials/todo/bundle',
'./example/imagery/bundle',
'./example/eventGenerator/bundle',
'./example/generator/bundle'
], function (todoPlugin) {
'./example/generator/bundle',
], function (grootify, todoPlugin) {
grootify(mct);
todoPlugin(mct);
mct.start();
})
@@ -44,6 +45,7 @@
</script>
<link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">
<link rel="stylesheet" href="platform/commonUI/general/res/css/openmct.css">
<link rel="stylesheet" href="tutorials/todo/todo.css">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-16x16.png" sizes="16x16">

View File

@@ -35,7 +35,8 @@ requirejs.config({
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",
"uuid": "bower_components/node-uuid/uuid",
"zepto": "bower_components/zepto/zepto.min"
"zepto": "bower_components/zepto/zepto.min",
"lodash": "bower_components/lodash/lodash"
},
"shim": {
"angular": {

View File

@@ -1,8 +1,21 @@
define([
'EventEmitter',
'legacyRegistry',
'./api/api'
], function (EventEmitter, legacyRegistry, api) {
'uuid',
'./api/api',
'text!./adapter/templates/edit-object-replacement.html',
'./ui/Dialog',
'./api/events/Events',
'./api/objects/bundle'
], function (
EventEmitter,
legacyRegistry,
uuid,
api,
editObjectTemplate,
Dialog,
Events
) {
function MCT() {
EventEmitter.call(this);
this.legacyBundle = { extensions: {} };
@@ -15,36 +28,68 @@ define([
});
MCT.prototype.MCT = MCT;
MCT.prototype.type = function (key, type) {
var legacyDef = type.toLegacyDefinition();
legacyDef.key = key;
this.legacyBundle.extensions.types =
this.legacyBundle.extensions.types || [];
this.legacyBundle.extensions.types.push(legacyDef);
MCT.prototype.legacyExtension = function (category, extension) {
this.legacyBundle.extensions[category] =
this.legacyBundle.extensions[category] || [];
this.legacyBundle.extensions[category].push(extension);
};
var viewFactory = type.view(this.regions.main);
if (viewFactory) {
var viewKey = key + "." + this.regions.main;
MCT.prototype.view = function (region, factory) {
var viewKey = region + uuid();
var adaptedViewKey = "adapted-view-" + region;
this.legacyBundle.extensions.views =
this.legacyBundle.extensions.views || [];
this.legacyBundle.extensions.views.push({
this.legacyExtension(
region === this.regions.main ? 'views' : 'representations',
{
name: "A view",
key: "adapted-view",
template: '<mct-view key="\'' +
key: adaptedViewKey,
editable: true,
template: '<mct-view region="\'' +
region +
'\'" ' +
'key="\'' +
viewKey +
'\'" ' +
'mct-object="domainObject">' +
'</mct-view>'
});
}
);
this.legacyBundle.extensions.newViews =
this.legacyBundle.extensions.newViews || [];
this.legacyBundle.extensions.newViews.push({
factory: viewFactory,
key: viewKey
});
}
this.legacyExtension('policies', {
category: "view",
implementation: function Policy() {
this.allow = function (view, domainObject) {
if (view.key === adaptedViewKey) {
return !!factory(domainObject);
}
return true;
};
}
});
this.legacyExtension('newViews', {
factory: factory,
region: region,
key: viewKey
});
};
MCT.prototype.type = function (key, type) {
var legacyDef = type.toLegacyDefinition();
legacyDef.key = key;
type.key = key;
this.legacyExtension('types', legacyDef);
this.legacyExtension('representations', {
key: "edit-object",
priority: "preferred",
template: editObjectTemplate,
type: key
});
};
MCT.prototype.dialog = function (view, title) {
return new Dialog(view, title).show();
};
MCT.prototype.start = function () {
@@ -53,9 +98,12 @@ define([
};
MCT.prototype.regions = {
main: "MAIN"
main: "MAIN",
toolbar: "TOOLBAR"
};
MCT.prototype.events = new Events();
MCT.prototype.verbs = {
mutate: function (domainObject, mutator) {
return domainObject.useCapability('mutation', mutator)
@@ -63,6 +111,10 @@ define([
var persistence = domainObject.getCapability('persistence');
return persistence.persist();
});
},
observe: function (domainObject, callback) {
var mutation = domainObject.getCapability('mutation');
return mutation.listen(callback);
}
};

View File

@@ -3,27 +3,22 @@ define(['angular'], function (angular) {
var factories = {};
newViews.forEach(function (newView) {
factories[newView.key] = newView.factory;
factories[newView.region] = factories[newView.region] || {};
factories[newView.region][newView.key] = newView.factory;
});
return {
restrict: 'E',
link: function (scope, element, attrs) {
var key = undefined;
var mctObject = undefined;
var key, mctObject, region;
function maybeShow() {
if (!factories[key]) {
return;
}
if (!mctObject) {
if (!factories[region] || !factories[region][key] || !mctObject) {
return;
}
var view = factories[key](mctObject);
var elements = view.elements();
element.empty();
element.append(elements);
var view = factories[region][key](mctObject);
view.show(element[0]);
}
function setKey(k) {
@@ -36,12 +31,19 @@ define(['angular'], function (angular) {
maybeShow();
}
function setRegion(r) {
region = r;
maybeShow();
}
scope.$watch('key', setKey);
scope.$watch('region', setRegion);
scope.$watch('mctObject', setObject);
},
scope: {
key: "=",
region: "=",
mctObject: "="
}
};

View File

@@ -0,0 +1,46 @@
<div class="abs l-flex-col" ng-controller="EditObjectController as EditObjectController">
<div mct-before-unload="EditObjectController.getUnloadWarning()"
class="holder flex-elem l-flex-row object-browse-bar ">
<div class="items-select left flex-elem l-flex-row grows">
<mct-representation key="'back-arrow'"
mct-object="domainObject"
class="flex-elem l-back"></mct-representation>
<mct-representation key="'object-header'"
mct-object="domainObject"
class="l-flex-row flex-elem grows object-header">
</mct-representation>
</div>
<div class="btn-bar right l-flex-row flex-elem flex-justify-end flex-fixed">
<mct-representation key="'switcher'"
mct-object="domainObject"
ng-model="representation">
</mct-representation>
<!-- Temporarily, on mobile, the action buttons are hidden-->
<mct-representation key="'action-group'"
mct-object="domainObject"
parameters="{ category: 'view-control' }"
class="mobile-hide">
</mct-representation>
</div>
</div>
<div class="holder l-flex-col flex-elem grows l-object-wrapper">
<div class="holder l-flex-col flex-elem grows l-object-wrapper-inner">
<!-- Toolbar and Save/Cancel buttons -->
<div class="l-edit-controls flex-elem l-flex-row flex-align-end">
<mct-representation key="'adapted-view-TOOLBAR'"
mct-object="domainObject"
class="flex-elem grows">
</mct-representation>
<mct-representation key="'edit-action-buttons'"
mct-object="domainObject"
class='flex-elem conclude-editing'>
</mct-representation>
</div>
<mct-representation key="representation.selected.key"
mct-object="representation.selected.key && domainObject"
class="abs flex-elem grows object-holder-main scroll"
toolbar="toolbar">
</mct-representation>
</div><!--/ l-object-wrapper-inner -->
</div>
</div>

183
src/api/TimeConductor.js Normal file
View File

@@ -0,0 +1,183 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['EventEmitter'], function (EventEmitter) {
/**
* The public API for setting and querying time conductor state. The
* time conductor is the means by which the temporal bounds of a view
* are controlled. Time-sensitive views will typically respond to
* changes to bounds or other properties of the time conductor and
* update the data displayed based on the time conductor state.
*
* The TimeConductor extends the EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are
* documented below.
* @constructor
*/
function TimeConductor() {
EventEmitter.call(this);
//The Time System
this.system = undefined;
//The Time Of Interest
this.toi = undefined;
this.boundsVal = {
start: undefined,
end: undefined
};
//Default to fixed mode
this.followMode = false;
}
TimeConductor.prototype = Object.create(EventEmitter.prototype);
/**
* Validate the given bounds. This can be used for pre-validation of
* bounds, for example by views validating user inputs.
* @param bounds The start and end time of the conductor.
* @returns {string | true} A validation error, or true if valid
*/
TimeConductor.prototype.validateBounds = function (bounds) {
if ((bounds.start === undefined) ||
(bounds.end === undefined) ||
isNaN(bounds.start) ||
isNaN(bounds.end)
) {
return "Start and end must be specified as integer values";
} else if (bounds.start > bounds.end) {
return "Specified start date exceeds end bound";
}
return true;
};
function throwOnError(validationResult) {
if (validationResult !== true) {
throw new Error(validationResult);
}
}
/**
* Get or set the follow mode of the time conductor. In follow mode the
* time conductor ticks, regularly updating the bounds from a timing
* source appropriate to the selected time system and mode of the time
* conductor.
* @fires TimeConductor#follow
* @param {boolean} followMode
* @returns {boolean}
*/
TimeConductor.prototype.follow = function (followMode) {
if (arguments.length > 0) {
this.followMode = followMode;
/**
* @event TimeConductor#follow The TimeConductor has toggled
* into or out of follow mode.
* @property {boolean} followMode true if follow mode is
* enabled, otherwise false.
*/
this.emit('follow', this.followMode);
}
return this.followMode;
};
/**
* @typedef {Object} TimeConductorBounds
* @property {number} start The start time displayed by the time conductor in ms since epoch. Epoch determined by current time system
* @property {number} end The end time displayed by the time conductor in ms since epoch.
*/
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires TimeConductor#bounds
* @returns {TimeConductorBounds}
*/
TimeConductor.prototype.bounds = function (newBounds) {
if (arguments.length > 0) {
throwOnError(this.validateBounds(newBounds));
this.boundsVal = newBounds;
/**
* @event TimeConductor#bounds The start time, end time, or
* both have been updated
* @property {TimeConductorBounds} bounds
*/
this.emit('bounds', this.boundsVal);
}
return this.boundsVal;
};
/**
* Get or set the time system of the TimeConductor. Time systems determine
* units, epoch, and other aspects of time representation. When changing
* the time system in use, new valid bounds must also be provided.
* @param {TimeSystem} newTimeSystem
* @param {TimeConductorBounds} bounds
* @fires TimeConductor#timeSystem
* @returns {TimeSystem} The currently applied time system
*/
TimeConductor.prototype.timeSystem = function (newTimeSystem, bounds) {
if (arguments.length >= 2) {
this.system = newTimeSystem;
/**
* @event TimeConductor#timeSystem The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
// Do something with bounds here. Try and convert between
// time systems? Or just set defaults when time system changes?
// eg.
this.bounds(bounds);
} else if (arguments.length === 1) {
throw new Error('Must set bounds when changing time system');
}
return this.system;
};
/**
* Get or set the Time of Interest. The Time of Interest is the temporal
* focus of the current view. It can be manipulated by the user from the
* time conductor or from other views.
* @fires TimeConductor#timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
*/
TimeConductor.prototype.timeOfInterest = function (newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* @event TimeConductor#timeOfInterest The Time of Interest has moved.
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
};
return TimeConductor;
});

View File

@@ -0,0 +1,110 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./TimeConductor'], function (TimeConductor) {
describe("The Time Conductor", function () {
var tc,
timeSystem,
bounds,
eventListener,
toi,
follow;
beforeEach(function () {
tc = new TimeConductor();
timeSystem = {};
bounds = {start: 0, end: 0};
eventListener = jasmine.createSpy("eventListener");
toi = 111;
follow = true;
});
it("Supports setting and querying of time of interest and and follow mode", function () {
expect(tc.timeOfInterest()).not.toBe(toi);
tc.timeOfInterest(toi);
expect(tc.timeOfInterest()).toBe(toi);
expect(tc.follow()).not.toBe(follow);
tc.follow(follow);
expect(tc.follow()).toBe(follow);
});
it("Allows setting of valid bounds", function () {
bounds = {start: 0, end: 1};
expect(tc.bounds()).not.toBe(bounds);
expect(tc.bounds.bind(tc, bounds)).not.toThrow();
expect(tc.bounds()).toBe(bounds);
});
it("Disallows setting of invalid bounds", function () {
bounds = {start: 1, end: 0};
expect(tc.bounds()).not.toBe(bounds);
expect(tc.bounds.bind(tc, bounds)).toThrow();
expect(tc.bounds()).not.toBe(bounds);
bounds = {start: 1};
expect(tc.bounds()).not.toBe(bounds);
expect(tc.bounds.bind(tc, bounds)).toThrow();
expect(tc.bounds()).not.toBe(bounds);
});
it("Allows setting of time system with bounds", function () {
expect(tc.timeSystem()).not.toBe(timeSystem);
expect(tc.timeSystem.bind(tc, timeSystem, bounds)).not.toThrow();
expect(tc.timeSystem()).toBe(timeSystem);
});
it("Disallows setting of time system without bounds", function () {
expect(tc.timeSystem()).not.toBe(timeSystem);
expect(tc.timeSystem.bind(tc, timeSystem)).toThrow();
expect(tc.timeSystem()).not.toBe(timeSystem);
});
it("Emits an event when time system changes", function () {
expect(eventListener).not.toHaveBeenCalled();
tc.on("timeSystem", eventListener);
tc.timeSystem(timeSystem, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it("Emits an event when time of interest changes", function () {
expect(eventListener).not.toHaveBeenCalled();
tc.on("timeOfInterest", eventListener);
tc.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it("Emits an event when bounds change", function () {
expect(eventListener).not.toHaveBeenCalled();
tc.on("bounds", eventListener);
tc.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds);
});
it("Emits an event when follow mode changes", function () {
expect(eventListener).not.toHaveBeenCalled();
tc.on("follow", eventListener);
tc.follow(follow);
expect(eventListener).toHaveBeenCalledWith(follow);
});
});
});

View File

@@ -15,14 +15,16 @@ define(function () {
*/
function Type(definition) {
this.definition = definition;
this.views = {};
}
Type.prototype.view = function (region, factory) {
if (arguments.length > 1) {
this.views[region] = factory;
}
return this.views[region];
/**
* Check if a domain object is an instance of this type.
* @param domainObject
* @returns {boolean} true if the domain object is of this type
*/
Type.prototype.check = function (domainObject) {
// Depends on assignment from MCT.
return domainObject.getModel().type === this.key;
};
/**

View File

@@ -1,21 +1,24 @@
define(['EventEmitter'], function (EventEmitter) {
define(function () {
function View() {
EventEmitter.call(this);
}
View.prototype = Object.create(EventEmitter.prototype);
/**
* Show this view in the specified container. If this view is already
* showing elsewhere, it will be removed from that location.
*
* @param {HTMLElement} container the element to populate
*/
View.prototype.show = function (container) {
};
['elements', 'model'].forEach(function (method) {
View.prototype[method] = function (value) {
this.viewState =
this.viewState || { elements: [], model: undefined };
if (arguments.length > 0) {
this.viewState[method] = value;
this.emit(method, value);
}
return this.viewState[method];
}
});
/**
* Release any resources associated with this view.
*
* Subclasses should override this method to release any resources
* they obtained during a `show` call.
*/
View.prototype.destroy = function () {
};
return View;
});

View File

@@ -1,12 +1,18 @@
define([
'./Type',
'./View'
'./TimeConductor',
'./View',
'./objects/ObjectAPI'
], function (
Type,
View
TimeConductor,
View,
ObjectAPI
) {
return {
Type: Type,
View: View
TimeConductor: new TimeConductor(),
View: View,
Objects: ObjectAPI
};
});

View File

@@ -0,0 +1,39 @@
define([],
function () {
function EventDecorator(mct, domainObject) {
if (domainObject._proxied){
return domainObject;
}
Object.defineProperty(domainObject, "_proxied", {value: true, writable: false, enumerable: false, readable: true});
var handler = function (path){
return {
'set': function (target, name, value) {
target[name] = value;
mct.events.mutation(domainObject).emit([path, name].join("."), value);
mct.events.mutation(domainObject).emit("any", value);
return true;
}
}
};
function decorateObject(object, property, path) {
// Decorate object with a proxy
var value = object[property] = new Proxy(object[property], handler(path));
// Enumerate properties and decorate any sub objects with
// proxies
Object.keys(object[property]).filter(function (key){
return typeof(value[key]) === "object";
}).forEach(function (key) {
decorateObject(object[property], key, [path, key].join("."));
});
}
decorateObject(domainObject, "model", "model");
return domainObject;
}
return EventDecorator;
});

31
src/api/events/Events.js Normal file
View File

@@ -0,0 +1,31 @@
define(
[
"EventEmitter"
],
function (EventEmitter) {
function Events() {
this.eventEmitter = new EventEmitter();
}
Events.prototype.mutation = function (domainObject) {
var eventEmitter = this.eventEmitter;
function qualifiedEventName(eventName) {
return ['mutation', domainObject.getId(), eventName].join(':');
}
return {
on: function(eventName, callback) {
return eventEmitter.on(qualifiedEventName(eventName), callback);
},
emit: function(eventName) {
var args = Array.prototype.slice.call(arguments, 1);
args.unshift(qualifiedEventName(eventName));
return eventEmitter.emit.apply(eventEmitter, args);
}
}
};
return Events;
});

87
src/api/events/README.md Normal file
View File

@@ -0,0 +1,87 @@
* Can't use defineProperty. Will not trigger on properties added to model.
Can only use Proxy or setter
/**
* Mutate function on domainObject
*/
domainObject.mutate("property", function (currentVal) {
return "12";
});
//No way of mutating multiple properties at once.
// Removes need to calculate deltas to trigger
/*domainObject.mutate(function (domainObject) {
object.foo = "bar";
return object;
});*/
//Mutating an array property
domainObject.mutate("arrayProperty", function (array) {
array.push("foo");
array[2] = "bar";
//mutate function can calculate delta to decide
// whether to trigger update
return object;
});
domainObject.mutate("property.nestedProperty", function(nestedValue){
return "someNewNestedValue"
});
/*
Problem here - no way to prevent arbitrary mutation
of the object from within the mutate function.
*/
domainObject.mutate("property.nestedProperty", function(nestedObject){
nestedObject.someprop = "someVal";
nestedObject.something = {};
nestedObject.something.else = "Hi";
return nestedObject;
});
/**
* Hybrid setter / mutator approach
*/
domainObject.mutate("property", "value");
domainObject.mutate("property.nestedProperty", "value");
domainObject.mutate("arrayProperty", function(array){
array.push("foo");
array[1] = "bar";
});
// OR
// Mutate the array outside and use the same approach
domainObject.mutate("arrayProperty", arrayVal);
/**
* Setter function on domainObject. I think this is
* out because of the difficulty in support Arrays
*/
//Setters are out because of arrays I think
domainObject.set("property", value);
domainObject.set("property.nestedProperty", value);
domainObject.on("configuration.layout");
domainObject.on("configuration.layout.positions");
// The following would trigger configuration.views
// watcher, but not configuration.views.view1
domainObject.set("configuration.layout", {"positions": [{"123-1234-1232-123": [100, 200]}]});
// Don't think this will work, it's an unnatural way
// of using js.
//Using a setter for everything requires unnatural
// use of javascript
domainObject.set("configuration", {})
.set("layout", {})
.set("positions", [{"123-1234-1232-123": [100, 200]}]);
// VanillaJS
domainObject.model.configuration = {};
domainObject.model.configuration.layout = {};
domainObject.model.configuration.positions = [{"123-1234-1232-123": [100, 200]}];
// Layout does exist
domainObject.set("configuration")
.set("layout")
.set("positions", [{"123-1234-1232-123": [100, 200]}]);

View File

@@ -0,0 +1,70 @@
define([
'./object-utils',
'./ObjectAPI'
], function (
utils,
ObjectAPI
) {
function ObjectServiceProvider(objectService, instantiate) {
this.objectService = objectService;
this.instantiate = instantiate;
}
ObjectServiceProvider.prototype.save = function (object) {
var key = object.key,
keyString = utils.makeKeyString(key),
newObject = this.instantiate(utils.toOldFormat(object), keyString);
return object.getCapability('persistence')
.persist()
.then(function () {
return utils.toNewFormat(object, key);
});
};
ObjectServiceProvider.prototype.delete = function (object) {
// TODO!
};
ObjectServiceProvider.prototype.get = function (key) {
var keyString = utils.makeKeyString(key);
return this.objectService.getObjects([keyString])
.then(function (results) {
var model = JSON.parse(JSON.stringify(results[keyString].getModel()));
return utils.toNewFormat(model, key);
});
};
// Injects new object API as a decorator so that it hijacks all requests.
// Object providers implemented on new API should just work, old API should just work, many things may break.
function LegacyObjectAPIInterceptor(ROOTS, instantiate, objectService) {
this.getObjects = function (keys) {
var results = {},
promises = keys.map(function (keyString) {
var key = utils.parseKeyString(keyString);
return ObjectAPI.get(key)
.then(function (object) {
object = utils.toOldFormat(object)
results[keyString] = instantiate(object, keyString);
});
});
return Promise.all(promises)
.then(function () {
return results;
});
};
ObjectAPI._supersecretSetFallbackProvider(
new ObjectServiceProvider(objectService, instantiate)
);
ROOTS.forEach(function (r) {
ObjectAPI.addRoot(utils.parseKeyString(r.id));
});
return this;
}
return LegacyObjectAPIInterceptor;
});

View File

@@ -0,0 +1,92 @@
define([
'lodash',
'./object-utils'
], function (
_,
utils
) {
/**
Object API. Intercepts the existing object API while also exposing
A new Object API.
MCT.objects.get('mine')
.then(function (root) {
console.log(root);
MCT.objects.getComposition(root)
.then(function (composition) {
console.log(composition)
})
});
*/
var Objects = {},
ROOT_REGISTRY = [],
PROVIDER_REGISTRY = {},
FALLBACK_PROVIDER;
Objects._supersecretSetFallbackProvider = function (p) {
FALLBACK_PROVIDER = p;
};
// Root provider is hardcoded in; can't be skipped.
var RootProvider = {
'get': function () {
return Promise.resolve({
name: 'The root object',
type: 'root',
composition: ROOT_REGISTRY
});
}
};
// Retrieve the provider for a given key.
function getProvider(key) {
if (key.identifier === 'ROOT') {
return RootProvider;
}
return PROVIDER_REGISTRY[key.namespace] || FALLBACK_PROVIDER;
};
Objects.addProvider = function (namespace, provider) {
PROVIDER_REGISTRY[namespace] = provider;
};
[
'save',
'delete',
'get'
].forEach(function (method) {
Objects[method] = function () {
var key = arguments[0],
provider = getProvider(key);
if (!provider) {
throw new Error('No Provider Matched');
}
if (!provider[method]) {
throw new Error('Provider does not support [' + method + '].');
}
return provider[method].apply(provider, arguments);
};
});
Objects.addRoot = function (key) {
ROOT_REGISTRY.unshift(key);
};
Objects.removeRoot = function (key) {
ROOT_REGISTRY = ROOT_REGISTRY.filter(function (k) {
return (
k.identifier !== key.identifier ||
k.namespace !== key.namespace
);
});
};
return Objects;
});

100
src/api/objects/README.md Normal file
View File

@@ -0,0 +1,100 @@
# Object API - Overview
The object API provides methods for fetching domain objects.
# Keys
Keys are a composite identifier that is used to create and persist objects. Ex:
```javascript
{
namespace: 'elastic',
identifier: 'myIdentifier'
}
```
In old MCT days, we called this an "id", and we encoded it in a single string.
The above key would encode into the identifier, `elastic:myIdentifier`.
When interacting with the API you will be dealing with key objects.
# Configuring the Object API
The following methods should be used before calling run. They allow you to
configure the persistence space of MCT.
* `MCT.objects.addRoot(key)` -- add a "ROOT" to Open MCT by specifying it's
key.
* `MCT.objects.removeRoot(key)` -- Remove a "ROOT" from Open MCT by key.
* `MCT.objects.addProvider(namespace, provider)` -- register an object provider
for a specific namespace. See below for documentation on the provider
interface.
# Using the object API
The object API provides methods for getting, saving, and deleting objects.
* MCT.objects.get(key) -> returns promise for an object
* MCT.objects.save(object) -> returns promise that is resolved when object
has been saved
* MCT.objects.delete(object) -> returns promise that is resolved when object has
been deleted
## Configuration Example: Adding a groot
The following example adds a new root object for groot and populates it with
some pieces of groot.
```javascript
var ROOT_KEY = {
namespace: 'groot',
identifier: 'groot'
};
var GROOT_ROOT = {
name: 'I am groot',
type: 'folder',
composition: [
{
namespace: 'groot',
identifier: 'arms'
},
{
namespace: 'groot',
identifier: 'legs'
},
{
namespace: 'groot',
identifier: 'torso'
}
]
};
var GrootProvider = {
get: function (key) {
if (key.identifier === 'groot') {
return Promise.resolve(GROOT_ROOT);
}
return Promise.resolve({
name: 'Groot\'s ' + key.identifier
});
}
};
mct.Objects.addRoot(ROOT_KEY);
mct.Objects.addProvider('groot', GrootProvider);
```
### Making a custom provider:
All methods on the provider interface are optional, so you do not need
to modify them.
* `provider.get(key)` -> promise for a domain object.
* `provider.save(domainObject)` -> returns promise that is fulfilled when object
has been saved.
* `provider.delete(domainObject)` -> returns promise that is fulfilled when
object has been deleted.

49
src/api/objects/bundle.js Normal file
View File

@@ -0,0 +1,49 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
'./LegacyObjectAPIInterceptor',
'legacyRegistry'
], function (
LegacyObjectAPIInterceptor,
legacyRegistry
) {
legacyRegistry.register('src/api/objects', {
name: 'Object API',
description: 'The public Objects API',
extensions: {
components: [
{
provides: "objectService",
type: "decorator",
priority: "mandatory",
implementation: LegacyObjectAPIInterceptor,
depends: [
"roots[]",
"instantiate"
]
}
]
}
});
});

View File

@@ -0,0 +1,67 @@
define([
], function (
) {
// take a key string and turn it into a key object
// 'scratch:root' ==> {namespace: 'scratch', identifier: 'root'}
var parseKeyString = function (key) {
if (typeof key === 'object') {
return key;
}
var namespace = '',
identifier = key;
for (var i = 0, escaped = false, len=key.length; i < len; i++) {
if (key[i] === ":" && !escaped) {
namespace = key.slice(0, i);
identifier = key.slice(i + 1);
break;
}
}
return {
namespace: namespace,
identifier: identifier
};
};
// take a key and turn it into a key string
// {namespace: 'scratch', identifier: 'root'} ==> 'scratch:root'
var makeKeyString = function (key) {
if (typeof key === 'string') {
return key;
}
if (!key.namespace) {
return key.identifier;
}
return [
key.namespace.replace(':', '\\:'),
key.identifier.replace(':', '\\:')
].join(':');
};
// Converts composition to use key strings instead of keys
var toOldFormat = function (model) {
delete model.key;
if (model.composition) {
model.composition = model.composition.map(makeKeyString);
}
return model;
};
// converts composition to use keys instead of key strings
var toNewFormat = function (model, key) {
model.key = key;
if (model.composition) {
model.composition = model.composition.map(parseKeyString);
}
return model;
};
return {
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString
};
});

43
src/ui/Dialog.js Normal file
View File

@@ -0,0 +1,43 @@
define(['text!./dialog.html', 'zepto'], function (dialogTemplate, $) {
function Dialog(view, title) {
this.view = view;
this.title = title;
}
Dialog.prototype.show = function () {
var $body = $('body');
var $dialog = $(dialogTemplate);
var $contents = $dialog.find('.contents .editor');
var $close = $dialog.find('.close');
var $ok = $dialog.find('.ok');
var $cancel = $dialog.find('.cancel');
var view = this.view;
function dismiss() {
$dialog.remove();
view.destroy();
}
if (this.title) {
$dialog.find('.title').text(this.title);
}
$body.append($dialog);
this.view.show($contents[0]);
return new Promise(function (resolve, reject) {
$ok.on('click', resolve);
$ok.on('click', dismiss);
$cancel.on('click', reject);
$cancel.on('click', dismiss);
$close.on('click', reject);
$close.on('click', dismiss);
});
};
return Dialog;
});

21
src/ui/dialog.html Normal file
View File

@@ -0,0 +1,21 @@
<div class="abs overlay">
<div class="abs blocker"></div>
<div class="abs holder">
<a class="clk-icon icon ui-symbol close">x</a>
<div class="abs contents">
<div class="abs top-bar">
<div class="title"></div>
<div class="hint"></div>
</div>
<div class='abs editor'>
</div>
<div class="abs bottom-bar">
<a class='s-btn major ok'>OK</a>
<a class='s-btn cancel'>Cancel</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,44 @@
define(function () {
return function grootPlugin(mct) {
var ROOT_KEY = {
namespace: 'groot',
identifier: 'groot'
};
var GROOT_ROOT = {
name: 'I am groot',
type: 'folder',
composition: [
{
namespace: 'groot',
identifier: 'arms'
},
{
namespace: 'groot',
identifier: 'legs'
},
{
namespace: 'groot',
identifier: 'torso'
}
]
};
var GrootProvider = {
get: function (key) {
if (key.identifier === 'groot') {
return Promise.resolve(GROOT_ROOT);
}
return Promise.resolve({
name: 'Groot\'s ' + key.identifier
});
}
};
mct.Objects.addRoot(ROOT_KEY);
mct.Objects.addProvider('groot', GrootProvider);
return mct;
};
});

View File

@@ -1,19 +0,0 @@
define([
'legacyRegistry',
'./src/controllers/TodoController'
], function (
legacyRegistry,
TodoController
) {
legacyRegistry.register("tutorials/todo", {
"name": "To-do Plugin",
"description": "Allows creating and editing to-do lists.",
"extensions": {
"stylesheets": [
{
"stylesheetUrl": "css/todo.css"
}
]
}
});
});

View File

@@ -1,29 +0,0 @@
<div ng-controller="TodoController" class="example-todo">
<div class="example-button-group">
<a ng-class="{ selected: checkVisibility(true) }"
ng-click="setVisibility(true)">All</a>
<a ng-class="{ selected: checkVisibility(false, false) }"
ng-click="setVisibility(false, false)">Incomplete</a>
<a ng-class="{ selected: checkVisibility(false, true) }"
ng-click="setVisibility(false, true)">Complete</a>
</div>
<ul>
<li ng-repeat="task in model.tasks"
ng-class="{ 'example-task-completed': task.completed }"
ng-if="showTask(task)">
<input type="checkbox"
ng-checked="task.completed"
ng-click="toggleCompletion($index)">
<span ng-click="selectTask($index)"
ng-class="{ selected: isSelected($index) }"
class="example-task-description">
{{task.description}}
</span>
</li>
</ul>
<div ng-if="model.tasks.length < 1" class="example-message">
There are no tasks to show.
</div>
</div>

View File

@@ -1,97 +0,0 @@
define(function () {
// Form to display when adding new tasks
var NEW_TASK_FORM = {
name: "Add a Task",
sections: [{
rows: [{
name: 'Description',
key: 'description',
control: 'textfield',
required: true
}]
}]
};
function TodoController($scope, dialogService) {
var showAll = true,
showCompleted;
// Persist changes made to a domain object's model
function persist() {
var persistence =
$scope.domainObject.getCapability('persistence');
return persistence && persistence.persist();
}
// Remove a task
function removeTaskAtIndex(taskIndex) {
$scope.domainObject.useCapability('mutation', function (model) {
model.tasks.splice(taskIndex, 1);
});
persist();
}
// Add a task
function addNewTask(task) {
$scope.domainObject.useCapability('mutation', function (model) {
model.tasks.push(task);
});
persist();
}
// Change which tasks are visible
$scope.setVisibility = function (all, completed) {
showAll = all;
showCompleted = completed;
};
// Check if current visibility settings match
$scope.checkVisibility = function (all, completed) {
return showAll ? all : (completed === showCompleted);
};
// Toggle the completion state of a task
$scope.toggleCompletion = function (taskIndex) {
$scope.domainObject.useCapability('mutation', function (model) {
var task = model.tasks[taskIndex];
task.completed = !task.completed;
});
persist();
};
// Check whether a task should be visible
$scope.showTask = function (task) {
return showAll || (showCompleted === !!(task.completed));
};
// Handle selection state in edit mode
if ($scope.selection) {
// Expose the ability to select tasks
$scope.selectTask = function (taskIndex) {
$scope.selection.select({
removeTask: function () {
removeTaskAtIndex(taskIndex);
$scope.selection.deselect();
},
taskIndex: taskIndex
});
};
// Expose a check for current selection state
$scope.isSelected = function (taskIndex) {
return ($scope.selection.get() || {}).taskIndex ===
taskIndex;
};
// Expose a view-level selection proxy
$scope.selection.proxy({
addTask: function () {
dialogService.getUserInput(NEW_TASK_FORM, {})
.then(addNewTask);
}
});
}
}
return TodoController;
});

View File

@@ -0,0 +1,3 @@
<label>Description:
<input type="text" class="description">
</label>

View File

@@ -0,0 +1,9 @@
<div class="tool-bar btn-bar contents abs">
<a class="s-btn labeled example-add">
<span class="ui-symbol icon">+</span>
<span class="title-label">Add Task</span>
</a>
<a class="s-btn example-remove">
<span class="ui-symbol icon">Z</span>
</a>
</div>

29
tutorials/todo/todo.css Normal file
View File

@@ -0,0 +1,29 @@
.example-todo div.example-button-group {
margin-top: 12px;
margin-bottom: 12px;
}
.example-todo .example-button-group a {
padding: 3px;
margin: 3px;
}
.example-todo .example-button-group a.selected {
border: 1px gray solid;
border-radius: 3px;
background: #444;
}
.example-todo .example-task-completed .example-task-description {
text-decoration: line-through;
opacity: 0.75;
}
.example-todo .example-task-description.selected {
background: #46A;
border-radius: 3px;
}
.example-todo .example-message {
font-style: italic;
}

View File

@@ -1,8 +1,11 @@
define([
"text!./todo.html",
"text!./todo-task.html",
"text!./todo-toolbar.html",
"text!./todo-dialog.html",
"../../src/api/events/EventDecorator",
"zepto"
], function (todoTemplate, taskTemplate, $) {
], function (todoTemplate, taskTemplate, toolbarTemplate, dialogTemplate, eventDecorator, $) {
/**
* @param {mct.MCT} mct
*/
@@ -22,23 +25,38 @@ define([
});
function TodoView(domainObject) {
mct.View.apply(this);
this.domainObject = domainObject;
this.filterValue = "all";
this.elements($(todoTemplate));
var $els = $(this.elements());
this.$buttons = {
all: $els.find('.example-todo-button-all'),
incomplete: $els.find('.example-todo-button-incomplete'),
complete: $els.find('.example-todo-button-complete')
};
this.initialize();
this.on('model', this.render.bind(this));
this.model(domainObject);
}
TodoView.prototype = Object.create(mct.View.prototype);
TodoView.prototype.show = function (container) {
this.destroy();
this.$els = $(todoTemplate);
this.$buttons = {
all: this.$els.find('.example-todo-button-all'),
incomplete: this.$els.find('.example-todo-button-incomplete'),
complete: this.$els.find('.example-todo-button-complete')
};
$(container).empty().append(this.$els);
this.initialize();
this.render();
mct.verbs.observe(this.domainObject, this.render.bind(this));
mct.events.mutation(this.domainObject).on("*", function (value) {
console.log("model changed");
});
};
TodoView.prototype.destroy = function () {
if (this.unlisten) {
this.unlisten();
this.unlisten = undefined;
}
};
TodoView.prototype.setFilter = function (value) {
this.filterValue = value;
@@ -52,8 +70,8 @@ define([
};
TodoView.prototype.render = function () {
var $els = $(this.elements());
var domainObject = this.model();
var $els = this.$els;
var domainObject = this.domainObject;
var tasks = domainObject.getModel().tasks;
var $message = $els.find('.example-message');
var $list = $els.find('.example-todo-task-list');
@@ -70,38 +88,82 @@ define([
}
};
var filterValue = this.filterValue;
var model = domainObject.model;
Object.keys($buttons).forEach(function (k) {
$buttons[k].toggleClass('selected', filterValue === k);
});
tasks = tasks.filter(filters[filterValue]);
$list.empty();
tasks.forEach(function (task, index) {
var $taskEls = $(taskTemplate);
var $checkbox = $taskEls.find('.example-task-checked');
$checkbox.prop('checked', task.completed);
$taskEls.find('.example-task-description')
.text(task.description);
$checkbox.on('change', function () {
var checked = !!$checkbox.prop('checked');
mct.verbs.mutate(domainObject, function (model) {
model.tasks[index].completed = checked;
function renderTasks() {
$list.empty();
domainObject.getModel().tasks.forEach(function (task, index) {
var $taskEls = $(taskTemplate);
var $checkbox = $taskEls.find('.example-task-checked');
$checkbox.prop('checked', task.completed);
$taskEls.find('.example-task-description')
.text(task.description);
$checkbox.on('change', function () {
var checked = !!$checkbox.prop('checked');
domainObject.getModel().tasks[index].completed = checked;
});
});
$list.append($taskEls);
});
$list.append($taskEls);
});
}
renderTasks();
$message.toggle(tasks.length < 1);
mct.events.mutation(domainObject).on("model.tasks.length", renderTasks);
};
todoType.view(mct.regions.main, function (domainObject) {
return new TodoView(domainObject);
});
function TodoToolbarView(domainObject) {
this.domainObject = domainObject;
}
TodoToolbarView.prototype.show = function (container) {
var $els = $(toolbarTemplate);
var $add = $els.find('a.example-add');
var $remove = $els.find('a.example-remove');
var domainObject = this.domainObject;
$(container).append($els);
$add.on('click', function () {
var $dialog = $(dialogTemplate),
view = {
show: function (container) {
$(container).append($dialog);
},
destroy: function () {}
};
mct.dialog(view, "Add a Task").then(function () {
var description = $dialog.find('input').val();
domainObject.getModel().tasks.push({ description: description });
/*mct.verbs.mutate(domainObject, function (model) {
model.tasks.push({ description: description });
console.log(model);
});*/
});
});
$remove.on('click', window.alert.bind(window, "Remove!"));
};
TodoToolbarView.prototype.destroy = function () {
};
mct.type('example.todo', todoType);
mct.view(mct.regions.main, function (domainObject) {
return todoType.check(domainObject) && new TodoView(eventDecorator(mct, domainObject));
});
mct.view(mct.regions.toolbar, function (domainObject) {
domainObject = eventDecorator(mct, domainObject);
return todoType.check(domainObject) && new TodoToolbarView(domainObject);
});
return mct;
};