mirror of
https://github.com/nasa/openmct.git
synced 2025-06-26 03:00:13 +00:00
Compare commits
143 Commits
tc-api-tak
...
api-tutori
Author | SHA1 | Date | |
---|---|---|---|
b4dc50295c | |||
382dde300a | |||
02aa08a3ef | |||
c95d9c7956 | |||
b1b8df4d87 | |||
bd3c6665fb | |||
10e90519c0 | |||
d341a8be97 | |||
8c439d8109 | |||
9c88b7ce1d | |||
2463e4d59f | |||
d73c505bea | |||
831ecc59d9 | |||
1de26d3c5d | |||
11409ce509 | |||
93872ce074 | |||
8861644f2d | |||
d4948f771b | |||
8295a0bed1 | |||
0656a298da | |||
fe2ce91d50 | |||
14f30b2489 | |||
62d90a8114 | |||
87682607a5 | |||
7bf265b478 | |||
1d31fe8d02 | |||
bfdbc71e40 | |||
1147f3aa47 | |||
719f9f45e8 | |||
95ef70a24c | |||
d5aa998b4c | |||
7890fcae69 | |||
18843cee48 | |||
1879c122c7 | |||
d7ddb96c4e | |||
b55668426d | |||
5b656faa9d | |||
8d2c489fa9 | |||
4366b0870d | |||
47a543beb7 | |||
06f87c1472 | |||
c9c41cdcc8 | |||
14a56ea17e | |||
b2e7db71cc | |||
d51e6bfd92 | |||
d475d767d5 | |||
a63e053399 | |||
370b515c23 | |||
4a50f325cb | |||
dbe6a4efc1 | |||
13920d8802 | |||
b6a8c514aa | |||
e4a4704baa | |||
be0029e59a | |||
9cb273ef0a | |||
eec9b1cf4c | |||
1f96e84542 | |||
c289a27305 | |||
c944080790 | |||
96316de6e4 | |||
2240a87ddc | |||
d891affe48 | |||
21a618d1ce | |||
5de7a96ccc | |||
09a833f524 | |||
580a4e52b5 | |||
9c4e17bfab | |||
d3e5d95d6b | |||
c70793ac2d | |||
a6ef1d3423 | |||
4ca2f51d5e | |||
86ac80ddbd | |||
0525ba6b0b | |||
a79e958ffc | |||
03cb0ccb57 | |||
7205faa6bb | |||
136f2ae785 | |||
a07e2fb8e5 | |||
55b531bdeb | |||
7ece5897e8 | |||
a29c7a6eab | |||
c4fec1af6a | |||
a6996df3df | |||
0c660238f2 | |||
b73b824e55 | |||
1954d98628 | |||
7aa034ce23 | |||
385dc5d298 | |||
a88b4b31a1 | |||
04112956cf | |||
f1113fda24 | |||
33c208d8fe | |||
c557fb6cd5 | |||
bde2bc7709 | |||
5fe759aa91 | |||
a5b7badb95 | |||
eefd4c8669 | |||
7c11f2db4f | |||
7501f679f7 | |||
52e087d8f8 | |||
c5cd495fce | |||
37a417051d | |||
97d819739c | |||
3935378b0c | |||
952f95aa4c | |||
0a75a5be1f | |||
70b593e28a | |||
ed69a65f9b | |||
05b4f5401e | |||
2ff0c7b06a | |||
2330f1d135 | |||
ff92d3acab | |||
32d7187db6 | |||
00534f8af7 | |||
3795570938 | |||
362248a02e | |||
16d20eabd2 | |||
eb5566f041 | |||
757da1dff4 | |||
85432af187 | |||
f0ab817e87 | |||
96af931c0b | |||
9a5209f7c2 | |||
0818a7cda0 | |||
f35947361c | |||
9a8bcc0550 | |||
eff46b076c | |||
ab64b682c3 | |||
1c007ea256 | |||
6c1412784b | |||
6e7f4df5e3 | |||
5eff4e45c9 | |||
70a13a75d5 | |||
69cc1086df | |||
3d891073da | |||
73c2c01def | |||
7c82e31b66 | |||
2829b8d495 | |||
7ade873365 | |||
8788523c25 | |||
c301523156 | |||
2e8604e18d | |||
cae85f3e30 |
114
API.md
Normal file
114
API.md
Normal file
@ -0,0 +1,114 @@
|
||||
# Open MCT API
|
||||
|
||||
The Open MCT framework public api can be utilized by building the application (`gulp install`) and then copying the file from `dist/main.js` to your directory
|
||||
of choice.
|
||||
|
||||
Open MCT supports AMD, CommonJS, and standard browser loading; it's easy to use
|
||||
in your project.
|
||||
|
||||
## Overview
|
||||
|
||||
Open MCT's goal is to allow you to browse, create, edit, and visualize all of the domain knowledge you need on a daily basis.
|
||||
|
||||
To do this, the main building block provided by Open MCT is the domain object-- the temperature sensor on the starboard solar panel, an overlay plot comparing the results of all temperature sensor, the command dictionary for a spacecraft, the individual commands in that dictionary, your "my documents" folder: all of these things are domain objects.
|
||||
|
||||
Domain objects have Types-- so a specific instrument temperature sensor is a "Telemetry Point," and turning on a drill for a certain duration of time is an "Activity". Types allow you to form an ontology of knowledge and provide an abstraction for grouping, visualizing, and interpreting data.
|
||||
|
||||
And then we have Views. Views allow you to visualize a domain object. Views can apply to specific domain objects; they may also apply to certain types of domain objects, or they may apply to everything. Views are simply a method of visualizing domain objects.
|
||||
|
||||
Regions allow you to specify what views are displayed for specific types of domain objects in response to different user actions-- for instance, you may want to display a different view while editing, or you may want to update the toolbar display when objects are selected. Regions allow you to map views to specific user actions.
|
||||
|
||||
Domain objects can be mutated and persisted, developers can create custom actions and apply them to domain objects, and many more things can be done. For more information, read on.
|
||||
|
||||
## The API
|
||||
|
||||
### `MCT.Type(options)`
|
||||
Status: First Draft
|
||||
|
||||
Returns a `typeInstance`. `options` is an object supporting the following properties:
|
||||
|
||||
* `metadata`: `object` defining metadata used in displaying the object; has the following properties:
|
||||
* `label`: `string`, the human-readible name of the type. used in menus and inspector.
|
||||
* `glyph`: `string`, the name of the icon to display for this type, used in labels.
|
||||
* `description`: `string`, a human readible description of the object and what it is for.
|
||||
* `initialize`: `function` which initializes new instances of this type. it is called with an object, should add any default properties to that object.
|
||||
* `creatable`: `boolean`, if true, this object will be visible in the create menu.
|
||||
* `form`: `Array` an array of form fields, as defined... somewhere! Generates a property sheet that is visible while editing this object.
|
||||
|
||||
### `MCT.type(typeKey, typeInstance)`
|
||||
Status: First Draft
|
||||
|
||||
Register a `typeInstance` with a given Type `key` (a `string`). There can only be one `typeInstance` registered per type `key`. typeInstances must be registered before they can be utilized.
|
||||
|
||||
### `MCT.Objects`
|
||||
Status: First Draft
|
||||
|
||||
Allows you to register object providers, which allows you to integrate domain objects from various different sources. Also implements methods for mutation and persistence of objects. See [Object API](src/api/objects/README.md) for more details.
|
||||
|
||||
### `MCT.Composition`
|
||||
Status: First Draft
|
||||
|
||||
Objects can contain other objects, and the Composition API allows you to fetch the composition of any given domain object, or implement custom methods for defining composition as necessary.
|
||||
|
||||
### `MCT.view(region, definition)`
|
||||
Status: First Draft
|
||||
|
||||
Register a view factory for a specific region. View factories receive an instance of a domain object and return a `View` for that object, or return undefined if they do not know how to generate a view for that object.
|
||||
|
||||
* `ViewDefinition`: an object with the following properties:
|
||||
* `canView(domainObject)`: should return truthy if the view is valid for a given domain object, falsy if it is not capable of generating a view for that object.
|
||||
* `view(domainObject)`: should instantate and return a `View` for the given object.
|
||||
* `metadata()`: a function that returns metadata about this view. Optional.
|
||||
* `View`: an object containing a number of lifecycle methods:
|
||||
* `view.show(container)`: instantiate a view (a set of dom elements) and attach it to the container.
|
||||
* `view.destroy(container)`: remove any listeners and expect your dom elements to be destroyed.
|
||||
|
||||
For a basic introduction to views & types, check out these tutorials:
|
||||
|
||||
* [custom-view](custom-view.html) -- Implementing a custom view with vanilla javascript.
|
||||
* [custom-view-react](custom-view-react.html) -- Implementing a custom view with React.
|
||||
|
||||
### `MCT.conductor`
|
||||
Status: First Draft
|
||||
|
||||
The time conductor is an API that facilitates time synchronization across multiple components. Components that would like to be "time aware" may attach listeners to the time conductor API to allow them to remain synchronized with other components. For more information ont he time conductor API, please look at the API draft here: https://github.com/nasa/openmct/blob/66220b89ca568075f107505ba414de9457dc0427/platform/features/conductor-redux/src/README.md
|
||||
|
||||
### `MCT.selection`
|
||||
Status: First Draft
|
||||
|
||||
Tracks the application's selection state (which elements of a view has a user selected?)
|
||||
|
||||
One or more JavaScript objects may be selected at any given time. User code is responsible for any necessary type-checking.
|
||||
|
||||
The following methods are exposed from this object:
|
||||
|
||||
* `select(value)`: Add `value` to the current selection.
|
||||
* `deselect(value)`: Remove `value` from the current selection.
|
||||
* `selected()`: Get array of all selected objects.
|
||||
* `clear()`: Deselect all selected objects.
|
||||
|
||||
MCT.selection is an EventEmitter; a `change` event is emitted whenever the selection changes.
|
||||
|
||||
### `MCT.systems`
|
||||
Status: Not Implemented, Needs to be ported from old system.
|
||||
|
||||
A registry for different time system definitions. Based upon the previous time format system which utilized the "formats" extension category.
|
||||
|
||||
### `MCT.run([container])`
|
||||
Status: Stable Draft
|
||||
|
||||
Run the MCT application, loading the application into the `container`, a DOM element. If a container is not specified, the application is injected into the body of the page.
|
||||
|
||||
### `MCT.install(plugin)`
|
||||
Status: Stable Draft
|
||||
|
||||
Install a plugin in MCT. Must be called before calling `run`. Plugins are functions which are invoked with the `MCT` instance as their first argument, and are expected to use the MCT public API to add functionality.
|
||||
|
||||
For an example of writing a plugin, check out [plugin-example.html](plugin-example.html)
|
||||
|
||||
### `MCT.setAssetPath(path)`
|
||||
|
||||
Sets the path (absolute or relative) at which the Open MCT static files are being hosted. The default value is '.'.
|
||||
|
||||
Note that this API is transitional and will be removed in a future version.
|
||||
|
@ -18,6 +18,9 @@
|
||||
"node-uuid": "^1.4.7",
|
||||
"comma-separated-values": "^3.6.4",
|
||||
"FileSaver.js": "^0.0.2",
|
||||
"zepto": "^1.1.6"
|
||||
"zepto": "^1.1.6",
|
||||
"eventemitter3": "^1.2.0",
|
||||
"lodash": "3.10.1",
|
||||
"almond": "~0.3.2"
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ deployment:
|
||||
test:
|
||||
post:
|
||||
- gulp lint
|
||||
- gulp checkstyle
|
||||
|
||||
general:
|
||||
branches:
|
||||
|
65
composition-test.html
Normal file
65
composition-test.html
Normal file
@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||
<title>Implementing a composition provider</title>
|
||||
<script src="dist/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
var widgetParts = ['foo', 'bar', 'baz', 'bing', 'frobnak']
|
||||
|
||||
function fabricateName() {
|
||||
return [
|
||||
widgetParts[Math.floor(Math.random() * widgetParts.length)],
|
||||
widgetParts[Math.floor(Math.random() * widgetParts.length)],
|
||||
Math.floor(Math.random() * 1000)
|
||||
].join('_');
|
||||
}
|
||||
|
||||
MCT.type('example.widget-factory', new MCT.Type({
|
||||
metadata: {
|
||||
label: "Widget Factory",
|
||||
glyph: "s",
|
||||
description: "A factory for making widgets"
|
||||
},
|
||||
initialize: function (object) {
|
||||
object.widgetCount = 5;
|
||||
object.composition = [];
|
||||
},
|
||||
creatable: true,
|
||||
form: [
|
||||
{
|
||||
name: "Widget Count",
|
||||
control: "textfield",
|
||||
key: "widgetCount",
|
||||
property: "widgetCount",
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
MCT.Composition.addProvider({
|
||||
appliesTo: function (domainObject) {
|
||||
return domainObject.type === 'example.widget-factory';
|
||||
},
|
||||
load: function (domainObject) {
|
||||
var widgets = [];
|
||||
while (widgets.length < domainObject.widgetCount) {
|
||||
widgets.push({
|
||||
name: fabricateName(),
|
||||
key: {
|
||||
namespace: 'widget-factory',
|
||||
identifier: '' + widgets.length
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve(widgets);
|
||||
}
|
||||
});
|
||||
|
||||
MCT.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
144
custom-view-react.html
Normal file
144
custom-view-react.html
Normal file
@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||
<title>Implementing a Custom Type and View </title>
|
||||
<script src="dist/main.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.2.1/react.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.2.1/react-dom.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/babel">
|
||||
|
||||
// First, we're going to create the Todo List type, so that users can create
|
||||
// todo lists.
|
||||
|
||||
MCT.type('example.todo', new MCT.Type({
|
||||
metadata: {
|
||||
label: "To-Do List",
|
||||
glyph: "2",
|
||||
description: "A list of things that need to be done."
|
||||
},
|
||||
initialize: function (object) {
|
||||
object.tasks = [
|
||||
{ description: "This is a task." }
|
||||
];
|
||||
},
|
||||
creatable: true
|
||||
}));
|
||||
|
||||
/*
|
||||
Refresh the page, and you should be able to create new Todo Lists.
|
||||
unfortunately, when you navigate to a Todo list, you see a blank page. let's
|
||||
fix that by adding a main view for that todo list.
|
||||
|
||||
If you're wondering why this is commented out, well, it's because we'll
|
||||
write a new version later.
|
||||
*/
|
||||
|
||||
var Task = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<li>
|
||||
<input type="checkbox"
|
||||
checked={this.props.checked}/>
|
||||
<span>{this.props.description}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var TaskList = React.createClass({
|
||||
render: function () {
|
||||
var taskNodes = this.props.tasks.map(function(task) {
|
||||
return (
|
||||
<Task checked={task.checked}
|
||||
description={task.description}/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<ul>
|
||||
{taskNodes}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
MCT.view(MCT.regions.main, {
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'example.todo';
|
||||
},
|
||||
view: function (domainObject) {
|
||||
var mutableObject = MCT.Objects.getMutable(domainObject);
|
||||
|
||||
return {
|
||||
show: function (container) {
|
||||
ReactDOM.render(
|
||||
<TaskList tasks={domainObject.tasks}/>,
|
||||
container
|
||||
);
|
||||
mutableObject.on('tasks', function (tasks) {
|
||||
ReactDOM.render(
|
||||
<TaskList tasks={tasks}/>,
|
||||
container
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Refresh the page and you should see a todo list with checkboxes! Now let's
|
||||
Allow you to add tasks by mutating the object. We'll add a toolbar view to
|
||||
do this.
|
||||
*/
|
||||
|
||||
var TaskToolbar = React.createClass({
|
||||
render: function () {
|
||||
return (
|
||||
<button onClick={this.props.addTask}>Add Task</button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
MCT.view(MCT.regions.toolbar, {
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'example.todo';
|
||||
},
|
||||
view: function (domainObject) {
|
||||
var mutableObject = MCT.Objects.getMutable(domainObject);
|
||||
|
||||
function addTask(event) {
|
||||
var description = prompt('Task description');
|
||||
var tasks = mutableObject.get('tasks');
|
||||
tasks.push({
|
||||
description: description,
|
||||
complete: false
|
||||
});
|
||||
mutableObject.set('tasks', tasks);
|
||||
}
|
||||
|
||||
return {
|
||||
show: function (container) {
|
||||
ReactDOM.render(
|
||||
<TaskToolbar addTask={addTask}/>,
|
||||
container
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Refresh the page, edit the todo list, and you'll have a button that allows
|
||||
you to add tasks! Unfortunately, new tasks don't show in the list. Why?
|
||||
Well, if your view should update on mutation, you need to set up the correct
|
||||
listeners. Let's update the TodoView we made earlier:
|
||||
*/
|
||||
|
||||
MCT.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
160
custom-view.html
Normal file
160
custom-view.html
Normal file
@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||
<title>Implementing a Custom Type and View </title>
|
||||
<script src="dist/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
// First, we're going to create the Todo List type, so that users can create
|
||||
// todo lists.
|
||||
|
||||
MCT.type('example.todo', new MCT.Type({
|
||||
metadata: {
|
||||
label: "To-Do List",
|
||||
glyph: "2",
|
||||
description: "A list of things that need to be done."
|
||||
},
|
||||
initialize: function (object) {
|
||||
object.tasks = [
|
||||
{ description: "This is a task." }
|
||||
];
|
||||
},
|
||||
creatable: true
|
||||
}));
|
||||
|
||||
/*
|
||||
Refresh the page, and you should be able to create new Todo Lists.
|
||||
unfortunately, when you navigate to a Todo list, you see a blank page. let's
|
||||
fix that by adding a main view for that todo list.
|
||||
|
||||
If you're wondering why this is commented out, well, it's because we'll
|
||||
write a new version later.
|
||||
*/
|
||||
|
||||
// MCT.view(MCT.regions.main, {
|
||||
// canView: function (domainObject) {
|
||||
// return domainObject.type === 'example.todo';
|
||||
// },
|
||||
// view: function (domainObject) {
|
||||
// function renderTask(task) {
|
||||
// return [
|
||||
// '<li>',
|
||||
// '<input type="checkbox"' + (task.complete ? ' checked="true"' : '') + '>',
|
||||
// '<span>' + task.description + '</span>',
|
||||
// '</li>'
|
||||
// ].join('');
|
||||
// };
|
||||
//
|
||||
// function renderTaskList() {
|
||||
// return [
|
||||
// '<ul>',
|
||||
// domainObject.tasks.map(renderTask).join(''),
|
||||
// '</ul>'
|
||||
// ].join('');
|
||||
// };
|
||||
//
|
||||
// return {
|
||||
// show: function (container) {
|
||||
// container.innerHTML = renderTaskList();
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
|
||||
/*
|
||||
Refresh the page and you should see a todo list with checkboxes! Now let's
|
||||
Allow you to add tasks by mutating the object. We'll add a toolbar view to
|
||||
do this.
|
||||
*/
|
||||
|
||||
MCT.view(MCT.regions.toolbar, {
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'example.todo';
|
||||
},
|
||||
view: function (domainObject) {
|
||||
var mutableObject = MCT.Objects.getMutable(domainObject);
|
||||
|
||||
function addTask(event) {
|
||||
var description = prompt('Task description');
|
||||
var tasks = mutableObject.get('tasks');
|
||||
tasks.push({
|
||||
description: description,
|
||||
complete: false
|
||||
});
|
||||
mutableObject.set('tasks', tasks);
|
||||
}
|
||||
|
||||
return {
|
||||
show: function (container) {
|
||||
container.addEventListener('click', addTask);
|
||||
container.innerHTML = '<button>Add Task</button>';
|
||||
},
|
||||
destroy: function (container) {
|
||||
container.removeEventListener('click', addTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Refresh the page, edit the todo list, and you'll have a button that allows
|
||||
you to add tasks! Unfortunately, new tasks don't show in the list. Why?
|
||||
Well, if your view should update on mutation, you need to set up the correct
|
||||
listeners. Let's update the TodoView we made earlier:
|
||||
*/
|
||||
|
||||
MCT.view(MCT.regions.main, {
|
||||
canView: function(domainObject) {
|
||||
return domainObject.type === 'example.todo'
|
||||
},
|
||||
view: function (domainObject) {
|
||||
|
||||
var mutableObject = MCT.Objects.getMutable(domainObject);
|
||||
|
||||
function renderTask(task) {
|
||||
return [
|
||||
'<li>',
|
||||
'<input type="checkbox"' + (task.complete ? ' checked="true"' : '') + '>',
|
||||
'<span>' + task.description + '</span>',
|
||||
'</li>'
|
||||
].join('');
|
||||
}
|
||||
|
||||
function renderTaskList(tasks) {
|
||||
return [
|
||||
'<ul>',
|
||||
tasks.map(renderTask).join(''),
|
||||
'</ul>'
|
||||
].join('');
|
||||
}
|
||||
|
||||
function onCheckboxChange(event) {
|
||||
var checkbox = event.target;
|
||||
var taskEl = checkbox.parentNode;
|
||||
var taskList = taskEl.parentNode;
|
||||
var taskIndex = [].slice.apply(taskList.children).indexOf(taskEl);
|
||||
mutableObject.set('tasks[' + taskIndex + '].complete', checkbox.checked);
|
||||
}
|
||||
|
||||
return {
|
||||
show: function (container) {
|
||||
container.innerHTML = renderTaskList(domainObject.tasks);
|
||||
mutableObject.on('tasks', function (tasks) {
|
||||
container.innerHTML = renderTaskList(tasks);
|
||||
});
|
||||
container.addEventListener('change', onCheckboxChange);
|
||||
},
|
||||
destroy: function () {
|
||||
mutableObject.stopListening();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
MCT.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
@ -36,7 +36,7 @@ define([
|
||||
legacyRegistry
|
||||
) {
|
||||
"use strict";
|
||||
legacyRegistry.register("example/notifications", {
|
||||
legacyRegistry.register("example/msl-adapter", {
|
||||
"name" : "Mars Science Laboratory Data Adapter",
|
||||
"extensions" : {
|
||||
"types": [
|
||||
|
@ -45,11 +45,12 @@ define(
|
||||
function buildTaxonomy(dictionary){
|
||||
var models = {};
|
||||
|
||||
function addMeasurement(measurement){
|
||||
function addMeasurement(measurement, parent){
|
||||
var format = FORMAT_MAPPINGS[measurement.type];
|
||||
models[makeId(measurement)] = {
|
||||
type: "msl.measurement",
|
||||
name: measurement.name,
|
||||
location: parent,
|
||||
telemetry: {
|
||||
key: measurement.identifier,
|
||||
ranges: [{
|
||||
@ -62,17 +63,24 @@ define(
|
||||
};
|
||||
}
|
||||
|
||||
function addInstrument(subsystem) {
|
||||
var measurements = (subsystem.measurements || []);
|
||||
models[makeId(subsystem)] = {
|
||||
function addInstrument(subsystem, spacecraftId) {
|
||||
var measurements = (subsystem.measurements || []),
|
||||
instrumentId = makeId(subsystem);
|
||||
|
||||
models[instrumentId] = {
|
||||
type: "msl.instrument",
|
||||
name: subsystem.name,
|
||||
location: spacecraftId,
|
||||
composition: measurements.map(makeId)
|
||||
};
|
||||
measurements.forEach(addMeasurement);
|
||||
measurements.forEach(function(measurement) {
|
||||
addMeasurement(measurement, instrumentId);
|
||||
});
|
||||
}
|
||||
|
||||
(dictionary.instruments || []).forEach(addInstrument);
|
||||
(dictionary.instruments || []).forEach(function(instrument) {
|
||||
addInstrument(instrument, "msl:curiosity");
|
||||
});
|
||||
return models;
|
||||
}
|
||||
|
||||
|
53
gulpfile.js
53
gulpfile.js
@ -21,40 +21,33 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*global require,__dirname*/
|
||||
|
||||
var gulp = require('gulp'),
|
||||
requirejsOptimize = require('gulp-requirejs-optimize'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
rename = require('gulp-rename'),
|
||||
sass = require('gulp-sass'),
|
||||
bourbon = require('node-bourbon'),
|
||||
jshint = require('gulp-jshint'),
|
||||
jscs = require('gulp-jscs'),
|
||||
replace = require('gulp-replace-task'),
|
||||
karma = require('karma'),
|
||||
path = require('path'),
|
||||
fs = require('fs'),
|
||||
git = require('git-rev-sync'),
|
||||
moment = require('moment'),
|
||||
merge = require('merge-stream'),
|
||||
project = require('./package.json'),
|
||||
_ = require('lodash'),
|
||||
paths = {
|
||||
main: 'main.js',
|
||||
dist: 'dist',
|
||||
assets: 'dist/assets',
|
||||
scss: ['./platform/**/*.scss', './example/**/*.scss'],
|
||||
assets: [
|
||||
'./{example,platform}/**/*.{css,css.map,png,svg,ico,woff,eot,ttf}'
|
||||
],
|
||||
scripts: [ 'main.js', 'platform/**/*.js', 'src/**/*.js' ],
|
||||
specs: [ 'platform/**/*Spec.js', 'src/**/*Spec.js' ],
|
||||
static: [
|
||||
'index.html',
|
||||
'platform/**/*',
|
||||
'example/**/*',
|
||||
'bower_components/**/*'
|
||||
]
|
||||
},
|
||||
options = {
|
||||
requirejsOptimize: {
|
||||
name: paths.main.replace(/\.js$/, ''),
|
||||
name: 'bower_components/almond/almond.js',
|
||||
include: paths.main.replace('.js', ''),
|
||||
wrap: {
|
||||
startFile: "src/start.frag",
|
||||
endFile: "src/end.frag"
|
||||
},
|
||||
mainConfigFile: paths.main,
|
||||
wrapShim: true
|
||||
},
|
||||
@ -63,7 +56,6 @@ var gulp = require('gulp'),
|
||||
singleRun: true
|
||||
},
|
||||
sass: {
|
||||
includePaths: bourbon.includePaths,
|
||||
sourceComments: true
|
||||
},
|
||||
replace: {
|
||||
@ -77,6 +69,8 @@ var gulp = require('gulp'),
|
||||
};
|
||||
|
||||
gulp.task('scripts', function () {
|
||||
var requirejsOptimize = require('gulp-requirejs-optimize');
|
||||
var replace = require('gulp-replace-task');
|
||||
return gulp.src(paths.main)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(requirejsOptimize(options.requirejsOptimize))
|
||||
@ -86,10 +80,16 @@ gulp.task('scripts', function () {
|
||||
});
|
||||
|
||||
gulp.task('test', function (done) {
|
||||
var karma = require('karma');
|
||||
new karma.Server(options.karma, done).start();
|
||||
});
|
||||
|
||||
gulp.task('stylesheets', function () {
|
||||
var sass = require('gulp-sass');
|
||||
var rename = require('gulp-rename');
|
||||
var bourbon = require('node-bourbon');
|
||||
options.sass.includePaths = bourbon.includePaths;
|
||||
|
||||
return gulp.src(paths.scss, {base: '.'})
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass(options.sass).on('error', sass.logError))
|
||||
@ -103,6 +103,9 @@ gulp.task('stylesheets', function () {
|
||||
});
|
||||
|
||||
gulp.task('lint', function () {
|
||||
var jshint = require('gulp-jshint');
|
||||
var merge = require('merge-stream');
|
||||
|
||||
var nonspecs = paths.specs.map(function (glob) {
|
||||
return "!" + glob;
|
||||
}),
|
||||
@ -117,6 +120,8 @@ gulp.task('lint', function () {
|
||||
});
|
||||
|
||||
gulp.task('checkstyle', function () {
|
||||
var jscs = require('gulp-jscs');
|
||||
|
||||
return gulp.src(paths.scripts)
|
||||
.pipe(jscs())
|
||||
.pipe(jscs.reporter())
|
||||
@ -124,18 +129,20 @@ gulp.task('checkstyle', function () {
|
||||
});
|
||||
|
||||
gulp.task('fixstyle', function () {
|
||||
var jscs = require('gulp-jscs');
|
||||
|
||||
return gulp.src(paths.scripts, { base: '.' })
|
||||
.pipe(jscs({ fix: true }))
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
gulp.task('static', ['stylesheets'], function () {
|
||||
return gulp.src(paths.static, { base: '.' })
|
||||
gulp.task('assets', ['stylesheets'], function () {
|
||||
return gulp.src(paths.assets)
|
||||
.pipe(gulp.dest(paths.dist));
|
||||
});
|
||||
|
||||
gulp.task('watch', function () {
|
||||
gulp.watch(paths.scss, ['stylesheets']);
|
||||
return gulp.watch(paths.scss, ['stylesheets', 'assets']);
|
||||
});
|
||||
|
||||
gulp.task('serve', function () {
|
||||
@ -143,9 +150,9 @@ gulp.task('serve', function () {
|
||||
var app = require('./app.js');
|
||||
});
|
||||
|
||||
gulp.task('develop', ['serve', 'stylesheets', 'watch']);
|
||||
gulp.task('develop', ['serve', 'install', 'watch']);
|
||||
|
||||
gulp.task('install', [ 'static', 'scripts' ]);
|
||||
gulp.task('install', [ 'assets', 'scripts' ]);
|
||||
|
||||
gulp.task('verify', [ 'lint', 'test', 'checkstyle' ]);
|
||||
|
||||
|
@ -31,10 +31,14 @@
|
||||
<script type="text/javascript">
|
||||
require(['main'], function (mct) {
|
||||
require([
|
||||
'./tutorials/todo/todo',
|
||||
'./example/imagery/bundle',
|
||||
'./example/eventGenerator/bundle',
|
||||
'./example/generator/bundle'
|
||||
], mct.run.bind(mct));
|
||||
], function (todoPlugin) {
|
||||
mct.install(todoPlugin);
|
||||
mct.run();
|
||||
})
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">
|
||||
@ -48,7 +52,5 @@
|
||||
<div class="l-splash-holder s-splash-holder">
|
||||
<div class="l-splash s-splash"></div>
|
||||
</div>
|
||||
|
||||
<div ng-view></div>
|
||||
</body>
|
||||
</html>
|
||||
|
68
main.js
68
main.js
@ -28,13 +28,15 @@ requirejs.config({
|
||||
"angular-route": "bower_components/angular-route/angular-route.min",
|
||||
"csv": "bower_components/comma-separated-values/csv.min",
|
||||
"es6-promise": "bower_components/es6-promise/promise.min",
|
||||
"EventEmitter": "bower_components/eventemitter3/index",
|
||||
"moment": "bower_components/moment/moment",
|
||||
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
|
||||
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
|
||||
"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": {
|
||||
@ -43,6 +45,9 @@ requirejs.config({
|
||||
"angular-route": {
|
||||
"deps": ["angular"]
|
||||
},
|
||||
"EventEmitter": {
|
||||
"exports": "EventEmitter"
|
||||
},
|
||||
"moment-duration-format": {
|
||||
"deps": ["moment"]
|
||||
},
|
||||
@ -51,53 +56,32 @@ requirejs.config({
|
||||
},
|
||||
"zepto": {
|
||||
"exports": "Zepto"
|
||||
},
|
||||
"lodash": {
|
||||
"exports": "lodash"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
define([
|
||||
'./platform/framework/src/Main',
|
||||
'legacyRegistry',
|
||||
'./src/defaultRegistry',
|
||||
'./src/MCT'
|
||||
], function (Main, defaultRegistry, MCT) {
|
||||
var mct = new MCT();
|
||||
|
||||
'./platform/framework/bundle',
|
||||
'./platform/core/bundle',
|
||||
'./platform/representation/bundle',
|
||||
'./platform/commonUI/about/bundle',
|
||||
'./platform/commonUI/browse/bundle',
|
||||
'./platform/commonUI/edit/bundle',
|
||||
'./platform/commonUI/dialog/bundle',
|
||||
'./platform/commonUI/formats/bundle',
|
||||
'./platform/commonUI/general/bundle',
|
||||
'./platform/commonUI/inspect/bundle',
|
||||
'./platform/commonUI/mobile/bundle',
|
||||
'./platform/commonUI/themes/espresso/bundle',
|
||||
'./platform/commonUI/notification/bundle',
|
||||
'./platform/containment/bundle',
|
||||
'./platform/execution/bundle',
|
||||
'./platform/exporters/bundle',
|
||||
'./platform/telemetry/bundle',
|
||||
'./platform/features/clock/bundle',
|
||||
'./platform/features/imagery/bundle',
|
||||
'./platform/features/layout/bundle',
|
||||
'./platform/features/pages/bundle',
|
||||
'./platform/features/plot/bundle',
|
||||
'./platform/features/timeline/bundle',
|
||||
'./platform/features/table/bundle',
|
||||
'./platform/forms/bundle',
|
||||
'./platform/identity/bundle',
|
||||
'./platform/persistence/aggregator/bundle',
|
||||
'./platform/persistence/local/bundle',
|
||||
'./platform/persistence/queue/bundle',
|
||||
'./platform/policy/bundle',
|
||||
'./platform/entanglement/bundle',
|
||||
'./platform/search/bundle',
|
||||
'./platform/status/bundle',
|
||||
'./platform/commonUI/regions/bundle'
|
||||
], function (Main, legacyRegistry) {
|
||||
return {
|
||||
legacyRegistry: legacyRegistry,
|
||||
run: function () {
|
||||
return new Main().run(legacyRegistry);
|
||||
}
|
||||
mct.legacyRegistry = defaultRegistry;
|
||||
mct.run = function (domElement) {
|
||||
if (!domElement) { domElement = document.body; }
|
||||
var appDiv = document.createElement('div');
|
||||
appDiv.setAttribute('ng-view', '');
|
||||
appDiv.className = 'user-environ';
|
||||
domElement.appendChild(appDiv);
|
||||
mct.start();
|
||||
};
|
||||
mct.on('start', function () {
|
||||
return new Main().run(defaultRegistry);
|
||||
});
|
||||
|
||||
return mct;
|
||||
});
|
||||
|
64
object-provider.html
Normal file
64
object-provider.html
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||
<title>Groot Tutorial</title>
|
||||
<script src="dist/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
// First, we need a source of objects, so we're going to define a few
|
||||
// objects that we wish to expose. The first object is a folder which
|
||||
// contains the other objects.
|
||||
|
||||
|
||||
var GROOT_ROOT = {
|
||||
name: 'I am groot',
|
||||
type: 'folder',
|
||||
composition: [
|
||||
{
|
||||
namespace: 'groot',
|
||||
identifier: 'arms'
|
||||
},
|
||||
{
|
||||
namespace: 'groot',
|
||||
identifier: 'legs'
|
||||
},
|
||||
{
|
||||
namespace: 'groot',
|
||||
identifier: 'torso'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Now, we will define an object provider. This will allow us to fetch the
|
||||
// GROOT_ROOT object, plus any other objects in the `groot` namespace.
|
||||
// For more info, see the Object API documentation.
|
||||
|
||||
MCT.Objects.addProvider('groot', {
|
||||
// we'll only define a get function, objects from this provider will
|
||||
// not be mutable.
|
||||
get: function (key) {
|
||||
if (key.identifier === 'groot') {
|
||||
return Promise.resolve(GROOT_ROOT);
|
||||
}
|
||||
return Promise.resolve({
|
||||
name: 'Groot\'s ' + key.identifier
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Finally, we need to add a "ROOT." This is an identifier for an object
|
||||
// that Open MCT will load at run time and show at the top-level of the
|
||||
// navigation tree.
|
||||
|
||||
MCT.Objects.addRoot({
|
||||
namespace: 'groot',
|
||||
identifier: 'groot'
|
||||
});
|
||||
|
||||
MCT.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -24,23 +24,14 @@ define([
|
||||
"./src/BrowseController",
|
||||
"./src/PaneController",
|
||||
"./src/BrowseObjectController",
|
||||
"./src/creation/CreateMenuController",
|
||||
"./src/creation/LocatorController",
|
||||
"./src/MenuArrowController",
|
||||
"./src/navigation/NavigationService",
|
||||
"./src/creation/CreationPolicy",
|
||||
"./src/navigation/NavigateAction",
|
||||
"./src/windowing/NewTabAction",
|
||||
"./src/windowing/FullscreenAction",
|
||||
"./src/creation/CreateActionProvider",
|
||||
"./src/creation/AddActionProvider",
|
||||
"./src/creation/CreationService",
|
||||
"./src/windowing/WindowTitler",
|
||||
"text!./res/templates/browse.html",
|
||||
"text!./res/templates/create/locator.html",
|
||||
"text!./res/templates/browse-object.html",
|
||||
"text!./res/templates/create/create-button.html",
|
||||
"text!./res/templates/create/create-menu.html",
|
||||
"text!./res/templates/items/grid-item.html",
|
||||
"text!./res/templates/browse/object-header.html",
|
||||
"text!./res/templates/menu-arrow.html",
|
||||
@ -48,28 +39,20 @@ define([
|
||||
"text!./res/templates/items/items.html",
|
||||
"text!./res/templates/browse/object-properties.html",
|
||||
"text!./res/templates/browse/inspector-region.html",
|
||||
"text!./res/templates/view-object.html",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
BrowseController,
|
||||
PaneController,
|
||||
BrowseObjectController,
|
||||
CreateMenuController,
|
||||
LocatorController,
|
||||
MenuArrowController,
|
||||
NavigationService,
|
||||
CreationPolicy,
|
||||
NavigateAction,
|
||||
NewTabAction,
|
||||
FullscreenAction,
|
||||
CreateActionProvider,
|
||||
AddActionProvider,
|
||||
CreationService,
|
||||
WindowTitler,
|
||||
browseTemplate,
|
||||
locatorTemplate,
|
||||
browseObjectTemplate,
|
||||
createButtonTemplate,
|
||||
createMenuTemplate,
|
||||
gridItemTemplate,
|
||||
objectHeaderTemplate,
|
||||
menuArrowTemplate,
|
||||
@ -77,6 +60,7 @@ define([
|
||||
itemsTemplate,
|
||||
objectPropertiesTemplate,
|
||||
inspectorRegionTemplate,
|
||||
viewObjectTemplate,
|
||||
legacyRegistry
|
||||
) {
|
||||
|
||||
@ -136,22 +120,6 @@ define([
|
||||
"$route"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "CreateMenuController",
|
||||
"implementation": CreateMenuController,
|
||||
"depends": [
|
||||
"$scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "LocatorController",
|
||||
"implementation": LocatorController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$timeout",
|
||||
"objectService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "MenuArrowController",
|
||||
"implementation": MenuArrowController,
|
||||
@ -160,16 +128,10 @@ define([
|
||||
]
|
||||
}
|
||||
],
|
||||
"controls": [
|
||||
{
|
||||
"key": "locator",
|
||||
"template": locatorTemplate
|
||||
}
|
||||
],
|
||||
"representations": [
|
||||
{
|
||||
"key": "view-object",
|
||||
"templateUrl": "templates/view-object.html"
|
||||
"template": viewObjectTemplate
|
||||
},
|
||||
{
|
||||
"key": "browse-object",
|
||||
@ -181,17 +143,6 @@ define([
|
||||
"view"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "create-button",
|
||||
"template": createButtonTemplate
|
||||
},
|
||||
{
|
||||
"key": "create-menu",
|
||||
"template": createMenuTemplate,
|
||||
"uses": [
|
||||
"action"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "grid-item",
|
||||
"template": gridItemTemplate,
|
||||
@ -244,12 +195,6 @@ define([
|
||||
"implementation": NavigationService
|
||||
}
|
||||
],
|
||||
"policies": [
|
||||
{
|
||||
"implementation": CreationPolicy,
|
||||
"category": "creation"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"key": "navigate",
|
||||
@ -302,42 +247,6 @@ define([
|
||||
"editable": false
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"key": "CreateActionProvider",
|
||||
"provides": "actionService",
|
||||
"type": "provider",
|
||||
"implementation": CreateActionProvider,
|
||||
"depends": [
|
||||
"$q",
|
||||
"typeService",
|
||||
"navigationService",
|
||||
"policyService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "AddActionProvider",
|
||||
"provides": "actionService",
|
||||
"type": "provider",
|
||||
"implementation": AddActionProvider,
|
||||
"depends": [
|
||||
"$q",
|
||||
"typeService",
|
||||
"dialogService",
|
||||
"policyService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "CreationService",
|
||||
"provides": "creationService",
|
||||
"type": "provider",
|
||||
"implementation": CreationService,
|
||||
"depends": [
|
||||
"$q",
|
||||
"$log"
|
||||
]
|
||||
}
|
||||
],
|
||||
"runs": [
|
||||
{
|
||||
"implementation": WindowTitler,
|
||||
|
@ -1,130 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/creation/CreateAction"],
|
||||
function (CreateAction) {
|
||||
|
||||
describe("The create action", function () {
|
||||
var mockType,
|
||||
mockParent,
|
||||
mockContext,
|
||||
mockDialogService,
|
||||
mockCreationService,
|
||||
action;
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockType = jasmine.createSpyObj(
|
||||
"type",
|
||||
[
|
||||
"getKey",
|
||||
"getGlyph",
|
||||
"getName",
|
||||
"getDescription",
|
||||
"getProperties",
|
||||
"getInitialModel"
|
||||
]
|
||||
);
|
||||
mockParent = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[
|
||||
"getId",
|
||||
"getModel",
|
||||
"getCapability"
|
||||
]
|
||||
);
|
||||
mockContext = {
|
||||
domainObject: mockParent
|
||||
};
|
||||
mockDialogService = jasmine.createSpyObj(
|
||||
"dialogService",
|
||||
["getUserInput"]
|
||||
);
|
||||
mockCreationService = jasmine.createSpyObj(
|
||||
"creationService",
|
||||
["createObject"]
|
||||
);
|
||||
|
||||
mockType.getKey.andReturn("test");
|
||||
mockType.getGlyph.andReturn("T");
|
||||
mockType.getDescription.andReturn("a test type");
|
||||
mockType.getName.andReturn("Test");
|
||||
mockType.getProperties.andReturn([]);
|
||||
mockType.getInitialModel.andReturn({});
|
||||
|
||||
mockDialogService.getUserInput.andReturn(mockPromise({}));
|
||||
|
||||
action = new CreateAction(
|
||||
mockType,
|
||||
mockParent,
|
||||
mockContext,
|
||||
mockDialogService,
|
||||
mockCreationService
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes type-appropriate metadata", function () {
|
||||
var metadata = action.getMetadata();
|
||||
|
||||
expect(metadata.name).toEqual("Test");
|
||||
expect(metadata.description).toEqual("a test type");
|
||||
expect(metadata.glyph).toEqual("T");
|
||||
});
|
||||
|
||||
//TODO: Disabled for NEM Beta
|
||||
xit("invokes the creation service when performed", function () {
|
||||
action.perform();
|
||||
expect(mockCreationService.createObject).toHaveBeenCalledWith(
|
||||
{ type: "test" },
|
||||
mockParent
|
||||
);
|
||||
});
|
||||
|
||||
//TODO: Disabled for NEM Beta
|
||||
xit("does not create an object if the user cancels", function () {
|
||||
mockDialogService.getUserInput.andReturn({
|
||||
then: function (callback, fail) {
|
||||
fail();
|
||||
}
|
||||
});
|
||||
|
||||
action.perform();
|
||||
|
||||
expect(mockCreationService.createObject)
|
||||
.not.toHaveBeenCalled();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -43,6 +43,15 @@ define([
|
||||
"./src/capabilities/EditorCapability",
|
||||
"./src/capabilities/TransactionCapabilityDecorator",
|
||||
"./src/services/TransactionService",
|
||||
"./src/creation/CreateMenuController",
|
||||
"./src/creation/LocatorController",
|
||||
"./src/creation/CreationPolicy",
|
||||
"./src/creation/CreateActionProvider",
|
||||
"./src/creation/AddActionProvider",
|
||||
"./src/creation/CreationService",
|
||||
"text!./res/templates/create/locator.html",
|
||||
"text!./res/templates/create/create-button.html",
|
||||
"text!./res/templates/create/create-menu.html",
|
||||
"text!./res/templates/library.html",
|
||||
"text!./res/templates/edit-object.html",
|
||||
"text!./res/templates/edit-action-buttons.html",
|
||||
@ -72,6 +81,15 @@ define([
|
||||
EditorCapability,
|
||||
TransactionCapabilityDecorator,
|
||||
TransactionService,
|
||||
CreateMenuController,
|
||||
LocatorController,
|
||||
CreationPolicy,
|
||||
CreateActionProvider,
|
||||
AddActionProvider,
|
||||
CreationService,
|
||||
locatorTemplate,
|
||||
createButtonTemplate,
|
||||
createMenuTemplate,
|
||||
libraryTemplate,
|
||||
editObjectTemplate,
|
||||
editActionButtonsTemplate,
|
||||
@ -112,6 +130,22 @@ define([
|
||||
"$location",
|
||||
"policyService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "CreateMenuController",
|
||||
"implementation": CreateMenuController,
|
||||
"depends": [
|
||||
"$scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "LocatorController",
|
||||
"implementation": LocatorController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$timeout",
|
||||
"objectService"
|
||||
]
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
@ -219,10 +253,13 @@ define([
|
||||
},
|
||||
{
|
||||
"category": "navigation",
|
||||
"message": "There are unsaved changes.",
|
||||
"message": "Continuing will cause the loss of any unsaved changes.",
|
||||
"implementation": EditNavigationPolicy
|
||||
},
|
||||
{
|
||||
"implementation": CreationPolicy,
|
||||
"category": "creation"
|
||||
}
|
||||
|
||||
],
|
||||
"templates": [
|
||||
{
|
||||
@ -261,6 +298,17 @@ define([
|
||||
{
|
||||
"key": "topbar-edit",
|
||||
"template": topbarEditTemplate
|
||||
},
|
||||
{
|
||||
"key": "create-button",
|
||||
"template": createButtonTemplate
|
||||
},
|
||||
{
|
||||
"key": "create-menu",
|
||||
"template": createMenuTemplate,
|
||||
"uses": [
|
||||
"action"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
@ -282,7 +330,40 @@ define([
|
||||
"$q",
|
||||
"$log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "CreateActionProvider",
|
||||
"provides": "actionService",
|
||||
"type": "provider",
|
||||
"implementation": CreateActionProvider,
|
||||
"depends": [
|
||||
"typeService",
|
||||
"policyService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "AddActionProvider",
|
||||
"provides": "actionService",
|
||||
"type": "provider",
|
||||
"implementation": AddActionProvider,
|
||||
"depends": [
|
||||
"$q",
|
||||
"typeService",
|
||||
"dialogService",
|
||||
"policyService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "CreationService",
|
||||
"provides": "creationService",
|
||||
"type": "provider",
|
||||
"implementation": CreationService,
|
||||
"depends": [
|
||||
"$q",
|
||||
"$log"
|
||||
]
|
||||
}
|
||||
|
||||
],
|
||||
"representers": [
|
||||
{
|
||||
@ -316,6 +397,12 @@ define([
|
||||
"transactionService"
|
||||
]
|
||||
}
|
||||
],
|
||||
"controls": [
|
||||
{
|
||||
"key": "locator",
|
||||
"template": locatorTemplate
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
@ -74,6 +74,12 @@ define(
|
||||
self.domainObject.getCapability('editor').cancel();
|
||||
self.navigationService.removeListener(cancelEditing);
|
||||
}
|
||||
//If this is not the currently navigated object, then navigate
|
||||
// to it.
|
||||
if (this.navigationService.getNavigation() !== this.domainObject) {
|
||||
this.navigationService.setNavigation(this.domainObject);
|
||||
}
|
||||
|
||||
this.navigationService.addListener(cancelEditing);
|
||||
this.domainObject.useCapability("editor");
|
||||
};
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
|
||||
define(
|
||||
['../../../browse/src/creation/CreateWizard'],
|
||||
['../creation/CreateWizard'],
|
||||
function (CreateWizard) {
|
||||
|
||||
/**
|
||||
|
@ -43,11 +43,8 @@ define(
|
||||
* override this)
|
||||
* @param {ActionContext} context the context in which the
|
||||
* action is being performed
|
||||
* @param {NavigationService} navigationService the navigation service,
|
||||
* which handles changes in navigation. It allows the object
|
||||
* being browsed/edited to be set.
|
||||
*/
|
||||
function CreateAction(type, parent, context, $q, navigationService) {
|
||||
function CreateAction(type, parent, context) {
|
||||
this.metadata = {
|
||||
key: 'create',
|
||||
glyph: type.getGlyph(),
|
||||
@ -56,24 +53,8 @@ define(
|
||||
description: type.getDescription(),
|
||||
context: context
|
||||
};
|
||||
|
||||
this.type = type;
|
||||
this.parent = parent;
|
||||
this.navigationService = navigationService;
|
||||
this.$q = $q;
|
||||
}
|
||||
|
||||
// Get a count of views which are not flagged as non-editable.
|
||||
function countEditableViews(domainObject) {
|
||||
var views = domainObject && domainObject.useCapability('view'),
|
||||
count = 0;
|
||||
|
||||
// A view is editable unless explicitly flagged as not
|
||||
(views || []).forEach(function (view) {
|
||||
count += (view.editable !== false) ? 1 : 0;
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,26 +63,31 @@ define(
|
||||
*/
|
||||
CreateAction.prototype.perform = function () {
|
||||
var newModel = this.type.getInitialModel(),
|
||||
parentObject = this.navigationService.getNavigation(),
|
||||
editorCapability,
|
||||
newObject;
|
||||
newObject,
|
||||
editAction,
|
||||
editorCapability;
|
||||
|
||||
function onSave() {
|
||||
return editorCapability.save();
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
return editorCapability.cancel();
|
||||
}
|
||||
|
||||
newModel.type = this.type.getKey();
|
||||
newModel.location = parentObject.getId();
|
||||
newObject = parentObject.useCapability('instantiation', newModel);
|
||||
newModel.location = this.parent.getId();
|
||||
newObject = this.parent.useCapability('instantiation', newModel);
|
||||
editorCapability = newObject.hasCapability('editor') && newObject.getCapability("editor");
|
||||
|
||||
editorCapability = newObject.getCapability("editor");
|
||||
|
||||
if (countEditableViews(newObject) > 0 && newObject.hasCapability('composition')) {
|
||||
this.navigationService.setNavigation(newObject);
|
||||
return newObject.getCapability("action").perform("edit");
|
||||
} else {
|
||||
editAction = newObject.getCapability("action").getActions("edit")[0];
|
||||
//If an edit action is available, perform it
|
||||
if (editAction) {
|
||||
return editAction.perform();
|
||||
} else if (editorCapability) {
|
||||
//otherwise, use the save action
|
||||
editorCapability.edit();
|
||||
return newObject.useCapability("action").perform("save").then(function () {
|
||||
return editorCapability.save();
|
||||
}, function () {
|
||||
return editorCapability.cancel();
|
||||
});
|
||||
return newObject.getCapability("action").perform("save").then(onSave, onCancel);
|
||||
}
|
||||
};
|
||||
|
@ -44,10 +44,8 @@ define(
|
||||
* introduced in this bundle), responsible for handling actual
|
||||
* object creation.
|
||||
*/
|
||||
function CreateActionProvider($q, typeService, navigationService, policyService) {
|
||||
function CreateActionProvider(typeService, policyService) {
|
||||
this.typeService = typeService;
|
||||
this.navigationService = navigationService;
|
||||
this.$q = $q;
|
||||
this.policyService = policyService;
|
||||
}
|
||||
|
||||
@ -72,9 +70,7 @@ define(
|
||||
return new CreateAction(
|
||||
type,
|
||||
destination,
|
||||
context,
|
||||
self.$q,
|
||||
self.navigationService
|
||||
context
|
||||
);
|
||||
});
|
||||
};
|
@ -56,7 +56,10 @@ define(
|
||||
// A view is editable unless explicitly flagged as not
|
||||
(views || []).forEach(function (view) {
|
||||
if (view.editable === true ||
|
||||
(view.key === 'plot' && type.getKey() === 'telemetry.panel')) {
|
||||
(view.key === 'plot' && type.getKey() === 'telemetry.panel') ||
|
||||
(view.key === 'table' && type.getKey() === 'table') ||
|
||||
(view.key === 'rt-table' && type.getKey() === 'rttable')
|
||||
) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
@ -29,13 +29,10 @@ define(
|
||||
|
||||
describe("The create action provider", function () {
|
||||
var mockTypeService,
|
||||
mockDialogService,
|
||||
mockNavigationService,
|
||||
mockPolicyService,
|
||||
mockCreationPolicy,
|
||||
mockPolicyMap = {},
|
||||
mockTypes,
|
||||
mockQ,
|
||||
provider;
|
||||
|
||||
function createMockType(name) {
|
||||
@ -61,14 +58,6 @@ define(
|
||||
"typeService",
|
||||
["listTypes"]
|
||||
);
|
||||
mockDialogService = jasmine.createSpyObj(
|
||||
"dialogService",
|
||||
["getUserInput"]
|
||||
);
|
||||
mockNavigationService = jasmine.createSpyObj(
|
||||
"navigationService",
|
||||
["setNavigation"]
|
||||
);
|
||||
mockPolicyService = jasmine.createSpyObj(
|
||||
"policyService",
|
||||
["allow"]
|
||||
@ -91,9 +80,7 @@ define(
|
||||
mockTypeService.listTypes.andReturn(mockTypes);
|
||||
|
||||
provider = new CreateActionProvider(
|
||||
mockQ,
|
||||
mockTypeService,
|
||||
mockNavigationService,
|
||||
mockPolicyService
|
||||
);
|
||||
});
|
190
platform/commonUI/edit/test/creation/CreateActionSpec.js
Normal file
190
platform/commonUI/edit/test/creation/CreateActionSpec.js
Normal file
@ -0,0 +1,190 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* MCTRepresentationSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/creation/CreateAction"],
|
||||
function (CreateAction) {
|
||||
|
||||
describe("The create action", function () {
|
||||
var mockType,
|
||||
mockParent,
|
||||
mockContext,
|
||||
mockDomainObject,
|
||||
capabilities = {},
|
||||
mockEditAction,
|
||||
mockSaveAction,
|
||||
action;
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockType = jasmine.createSpyObj(
|
||||
"type",
|
||||
[
|
||||
"getKey",
|
||||
"getGlyph",
|
||||
"getName",
|
||||
"getDescription",
|
||||
"getProperties",
|
||||
"getInitialModel"
|
||||
]
|
||||
);
|
||||
mockParent = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[
|
||||
"getId",
|
||||
"getModel",
|
||||
"getCapability",
|
||||
"useCapability"
|
||||
]
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[
|
||||
"getId",
|
||||
"getModel",
|
||||
"getCapability",
|
||||
"hasCapability",
|
||||
"useCapability"
|
||||
]
|
||||
);
|
||||
mockDomainObject.hasCapability.andCallFake(function (name) {
|
||||
return !!capabilities[name];
|
||||
});
|
||||
mockDomainObject.getCapability.andCallFake(function (name) {
|
||||
return capabilities[name];
|
||||
});
|
||||
mockSaveAction = jasmine.createSpyObj(
|
||||
"saveAction",
|
||||
[
|
||||
"perform"
|
||||
]
|
||||
);
|
||||
|
||||
capabilities.action = jasmine.createSpyObj(
|
||||
"actionCapability",
|
||||
[
|
||||
"getActions",
|
||||
"perform"
|
||||
]
|
||||
);
|
||||
|
||||
capabilities.editor = jasmine.createSpyObj(
|
||||
"editorCapability",
|
||||
[
|
||||
"edit",
|
||||
"save",
|
||||
"cancel"
|
||||
]
|
||||
);
|
||||
|
||||
mockEditAction = jasmine.createSpyObj(
|
||||
"editAction",
|
||||
[
|
||||
"perform"
|
||||
]
|
||||
);
|
||||
|
||||
mockContext = {
|
||||
domainObject: mockParent
|
||||
};
|
||||
mockParent.useCapability.andReturn(mockDomainObject);
|
||||
|
||||
mockType.getKey.andReturn("test");
|
||||
mockType.getGlyph.andReturn("T");
|
||||
mockType.getDescription.andReturn("a test type");
|
||||
mockType.getName.andReturn("Test");
|
||||
mockType.getProperties.andReturn([]);
|
||||
mockType.getInitialModel.andReturn({});
|
||||
|
||||
action = new CreateAction(
|
||||
mockType,
|
||||
mockParent,
|
||||
mockContext
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes type-appropriate metadata", function () {
|
||||
var metadata = action.getMetadata();
|
||||
|
||||
expect(metadata.name).toEqual("Test");
|
||||
expect(metadata.description).toEqual("a test type");
|
||||
expect(metadata.glyph).toEqual("T");
|
||||
});
|
||||
|
||||
describe("the perform function", function () {
|
||||
beforeEach(function () {
|
||||
capabilities.action.getActions.andReturn([mockEditAction]);
|
||||
});
|
||||
|
||||
it("uses the instantiation capability when performed", function () {
|
||||
action.perform();
|
||||
expect(mockParent.useCapability).toHaveBeenCalledWith("instantiation", jasmine.any(Object));
|
||||
});
|
||||
|
||||
it("uses the edit action if available", function () {
|
||||
action.perform();
|
||||
expect(mockEditAction.perform).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the save action if object does not have an edit action" +
|
||||
" available", function () {
|
||||
capabilities.action.getActions.andReturn([]);
|
||||
capabilities.action.perform.andReturn(mockPromise(undefined));
|
||||
action.perform();
|
||||
expect(capabilities.action.perform).toHaveBeenCalledWith("save");
|
||||
});
|
||||
|
||||
describe("uses to editor capability", function () {
|
||||
var promise = jasmine.createSpyObj("promise", ["then"]);
|
||||
beforeEach(function () {
|
||||
capabilities.action.getActions.andReturn([]);
|
||||
capabilities.action.perform.andReturn(promise);
|
||||
});
|
||||
|
||||
it("to save the edit if user saves dialog", function () {
|
||||
action.perform();
|
||||
expect(promise.then).toHaveBeenCalled();
|
||||
promise.then.mostRecentCall.args[0]();
|
||||
expect(capabilities.editor.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("to cancel the edit if user cancels dialog", function () {
|
||||
action.perform();
|
||||
promise.then.mostRecentCall.args[1]();
|
||||
expect(capabilities.editor.cancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -48,6 +48,7 @@ define([
|
||||
"./src/directives/MCTSplitPane",
|
||||
"./src/directives/MCTSplitter",
|
||||
"./src/directives/MCTTree",
|
||||
"./src/filters/ReverseFilter.js",
|
||||
"text!./res/templates/bottombar.html",
|
||||
"text!./res/templates/controls/action-button.html",
|
||||
"text!./res/templates/controls/input-filter.html",
|
||||
@ -96,6 +97,7 @@ define([
|
||||
MCTSplitPane,
|
||||
MCTSplitter,
|
||||
MCTTree,
|
||||
ReverseFilter,
|
||||
bottombarTemplate,
|
||||
actionButtonTemplate,
|
||||
inputFilterTemplate,
|
||||
@ -146,7 +148,8 @@ define([
|
||||
"depends": [
|
||||
"stylesheets[]",
|
||||
"$document",
|
||||
"THEME"
|
||||
"THEME",
|
||||
"ASSETS_PATH"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -158,7 +161,7 @@ define([
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"implementation": "filters/ReverseFilter.js",
|
||||
"implementation": ReverseFilter,
|
||||
"key": "reverse"
|
||||
}
|
||||
],
|
||||
@ -404,6 +407,11 @@ define([
|
||||
"key": "THEME",
|
||||
"value": "unspecified",
|
||||
"priority": "fallback"
|
||||
},
|
||||
{
|
||||
"key": "ASSETS_PATH",
|
||||
"value": ".",
|
||||
"priority": "fallback"
|
||||
}
|
||||
],
|
||||
"containers": [
|
||||
|
@ -145,3 +145,8 @@
|
||||
.flex-justify-end {
|
||||
@include justify-content(flex-end);
|
||||
}
|
||||
|
||||
/********************************************* POPUPS */
|
||||
.t-popup {
|
||||
z-index: 75;
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ $uePaneMiniTabW: 10px;
|
||||
$uePaneMiniTabCollapsedW: 11px;
|
||||
$ueEditLeftPaneW: 75%;
|
||||
$treeSearchInputBarH: 25px;
|
||||
$ueTimeControlH: (33px, 20px, 20px);
|
||||
$ueTimeControlH: (33px, 18px, 20px);
|
||||
// Panes
|
||||
$ueBrowseLeftPaneTreeMinW: 150px;
|
||||
$ueBrowseLeftPaneTreeMaxW: 35%;
|
||||
|
@ -63,9 +63,10 @@ input, textarea {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
input[type="text"],
|
||||
input[type="search"] {
|
||||
vertical-align: baseline;
|
||||
padding: 3px 5px !important;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
|
@ -139,6 +139,17 @@
|
||||
background-size: $d $d;
|
||||
}
|
||||
|
||||
@mixin bgStripes($c: yellow, $a: 0.1, $bgsize: 5px, $angle: 90deg) {
|
||||
@include background-image(linear-gradient($angle,
|
||||
rgba($c, $a) 25%, transparent 25%,
|
||||
transparent 50%, rgba($c, $a) 50%,
|
||||
rgba($c, $a) 75%, transparent 75%,
|
||||
transparent 100%
|
||||
));
|
||||
background-repeat: repeat;
|
||||
background-size: $bgsize $bgsize;
|
||||
}
|
||||
|
||||
@mixin bgVertStripes($c: yellow, $a: 0.1, $d: 40px) {
|
||||
@include background-image(linear-gradient(-90deg,
|
||||
rgba($c, $a) 0%, rgba($c, $a) 50%,
|
||||
@ -322,13 +333,13 @@
|
||||
color: $fg;
|
||||
outline: none;
|
||||
&.error {
|
||||
background: rgba(red, 0.5);
|
||||
background-color: $colorFormFieldErrorBg;
|
||||
color: $colorFormFieldErrorFg;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin nice-input($bg: $colorInputBg, $fg: $colorInputFg) {
|
||||
@include input-base($bg, $fg);
|
||||
padding: 0 $interiorMarginSm;
|
||||
}
|
||||
|
||||
@mixin contextArrow() {
|
||||
|
@ -29,7 +29,7 @@
|
||||
.accordion-head {
|
||||
$op: 0.2;
|
||||
border-radius: $basicCr * 0.75;
|
||||
box-sizing: "border-box";
|
||||
box-sizing: border-box;
|
||||
background: rgba($colorBodyFg, $op);
|
||||
cursor: pointer;
|
||||
font-size: 0.75em;
|
||||
@ -396,11 +396,11 @@ input[type="search"] {
|
||||
left: auto;
|
||||
}
|
||||
.knob-l {
|
||||
@include border-left-radius($sliderKnobW);
|
||||
@include border-left-radius($sliderKnobR);
|
||||
cursor: w-resize;
|
||||
}
|
||||
.knob-r {
|
||||
@include border-right-radius($sliderKnobW);
|
||||
@include border-right-radius($sliderKnobR);
|
||||
cursor: e-resize;
|
||||
}
|
||||
.range {
|
||||
@ -426,7 +426,6 @@ input[type="search"] {
|
||||
@include user-select(none);
|
||||
font-size: 0.8rem;
|
||||
padding: $interiorMarginLg !important;
|
||||
width: 230px;
|
||||
.l-month-year-pager {
|
||||
$pagerW: 20px;
|
||||
height: $r1H;
|
||||
@ -518,6 +517,19 @@ input[type="search"] {
|
||||
}
|
||||
}
|
||||
|
||||
@include phone {
|
||||
.l-datetime-picker {
|
||||
padding: $interiorMargin !important;
|
||||
}
|
||||
.l-calendar {
|
||||
ul.l-cal-row {
|
||||
li {
|
||||
padding: 2px $interiorMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************** TEXTAREA */
|
||||
textarea {
|
||||
@include nice-textarea($colorInputBg, $colorInputFg);
|
||||
|
@ -10,25 +10,24 @@
|
||||
$knobHOffset: 0px;
|
||||
$knobM: ($sliderKnobW + $knobHOffset) * -1;
|
||||
$rangeValPad: $interiorMargin;
|
||||
$rangeValOffset: $sliderKnobW;
|
||||
$timeRangeSliderLROffset: 130px + $sliderKnobW + $rangeValOffset;
|
||||
$r1H: nth($ueTimeControlH,1);
|
||||
$rangeValOffset: $sliderKnobW + $interiorMargin;
|
||||
$timeRangeSliderLROffset: 150px + ($sliderKnobW * 2);
|
||||
$r1H: nth($ueTimeControlH,1); // Not currently used
|
||||
$r2H: nth($ueTimeControlH,2);
|
||||
$r3H: nth($ueTimeControlH,3);
|
||||
|
||||
display: block;
|
||||
height: $r1H + $r2H + $r3H + ($interiorMargin * 2);
|
||||
min-width: $minW;
|
||||
font-size: 0.8rem;
|
||||
|
||||
|
||||
.l-time-range-inputs-holder,
|
||||
.l-time-range-slider-holder,
|
||||
.l-time-range-ticks-holder
|
||||
{
|
||||
@include absPosDefault(0, visible);
|
||||
box-sizing: border-box;
|
||||
top: auto;
|
||||
position: relative;
|
||||
&:not(:first-child) {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
.l-time-range-slider,
|
||||
.l-time-range-ticks {
|
||||
@ -37,14 +36,21 @@
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
height: $r1H; bottom: $r2H + $r3H + ($interiorMarginSm * 2);
|
||||
padding-top: $interiorMargin;
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
padding-top: $interiorMargin;
|
||||
&.l-flex-row,
|
||||
.l-flex-row {
|
||||
@include align-items(center);
|
||||
.flex-elem {
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
.type-icon {
|
||||
font-size: 120%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.l-time-range-input,
|
||||
.l-time-range-input-w,
|
||||
.l-time-range-inputs-elem {
|
||||
margin-right: $interiorMargin;
|
||||
.lbl {
|
||||
@ -52,13 +58,27 @@
|
||||
}
|
||||
.ui-symbol.icon {
|
||||
font-size: 11px;
|
||||
width: 11px;
|
||||
}
|
||||
}
|
||||
.l-time-range-input-w {
|
||||
// Wraps a datetime text input field
|
||||
position: relative;
|
||||
input[type="text"] {
|
||||
width: 200px;
|
||||
&.picker-icon {
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
.icon-calendar {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.l-time-range-slider-holder {
|
||||
height: $r2H; bottom: $r3H + ($interiorMarginSm * 1);
|
||||
height: $r2H;
|
||||
.range-holder {
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
@ -73,24 +93,13 @@
|
||||
width: $myW;
|
||||
height: auto;
|
||||
z-index: 2;
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $myC;
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
&:before {
|
||||
// Vert line
|
||||
background-color: $myC;
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1;
|
||||
width: 2px;
|
||||
}
|
||||
&:after {
|
||||
// Circle element
|
||||
border-radius: $myW;
|
||||
@include transform(translateY(-50%));
|
||||
top: 50%; right: 0; bottom: auto; left: 0;
|
||||
width: auto;
|
||||
height: $myW;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
&:hover .toi-line {
|
||||
@ -126,9 +135,9 @@
|
||||
@include webkitProp(transform, translateX(-50%));
|
||||
color: $colorPlotLabelFg;
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
font-size: 0.7rem;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: 5px;
|
||||
white-space: nowrap;
|
||||
z-index: 2;
|
||||
}
|
||||
@ -138,16 +147,29 @@
|
||||
|
||||
.knob {
|
||||
z-index: 2;
|
||||
&:before {
|
||||
$mTB: 2px;
|
||||
$grippyW: 3px;
|
||||
$mLR: ($sliderKnobW - $grippyW)/2;
|
||||
@include bgStripes($c: pullForward($sliderColorKnob, 20%), $a: 1, $bgsize: 4px, $angle: 0deg);
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: $mTB; right: $mLR; bottom: $mTB; left: $mLR;
|
||||
}
|
||||
.range-value {
|
||||
@include trans-prop-nice-fade(.25s);
|
||||
padding: 0 $rangeValOffset;
|
||||
font-size: 0.7rem;
|
||||
position: absolute;
|
||||
height: $r2H;
|
||||
line-height: $r2H;
|
||||
white-space: nowrap;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
&:hover .range-value {
|
||||
color: $sliderColorKnobHov;
|
||||
&:hover {
|
||||
.range-value {
|
||||
color: $sliderColorKnobHov;
|
||||
}
|
||||
}
|
||||
&.knob-l {
|
||||
margin-left: $knobM;
|
||||
@ -170,7 +192,7 @@
|
||||
.l-time-domain-selector {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 46px;
|
||||
top: $interiorMargin;
|
||||
}
|
||||
|
||||
}
|
||||
@ -181,174 +203,64 @@
|
||||
padding: 1px 1px 0 $interiorMargin;
|
||||
}
|
||||
|
||||
/******************************************************************** MOBILE */
|
||||
|
||||
@include phoneandtablet {
|
||||
.l-time-controller, .l-time-range-inputs-holder {
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
.l-time-controller {
|
||||
|
||||
.l-time-domain-selector {
|
||||
select {
|
||||
height: 25px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.l-time-range-slider-holder, .l-time-range-ticks-holder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.time-range-start, .time-range-end, {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-input {
|
||||
display: block;
|
||||
.s-btn {
|
||||
padding-right: 18px;
|
||||
white-space: nowrap;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.l-time-range-inputs-elem {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.l-time-controller {
|
||||
min-width: 0;
|
||||
.l-time-range-slider-holder,
|
||||
.l-time-range-ticks-holder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include phone {
|
||||
.l-time-controller {
|
||||
height: 48px;
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
bottom: 24px;
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
width: 33%;
|
||||
bottom: -9px;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-input {
|
||||
margin-bottom: 5px;
|
||||
.s-btn {
|
||||
width: 66%;
|
||||
}
|
||||
}
|
||||
.l-time-range-inputs-elem {
|
||||
&.ui-symbol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.lbl {
|
||||
width: 33%;
|
||||
right: 0px;
|
||||
top: 5px;
|
||||
display: block;
|
||||
height: 25px;
|
||||
margin: 0;
|
||||
line-height: 25px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.l-time-controller {
|
||||
.l-time-range-inputs-holder {
|
||||
&.l-flex-row,
|
||||
.l-flex-row {
|
||||
@include align-items(flex-start);
|
||||
}
|
||||
.l-time-range-inputs-elem {
|
||||
&.type-icon {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
.t-inputs-w {
|
||||
@include flex-direction(column);
|
||||
.l-time-range-input-w:not(:first-child) {
|
||||
&:not(:first-child) {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
margin-right: 0;
|
||||
}
|
||||
.l-time-range-inputs-elem {
|
||||
&.lbl { display: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@include tablet {
|
||||
.l-time-controller {
|
||||
height: 17px;
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
bottom: -7px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
width: 23%;
|
||||
right: -4px;
|
||||
bottom: -10px;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-input {
|
||||
float: left;
|
||||
.s-btn {
|
||||
width: 100%;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include tabletLandscape {
|
||||
.l-time-controller {
|
||||
height: 17px;
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
bottom: -7px;
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
width: 23%;
|
||||
right: auto;
|
||||
bottom: -10px;
|
||||
left: 391px;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-inputs-elem {
|
||||
&.ui-symbol, &.lbl {
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pane-tree-hidden .l-time-controller {
|
||||
.l-time-domain-selector {
|
||||
left: 667px;
|
||||
}
|
||||
.l-time-range-inputs-holder {
|
||||
padding-left: 277px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include tabletPortrait {
|
||||
.l-time-controller {
|
||||
height: 17px;
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
bottom: -7px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
width: 23%;
|
||||
right: -4px;
|
||||
bottom: -10px;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-input {
|
||||
width: 38%;
|
||||
float: left;
|
||||
}
|
||||
.l-time-range-inputs-elem {
|
||||
&.ui-symbol, &.lbl {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@include phonePortrait {
|
||||
.l-time-controller {
|
||||
.l-time-range-inputs-holder {
|
||||
.t-inputs-w {
|
||||
@include flex(1 1 auto);
|
||||
padding-top: 25px; // Make room for the ever lovin' Time Domain Selector
|
||||
.flex-elem {
|
||||
@include flex(1 1 auto);
|
||||
width: 100%;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.l-time-domain-selector {
|
||||
right: auto;
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
|
||||
.holder.holder-treeview-elements {
|
||||
top: $bodyMargin;
|
||||
right: 0;
|
||||
bottom: $bodyMargin;
|
||||
bottom: $interiorMargin;
|
||||
left: $bodyMargin;
|
||||
.create-btn-holder {
|
||||
&.s-status-editing {
|
||||
@ -215,17 +215,17 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
|
||||
left: 0;
|
||||
.holder-object {
|
||||
top: $bodyMargin;
|
||||
bottom: $bodyMargin;
|
||||
bottom: $interiorMargin;
|
||||
}
|
||||
.holder-inspector {
|
||||
top: $bodyMargin;
|
||||
bottom: $bodyMargin;
|
||||
bottom: $interiorMargin;
|
||||
left: $bodyMargin;
|
||||
right: $bodyMargin;
|
||||
}
|
||||
.holder-elements {
|
||||
top: 0;
|
||||
bottom: $bodyMargin;
|
||||
bottom: $interiorMargin;
|
||||
left: $bodyMargin;
|
||||
right: $bodyMargin;
|
||||
}
|
||||
|
@ -19,18 +19,17 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<span class="s-btn"
|
||||
ng-controller="DateTimeFieldController">
|
||||
<span ng-controller="DateTimeFieldController">
|
||||
<input type="text"
|
||||
ng-model="textValue"
|
||||
ng-blur="restoreTextValue(); ngBlur()"
|
||||
ng-class="{
|
||||
error: textInvalid ||
|
||||
(structure.validate &&
|
||||
!structure.validate(ngModel[field]))
|
||||
!structure.validate(ngModel[field])),
|
||||
'picker-icon': structure.format === 'utc' || !structure.format
|
||||
}">
|
||||
</input>
|
||||
<a class="ui-symbol icon icon-calendar"
|
||||
</input><a class="ui-symbol icon icon-calendar"
|
||||
ng-if="structure.format === 'utc' || !structure.format"
|
||||
ng-click="picker.active = !picker.active">
|
||||
</a>
|
||||
@ -38,8 +37,7 @@
|
||||
<div mct-click-elsewhere="picker.active = false">
|
||||
<mct-control key="'datetime-picker'"
|
||||
ng-model="pickerModel"
|
||||
field="'value'"
|
||||
options="{ hours: true }">
|
||||
field="'value'">
|
||||
</mct-control>
|
||||
</div>
|
||||
</mct-popup>
|
||||
|
@ -19,42 +19,43 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div ng-controller="TimeRangeController as trCtrl">
|
||||
<form class="l-time-range-inputs-holder"
|
||||
<div ng-controller="TimeRangeController as trCtrl" class="l-flex-col">
|
||||
<form class="l-time-range-inputs-holder l-flex-row flex-elem"
|
||||
ng-submit="trCtrl.updateBoundsFromForm()">
|
||||
<span class="l-time-range-inputs-elem ui-symbol type-icon">C</span>
|
||||
<span class="l-time-range-input">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{
|
||||
format: parameters.format,
|
||||
validate: trCtrl.validateStart
|
||||
}"
|
||||
ng-model="formModel"
|
||||
ng-blur="trCtrl.updateBoundsFromForm()"
|
||||
field="'start'"
|
||||
class="time-range-start">
|
||||
</mct-control>
|
||||
<span class="l-time-range-inputs-elem ui-symbol type-icon flex-elem">C</span>
|
||||
<span class="l-time-range-inputs-elem t-inputs-w l-flex-row flex-elem">
|
||||
<span class="l-time-range-input-w flex-elem">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{
|
||||
format: parameters.format,
|
||||
validate: trCtrl.validateStart
|
||||
}"
|
||||
ng-model="formModel"
|
||||
ng-blur="trCtrl.updateBoundsFromForm()"
|
||||
field="'start'"
|
||||
class="time-range-start">
|
||||
</mct-control>
|
||||
</span>
|
||||
|
||||
<span class="l-time-range-inputs-elem lbl flex-elem">to</span>
|
||||
|
||||
<span class="l-time-range-input-w flex-elem" ng-controller="ToggleController as t2">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{
|
||||
format: parameters.format,
|
||||
validate: trCtrl.validateEnd
|
||||
}"
|
||||
ng-model="formModel"
|
||||
ng-blur="trCtrl.updateBoundsFromForm()"
|
||||
field="'end'"
|
||||
class="time-range-end">
|
||||
</mct-control>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="l-time-range-inputs-elem lbl">to</span>
|
||||
|
||||
<span class="l-time-range-input" ng-controller="ToggleController as t2">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{
|
||||
format: parameters.format,
|
||||
validate: trCtrl.validateEnd
|
||||
}"
|
||||
ng-model="formModel"
|
||||
ng-blur="trCtrl.updateBoundsFromForm()"
|
||||
field="'end'"
|
||||
class="time-range-end">
|
||||
</mct-control>
|
||||
</span>
|
||||
|
||||
<input type="submit" class="hidden">
|
||||
</form>
|
||||
|
||||
<div class="l-time-range-slider-holder">
|
||||
<div class="l-time-range-slider-holder flex-elem">
|
||||
<div class="l-time-range-slider">
|
||||
<div class="slider"
|
||||
mct-resize="spanWidth = bounds.width">
|
||||
@ -85,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="l-time-range-ticks-holder">
|
||||
<div class="l-time-range-ticks-holder flex-elem">
|
||||
<div class="l-time-range-ticks">
|
||||
<div
|
||||
ng-repeat="tick in ticks track by $index"
|
||||
|
@ -38,7 +38,7 @@ define(
|
||||
* @param $document Angular's jqLite-wrapped document element
|
||||
* @param {string} activeTheme the theme in use
|
||||
*/
|
||||
function StyleSheetLoader(stylesheets, $document, activeTheme) {
|
||||
function StyleSheetLoader(stylesheets, $document, activeTheme, assetPath) {
|
||||
var head = $document.find('head'),
|
||||
document = $document[0];
|
||||
|
||||
@ -47,6 +47,7 @@ define(
|
||||
// Create a link element, and construct full path
|
||||
var link = document.createElement('link'),
|
||||
path = [
|
||||
assetPath,
|
||||
stylesheet.bundle.path,
|
||||
stylesheet.bundle.resources,
|
||||
stylesheet.stylesheetUrl
|
||||
|
@ -49,10 +49,7 @@ define(
|
||||
position = [rect.left, rect.top],
|
||||
popup = popupService.display(div, position);
|
||||
|
||||
// TODO: Handle in CSS;
|
||||
// https://github.com/nasa/openmctweb/issues/298
|
||||
div.css('z-index', 75);
|
||||
|
||||
div.addClass('t-popup');
|
||||
transclude(function (clone) {
|
||||
div.append(clone);
|
||||
});
|
||||
|
@ -24,7 +24,15 @@ define(
|
||||
["../../src/directives/MCTPopup"],
|
||||
function (MCTPopup) {
|
||||
|
||||
var JQLITE_METHODS = ["on", "off", "find", "parent", "css", "append"];
|
||||
var JQLITE_METHODS = [
|
||||
"on",
|
||||
"off",
|
||||
"find",
|
||||
"parent",
|
||||
"css",
|
||||
"addClass",
|
||||
"append"
|
||||
];
|
||||
|
||||
describe("The mct-popup directive", function () {
|
||||
var mockCompile,
|
||||
|
@ -38,7 +38,6 @@ define(
|
||||
function InfoGestureButton($document, agentService, infoService, element, domainObject) {
|
||||
var dismissBubble,
|
||||
touchPosition,
|
||||
scopeOff,
|
||||
body = $document.find('body');
|
||||
|
||||
function trackPosition(event) {
|
||||
@ -94,10 +93,6 @@ define(
|
||||
element.on('click', showBubble);
|
||||
}
|
||||
|
||||
// Also make sure we dismiss bubble if representation is destroyed
|
||||
// before the mouse actually leaves it
|
||||
scopeOff = element.scope().$on('$destroy', hideBubble);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Detach any event handlers associated with this gesture.
|
||||
@ -109,7 +104,6 @@ define(
|
||||
hideBubble();
|
||||
// ...and detach listeners
|
||||
element.off('click', showBubble);
|
||||
scopeOff();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -137,6 +137,11 @@ define(
|
||||
);
|
||||
});
|
||||
|
||||
// https://github.com/nasa/openmct/issues/948
|
||||
it("does not try to access scope", function () {
|
||||
expect(mockElement.scope).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -32,11 +32,12 @@ $sliderColorBase: $colorKey;
|
||||
$sliderColorRangeHolder: rgba(black, 0.1);
|
||||
$sliderColorRange: rgba($sliderColorBase, 0.3);
|
||||
$sliderColorRangeHov: rgba($sliderColorBase, 0.5);
|
||||
$sliderColorKnob: rgba($sliderColorBase, 0.6);
|
||||
$sliderColorKnobHov: $sliderColorBase;
|
||||
$sliderColorKnob: $sliderColorBase;
|
||||
$sliderColorKnobHov: pullForward($sliderColorKnob, $ltGamma);
|
||||
$sliderColorRangeValHovBg: rgba($sliderColorBase, 0.1);
|
||||
$sliderColorRangeValHovFg: $colorKeyFg;
|
||||
$sliderKnobW: nth($ueTimeControlH,2)/2;
|
||||
$sliderKnobW: 15px;
|
||||
$sliderKnobR: 2px;
|
||||
$timeControllerToiLineColor: #00c2ff;
|
||||
$timeControllerToiLineColorHov: #fff;
|
||||
|
||||
@ -69,8 +70,10 @@ $colorCreateMenuText: $colorMenuFg;
|
||||
$colorCheck: $colorKey;
|
||||
$colorFormRequired: $colorAlt1;
|
||||
$colorFormValid: #33cc33;
|
||||
$colorFormError: #cc0000;
|
||||
$colorFormError: #990000;
|
||||
$colorFormInvalid: #ff3300;
|
||||
$colorFormFieldErrorBg: $colorFormError;
|
||||
$colorFormFieldErrorFg: rgba(#fff, 0.6);
|
||||
$colorFormLines: rgba(#fff, 0.1);
|
||||
$colorFormSectionHeader: rgba(#fff, 0.1);
|
||||
$colorInputBg: rgba(#000, 0.1);
|
||||
|
@ -32,11 +32,12 @@ $sliderColorBase: $colorKey;
|
||||
$sliderColorRangeHolder: rgba(black, 0.07);
|
||||
$sliderColorRange: rgba($sliderColorBase, 0.2);
|
||||
$sliderColorRangeHov: rgba($sliderColorBase, 0.4);
|
||||
$sliderColorKnob: rgba($sliderColorBase, 0.5);
|
||||
$sliderColorKnob: pushBack($sliderColorBase, 20%);
|
||||
$sliderColorKnobHov: rgba($sliderColorBase, 0.7);
|
||||
$sliderColorRangeValHovBg: $sliderColorRange; //rgba($sliderColorBase, 0.1);
|
||||
$sliderColorRangeValHovBg: $sliderColorRange;
|
||||
$sliderColorRangeValHovFg: $colorBodyFg;
|
||||
$sliderKnobW: nth($ueTimeControlH,2)/2;
|
||||
$sliderKnobW: 15px;
|
||||
$sliderKnobR: 2px;
|
||||
$timeControllerToiLineColor: $colorBodyFg;
|
||||
$timeControllerToiLineColorHov: #0052b5;
|
||||
|
||||
@ -69,8 +70,10 @@ $colorCreateMenuText: $colorBodyFg;
|
||||
$colorCheck: $colorKey;
|
||||
$colorFormRequired: $colorKey;
|
||||
$colorFormValid: #33cc33;
|
||||
$colorFormError: #cc0000;
|
||||
$colorFormError: #990000;
|
||||
$colorFormInvalid: #ff2200;
|
||||
$colorFormFieldErrorBg: $colorFormError;
|
||||
$colorFormFieldErrorFg: rgba(#fff, 0.6);
|
||||
$colorFormLines: rgba(#000, 0.1);
|
||||
$colorFormSectionHeader: rgba(#000, 0.05);
|
||||
$colorInputBg: $colorGenBg;
|
||||
|
@ -42,11 +42,19 @@ define(
|
||||
function addWorker(worker) {
|
||||
var key = worker.key;
|
||||
if (!workerUrls[key]) {
|
||||
workerUrls[key] = [
|
||||
worker.bundle.path,
|
||||
worker.bundle.sources,
|
||||
worker.scriptUrl
|
||||
].join("/");
|
||||
if (worker.scriptUrl) {
|
||||
workerUrls[key] = [
|
||||
worker.bundle.path,
|
||||
worker.bundle.sources,
|
||||
worker.scriptUrl
|
||||
].join("/");
|
||||
} else if (worker.scriptText) {
|
||||
var blob = new Blob(
|
||||
[worker.scriptText],
|
||||
{type: 'application/javascript'}
|
||||
);
|
||||
workerUrls[key] = URL.createObjectURL(blob);
|
||||
}
|
||||
sharedWorkers[key] = worker.shared;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ define([
|
||||
"./src/controllers/TableOptionsController",
|
||||
'../../commonUI/regions/src/Region',
|
||||
'../../commonUI/browse/src/InspectorRegion',
|
||||
"text!./res/templates/table-options-edit.html",
|
||||
"text!./res/templates/rt-table.html",
|
||||
"text!./res/templates/historical-table.html",
|
||||
"legacyRegistry"
|
||||
], function (
|
||||
MCTTable,
|
||||
@ -35,6 +38,9 @@ define([
|
||||
TableOptionsController,
|
||||
Region,
|
||||
InspectorRegion,
|
||||
tableOptionsEditTemplate,
|
||||
rtTableTemplate,
|
||||
historicalTableTemplate,
|
||||
legacyRegistry
|
||||
) {
|
||||
/**
|
||||
@ -128,23 +134,23 @@ define([
|
||||
"name": "Historical Table",
|
||||
"key": "table",
|
||||
"glyph": "\ue604",
|
||||
"templateUrl": "templates/historical-table.html",
|
||||
"template": historicalTableTemplate,
|
||||
"needs": [
|
||||
"telemetry"
|
||||
],
|
||||
"delegation": true,
|
||||
"editable": true
|
||||
"editable": false
|
||||
},
|
||||
{
|
||||
"name": "Real-time Table",
|
||||
"key": "rt-table",
|
||||
"glyph": "\ue620",
|
||||
"templateUrl": "templates/rt-table.html",
|
||||
"template": rtTableTemplate,
|
||||
"needs": [
|
||||
"telemetry"
|
||||
],
|
||||
"delegation": true,
|
||||
"editable": true
|
||||
"editable": false
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
@ -157,7 +163,7 @@ define([
|
||||
"representations": [
|
||||
{
|
||||
"key": "table-options-edit",
|
||||
"templateUrl": "templates/table-options-edit.html"
|
||||
"template": tableOptionsEditTemplate
|
||||
}
|
||||
],
|
||||
"stylesheets": [
|
||||
|
@ -127,7 +127,16 @@ define([
|
||||
14400000,
|
||||
28800000,
|
||||
43200000,
|
||||
86400000
|
||||
86400000,
|
||||
86400000 * 2,
|
||||
86400000 * 5,
|
||||
86400000 * 10,
|
||||
86400000 * 20,
|
||||
86400000 * 30,
|
||||
86400000 * 60,
|
||||
86400000 * 120,
|
||||
86400000 * 240,
|
||||
86400000 * 365
|
||||
],
|
||||
"width": 200
|
||||
}
|
||||
|
@ -1,16 +1,22 @@
|
||||
.l-timeline-gantt {
|
||||
min-width: 2px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: $timelineSwimlaneGanttVM; bottom: $timelineSwimlaneGanttVM;
|
||||
|
||||
.bar {
|
||||
@include ellipsize();
|
||||
height: $activityBarH;
|
||||
line-height: $activityBarH + 2;
|
||||
line-height: $activityBarH;
|
||||
padding: 0 $interiorMargin;
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
$iconW: 20px;
|
||||
@include absPosDefault();
|
||||
display: block;
|
||||
&.s-activity-type {
|
||||
right: auto; width: $iconW;
|
||||
text-align: center;
|
||||
&.timeline {
|
||||
&:before {
|
||||
content:"S";
|
||||
@ -23,7 +29,9 @@
|
||||
}
|
||||
}
|
||||
&.s-title {
|
||||
text-shadow: rgba(black, 0.1) 0 1px 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
left: $iconW;
|
||||
}
|
||||
&.duration {
|
||||
left: auto;
|
||||
@ -52,6 +60,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
&.sm .bar span {
|
||||
// Hide icon and label if width is too small
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-mode .s-timeline-gantt,
|
||||
@ -59,7 +71,7 @@
|
||||
.handle {
|
||||
cursor: col-resize;
|
||||
&.mid {
|
||||
cursor: move;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
}
|
||||
}
|
@ -32,20 +32,10 @@
|
||||
}
|
||||
|
||||
.s-timeline-gantt {
|
||||
$br: $controlCr;
|
||||
.bar {
|
||||
color: $colorGanttBarFg;
|
||||
@include activityBg($colorGanttBarBg);
|
||||
border-radius: $br;
|
||||
box-shadow: $shdwGanttBar;
|
||||
&.expanded {
|
||||
@include border-top-radius($br);
|
||||
@include border-bottom-radius(0);
|
||||
}
|
||||
&.leaf {
|
||||
@include border-top-radius(0);
|
||||
@include border-bottom-radius($br);
|
||||
}
|
||||
.s-toggle {
|
||||
color: $colorGanttToggle;
|
||||
}
|
||||
|
@ -52,6 +52,9 @@
|
||||
// Tree area with item title
|
||||
right: auto; // Set this to auto and uncomment width below when additional tabular columns are added
|
||||
width: $timelineTabularTitleW;
|
||||
.l-swimlanes-holder {
|
||||
bottom: $scrollbarTrackSize;
|
||||
}
|
||||
}
|
||||
&.l-tabular-r {
|
||||
// Start, end, duration, activity modes columns
|
||||
@ -67,6 +70,7 @@
|
||||
&.l-timeline-gantt {
|
||||
.l-swimlanes-holder {
|
||||
@include scrollV(scroll);
|
||||
bottom: $scrollbarTrackSize;
|
||||
}
|
||||
}
|
||||
&.l-timeline-resource-legend {
|
||||
|
@ -20,6 +20,7 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="t-timeline-gantt l-timeline-gantt s-timeline-gantt"
|
||||
ng-class="{ sm: gantt.width(timespan, parameters.scroll, parameters.toPixels) < 25 }"
|
||||
title="{{model.name}}"
|
||||
ng-controller="TimelineGanttController as gantt"
|
||||
ng-style="timespan ? {
|
||||
|
@ -103,6 +103,13 @@
|
||||
<!-- TOP PANE GANTT BARS -->
|
||||
<div class="split-pane-component l-timeline-pane t-pane-h l-pane-top t-timeline-gantt l-timeline-gantt s-timeline-gantt">
|
||||
<div class="l-hover-btns-holder s-hover-btns-holder t-btns-zoom">
|
||||
<a class="t-btn l-btn s-btn"
|
||||
ng-click="zoomController.fit()"
|
||||
ng-show="true"
|
||||
title="Zoom to fit">
|
||||
<span class="ui-symbol icon zoom-in">I</span>
|
||||
</a>
|
||||
|
||||
<a class="t-btn l-btn s-btn"
|
||||
ng-click="zoomController.zoom(-1)"
|
||||
ng-show="true"
|
||||
@ -121,7 +128,7 @@
|
||||
<div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x">
|
||||
<mct-include key="'timeline-ticks'"
|
||||
parameters="{
|
||||
fullWidth: zoomController.toPixels(zoomController.duration()),
|
||||
fullWidth: timelineController.width(zoomController),
|
||||
start: scroll.x,
|
||||
width: scroll.width,
|
||||
step: zoomController.toPixels(zoomController.zoom()),
|
||||
|
@ -97,6 +97,8 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch("configuration", swimlanePopulator.configure);
|
||||
|
||||
// Recalculate swimlane state on changes
|
||||
$scope.$watch("domainObject", swimlanePopulator.populate);
|
||||
|
||||
|
@ -32,16 +32,26 @@ define(
|
||||
var zoomLevels = ZOOM_CONFIGURATION.levels || [1000],
|
||||
zoomIndex = Math.floor(zoomLevels.length / 2),
|
||||
tickWidth = ZOOM_CONFIGURATION.width || 200,
|
||||
bounds = { x: 0, width: tickWidth },
|
||||
duration = 86400000; // Default duration in view
|
||||
|
||||
// Round a duration to a larger value, to ensure space for editing
|
||||
function roundDuration(value) {
|
||||
// Ensure there's always an extra day or so
|
||||
var sz = zoomLevels[zoomLevels.length - 1];
|
||||
var tickCount = bounds.width / tickWidth,
|
||||
sz = zoomLevels[zoomLevels.length - 1] * tickCount;
|
||||
value *= 1.25; // Add 25% padding to start
|
||||
return Math.ceil(value / sz) * sz;
|
||||
}
|
||||
|
||||
function toMillis(pixels) {
|
||||
return (pixels / tickWidth) * zoomLevels[zoomIndex];
|
||||
}
|
||||
|
||||
function toPixels(millis) {
|
||||
return tickWidth * millis / zoomLevels[zoomIndex];
|
||||
}
|
||||
|
||||
// Get/set zoom level
|
||||
function setZoomLevel(level) {
|
||||
if (!isNaN(level)) {
|
||||
@ -53,20 +63,27 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
// Persist current zoom level
|
||||
function storeZoom() {
|
||||
var isEditMode = $scope.commit &&
|
||||
$scope.domainObject &&
|
||||
$scope.domainObject.hasCapability('editor') &&
|
||||
$scope.domainObject.getCapability('editor').inEditContext();
|
||||
if (isEditMode) {
|
||||
$scope.configuration = $scope.configuration || {};
|
||||
$scope.configuration.zoomLevel = zoomIndex;
|
||||
$scope.commit();
|
||||
function initializeZoomFromTimespan(timespan) {
|
||||
var timelineDuration = timespan.getDuration();
|
||||
zoomIndex = 0;
|
||||
while (toMillis(bounds.width) < timelineDuration &&
|
||||
zoomIndex < zoomLevels.length - 1) {
|
||||
zoomIndex += 1;
|
||||
}
|
||||
bounds.x = toPixels(timespan.getStart());
|
||||
}
|
||||
|
||||
function initializeZoom() {
|
||||
if ($scope.domainObject) {
|
||||
$scope.domainObject.useCapability('timespan')
|
||||
.then(initializeZoomFromTimespan);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch("configuration.zoomLevel", setZoomLevel);
|
||||
$scope.$watch("scroll", function (scroll) {
|
||||
bounds = scroll;
|
||||
});
|
||||
$scope.$watch("domainObject", initializeZoom);
|
||||
|
||||
return {
|
||||
/**
|
||||
@ -83,27 +100,29 @@ define(
|
||||
zoom: function (amount) {
|
||||
// Update the zoom level if called with an argument
|
||||
if (arguments.length > 0 && !isNaN(amount)) {
|
||||
var center = this.toMillis(bounds.x + bounds.width / 2);
|
||||
setZoomLevel(zoomIndex + amount);
|
||||
storeZoom(zoomIndex);
|
||||
bounds.x = this.toPixels(center) - bounds.width / 2;
|
||||
}
|
||||
return zoomLevels[zoomIndex];
|
||||
},
|
||||
/**
|
||||
* Set the zoom level to fit the bounds of the timeline
|
||||
* being viewed.
|
||||
*/
|
||||
fit: initializeZoom,
|
||||
/**
|
||||
* Get the width, in pixels, of a specific time duration at
|
||||
* the current zoom level.
|
||||
* @returns {number} the number of pixels
|
||||
*/
|
||||
toPixels: function (millis) {
|
||||
return tickWidth * millis / zoomLevels[zoomIndex];
|
||||
},
|
||||
toPixels: toPixels,
|
||||
/**
|
||||
* Get the time duration, in milliseconds, occupied by the
|
||||
* width (specified in pixels) at the current zoom level.
|
||||
* @returns {number} the number of pixels
|
||||
*/
|
||||
toMillis: function (pixels) {
|
||||
return (pixels / tickWidth) * zoomLevels[zoomIndex];
|
||||
},
|
||||
toMillis: toMillis,
|
||||
/**
|
||||
* Get or set the current displayed duration. If used as a
|
||||
* setter, this will typically be rounded up to ensure extra
|
||||
|
@ -43,8 +43,7 @@ define(
|
||||
var swimlanes = [],
|
||||
start = Number.POSITIVE_INFINITY,
|
||||
end = Number.NEGATIVE_INFINITY,
|
||||
colors = (configuration.colors || {}),
|
||||
assigner = new TimelineColorAssigner(colors),
|
||||
assigner,
|
||||
lastDomainObject;
|
||||
|
||||
// Track extremes of start/end times
|
||||
@ -152,8 +151,15 @@ define(
|
||||
recalculateSwimlanes(lastDomainObject);
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
var colors = (configuration.colors || {});
|
||||
assigner = new TimelineColorAssigner(colors);
|
||||
configuration.colors = colors;
|
||||
recalculateSwimlanes(lastDomainObject);
|
||||
}
|
||||
|
||||
// Ensure colors are exposed in configuration
|
||||
configuration.colors = colors;
|
||||
initialize();
|
||||
|
||||
return {
|
||||
/**
|
||||
@ -188,6 +194,15 @@ define(
|
||||
*/
|
||||
end: function () {
|
||||
return end;
|
||||
},
|
||||
/**
|
||||
* Pass a new configuration object (to retrieve and store
|
||||
* swimlane configuration)
|
||||
* @param newConfig
|
||||
*/
|
||||
configure: function (newConfig) {
|
||||
configuration = newConfig;
|
||||
initialize();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -68,6 +68,14 @@ define(
|
||||
};
|
||||
}
|
||||
|
||||
function fireWatch(expr, value) {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (call.args[0] === expr) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
beforeEach(function () {
|
||||
var mockA, mockB, mockUtilization, mockPromise, mockGraph, testCapabilities;
|
||||
@ -141,9 +149,15 @@ define(
|
||||
expect(mockScope.scroll.y).toEqual(0);
|
||||
});
|
||||
|
||||
it("watches for a configuration object", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"configuration",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("repopulates when modifications are made", function () {
|
||||
var fnWatchCall,
|
||||
strWatchCall;
|
||||
var fnWatchCall;
|
||||
|
||||
// Find the $watch that was given a function
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
@ -151,16 +165,11 @@ define(
|
||||
// white-box: we know the first call is
|
||||
// the one we're looking for
|
||||
fnWatchCall = fnWatchCall || call;
|
||||
} else if (typeof call.args[0] === 'string') {
|
||||
strWatchCall = strWatchCall || call;
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure string watch was for domainObject
|
||||
expect(strWatchCall.args[0]).toEqual('domainObject');
|
||||
// Initially populate
|
||||
strWatchCall.args[1](mockDomainObject);
|
||||
|
||||
fireWatch('domainObject', mockDomainObject);
|
||||
// There should be to swimlanes
|
||||
expect(controller.swimlanes().length).toEqual(2);
|
||||
|
||||
@ -182,23 +191,23 @@ define(
|
||||
// order of $watch calls in TimelineController.
|
||||
|
||||
// Initially populate
|
||||
mockScope.$watch.calls[0].args[1](mockDomainObject);
|
||||
fireWatch('domainObject', mockDomainObject);
|
||||
|
||||
// Verify precondition - no graphs
|
||||
expect(controller.graphs().length).toEqual(0);
|
||||
|
||||
// Execute the watch function for graph state
|
||||
tmp = mockScope.$watch.calls[2].args[0]();
|
||||
tmp = mockScope.$watch.calls[3].args[0]();
|
||||
|
||||
// Change graph state
|
||||
testConfiguration.graph = { a: true, b: true };
|
||||
|
||||
// Verify that this would have triggered a watch
|
||||
expect(mockScope.$watch.calls[2].args[0]())
|
||||
expect(mockScope.$watch.calls[3].args[0]())
|
||||
.not.toEqual(tmp);
|
||||
|
||||
// Run the function the watch would have triggered
|
||||
mockScope.$watch.calls[2].args[1]();
|
||||
mockScope.$watch.calls[3].args[1]();
|
||||
|
||||
// Should have some graphs now
|
||||
expect(controller.graphs().length).toEqual(2);
|
||||
@ -211,7 +220,7 @@ define(
|
||||
mockZoom.duration.andReturn(12345);
|
||||
|
||||
// Initially populate
|
||||
mockScope.$watch.calls[0].args[1](mockDomainObject);
|
||||
fireWatch('domainObject', mockDomainObject);
|
||||
|
||||
expect(controller.width(mockZoom)).toEqual(54321);
|
||||
// Verify interactions; we took zoom's duration for our start/end,
|
||||
|
@ -32,11 +32,7 @@ define(
|
||||
|
||||
beforeEach(function () {
|
||||
testConfiguration = {
|
||||
levels: [
|
||||
1000,
|
||||
2000,
|
||||
3500
|
||||
],
|
||||
levels: [1000, 2000, 3500],
|
||||
width: 12321
|
||||
};
|
||||
mockScope = jasmine.createSpyObj("$scope", ['$watch']);
|
||||
@ -74,32 +70,61 @@ define(
|
||||
expect(controller.zoom()).toEqual(3500);
|
||||
});
|
||||
|
||||
it("does not normally persist zoom changes", function () {
|
||||
controller.zoom(1);
|
||||
expect(mockScope.commit).not.toHaveBeenCalled();
|
||||
it("observes scroll bounds", function () {
|
||||
expect(mockScope.$watch)
|
||||
.toHaveBeenCalledWith("scroll", jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("persists zoom changes in Edit mode", function () {
|
||||
mockScope.domainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['hasCapability', 'getCapability']
|
||||
);
|
||||
mockScope.domainObject.hasCapability.andCallFake(function (c) {
|
||||
return c === 'editor';
|
||||
describe("when watches have fired", function () {
|
||||
var mockDomainObject,
|
||||
mockPromise,
|
||||
mockTimespan,
|
||||
testStart,
|
||||
testEnd;
|
||||
|
||||
beforeEach(function () {
|
||||
testStart = 3000;
|
||||
testEnd = 5500;
|
||||
|
||||
mockDomainObject = jasmine.createSpyObj('domainObject', [
|
||||
'getId',
|
||||
'getModel',
|
||||
'getCapability',
|
||||
'useCapability'
|
||||
]);
|
||||
mockPromise = jasmine.createSpyObj('promise', ['then']);
|
||||
mockTimespan = jasmine.createSpyObj('timespan', [
|
||||
'getStart',
|
||||
'getEnd',
|
||||
'getDuration'
|
||||
]);
|
||||
|
||||
mockDomainObject.useCapability.andCallFake(function (c) {
|
||||
return c === 'timespan' && mockPromise;
|
||||
});
|
||||
mockPromise.then.andCallFake(function (callback) {
|
||||
callback(mockTimespan);
|
||||
});
|
||||
mockTimespan.getStart.andReturn(testStart);
|
||||
mockTimespan.getEnd.andReturn(testEnd);
|
||||
mockTimespan.getDuration.andReturn(testEnd - testStart);
|
||||
|
||||
mockScope.scroll = { x: 0, width: 20000 };
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
call.args[1](mockScope[call.args[0]]);
|
||||
});
|
||||
});
|
||||
mockScope.domainObject.getCapability.andCallFake(function (c) {
|
||||
if (c === 'editor') {
|
||||
return {
|
||||
inEditContext: function () {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
it("zooms to fit the timeline", function () {
|
||||
var x1 = mockScope.scroll.x,
|
||||
x2 = mockScope.scroll.x + mockScope.scroll.width;
|
||||
expect(Math.round(controller.toMillis(x1)))
|
||||
.toEqual(testStart);
|
||||
expect(Math.round(controller.toMillis(x2)))
|
||||
.toBeGreaterThan(testEnd);
|
||||
});
|
||||
controller.zoom(1);
|
||||
expect(mockScope.commit).toHaveBeenCalled();
|
||||
expect(mockScope.configuration.zoomLevel)
|
||||
.toEqual(jasmine.any(Number));
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -177,6 +177,10 @@ define(
|
||||
// representation to store local variables into.
|
||||
$scope.representation = {};
|
||||
|
||||
// Change templates (passing in undefined to clear
|
||||
// if we don't have enough info to show a template.)
|
||||
changeTemplate(canRepresent ? representation : undefined);
|
||||
|
||||
// Any existing representers are no longer valid; release them.
|
||||
destroyRepresenters();
|
||||
|
||||
@ -222,10 +226,6 @@ define(
|
||||
// next change object/key pair changes
|
||||
toClear = uses.concat(['model']);
|
||||
}
|
||||
|
||||
// Change templates (passing in undefined to clear
|
||||
// if we don't have enough info to show a template.)
|
||||
changeTemplate(canRepresent ? representation : undefined);
|
||||
}
|
||||
|
||||
// Update the representation when the key changes (e.g. if a
|
||||
|
@ -46,7 +46,7 @@ define(
|
||||
// Find the relevant scope...
|
||||
var rect,
|
||||
scope = element.scope && element.scope();
|
||||
|
||||
|
||||
if (scope && scope.$broadcast) {
|
||||
// Get the representation's bounds, to convert
|
||||
// drop position
|
||||
|
@ -194,21 +194,6 @@ define(
|
||||
.toHaveBeenCalledWith(testViews[1]);
|
||||
});
|
||||
|
||||
it("exposes configuration before changing templates", function () {
|
||||
var observedConfiguration;
|
||||
|
||||
mockChangeTemplate.andCallFake(function () {
|
||||
observedConfiguration = mockScope.configuration;
|
||||
});
|
||||
|
||||
mockScope.key = "xyz";
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
fireWatch('key', mockScope.key);
|
||||
fireWatch('domainObject', mockDomainObject);
|
||||
|
||||
expect(observedConfiguration).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not load templates until there is an object", function () {
|
||||
mockScope.key = "xyz";
|
||||
|
||||
|
@ -28,6 +28,7 @@ define([
|
||||
"text!./res/templates/search-item.html",
|
||||
"text!./res/templates/search.html",
|
||||
"text!./res/templates/search-menu.html",
|
||||
"text!./src/services/GenericSearchWorker.js",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
SearchController,
|
||||
@ -37,6 +38,7 @@ define([
|
||||
searchItemTemplate,
|
||||
searchTemplate,
|
||||
searchMenuTemplate,
|
||||
searchWorkerText,
|
||||
legacyRegistry
|
||||
) {
|
||||
|
||||
@ -114,7 +116,7 @@ define([
|
||||
"workers": [
|
||||
{
|
||||
"key": "genericSearchWorker",
|
||||
"scriptUrl": "services/GenericSearchWorker.js"
|
||||
"scriptText": searchWorkerText
|
||||
}
|
||||
]
|
||||
}
|
||||
|
50
plugin-example.html
Normal file
50
plugin-example.html
Normal file
@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
||||
<title>Implementing a basic plugin</title>
|
||||
<script src="dist/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
function WebPlugin(websites) {
|
||||
this.websites = websites;
|
||||
|
||||
var ROOTS = websites.reduce(function (rootMap, website) {
|
||||
rootMap[website] = {
|
||||
namespace: website,
|
||||
identifier: 'page'
|
||||
};
|
||||
return rootMap;
|
||||
}, {});
|
||||
|
||||
function installPlugin(MCT) {
|
||||
Object.keys(ROOTS).forEach(function (rootUrl) {
|
||||
MCT.Objects.addRoot(ROOTS[rootUrl]);
|
||||
MCT.Objects.addProvider(rootUrl, {
|
||||
get: function () {
|
||||
return Promise.resolve({
|
||||
type: 'example.page',
|
||||
url: rootUrl,
|
||||
name: rootUrl
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return installPlugin;
|
||||
}
|
||||
|
||||
var myWebPlugin = WebPlugin([
|
||||
'http://www.wikipedia.org/',
|
||||
'http://nasa.github.io/openmct'
|
||||
]);
|
||||
|
||||
MCT.install(myWebPlugin);
|
||||
|
||||
MCT.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -24,10 +24,27 @@ define(function () {
|
||||
|
||||
function BundleRegistry() {
|
||||
this.bundles = {};
|
||||
this.knownBundles = {};
|
||||
}
|
||||
|
||||
BundleRegistry.prototype.register = function (path, definition) {
|
||||
this.bundles[path] = definition;
|
||||
if (this.knownBundles.hasOwnProperty(path)) {
|
||||
throw new Error('Cannot register bundle with duplicate path', path);
|
||||
}
|
||||
this.knownBundles[path] = definition;
|
||||
};
|
||||
|
||||
BundleRegistry.prototype.enable = function (path) {
|
||||
if (!this.knownBundles[path]) {
|
||||
throw new Error('Unknown bundle ' + path);
|
||||
}
|
||||
this.bundles[path] = this.knownBundles[path];
|
||||
};
|
||||
|
||||
BundleRegistry.prototype.disable = function (path) {
|
||||
if (!this.bundles[path]) {
|
||||
throw new Error('Tried to disable inactive bundle ' + path);
|
||||
}
|
||||
};
|
||||
|
||||
BundleRegistry.prototype.contains = function (path) {
|
||||
@ -42,8 +59,14 @@ define(function () {
|
||||
return Object.keys(this.bundles);
|
||||
};
|
||||
|
||||
BundleRegistry.prototype.remove = function (path) {
|
||||
BundleRegistry.prototype.remove = BundleRegistry.prototype.disable;
|
||||
|
||||
BundleRegistry.prototype.delete = function (path) {
|
||||
if (!this.knownBundles[path]) {
|
||||
throw new Error('Cannot remove Unknown Bundle ' + path);
|
||||
}
|
||||
delete this.bundles[path];
|
||||
delete this.knownBundles[path];
|
||||
};
|
||||
|
||||
return BundleRegistry;
|
||||
|
151
src/MCT.js
Normal file
151
src/MCT.js
Normal file
@ -0,0 +1,151 @@
|
||||
define([
|
||||
'EventEmitter',
|
||||
'legacyRegistry',
|
||||
'uuid',
|
||||
'./api/api',
|
||||
'text!./adapter/templates/edit-object-replacement.html',
|
||||
'./Selection',
|
||||
'./api/objects/object-utils'
|
||||
], function (
|
||||
EventEmitter,
|
||||
legacyRegistry,
|
||||
uuid,
|
||||
api,
|
||||
editObjectTemplate,
|
||||
Selection,
|
||||
objectUtils
|
||||
) {
|
||||
function MCT() {
|
||||
EventEmitter.call(this);
|
||||
this.legacyBundle = { extensions: {
|
||||
services: [
|
||||
{
|
||||
key: "mct",
|
||||
implementation: function () {
|
||||
return this;
|
||||
}.bind(this)
|
||||
}
|
||||
]
|
||||
} };
|
||||
|
||||
this.selection = new Selection();
|
||||
this.on('navigation', this.selection.clear.bind(this.selection));
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set path to where assets are hosted. This should be the path to main.js.
|
||||
*/
|
||||
MCT.prototype.setAssetPath = function (path) {
|
||||
this.legacyExtension('constants', {
|
||||
key: "ASSETS_PATH",
|
||||
value: path
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a new type of view.
|
||||
*
|
||||
* @param region the region identifier (see mct.regions)
|
||||
* @param {ViewDefinition} definition the definition for this view
|
||||
*/
|
||||
MCT.prototype.view = function (region, definition) {
|
||||
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) {
|
||||
var model = domainObject.getModel();
|
||||
var newDO = objectUtils.toNewFormat(model);
|
||||
return definition.canView(newDO);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this.legacyExtension('newViews', {
|
||||
factory: definition,
|
||||
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.start = function () {
|
||||
this.legacyExtension('runs', {
|
||||
depends: ['navigationService'],
|
||||
implementation: function (navigationService) {
|
||||
navigationService
|
||||
.addListener(this.emit.bind(this, 'navigation'));
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
legacyRegistry.register('adapter', this.legacyBundle);
|
||||
legacyRegistry.enable('adapter');
|
||||
this.emit('start');
|
||||
};
|
||||
|
||||
/**
|
||||
* Install a plugin in MCT.
|
||||
*
|
||||
* @param `Function` plugin -- a plugin install function which will be
|
||||
* invoked with the mct instance.
|
||||
*/
|
||||
MCT.prototype.install = function (plugin) {
|
||||
plugin(this);
|
||||
};
|
||||
|
||||
MCT.prototype.regions = {
|
||||
main: "MAIN",
|
||||
properties: "PROPERTIES",
|
||||
toolbar: "TOOLBAR"
|
||||
};
|
||||
|
||||
return MCT;
|
||||
});
|
32
src/Selection.js
Normal file
32
src/Selection.js
Normal file
@ -0,0 +1,32 @@
|
||||
define(['EventEmitter'], function (EventEmitter) {
|
||||
function Selection() {
|
||||
EventEmitter.call(this);
|
||||
this.selectedValues = [];
|
||||
}
|
||||
|
||||
Selection.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
Selection.prototype.select = function (value) {
|
||||
this.selectedValues.push(value);
|
||||
this.emit('change', this.selectedValues);
|
||||
return this.deselect.bind(this, value);
|
||||
};
|
||||
|
||||
Selection.prototype.deselect = function (value) {
|
||||
this.selectedValues = this.selectedValues.filter(function (v) {
|
||||
return v !== value;
|
||||
});
|
||||
this.emit('change', this.selectedValues);
|
||||
};
|
||||
|
||||
Selection.prototype.selected = function () {
|
||||
return this.selectedValues;
|
||||
};
|
||||
|
||||
Selection.prototype.clear = function () {
|
||||
this.selectedValues = [];
|
||||
this.emit('change', this.selectedValues);
|
||||
};
|
||||
|
||||
return Selection;
|
||||
});
|
44
src/adapter/actions/ActionDialogDecorator.js
Normal file
44
src/adapter/actions/ActionDialogDecorator.js
Normal file
@ -0,0 +1,44 @@
|
||||
define([
|
||||
'../../api/objects/object-utils'
|
||||
], function (objectUtils) {
|
||||
function ActionDialogDecorator(mct, newViews, actionService) {
|
||||
this.actionService = actionService;
|
||||
this.mct = mct;
|
||||
this.definitions = newViews.filter(function (newView) {
|
||||
return newView.region === mct.regions.properties;
|
||||
}).map(function (newView) {
|
||||
return newView.factory;
|
||||
});
|
||||
}
|
||||
|
||||
ActionDialogDecorator.prototype.getActions = function (context) {
|
||||
var mct = this.mct;
|
||||
var definitions = this.definitions;
|
||||
|
||||
return this.actionService.getActions(context).map(function (action) {
|
||||
if (action.dialogService) {
|
||||
var domainObject = objectUtils.toNewFormat(
|
||||
context.domainObject.getModel(),
|
||||
objectUtils.parseKeyString(context.domainObject.getId())
|
||||
);
|
||||
|
||||
definitions = definitions.filter(function (definition) {
|
||||
return definition.canView(domainObject);
|
||||
});
|
||||
|
||||
if (definitions.length > 0) {
|
||||
action.dialogService = Object.create(action.dialogService);
|
||||
action.dialogService.getUserInput = function (form, value) {
|
||||
return new mct.Dialog(
|
||||
definitions[0].view(context.domainObject),
|
||||
form.title
|
||||
).show();
|
||||
};
|
||||
}
|
||||
}
|
||||
return action;
|
||||
});
|
||||
};
|
||||
|
||||
return ActionDialogDecorator;
|
||||
});
|
65
src/adapter/bundle.js
Normal file
65
src/adapter/bundle.js
Normal file
@ -0,0 +1,65 @@
|
||||
define([
|
||||
'legacyRegistry',
|
||||
'./actions/ActionDialogDecorator',
|
||||
'./directives/MCTView',
|
||||
'./services/Instantiate',
|
||||
'./capabilities/APICapabilityDecorator',
|
||||
'./policies/AdapterCompositionPolicy'
|
||||
], function (
|
||||
legacyRegistry,
|
||||
ActionDialogDecorator,
|
||||
MCTView,
|
||||
Instantiate,
|
||||
APICapabilityDecorator,
|
||||
AdapterCompositionPolicy
|
||||
) {
|
||||
legacyRegistry.register('src/adapter', {
|
||||
"extensions": {
|
||||
"directives": [
|
||||
{
|
||||
key: "mctView",
|
||||
implementation: MCTView,
|
||||
depends: [
|
||||
"newViews[]",
|
||||
"mct"
|
||||
]
|
||||
}
|
||||
],
|
||||
services: [
|
||||
{
|
||||
key: "instantiate",
|
||||
priority: "mandatory",
|
||||
implementation: Instantiate,
|
||||
depends: [
|
||||
"capabilityService",
|
||||
"identifierService",
|
||||
"cacheService"
|
||||
]
|
||||
}
|
||||
],
|
||||
components: [
|
||||
{
|
||||
type: "decorator",
|
||||
provides: "capabilityService",
|
||||
implementation: APICapabilityDecorator,
|
||||
depends: [
|
||||
"$injector"
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "decorator",
|
||||
provides: "actionService",
|
||||
implementation: ActionDialogDecorator,
|
||||
depends: [ "mct", "newViews[]" ]
|
||||
}
|
||||
],
|
||||
policies: [
|
||||
{
|
||||
category: "composition",
|
||||
implementation: AdapterCompositionPolicy,
|
||||
depends: [ "mct" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
37
src/adapter/capabilities/APICapabilityDecorator.js
Normal file
37
src/adapter/capabilities/APICapabilityDecorator.js
Normal file
@ -0,0 +1,37 @@
|
||||
define([
|
||||
'./synchronizeMutationCapability',
|
||||
'./AlternateCompositionCapability'
|
||||
], function (
|
||||
synchronizeMutationCapability,
|
||||
AlternateCompositionCapability
|
||||
) {
|
||||
|
||||
/**
|
||||
* Overrides certain capabilities to keep consistency between old API
|
||||
* and new API.
|
||||
*/
|
||||
function APICapabilityDecorator($injector, capabilityService) {
|
||||
this.$injector = $injector;
|
||||
this.capabilityService = capabilityService;
|
||||
}
|
||||
|
||||
APICapabilityDecorator.prototype.getCapabilities = function (
|
||||
model
|
||||
) {
|
||||
var capabilities = this.capabilityService.getCapabilities(model);
|
||||
if (capabilities.mutation) {
|
||||
capabilities.mutation =
|
||||
synchronizeMutationCapability(capabilities.mutation);
|
||||
}
|
||||
if (AlternateCompositionCapability.appliesTo(model)) {
|
||||
capabilities.composition = function (domainObject) {
|
||||
return new AlternateCompositionCapability(this.$injector, domainObject)
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
};
|
||||
|
||||
return APICapabilityDecorator;
|
||||
|
||||
});
|
102
src/adapter/capabilities/AlternateCompositionCapability.js
Normal file
102
src/adapter/capabilities/AlternateCompositionCapability.js
Normal file
@ -0,0 +1,102 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining AlternateCompositionCapability. Created by vwoeltje on 11/7/14.
|
||||
*/
|
||||
define([
|
||||
'../../api/objects/object-utils',
|
||||
'../../api/composition/CompositionAPI'
|
||||
], function (objectUtils, CompositionAPI) {
|
||||
|
||||
function AlternateCompositionCapability($injector, domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
|
||||
this.getDependencies = function () {
|
||||
this.instantiate = $injector.get("instantiate");
|
||||
this.contextualize = $injector.get("contextualize");
|
||||
this.getDependencies = undefined;
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
AlternateCompositionCapability.prototype.add = function (child, index) {
|
||||
if (typeof index !== 'undefined') {
|
||||
// At first glance I don't see a location in the existing
|
||||
// codebase where add is called with an index. Won't support.
|
||||
throw new Error(
|
||||
'Composition Capability does not support adding at index'
|
||||
);
|
||||
}
|
||||
|
||||
function addChildToComposition(model) {
|
||||
var existingIndex = model.composition.indexOf(child.getId());
|
||||
if (existingIndex === -1) {
|
||||
model.composition.push(child.getId())
|
||||
}
|
||||
}
|
||||
|
||||
return this.domainObject.useCapability(
|
||||
'mutation',
|
||||
addChildToComposition
|
||||
)
|
||||
.then(this.invoke.bind(this))
|
||||
.then(function (children) {
|
||||
return children.filter(function (c) {
|
||||
return c.getId() === child.getId();
|
||||
})[0];
|
||||
});
|
||||
};
|
||||
|
||||
AlternateCompositionCapability.prototype.contextualizeChild = function (
|
||||
child
|
||||
) {
|
||||
if (this.getDependencies) {
|
||||
this.getDependencies();
|
||||
}
|
||||
|
||||
var keyString = objectUtils.makeKeyString(child.key);
|
||||
var oldModel = objectUtils.toOldFormat(child);
|
||||
var newDO = this.instantiate(oldModel, keyString);
|
||||
return this.contextualize(newDO, this.domainObject);
|
||||
|
||||
};
|
||||
|
||||
AlternateCompositionCapability.prototype.invoke = function () {
|
||||
var newFormatDO = objectUtils.toNewFormat(
|
||||
this.domainObject.getModel(),
|
||||
this.domainObject.getId()
|
||||
);
|
||||
var collection = CompositionAPI(newFormatDO);
|
||||
return collection.load()
|
||||
.then(function (children) {
|
||||
collection.destroy();
|
||||
return children.map(this.contextualizeChild, this);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
AlternateCompositionCapability.appliesTo = function (model) {
|
||||
return !!CompositionAPI(objectUtils.toNewFormat(model, model.id));
|
||||
};
|
||||
|
||||
return AlternateCompositionCapability;
|
||||
}
|
||||
);
|
27
src/adapter/capabilities/synchronizeMutationCapability.js
Normal file
27
src/adapter/capabilities/synchronizeMutationCapability.js
Normal file
@ -0,0 +1,27 @@
|
||||
define([
|
||||
|
||||
], function (
|
||||
|
||||
) {
|
||||
|
||||
/**
|
||||
* Wraps the mutation capability and synchronizes the mutation
|
||||
*/
|
||||
function synchronizeMutationCapability(mutationConstructor) {
|
||||
|
||||
return function makeCapability(domainObject) {
|
||||
var capability = mutationConstructor(domainObject);
|
||||
var oldListen = capability.listen.bind(capability);
|
||||
capability.listen = function (listener) {
|
||||
return oldListen(function (newModel) {
|
||||
capability.domainObject.model =
|
||||
JSON.parse(JSON.stringify(newModel));
|
||||
listener(newModel);
|
||||
});
|
||||
};
|
||||
return capability;
|
||||
}
|
||||
};
|
||||
|
||||
return synchronizeMutationCapability;
|
||||
});
|
65
src/adapter/directives/MCTView.js
Normal file
65
src/adapter/directives/MCTView.js
Normal file
@ -0,0 +1,65 @@
|
||||
define([
|
||||
'angular',
|
||||
'./Region',
|
||||
'../../api/objects/object-utils'
|
||||
], function (
|
||||
angular,
|
||||
Region,
|
||||
objectUtils
|
||||
) {
|
||||
function MCTView(newViews, PublicAPI) {
|
||||
var definitions = {};
|
||||
|
||||
newViews.forEach(function (newView) {
|
||||
definitions[newView.region] = definitions[newView.region] || {};
|
||||
definitions[newView.region][newView.key] = newView.factory;
|
||||
});
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function (scope, element, attrs) {
|
||||
var key, mctObject, regionId, region;
|
||||
|
||||
function maybeShow() {
|
||||
if (!definitions[regionId] || !definitions[regionId][key] || !mctObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
region.show(definitions[regionId][key].view(mctObject));
|
||||
}
|
||||
|
||||
function setKey(k) {
|
||||
key = k;
|
||||
maybeShow();
|
||||
}
|
||||
|
||||
function setObject(obj) {
|
||||
mctObject = undefined;
|
||||
PublicAPI.Objects.get(objectUtils.parseKeyString(obj.getId()))
|
||||
.then(function (mobj) {
|
||||
mctObject = mobj;
|
||||
maybeShow();
|
||||
});
|
||||
}
|
||||
|
||||
function setRegionId(r) {
|
||||
regionId = r;
|
||||
maybeShow();
|
||||
}
|
||||
|
||||
region = new Region(element[0]);
|
||||
|
||||
scope.$watch('key', setKey);
|
||||
scope.$watch('region', setRegionId);
|
||||
scope.$watch('mctObject', setObject);
|
||||
},
|
||||
scope: {
|
||||
key: "=",
|
||||
region: "=",
|
||||
mctObject: "="
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return MCTView;
|
||||
});
|
23
src/adapter/directives/Region.js
Normal file
23
src/adapter/directives/Region.js
Normal file
@ -0,0 +1,23 @@
|
||||
define([], function () {
|
||||
function Region(element) {
|
||||
this.activeView = undefined;
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
Region.prototype.clear = function () {
|
||||
if (this.activeView) {
|
||||
this.activeView.destroy();
|
||||
this.activeView = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
Region.prototype.show = function (view) {
|
||||
this.clear();
|
||||
this.activeView = view;
|
||||
if (this.activeView) {
|
||||
this.activeView.show(this.element);
|
||||
}
|
||||
};
|
||||
|
||||
return Region;
|
||||
});
|
26
src/adapter/policies/AdapterCompositionPolicy.js
Normal file
26
src/adapter/policies/AdapterCompositionPolicy.js
Normal file
@ -0,0 +1,26 @@
|
||||
define([], function () {
|
||||
function AdapterCompositionPolicy(mct) {
|
||||
this.mct = mct;
|
||||
}
|
||||
|
||||
AdapterCompositionPolicy.prototype.allow = function (
|
||||
containerType,
|
||||
childType
|
||||
) {
|
||||
var containerObject = containerType.getInitialModel();
|
||||
var childObject = childType.getInitialModel();
|
||||
|
||||
containerObject.type = containerType.getKey();
|
||||
childObject.type = childType.getKey();
|
||||
|
||||
var composition = this.mct.Composition(containerObject);
|
||||
|
||||
if (composition) {
|
||||
return composition.canContain(childObject);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return AdapterCompositionPolicy;
|
||||
});
|
49
src/adapter/services/Instantiate.js
Normal file
49
src/adapter/services/Instantiate.js
Normal 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.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['../../../platform/core/src/objects/DomainObjectImpl'],
|
||||
function (DomainObjectImpl) {
|
||||
|
||||
/**
|
||||
* Overrides platform version of instantiate, passes Id with model such
|
||||
* that capability detection can utilize new format domain objects.
|
||||
*/
|
||||
function Instantiate(
|
||||
capabilityService,
|
||||
identifierService,
|
||||
cacheService
|
||||
) {
|
||||
return function (model, id) {
|
||||
id = id || identifierService.generate();
|
||||
var old_id = model.id;
|
||||
model.id = id;
|
||||
var capabilities = capabilityService.getCapabilities(model);
|
||||
model.id = old_id;
|
||||
cacheService.put(id, model);
|
||||
return new DomainObjectImpl(id, model, capabilities);
|
||||
};
|
||||
}
|
||||
|
||||
return Instantiate;
|
||||
}
|
||||
);
|
46
src/adapter/templates/edit-object-replacement.html
Normal file
46
src/adapter/templates/edit-object-replacement.html
Normal 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
183
src/api/TimeConductor.js
Normal 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;
|
||||
});
|
110
src/api/TimeConductorSpec.js
Normal file
110
src/api/TimeConductorSpec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
54
src/api/Type.js
Normal file
54
src/api/Type.js
Normal file
@ -0,0 +1,54 @@
|
||||
define(function () {
|
||||
/**
|
||||
* @typedef TypeDefinition
|
||||
* @property {Metadata} metadata displayable metadata about this type
|
||||
* @property {function (object)} [initialize] a function which initializes
|
||||
* the model for new domain objects of this type
|
||||
* @property {boolean} [creatable] true if users should be allowed to
|
||||
* create this type (default: false)
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {TypeDefinition} definition
|
||||
* @constructor
|
||||
*/
|
||||
function Type(definition) {
|
||||
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.type === this.key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a definition for this type that can be registered using the
|
||||
* legacy bundle format.
|
||||
* @private
|
||||
*/
|
||||
Type.prototype.toLegacyDefinition = function () {
|
||||
var def = {};
|
||||
def.name = this.definition.metadata.label;
|
||||
def.glyph = this.definition.metadata.glyph;
|
||||
def.description = this.definition.metadata.description;
|
||||
def.properties = this.definition.form;
|
||||
|
||||
if (this.definition.initialize) {
|
||||
def.model = {};
|
||||
this.definition.initialize(def.model);
|
||||
}
|
||||
|
||||
if (this.definition.creatable) {
|
||||
def.features = ['creation'];
|
||||
}
|
||||
return def;
|
||||
};
|
||||
|
||||
return Type;
|
||||
});
|
38
src/api/View.js
Normal file
38
src/api/View.js
Normal file
@ -0,0 +1,38 @@
|
||||
define([], function () {
|
||||
|
||||
/**
|
||||
* A View is used to provide displayable content, and to react to
|
||||
* associated life cycle events.
|
||||
*
|
||||
* @interface
|
||||
*/
|
||||
function View() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the supplied DOM element with the contents of this view.
|
||||
*
|
||||
* View implementations should use this method to attach any
|
||||
* listeners or acquire other resources that are necessary to keep
|
||||
* the contents of this view up-to-date.
|
||||
*
|
||||
* @param {HTMLElement} container the DOM element to populate
|
||||
*/
|
||||
View.prototype.show = function (container) {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Release any resources associated with this view.
|
||||
*
|
||||
* View implementations should use this method to detach any
|
||||
* listeners or release other resources that are no longer necessary
|
||||
* once a view is no longer used.
|
||||
*/
|
||||
View.prototype.destroy = function () {
|
||||
|
||||
};
|
||||
|
||||
return View;
|
||||
});
|
44
src/api/ViewDefinition.js
Normal file
44
src/api/ViewDefinition.js
Normal file
@ -0,0 +1,44 @@
|
||||
define(function () {
|
||||
|
||||
/**
|
||||
* Defines a kind of view.
|
||||
* @interface
|
||||
*/
|
||||
function ViewDefinition() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata about this view, as may be used in the user interface
|
||||
* to present options for this view.
|
||||
* @param {*} object the object to be shown in this view
|
||||
* @returns {mct.ViewMetadata} metadata about this view
|
||||
*/
|
||||
ViewDefinition.prototype.metadata = function (object) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiate a new view of this object. Callers of this method are
|
||||
* responsible for calling `canView` before instantiating views in this
|
||||
* manner.
|
||||
*
|
||||
* @param {*} object the object to be shown in this view
|
||||
* @returns {mct.View} a view of this object
|
||||
*/
|
||||
ViewDefinition.prototype.view = function (object) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if this view is capable of showing this object. Users of
|
||||
* views should use this method before calling `show`.
|
||||
*
|
||||
* Subclasses should override this method to control the applicability
|
||||
* of this view to other objects.
|
||||
*
|
||||
* @param {*} object the object to be shown in this view
|
||||
* @returns {boolean} true if this view is applicable to this object
|
||||
*/
|
||||
ViewDefinition.prototype.canView = function (object) {
|
||||
};
|
||||
|
||||
return ViewDefinition;
|
||||
});
|
24
src/api/api.js
Normal file
24
src/api/api.js
Normal file
@ -0,0 +1,24 @@
|
||||
define([
|
||||
'./Type',
|
||||
'./TimeConductor',
|
||||
'./View',
|
||||
'./objects/ObjectAPI',
|
||||
'./composition/CompositionAPI',
|
||||
'./ui/Dialog'
|
||||
], function (
|
||||
Type,
|
||||
TimeConductor,
|
||||
View,
|
||||
ObjectAPI,
|
||||
CompositionAPI,
|
||||
Dialog
|
||||
) {
|
||||
return {
|
||||
Type: Type,
|
||||
TimeConductor: new TimeConductor(),
|
||||
View: View,
|
||||
Objects: ObjectAPI,
|
||||
Composition: CompositionAPI,
|
||||
Dialog: Dialog
|
||||
};
|
||||
});
|
39
src/api/composition/CompositionAPI.js
Normal file
39
src/api/composition/CompositionAPI.js
Normal file
@ -0,0 +1,39 @@
|
||||
define([
|
||||
'lodash',
|
||||
'EventEmitter',
|
||||
'./DefaultCompositionProvider',
|
||||
'./CompositionCollection'
|
||||
], function (
|
||||
_,
|
||||
EventEmitter,
|
||||
DefaultCompositionProvider,
|
||||
CompositionCollection
|
||||
) {
|
||||
|
||||
var PROVIDER_REGISTRY = [];
|
||||
|
||||
function getProvider (object) {
|
||||
return _.find(PROVIDER_REGISTRY, function (p) {
|
||||
return p.appliesTo(object);
|
||||
});
|
||||
};
|
||||
|
||||
function composition(object) {
|
||||
var provider = getProvider(object);
|
||||
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new CompositionCollection(object, provider);
|
||||
};
|
||||
|
||||
composition.addProvider = function (provider) {
|
||||
PROVIDER_REGISTRY.unshift(provider);
|
||||
};
|
||||
|
||||
composition.addProvider(new DefaultCompositionProvider());
|
||||
|
||||
return composition;
|
||||
|
||||
});
|
122
src/api/composition/CompositionCollection.js
Normal file
122
src/api/composition/CompositionCollection.js
Normal file
@ -0,0 +1,122 @@
|
||||
define([
|
||||
'EventEmitter',
|
||||
'lodash',
|
||||
'../objects/object-utils'
|
||||
], function (
|
||||
EventEmitter,
|
||||
_,
|
||||
objectUtils
|
||||
) {
|
||||
|
||||
function CompositionCollection(domainObject, provider) {
|
||||
EventEmitter.call(this);
|
||||
this.domainObject = domainObject;
|
||||
this.provider = provider;
|
||||
if (this.provider.on) {
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
this.provider.on(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CompositionCollection.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
CompositionCollection.prototype.onProviderAdd = function (child) {
|
||||
this.add(child, true);
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.onProviderRemove = function (child) {
|
||||
this.remove(child, true);
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.indexOf = function (child) {
|
||||
return _.findIndex(this._children, function (other) {
|
||||
return objectUtils.equals(child, other);
|
||||
});
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.contains = function (child) {
|
||||
return this.indexOf(child) !== -1;
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.add = function (child, skipMutate) {
|
||||
if (!this._children) {
|
||||
throw new Error("Must load composition before you can add!");
|
||||
}
|
||||
if (!this.canContain(child)) {
|
||||
throw new Error("This object cannot contain that object.");
|
||||
}
|
||||
if (this.contains(child)) {
|
||||
if (skipMutate) {
|
||||
return; // don't add twice, don't error.
|
||||
}
|
||||
throw new Error("Unable to add child: already in composition");
|
||||
}
|
||||
this._children.push(child);
|
||||
this.emit('add', child);
|
||||
if (!skipMutate) {
|
||||
// add after we have added.
|
||||
this.provider.add(this.domainObject, child);
|
||||
}
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.load = function () {
|
||||
return this.provider.load(this.domainObject)
|
||||
.then(function (children) {
|
||||
this._children = [];
|
||||
children.map(function (c) {
|
||||
this.add(c, true);
|
||||
}, this);
|
||||
this.emit('load');
|
||||
return this._children.slice();
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.remove = function (child, skipMutate) {
|
||||
if (!this.contains(child)) {
|
||||
if (skipMutate) {
|
||||
return;
|
||||
}
|
||||
throw new Error("Unable to remove child: not found in composition");
|
||||
}
|
||||
var index = this.indexOf(child);
|
||||
var removed = this._children.splice(index, 1)[0];
|
||||
this.emit('remove', index, child);
|
||||
if (!skipMutate) {
|
||||
// trigger removal after we have internally removed it.
|
||||
this.provider.remove(this.domainObject, removed);
|
||||
}
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.canContain = function (domainObject) {
|
||||
return this.provider.canContain(this.domainObject, domainObject);
|
||||
};
|
||||
|
||||
CompositionCollection.prototype.destroy = function () {
|
||||
if (this.provider.off) {
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'add',
|
||||
this.onProviderAdd,
|
||||
this
|
||||
);
|
||||
this.provider.off(
|
||||
this.domainObject,
|
||||
'remove',
|
||||
this.onProviderRemove,
|
||||
this
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return CompositionCollection
|
||||
});
|
80
src/api/composition/DefaultCompositionProvider.js
Normal file
80
src/api/composition/DefaultCompositionProvider.js
Normal file
@ -0,0 +1,80 @@
|
||||
define([
|
||||
'lodash',
|
||||
'EventEmitter',
|
||||
'../objects/ObjectAPI',
|
||||
'../objects/object-utils'
|
||||
], function (
|
||||
_,
|
||||
EventEmitter,
|
||||
ObjectAPI,
|
||||
objectUtils
|
||||
) {
|
||||
|
||||
function makeEventName(domainObject, event) {
|
||||
return event + ':' + objectUtils.makeKeyString(domainObject.key);
|
||||
}
|
||||
|
||||
function DefaultCompositionProvider() {
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
|
||||
DefaultCompositionProvider.prototype =
|
||||
Object.create(EventEmitter.prototype);
|
||||
|
||||
DefaultCompositionProvider.prototype.appliesTo = function (domainObject) {
|
||||
return !!domainObject.composition;
|
||||
};
|
||||
|
||||
DefaultCompositionProvider.prototype.load = function (domainObject) {
|
||||
return Promise.all(domainObject.composition.map(ObjectAPI.get));
|
||||
};
|
||||
|
||||
DefaultCompositionProvider.prototype.on = function (
|
||||
domainObject,
|
||||
event,
|
||||
listener,
|
||||
context
|
||||
) {
|
||||
// these can likely be passed through to the mutation service instead
|
||||
// of using an eventemitter.
|
||||
this.addListener(
|
||||
makeEventName(domainObject, event),
|
||||
listener,
|
||||
context
|
||||
);
|
||||
};
|
||||
|
||||
DefaultCompositionProvider.prototype.off = function (
|
||||
domainObject,
|
||||
event,
|
||||
listener,
|
||||
context
|
||||
) {
|
||||
// these can likely be passed through to the mutation service instead
|
||||
// of using an eventemitter.
|
||||
this.removeListener(
|
||||
makeEventName(domainObject, event),
|
||||
listener,
|
||||
context
|
||||
);
|
||||
};
|
||||
|
||||
DefaultCompositionProvider.prototype.canContain = function (domainObject, child) {
|
||||
return true;
|
||||
};
|
||||
|
||||
DefaultCompositionProvider.prototype.remove = function (domainObject, child) {
|
||||
// TODO: this needs to be synchronized via mutation
|
||||
var index = domainObject.composition.indexOf(child);
|
||||
domainObject.composition.splice(index, 1);
|
||||
this.emit(makeEventName(domainObject, 'remove'), child);
|
||||
};
|
||||
|
||||
DefaultCompositionProvider.prototype.add = function (domainObject, child) {
|
||||
// TODO: this needs to be synchronized via mutation
|
||||
domainObject.composition.push(child.key);
|
||||
this.emit(makeEventName(domainObject, 'add'), child);
|
||||
};
|
||||
|
||||
return DefaultCompositionProvider;
|
||||
});
|
37
src/api/composition/README.md
Normal file
37
src/api/composition/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Composition API - Overview
|
||||
|
||||
The composition API is straightforward:
|
||||
|
||||
MCT.composition(object) -- returns a `CompositionCollection` if the object has
|
||||
composition, returns undefined if it doesn't.
|
||||
|
||||
## CompositionCollection
|
||||
|
||||
Has three events:
|
||||
* `load`: when the collection has completed loading.
|
||||
* `add`: when a new object has been added to the collection.
|
||||
* `remove` when an object has been removed from the collection.
|
||||
|
||||
Has three methods:
|
||||
|
||||
`Collection.load()` -- returns a promise that is fulfilled when the composition
|
||||
has loaded.
|
||||
`Collection.add(object)` -- add a domain object to the composition.
|
||||
`Collection.remove(object)` -- remove the object from the composition.
|
||||
|
||||
## Composition providers
|
||||
composition providers are anything that meets the following interface:
|
||||
|
||||
* `provider.appliesTo(domainObject)` -> return true if this provider can provide
|
||||
composition for a given domain object.
|
||||
* `provider.add(domainObject, childObject)` -> adds object
|
||||
* `provider.remove(domainObject, childObject)` -> immediately removes objects
|
||||
* `provider.load(domainObject)` -> returns promise for array of children
|
||||
|
||||
TODO: need to figure out how to make the provider event listeners invisible to the provider.
|
||||
|
||||
There is a default composition provider which handles loading composition for
|
||||
any object with a `composition` property. If you want specialized composition
|
||||
loading behavior, implement your own composition provider and register it with
|
||||
|
||||
`MCT.composition.addProvider(myProvider)`
|
109
src/api/objects/LegacyObjectAPIInterceptor.js
Normal file
109
src/api/objects/LegacyObjectAPIInterceptor.js
Normal file
@ -0,0 +1,109 @@
|
||||
define([
|
||||
'./object-utils',
|
||||
'./ObjectAPI',
|
||||
'./objectEventEmitter'
|
||||
], function (
|
||||
utils,
|
||||
ObjectAPI,
|
||||
objectEventEmitter
|
||||
) {
|
||||
function ObjectServiceProvider(objectService, instantiate, topic) {
|
||||
this.objectService = objectService;
|
||||
this.instantiate = instantiate;
|
||||
|
||||
this.generalTopic = topic('mutation');
|
||||
this.bridgeEventBuses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges old and new style mutation events to provide compatibility between the two APIs
|
||||
* @private
|
||||
*/
|
||||
ObjectServiceProvider.prototype.bridgeEventBuses = function () {
|
||||
var removeGeneralTopicListener;
|
||||
|
||||
var handleMutation = function (newStyleObject) {
|
||||
var keyString = utils.makeKeyString(newStyleObject.key);
|
||||
var oldStyleObject = this.instantiate(utils.toOldFormat(newStyleObject), keyString);
|
||||
|
||||
// Don't trigger self
|
||||
removeGeneralTopicListener();
|
||||
|
||||
oldStyleObject.getCapability('mutation').mutate(function () {
|
||||
return utils.toOldFormat(newStyleObject);
|
||||
});
|
||||
|
||||
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
|
||||
}.bind(this);
|
||||
|
||||
var handleLegacyMutation = function (legacyObject){
|
||||
var newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
|
||||
|
||||
//Don't trigger self
|
||||
objectEventEmitter.off('mutation', handleMutation);
|
||||
objectEventEmitter.emit(newStyleObject.key.identifier + ":*", newStyleObject);
|
||||
objectEventEmitter.on('mutation', handleMutation);
|
||||
}.bind(this);
|
||||
|
||||
objectEventEmitter.on('mutation', handleMutation);
|
||||
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
|
||||
};
|
||||
|
||||
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 = 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, topic, 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, topic)
|
||||
);
|
||||
|
||||
ROOTS.forEach(function (r) {
|
||||
ObjectAPI.addRoot(utils.parseKeyString(r.id));
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
return LegacyObjectAPIInterceptor;
|
||||
});
|
54
src/api/objects/MutableObject.js
Normal file
54
src/api/objects/MutableObject.js
Normal file
@ -0,0 +1,54 @@
|
||||
define([
|
||||
'lodash',
|
||||
'./objectEventEmitter'
|
||||
], function (
|
||||
_,
|
||||
objectEventEmitter
|
||||
) {
|
||||
|
||||
var ANY_OBJECT_EVENT = "mutation";
|
||||
|
||||
/**
|
||||
* The MutableObject wraps a DomainObject and provides getters and
|
||||
* setters for
|
||||
* @param eventEmitter
|
||||
* @param object
|
||||
* @constructor
|
||||
*/
|
||||
function MutableObject(object) {
|
||||
this.object = object;
|
||||
this.unlisteners = [];
|
||||
}
|
||||
|
||||
function qualifiedEventName(object, eventName) {
|
||||
return [object.key.identifier, eventName].join(':');
|
||||
}
|
||||
|
||||
MutableObject.prototype.stopListening = function () {
|
||||
this.unlisteners.forEach(function (unlisten) {
|
||||
unlisten();
|
||||
})
|
||||
};
|
||||
|
||||
MutableObject.prototype.on = function(path, callback) {
|
||||
var fullPath = qualifiedEventName(this.object, path);
|
||||
objectEventEmitter.on(fullPath, callback);
|
||||
this.unlisteners.push(objectEventEmitter.off.bind(objectEventEmitter, fullPath, callback));
|
||||
};
|
||||
|
||||
MutableObject.prototype.set = function (path, value) {
|
||||
|
||||
_.set(this.object, path, value);
|
||||
_.set(this.object, 'modified', Date.now());
|
||||
|
||||
//Emit event specific to property
|
||||
objectEventEmitter.emit(qualifiedEventName(this.object, path), value);
|
||||
//Emit wildcare event
|
||||
objectEventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
|
||||
|
||||
//Emit a general "any object" event
|
||||
objectEventEmitter.emit(ANY_OBJECT_EVENT, this.object);
|
||||
};
|
||||
|
||||
return MutableObject;
|
||||
});
|
110
src/api/objects/MutableObjectSpec.js
Normal file
110
src/api/objects/MutableObjectSpec.js
Normal 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(['./MutableObject'], function (MutableObject) {
|
||||
describe('Mutable Domain Objects', function () {
|
||||
var domainObject,
|
||||
mutableObject,
|
||||
eventEmitter,
|
||||
arrayProperty,
|
||||
objectProperty,
|
||||
identifier;
|
||||
|
||||
beforeEach(function () {
|
||||
identifier = 'objectId';
|
||||
arrayProperty = [
|
||||
'First array element',
|
||||
'Second array element',
|
||||
'Third array element'
|
||||
];
|
||||
objectProperty = {
|
||||
prop1: 'val1',
|
||||
prop2: 'val2',
|
||||
prop3: {
|
||||
propA: 'valA',
|
||||
propB: 'valB',
|
||||
propC: []
|
||||
}
|
||||
};
|
||||
domainObject = {
|
||||
key: {
|
||||
identifier: identifier
|
||||
},
|
||||
stringProperty: 'stringValue',
|
||||
objectProperty: objectProperty,
|
||||
arrayProperty: arrayProperty
|
||||
};
|
||||
eventEmitter = jasmine.createSpyObj('eventEmitter', [
|
||||
'emit'
|
||||
]);
|
||||
mutableObject = new MutableObject(eventEmitter, domainObject);
|
||||
});
|
||||
|
||||
it('Supports getting and setting of object properties', function () {
|
||||
expect(domainObject.stringProperty).toEqual('stringValue');
|
||||
mutableObject.set('stringProperty', 'updated');
|
||||
expect(domainObject.stringProperty).toEqual('updated');
|
||||
|
||||
var newArrayProperty = [];
|
||||
expect(domainObject.arrayProperty).toEqual(arrayProperty);
|
||||
mutableObject.set('arrayProperty', newArrayProperty);
|
||||
expect(domainObject.arrayProperty).toEqual(newArrayProperty);
|
||||
|
||||
var newObjectProperty = [];
|
||||
expect(domainObject.objectProperty).toEqual(objectProperty);
|
||||
mutableObject.set('objectProperty', newObjectProperty);
|
||||
expect(domainObject.objectProperty).toEqual(newObjectProperty);
|
||||
});
|
||||
|
||||
it('Supports getting and setting of nested properties', function () {
|
||||
expect(domainObject.objectProperty).toEqual(objectProperty);
|
||||
expect(domainObject.objectProperty.prop1).toEqual(objectProperty.prop1);
|
||||
expect(domainObject.objectProperty.prop3.propA).toEqual(objectProperty.prop3.propA);
|
||||
|
||||
mutableObject.set('objectProperty.prop1', 'new-prop-1');
|
||||
expect(domainObject.objectProperty.prop1).toEqual('new-prop-1');
|
||||
|
||||
mutableObject.set('objectProperty.prop3.propA', 'new-prop-A');
|
||||
expect(domainObject.objectProperty.prop3.propA).toEqual('new-prop-A');
|
||||
|
||||
mutableObject.set('arrayProperty.1', 'New Second Array Element');
|
||||
expect(arrayProperty[1]).toEqual('New Second Array Element');
|
||||
});
|
||||
|
||||
it('Fires events when properties change', function () {
|
||||
var newString = 'updated'
|
||||
mutableObject.set('stringProperty', newString);
|
||||
expect(eventEmitter.emit).toHaveBeenCalledWith([identifier, 'stringProperty'].join(':'), newString);
|
||||
|
||||
var newArray = [];
|
||||
mutableObject.set('arrayProperty', newArray);
|
||||
expect(eventEmitter.emit).toHaveBeenCalledWith([identifier, 'arrayProperty'].join(':'), newArray);
|
||||
|
||||
});
|
||||
|
||||
it('Fires wildcard event when any property changes', function () {
|
||||
var newString = 'updated'
|
||||
mutableObject.set('objectProperty.prop3.propA', newString);
|
||||
expect(eventEmitter.emit).toHaveBeenCalledWith([identifier, '*'].join(':'), domainObject);
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user