Compare commits

...

40 Commits

Author SHA1 Message Date
7ff2b507e3 Added examples 2016-07-01 11:03:53 -07:00
dde7de01e2 Object events prototype 2016-07-01 11:03:52 -07:00
b55668426d Merge pull request #1062 from nasa/tc-redux
[Time Conductor] V2 Public API
2016-07-01 10:22:16 -07:00
5b656faa9d Added tests 2016-07-01 10:22:44 -07:00
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
4366b0870d [Time Conductor] API redesign. Initial commit of V2 public API. Addresses #933 2016-07-01 10:22:44 -07:00
47a543beb7 Merge pull request #1067 from nasa/api-css
[API] Remove stylesheet from example
2016-07-01 10:17:36 -07:00
06f87c1472 Merge pull request #1029 from nasa/api-toolbar-add-only
[API Prototype] Add toolbar
2016-07-01 10:13:29 -07:00
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
14a56ea17e Merge pull request #1028 from nasa/api-view
[API Prototype] Support imperative view registration
2016-07-01 10:09:29 -07:00
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
d51e6bfd92 Merge pull request #1030 from nasa/api-tutorial/objects
Api tutorial/objects
2016-07-01 10:03:42 -07:00
370b515c23 [API] Synchronize view to model 2016-06-17 14:21:37 -07:00
4a50f325cb [API] Allow tasks to be added 2016-06-17 14:18:51 -07:00
dbe6a4efc1 [API] Title dialog 2016-06-17 14:05:00 -07:00
13920d8802 [API] Resolve/reject from dialog 2016-06-17 14:00:45 -07:00
b6a8c514aa [API] Show dialog from toolbar 2016-06-17 13:51:15 -07:00
e4a4704baa [API] Listen to add/remove buttons 2016-06-17 13:41:59 -07:00
be0029e59a [API] Get todo toolbar to look right 2016-06-17 13:38:54 -07:00
9cb273ef0a [API] Get registered toolbar to appear 2016-06-17 13:30:08 -07:00
eec9b1cf4c [API] Support distinct region registration 2016-06-17 13:10:24 -07:00
1f96e84542 [API] Override template to allow toolbar injection 2016-06-17 12:16:46 -07:00
c289a27305 [API] Begin adding toolbar 2016-06-17 11:43:49 -07:00
c944080790 [API] Remove stylesheet from example
No need to provide custom API for this.
2016-06-17 11:27:04 -07:00
96316de6e4 [API] Update view API 2016-06-17 11:16:08 -07:00
2240a87ddc [API] Move view off of type 2016-06-17 10:53:56 -07:00
d891affe48 [API] Move view off of type 2016-06-17 10:21:00 -07:00
21a618d1ce Merge branch 'api-type-proto' into api-view 2016-06-17 10:19:44 -07:00
580a4e52b5 Merge branch 'api-tutorials' into api-type-driven 2016-06-07 14:01:08 -07:00
4ca2f51d5e [API] Use subclass style 2016-05-27 17:08:25 -07:00
86ac80ddbd [API] Persist mutations 2016-05-27 16:56:08 -07:00
0525ba6b0b [API] Check/uncheck todos 2016-05-27 16:55:10 -07:00
a79e958ffc [API] Show tasks from todo 2016-05-27 16:46:06 -07:00
03cb0ccb57 [API] Get View to render 2016-05-27 16:36:55 -07:00
7205faa6bb [API] Add adapter bundle 2016-05-27 16:27:47 -07:00
136f2ae785 [API] Add MCTView directive as an adapter 2016-05-27 16:19:20 -07:00
a07e2fb8e5 [API] Implement View 2016-05-27 16:08:43 -07:00
55b531bdeb [API] Sketch in view instantiation 2016-05-27 15:49:16 -07:00
7ece5897e8 [API] Begin adding View 2016-05-27 15:31:54 -07:00
a29c7a6eab [API] Deangularize todo templates 2016-05-27 13:46:30 -07:00
25 changed files with 975 additions and 192 deletions

View File

@ -33,7 +33,6 @@
require([
'./tutorials/grootprovider/groots',
'./tutorials/todo/todo',
'./tutorials/todo/bundle',
'./example/imagery/bundle',
'./example/eventGenerator/bundle',
'./example/generator/bundle',
@ -46,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

@ -65,6 +65,8 @@ define([
'legacyRegistry',
'./src/MCT',
'./src/adapter/bundle',
'./platform/framework/bundle',
'./platform/core/bundle',
'./platform/representation/bundle',

View File

@ -1,12 +1,20 @@
define([
'EventEmitter',
'legacyRegistry',
'uuid',
'./api/api',
'text!./adapter/templates/edit-object-replacement.html',
'./ui/Dialog',
'./api/events/Events',
'./api/objects/bundle'
], function (
EventEmitter,
legacyRegistry,
api
uuid,
api,
editObjectTemplate,
Dialog,
Events
) {
function MCT() {
EventEmitter.call(this);
@ -18,13 +26,70 @@ define([
Object.keys(api).forEach(function (k) {
MCT.prototype[k] = api[k];
});
MCT.prototype.MCT = MCT;
MCT.prototype.legacyExtension = function (category, extension) {
this.legacyBundle.extensions[category] =
this.legacyBundle.extensions[category] || [];
this.legacyBundle.extensions[category].push(extension);
};
MCT.prototype.view = function (region, factory) {
var viewKey = region + uuid();
var adaptedViewKey = "adapted-view-" + region;
this.legacyExtension(
region === this.regions.main ? 'views' : 'representations',
{
name: "A view",
key: adaptedViewKey,
editable: true,
template: '<mct-view region="\'' +
region +
'\'" ' +
'key="\'' +
viewKey +
'\'" ' +
'mct-object="domainObject">' +
'</mct-view>'
}
);
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;
this.legacyBundle.extensions.types =
this.legacyBundle.extensions.types || [];
this.legacyBundle.extensions.types.push(legacyDef);
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 () {
@ -32,5 +97,26 @@ define([
this.emit('start');
};
MCT.prototype.regions = {
main: "MAIN",
toolbar: "TOOLBAR"
};
MCT.prototype.events = new Events();
MCT.prototype.verbs = {
mutate: function (domainObject, mutator) {
return domainObject.useCapability('mutation', mutator)
.then(function () {
var persistence = domainObject.getCapability('persistence');
return persistence.persist();
});
},
observe: function (domainObject, callback) {
var mutation = domainObject.getCapability('mutation');
return mutation.listen(callback);
}
};
return MCT;
});

16
src/adapter/bundle.js Normal file
View File

@ -0,0 +1,16 @@
define([
'legacyRegistry',
'./directives/MCTView'
], function (legacyRegistry, MCTView) {
legacyRegistry.register('src/adapter', {
"extensions": {
"directives": [
{
key: "mctView",
implementation: MCTView,
depends: ["newViews[]"]
}
]
}
});
});

View File

@ -0,0 +1,53 @@
define(['angular'], function (angular) {
function MCTView(newViews) {
var factories = {};
newViews.forEach(function (newView) {
factories[newView.region] = factories[newView.region] || {};
factories[newView.region][newView.key] = newView.factory;
});
return {
restrict: 'E',
link: function (scope, element, attrs) {
var key, mctObject, region;
function maybeShow() {
if (!factories[region] || !factories[region][key] || !mctObject) {
return;
}
var view = factories[region][key](mctObject);
view.show(element[0]);
}
function setKey(k) {
key = k;
maybeShow();
}
function setObject(obj) {
mctObject = obj;
maybeShow();
}
function setRegion(r) {
region = r;
maybeShow();
}
scope.$watch('key', setKey);
scope.$watch('region', setRegion);
scope.$watch('mctObject', setObject);
},
scope: {
key: "=",
region: "=",
mctObject: "="
}
};
}
return MCTView;
});

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

@ -17,6 +17,16 @@ define(function () {
this.definition = definition;
}
/**
* 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;
};
/**
* Get a definition for this type that can be registered using the
* legacy bundle format.

24
src/api/View.js Normal file
View File

@ -0,0 +1,24 @@
define(function () {
function View() {
}
/**
* 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) {
};
/**
* 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',
'./TimeConductor',
'./View',
'./objects/ObjectAPI'
], function (
Type,
TimeConductor,
View,
ObjectAPI
) {
return {
Type: Type,
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]}]);

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

@ -1,59 +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": {
"views": [
{
"key": "example.todo",
"type": "example.todo",
"glyph": "2",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true,
"toolbar": {
"sections": [
{
"items": [
{
"text": "Add Task",
"glyph": "+",
"method": "addTask",
"control": "button"
}
]
},
{
"items": [
{
"glyph": "Z",
"method": "removeTask",
"control": "button"
}
]
}
]
}
}
],
"controllers": [
{
"key": "TodoController",
"implementation": TodoController,
"depends": [ "$scope", "dialogService" ]
}
],
"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,5 @@
<li>
<input type="checkbox" class="example-task-checked">
<span class="example-task-description">
</span>
</li>

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

14
tutorials/todo/todo.html Normal file
View File

@ -0,0 +1,14 @@
<div class="example-todo">
<div class="example-button-group">
<a class="example-todo-button-all">All</a>
<a class="example-todo-button-incomplete">Incomplete</a>
<a class="example-todo-button-complete">Complete</a>
</div>
<ul class="example-todo-task-list">
</ul>
<div class="example-message">
There are no tasks to show.
</div>
</div>

View File

@ -1,4 +1,14 @@
define(function () {
define([
"text!./todo.html",
"text!./todo-task.html",
"text!./todo-toolbar.html",
"text!./todo-dialog.html",
"../../src/api/events/EventDecorator",
"zepto"
], function (todoTemplate, taskTemplate, toolbarTemplate, dialogTemplate, eventDecorator, $) {
/**
* @param {mct.MCT} mct
*/
return function todoPlugin(mct) {
var todoType = new mct.Type({
metadata: {
@ -7,12 +17,153 @@ define(function () {
description: "A list of things that need to be done."
},
initialize: function (model) {
model.tasks = [];
model.tasks = [
{ description: "This is a task." }
];
},
creatable: true
});
function TodoView(domainObject) {
this.domainObject = domainObject;
this.filterValue = "all";
}
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;
this.render();
};
TodoView.prototype.initialize = function () {
Object.keys(this.$buttons).forEach(function (k) {
this.$buttons[k].on('click', this.setFilter.bind(this, k));
}, this);
};
TodoView.prototype.render = function () {
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');
var $buttons = this.$buttons;
var filters = {
all: function () {
return true;
},
incomplete: function (task) {
return !task.completed;
},
complete: function (task) {
return task.completed;
}
};
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]);
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);
});
}
renderTasks();
$message.toggle(tasks.length < 1);
mct.events.mutation(domainObject).on("model.tasks.length", renderTasks);
};
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;
};