Compare commits

..

1 Commits

Author SHA1 Message Date
d2e064dc52 Table fixes
- Fixed sorting and filtering on non-string columns
- Fixed warnings about missing required prop and invalid default value in table rows
- Fixed error occuring when formatting non-existent column
2018-09-25 11:38:15 -07:00
132 changed files with 6539 additions and 6870 deletions

2
app.js
View File

@ -16,7 +16,7 @@ const request = require('request');
// Defaults
options.port = options.port || options.p || 8080;
options.host = options.host || options.h || 'localhost';
options.host = options.host || options.h || 'localhost'
options.directory = options.directory || options.D || '.';
// Show command line options

View File

@ -26,16 +26,12 @@ define([
"./src/NotificationLaunchController",
"./src/DialogLaunchIndicator",
"./src/NotificationLaunchIndicator",
"./res/dialog-launch.html",
"./res/notification-launch.html",
'legacyRegistry'
], function (
DialogLaunchController,
NotificationLaunchController,
DialogLaunchIndicator,
NotificationLaunchIndicator,
DialogLaunch,
NotificationLaunch,
legacyRegistry
) {
"use strict";
@ -45,11 +41,11 @@ define([
"templates": [
{
"key": "dialogLaunchTemplate",
"template": DialogLaunch
"templateUrl": "dialog-launch.html"
},
{
"key": "notificationLaunchTemplate",
"template": NotificationLaunch
"templateUrl": "notification-launch.html"
}
],
"controllers": [

View File

@ -51,26 +51,76 @@ define(
return actionTexts[Math.floor(Math.random()*3)];
}
function getExampleActions() {
var actions = [
{
label: "Try Again",
callback: function () {
$log.debug("Try Again pressed");
}
},
{
label: "Remove",
callback: function () {
$log.debug("Remove pressed");
}
},
{
label: "Cancel",
callback: function () {
$log.debug("Cancel pressed");
}
}
];
// Randomly remove some actions off the top; leave at least one
actions.splice(0,Math.floor(Math.random() * actions.length));
return actions;
}
function getExampleSeverity() {
var severities = [
"info",
"alert",
"error"
];
return severities[Math.floor(Math.random() * severities.length)];
}
/**
* Launch a new notification with a severity level of 'Error'.
*/
$scope.newError = function () {
$scope.newError = function(){
notificationService.notify({
title: "Example error notification " + messageCounter++,
hint: "An error has occurred",
severity: "error"
});
severity: "error",
primaryOption: {
label: 'Retry',
callback: function() {
$log.info('Retry clicked');
}
},
options: getExampleActions()});
};
/**
* Launch a new notification with a severity of 'Alert'.
*/
$scope.newAlert = function () {
$scope.newAlert = function(){
notificationService.notify({
title: "Alert notification " + (messageCounter++),
hint: "This is an alert message",
severity: "alert",
autoDismiss: true
});
primaryOption: {
label: 'Retry',
callback: function() {
$log.info('Retry clicked');
}
},
options: getExampleActions()});
};
@ -78,38 +128,39 @@ define(
* Launch a new notification with a progress bar that is updated
* periodically, tracking an ongoing process.
*/
$scope.newProgress = function () {
$scope.newProgress = function(){
var notificationModel = {
title: "Progress notification example",
severity: "info",
progress: 0,
actionText: getExampleActionText()
actionText: getExampleActionText(),
unknownProgress: false
};
/**
* Simulate an ongoing process and update the progress bar.
* @param notification
*/
function incrementProgress() {
function incrementProgress(notificationModel) {
notificationModel.progress = Math.min(100, Math.floor(notificationModel.progress + Math.random() * 30));
notificationModel.progressText = ["Estimated time" +
" remaining:" +
" about ", 60 - Math.floor((notificationModel.progress / 100) * 60), " seconds"].join(" ");
if (notificationModel.progress < 100) {
$timeout(function () {
incrementProgress(notificationModel);
}, 1000);
$timeout(function(){incrementProgress(notificationModel);}, 1000);
}
}
notificationService.notify(notificationModel);
incrementProgress();
incrementProgress(notificationModel);
};
/**
* Launch a new notification with severity level of INFO.
*/
$scope.newInfo = function () {
$scope.newInfo = function(){
notificationService.info({
title: "Example Info notification " + messageCounter++
});

View File

@ -75,7 +75,6 @@
}));
openmct.install(openmct.plugins.SummaryWidget());
openmct.install(openmct.plugins.Notebook());
openmct.install(openmct.plugins.FolderView());
openmct.time.clock('local', {start: -THIRTY_MINUTES, end: 0});
openmct.time.timeSystem('utc');
openmct.start();

View File

@ -35,10 +35,12 @@ define([
"./src/windowing/WindowTitler",
"./res/templates/browse.html",
"./res/templates/browse-object.html",
"./res/templates/items/grid-item.html",
"./res/templates/browse/object-header.html",
"./res/templates/browse/object-header-frame.html",
"./res/templates/menu-arrow.html",
"./res/templates/back-arrow.html",
"./res/templates/items/items.html",
"./res/templates/browse/object-properties.html",
"./res/templates/browse/inspector-region.html",
'legacyRegistry'
@ -57,10 +59,12 @@ define([
WindowTitler,
browseTemplate,
browseObjectTemplate,
gridItemTemplate,
objectHeaderTemplate,
objectHeaderFrameTemplate,
menuArrowTemplate,
backArrowTemplate,
itemsTemplate,
objectPropertiesTemplate,
inspectorRegionTemplate,
legacyRegistry
@ -152,6 +156,19 @@ define([
"view"
]
},
{
"key": "grid-item",
"template": gridItemTemplate,
"uses": [
"type",
"action",
"location"
],
"gestures": [
"info",
"menu"
]
},
{
"key": "object-header",
"template": objectHeaderTemplate,
@ -234,6 +251,23 @@ define([
"priority": "default"
}
],
"views": [
{
"key": "items",
"name": "Grid",
"cssClass": "icon-thumbs-strip",
"description": "Grid of available items",
"template": itemsTemplate,
"uses": [
"composition"
],
"gestures": [
"drop"
],
"type": "folder",
"editable": false
}
],
"runs": [
{
"implementation": WindowTitler,

View File

@ -0,0 +1,45 @@
<!--
Open MCT, Copyright (c) 2014-2018, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<!-- For selected, add class 'selected' to outer div -->
<div class='item grid-item' ng-click='action.perform("navigate")'>
<div class='contents abs'>
<div class='top-bar bar abs'>
<span class='icon-people' title='Shared'></span>
<mct-representation class="desktop-hide" key="'info-button'" mct-object="domainObject"></mct-representation>
</div>
<div class='item-main abs lg'>
<span class="t-item-icon" ng-class="{ 'l-icon-link':location.isLink() }">
<span class="t-item-icon-glyph ng-binding {{type.getCssClass()}}"></span>
</span>
<div class='abs item-open icon-pointer-right'></div>
</div>
<div class='bottom-bar bar abs'>
<div class='title'>{{model.name}}</div>
<div class='details'>
<span>{{type.getName()}}</span>
<span ng-show="model.composition !== undefined">
- {{model.composition.length}} Item<span ng-show="model.composition.length > 1">s</span>
</span>
</div>
</div>
</div>
</div>

View File

@ -2,41 +2,26 @@
Open MCT, Copyright (c) 2014-2018, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<template>
<span class="c-status">
<indicators></indicators>
<notification-banner></notification-banner>
</span>
</template>
<style lang="scss">
.c-status {
width: 100%;
}
</style>
<script>
import Indicators from './Indicators.vue';
import NotificationBanner from './NotificationBanner.vue';
export default {
components: {
Indicators,
NotificationBanner
}
}
</script>
<div class='items-holder grid abs'>
<mct-representation key="'grid-item'"
ng-repeat="childObject in composition"
mct-object="childObject">
</mct-representation>
</div>

View File

@ -58,7 +58,7 @@ define([], function () {
function checkNavigation() {
var navigatedObject = navigationService.getNavigation();
if (navigatedObject && navigatedObject.hasCapability('context')) {
if (navigatedObject.hasCapability('context')) {
if (!navigatedObject.getCapability('editor').isEditContextRoot()) {
preventOrphanNavigation(navigatedObject);
}

View File

@ -405,8 +405,7 @@ define([
"description": "Provides transactional editing capabilities",
"implementation": EditorCapability,
"depends": [
"transactionService",
"openmct"
"transactionService"
]
}
],

View File

@ -36,11 +36,9 @@ define(
*/
function EditorCapability(
transactionService,
openmct,
domainObject
) {
this.transactionService = transactionService;
this.openmct = openmct;
this.domainObject = domainObject;
}
@ -50,19 +48,27 @@ define(
* or finish() are called.
*/
EditorCapability.prototype.edit = function () {
console.warn('DEPRECATED: cannot edit via edit capability, use openmct.editor instead.');
this.openmct.editor.edit();
this.transactionService.startTransaction();
this.domainObject.getCapability('status').set('editing', true);
};
function isEditContextRoot(domainObject) {
return domainObject.getCapability('status').get('editing');
}
function isEditing(domainObject) {
return isEditContextRoot(domainObject) ||
domainObject.hasCapability('context') &&
isEditing(domainObject.getCapability('context').getParent());
}
/**
* Determines whether this object, or any of its ancestors are
* currently being edited.
* @returns boolean
*/
EditorCapability.prototype.inEditContext = function () {
console.warn('DEPRECATION WARNING: isEditing checks must be done via openmct.editor.');
return this.openmct.editor.isEditing();
return isEditing(this.domainObject);
};
/**
@ -71,8 +77,7 @@ define(
* @returns {*}
*/
EditorCapability.prototype.isEditContextRoot = function () {
console.warn('DEPRECATION WARNING: isEditing checks must be done via openmct.editor.');
return this.openmct.editor.isEditing();
return isEditContextRoot(this.domainObject);
};
/**
@ -81,7 +86,10 @@ define(
* @returns {*}
*/
EditorCapability.prototype.save = function () {
console.warn('DEPRECATED: cannot save via edit capability, use openmct.editor instead.');
var transactionService = this.transactionService;
return transactionService.commit().then(function () {
transactionService.startTransaction();
});
};
EditorCapability.prototype.invoke = EditorCapability.prototype.edit;
@ -92,7 +100,16 @@ define(
* @returns {*}
*/
EditorCapability.prototype.finish = function () {
console.warn('DEPRECATED: cannot finish via edit capability, use openmct.editor instead.');
var domainObject = this.domainObject;
if (this.transactionService.isActive()) {
return this.transactionService.cancel().then(function () {
domainObject.getCapability("status").set("editing", false);
return domainObject;
});
} else {
return Promise.resolve(domainObject);
}
};
/**

View File

@ -56,7 +56,7 @@ define([
};
DurationFormat.prototype.validate = function (text) {
return moment.utc(text, DATE_FORMATS, true).isValid();
return moment.utc(text, DATE_FORMATS).isValid();
};
return DurationFormat;

View File

@ -29,7 +29,6 @@ define([
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS",
DATE_FORMATS = [
DATE_FORMAT,
DATE_FORMAT + "Z",
"YYYY-MM-DD HH:mm:ss",
"YYYY-MM-DD HH:mm",
"YYYY-MM-DD"
@ -53,14 +52,70 @@ define([
this.key = "utc";
}
/**
* Returns an appropriate time format based on the provided value and
* the threshold required.
* @private
*/
function getScaledFormat(d) {
var momentified = moment.utc(d);
/**
* Uses logic from d3 Time-Scales, v3 of the API. See
* https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md
*
* Licensed
*/
var format = [
[".SSS", function (m) {
return m.milliseconds();
}],
[":ss", function (m) {
return m.seconds();
}],
["HH:mm", function (m) {
return m.minutes();
}],
["HH", function (m) {
return m.hours();
}],
["ddd DD", function (m) {
return m.days() &&
m.date() !== 1;
}],
["MMM DD", function (m) {
return m.date() !== 1;
}],
["MMMM", function (m) {
return m.month();
}],
["YYYY", function () {
return true;
}]
].filter(function (row) {
return row[1](momentified);
})[0][0];
if (format !== undefined) {
return moment.utc(d).format(format);
}
}
/**
* @param {number} value The value to format.
* @param {number} [minValue] Contextual information for scaled formatting used in linear scales such as conductor
* and plot axes. Specifies the smallest number on the scale.
* @param {number} [maxValue] Contextual information for scaled formatting used in linear scales such as conductor
* and plot axes. Specifies the largest number on the scale
* @param {number} [count] Contextual information for scaled formatting used in linear scales such as conductor
* and plot axes. The number of labels on the scale.
* @returns {string} the formatted date(s). If multiple values were requested, then an array of
* formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position
* in the array.
*/
UTCTimeFormat.prototype.format = function (value) {
if (value !== undefined) {
if (arguments.length > 1) {
return getScaledFormat(value);
} else if (value !== undefined) {
return moment.utc(value).format(DATE_FORMAT) + "Z";
} else {
return value;
@ -75,7 +130,7 @@ define([
};
UTCTimeFormat.prototype.validate = function (text) {
return moment.utc(text, DATE_FORMATS, true).isValid();
return moment.utc(text, DATE_FORMATS).isValid();
};
return UTCTimeFormat;

View File

@ -0,0 +1,62 @@
/*****************************************************************************
* 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([
"../src/UTCTimeFormat",
"moment"
], function (
UTCTimeFormat,
moment
) {
describe("The UTCTimeFormat class", function () {
var format;
var scale;
beforeEach(function () {
format = new UTCTimeFormat();
scale = {min: 0, max: 0};
});
it("Provides an appropriately scaled time format based on the input" +
" time", function () {
var TWO_HUNDRED_MS = 200;
var THREE_SECONDS = 3000;
var FIVE_MINUTES = 5 * 60 * 1000;
var ONE_HOUR_TWENTY_MINS = (1 * 60 * 60 * 1000) + (20 * 60 * 1000);
var TEN_HOURS = (10 * 60 * 60 * 1000);
var JUNE_THIRD = moment.utc("2016-06-03", "YYYY-MM-DD");
var APRIL = moment.utc("2016-04", "YYYY-MM");
var TWENTY_SIXTEEN = moment.utc("2016", "YYYY");
expect(format.format(TWO_HUNDRED_MS, scale)).toBe(".200");
expect(format.format(THREE_SECONDS, scale)).toBe(":03");
expect(format.format(FIVE_MINUTES, scale)).toBe("00:05");
expect(format.format(ONE_HOUR_TWENTY_MINS, scale)).toBe("01:20");
expect(format.format(TEN_HOURS, scale)).toBe("10");
expect(format.format(JUNE_THIRD, scale)).toBe("Fri 03");
expect(format.format(APRIL, scale)).toBe("April");
expect(format.format(TWENTY_SIXTEEN, scale)).toBe("2016");
});
});
});

View File

@ -60,7 +60,7 @@ define(
};
//If the notification is dismissed by the user, close
// the dialog.
notification.on('dismiss', function () {
notification.onDismiss(function () {
dialog.dismiss();
});

View File

@ -23,17 +23,33 @@
define([
"./src/NotificationIndicatorController",
"./src/NotificationIndicator",
"./src/NotificationService",
"./res/notification-indicator.html",
'legacyRegistry'
], function (
NotificationIndicatorController,
NotificationIndicator,
NotificationService,
notificationIndicatorTemplate,
legacyRegistry
) {
legacyRegistry.register("platform/commonUI/notification", {
"extensions": {
"constants": [
{
"key": "DEFAULT_AUTO_DISMISS",
"value": 3000
},
{
"key": "FORCE_AUTO_DISMISS",
"value": 1000
},
{
"key": "MINIMIZE_TIMEOUT",
"value": 300
}
],
"templates": [
{
"key": "notificationIndicatorTemplate",
@ -60,11 +76,12 @@ define([
"services": [
{
"key": "notificationService",
"implementation": function (openmct) {
return openmct.notifications;
},
"implementation": NotificationService,
"depends": [
"openmct"
"$timeout",
"topic",
"DEFAULT_AUTO_DISMISS",
"MINIMIZE_TIMEOUT"
]
}
]

View File

@ -0,0 +1,437 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/**
* This bundle implements the notification service, which can be used to
* show banner notifications to the user. Banner notifications
* are used to inform users of events in a non-intrusive way. As
* much as possible, notifications share a model with blocking
* dialogs so that the same information can be provided in a dialog
* and then minimized to a banner notification if needed.
*
* @namespace platform/commonUI/notification
*/
define(
['moment'],
function (moment) {
/**
* A representation of a user action. Options are provided to
* dialogs and notifications and are shown as buttons.
*
* @typedef {object} NotificationOption
* @property {string} label the label to appear on the button for
* this action
* @property {function} callback a callback function to be invoked
* when the button is clicked
*/
/**
* A representation of a banner notification. Banner notifications
* are used to inform users of events in a non-intrusive way. As
* much as possible, notifications share a model with blocking
* dialogs so that the same information can be provided in a dialog
* and then minimized to a banner notification if needed, or vice-versa.
*
* @typedef {object} NotificationModel
* @property {string} title The title of the message
* @property {string} severity The importance of the message (one of
* 'info', 'alert', or 'error' where info < alert <error)
* @property {number} [progress] The completion status of a task
* represented numerically
* @property {boolean} [unknownProgress] a boolean indicating that the
* progress of the underlying task is unknown. This will result in a
* visually distinct progress bar.
* @property {boolean} [autoDismiss] If truthy, dialog will
* be automatically minimized or dismissed (depending on severity).
* Additionally, if the provided value is a number, it will be used
* as the delay period before being dismissed.
* @property {boolean} [dismissable=true] If true, notification will
* include an option to dismiss it completely.
* @property {NotificationOption} [primaryOption] the default user
* response to
* this message. Will be represented as a button with the provided
* label and action. May be used by banner notifications to display
* only the most important option to users.
* @property {NotificationOption[]} [options] any additional
* actions the user can take. Will be represented as additional buttons
* that may or may not be available from a banner.
* @see DialogModel
*/
/**
* A wrapper object that is returned as a handle to a newly created
* notification. Wraps the notifications model and decorates with
* functions to dismiss or minimize the notification.
*
* @typedef {object} Notification
* @property {function} dismiss This method is added to the object
* returned by {@link NotificationService#notify} and can be used to
* dismiss this notification. Dismissing a notification will remove
* it completely and it will not appear in the notification indicator
* @property {function} minimize This method is added to the object
* returned by {@link NotificationService#notify} and can be used to
* minimize this notification. Minimizing a notification will send
* it to the notification indicator
* @property {function} dismissOrMinimize This method is added to the
* object returned by {@link NotificationService#notify}. It will
* hide the notification by either dismissing or minimizing it,
* depending on severity. Typically this is the method that should
* be used for dismissing a notification. If more control is
* required, then the minimize or dismiss functions can be called
* individually.
* @property {function} onDismiss Allows listening for on dismiss
* events. This allows cleanup etc. when the notification is dismissed.
*/
/**
* The notification service is responsible for informing the user of
* events via the use of banner notifications.
* @memberof platform/commonUI/notification
* @constructor
* @param $timeout the Angular $timeout service
* @param defaultAutoDismissTimeout The period of time that an
* auto-dismissed message will be displayed for.
* @param minimizeAnimationTimeout When notifications are minimized, a brief
* animation is shown. This animation requires some time to execute,
* so a timeout is required before the notification is hidden
*/
function NotificationService($timeout, topic, defaultAutoDismissTimeout, minimizeAnimationTimeout) {
this.notifications = [];
this.$timeout = $timeout;
this.highest = { severity: "info" };
this.AUTO_DISMISS_TIMEOUT = defaultAutoDismissTimeout;
this.MINIMIZE_ANIMATION_TIMEOUT = minimizeAnimationTimeout;
this.topic = topic;
/*
* A context in which to hold the active notification and a
* handle to its timeout.
*/
this.active = {};
}
/**
* Minimize a notification. The notification will still be available
* from the notification list. Typically notifications with a
* severity of 'info' should not be minimized, but rather
* dismissed. If you're not sure which is appropriate,
* use {@link Notification#dismissOrMinimize}
*
* @private
*/
NotificationService.prototype.minimize = function (service, notification) {
//Check this is a known notification
var index = service.notifications.indexOf(notification);
if (service.active.timeout) {
/*
Method can be called manually (clicking dismiss) or
automatically from an auto-timeout. this.active.timeout
acts as a semaphore to prevent race conditions. Cancel any
timeout in progress (for the case where a manual dismiss
has shortcut an active auto-dismiss), and clear the
semaphore.
*/
service.$timeout.cancel(service.active.timeout);
delete service.active.timeout;
}
if (index >= 0) {
notification.model.minimized = true;
//Add a brief timeout before showing the next notification
// in order to allow the minimize animation to run through.
service.$timeout(function () {
service.setActiveNotification(service.selectNextNotification());
}, service.MINIMIZE_ANIMATION_TIMEOUT);
}
};
/**
* Completely removes a notification. This will dismiss it from the
* message banner and remove it from the list of notifications.
* Typically only notifications with a severity of info should be
* dismissed. If you're not sure whether to dismiss or minimize a
* notification, use {@link Notification#dismissOrMinimize}.
* dismiss
*
* @private
*/
NotificationService.prototype.dismiss = function (service, notification) {
//Check this is a known notification
var index = service.notifications.indexOf(notification);
if (service.active.timeout) {
/* Method can be called manually (clicking dismiss) or
* automatically from an auto-timeout. this.active.timeout
* acts as a semaphore to prevent race conditions. Cancel any
* timeout in progress (for the case where a manual dismiss
* has shortcut an active auto-dismiss), and clear the
* semaphore.
*/
service.$timeout.cancel(service.active.timeout);
delete service.active.timeout;
}
if (index >= 0) {
service.notifications.splice(index, 1);
}
service.setActiveNotification(service.selectNextNotification());
this.setHighestSeverity();
};
/**
* Depending on the severity of the notification will selectively
* dismiss or minimize where appropriate.
*
* @private
*/
NotificationService.prototype.dismissOrMinimize = function (notification) {
var model = notification.model;
if (model.severity === "info") {
if (model.autoDismiss === false) {
notification.minimize();
} else {
notification.dismiss();
}
} else {
notification.minimize();
}
};
/**
* Returns the notification that is currently visible in the banner area
* @returns {Notification}
*/
NotificationService.prototype.getActiveNotification = function () {
return this.active.notification;
};
/**
* A convenience method for info notifications. Notifications
* created via this method will be auto-dismissed after a default
* wait period unless explicitly forbidden by the caller through
* the {autoDismiss} property on the {NotificationModel}, in which
* case the notification will be minimized after the wait.
* @param {NotificationModel | string} message either a string for
* the title of the notification message, or a {@link NotificationModel}
* defining the options notification to display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
NotificationService.prototype.info = function (message) {
var notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "info";
return this.notify(notificationModel);
};
/**
* A convenience method for alert notifications. Notifications
* created via this method will will have severity of "alert" enforced
* @param {NotificationModel | string} message either a string for
* the title of the alert message with default options, or a
* {@link NotificationModel} defining the options notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
NotificationService.prototype.alert = function (message) {
var notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "alert";
return this.notify(notificationModel);
};
/**
* A convenience method for error notifications. Notifications
* created via this method will will have severity of "error" enforced
* @param {NotificationModel | string} message either a string for
* the title of the error message with default options, or a
* {@link NotificationModel} defining the options notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
NotificationService.prototype.error = function (message) {
var notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "error";
return this.notify(notificationModel);
};
/**
* @private
*/
NotificationService.prototype.setHighestSeverity = function () {
var severity = {
"info": 1,
"alert": 2,
"error": 3
};
this.highest.severity = this.notifications.reduce(function (previous, notification) {
if (severity[notification.model.severity] > severity[previous]) {
return notification.model.severity;
} else {
return previous;
}
}, "info");
};
/**
* Notifies the user of an event. If there is a banner notification
* already active, then it will be dismissed or minimized automatically,
* and the provided notification displayed in its place.
*
* @param {NotificationModel} notificationModel The notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to {@link Notification#dismiss} or {@link Notification#minimize}
*/
NotificationService.prototype.notify = function (notificationModel) {
var self = this,
notification,
activeNotification = self.active.notification,
topic = this.topic();
notificationModel.severity = notificationModel.severity || "info";
notificationModel.timestamp = moment.utc().format('YYYY-MM-DD hh:mm:ss.ms');
notification = {
model: notificationModel,
minimize: function () {
self.minimize(self, notification);
},
dismiss: function () {
self.dismiss(self, notification);
topic.notify();
},
dismissOrMinimize: function () {
self.dismissOrMinimize(notification);
},
onDismiss: function (callback) {
topic.listen(callback);
}
};
//Notifications support a 'dismissable' attribute. This is a
// convenience to support adding a 'dismiss' option to the
// notification for the common case of dismissing a
// notification. Could also be done manually by specifying an
// option on the model
if (notificationModel.dismissable !== false) {
notificationModel.options = notificationModel.options || [];
notificationModel.options.unshift({
label: "Dismiss",
callback: function () {
notification.dismiss();
}
});
}
this.notifications.push(notification);
this.setHighestSeverity();
/*
Check if there is already an active (ie. visible) notification
*/
if (!this.active.notification) {
this.setActiveNotification(notification);
} else if (!this.active.timeout) {
/*
If there is already an active notification, time it out. If it's
already got a timeout in progress (either because it has had
timeout forced because of a queue of messages, or it had an
autodismiss specified), leave it to run. Otherwise force a
timeout.
This notification has been added to queue and will be
serviced as soon as possible.
*/
this.active.timeout = this.$timeout(function () {
activeNotification.dismissOrMinimize();
}, this.AUTO_DISMISS_TIMEOUT);
}
return notification;
};
/**
* Used internally by the NotificationService
* @private
*/
NotificationService.prototype.setActiveNotification = function (notification) {
var shouldAutoDismiss;
this.active.notification = notification;
if (!notification) {
delete this.active.timeout;
return;
}
if (notification.model.severity === "info") {
shouldAutoDismiss = true;
} else {
shouldAutoDismiss = notification.model.autoDismiss;
}
if (shouldAutoDismiss || this.selectNextNotification()) {
this.active.timeout = this.$timeout(function () {
notification.dismissOrMinimize();
}, this.AUTO_DISMISS_TIMEOUT);
} else {
delete this.active.timeout;
}
};
/**
* Used internally by the NotificationService
*
* @private
*/
NotificationService.prototype.selectNextNotification = function () {
var notification,
i = 0;
/*
Loop through the notifications queue and find the first one that
has not already been minimized (manually or otherwise).
*/
for (; i < this.notifications.length; i++) {
notification = this.notifications[i];
if (!notification.model.minimized &&
notification !== this.active.notification) {
return notification;
}
}
};
return NotificationService;
}
);

View File

@ -0,0 +1,275 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global describe,it,expect,beforeEach,jasmine*/
define(
['../src/NotificationService'],
function (NotificationService) {
describe("The notification service ", function () {
var notificationService,
mockTimeout,
mockAutoDismiss,
mockMinimizeTimeout,
mockTopicFunction,
mockTopicObject,
infoModel,
alertModel,
errorModel;
function elapseTimeout() {
mockTimeout.calls.mostRecent().args[0]();
}
beforeEach(function () {
mockTimeout = jasmine.createSpy("$timeout");
mockTopicFunction = jasmine.createSpy("topic");
mockTopicObject = jasmine.createSpyObj("topicObject", ["listen", "notify"]);
mockTopicFunction.and.returnValue(mockTopicObject);
mockAutoDismiss = mockMinimizeTimeout = 1000;
notificationService = new NotificationService(mockTimeout, mockTopicFunction, mockAutoDismiss, mockMinimizeTimeout);
infoModel = {
title: "Mock Info Notification",
severity: "info"
};
alertModel = {
title: "Mock Alert Notification",
severity: "alert"
};
errorModel = {
title: "Mock Error Notification",
severity: "error"
};
});
it("notifies listeners on dismissal of notification", function () {
var dismissListener = jasmine.createSpy("ondismiss");
var notification = notificationService.notify(infoModel);
notification.onDismiss(dismissListener);
expect(mockTopicObject.listen).toHaveBeenCalled();
notification.dismiss();
expect(mockTopicObject.notify).toHaveBeenCalled();
mockTopicObject.listen.calls.mostRecent().args[0]();
expect(dismissListener).toHaveBeenCalled();
});
it("dismisses a notification when the notification's dismiss method is used", function () {
var notification = notificationService.info(infoModel);
notification.dismiss();
expect(notificationService.getActiveNotification()).toBeUndefined();
expect(notificationService.notifications.length).toEqual(0);
});
it("minimizes a notification when the notification's minimize method is used", function () {
var notification = notificationService.info(infoModel);
notification.minimize();
elapseTimeout(); // needed for the minimize animation timeout
expect(notificationService.getActiveNotification()).toBeUndefined();
expect(notificationService.notifications.length).toEqual(1);
expect(notificationService.notifications[0]).toEqual(notification);
});
describe("when receiving info notifications", function () {
it("minimizes info notifications if the caller disables auto-dismiss", function () {
infoModel.autoDismiss = false;
var notification = notificationService.info(infoModel);
elapseTimeout();
// 2nd elapse for the minimize animation timeout
elapseTimeout();
expect(notificationService.getActiveNotification()).toBeUndefined();
expect(notificationService.notifications.length).toEqual(1);
expect(notificationService.notifications[0]).toEqual(notification);
});
it("dismisses info notifications if the caller ignores auto-dismiss", function () {
notificationService.info(infoModel);
elapseTimeout();
expect(notificationService.getActiveNotification()).toBeUndefined();
expect(notificationService.notifications.length).toEqual(0);
});
it("dismisses info notifications if the caller requests auto-dismiss", function () {
infoModel.autoDismiss = true;
notificationService.info(infoModel);
elapseTimeout();
expect(notificationService.getActiveNotification()).toBeUndefined();
expect(notificationService.notifications.length).toEqual(0);
});
});
describe("when receiving alert notifications", function () {
it("minimizes alert notifications if the caller enables auto-dismiss", function () {
alertModel.autoDismiss = true;
var notification = notificationService.alert(alertModel);
elapseTimeout();
elapseTimeout();
expect(notificationService.getActiveNotification()).toBeUndefined();
expect(notificationService.notifications.length).toEqual(1);
expect(notificationService.notifications[0]).toEqual(notification);
});
it("keeps alert notifications active if the caller disables auto-dismiss", function () {
mockTimeout.and.callFake(function (callback, time) {
callback();
});
alertModel.autoDismiss = false;
var notification = notificationService.alert(alertModel);
expect(notificationService.getActiveNotification()).toEqual(notification);
expect(notificationService.notifications.length).toEqual(1);
expect(notificationService.notifications[0]).toEqual(notification);
});
it("keeps alert notifications active if the caller ignores auto-dismiss", function () {
mockTimeout.and.callFake(function (callback, time) {
callback();
});
var notification = notificationService.alert(alertModel);
expect(notificationService.getActiveNotification()).toEqual(notification);
expect(notificationService.notifications.length).toEqual(1);
expect(notificationService.notifications[0]).toEqual(notification);
});
});
describe("when receiving error notifications", function () {
it("minimizes error notifications if the caller enables auto-dismiss", function () {
errorModel.autoDismiss = true;
var notification = notificationService.error(errorModel);
elapseTimeout();
elapseTimeout();
expect(notificationService.getActiveNotification()).toBeUndefined();
expect(notificationService.notifications.length).toEqual(1);
expect(notificationService.notifications[0]).toEqual(notification);
});
it("keeps error notifications active if the caller disables auto-dismiss", function () {
mockTimeout.and.callFake(function (callback, time) {
callback();
});
errorModel.autoDismiss = false;
var notification = notificationService.error(errorModel);
expect(notificationService.getActiveNotification()).toEqual(notification);
expect(notificationService.notifications.length).toEqual(1);
expect(notificationService.notifications[0]).toEqual(notification);
});
it("keeps error notifications active if the caller ignores auto-dismiss", function () {
mockTimeout.and.callFake(function (callback, time) {
callback();
});
var notification = notificationService.error(errorModel);
expect(notificationService.getActiveNotification()).toEqual(notification);
expect(notificationService.notifications.length).toEqual(1);
expect(notificationService.notifications[0]).toEqual(notification);
});
});
describe("when called with multiple notifications", function () {
it("auto-dismisses the previously active notification, making the new notification active", function () {
var activeNotification;
infoModel.autoDismiss = false;
//First pre-load with a info message
notificationService.notify(infoModel);
activeNotification = notificationService.getActiveNotification();
//Initially expect the active notification to be info
expect(activeNotification.model).toBe(infoModel);
//Then notify of an error
notificationService.notify(errorModel);
//But it should be auto-dismissed and replaced with the
// error notification
elapseTimeout();
//Two timeouts, one is to force minimization after
// displaying the message for a minimum period, the
// second is to allow minimization animation to take place.
elapseTimeout();
activeNotification = notificationService.getActiveNotification();
expect(activeNotification.model).toBe(errorModel);
});
it("auto-minimizes an active error notification", function () {
var activeNotification;
//First pre-load with an error message
notificationService.notify(errorModel);
//Then notify of info
notificationService.notify(infoModel);
expect(notificationService.notifications.length).toEqual(2);
//Mock the auto-minimize
elapseTimeout();
//Two timeouts, one is to force minimization after
// displaying the message for a minimum period, the
// second is to allow minimization animation to take place.
elapseTimeout();
//Previous error message should be minimized, not
// dismissed
expect(notificationService.notifications.length).toEqual(2);
activeNotification = notificationService.getActiveNotification();
expect(activeNotification.model).toBe(infoModel);
expect(errorModel.minimized).toEqual(true);
});
it("auto-minimizes errors when a number of them arrive in short succession", function () {
var activeNotification,
error2 = {
title: "Second Mock Error Notification",
severity: "error"
},
error3 = {
title: "Third Mock Error Notification",
severity: "error"
};
//First pre-load with a info message
notificationService.notify(errorModel);
//Then notify of a third error
notificationService.notify(error2);
notificationService.notify(error3);
expect(notificationService.notifications.length).toEqual(3);
//Mock the auto-minimize
elapseTimeout();
//Two timeouts, one is to force minimization after
// displaying the message for a minimum period, the
// second is to allow minimization animation to take place.
elapseTimeout();
//Previous error message should be minimized, not
// dismissed
expect(notificationService.notifications.length).toEqual(3);
activeNotification = notificationService.getActiveNotification();
expect(activeNotification.model).toBe(error2);
expect(errorModel.minimized).toEqual(true);
//Mock the second auto-minimize
elapseTimeout();
//Two timeouts, one is to force minimization after
// displaying the message for a minimum period, the
// second is to allow minimization animation to take place.
elapseTimeout();
activeNotification = notificationService.getActiveNotification();
expect(activeNotification.model).toBe(error3);
expect(error2.minimized).toEqual(true);
});
});
});
}
);

View File

@ -0,0 +1,43 @@
/*****************************************************************************
* 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([
"./src/ConductorRepresenter",
'legacyRegistry'
], function (
ConductorRepresenter,
legacyRegistry
) {
legacyRegistry.register("platform/features/conductor/compatibility", {
"extensions": {
"representers": [
{
"implementation": ConductorRepresenter,
"depends": [
"openmct"
]
}
]
}
});
});

View File

@ -0,0 +1,95 @@
/*****************************************************************************
* 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(
[],
function () {
/**
* Representer that provides a compatibility layer between the new
* time conductor and existing representations / views. Listens to
* the v2 time conductor API and generates v1 style events using the
* Angular event bus. This is transitional code code and will be
* removed.
*
* Deprecated immediately as this is temporary code
*
* @deprecated
* @constructor
*/
function ConductorRepresenter(
openmct,
scope,
element
) {
this.timeAPI = openmct.time;
this.scope = scope;
this.element = element;
this.boundsListener = this.boundsListener.bind(this);
this.timeSystemListener = this.timeSystemListener.bind(this);
this.followListener = this.followListener.bind(this);
}
ConductorRepresenter.prototype.boundsListener = function (bounds) {
var timeSystem = this.timeAPI.timeSystem();
this.scope.$broadcast('telemetry:display:bounds', {
start: bounds.start,
end: bounds.end,
domain: timeSystem.key
}, this.timeAPI.clock() !== undefined);
};
ConductorRepresenter.prototype.timeSystemListener = function (timeSystem) {
var bounds = this.timeAPI.bounds();
this.scope.$broadcast('telemetry:display:bounds', {
start: bounds.start,
end: bounds.end,
domain: timeSystem.key
}, this.timeAPI.clock() !== undefined);
};
ConductorRepresenter.prototype.followListener = function () {
this.boundsListener(this.timeAPI.bounds());
};
// Handle a specific representation of a specific domain object
ConductorRepresenter.prototype.represent = function represent(representation) {
if (representation.key === 'browse-object') {
this.destroy();
this.timeAPI.on("bounds", this.boundsListener);
this.timeAPI.on("timeSystem", this.timeSystemListener);
this.timeAPI.on("follow", this.followListener);
}
};
ConductorRepresenter.prototype.destroy = function destroy() {
this.timeAPI.off("bounds", this.boundsListener);
this.timeAPI.off("timeSystem", this.timeSystemListener);
this.timeAPI.off("follow", this.followListener);
};
return ConductorRepresenter;
}
);

View File

@ -0,0 +1,148 @@
/*****************************************************************************
* 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([
"./src/ui/TimeConductorController",
"./src/ui/ConductorAxisController",
"./src/ui/ConductorTOIController",
"./src/ui/ConductorTOIDirective",
"./src/ui/TimeOfInterestController",
"./src/ui/ConductorAxisDirective",
"./src/ui/NumberFormat",
"./src/ui/StringFormat",
"./res/templates/time-conductor.html",
"./res/templates/mode-selector/mode-selector.html",
"./res/templates/mode-selector/mode-menu.html",
"./res/templates/time-of-interest.html",
"legacyRegistry"
], function (
TimeConductorController,
ConductorAxisController,
ConductorTOIController,
ConductorTOIDirective,
TimeOfInterestController,
ConductorAxisDirective,
NumberFormat,
StringFormat,
timeConductorTemplate,
modeSelectorTemplate,
modeMenuTemplate,
timeOfInterest,
legacyRegistry
) {
legacyRegistry.register("platform/features/conductor/core", {
"extensions": {
"controllers": [
{
"key": "TimeConductorController",
"implementation": TimeConductorController,
"depends": [
"$scope",
"$window",
"openmct",
"formatService",
"CONDUCTOR_CONFIG"
]
},
{
"key": "ConductorTOIController",
"implementation": ConductorTOIController,
"depends": [
"$scope",
"openmct",
"formatService"
]
},
{
"key": "TimeOfInterestController",
"implementation": TimeOfInterestController,
"depends": [
"$scope",
"openmct",
"formatService"
]
}
],
"directives": [
{
"key": "conductorAxis",
"implementation": ConductorAxisDirective,
"depends": [
"openmct",
"formatService"
]
},
{
"key": "conductorToi",
"implementation": ConductorTOIDirective
}
],
"templates": [
{
"key": "conductor",
"template": timeConductorTemplate
},
{
"key": "mode-menu",
"template": modeMenuTemplate
},
{
"key": "mode-selector",
"template": modeSelectorTemplate
},
{
"key": "time-of-interest",
"template": timeOfInterest
}
],
"representations": [
{
"key": "time-conductor",
"template": timeConductorTemplate
}
],
"licenses": [
{
"name": "D3: Data-Driven Documents",
"version": "4.1.0",
"author": "Mike Bostock",
"description": "D3 (or D3.js) is a JavaScript library for visualizing data using web standards. D3 helps you bring data to life using SVG, Canvas and HTML. D3 combines powerful visualization and interaction techniques with a data-driven approach to DOM manipulation, giving you the full capabilities of modern browsers and the freedom to design the right visual interface for your data.",
"website": "https://d3js.org/",
"copyright": "Copyright 2010-2016 Mike Bostock",
"license": "BSD-3-Clause",
"link": "https://github.com/d3/d3/blob/master/LICENSE"
}
],
"formats": [
{
"key": "number",
"implementation": NumberFormat
},
{
"key": "string",
"implementation": StringFormat
}
]
}
});
});

View File

@ -0,0 +1,46 @@
<!--
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.
-->
<div class="w-menu">
<div class="col menu-items">
<ul>
<li ng-repeat="metadata in ngModel.options"
ng-click="ngModel.select(metadata)">
<a ng-mouseover="ngModel.activeMetadata = metadata"
ng-mouseleave="ngModel.activeMetadata = undefined"
class="menu-item-a {{metadata.cssClass}}">
{{metadata.name}}
</a>
</li>
</ul>
</div>
<div class="col menu-item-description">
<div class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssClass}}"></div>
<div class="w-title-desc">
<div class="desc-area title">
{{ngModel.activeMetadata.name}}
</div>
<div class="desc-area description">
{{ngModel.activeMetadata.description}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,33 @@
<!--
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.
-->
<span ng-controller="ClickAwayController as modeController">
<div class="s-menu-button"
ng-click="modeController.toggle()">
<span class="title-label">{{ngModel.selected.name}}</span>
</div>
<div class="menu super-menu mini l-mode-selector-menu"
ng-show="modeController.isActive()">
<mct-include key="'mode-menu'"
ng-model="ngModel">
</mct-include>
</div>
</span>

View File

@ -0,0 +1,117 @@
<!-- Parent holder for time conductor. follow-mode | fixed-mode -->
<div ng-controller="TimeConductorController as tcController"
class="holder grows flex-elem l-flex-row l-time-conductor {{tcController.isFixed ? 'fixed-mode' : 'realtime-mode'}} {{timeSystemModel.selected.metadata.key}}-time-system"
ng-class="{'status-panning': tcController.panning}">
<div class="flex-elem holder time-conductor-icon">
<div class="hand-little"></div>
<div class="hand-big"></div>
</div>
<div class="flex-elem holder grows l-flex-col l-time-conductor-inner">
<!-- Holds inputs and ticks -->
<div class="l-time-conductor-inputs-and-ticks l-row-elem flex-elem no-margin">
<form class="l-time-conductor-inputs-holder"
ng-submit="tcController.isFixed ? tcController.setBoundsFromView(boundsModel) : tcController.setOffsetsFromView(boundsModel)">
<span class="l-time-range-w start-w">
<span class="l-time-conductor-inputs">
<span class="l-time-range-input-w start-date">
<span class="title"></span>
<mct-control key="'datetime-field'"
structure="{
format: timeSystemModel.format,
validate: tcController.validation.validateStart
}"
ng-model="boundsModel"
ng-blur="tcController.setBoundsFromView(boundsModel)"
field="'start'"
class="time-range-input">
</mct-control>
</span>
<span class="l-time-range-input-w time-delta start-delta"
ng-class="{'hide':tcController.isFixed}">
-
<mct-control key="'datetime-field'"
structure="{
format: timeSystemModel.durationFormat,
validate: tcController.validation.validateStartOffset
}"
ng-model="boundsModel"
ng-blur="tcController.setOffsetsFromView(boundsModel)"
field="'startOffset'"
class="s-input-inline hrs-min-input">
</mct-control>
</span>
</span>
</span>
<span class="l-time-range-w end-w">
<span class="l-time-conductor-inputs">
<span class="l-time-range-input-w end-date"
ng-controller="ToggleController as t2">
<span class="title"></span>
<mct-control key="'datetime-field'"
structure="{
format: timeSystemModel.format,
validate: tcController.validation.validateEnd
}"
ng-model="boundsModel"
ng-blur="tcController.setBoundsFromView(boundsModel)"
ng-disabled="!tcController.isFixed"
field="'end'"
class="time-range-input">
</mct-control>
</span>
<span class="l-time-range-input-w time-delta end-delta"
ng-class="{'hide': tcController.isFixed}">
+
<mct-control key="'datetime-field'"
structure="{
format: timeSystemModel.durationFormat,
validate: tcController.validation.validateEndOffset
}"
ng-model="boundsModel"
ng-blur="tcController.setOffsetsFromView(boundsModel)"
field="'endOffset'"
class="s-input-inline hrs-min-input">
</mct-control>
</span>
</span>
</span>
<input type="submit" class="invisible">
</form>
<conductor-axis class="mobile-hide" view-service="tcController.conductorViewService"></conductor-axis>
</div>
<!-- Holds time system and session selectors, and zoom control -->
<div class="l-time-conductor-controls l-row-elem l-flex-row flex-elem">
<mct-include
key="'mode-selector'"
ng-model="tcController.menu"
class="holder flex-elem menus-up mode-selector">
</mct-include>
<mct-control
key="'menu-button'"
class="holder flex-elem menus-up time-system"
structure="{
text: timeSystemModel.selected.name,
click: tcController.setTimeSystemFromView,
options: tcController.timeSystemsForClocks[tcController.menu.selected.key]
}">
</mct-control>
<!-- Zoom control -->
<div ng-if="tcController.zoom"
class="l-time-conductor-zoom-w grows flex-elem l-flex-row">
{{currentZoom}}
<span class="time-conductor-zoom-current-range flex-elem flex-fixed holder">{{timeUnits}}</span>
<input class="time-conductor-zoom flex-elem" type="range"
ng-model="tcController.currentZoom"
ng-mouseUp="tcController.onZoomStop(tcController.currentZoom)"
ng-change="tcController.onZoom(tcController.currentZoom)"
min="0.01"
step="0.01"
max="0.99" />
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
<div class="abs angular-controller"
ng-controller="TimeOfInterestController as toi">
<div class="l-flex-row l-toi">
<span class="flex-elem l-flex-row l-toi-buttons">
<a class="flex-elem t-button-resync icon-button" title="Re-sync Time of Interest"
ng-click="toi.resync()"></a>
<a class="flex-elem t-button-unpin icon-button" title="Unset Time of Interest"
ng-click="toi.dismiss()"></a>
</span>
<span class="flex-elem l-toi-val">{{toi.toiText}}</span>
</div>
</div>

View File

@ -0,0 +1,236 @@
/*****************************************************************************
* 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(
[
"d3-selection",
"d3-scale",
"d3-axis"
],
function (d3Selection, d3Scale, d3Axis) {
var PADDING = 1;
/**
* Controller that renders a horizontal time scale spanning the current bounds defined in the time conductor.
* Used by the mct-conductor-axis directive
* @constructor
*/
function ConductorAxisController(openmct, formatService, scope, element) {
// Dependencies
this.formatService = formatService;
this.timeAPI = openmct.time;
this.scope = scope;
this.bounds = this.timeAPI.bounds();
//Bind all class functions to 'this'
Object.keys(ConductorAxisController.prototype).filter(function (key) {
return typeof ConductorAxisController.prototype[key] === 'function';
}).forEach(function (key) {
this[key] = ConductorAxisController.prototype[key].bind(this);
}.bind(this));
this.initialize(element);
}
/**
* @private
*/
ConductorAxisController.prototype.destroy = function () {
this.timeAPI.off('timeSystem', this.changeTimeSystem);
this.timeAPI.off('bounds', this.changeBounds);
this.viewService.off("zoom", this.onZoom);
this.viewService.off("zoom-stop", this.onZoomStop);
};
/**
* @private
*/
ConductorAxisController.prototype.initialize = function (element) {
this.target = element[0].firstChild;
var height = this.target.offsetHeight;
var vis = d3Selection.select(this.target)
.append("svg:svg")
.attr("width", "100%")
.attr("height", height);
this.xAxis = d3Axis.axisTop();
// draw x axis with labels. CSS is used to position them.
this.axisElement = vis.append("g");
if (this.timeAPI.timeSystem() !== undefined) {
this.changeTimeSystem(this.timeAPI.timeSystem());
this.setScale();
}
//Respond to changes in conductor
this.timeAPI.on("timeSystem", this.changeTimeSystem);
this.timeAPI.on("bounds", this.changeBounds);
this.scope.$on("$destroy", this.destroy);
this.viewService.on("zoom", this.onZoom);
this.viewService.on("zoom-stop", this.onZoomStop);
};
/**
* @private
*/
ConductorAxisController.prototype.changeBounds = function (bounds) {
this.bounds = bounds;
if (!this.zooming) {
this.setScale();
}
};
/**
* Set the scale of the axis, based on current conductor bounds.
*/
ConductorAxisController.prototype.setScale = function () {
var width = this.target.offsetWidth;
var timeSystem = this.timeAPI.timeSystem();
var bounds = this.bounds;
if (timeSystem.isUTCBased) {
this.xScale = this.xScale || d3Scale.scaleUtc();
this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]);
} else {
this.xScale = this.xScale || d3Scale.scaleLinear();
this.xScale.domain([bounds.start, bounds.end]);
}
this.xAxis.scale(this.xScale);
this.xScale.range([PADDING, width - PADDING * 2]);
this.axisElement.call(this.xAxis);
this.msPerPixel = (bounds.end - bounds.start) / width;
};
/**
* When the time system changes, update the scale and formatter used for showing times.
* @param timeSystem
*/
ConductorAxisController.prototype.changeTimeSystem = function (timeSystem) {
var key = timeSystem.timeFormat;
if (key !== undefined) {
var format = this.formatService.getFormat(key);
var bounds = this.timeAPI.bounds();
//The D3 scale used depends on the type of time system as d3
// supports UTC out of the box.
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
} else {
this.xScale = d3Scale.scaleLinear();
}
this.xAxis.scale(this.xScale);
//Define a custom format function
this.xAxis.tickFormat(function (tickValue) {
// Normalize date representations to numbers
if (tickValue instanceof Date) {
tickValue = tickValue.getTime();
}
return format.format(tickValue, {
min: bounds.start,
max: bounds.end
});
});
this.axisElement.call(this.xAxis);
}
};
/**
* The user has stopped panning the time conductor scale element.
* @event panStop
*/
/**
* Called on release of mouse button after dragging the scale left or right.
* @fires platform.features.conductor.ConductorAxisController~panStop
*/
ConductorAxisController.prototype.panStop = function () {
//resync view bounds with time conductor bounds
this.viewService.emit("pan-stop");
this.timeAPI.bounds(this.bounds);
};
/**
* Rescales the axis when the user zooms. Although zoom ultimately results in a bounds change once the user
* releases the zoom slider, dragging the slider will not immediately change the conductor bounds. It will
* however immediately update the scale and the bounds displayed in the UI.
* @private
* @param {ZoomLevel}
*/
ConductorAxisController.prototype.onZoom = function (zoom) {
this.zooming = true;
this.bounds = zoom.bounds;
this.setScale();
};
/**
* @private
*/
ConductorAxisController.prototype.onZoomStop = function (zoom) {
this.zooming = false;
};
/**
* @event platform.features.conductor.ConductorAxisController~pan
* Fired when the time conductor is panned
*/
/**
* Initiate panning via a click + drag gesture on the time conductor
* scale. Panning triggers a "pan" event
* @param {number} delta the offset from the original click event
* @see TimeConductorViewService#
* @fires platform.features.conductor.ConductorAxisController~pan
*/
ConductorAxisController.prototype.pan = function (delta) {
if (this.timeAPI.clock() === undefined) {
var deltaInMs = delta[0] * this.msPerPixel;
var bounds = this.timeAPI.bounds();
var start = Math.floor((bounds.start - deltaInMs) / 1000) * 1000;
var end = Math.floor((bounds.end - deltaInMs) / 1000) * 1000;
this.bounds = {
start: start,
end: end
};
this.setScale();
this.viewService.emit("pan", this.bounds);
}
};
/**
* Invoked on element resize. Will rebuild the scale based on the new dimensions of the element.
*/
ConductorAxisController.prototype.resize = function () {
this.setScale();
};
return ConductorAxisController;
}
);

View File

@ -0,0 +1,169 @@
/*****************************************************************************
* 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([
'./ConductorAxisController',
'zepto',
'd3-selection',
'd3-scale'
], function (
ConductorAxisController,
$,
d3Selection,
d3Scale
) {
describe("The ConductorAxisController", function () {
var controller,
mockConductor,
mockConductorViewService,
mockFormatService,
mockScope,
mockBounds,
element,
mockTimeSystem,
mockFormat;
function getCallback(target, name) {
return target.calls.all().filter(function (call) {
return call.args[0] === name;
})[0].args[1];
}
beforeEach(function () {
mockScope = jasmine.createSpyObj("scope", [
"$on"
]);
//Add some HTML elements
mockBounds = {
start: 100,
end: 200
};
mockConductor = jasmine.createSpyObj("conductor", [
"timeSystem",
"bounds",
"on",
"off",
"clock"
]);
mockConductor.bounds.and.returnValue(mockBounds);
mockFormatService = jasmine.createSpyObj("formatService", [
"getFormat"
]);
mockConductorViewService = jasmine.createSpyObj("conductorViewService", [
"on",
"off",
"emit"
]);
spyOn(d3Scale, 'scaleUtc').and.callThrough();
spyOn(d3Scale, 'scaleLinear').and.callThrough();
element = $('<div style="width: 100px;"><div style="width: 100%;"></div></div>');
$(document).find('body').append(element);
ConductorAxisController.prototype.viewService = mockConductorViewService;
controller = new ConductorAxisController({time: mockConductor}, mockFormatService, mockScope, element);
mockTimeSystem = {};
mockFormat = jasmine.createSpyObj("format", [
"format"
]);
mockTimeSystem.timeFormat = "mockFormat";
mockFormatService.getFormat.and.returnValue(mockFormat);
mockConductor.timeSystem.and.returnValue(mockTimeSystem);
mockTimeSystem.isUTCBased = false;
});
it("listens for changes to time system and bounds", function () {
expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
expect(mockConductor.on).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
it("on scope destruction, deregisters listeners", function () {
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", controller.destroy);
controller.destroy();
expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
expect(mockConductor.off).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
describe("when the time system changes", function () {
it("uses a UTC scale for UTC time systems", function () {
mockTimeSystem.isUTCBased = true;
controller.changeTimeSystem(mockTimeSystem);
expect(d3Scale.scaleUtc).toHaveBeenCalled();
expect(d3Scale.scaleLinear).not.toHaveBeenCalled();
});
it("uses a linear scale for non-UTC time systems", function () {
mockTimeSystem.isUTCBased = false;
controller.changeTimeSystem(mockTimeSystem);
expect(d3Scale.scaleLinear).toHaveBeenCalled();
expect(d3Scale.scaleUtc).not.toHaveBeenCalled();
});
it("sets axis domain to time conductor bounds", function () {
mockTimeSystem.isUTCBased = false;
controller.setScale();
expect(controller.xScale.domain()).toEqual([mockBounds.start, mockBounds.end]);
});
it("uses the format specified by the time system to format tick" +
" labels", function () {
controller.changeTimeSystem(mockTimeSystem);
expect(mockFormat.format).toHaveBeenCalled();
});
it('responds to zoom events', function () {
expect(mockConductorViewService.on).toHaveBeenCalledWith("zoom", controller.onZoom);
var cb = getCallback(mockConductorViewService.on, "zoom");
spyOn(controller, 'setScale').and.callThrough();
cb({bounds: {start: 0, end: 100}});
expect(controller.setScale).toHaveBeenCalled();
});
it('adjusts scale on pan', function () {
spyOn(controller, 'setScale').and.callThrough();
controller.pan(100);
expect(controller.setScale).toHaveBeenCalled();
});
it('emits event on pan', function () {
spyOn(controller, 'setScale').and.callThrough();
controller.pan(100);
expect(mockConductorViewService.emit).toHaveBeenCalledWith("pan", jasmine.any(Object));
});
it('cleans up listeners on destruction', function () {
controller.destroy();
expect(mockConductor.off).toHaveBeenCalledWith("bounds", controller.changeBounds);
expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
expect(mockConductorViewService.off).toHaveBeenCalledWith("zoom", controller.onZoom);
});
});
});
});

View File

@ -0,0 +1,56 @@
/*****************************************************************************
* 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(['./ConductorAxisController'], function (ConductorAxisController) {
function ConductorAxisDirective() {
/**
* The mct-conductor-axis renders a horizontal axis with regular
* labelled 'ticks'. It requires 'start' and 'end' integer values to
* be specified as attributes.
*/
return {
controller: [
'openmct',
'formatService',
'$scope',
'$element',
ConductorAxisController
],
controllerAs: 'axis',
scope: {
viewService: "="
},
bindToController: true,
restrict: 'E',
priority: 1000,
template: '<div class="l-axis-holder" ' +
' mct-drag-down="axis.panStart()"' +
' mct-drag-up="axis.panStop(delta)"' +
' mct-drag="axis.pan(delta)"' +
' mct-resize="axis.resize()"></div>'
};
}
return ConductorAxisDirective;
});

View File

@ -0,0 +1,123 @@
/*****************************************************************************
* 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(
["zepto"],
function ($) {
/**
* Controller for the Time of Interest indicator in the conductor itself. Sets the horizontal position of the
* TOI indicator based on the current value of the TOI, and the width of the TOI conductor.
* @memberof platform.features.conductor
*/
function ConductorTOIController($scope, openmct) {
this.timeAPI = openmct.time;
//Bind all class functions to 'this'
Object.keys(ConductorTOIController.prototype).filter(function (key) {
return typeof ConductorTOIController.prototype[key] === 'function';
}).forEach(function (key) {
this[key] = ConductorTOIController.prototype[key].bind(this);
}.bind(this));
this.timeAPI.on('timeOfInterest', this.changeTimeOfInterest);
this.viewService.on('zoom', this.setOffsetFromZoom);
this.viewService.on('pan', this.setOffsetFromBounds);
var timeOfInterest = this.timeAPI.timeOfInterest();
if (timeOfInterest) {
this.changeTimeOfInterest(timeOfInterest);
}
$scope.$on('$destroy', this.destroy);
}
/**
* @private
*/
ConductorTOIController.prototype.destroy = function () {
this.timeAPI.off('timeOfInterest', this.changeTimeOfInterest);
this.viewService.off('zoom', this.setOffsetFromZoom);
this.viewService.off('pan', this.setOffsetFromBounds);
};
/**
* Given some bounds, set horizontal position of TOI indicator based
* on current conductor TOI value. Bounds are provided so that
* ephemeral bounds from zoom and pan events can be used as well
* as current conductor bounds, allowing TOI to be updated in
* realtime during scroll and zoom.
* @param {TimeConductorBounds} bounds
*/
ConductorTOIController.prototype.setOffsetFromBounds = function (bounds) {
var toi = this.timeAPI.timeOfInterest();
if (toi !== undefined) {
var offset = toi - bounds.start;
var duration = bounds.end - bounds.start;
this.left = offset / duration * 100;
this.pinned = true;
} else {
this.left = 0;
this.pinned = false;
}
};
/**
* @private
*/
ConductorTOIController.prototype.setOffsetFromZoom = function (zoom) {
return this.setOffsetFromBounds(zoom.bounds);
};
/**
* Invoked when time of interest changes. Will set the horizontal offset of the TOI indicator.
* @private
*/
ConductorTOIController.prototype.changeTimeOfInterest = function () {
var bounds = this.timeAPI.bounds();
if (bounds) {
this.setOffsetFromBounds(bounds);
}
};
/**
* On a mouse click event within the TOI element, convert position within element to a time of interest, and
* set the time of interest on the conductor.
* @param e The angular $event object
*/
ConductorTOIController.prototype.setTOIFromPosition = function (e) {
//TOI is set using the alt key modified + primary click
if (e.altKey) {
var element = $(e.currentTarget);
var width = element.width();
var relativeX = e.pageX - element.offset().left;
var percX = relativeX / width;
var bounds = this.timeAPI.bounds();
var timeRange = bounds.end - bounds.start;
this.timeAPI.timeOfInterest(timeRange * percX + bounds.start);
}
};
return ConductorTOIController;
}
);

View File

@ -0,0 +1,153 @@
/*****************************************************************************
* 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([
'./ConductorTOIController'
], function (
ConductorTOIController
) {
var mockConductor;
var mockConductorViewService;
var mockScope;
var mockAPI;
var conductorTOIController;
function getNamedCallback(thing, name) {
return thing.calls.all().filter(function (call) {
return call.args[0] === name;
}).map(function (call) {
return call.args;
})[0][1];
}
describe("The ConductorTOIController", function () {
beforeEach(function () {
mockConductor = jasmine.createSpyObj("conductor", [
"bounds",
"timeOfInterest",
"on",
"off"
]);
mockAPI = {time: mockConductor};
mockConductorViewService = jasmine.createSpyObj("conductorViewService", [
"on",
"off"
]);
mockScope = jasmine.createSpyObj("openMCT", [
"$on"
]);
ConductorTOIController.prototype.viewService = mockConductorViewService;
conductorTOIController = new ConductorTOIController(mockScope, mockAPI);
});
it("listens to changes in the time of interest on the conductor", function () {
expect(mockConductor.on).toHaveBeenCalledWith("timeOfInterest", jasmine.any(Function));
});
describe("when responding to changes in the time of interest", function () {
var toiCallback;
beforeEach(function () {
var bounds = {
start: 0,
end: 200
};
mockConductor.bounds.and.returnValue(bounds);
toiCallback = getNamedCallback(mockConductor.on, "timeOfInterest");
});
it("calculates the correct horizontal offset based on bounds and current TOI", function () {
//Expect time of interest position to be 50% of element width
mockConductor.timeOfInterest.and.returnValue(100);
toiCallback();
expect(conductorTOIController.left).toBe(50);
//Expect time of interest position to be 25% of element width
mockConductor.timeOfInterest.and.returnValue(50);
toiCallback();
expect(conductorTOIController.left).toBe(25);
//Expect time of interest position to be 0% of element width
mockConductor.timeOfInterest.and.returnValue(0);
toiCallback();
expect(conductorTOIController.left).toBe(0);
//Expect time of interest position to be 100% of element width
mockConductor.timeOfInterest.and.returnValue(200);
toiCallback();
expect(conductorTOIController.left).toBe(100);
});
it("renders the TOI indicator visible", function () {
expect(conductorTOIController.pinned).toBeFalsy();
mockConductor.timeOfInterest.and.returnValue(100);
toiCallback();
expect(conductorTOIController.pinned).toBe(true);
});
});
it("responds to zoom events", function () {
var mockZoom = {
bounds: {
start: 500,
end: 1000
}
};
expect(mockConductorViewService.on).toHaveBeenCalledWith("zoom", jasmine.any(Function));
// Should correspond to horizontal offset of 50%
mockConductor.timeOfInterest.and.returnValue(750);
var zoomCallback = getNamedCallback(mockConductorViewService.on, "zoom");
zoomCallback(mockZoom);
expect(conductorTOIController.left).toBe(50);
});
it("responds to pan events", function () {
var mockPanBounds = {
start: 1000,
end: 3000
};
expect(mockConductorViewService.on).toHaveBeenCalledWith("pan", jasmine.any(Function));
// Should correspond to horizontal offset of 25%
mockConductor.timeOfInterest.and.returnValue(1500);
var panCallback = getNamedCallback(mockConductorViewService.on, "pan");
panCallback(mockPanBounds);
expect(conductorTOIController.left).toBe(25);
});
it("Cleans up all listeners when controller destroyed", function () {
var zoomCB = getNamedCallback(mockConductorViewService.on, "zoom");
var panCB = getNamedCallback(mockConductorViewService.on, "pan");
var toiCB = getNamedCallback(mockConductor.on, "timeOfInterest");
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", jasmine.any(Function));
getNamedCallback(mockScope.$on, "$destroy")();
expect(mockConductorViewService.off).toHaveBeenCalledWith("zoom", zoomCB);
expect(mockConductorViewService.off).toHaveBeenCalledWith("pan", panCB);
expect(mockConductor.off).toHaveBeenCalledWith("timeOfInterest", toiCB);
});
});
});

View File

@ -0,0 +1,63 @@
/*****************************************************************************
* 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(['./ConductorTOIController'], function (ConductorTOIController) {
/**
* A directive that encapsulates the TOI specific behavior of the Time Conductor UI.
* @constructor
*/
function ConductorTOIDirective() {
/**
* The mct-conductor-axis renders a horizontal axis with regular
* labelled 'ticks'. It requires 'start' and 'end' integer values to
* be specified as attributes.
*/
return {
controller: [
'$scope',
'openmct',
ConductorTOIController
],
controllerAs: 'toi',
scope: {
viewService: "="
},
bindToController: true,
restrict: 'E',
priority: 1000,
template:
'<div class="l-data-visualization-holder l-row-elem flex-elem">' +
' <a class="l-page-button s-icon-button icon-pointer-left"></a>' +
' <div class="l-data-visualization" ng-click="toi.setTOIFromPosition($event)">' +
' <mct-include key="\'time-of-interest\'" class="l-toi-holder show-val" ' +
' ng-class="{ pinned: toi.pinned, \'val-to-left\': toi.left > 80 }" ' +
' ng-style="{\'left\': toi.left + \'%\'}"></mct-include>' +
' </div>' +
' <a class="l-page-button align-right s-icon-button icon-pointer-right"></a>' +
'</div>'
};
}
return ConductorTOIDirective;
});

View File

@ -20,29 +20,35 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export default class MCTNotification extends EventEmitter {
define([], function () {
constructor(notificationModel, notificationAPI) {
super();
this.notifications = notificationAPI;
this.model = notificationModel;
this.initializeModel();
/**
* Formatter for basic numbers. Provides basic support for non-UTC
* numbering systems
*
* @implements {Format}
* @constructor
* @memberof platform/commonUI/formats
*/
function NumberFormat() {
this.key = 'number';
}
minimize() {
this.notifications.minimize(this);
}
NumberFormat.prototype.format = function (value) {
if (isNaN(value)) {
return '';
} else {
return '' + value;
}
};
dismiss() {
this.notifications.dismiss(this)
}
NumberFormat.prototype.parse = function (text) {
return parseFloat(text);
};
dismissOrMinimize() {
this.notifications.dismissOrMinimize(this);
}
NumberFormat.prototype.validate = function (text) {
return !isNaN(text);
};
initializeModel() {
this.model.minimized = this.model.minimized || false;
}
}
return NumberFormat;
});

View File

@ -0,0 +1,49 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./NumberFormat'], function (NumberFormat) {
describe("The NumberFormat class", function () {
var format;
beforeEach(function () {
format = new NumberFormat();
});
it("The format function takes a string and produces a number", function () {
var text = format.format(1);
expect(text).toBe("1");
expect(typeof text).toBe("string");
});
it("The parse function takes a string and produces a number", function () {
var number = format.parse("1");
expect(number).toBe(1);
expect(typeof number).toBe("number");
});
it("validates that the input is a number", function () {
expect(format.validate("1")).toBe(true);
expect(format.validate(1)).toBe(true);
expect(format.validate("1.1")).toBe(true);
expect(format.validate("abc")).toBe(false);
});
});
});

View File

@ -20,17 +20,34 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./FolderGridView',
'./FolderListView'
], function (
FolderGridView,
FolderListView
) {
return function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new FolderGridView(openmct));
openmct.objectViews.addProvider(new FolderListView(openmct));
};
define([], function () {
/**
* Formatter for basic strings.
*
* @implements {Format}
* @constructor
* @memberof platform/commonUI/formats
*/
function StringFormat() {
this.key = 'string';
}
StringFormat.prototype.format = function (string) {
if (typeof string === 'string') {
return string;
} else {
return '' + string;
}
};
StringFormat.prototype.parse = function (string) {
return string;
};
StringFormat.prototype.validate = function (string) {
return typeof string === 'string';
};
return StringFormat;
});

View File

@ -0,0 +1,554 @@
/*****************************************************************************
* 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(
[
'moment',
'./TimeConductorValidation',
'./TimeConductorViewService'
],
function (
moment,
TimeConductorValidation,
TimeConductorViewService
) {
var timeUnitsMegastructure = [
["Decades", function (r) {
return r.years() > 15;
}],
["Years", function (r) {
return r.years() > 1;
}],
["Months", function (r) {
return r.years() === 1 || r.months() > 1;
}],
["Days", function (r) {
return r.months() === 1 || r.days() > 1;
}],
["Hours", function (r) {
return r.days() === 1 || r.hours() > 1;
}],
["Minutes", function (r) {
return r.hours() === 1 || r.minutes() > 1;
}],
["Seconds", function (r) {
return r.minutes() === 1 || r.seconds() > 1;
}],
["Milliseconds", function (r) {
return true;
}]
];
/**
* Controller for the Time Conductor UI element. The Time Conductor
* includes form fields for specifying time bounds and relative time
* offsets for queries, as well as controls for selection mode,
* time systems, and zooming.
* @memberof platform.features.conductor
* @constructor
*/
function TimeConductorController(
$scope,
$window,
openmct,
formatService,
config
) {
//Bind functions that are used as callbacks to 'this'.
[
"selectMenuOption",
"onPan",
"onPanStop",
"setViewFromBounds",
"setViewFromClock",
"setViewFromOffsets",
"setViewFromTimeSystem",
"setTimeSystemFromView",
"destroy"
].forEach(function (name) {
this[name] = this[name].bind(this);
}.bind(this));
this.$scope = $scope;
this.$window = $window;
this.timeAPI = openmct.time;
this.conductorViewService = new TimeConductorViewService(openmct);
this.validation = new TimeConductorValidation(this.timeAPI);
this.formatService = formatService;
this.config = config;
this.timeSystemsForClocks = {};
this.$scope.timeSystemModel = {};
this.$scope.boundsModel = {};
this.timeSystems = this.timeAPI.getAllTimeSystems().reduce(function (map, timeSystem) {
map[timeSystem.key] = timeSystem;
return map;
}, {});
this.isFixed = this.timeAPI.clock() === undefined;
var options = this.optionsFromConfig(config);
this.menu = {
selected: undefined,
options: options,
select: this.selectMenuOption
};
//Set the initial state of the UI from the conductor state
var timeSystem = this.timeAPI.timeSystem();
if (timeSystem) {
this.setViewFromTimeSystem(timeSystem);
}
this.setViewFromClock(this.timeAPI.clock());
var offsets = this.timeAPI.clockOffsets();
if (offsets) {
this.setViewFromOffsets(offsets);
}
var bounds = this.timeAPI.bounds();
if (bounds && bounds.start !== undefined && bounds.end !== undefined) {
this.setViewFromBounds(bounds);
}
this.conductorViewService.on('pan', this.onPan);
this.conductorViewService.on('pan-stop', this.onPanStop);
//Respond to any subsequent conductor changes
this.timeAPI.on('bounds', this.setViewFromBounds);
this.timeAPI.on('timeSystem', this.setViewFromTimeSystem);
this.timeAPI.on('clock', this.setViewFromClock);
this.timeAPI.on('clockOffsets', this.setViewFromOffsets);
this.$scope.$on('$destroy', this.destroy);
}
/**
* Given a key for a clock, retrieve the clock object.
* @private
* @param key
* @returns {Clock}
*/
TimeConductorController.prototype.getClock = function (key) {
return this.timeAPI.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
};
/**
* Activate the selected menu option. Menu options correspond to clocks.
* A distinction is made to avoid confusion between the menu options and
* their metadata, and actual {@link Clock} objects.
*
* @private
* @param newOption
*/
TimeConductorController.prototype.selectMenuOption = function (newOption) {
if (this.menu.selected.key === newOption.key) {
return;
}
this.menu.selected = newOption;
var config = this.getConfig(this.timeAPI.timeSystem(), newOption.clock);
if (!config) {
// Clock does not support this timeSystem, fallback to first
// option provided for clock.
config = this.config.menuOptions.filter(function (menuOption) {
return menuOption.clock === (newOption.clock && newOption.clock.key);
})[0];
}
if (config.clock) {
this.timeAPI.clock(config.clock, config.clockOffsets);
this.timeAPI.timeSystem(config.timeSystem);
} else {
this.timeAPI.stopClock();
this.timeAPI.timeSystem(config.timeSystem, config.bounds);
}
};
/**
* From the provided configuration, build the available menu options.
* @private
* @param config
* @returns {*[]}
*/
TimeConductorController.prototype.optionsFromConfig = function (config) {
/*
* "Fixed Mode" is always the first available option.
*/
var options = [{
key: 'fixed',
name: 'Fixed Timespan Mode',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-calendar'
}];
var clocks = {};
var timeSystemsForClocks = this.timeSystemsForClocks;
(config.menuOptions || []).forEach(function (menuOption) {
var clockKey = menuOption.clock || 'fixed';
var clock = this.getClock(clockKey);
if (clock !== undefined) {
clocks[clock.key] = clock;
}
var timeSystem = this.timeSystems[menuOption.timeSystem];
if (timeSystem !== undefined) {
timeSystemsForClocks[clockKey] = timeSystemsForClocks[clockKey] || [];
timeSystemsForClocks[clockKey].push(timeSystem);
}
}, this);
/*
* Populate the clocks menu with metadata from the available clocks
*/
Object.values(clocks).forEach(function (clock) {
options.push({
key: clock.key,
name: clock.name,
description: "Monitor streaming data in real-time. The Time " +
"Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
clock: clock
});
}.bind(this));
return options;
};
/**
* When bounds change, set UI values from the new bounds.
* @param {TimeBounds} bounds the bounds
*/
TimeConductorController.prototype.setViewFromBounds = function (bounds) {
if (!this.zooming && !this.panning) {
this.$scope.boundsModel.start = bounds.start;
this.$scope.boundsModel.end = bounds.end;
if (this.supportsZoom()) {
var config = this.getConfig(this.timeAPI.timeSystem(), this.timeAPI.clock());
this.currentZoom = this.toSliderValue(bounds.end - bounds.start, config.zoomOutLimit, config.zoomInLimit);
this.toTimeUnits(bounds.end - bounds.start);
}
/*
Ensure that a digest occurs, capped at the browser's refresh
rate.
*/
if (!this.pendingUpdate) {
this.pendingUpdate = true;
this.$window.requestAnimationFrame(function () {
this.pendingUpdate = false;
this.$scope.$digest();
}.bind(this));
}
}
};
/**
* Retrieve any configuration defined for the provided time system and
* clock
* @private
* @param timeSystem
* @param clock
* @returns {object} The Time Conductor configuration corresponding to
* the provided combination of time system and clock
*/
TimeConductorController.prototype.getConfig = function (timeSystem, clock) {
var clockKey = clock && clock.key;
var timeSystemKey = timeSystem && timeSystem.key;
var option = this.config.menuOptions.filter(function (menuOption) {
return menuOption.timeSystem === timeSystemKey && menuOption.clock === clockKey;
})[0];
return option;
};
/**
* When the clock offsets change, update the values in the UI
* @param {ClockOffsets} offsets
* @private
*/
TimeConductorController.prototype.setViewFromOffsets = function (offsets) {
this.$scope.boundsModel.startOffset = Math.abs(offsets.start);
this.$scope.boundsModel.endOffset = offsets.end;
};
/**
* When form values for bounds change, update the bounds in the Time API
* to trigger an application-wide bounds change.
* @param {object} boundsModel
*/
TimeConductorController.prototype.setBoundsFromView = function (boundsModel) {
var bounds = this.timeAPI.bounds();
if (boundsModel.start !== bounds.start || boundsModel.end !== bounds.end) {
this.timeAPI.bounds({
start: boundsModel.start,
end: boundsModel.end
});
}
};
/**
* When form values for bounds change, update the bounds in the Time API
* to trigger an application-wide bounds change.
* @param {object} formModel
*/
TimeConductorController.prototype.setOffsetsFromView = function (boundsModel) {
if (this.validation.validateStartOffset(boundsModel.startOffset) && this.validation.validateEndOffset(boundsModel.endOffset)) {
var offsets = {
start: 0 - boundsModel.startOffset,
end: boundsModel.endOffset
};
var existingOffsets = this.timeAPI.clockOffsets();
if (offsets.start !== existingOffsets.start || offsets.end !== existingOffsets.end) {
//Sychronize offsets between form and time API
this.timeAPI.clockOffsets(offsets);
}
}
};
/**
* @private
* @returns {boolean}
*/
TimeConductorController.prototype.supportsZoom = function () {
var config = this.getConfig(this.timeAPI.timeSystem(), this.timeAPI.clock());
return config && (config.zoomInLimit !== undefined && config.zoomOutLimit !== undefined);
};
/**
* Update the UI state to reflect a change in clock. Provided conductor
* configuration will be checked for compatibility between the new clock
* and the currently selected time system. If configuration is not available,
* an attempt will be made to default to a time system that is compatible
* with the new clock
*
* @private
* @param {Clock} clock
*/
TimeConductorController.prototype.setViewFromClock = function (clock) {
var newClockKey = clock ? clock.key : 'fixed';
var timeSystems = this.timeSystemsForClocks[newClockKey];
var menuOption = this.menu.options.filter(function (option) {
return option.key === (newClockKey);
})[0];
this.menu.selected = menuOption;
//Try to find currently selected time system in time systems for clock
var selectedTimeSystem = timeSystems.filter(function (timeSystem) {
return timeSystem.key === this.$scope.timeSystemModel.selected.key;
}.bind(this))[0];
var config = this.getConfig(selectedTimeSystem, clock);
if (selectedTimeSystem === undefined) {
selectedTimeSystem = timeSystems[0];
config = this.getConfig(selectedTimeSystem, clock);
if (clock === undefined) {
this.timeAPI.timeSystem(selectedTimeSystem, config.bounds);
} else {
//When time system changes, some start bounds need to be provided
this.timeAPI.timeSystem(selectedTimeSystem, {
start: clock.currentValue() + config.clockOffsets.start,
end: clock.currentValue() + config.clockOffsets.end
});
}
}
this.isFixed = clock === undefined;
if (clock === undefined) {
this.setViewFromBounds(this.timeAPI.bounds());
}
this.zoom = this.supportsZoom();
this.$scope.timeSystemModel.options = timeSystems;
};
/**
* Respond to time system selection from UI
*
* Allows time system to be changed by key. This supports selection
* from the menu. Resolves a TimeSystem object and then invokes
* TimeConductorController#setTimeSystem
* @param key
* @see TimeConductorController#setTimeSystem
*/
TimeConductorController.prototype.setTimeSystemFromView = function (key) {
var clock = this.menu.selected.clock;
var timeSystem = this.timeSystems[key];
var config = this.getConfig(timeSystem, clock);
this.$scope.timeSystemModel.selected = timeSystem;
this.$scope.timeSystemModel.format = timeSystem.timeFormat;
if (clock === undefined) {
this.timeAPI.timeSystem(timeSystem, config.bounds);
} else {
this.timeAPI.clock(clock, config.clockOffsets);
this.timeAPI.timeSystem(timeSystem);
}
};
/**
* Handles time system change from time conductor
*
* Sets the selected time system. Will populate form with the default
* bounds and offsets defined in the selected time system.
*
* @param newTimeSystem
*/
TimeConductorController.prototype.setViewFromTimeSystem = function (timeSystem) {
var oldKey = (this.$scope.timeSystemModel.selected || {}).key;
var timeSystemModel = this.$scope.timeSystemModel;
if (timeSystem && (timeSystem.key !== oldKey)) {
var config = this.getConfig(timeSystem, this.timeAPI.clock());
timeSystemModel.selected = timeSystem;
timeSystemModel.format = timeSystem.timeFormat;
timeSystemModel.durationFormat = timeSystem.durationFormat;
if (this.supportsZoom()) {
timeSystemModel.minZoom = config.zoomOutLimit;
timeSystemModel.maxZoom = config.zoomInLimit;
}
}
this.zoom = this.supportsZoom();
};
/**
* Takes a time span and calculates a slider increment value, used
* to set the horizontal offset of the slider.
* @private
* @param {number} timeSpan a duration of time, in ms
* @returns {number} a value between 0.01 and 0.99, in increments of .01
*/
TimeConductorController.prototype.toSliderValue = function (timeSpan, zoomOutLimit, zoomInLimit) {
var perc = timeSpan / (zoomOutLimit - zoomInLimit);
return 1 - Math.pow(perc, 1 / 4);
};
/**
* Given a time span, set a label for the units of time that it,
* roughly, represents. Leverages
* @param {TimeSpan} timeSpan
*/
TimeConductorController.prototype.toTimeUnits = function (timeSpan) {
var timeSystem = this.timeAPI.timeSystem();
if (timeSystem && timeSystem.isUTCBased) {
var momentified = moment.duration(timeSpan);
this.$scope.timeUnits = timeUnitsMegastructure.filter(function (row) {
return row[1](momentified);
})[0][0];
}
};
/**
* Zooming occurs when the user manipulates the zoom slider.
* Zooming updates the scale and bounds fields immediately, but does
* not trigger a bounds change to other views until the mouse button
* is released.
* @param bounds
*/
TimeConductorController.prototype.onZoom = function (sliderValue) {
var config = this.getConfig(this.timeAPI.timeSystem(), this.timeAPI.clock());
var timeSpan = Math.pow((1 - sliderValue), 4) * (config.zoomOutLimit - config.zoomInLimit);
var zoom = this.conductorViewService.zoom(timeSpan);
this.zooming = true;
this.$scope.boundsModel.start = zoom.bounds.start;
this.$scope.boundsModel.end = zoom.bounds.end;
this.toTimeUnits(zoom.bounds.end - zoom.bounds.start);
if (zoom.offsets) {
this.setViewFromOffsets(zoom.offsets);
}
};
/**
* Fired when user has released the zoom slider
* @event platform.features.conductor.TimeConductorController~zoomStop
*/
/**
* Invoked when zoom slider is released by user. Will update the time conductor with the new bounds, triggering
* a global bounds change event.
* @fires platform.features.conductor.TimeConductorController~zoomStop
*/
TimeConductorController.prototype.onZoomStop = function () {
if (this.timeAPI.clock() !== undefined) {
this.setOffsetsFromView(this.$scope.boundsModel);
}
this.setBoundsFromView(this.$scope.boundsModel);
this.zooming = false;
this.conductorViewService.emit('zoom-stop');
};
/**
* Panning occurs when the user grabs the conductor scale and drags
* it left or right to slide the window of time represented by the
* conductor. Panning updates the scale and bounds fields
* immediately, but does not trigger a bounds change to other views
* until the mouse button is released.
* @param {TimeConductorBounds} bounds
*/
TimeConductorController.prototype.onPan = function (bounds) {
this.panning = true;
this.$scope.boundsModel.start = bounds.start;
this.$scope.boundsModel.end = bounds.end;
};
/**
* Called when the user releases the mouse button after panning.
*/
TimeConductorController.prototype.onPanStop = function () {
this.panning = false;
};
/**
* @private
*/
TimeConductorController.prototype.destroy = function () {
this.timeAPI.off('bounds', this.setViewFromBounds);
this.timeAPI.off('timeSystem', this.setViewFromTimeSystem);
this.timeAPI.off('clock', this.setViewFromClock);
this.timeAPI.off('follow', this.setFollow);
this.timeAPI.off('clockOffsets', this.setViewFromOffsets);
this.conductorViewService.off('pan', this.onPan);
this.conductorViewService.off('pan-stop', this.onPanStop);
};
return TimeConductorController;
}
);

View File

@ -0,0 +1,513 @@
/*****************************************************************************
* 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(['./TimeConductorController'], function (TimeConductorController) {
xdescribe("The time conductor controller", function () {
var mockScope;
var mockWindow;
var mockTimeConductor;
var mockConductorViewService;
var mockTimeSystems;
var controller;
var mockFormatService;
var mockFormat;
var mockLocation;
beforeEach(function () {
mockScope = jasmine.createSpyObj("$scope", [
"$watch",
"$on"
]);
mockWindow = jasmine.createSpyObj("$window", ["requestAnimationFrame"]);
mockTimeConductor = jasmine.createSpyObj(
"TimeConductor",
[
"bounds",
"timeSystem",
"on",
"off"
]
);
mockTimeConductor.bounds.and.returnValue({start: undefined, end: undefined});
mockConductorViewService = jasmine.createSpyObj(
"ConductorViewService",
[
"availableModes",
"mode",
"availableTimeSystems",
"deltas",
"deltas",
"on",
"off"
]
);
mockConductorViewService.availableModes.and.returnValue([]);
mockConductorViewService.availableTimeSystems.and.returnValue([]);
mockFormatService = jasmine.createSpyObj('formatService', [
'getFormat'
]);
mockFormat = jasmine.createSpyObj('format', [
'format'
]);
mockFormatService.getFormat.and.returnValue(mockFormat);
mockLocation = jasmine.createSpyObj('location', [
'search'
]);
mockLocation.search.and.returnValue({});
mockTimeSystems = [];
});
function getListener(target, event) {
return target.calls.all().filter(function (call) {
return call.args[0] === event;
})[0].args[1];
}
describe("when time conductor state changes", function () {
var mockDeltaFormat;
var defaultBounds;
var defaultDeltas;
var mockDefaults;
var timeSystem;
var tsListener;
beforeEach(function () {
mockFormat = {};
mockDeltaFormat = {};
defaultBounds = {
start: 2,
end: 3
};
defaultDeltas = {
start: 10,
end: 20
};
mockDefaults = {
deltas: defaultDeltas,
bounds: defaultBounds
};
timeSystem = {
metadata: {
key: 'mock'
},
formats: function () {
return [mockFormat];
},
deltaFormat: function () {
return mockDeltaFormat;
},
defaults: function () {
return mockDefaults;
}
};
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockFormatService,
'fixed',
true
);
tsListener = getListener(mockTimeConductor.on, "timeSystem");
});
it("listens for changes to conductor state", function () {
expect(mockTimeConductor.on).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
expect(mockTimeConductor.on).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
it("deregisters conductor listens when scope is destroyed", function () {
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", controller.destroy);
controller.destroy();
expect(mockTimeConductor.off).toHaveBeenCalledWith("timeSystem", controller.changeTimeSystem);
expect(mockTimeConductor.off).toHaveBeenCalledWith("bounds", controller.changeBounds);
});
it("when time system changes, sets time system on scope", function () {
expect(tsListener).toBeDefined();
tsListener(timeSystem);
expect(mockScope.timeSystemModel).toBeDefined();
expect(mockScope.timeSystemModel.selected).toBe(timeSystem);
expect(mockScope.timeSystemModel.format).toBe(mockFormat);
expect(mockScope.timeSystemModel.deltaFormat).toBe(mockDeltaFormat);
});
it("when time system changes, sets defaults on scope", function () {
mockDefaults.zoom = {
min: 100,
max: 10
};
mockTimeConductor.timeSystem.and.returnValue(timeSystem);
tsListener(timeSystem);
expect(mockScope.boundsModel.start).toEqual(defaultBounds.start);
expect(mockScope.boundsModel.end).toEqual(defaultBounds.end);
expect(mockScope.boundsModel.startDelta).toEqual(defaultDeltas.start);
expect(mockScope.boundsModel.endDelta).toEqual(defaultDeltas.end);
expect(mockScope.timeSystemModel.minZoom).toBe(mockDefaults.zoom.min);
expect(mockScope.timeSystemModel.maxZoom).toBe(mockDefaults.zoom.max);
});
it("supports zoom if time system defines zoom defaults", function () {
mockDefaults.zoom = undefined;
tsListener(timeSystem);
expect(controller.supportsZoom).toBe(false);
mockDefaults.zoom = {
min: 100,
max: 10
};
var anotherTimeSystem = Object.create(timeSystem);
timeSystem.defaults = function () {
return mockDefaults;
};
tsListener(anotherTimeSystem);
expect(controller.supportsZoom).toBe(true);
});
it("when bounds change, sets the correct zoom slider value", function () {
var bounds = {
start: 0,
end: 50
};
mockDefaults.zoom = {
min: 100,
max: 0
};
function exponentializer(rawValue) {
return 1 - Math.pow(rawValue, 1 / 4);
}
mockTimeConductor.timeSystem.and.returnValue(timeSystem);
//Set zoom defaults
tsListener(timeSystem);
controller.changeBounds(bounds);
expect(controller.currentZoom).toEqual(exponentializer(0.5));
});
it("when bounds change, sets them on scope", function () {
var bounds = {
start: 1,
end: 2
};
var boundsListener = getListener(mockTimeConductor.on, "bounds");
expect(boundsListener).toBeDefined();
boundsListener(bounds);
expect(mockScope.boundsModel).toBeDefined();
expect(mockScope.boundsModel.start).toEqual(bounds.start);
expect(mockScope.boundsModel.end).toEqual(bounds.end);
});
});
describe("when user makes changes from UI", function () {
var mode = "realtime";
var ts1Metadata;
var ts2Metadata;
var ts3Metadata;
beforeEach(function () {
mode = "realtime";
ts1Metadata = {
'key': 'ts1',
'name': 'Time System One',
'cssClass': 'cssClassOne'
};
ts2Metadata = {
'key': 'ts2',
'name': 'Time System Two',
'cssClass': 'cssClassTwo'
};
ts3Metadata = {
'key': 'ts3',
'name': 'Time System Three',
'cssClass': 'cssClassThree'
};
mockTimeSystems = [
{
metadata: ts1Metadata
},
{
metadata: ts2Metadata
},
{
metadata: ts3Metadata
}
];
//Wrap in mock constructors
mockConductorViewService.systems = mockTimeSystems;
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockFormatService,
"fixed",
true
);
});
it("sets the mode on scope", function () {
mockConductorViewService.availableTimeSystems.and.returnValue(mockTimeSystems);
controller.setMode(mode);
expect(mockScope.modeModel.selectedKey).toEqual(mode);
});
it("sets available time systems on scope when mode changes", function () {
mockConductorViewService.availableTimeSystems.and.returnValue(mockTimeSystems);
controller.setMode(mode);
expect(mockScope.timeSystemModel.options.length).toEqual(3);
expect(mockScope.timeSystemModel.options[0]).toEqual(ts1Metadata);
expect(mockScope.timeSystemModel.options[1]).toEqual(ts2Metadata);
expect(mockScope.timeSystemModel.options[2]).toEqual(ts3Metadata);
});
it("sets bounds on the time conductor", function () {
var formModel = {
start: 1,
end: 10
};
controller.setBounds(formModel);
expect(mockTimeConductor.bounds).toHaveBeenCalledWith(formModel);
});
it("applies deltas when they change in form", function () {
var deltas = {
start: 1000,
end: 2000
};
var formModel = {
startDelta: deltas.start,
endDelta: deltas.end
};
controller.setDeltas(formModel);
expect(mockConductorViewService.deltas).toHaveBeenCalledWith(deltas);
});
it("sets the time system on the time conductor", function () {
var defaultBounds = {
start: 5,
end: 6
};
var timeSystem = {
metadata: {
key: 'testTimeSystem'
},
defaults: function () {
return {
bounds: defaultBounds
};
}
};
controller.timeSystems = [timeSystem];
controller.selectTimeSystemByKey('testTimeSystem');
expect(mockTimeConductor.timeSystem).toHaveBeenCalledWith(timeSystem, defaultBounds);
});
it("updates form bounds during pan events", function () {
var testBounds = {
start: 10,
end: 20
};
expect(controller.$scope.boundsModel.start).not.toBe(testBounds.start);
expect(controller.$scope.boundsModel.end).not.toBe(testBounds.end);
expect(controller.conductorViewService.on).toHaveBeenCalledWith("pan",
controller.onPan);
getListener(controller.conductorViewService.on, "pan")(testBounds);
expect(controller.$scope.boundsModel.start).toBe(testBounds.start);
expect(controller.$scope.boundsModel.end).toBe(testBounds.end);
});
});
describe("when the URL defines conductor state", function () {
var urlBounds;
var urlTimeSystem;
var urlDeltas;
var mockDeltaFormat;
var defaultBounds;
var defaultDeltas;
var mockDefaults;
var timeSystem;
var otherTimeSystem;
var mockSearchObject;
beforeEach(function () {
mockFormat = {};
mockDeltaFormat = {};
defaultBounds = {
start: 2,
end: 3
};
defaultDeltas = {
start: 10,
end: 20
};
mockDefaults = {
deltas: defaultDeltas,
bounds: defaultBounds
};
timeSystem = {
metadata: {
key: 'mockTimeSystem'
},
formats: function () {
return [mockFormat];
},
deltaFormat: function () {
return mockDeltaFormat;
},
defaults: function () {
return mockDefaults;
}
};
otherTimeSystem = {
metadata: {
key: 'otherTimeSystem'
},
formats: function () {
return [mockFormat];
},
deltaFormat: function () {
return mockDeltaFormat;
},
defaults: function () {
return mockDefaults;
}
};
mockConductorViewService.systems = [timeSystem, otherTimeSystem];
urlBounds = {
start: 100,
end: 200
};
urlTimeSystem = "otherTimeSystem";
urlDeltas = {
start: 300,
end: 400
};
mockSearchObject = {
"tc.startBound": urlBounds.start,
"tc.endBound": urlBounds.end,
"tc.startDelta": urlDeltas.start,
"tc.endDelta": urlDeltas.end,
"tc.timeSystem": urlTimeSystem
};
mockLocation.search.and.returnValue(mockSearchObject);
mockTimeConductor.timeSystem.and.returnValue(timeSystem);
controller = new TimeConductorController(
mockScope,
mockWindow,
mockLocation,
{conductor: mockTimeConductor},
mockConductorViewService,
mockFormatService,
"fixed",
true
);
spyOn(controller, "setMode");
spyOn(controller, "selectTimeSystemByKey");
});
it("sets conductor state from URL", function () {
mockSearchObject["tc.mode"] = "fixed";
controller.setStateFromSearchParams(mockSearchObject);
expect(controller.selectTimeSystemByKey).toHaveBeenCalledWith("otherTimeSystem");
expect(mockTimeConductor.bounds).toHaveBeenCalledWith(urlBounds);
});
it("sets mode from URL", function () {
mockTimeConductor.bounds.reset();
mockSearchObject["tc.mode"] = "realtime";
controller.setStateFromSearchParams(mockSearchObject);
expect(controller.setMode).toHaveBeenCalledWith("realtime");
expect(controller.selectTimeSystemByKey).toHaveBeenCalledWith("otherTimeSystem");
expect(mockConductorViewService.deltas).toHaveBeenCalledWith(urlDeltas);
expect(mockTimeConductor.bounds).not.toHaveBeenCalled();
});
describe("when conductor state changes", function () {
it("updates the URL with the mode", function () {
controller.setMode("realtime", "fixed");
expect(mockLocation.search).toHaveBeenCalledWith("tc.mode", "fixed");
});
it("updates the URL with the bounds", function () {
mockConductorViewService.mode.and.returnValue("fixed");
controller.changeBounds({start: 500, end: 600});
expect(mockLocation.search).toHaveBeenCalledWith("tc.startBound", 500);
expect(mockLocation.search).toHaveBeenCalledWith("tc.endBound", 600);
});
it("updates the URL with the deltas", function () {
controller.setDeltas({startDelta: 700, endDelta: 800});
expect(mockLocation.search).toHaveBeenCalledWith("tc.startDelta", 700);
expect(mockLocation.search).toHaveBeenCalledWith("tc.endDelta", 800);
});
it("updates the URL with the time system", function () {
controller.changeTimeSystem(otherTimeSystem);
expect(mockLocation.search).toHaveBeenCalledWith("tc.timeSystem", "otherTimeSystem");
});
});
});
});
});

View File

@ -0,0 +1,69 @@
/*****************************************************************************
* 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(
[],
function () {
/**
* Form validation for the TimeConductorController.
* @param conductor
* @constructor
*/
function TimeConductorValidation(timeAPI) {
var self = this;
this.timeAPI = timeAPI;
/*
* Bind all class functions to 'this'
*/
Object.keys(TimeConductorValidation.prototype).filter(function (key) {
return typeof TimeConductorValidation.prototype[key] === 'function';
}).forEach(function (key) {
self[key] = self[key].bind(self);
});
}
/**
* Validation methods below are invoked directly from controls in the TimeConductor form
*/
TimeConductorValidation.prototype.validateStart = function (start) {
var bounds = this.timeAPI.bounds();
return this.timeAPI.validateBounds({start: start, end: bounds.end}) === true;
};
TimeConductorValidation.prototype.validateEnd = function (end) {
var bounds = this.timeAPI.bounds();
return this.timeAPI.validateBounds({start: bounds.start, end: end}) === true;
};
TimeConductorValidation.prototype.validateStartOffset = function (startOffset) {
return !isNaN(startOffset) && startOffset > 0;
};
TimeConductorValidation.prototype.validateEndOffset = function (endOffset) {
return !isNaN(endOffset) && endOffset >= 0;
};
return TimeConductorValidation;
}
);

View File

@ -0,0 +1,73 @@
/*****************************************************************************
* 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(['./TimeConductorValidation'], function (TimeConductorValidation) {
describe("The Time Conductor Validation class", function () {
var timeConductorValidation,
mockTimeConductor;
beforeEach(function () {
mockTimeConductor = jasmine.createSpyObj("timeConductor", [
"validateBounds",
"bounds"
]);
timeConductorValidation = new TimeConductorValidation(mockTimeConductor);
});
describe("Validates start and end values using Time Conductor", function () {
beforeEach(function () {
var mockBounds = {
start: 10,
end: 20
};
mockTimeConductor.bounds.and.returnValue(mockBounds);
});
it("Validates start values using Time Conductor", function () {
var startValue = 30;
timeConductorValidation.validateStart(startValue);
expect(mockTimeConductor.validateBounds).toHaveBeenCalled();
});
it("Validates end values using Time Conductor", function () {
var endValue = 40;
timeConductorValidation.validateEnd(endValue);
expect(mockTimeConductor.validateBounds).toHaveBeenCalled();
});
});
it("Validates that start Offset is valid number > 0", function () {
expect(timeConductorValidation.validateStartOffset(-1)).toBe(false);
expect(timeConductorValidation.validateStartOffset("abc")).toBe(false);
expect(timeConductorValidation.validateStartOffset("1")).toBe(true);
expect(timeConductorValidation.validateStartOffset(1)).toBe(true);
});
it("Validates that end Offset is valid number >= 0", function () {
expect(timeConductorValidation.validateEndOffset(-1)).toBe(false);
expect(timeConductorValidation.validateEndOffset("abc")).toBe(false);
expect(timeConductorValidation.validateEndOffset("1")).toBe(true);
expect(timeConductorValidation.validateEndOffset(0)).toBe(true);
expect(timeConductorValidation.validateEndOffset(1)).toBe(true);
});
});
});

View File

@ -0,0 +1,97 @@
/*****************************************************************************
* 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 TimeConductorViewService acts as an event bus between different
* elements of the Time Conductor UI. Zooming and panning occur via this
* service, as they are specific behaviour of the UI, and not general
* functions of the time API.
*
* Synchronization of conductor state between the Time API and the URL
* also occurs from the conductor view service, whose lifecycle persists
* between view changes.
*
* @memberof platform.features.conductor
* @param conductor
* @constructor
*/
function TimeConductorViewService(openmct) {
EventEmitter.call(this);
this.timeAPI = openmct.time;
}
TimeConductorViewService.prototype = Object.create(EventEmitter.prototype);
/**
* An event to indicate that zooming is taking place
* @event platform.features.conductor.TimeConductorViewService~zoom
* @property {ZoomLevel} zoom the new zoom level.
*/
/**
* Zoom to given time span. Will fire a zoom event with new zoom
* bounds. Zoom bounds emitted in this way are considered ephemeral
* and should be overridden by any time conductor bounds events. Does
* not set bounds globally.
* @param {number} zoom A time duration in ms
* @fires platform.features.conductor.TimeConductorViewService~zoom
* @see module:openmct.TimeConductor#bounds
*/
TimeConductorViewService.prototype.zoom = function (timeSpan) {
var zoom = {};
// If a tick source is defined, then the concept of 'now' is
// important. Calculate zoom based on 'now'.
if (this.timeAPI.clock() !== undefined) {
zoom.offsets = {
start: -timeSpan,
end: this.timeAPI.clockOffsets().end
};
var currentVal = this.timeAPI.clock().currentValue();
zoom.bounds = {
start: currentVal + zoom.offsets.start,
end: currentVal + zoom.offsets.end
};
} else {
var bounds = this.timeAPI.bounds();
var center = bounds.start + ((bounds.end - bounds.start)) / 2;
bounds.start = center - timeSpan / 2;
bounds.end = center + timeSpan / 2;
zoom.bounds = bounds;
}
this.emit("zoom", zoom);
return zoom;
};
return TimeConductorViewService;
}
);

View File

@ -0,0 +1,109 @@
/*****************************************************************************
* 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(
[],
function () {
/**
* Controller for the Time of Interest element used in various views to display the TOI. Responsible for setting
* the text label for the current TOI, and for toggling the (un)pinned state which determines whether the TOI
* indicator is visible.
* @constructor
*/
function TimeOfInterestController($scope, openmct, formatService) {
this.timeAPI = openmct.time;
this.formatService = formatService;
this.format = undefined;
this.toiText = undefined;
this.$scope = $scope;
//Bind all class functions to 'this'
Object.keys(TimeOfInterestController.prototype).filter(function (key) {
return typeof TimeOfInterestController.prototype[key] === 'function';
}).forEach(function (key) {
this[key] = TimeOfInterestController.prototype[key].bind(this);
}.bind(this));
this.timeAPI.on('timeOfInterest', this.changeTimeOfInterest);
this.timeAPI.on('timeSystem', this.changeTimeSystem);
if (this.timeAPI.timeSystem() !== undefined) {
this.changeTimeSystem(this.timeAPI.timeSystem());
var toi = this.timeAPI.timeOfInterest();
if (toi) {
this.changeTimeOfInterest(toi);
}
}
$scope.$on('$destroy', this.destroy);
}
/**
* Called when the time of interest changes on the conductor. Will pin (display) the TOI indicator, and set the
* text using the default formatter of the currently active Time System.
* @private
* @param {integer} toi Current time of interest in ms
*/
TimeOfInterestController.prototype.changeTimeOfInterest = function (toi) {
if (toi !== undefined) {
this.$scope.pinned = true;
this.toiText = this.format.format(toi);
} else {
this.$scope.pinned = false;
}
};
/**
* When time system is changed, update the formatter used to
* display the current TOI label
*/
TimeOfInterestController.prototype.changeTimeSystem = function (timeSystem) {
this.format = this.formatService.getFormat(timeSystem.timeFormat);
};
/**
* @private
*/
TimeOfInterestController.prototype.destroy = function () {
this.timeAPI.off('timeOfInterest', this.changeTimeOfInterest);
this.timeAPI.off('timeSystem', this.changeTimeSystem);
};
/**
* Will unpin (hide) the TOI indicator. Has the effect of setting the time of interest to `undefined` on the
* Time Conductor
*/
TimeOfInterestController.prototype.dismiss = function () {
this.timeAPI.timeOfInterest(undefined);
};
/**
* Sends out a time of interest event with the effect of resetting
* the TOI displayed in views.
*/
TimeOfInterestController.prototype.resync = function () {
this.timeAPI.timeOfInterest(this.timeAPI.timeOfInterest());
};
return TimeOfInterestController;
}
);

View File

@ -19,10 +19,10 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<a class="c-hyperlink u-links" ng-controller="HyperlinkController as hyperlink" href="{{domainObject.getModel().url}}"
<a class="l-hyperlink s-hyperlink" ng-controller="HyperlinkController as hyperlink" href="{{domainObject.getModel().url}}"
ng-attr-target="{{hyperlink.openNewTab() ? '_blank' : undefined}}"
ng-class="{
'c-hyperlink--button u-fills-container' : hyperlink.isButton(),
'c-hyperlink--link' : !hyperlink.isButton() }">
<span class="c-hyperlink__label">{{domainObject.getModel().displayText}}</span>
's-button': hyperlink.isButton()
}">
<span class="label">{{domainObject.getModel().displayText}}</span>
</a>

View File

@ -0,0 +1,83 @@
<!--
Open MCT, Copyright (c) 2014-2018, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<div class="abs l-layout {{ domainObject.getModel().layoutAdvancedCss }}"
ng-controller="LayoutController as controller"
ng-click="controller.bypassSelection($event)">
<!-- Background grid -->
<div class="l-grid-holder"
ng-show="!controller.drilledIn"
ng-click="controller.bypassSelection($event)">
<div class="l-grid l-grid-x"
ng-if="!controller.getGridSize()[0] < 3"
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
<div class="l-grid l-grid-y"
ng-if="!controller.getGridSize()[1] < 3"
ng-style="{ 'background-size': '100% ' + controller.getGridSize() [1] + 'px' }"></div>
</div>
<div class="frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border t-object-type-{{ childObject.getModel().type }}"
data-layout-id="{{childObject.getId() + '-' + $id}}"
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(childObject) }"
ng-repeat="childObject in composition"
ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)"
mct-selectable="controller.getContext(childObject)"
ng-dblclick="controller.drill($event, childObject)"
ng-style="controller.getFrameStyle(childObject.getId())">
<mct-representation key="'frame'"
class="t-rep-frame holder contents abs"
mct-object="childObject">
</mct-representation>
<!-- Drag handles -->
<span class="abs t-edit-handle-holder" ng-if="controller.selected(childObject) && !controller.isDrilledIn(childObject)">
<span class="edit-handle edit-move"
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])"
mct-drag="controller.continueDrag(delta)"
mct-drag-up="controller.endDrag()">
</span>
<span class="edit-corner edit-resize-nw"
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [-1,-1])"
mct-drag="controller.continueDrag(delta)"
mct-drag-up="controller.endDrag()">
</span>
<span class="edit-corner edit-resize-ne"
mct-drag-down="controller.startDrag(childObject.getId(), [0,1], [1,-1])"
mct-drag="controller.continueDrag(delta)"
mct-drag-up="controller.endDrag()">
</span>
<span class="edit-corner edit-resize-sw"
mct-drag-down="controller.startDrag(childObject.getId(), [1,0], [-1,1])"
mct-drag="controller.continueDrag(delta)"
mct-drag-up="controller.endDrag()">
</span>
<span class="edit-corner edit-resize-se"
mct-drag-down="controller.startDrag(childObject.getId(), [0,0], [1,1])"
mct-drag="controller.continueDrag(delta)"
mct-drag-up="controller.endDrag()">
</span>
</span>
</div>
</div>

View File

@ -0,0 +1,524 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/**
* This bundle implements object types and associated views for
* display-building.
* @namespace platform/features/layout
*/
define(
[
'zepto',
'./LayoutDrag'
],
function (
$,
LayoutDrag
) {
var DEFAULT_DIMENSIONS = [12, 8],
DEFAULT_GRID_SIZE = [32, 32],
MINIMUM_FRAME_SIZE = [320, 180];
var DEFAULT_HIDDEN_FRAME_TYPES = [
'hyperlink'
];
/**
* The LayoutController is responsible for supporting the
* Layout view. It arranges frames according to saved configuration
* and provides methods for updating these based on mouse
* movement.
* @memberof platform/features/layout
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function LayoutController($scope, $element, openmct) {
var self = this,
callbackCount = 0;
this.$element = $element;
// Update grid size when it changed
function updateGridSize(layoutGrid) {
var oldSize = self.gridSize;
self.gridSize = layoutGrid || DEFAULT_GRID_SIZE;
// Only update panel positions if this actually changed things
if (self.gridSize[0] !== oldSize[0] ||
self.gridSize[1] !== oldSize[1]) {
self.layoutPanels(Object.keys(self.positions));
}
}
// Position a panel after a drop event
function handleDrop(e, id, position) {
if (e.defaultPrevented) {
return;
}
$scope.configuration = $scope.configuration || {};
$scope.configuration.panels = $scope.configuration.panels || {};
self.openmct.objects.get(id).then(function (object) {
$scope.configuration.panels[id] = {
position: [
Math.floor(position.x / self.gridSize[0]),
Math.floor(position.y / self.gridSize[1])
],
dimensions: self.defaultDimensions(),
hasFrame: self.getDefaultFrame(object.type)
};
// Store the id so that the newly-dropped object
// gets selected during refresh composition
self.droppedIdToSelectAfterRefresh = id;
self.commit();
// Populate template-facing position for this id
self.rawPositions[id] = $scope.configuration.panels[id];
self.populatePosition(id);
refreshComposition();
});
// Layout may contain embedded views which will
// listen for drops, so call preventDefault() so
// that they can recognize that this event is handled.
e.preventDefault();
}
//Will fetch fully contextualized composed objects, and populate
// scope with them.
function refreshComposition() {
//Keep a track of how many composition callbacks have been made
var thisCount = ++callbackCount;
$scope.domainObject.useCapability('composition').then(function (composition) {
var ids;
//Is this callback for the most recent composition
// request? If not, discard it. Prevents race condition
if (thisCount === callbackCount) {
ids = composition.map(function (object) {
return object.getId();
}) || [];
$scope.composition = composition;
self.layoutPanels(ids);
self.setFrames(ids);
if (self.selectedId &&
self.selectedId !== $scope.domainObject.getId() &&
composition.indexOf(self.selectedId) === -1) {
// Click triggers selection of layout parent.
self.$element[0].click();
}
}
});
}
// End drag; we don't want to put $scope into this
// because it triggers "cpws" (copy window or scope)
// errors in Angular.
this.endDragInScope = function () {
// Write to configuration; this is watched and
// saved by the EditRepresenter.
$scope.configuration =
$scope.configuration || {};
$scope.configuration.panels =
$scope.configuration.panels || {};
$scope.configuration.panels[self.activeDragId] =
$scope.configuration.panels[self.activeDragId] || {};
$scope.configuration.panels[self.activeDragId].position =
self.rawPositions[self.activeDragId].position;
$scope.configuration.panels[self.activeDragId].dimensions =
self.rawPositions[self.activeDragId].dimensions;
self.commit();
};
// Sets the selectable object in response to the selection change event.
function setSelection(selectable) {
var selection = selectable[0];
if (!selection) {
delete self.selectedId;
return;
}
self.selectedId = selection.context.oldItem.getId();
self.drilledIn = undefined;
self.selectable = selectable;
}
this.positions = {};
this.rawPositions = {};
this.gridSize = DEFAULT_GRID_SIZE;
this.$scope = $scope;
this.drilledIn = undefined;
this.openmct = openmct;
// Watch for changes to the grid size in the model
$scope.$watch("model.layoutGrid", updateGridSize);
// Update composed objects on screen, and position panes
$scope.$watchCollection("model.composition", refreshComposition);
openmct.selection.on('change', setSelection);
$scope.$on("$destroy", function () {
openmct.selection.off("change", setSelection);
self.unlisten();
});
$scope.$on("mctDrop", handleDrop);
self.unlisten = self.$scope.domainObject.getCapability('mutation').listen(function (model) {
$scope.configuration = model.configuration.layout;
$scope.model = model;
var panels = $scope.configuration.panels;
Object.keys(panels).forEach(function (key) {
if (self.frames && self.frames.hasOwnProperty(key)) {
self.frames[key] = panels[key].hasFrame;
}
});
});
}
// Utility function to copy raw positions from configuration,
// without writing directly to configuration (to avoid triggering
// persistence from watchers during drags).
function shallowCopy(obj, keys) {
var copy = {};
keys.forEach(function (k) {
copy[k] = obj[k];
});
return copy;
}
/**
* Set the frames value. If a configuration panel has "hasFrame' property,
* use that value, otherwise set a default value. A 'hyperlink' object should
* have no frame by default.
*
* @param {string[]} ids the object ids
* @private
*/
LayoutController.prototype.setFrames = function (ids) {
var panels = shallowCopy(this.$scope.configuration.panels || {}, ids);
this.frames = {};
this.$scope.composition.forEach(function (object) {
var id = object.getId();
panels[id] = panels[id] || {};
if (panels[id].hasOwnProperty('hasFrame')) {
this.frames[id] = panels[id].hasFrame;
} else {
this.frames[id] = this.getDefaultFrame(object.getModel().type);
}
}, this);
};
/**
* Gets the default value for frame.
*
* @param type the domain object type
* @return {boolean} true if the object should have
* frame by default, false, otherwise
*/
LayoutController.prototype.getDefaultFrame = function (type) {
return DEFAULT_HIDDEN_FRAME_TYPES.indexOf(type) === -1;
};
// Convert from { positions: ..., dimensions: ... } to an
// appropriate ng-style argument, to position frames.
LayoutController.prototype.convertPosition = function (raw) {
var gridSize = this.gridSize;
// Multiply position/dimensions by grid size
return {
left: (gridSize[0] * raw.position[0]) + 'px',
top: (gridSize[1] * raw.position[1]) + 'px',
width: (gridSize[0] * raw.dimensions[0]) + 'px',
height: (gridSize[1] * raw.dimensions[1]) + 'px',
minWidth: (gridSize[0] * raw.dimensions[0]) + 'px',
minHeight: (gridSize[1] * raw.dimensions[1]) + 'px'
};
};
// Generate default positions for a new panel
LayoutController.prototype.defaultDimensions = function () {
var gridSize = this.gridSize;
return MINIMUM_FRAME_SIZE.map(function (min, i) {
return Math.max(
Math.ceil(min / gridSize[i]),
DEFAULT_DIMENSIONS[i]
);
});
};
// Generate a default position (in its raw format) for a frame.
// Use an index to ensure that default positions are unique.
LayoutController.prototype.defaultPosition = function (index) {
return {
position: [index, index],
dimensions: this.defaultDimensions()
};
};
// Store a computed position for a contained frame by its
// domain object id. Called in a forEach loop, so arguments
// are as expected there.
LayoutController.prototype.populatePosition = function (id, index) {
this.rawPositions[id] =
this.rawPositions[id] || this.defaultPosition(index || 0);
this.positions[id] =
this.convertPosition(this.rawPositions[id]);
};
/**
* Get a style object for a frame with the specified domain
* object identifier, suitable for use in an `ng-style`
* directive to position a frame as configured for this layout.
* @param {string} id the object identifier
* @returns {Object.<string, string>} an object with
* appropriate left, width, etc fields for positioning
*/
LayoutController.prototype.getFrameStyle = function (id) {
// Called in a loop, so just look up; the "positions"
// object is kept up to date by a watch.
return this.positions[id];
};
/**
* Start a drag gesture to move/resize a frame.
*
* The provided position and dimensions factors will determine
* whether this is a move or a resize, and what type it
* will be. For instance, a position factor of [1, 1]
* will move a frame along with the mouse as the drag
* proceeds, while a dimension factor of [0, 0] will leave
* dimensions unchanged. Combining these in different
* ways results in different handles; a position factor of
* [1, 0] and a dimensions factor of [-1, 0] will implement
* a left-edge resize, as the horizontal position will move
* with the mouse while the horizontal dimensions shrink in
* kind (and vertical properties remain unmodified.)
*
* @param {string} id the identifier of the domain object
* in the frame being manipulated
* @param {number[]} posFactor the position factor
* @param {number[]} dimFactor the dimensions factor
*/
LayoutController.prototype.startDrag = function (id, posFactor, dimFactor) {
this.activeDragId = id;
this.activeDrag = new LayoutDrag(
this.rawPositions[id],
posFactor,
dimFactor,
this.gridSize
);
};
/**
* Continue an active drag gesture.
* @param {number[]} delta the offset, in pixels,
* of the current pointer position, relative
* to its position when the drag started
*/
LayoutController.prototype.continueDrag = function (delta) {
if (this.activeDrag) {
this.rawPositions[this.activeDragId] =
this.activeDrag.getAdjustedPosition(delta);
this.populatePosition(this.activeDragId);
}
};
/**
* Compute panel positions based on the layout's object model.
* Defined as member function to facilitate testing.
* @private
*/
LayoutController.prototype.layoutPanels = function (ids) {
var configuration = this.$scope.configuration || {},
self = this;
// Pull panel positions from configuration
this.rawPositions =
shallowCopy(configuration.panels || {}, ids);
// Clear prior computed positions
this.positions = {};
// Update width/height that we are tracking
this.gridSize =
(this.$scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE;
// Compute positions and add defaults where needed
ids.forEach(function (id, index) {
self.populatePosition(id, index);
});
};
/**
* End the active drag gesture. This will update the
* view configuration.
*/
LayoutController.prototype.endDrag = function () {
this.dragInProgress = true;
setTimeout(function () {
this.dragInProgress = false;
}.bind(this), 0);
this.endDragInScope();
};
/**
* Checks if the object is currently selected.
*
* @param {string} obj the object to check for selection
* @returns {boolean} true if selected, otherwise false
*/
LayoutController.prototype.selected = function (obj) {
var sobj = this.openmct.selection.get()[0];
return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false;
};
/**
* Bypasses selection if drag is in progress.
*
* @param event the angular event object
*/
LayoutController.prototype.bypassSelection = function (event) {
if (this.dragInProgress) {
if (event) {
event.stopPropagation();
}
return;
}
};
/**
* Checks if the domain object is drilled in.
*
* @param domainObject the domain object
* @return true if the object is drilled in, false otherwise
*/
LayoutController.prototype.isDrilledIn = function (domainObject) {
return this.drilledIn === domainObject.getId();
};
/**
* Puts the given object in the drilled-in mode.
*
* @param event the angular event object
* @param domainObject the domain object
*/
LayoutController.prototype.drill = function (event, domainObject) {
if (event) {
event.stopPropagation();
}
if (!domainObject.getCapability('editor').inEditContext()) {
return;
}
if (!domainObject.hasCapability('composition')) {
return;
}
// Disable since fixed position doesn't use the selection API yet
if (domainObject.getModel().type === 'telemetry.fixed') {
return;
}
this.drilledIn = domainObject.getId();
};
/**
* Check if the object has frame.
*
* @param {object} obj the object
* @return {boolean} true if object has frame, otherwise false
*/
LayoutController.prototype.hasFrame = function (obj) {
return this.frames[obj.getId()];
};
/**
* Get the size of the grid, in pixels. The returned array
* is in the form `[x, y]`.
* @returns {number[]} the grid size
*/
LayoutController.prototype.getGridSize = function () {
return this.gridSize;
};
/**
* Gets the selection context.
*
* @param domainObject the domain object
* @returns {object} the context object which includes item and oldItem
*/
LayoutController.prototype.getContext = function (domainObject) {
return {
item: domainObject.useCapability('adapter'),
oldItem: domainObject
};
};
LayoutController.prototype.commit = function () {
var model = this.$scope.model;
model.configuration = model.configuration || {};
model.configuration.layout = this.$scope.configuration;
this.$scope.domainObject.useCapability('mutation', function () {
return model;
});
};
/**
* Selects a newly-dropped object.
*
* @param classSelector the css class selector
* @param domainObject the domain object
*/
LayoutController.prototype.selectIfNew = function (selector, domainObject) {
if (domainObject.getId() === this.droppedIdToSelectAfterRefresh) {
setTimeout(function () {
$('[data-layout-id="' + selector + '"]')[0].click();
delete this.droppedIdToSelectAfterRefresh;
}.bind(this), 0);
}
};
return LayoutController;
}
);

View File

@ -0,0 +1,479 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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(
[
"../src/LayoutController",
"zepto"
],
function (
LayoutController,
$
) {
describe("The Layout controller", function () {
var mockScope,
mockEvent,
testModel,
testConfiguration,
controller,
mockCompositionCapability,
mockComposition,
mockCompositionObjects,
mockOpenMCT,
mockSelection,
mockDomainObjectCapability,
mockObjects,
unlistenFunc,
$element = [],
selectable = [];
function mockPromise(value) {
return {
then: function (thenFunc) {
return mockPromise(thenFunc(value));
}
};
}
function mockDomainObject(id) {
return {
getId: function () {
return id;
},
useCapability: function () {
return mockCompositionCapability;
},
getModel: function () {
if (id === 'b') {
return {
type : 'hyperlink'
};
} else {
return {};
}
},
getCapability: function () {
return mockDomainObjectCapability;
},
hasCapability: function (param) {
if (param === 'composition') {
return id !== 'b';
}
},
type: "testType"
};
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
["$watch", "$watchCollection", "$on"]
);
mockEvent = jasmine.createSpyObj(
'event',
['preventDefault', 'stopPropagation']
);
testModel = {};
mockComposition = ["a", "b", "c"];
mockCompositionObjects = mockComposition.map(mockDomainObject);
testConfiguration = {
panels: {
a: {
position: [20, 10],
dimensions: [5, 5]
}
}
};
unlistenFunc = jasmine.createSpy("unlisten");
mockDomainObjectCapability = jasmine.createSpyObj('capability',
['inEditContext', 'listen']
);
mockDomainObjectCapability.listen.and.returnValue(unlistenFunc);
mockCompositionCapability = mockPromise(mockCompositionObjects);
mockScope.domainObject = mockDomainObject("mockDomainObject");
mockScope.model = testModel;
mockScope.configuration = testConfiguration;
selectable[0] = {
context: {
oldItem: mockScope.domainObject
}
};
mockSelection = jasmine.createSpyObj("selection", [
'select',
'on',
'off',
'get'
]);
mockSelection.get.and.returnValue(selectable);
mockObjects = jasmine.createSpyObj('objects', [
'get'
]);
mockObjects.get.and.returnValue(mockPromise(mockDomainObject("mockObject")));
mockOpenMCT = {
selection: mockSelection,
objects: mockObjects
};
$element = $('<div></div>');
$(document).find('body').append($element);
spyOn($element[0], 'click');
spyOn(mockScope.domainObject, "useCapability").and.callThrough();
controller = new LayoutController(mockScope, $element, mockOpenMCT);
spyOn(controller, "layoutPanels").and.callThrough();
spyOn(controller, "commit");
jasmine.clock().install();
});
afterEach(function () {
$element.remove();
jasmine.clock().uninstall();
});
it("listens for selection change events", function () {
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
it("cleans up on scope destroy", function () {
expect(mockScope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
mockScope.$on.calls.all()[0].args[1]();
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
'change',
jasmine.any(Function)
);
});
// Model changes will indicate that panel positions
// may have changed, for instance.
it("watches for changes to composition", function () {
expect(mockScope.$watchCollection).toHaveBeenCalledWith(
"model.composition",
jasmine.any(Function)
);
});
it("Retrieves updated composition from composition capability", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
expect(mockScope.domainObject.useCapability).toHaveBeenCalledWith(
"composition"
);
expect(controller.layoutPanels).toHaveBeenCalledWith(
mockComposition
);
});
it("Is robust to concurrent changes to composition", function () {
var secondMockComposition = ["a", "b", "c", "d"],
secondMockCompositionObjects = secondMockComposition.map(mockDomainObject),
firstCompositionCB,
secondCompositionCB;
spyOn(mockCompositionCapability, "then");
mockScope.$watchCollection.calls.mostRecent().args[1]();
mockScope.$watchCollection.calls.mostRecent().args[1]();
firstCompositionCB = mockCompositionCapability.then.calls.all()[0].args[0];
secondCompositionCB = mockCompositionCapability.then.calls.all()[1].args[0];
//Resolve promises in reverse order
secondCompositionCB(secondMockCompositionObjects);
firstCompositionCB(mockCompositionObjects);
//Expect the promise call that was initiated most recently to
// be the one used to populate scope, irrespective of order that
// it was eventually resolved
expect(mockScope.composition).toBe(secondMockCompositionObjects);
});
it("provides styles for frames, from configuration", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
expect(controller.getFrameStyle("a")).toEqual({
top: "320px",
left: "640px",
width: "160px",
height: "160px",
minWidth : '160px',
minHeight : '160px'
});
});
it("provides default styles for frames", function () {
var styleB, styleC;
// b and c do not have configured positions
mockScope.$watchCollection.calls.mostRecent().args[1]();
styleB = controller.getFrameStyle("b");
styleC = controller.getFrameStyle("c");
// Should have a position, but we don't care what
expect(styleB.left).toBeDefined();
expect(styleB.top).toBeDefined();
expect(styleC.left).toBeDefined();
expect(styleC.top).toBeDefined();
// Should have ensured some difference in position
expect(styleB).not.toEqual(styleC);
});
it("allows panels to be dragged", function () {
// Populate scope
mockScope.$watchCollection.calls.mostRecent().args[1]();
// Verify precondition
expect(testConfiguration.panels.b).not.toBeDefined();
// Do a drag
controller.startDrag("b", [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
// We do not look closely at the details here;
// that is tested in LayoutDragSpec. Just make sure
// that a configuration for b has been defined.
expect(testConfiguration.panels.b).toBeDefined();
});
it("invokes commit after drag", function () {
// Populate scope
mockScope.$watchCollection.calls.mostRecent().args[1]();
// Do a drag
controller.startDrag("b", [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
expect(controller.commit).toHaveBeenCalled();
});
it("listens for drop events", function () {
// Layout should position panels according to
// where the user dropped them, so it needs to
// listen for drop events.
expect(mockScope.$on).toHaveBeenCalledWith(
'mctDrop',
jasmine.any(Function)
);
// Verify precondition
expect(testConfiguration.panels.d).not.toBeDefined();
// Notify that a drop occurred
mockScope.$on.calls.mostRecent().args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
expect(testConfiguration.panels.d).toBeDefined();
expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(controller.commit).toHaveBeenCalled();
});
it("ignores drops when default has been prevented", function () {
// Avoids redundant drop-handling, WTD-1233
mockEvent.defaultPrevented = true;
// Notify that a drop occurred
mockScope.$on.calls.mostRecent().args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
expect(testConfiguration.panels.d).not.toBeDefined();
});
it("ensures a minimum frame size", function () {
var styleB;
// Start with a very small frame size
testModel.layoutGrid = [1, 1];
// White-boxy; we know which watch is which
mockScope.$watch.calls.all()[0].args[1](testModel.layoutGrid);
mockScope.$watchCollection.calls.all()[0].args[1](testModel.composition);
styleB = controller.getFrameStyle("b");
// Resulting size should still be reasonably large pixel-size
expect(parseInt(styleB.width, 10)).toBeGreaterThan(63);
expect(parseInt(styleB.width, 10)).toBeGreaterThan(31);
});
it("ensures a minimum frame size on drop", function () {
var style;
// Start with a very small frame size
testModel.layoutGrid = [1, 1];
mockScope.$watch.calls.all()[0].args[1](testModel.layoutGrid);
// Add a new object to the composition
mockComposition = ["a", "b", "c", "d"];
mockCompositionObjects = mockComposition.map(mockDomainObject);
mockCompositionCapability = mockPromise(mockCompositionObjects);
// Notify that a drop occurred
mockScope.$on.calls.mostRecent().args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
style = controller.getFrameStyle("d");
// Resulting size should still be reasonably large pixel-size
expect(parseInt(style.width, 10)).toBeGreaterThan(63);
expect(parseInt(style.height, 10)).toBeGreaterThan(31);
});
it("updates positions of existing objects on a drop", function () {
var oldStyle;
mockScope.$watchCollection.calls.mostRecent().args[1]();
oldStyle = controller.getFrameStyle("b");
expect(oldStyle).toBeDefined();
// ...drop event...
mockScope.$on.calls.mostRecent()
.args[1](mockEvent, 'b', { x: 300, y: 100 });
expect(controller.getFrameStyle("b"))
.not.toEqual(oldStyle);
});
it("allows objects to be selected", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
expect(controller.selected(childObj)).toBe(true);
});
it("prevents event bubbling while drag is in progress", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
var childObj = mockCompositionObjects[0];
// Do a drag
controller.startDrag(childObj.getId(), [1, 1], [0, 0]);
controller.continueDrag([100, 100]);
controller.endDrag();
// Because mouse position could cause the parent object to be selected, this should be ignored.
controller.bypassSelection(mockEvent);
expect(mockEvent.stopPropagation).toHaveBeenCalled();
// Shoud be able to select another object when dragging is done.
jasmine.clock().tick(0);
mockEvent.stopPropagation.calls.reset();
controller.bypassSelection(mockEvent);
expect(mockEvent.stopPropagation).not.toHaveBeenCalled();
});
it("shows frames by default", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
expect(controller.hasFrame(mockCompositionObjects[0])).toBe(true);
});
it("hyperlinks hide frame by default", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
expect(controller.hasFrame(mockCompositionObjects[1])).toBe(false);
});
it("selects the parent object when selected object is removed", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
var childObj = mockCompositionObjects[0];
selectable[0].context.oldItem = childObj;
mockOpenMCT.selection.on.calls.mostRecent().args[1](selectable);
var composition = ["b", "c"];
mockScope.$watchCollection.calls.mostRecent().args[1](composition);
expect($element[0].click).toHaveBeenCalled();
});
it("allows objects to be drilled-in only when editing", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
var childObj = mockCompositionObjects[0];
childObj.getCapability().inEditContext.and.returnValue(false);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("allows objects to be drilled-in only if it has sub objects", function () {
mockScope.$watchCollection.calls.mostRecent().args[1]();
var childObj = mockCompositionObjects[1];
childObj.getCapability().inEditContext.and.returnValue(true);
controller.drill(mockEvent, childObj);
expect(controller.isDrilledIn(childObj)).toBe(false);
});
it("selects a newly-dropped object", function () {
mockScope.$on.calls.mostRecent().args[1](
mockEvent,
'd',
{ x: 300, y: 100 }
);
var childObj = mockDomainObject("d");
var testElement = $("<div data-layout-id='some-id'></div>");
$element.append(testElement);
spyOn(testElement[0], 'click');
controller.selectIfNew('some-id', childObj);
jasmine.clock().tick(0);
expect(testElement[0].click).toHaveBeenCalled();
});
});
}
);

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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([
'./src/controllers/ListViewController',
'./src/directives/MCTGesture',
'./res/templates/listview.html',
'legacyRegistry'
], function (
ListViewController,
MCTGesture,
listViewTemplate,
legacyRegistry
) {
legacyRegistry.register("platform/features/listview", {
"name": "List View Plugin",
"description": "Allows folder contents to be shown in list format",
"extensions":
{
"views": [
{
"key": "list",
"type": "folder",
"name": "List",
"cssClass": "icon-list-view",
"template": listViewTemplate
}
],
"controllers": [
{
"key": "ListViewController",
"implementation": ListViewController,
"depends": ["$scope", "formatService"]
}
],
"directives": [
{
"key": "mctGesture",
"implementation" : MCTGesture,
"depends": ["gestureService"]
}
]
}
});
});

View File

@ -0,0 +1,88 @@
<!--
Open MCT, Copyright (c) 2014-2018, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT 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 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.
-->
<div ng-controller="ListViewController">
<table class="list-view">
<thead>
<tr>
<th class="sortable"
ng-click="reverseSort = (orderByField === 'title')? !reverseSort:false; orderByField='title';"
ng-class="{
sort: orderByField == 'title',
asc: !reverseSort,
desc: reverseSort
}">
Name
</th>
<th class="sortable"
ng-click="reverseSort = (orderByField === 'type')? !reverseSort:false; orderByField='type';"
ng-class="{
sort: orderByField == 'type',
asc: !reverseSort,
desc: reverseSort
}">
Type
</th>
<th class="sortable"
ng-click="reverseSort = (orderByField === 'persisted')? !reverseSort:true; orderByField='persisted';"
ng-class="{
sort: orderByField == 'persisted',
asc: !reverseSort,
desc: reverseSort
}">
Created Date
</th>
<th class="sortable"
ng-click="reverseSort = (orderByField === 'modified')? !reverseSort:true; orderByField='modified';"
ng-class="{
sort: orderByField == 'modified',
asc: !reverseSort,
desc: reverseSort
}">
Update Date
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="child in children|orderBy:orderByField:reverseSort"
mct-gesture="['menu','info']"
mct-object="child.asDomainObject"
ng-click="child.action.perform('navigate')">
<td>
<div class="l-flex-row">
<span class="flex-elem t-item-icon" ng-class="{ 'l-icon-link': child.location.isLink()}">
<span class="t-item-icon-glyph {{child.icon}}"></span>
</span>
<span class="t-title-label flex-elem grows">{{child.title}}</span>
</div>
</td>
<td>{{child.type}}</td>
<td>{{child.persisted}}</td>
<td>{{child.modified}}</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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(function () {
function ListViewController($scope, formatService) {
this.$scope = $scope;
$scope.orderByField = 'title';
$scope.reverseSort = false;
this.updateView();
var unlisten = $scope.domainObject.getCapability('mutation')
.listen(this.updateView.bind(this));
this.utc = formatService.getFormat('utc');
//Trigger digestive cycle with $apply to update list view
setTimeout(function () {
$scope.$apply();
});
$scope.$on('$destroy', function () {
unlisten();
});
}
ListViewController.prototype.updateView = function () {
this.$scope.domainObject.useCapability('composition')
.then(function (children) {
var formattedChildren = this.formatChildren(children);
this.$scope.children = formattedChildren;
this.$scope.data = {children: formattedChildren};
}.bind(this)
);
};
ListViewController.prototype.formatChildren = function (children) {
return children.map(function (child) {
return {
icon: child.getCapability('type').getCssClass(),
title: child.getModel().name,
type: child.getCapability('type').getName(),
persisted: this.utc.format(child.getModel().persisted),
modified: this.utc.format(child.getModel().modified),
asDomainObject: child,
location: child.getCapability('location'),
action: child.getCapability('action')
};
}, this);
};
return ListViewController;
});

View File

@ -21,21 +21,24 @@
*****************************************************************************/
define(function () {
function DisplayLayoutType() {
function MCTGesture(gestureService) {
return {
name: "Display Layout",
creatable: true,
cssClass: 'icon-layout',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
layout: {
panels: {}
}
};
restrict : 'A',
scope: {
domainObject: '=mctObject'
},
link : function ($scope, $element, attrs) {
var activeGestures = gestureService.attachGestures(
$element,
$scope.domainObject,
$scope.$eval(attrs.mctGesture)
);
$scope.$on('$destroy', function () {
activeGestures.destroy();
delete this.activeGestures;
});
}
}
};
}
return DisplayLayoutType;
});
return MCTGesture;
});

View File

@ -0,0 +1,157 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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(
["../../src/controllers/ListViewController"],
function (ListViewController) {
describe("The Controller for the ListView", function () {
var scope,
unlistenFunc,
domainObject,
childObject,
childModel,
typeCapability,
mutationCapability,
formatService,
compositionPromise,
controller;
beforeEach(function () {
unlistenFunc = jasmine.createSpy("unlisten");
mutationCapability = jasmine.createSpyObj(
"mutationCapability",
["listen"]
);
mutationCapability.listen.and.returnValue(unlistenFunc);
formatService = jasmine.createSpyObj(
"formatService",
["getFormat"]
);
formatService.getFormat.and.returnValue(jasmine.createSpyObj(
'utc',
["format"]
));
formatService.getFormat().format.and.callFake(function (v) {
return "formatted " + v;
});
typeCapability = jasmine.createSpyObj(
"typeCapability",
["getCssClass", "getName"]
);
typeCapability.getCssClass.and.returnValue("icon-folder");
typeCapability.getName.and.returnValue("Folder");
childModel = jasmine.createSpyObj(
"childModel",
["persisted", "modified", "name"]
);
childModel.persisted = 1496867697303;
childModel.modified = 1496867697303;
childModel.name = "Battery Charge Status";
childObject = jasmine.createSpyObj(
"childObject",
["getModel", "getCapability"]
);
childObject.getModel.and.returnValue(
childModel
);
childObject.getCapability.and.callFake(function (arg) {
if (arg === 'location') {
return '';
} else if (arg === 'type') {
return typeCapability;
}
});
childObject.location = '';
domainObject = jasmine.createSpyObj(
"domainObject",
["getCapability", "useCapability"]
);
compositionPromise = Promise.resolve([childObject]);
domainObject.useCapability.and.returnValue(compositionPromise);
domainObject.getCapability.and.returnValue(
mutationCapability
);
scope = jasmine.createSpyObj(
"$scope",
["$on", "$apply"]
);
scope.domainObject = domainObject;
controller = new ListViewController(scope, formatService);
return compositionPromise;
});
it("uses the UTC time format", function () {
expect(formatService.getFormat).toHaveBeenCalledWith('utc');
});
it("updates the view", function () {
var child = scope.children[0];
var testChild = {
icon: "icon-folder",
title: "Battery Charge Status",
type: "Folder",
persisted: formatService.getFormat('utc')
.format(childModel.persisted),
modified: formatService.getFormat('utc')
.format(childModel.modified),
asDomainObject: childObject,
location: '',
action: childObject.getCapability('action')
};
expect(child).toEqual(testChild);
});
it("updates the scope when mutation occurs", function () {
var applyPromise = new Promise(function (resolve) {
scope.$apply.and.callFake(resolve);
});
domainObject.useCapability.and.returnValue(Promise.resolve([]));
expect(mutationCapability.listen).toHaveBeenCalledWith(jasmine.any(Function));
mutationCapability.listen.calls.mostRecent().args[0]();
return applyPromise.then(function () {
expect(scope.children.length).toEqual(0);
expect(scope.$apply).toHaveBeenCalled();
});
});
it("releases listeners on $destroy", function () {
expect(scope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
scope.$on.calls.mostRecent().args[1]();
expect(unlistenFunc).toHaveBeenCalled();
});
});
}
);

View File

@ -0,0 +1,86 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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(
["../../src/directives/MCTGesture"],
function (MCTGesture) {
describe("The Gesture Listener for the ListView items", function () {
var mctGesture,
gestureService,
scope,
element,
attrs,
attachedGesture;
beforeEach(function () {
attachedGesture = jasmine.createSpyObj(
"attachedGesture",
['destroy']
);
gestureService = jasmine.createSpyObj(
"gestureService",
["attachGestures"]
);
gestureService.attachGestures.and.returnValue(
attachedGesture
);
mctGesture = MCTGesture(gestureService);
});
it("creates a directive Object", function () {
expect(mctGesture).toBeDefined();
});
it("has link function that attaches gesture to gestureService",
function () {
attrs = {
mctGesture: "menu,info"
};
element = jasmine.createSpy("element");
scope = jasmine.createSpyObj(
"$scope",
["$on", "$eval"]
);
scope.domainObject = "fake domainObject";
mctGesture.link(scope, element, attrs);
expect(gestureService.attachGestures).toHaveBeenCalled();
}
);
it("release gesture service on $destroy", function () {
attrs = {
mctGesture: "menu,info"
};
element = jasmine.createSpy("element");
scope = jasmine.createSpyObj(
"$scope",
["$on", "$eval"]
);
scope.domainObject = "fake domainObject";
mctGesture.link(scope, element, attrs);
expect(scope.$on).toHaveBeenCalledWith(
'$destroy',
jasmine.any(Function)
);
scope.$on.calls.mostRecent().args[1]();
expect(attachedGesture.destroy).toHaveBeenCalled();
});
});
}
);

View File

@ -40,7 +40,6 @@ define([
'./styles-new/core.scss',
'./styles-new/notebook.scss',
'./ui/components/layout/Layout.vue',
'./ui/overlayService/overlayService',
'vue'
], function (
EventEmitter,
@ -62,7 +61,6 @@ define([
coreStyles,
NotebookStyles,
Layout,
OverlayService,
Vue
) {
/**
@ -223,18 +221,11 @@ define([
*/
this.indicators = new api.IndicatorAPI(this);
this.notifications = new api.NotificationAPI();
this.Dialog = api.Dialog;
this.editor = new api.EditorAPI.default(this);
this.OverlayService = new OverlayService();
this.legacyRegistry = defaultRegistry;
this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable());
this.install(this.plugins.DisplayLayout());
if (typeof BUILD_CONSTANTS !== 'undefined') {
this.install(buildInfoPlugin(BUILD_CONSTANTS));
@ -319,17 +310,14 @@ define([
this.$injector.get('objectService');
var appLayout = new Vue({
components: {
'Layout': Layout.default
},
mixins: [Layout.default],
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
}
});
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout;
this.layout = appLayout;
Browse(this);
this.router.start();
this.emit('start');

View File

@ -3,16 +3,6 @@ define([
], function (
) {
const DEFAULT_VIEW_PRIORITY = 100;
const PRIORITY_LEVELS = {
"fallback": Number.NEGATIVE_INFINITY,
"default": -100,
"none": 0,
"optional": DEFAULT_VIEW_PRIORITY,
"preferred": 1000,
"mandatory": Number.POSITIVE_INFINITY
};
function LegacyViewProvider(legacyView, openmct, convertToLegacyObject) {
console.warn(`DEPRECATION WARNING: Migrate ${legacyView.key} from ${legacyView.bundle.path} to use the new View APIs. Legacy view support will be removed soon.`);
@ -94,13 +84,6 @@ define([
scope.$destroy();
}
}
},
priority: function () {
let priority = legacyView.priority || DEFAULT_VIEW_PRIORITY;
if (typeof priority === 'string') {
priority = PRIORITY_LEVELS[priority];
}
return priority;
}
};
};

View File

@ -1,95 +0,0 @@
define([
], function (
) {
const DEFAULT_VIEW_PRIORITY = 100;
const PRIORITY_LEVELS = {
"fallback": Number.NEGATIVE_INFINITY,
"default": -100,
"none": 0,
"optional": DEFAULT_VIEW_PRIORITY,
"preferred": 1000,
"mandatory": Number.POSITIVE_INFINITY
};
function TypeInspectorViewProvider(typeDefinition, openmct, convertToLegacyObject) {
console.warn(`DEPRECATION WARNING: Migrate ${typeDefinition.key} from ${typeDefinition.bundle.path} to use the new Inspector View APIs. Legacy Inspector view support will be removed soon.`);
let representation = openmct.$injector.get('representations[]')
.filter((r) => r.key === typeDefinition.inspector)[0];
return {
key: representation.key,
name: representation.name,
cssClass: representation.cssClass,
description: representation.description,
canView: function (selection) {
if (!selection[0] || !selection[0].context.item) {
return false;
}
let domainObject = selection[0].context.item;
return domainObject.type === typeDefinition.key;
},
view: function (selection) {
let domainObject = selection[0].context.item;
let $rootScope = openmct.$injector.get('$rootScope');
let templateLinker = openmct.$injector.get('templateLinker');
let scope = $rootScope.$new();
let legacyObject = convertToLegacyObject(domainObject);
let isDestroyed = false;
scope.domainObject = legacyObject;
scope.model = legacyObject.getModel();
return {
show: function (container) {
// TODO: implement "gestures" support ?
let uses = representation.uses || [];
let promises = [];
let results = uses.map(function (capabilityKey, i) {
let result = legacyObject.useCapability(capabilityKey);
if (result.then) {
promises.push(result.then(function (r) {
results[i] = r;
}));
}
return result;
});
function link() {
if (isDestroyed) {
return;
}
uses.forEach(function (key, i) {
scope[key] = results[i];
});
templateLinker.link(
scope,
openmct.$angular.element(container),
representation
);
container.style.height = '100%';
}
if (promises.length) {
Promise.all(promises)
.then(function () {
link();
scope.$digest();
});
} else {
link();
}
},
destroy: function () {
scope.$destroy();
}
}
}
};
};
return TypeInspectorViewProvider;
});

View File

@ -1,10 +1,8 @@
define([
'./LegacyViewProvider',
'./TypeInspectorViewProvider',
'../../api/objects/object-utils'
], function (
LegacyViewProvider,
TypeInspectorViewProvider,
objectUtils
) {
function installLegacyViews(openmct, legacyViews, instantiate) {
@ -18,13 +16,6 @@ define([
legacyViews.forEach(function (legacyView) {
openmct.objectViews.addProvider(new LegacyViewProvider(legacyView, openmct, convertToLegacyObject));
});
let inspectorTypes = openmct.$injector.get('types[]')
.filter((t) => t.hasOwnProperty('inspector'));
inspectorTypes.forEach(function (typeDefinition) {
openmct.inspectorViews.addProvider(new TypeInspectorViewProvider(typeDefinition, openmct, convertToLegacyObject));
});
}
return installLegacyViews;

View File

@ -1,83 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export default class Editor extends EventEmitter {
constructor(openmct) {
super();
this.editing = false;
this.openmct = openmct;
}
/**
* Initiate an editing session. This will start a transaction during
* which any persist operations will be deferred until either save()
* or finish() are called.
*/
edit() {
this.editing = true;
this.getTransactionService().startTransaction();
this.emit('isEditing', true);
}
/**
* @returns true if the application is in edit mode, false otherwise.
*/
isEditing() {
return this.editing;
}
/**
* Save any unsaved changes from this editing session. This will
* end the current transaction.
*/
save() {
return this.getTransactionService().commit().then((result)=>{
this.editing = false;
this.emit('isEditing', false);
return result
}).catch((error)=>{
throw error;
});
}
/**
* End the currently active transaction and discard unsaved changes.
*/
cancel() {
this.getTransactionService().cancel();
this.editing = false;
this.emit('isEditing', false);
}
/**
* @private
*/
getTransactionService() {
if (!this.transactionService) {
this.transactionService = this.openmct.$injector.get('transactionService');
}
return this.transactionService;
}
}

View File

@ -28,10 +28,7 @@ define([
'./ui/Dialog',
'./ui/GestureAPI',
'./telemetry/TelemetryAPI',
'./indicators/IndicatorAPI',
'./notifications/NotificationAPI',
'./Editor'
'./indicators/IndicatorAPI'
], function (
TimeAPI,
ObjectAPI,
@ -40,9 +37,7 @@ define([
Dialog,
GestureAPI,
TelemetryAPI,
IndicatorAPI,
NotificationAPI,
EditorAPI
IndicatorAPI
) {
return {
TimeAPI: TimeAPI,
@ -52,8 +47,6 @@ define([
TypeRegistry: TypeRegistry,
GestureAPI: GestureAPI,
TelemetryAPI: TelemetryAPI,
IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI
IndicatorAPI: IndicatorAPI
};
});

View File

@ -28,7 +28,7 @@ define([
) {
function IndicatorAPI(openmct) {
this.openmct = openmct;
this.indicatorObjects = [];
this.indicatorElements = [];
}
IndicatorAPI.prototype.simpleIndicator = function () {
@ -55,7 +55,12 @@ define([
*
*/
IndicatorAPI.prototype.add = function (indicator) {
this.indicatorObjects.push(indicator);
// So that we can consistently position indicator elements,
// guarantee that they are wrapped in an element we control
var wrapperNode = document.createElement('div');
wrapperNode.className = 'h-indicator';
wrapperNode.appendChild(indicator.element);
this.indicatorElements.push(wrapperNode);
};
return IndicatorAPI;

View File

@ -1,353 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/**
* This bundle implements the notification service, which can be used to
* show banner notifications to the user. Banner notifications
* are used to inform users of events in a non-intrusive way. As
* much as possible, notifications share a model with blocking
* dialogs so that the same information can be provided in a dialog
* and then minimized to a banner notification if needed.
*
* @namespace platform/api/notifications
*/
import moment from 'moment';
import EventEmitter from 'EventEmitter';
import MCTNotification from './MCTNotification.js';
/**
* A representation of a banner notification. Banner notifications
* are used to inform users of events in a non-intrusive way. As
* much as possible, notifications share a model with blocking
* dialogs so that the same information can be provided in a dialog
* and then minimized to a banner notification if needed, or vice-versa.
*
* @typedef {object} NotificationModel
* @property {string} title The title of the message
* @property {string} severity The importance of the message (one of
* 'info', 'alert', or 'error' where info < alert <error)
* @property {number} [progress] The completion status of a task
* represented numerically
* @property {boolean} [unknownProgress] a boolean indicating that the
* progress of the underlying task is unknown. This will result in a
* visually distinct progress bar.
* @property {boolean} [autoDismiss] If truthy, dialog will
* be automatically minimized or dismissed (depending on severity).
* Additionally, if the provided value is a number, it will be used
* as the delay period before being dismissed.
* @property {boolean} [dismissable=true] If true, notification will
* include an option to dismiss it completely.
* @see DialogModel
*/
const DEFAULT_AUTO_DISMISS_TIMEOUT = 3000;
const MINIMIZE_ANIMATION_TIMEOUT = 300;
/**
* The notification service is responsible for informing the user of
* events via the use of banner notifications.
* @memberof platform/commonUI/notification
* @constructor
* @param defaultAutoDismissTimeout The period of time that an
* auto-dismissed message will be displayed for.
* @param minimizeAnimationTimeout When notifications are minimized, a brief
* animation is shown. This animation requires some time to execute,
* so a timeout is required before the notification is hidden
*/
export default class NotificationAPI extends EventEmitter {
constructor() {
super();
this.notifications = [];
this.highest = { severity: "info" };
/*
* A context in which to hold the active notification and a
* handle to its timeout.
*/
this.activeNotification = undefined;
}
/**
* Minimize a notification. The notification will still be available
* from the notification list. Typically notifications with a
* severity of 'info' should not be minimized, but rather
* dismissed. If you're not sure which is appropriate,
* use {@link Notification#dismissOrMinimize}
*
* @private
*/
minimize(notification) {
//Check this is a known notification
let index = this.notifications.indexOf(notification);
if (this.activeTimeout) {
/*
Method can be called manually (clicking dismiss) or
automatically from an auto-timeout. this.activeTimeout
acts as a semaphore to prevent race conditions. Cancel any
timeout in progress (for the case where a manual dismiss
has shortcut an active auto-dismiss), and clear the
semaphore.
*/
clearTimeout(this.activeTimeout);
delete this.activeTimeout;
}
if (index >= 0) {
notification.model.minimized = true;
//Add a brief timeout before showing the next notification
// in order to allow the minimize animation to run through.
setTimeout(() => {
notification.emit('destroy');
this.setActiveNotification(this.selectNextNotification());
}, MINIMIZE_ANIMATION_TIMEOUT);
}
}
/**
* Completely removes a notification. This will dismiss it from the
* message banner and remove it from the list of notifications.
* Typically only notifications with a severity of info should be
* dismissed. If you're not sure whether to dismiss or minimize a
* notification, use {@link Notification#dismissOrMinimize}.
* dismiss
*
* @private
*/
dismiss(notification) {
//Check this is a known notification
let index = this.notifications.indexOf(notification);
if (this.activeTimeout) {
/* Method can be called manually (clicking dismiss) or
* automatically from an auto-timeout. this.activeTimeout
* acts as a semaphore to prevent race conditions. Cancel any
* timeout in progress (for the case where a manual dismiss
* has shortcut an active auto-dismiss), and clear the
* semaphore.
*/
clearTimeout(this.activeTimeout);
delete this.activeTimeout;
}
if (index >= 0) {
this.notifications.splice(index, 1);
}
this.setActiveNotification(this.selectNextNotification());
this.setHighestSeverity();
notification.emit('destroy');
}
/**
* Depending on the severity of the notification will selectively
* dismiss or minimize where appropriate.
*
* @private
*/
dismissOrMinimize(notification) {
let model = notification.model;
if (model.severity === "info") {
if (model.autoDismiss === false) {
this.minimize(notification);
} else {
this.dismiss(notification);
}
} else {
this.minimize(notification);
}
}
/**
* Returns the notification that is currently visible in the banner area
* @returns {Notification}
*/
getActiveNotification() {
return this.activeNotification;
}
/**
* A convenience method for info notifications. Notifications
* created via this method will be auto-destroy after a default
* wait period unless explicitly forbidden by the caller through
* the {autoDismiss} property on the {NotificationModel}, in which
* case the notification will be minimized after the wait.
* @param {NotificationModel | string} message either a string for
* the title of the notification message, or a {@link NotificationModel}
* defining the options notification to display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
info(message) {
let notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "info";
return this.notify(notificationModel);
}
/**
* A convenience method for alert notifications. Notifications
* created via this method will will have severity of "alert" enforced
* @param {NotificationModel | string} message either a string for
* the title of the alert message with default options, or a
* {@link NotificationModel} defining the options notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
alert(message) {
let notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "alert";
return this.notify(notificationModel);
}
/**
* A convenience method for error notifications. Notifications
* created via this method will will have severity of "error" enforced
* @param {NotificationModel | string} message either a string for
* the title of the error message with default options, or a
* {@link NotificationModel} defining the options of the notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to dismiss or minimize
*/
error(message) {
let notificationModel = typeof message === "string" ? {title: message} : message;
notificationModel.severity = "error";
return this.notify(notificationModel);
}
/**
* @private
*/
setHighestSeverity() {
let severity = {
"info": 1,
"alert": 2,
"error": 3
};
this.highest.severity = this.notifications.reduce((previous, notification) => {
if (severity[notification.model.severity] > severity[previous]) {
return notification.model.severity;
} else {
return previous;
}
}, "info");
}
/**
* Notifies the user of an event. If there is a banner notification
* already active, then it will be dismissed or minimized automatically,
* and the provided notification displayed in its place.
*
* @param {NotificationModel} notificationModel The notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to {@link Notification#dismiss} or {@link Notification#minimize}
*/
notify(notificationModel) {
let notification;
let activeNotification = this.activeNotification;
notificationModel.severity = notificationModel.severity || "info";
notificationModel.timestamp = moment.utc().format('YYYY-MM-DD hh:mm:ss.ms');
notification = new MCTNotification(notificationModel, this);
this.notifications.push(notification);
this.setHighestSeverity();
/*
Check if there is already an active (ie. visible) notification
*/
if (!this.activeNotification) {
this.setActiveNotification(notification);
} else if (!this.activeTimeout) {
/*
If there is already an active notification, time it out. If it's
already got a timeout in progress (either because it has had
timeout forced because of a queue of messages, or it had an
autodismiss specified), leave it to run. Otherwise force a
timeout.
This notification has been added to queue and will be
serviced as soon as possible.
*/
this.activeTimeout = setTimeout(() => {
this.dismissOrMinimize(activeNotification);
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
}
return notification;
}
/**
* Used internally by the NotificationService
* @private
*/
setActiveNotification(notification) {
let shouldAutoDismiss;
this.activeNotification = notification;
if (!notification) {
delete this.activeTimeout;
return;
}
this.emit('notification', notification);
if (notification.model.severity === "info") {
shouldAutoDismiss = true;
} else {
shouldAutoDismiss = notification.model.autoDismiss;
}
if (shouldAutoDismiss || this.selectNextNotification()) {
this.activeTimeout = setTimeout(() => {
this.dismissOrMinimize(notification);
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
} else {
delete this.activeTimeout;
}
}
/**
* Used internally by the NotificationService
*
* @private
*/
selectNextNotification() {
let notification;
let i = 0;
/*
Loop through the notifications queue and find the first one that
has not already been minimized (manually or otherwise).
*/
for (; i < this.notifications.length; i++) {
notification = this.notifications[i];
if (!notification.model.minimized &&
notification !== this.activeNotification) {
return notification;
}
}
}
}

View File

@ -55,7 +55,11 @@ define([
'../platform/exporters/bundle',
'../platform/features/clock/bundle',
'../platform/features/fixed/bundle',
'../platform/features/conductor/core/bundle',
'../platform/features/conductor/compatibility/bundle',
'../platform/features/imagery/bundle',
'../platform/features/layout/bundle',
'../platform/features/listview/bundle',
'../platform/features/my-items/bundle',
'../platform/features/pages/bundle',
'../platform/features/hyperlink/bundle',
@ -98,6 +102,8 @@ define([
'platform/features/clock',
'platform/features/fixed',
'platform/features/imagery',
'platform/features/layout',
'platform/features/listview',
'platform/features/pages',
'platform/features/hyperlink',
'platform/features/timeline',

View File

@ -1,327 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
<template>
<div class="l-layout"
@dragover="handleDragOver"
@click="bypassSelection"
@drop="handleDrop">
<div class="l-layout__object">
<!-- Background grid -->
<div class="l-layout__grid-holder c-grid"
v-if="!drilledIn">
<div class="c-grid__x l-grid l-grid-x"
v-if="gridSize[0] >= 3"
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]">
</div>
<div class="c-grid__y l-grid l-grid-y"
v-if="gridSize[1] >= 3"
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"></div>
</div>
<layout-frame v-for="item in frameItems"
class="l-layout__frame"
:key="item.id"
:item="item"
:gridSize="gridSize"
@drilledIn="updateDrilledInState"
@dragInProgress="updatePosition"
@endDrag="endDrag">
</layout-frame>
</div>
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
.l-layout,
.c-grid,
.c-grid__x,
.c-grid__y {
@include abs();
}
.l-layout {
display: flex;
flex-direction: column;
&__grid-holder {
display: none;
}
&__object {
flex: 1 1 auto;
overflow: auto;
}
&__frame {
position: absolute;
}
}
.c-grid {
pointer-events: none;
&__x { @include bgTicks($colorGridLines, 'x'); }
&__y { @include bgTicks($colorGridLines, 'y'); }
}
.is-editing {
.l-shell__main-container > .l-layout {
// Target the top-most layout container and color its background
background: rgba($editColor, 0.1);
}
.s-selected,
.s-selected-parent {
.l-layout {
// Show the layout grid for the top-most child of the current selection,
// and hide the grid for deeper nested levels.
[class*="__grid-holder"] {
display: block;
}
.l-layout [class*="__grid-holder"] {
display: none;
}
}
}
}
</style>
<script>
import LayoutFrame from './LayoutFrame.vue';
const DEFAULT_GRID_SIZE = [32, 32],
DEFAULT_DIMENSIONS = [12, 8],
DEFAULT_POSITION = [0, 0],
MINIMUM_FRAME_SIZE = [320, 180],
DEFAULT_HIDDEN_FRAME_TYPES = [
'hyperlink'
];
export default {
data() {
return {
gridSize: [],
frameItems: [],
frames: [],
frameStyles: [],
rawPositions: {},
initSelect: true,
drilledIn: undefined
}
},
inject: ['openmct'],
props: ['domainObject'],
components: {
LayoutFrame
},
created: function () {
this.newDomainObject = this.domainObject;
this.gridSize = this.newDomainObject.layoutGrid || DEFAULT_GRID_SIZE;
this.composition = this.openmct.composition.get(this.newDomainObject);
let panels = (((this.newDomainObject.configuration || {}).layout || {}).panels || {});
if (this.composition !== undefined) {
this.composition.load().then((composition) => {
composition.forEach(function (domainObject) {
this.readLayoutConfiguration(domainObject, panels);
this.makeFrameItem(domainObject, false);
}.bind(this));
this.composition.on('add', this.onAddComposition);
this.composition.on('remove', this.onRemoveComposition);
});
}
this.unlisten = this.openmct.objects.observe(this.newDomainObject, '*', function (obj) {
this.newDomainObject = JSON.parse(JSON.stringify(obj));
this.gridSize = this.newDomainObject.layoutGrid || DEFAULT_GRID_SIZE;;
}.bind(this));
},
methods: {
readLayoutConfiguration(domainObject, panels) {
let id = this.openmct.objects.makeKeyString(domainObject.identifier);
this.rawPositions[id] = {
position: panels[id].position || DEFAULT_POSITION,
dimensions: panels[id].dimensions || this.defaultDimensions()
};
this.frameStyles[id] = this.convertPosition(this.rawPositions[id]);
this.frames[id] = panels[id].hasOwnProperty('hasFrame') ?
panels[id].hasFrame :
this.hasFrameByDefault(domainObject.type);
},
makeFrameItem(domainObject, initSelect) {
let id = this.openmct.objects.makeKeyString(domainObject.identifier);
this.frameItems.push({
id: id,
hasFrame: this.frames[id],
domainObject,
style: this.frameStyles[id],
drilledIn: this.isDrilledIn(id),
initSelect: initSelect,
rawPosition: this.rawPositions[id]
});
},
onAddComposition(domainObject) {
let id = this.openmct.objects.makeKeyString(domainObject.identifier);
this.rawPositions[id] = {
position: [
Math.floor(this.droppedObjectPosition.x / this.gridSize[0]),
Math.floor(this.droppedObjectPosition.y / this.gridSize[1])
],
dimensions: this.defaultDimensions()
};
this.frameStyles[id] = this.convertPosition(this.rawPositions[id]);
this.frames[id] = this.hasFrameByDefault(domainObject.type);
let newPanel = this.rawPositions[id];
newPanel.hasFrame = this.frames[id];
this.mutate("configuration.layout.panels[" + id + "]", newPanel);
this.makeFrameItem(domainObject, true);
},
onRemoveComposition(identifier) {
// TODO: remove the object from frameItems
},
defaultDimensions() {
let gridSize = this.gridSize;
return MINIMUM_FRAME_SIZE.map(function (min, i) {
return Math.max(
Math.ceil(min / gridSize[i]),
DEFAULT_DIMENSIONS[i]
);
});
},
convertPosition(raw) {
return {
left: (this.gridSize[0] * raw.position[0]) + 'px',
top: (this.gridSize[1] * raw.position[1]) + 'px',
width: (this.gridSize[0] * raw.dimensions[0]) + 'px',
height: (this.gridSize[1] * raw.dimensions[1]) + 'px',
minWidth: (this.gridSize[0] * raw.dimensions[0]) + 'px',
minHeight: (this.gridSize[1] * raw.dimensions[1]) + 'px'
};
},
/**
* Checks if the frame should be hidden or not.
*
* @param type the domain object type
* @return {boolean} true if the object should have
* frame by default, false, otherwise
*/
hasFrameByDefault(type) {
return DEFAULT_HIDDEN_FRAME_TYPES.indexOf(type) === -1;
},
setSelection(selection) {
if (selection.length === 0) {
return;
}
this.updateDrilledInState();
},
updateDrilledInState(id) {
this.drilledIn = id;
this.frameItems.forEach(function (item) {
item.drilledIn = item.id === id;
});
},
isDrilledIn(id) {
return this.drilledIn === id;
},
updatePosition(id, newPosition) {
let newStyle = this.convertPosition(newPosition);
this.frameStyles[id] = newStyle;
this.rawPositions[id] = newPosition;
this.frameItems.forEach(function (item) {
if (item.id === id) {
item.style = newStyle;
item.rawPosition = newPosition;
}
});
},
bypassSelection($event) {
if (this.dragInProgress) {
if ($event) {
$event.stopImmediatePropagation();
}
return;
}
},
endDrag(id) {
this.dragInProgress = true;
setTimeout(function () {
this.dragInProgress = false;
}.bind(this), 0);
let path = "configuration.layout.panels[" + id + "]";
this.mutate(path + ".dimensions", this.rawPositions[id].dimensions);
this.mutate(path + ".position", this.rawPositions[id].position);
},
mutate(path, value) {
this.openmct.objects.mutate(this.newDomainObject, path, value);
},
handleDrop($event) {
$event.preventDefault();
let child = JSON.parse($event.dataTransfer.getData('domainObject'));
let duplicates = [];
let composition = this.newDomainObject.composition;
composition.forEach((object) => {
if (this.openmct.objects.makeKeyString(JSON.parse(JSON.stringify(object))) ===
this.openmct.objects.makeKeyString(child.identifier)) {
duplicates.push(object);
}
});
// Disallow adding a duplicate object to the composition
if (duplicates.length !== 0) {
return;
}
let elementRect = this.$el.getBoundingClientRect();
this.droppedObjectPosition = {
x: $event.pageX - elementRect.left,
y: $event.pageY - elementRect.top
}
// TODO: use the composition API to add child once the default composition
// provider supports it instead of mutating the composition directly.
// this.composition.add(child).
composition.push(child.identifier);
this.mutate('composition', composition);
},
handleDragOver($event){
$event.preventDefault();
}
},
mounted() {
this.openmct.selection.on('change', this.setSelection);
},
destroyed: function () {
this.composition.off('add', this.onAddComposition);
this.composition.off('remove', this.onRemoveComposition);
this.openmct.off('change', this.selection);
this.unlisten();
}
}
</script>

View File

@ -1,115 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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(
[],
function () {
/**
* Handles drag interactions on frames in layouts. This will
* provides new positions/dimensions for frames based on
* relative pixel positions provided; these will take into account
* the grid size (in a snap-to sense) and will enforce some minimums
* on both position and dimensions.
*
* The provided position and dimensions factors will determine
* whether this is a move or a resize, and what type of resize it
* will be. For instance, a position factor of [1, 1]
* will move a frame along with the mouse as the drag
* proceeds, while a dimension factor of [0, 0] will leave
* dimensions unchanged. Combining these in different
* ways results in different handles; a position factor of
* [1, 0] and a dimensions factor of [-1, 0] will implement
* a left-edge resize, as the horizontal position will move
* with the mouse while the horizontal dimensions shrink in
* kind (and vertical properties remain unmodified.)
*
* @param {object} rawPosition the initial position/dimensions
* of the frame being interacted with
* @param {number[]} posFactor the position factor
* @param {number[]} dimFactor the dimensions factor
* @param {number[]} the size of each grid element, in pixels
* @constructor
* @memberof platform/features/layout
*/
function LayoutDrag(rawPosition, posFactor, dimFactor, gridSize) {
this.rawPosition = rawPosition;
this.posFactor = posFactor;
this.dimFactor = dimFactor;
this.gridSize = gridSize;
}
// Convert a delta from pixel coordinates to grid coordinates,
// rounding to whole-number grid coordinates.
function toGridDelta(gridSize, pixelDelta) {
return pixelDelta.map(function (v, i) {
return Math.round(v / gridSize[i]);
});
}
// Utility function to perform element-by-element multiplication
function multiply(array, factors) {
return array.map(function (v, i) {
return v * factors[i];
});
}
// Utility function to perform element-by-element addition
function add(array, other) {
return array.map(function (v, i) {
return v + other[i];
});
}
// Utility function to perform element-by-element max-choosing
function max(array, other) {
return array.map(function (v, i) {
return Math.max(v, other[i]);
});
}
/**
* Get a new position object in grid coordinates, with
* position and dimensions both offset appropriately
* according to the factors supplied in the constructor.
* @param {number[]} pixelDelta the offset from the
* original position, in pixels
*/
LayoutDrag.prototype.getAdjustedPosition = function (pixelDelta) {
var gridDelta = toGridDelta(this.gridSize, pixelDelta);
return {
position: max(add(
this.rawPosition.position,
multiply(gridDelta, this.posFactor)
), [0, 0]),
dimensions: max(add(
this.rawPosition.dimensions,
multiply(gridDelta, this.dimFactor)
), [1, 1])
};
};
return LayoutDrag;
}
);

View File

@ -1,338 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
<template>
<div class="c-frame has-local-controls is-selectable is-moveable"
:style="item.style"
:class="classObject"
@dblclick="drill(item.id, $event)">
<div class="c-frame__header">
<div class="c-frame__header__start">
<div class="c-frame__name icon-object">{{ item.domainObject.name }}</div>
<div class="c-frame__context-actions c-disclosure-button"></div>
</div>
<div class="c-frame__header__end">
<div class="c-button icon-expand local-controls--hidden"></div>
</div>
</div>
<object-view class="c-frame__object-view"
:object="item.domainObject"></object-view>
<!-- Drag handles -->
<div class="c-frame-edit">
<div class="c-frame-edit__move"
@mousedown="startDrag([1,1], [0,0], $event)"></div>
<div class="c-frame-edit__handle --nw"
@mousedown="startDrag([1,1], [-1,-1], $event)"></div>
<div class="c-frame-edit__handle --ne"
@mousedown="startDrag([0,1], [1,-1], $event)"></div>
<div class="c-frame-edit__handle --sw"
@mousedown="startDrag([1,0], [-1,1], $event)"></div>
<div class="c-frame-edit__handle --se"
@mousedown="startDrag([0,0], [1,1], $event)"></div>
</div>
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
/******************************* FRAME */
.c-frame {
display: flex;
flex-direction: column;
border-width: 1px;
border-color: transparent;
/*************************** HEADER */
&__header {
display: flex;
align-items: center;
flex: 0 0 auto;
margin-bottom: $interiorMargin;
> [class*="__"] {
display: flex;
align-items: center;
}
> * + * {
margin-left: $interiorMargin;
}
[class*="__start"] {
flex: 1 1 auto;
overflow: hidden;
}
[class*="__end"] {
//justify-content: flex-end;
flex: 0 0 auto;
[class*="button"] {
font-size: 0.7em;
}
}
}
&__name {
@include ellipsize();
flex: 0 1 auto;
font-size: 1.2em;
&:before {
// Object type icon
flex: 0 0 auto;
margin-right: $interiorMarginSm;
}
}
/*************************** OBJECT VIEW */
&__object-view {
flex: 1 1 auto;
overflow: auto;
.c-object-view {
.u-fills-container {
// Expand component types that fill a container
@include abs();
}
}
}
/*************************** NO-FRAME */
&.no-frame {
> [class*="__header"] {
display: none;
}
}
&:not(.no-frame) {
background: $colorBodyBg;
border: 1px solid $colorInteriorBorder;
padding: $interiorMargin;
}
/*************************** SELECTION */
&.is-selectable {
&:hover {
box-shadow: $browseShdwSelectableHov;
}
}
&.s-selected, // LEGACY
&.is-selected {
border: $browseBorderSelected;
}
}
/*************************** EDITING */
.is-editing {
.c-frame {
&:not(.is-drilled-in).is-selectable {
border: $editBorderSelectable;
&:hover {
border: $editBorderSelectableHov;
}
&.s-selected,
&.is-selected {
border: $editBorderSelected;
> .c-frame-edit {
display: block; // Show the editing rect and handles
}
}
}
&.is-drilled-in {
border: $editBorderDrilledIn;
}
.u-links {
// Applied in markup to objects that provide links. Disable while editing.
pointer-events: none;
}
}
}
.c-frame-edit {
// The editing rect and handles
$z: 10;
@include abs();
box-shadow: rgba($editColor, 0.5) 0 0 10px;
display: none;
&__move {
@include abs();
cursor: move;
z-index: $z;
}
&__handle {
$d: 8px;
$o: floor($d * -0.5);
background: rgba($editColor, 0.3);
border: 1px solid $editColor;
position: absolute;
width: $d; height: $d;
top: auto; right: auto; bottom: auto; left: auto;
z-index: $z + 1;
&:before {
// Extended hit area
$m: -5px;
content: '';
display: block;
position: absolute;
top: $m; right: $m; bottom: $m; left: $m;
z-index: -1;
}
&:hover {
background: $editColor;
}
&.--nw {
cursor: nw-resize;
left: $o; top: $o;
}
&.--ne {
cursor: ne-resize;
right: $o; top: $o;
}
&.--se {
cursor: se-resize;
right: $o; bottom: $o;
}
&.--sw {
cursor: sw-resize;
left: $o; bottom: $o;
}
}
}
</style>
<script>
import ObjectView from '../../ui/components/layout/ObjectView.vue'
import LayoutDrag from './LayoutDrag'
export default {
inject: ['openmct'],
props: {
item: Object,
gridSize: Array
},
components: {
ObjectView
},
computed: {
classObject: function () {
return {
'is-drilled-in': this.item.drilledIn,
'no-frame': !this.item.hasFrame
}
}
},
methods: {
drill(id, $event) {
if ($event) {
$event.stopPropagation();
}
if (!this.isBeingEdited(this.item.domainObject)) {
return;
}
if (this.openmct.composition.get(this.item.domainObject) === undefined) {
return;
}
// Disable for fixed position.
if (this.item.domainObject.type === 'telemetry.fixed') {
return;
}
this.$emit('drilledIn', id);
},
isBeingEdited(object) {
// TODO: add logic when inEditContext() is implemented in Vue.
return true;
},
updatePosition(event) {
let currentPosition = [event.pageX, event.pageY];
this.initialPosition = this.initialPosition || currentPosition;
this.delta = currentPosition.map(function (value, index) {
return value - this.initialPosition[index];
}.bind(this));
},
startDrag(posFactor, dimFactor, event) {
document.body.addEventListener('mousemove', this.continueDrag);
document.body.addEventListener('mouseup', this.endDrag);
this.updatePosition(event);
this.activeDrag = new LayoutDrag(
this.item.rawPosition,
posFactor,
dimFactor,
this.gridSize
);
event.preventDefault();
},
continueDrag(event) {
event.preventDefault();
this.updatePosition(event);
if (this.activeDrag) {
this.$emit('dragInProgress', this.item.id, this.activeDrag.getAdjustedPosition(this.delta));
}
},
endDrag(event) {
document.body.removeEventListener('mousemove', this.continueDrag);
document.body.removeEventListener('mouseup', this.endDrag);
this.continueDrag(event);
this.$emit('endDrag', this.item.id);
this.initialPosition = undefined;
event.preventDefault();
}
},
mounted() {
this.removeSelectable = this.openmct.selection.selectable(
this.$el,
{
item: this.item.domainObject
},
this.item.initSelect
);
},
destroyed() {
this.removeSelectable();
}
}
</script>

View File

@ -1,67 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import Layout from './DisplayLayout.vue'
import Vue from 'vue'
import objectUtils from '../../api/objects/object-utils.js'
import DisplayLayoutType from './DisplayLayoutType.js'
export default function () {
return function (openmct) {
openmct.objectViews.addProvider({
key: 'layout.view',
canView: function (domainObject) {
return domainObject.type === 'layout';
},
view: function (domainObject) {
let component;
return {
show(container) {
component = new Vue({
components: {
Layout
},
template: '<layout :domain-object="domainObject"></layout>',
provide: {
openmct,
objectUtils
},
el: container,
data () {
return {
domainObject: domainObject
}
}
});
},
destroy() {
component.$destroy();
}
};
},
priority() {
return 100;
}
});
openmct.types.addType('layout', DisplayLayoutType());
}
}

View File

@ -1,67 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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([
'./components/GridView.vue',
'vue'
], function (
GridViewComponent,
Vue
) {
function FolderGridView(openmct) {
return {
key: 'grid',
name: 'Grid Vue',
cssClass: 'icon-thumbs-strip',
canView: function (domainObject) {
return domainObject.type === 'folder';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
components: {
gridViewComponent: GridViewComponent.default
},
provide: {
openmct,
domainObject
},
el: element,
template: '<grid-view-component></grid-view-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}
return FolderGridView;
});

View File

@ -1,70 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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([
'./components/ListView.vue',
'vue',
'moment'
], function (
ListViewComponent,
Vue,
Moment
) {
function FolderListView(openmct) {
return {
key: 'list-view',
name: 'List Vue',
cssClass: 'icon-list-view',
canView: function (domainObject) {
return domainObject.type === 'folder';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
components: {
listViewComponent: ListViewComponent.default
},
provide: {
openmct,
domainObject,
Moment
},
el: element,
template: '<list-view-component></list-view-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}
return FolderListView;
});

View File

@ -1,223 +0,0 @@
<template>
<div class="l-grid-view">
<div v-for="(item, index) in items"
v-bind:key="index"
class="l-grid-view__item c-grid-item"
:class="{ 'is-alias': item.isAlias === true }"
@click="navigate(item.model.identifier.key)">
<div class="c-grid-item__type-icon"
:class="(item.type.cssClass != undefined) ? 'bg-' + item.type.cssClass : 'bg-icon-object-unknown'">
</div>
<div class="c-grid-item__details">
<!-- Name and metadata -->
<div class="c-grid-item__name"
:title="item.model.name">{{item.model.name}}</div>
<div class="c-grid-item__metadata"
:title="item.type.name">
<span class="c-grid-item__metadata__type">{{item.type.name}}</span>
<span class="c-grid-item__metadata__item-count" v-if="item.model.composition !== undefined">
{{item.model.composition.length}} item<span v-if="item.model.composition.length !== 1">s</span>
</span>
</div>
</div>
<div class="c-grid-item__controls">
<div class="icon-people" title='Shared'></div>
<button class="c-click-icon icon-info c-info-button" title='More Info'></button>
<div class="icon-pointer-right c-pointer-icon"></div>
</div>
</div>
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
/******************************* GRID VIEW */
.l-grid-view {
display: flex;
flex-flow: column nowrap;
&__item {
flex: 0 0 auto;
+ .l-grid-view__item { margin-top: $interiorMargin; }
}
body.desktop & {
flex-flow: row wrap;
&__item {
height: $gridItemDesk;
width: $gridItemDesk;
margin: 0 $interiorMargin $interiorMargin 0;
}
}
}
/******************************* GRID ITEMS */
.c-grid-item {
// Mobile-first
@include button($bg: $colorItemBg, $fg: $colorItemFg);
cursor: pointer;
display: flex;
padding: $interiorMarginLg;
&__type-icon {
filter: $colorKeyFilter;
flex: 0 0 $gridItemMobile;
font-size: floor($gridItemMobile / 2);
margin-right: $interiorMarginLg;
}
&.is-alias {
// Object is an alias to an original.
[class*='__type-icon'] {
@include isAlias();
color: $colorIconAliasForKeyFilter;
}
}
&__details {
display: flex;
flex-flow: column nowrap;
flex: 1 1 auto;
}
&__name {
@include ellipsize();
color: $colorItemFg;
font-size: 1.2em;
font-weight: 400;
margin-bottom: $interiorMarginSm;
}
&__metadata {
color: $colorItemFgDetails;
font-size: 0.9em;
body.mobile & {
[class*='__item-count'] {
&:before {
content: ' - ';
}
}
}
}
&__controls {
color: $colorItemFgDetails;
flex: 0 0 64px;
font-size: 1.2em;
display: flex;
align-items: center;
justify-content: flex-end;
> * + * {
margin-left: $interiorMargin;
}
}
body.desktop & {
$transOutMs: 300ms;
flex-flow: column nowrap;
transition: background $transOutMs ease-in-out;
&:hover {
background: $colorItemBgHov;
transition: $transIn;
.c-grid-item__type-icon {
filter: $colorKeyFilterHov;
transform: scale(1);
transition: $transInBounce;
}
}
> * {
margin: 0; // Reset from mobile
}
&__controls {
align-items: start;
flex: 0 0 auto;
order: 1;
.c-info-button,
.c-pointer-icon { display: none; }
}
&__type-icon {
flex: 1 1 auto;
font-size: floor($gridItemDesk / 3);
margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;
order: 2;
transform: scale(0.9);
transform-origin: center;
transition: all $transOutMs ease-in-out;
}
&__details {
flex: 0 0 auto;
justify-content: flex-end;
order: 3;
}
&__metadata {
display: flex;
&__type {
flex: 1 1 auto;
@include ellipsize();
}
&__item-count {
opacity: 0.7;
flex: 0 0 auto;
}
}
}
}
</style>
<script>
export default {
inject: ['openmct', 'domainObject'],
data() {
var items = [],
unknownObjectType = {
definition: {
cssClass: 'icon-object-unknown',
name: 'Unknown Type'
}
};
var composition = this.openmct.composition.get(this.domainObject);
if (composition) {
composition.load().then((array) => {
if (Array.isArray(array)) {
array.forEach((model) => {
var type = this.openmct.types.get(model.type) || unknownObjectType;
items.push({
model: model,
type: type.definition,
isAlias: this.domainObject.identifier.key !== model.location
});
});
}
});
}
return {
items: items
}
},
methods: {
navigate(identifier) {
let currentLocation = this.openmct.router.currentLocation.path,
navigateToPath = `${currentLocation}/${identifier}`;
this.openmct.router.setPath(navigateToPath);
}
}
}
</script>

View File

@ -1,215 +0,0 @@
<template>
<div class="c-table c-table--sortable c-list-view">
<table class="c-table__body">
<thead class="c-table__header">
<tr>
<th class="is-sortable"
v-bind:class="[orderByField == 'name' ? 'is-sorting' : '', sortClass]"
@click="sortTrigger('name', 'asc')">
Name
</th>
<th class="is-sortable"
v-bind:class="[orderByField == 'type' ? 'is-sorting' : '', sortClass]"
@click="sortTrigger('type', 'asc')">
Type
</th>
<th class="is-sortable"
v-bind:class="[orderByField == 'createdDate' ? 'is-sorting' : '', sortClass]"
@click="sortTrigger('createdDate', 'desc')">
Created Date
</th>
<th class="is-sortable"
v-bind:class="[orderByField == 'updatedDate' ? 'is-sorting' : '', sortClass]"
@click="sortTrigger('updatedDate', 'desc')">
Updated Date
</th>
<th class="is-sortable"
v-bind:class="[orderByField == 'items' ? 'is-sorting' : '', sortClass]"
@click="sortTrigger('items', 'asc')">
Items
</th>
</tr>
</thead>
<tbody>
<tr class="c-list-item"
v-for="(item,index) in sortedItems"
v-bind:key="index"
:class="{ 'is-alias': item.isAlias === true }"
@click="navigate(item.identifier)">
<td class="c-list-item__name">
<div class="c-list-item__type-icon" :class="(item.cssClass != undefined) ? item.cssClass : 'icon-object-unknown'"></div>
{{item.name}}
</td>
<td class="c-list-item__type">{{ item.type }}</td>
<td class="c-list-item__date-created">{{ formatTime(item.createdDate, 'YYYY-MM-DD HH:mm:ss:SSS') }}Z</td>
<td class="c-list-item__date-updated">{{ formatTime(item.updatedDate, 'YYYY-MM-DD HH:mm:ss:SSS') }}Z</td>
<td class="c-list-item__items">{{ item.items }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
/******************************* LIST VIEW */
.c-list-view {
overflow-x: auto !important;
overflow-y: auto;
tbody tr {
background: $colorListItemBg;
transition: $transOut;
}
body.desktop & {
tbody tr {
cursor: pointer;
&:hover {
background: $colorListItemBgHov;
transition: $transIn;
}
}
}
td {
$p: floor($interiorMargin * 1.5);
font-size: 1.1em;
padding-top: $p;
padding-bottom: $p;
&:not(.c-list-item__name) {
color: $colorItemFgDetails;
}
}
}
.c-list-item {
&__name {
@include ellipsize();
}
&__type-icon {
color: $colorKey;
display: inline-block;
width: 1em;
margin-right:$interiorMarginSm;
}
&.is-alias {
// Object is an alias to an original.
[class*='__type-icon'] {
&:after {
color: $colorIconAlias;
content: $glyph-icon-link;
font-family: symbolsfont;
display: block;
position: absolute;
text-shadow: rgba(black, 0.5) 0 1px 2px;
top: auto; left: -1px; bottom: 1px; right: auto;
transform-origin: bottom left;
transform: scale(0.65);
}
}
}
}
/******************************* LIST ITEM */
</style>
<script>
export default {
inject: ['openmct', 'domainObject', 'Moment'],
data() {
var items = [],
unknownObjectType = {
definition: {
cssClass: 'icon-object-unknown',
name: 'Unknown Type'
}
},
composition = this.openmct.composition.get(this.domainObject);
if (composition) {
composition.load().then((array) => {
if (Array.isArray(array)) {
array.forEach(model => {
var type = this.openmct.types.get(model.type) || unknownObjectType;
items.push({
name: model.name,
identifier: model.identifier.key,
type: type.definition.name,
isAlias: false,
cssClass: type.definition.cssClass,
createdDate: model.persisted,
updatedDate: model.modified,
items: model.composition ? model.composition.length : 0,
isAlias: this.domainObject.identifier.key !== model.location
});
});
}
});
}
return {
items: items,
orderByField: 'name',
sortClass: 'asc',
}
},
computed: {
sortedItems () {
if (this.sortClass === 'asc') {
return this.items.sort(this.ascending.bind(this));
} else if (this.sortClass === 'desc') {
return this.items.sort(this.descending.bind(this));
}
},
formatTime () {
return function (timestamp, format) {
return this.Moment(timestamp).format(format);
}
}
},
methods: {
navigate(identifier) {
let currentLocation = this.openmct.router.currentLocation.path,
navigateToPath = `${currentLocation}/${identifier}`;
this.openmct.router.setPath(navigateToPath);
},
sortTrigger(field, sortOrder) {
if (this.orderByField === field) {
this.sortClass = (this.sortClass === 'asc') ? 'desc' : 'asc';
} else {
this.sortClass = sortOrder;
}
this.orderByField = field;
},
ascending(first, second) {
if (first[this.orderByField] < second[this.orderByField]) {
return -1;
} else if (first[this.orderByField] > second[this.orderByField]) {
return 1;
} else {
return 0;
}
},
descending(first, second) {
if (first[this.orderByField] > second[this.orderByField]) {
return -1;
} else if (first[this.orderByField] < second[this.orderByField]) {
return 1;
} else {
return 0;
}
}
}
}
</script>

View File

@ -51,12 +51,64 @@ define([
function LocalTimeFormat() {
}
/**
* Returns an appropriate time format based on the provided value and
* the threshold required.
* @private
*/
function getScaledFormat(d) {
var momentified = moment.utc(d);
/**
* Uses logic from d3 Time-Scales, v3 of the API. See
* https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md
*
* Licensed
*/
return [
[".SSS", function (m) {
return m.milliseconds();
}],
[":ss", function (m) {
return m.seconds();
}],
["hh:mma", function (m) {
return m.minutes();
}],
["hha", function (m) {
return m.hours();
}],
["ddd DD", function (m) {
return m.days() &&
m.date() !== 1;
}],
["MMM DD", function (m) {
return m.date() !== 1;
}],
["MMMM", function (m) {
return m.month();
}],
["YYYY", function () {
return true;
}]
].filter(function (row) {
return row[1](momentified);
})[0][0];
}
/**
*
* @param value
* @param {Scale} [scale] Optionally provides context to the
* format request, allowing for scale-appropriate formatting.
* @returns {string} the formatted date
*/
LocalTimeFormat.prototype.format = function (value, scale) {
if (scale !== undefined) {
var scaledFormat = getScaledFormat(value, scale);
if (scaledFormat) {
return moment.utc(value).format(scaledFormat);
}
}
return moment(value).format(DATE_FORMAT);
};

View File

@ -41,7 +41,7 @@ define([], function () {
this.timeFormat = 'local-format';
this.durationFormat = 'duration';
this.isUTCBased = false;
this.isUTCBased = true;
}
return LocalTimeSystem;

View File

@ -8,7 +8,8 @@
<span>{{formatTime(entry.createdOn, 'HH:mm:ss')}}</span>
</div>
<div class="c-ne__content">
<div class="c-ne__text c-input-inline"
<!-- TODO: fix styling for c-input-inline when SCSS is merged and remove s-input-inline class here -->
<div class="c-ne__text c-input-inline s-input-inline"
contenteditable="true"
ref="contenteditable"
v-on:blur="textBlur($event, entry.id)"
@ -27,8 +28,8 @@
</div>
<div class="c-ne__local-controls--hidden">
<button class="c-click-icon icon-trash"
<a class="c-icon-button icon-trash"
title="Delete this entry"
v-on:click="deleteEntry"></button>
v-on:click="deleteEntry"></a>
</div>
</li>

View File

@ -1,29 +0,0 @@
<div class="u-contents">
<div class="t-snapshot abs l-view-header">
<div class="abs object-browse-bar l-flex-row">
<div class="left flex-elem l-flex-row grows">
<div class="object-header flex-elem l-flex-row grows">
<div class="type-icon flex-elem embed-icon holder" v-bind:class="embed.cssClass"></div>
<div class="title-label flex-elem holder flex-can-shrink">{{embed.name}}</div>
</div>
</div>
</div>
</div>
<div class="btn-bar right l-flex-row flex-elem flex-justify-end flex-fixed">
<div class="flex-elem holder flex-can-shrink s-snapshot-datetime">
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
<a class="s-button icon-pencil" title="Annotate">
<span class="title-label">Annotate</span>
</a>
</div>
<div class="abs object-holder t-image-holder s-image-holder">
<div
class="image-main s-image-main"
v-bind:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }">
</div>
</div>
</div>

View File

@ -24,15 +24,11 @@ define([
'moment',
'zepto',
'../utils/SnapshotOverlay',
'../../res/templates/snapshotTemplate.html',
'vue'
],
function (
Moment,
$,
SnapshotOverlay,
SnapshotTemplate,
Vue
SnapshotOverlay
) {
function EmbedController (openmct, domainObject) {
this.openmct = openmct;
@ -57,24 +53,11 @@ function (
};
EmbedController.prototype.openSnapshot = function () {
var self = this,
snapshot = new Vue({
template: SnapshotTemplate,
data: function () {
return {
embed: self.embed
};
},
methods: {
formatTime: self.formatTime
}
});
function onDestroyCallback() {
snapshot.$destroy(true);
if (!this.snapshotOverlay) {
this.snapShotOverlay = new SnapshotOverlay(this.embed, this.formatTime);
} else {
this.snapShotOverlay = undefined;
}
this.openmct.OverlayService.show(snapshot.$mount().$el, {onDestroy: onDestroyCallback, cssClass: 'l-large-view'});
};
EmbedController.prototype.formatTime = function (unixTime, timeFormat) {

View File

@ -60,7 +60,6 @@ function (
this.container = container;
var notebookEmbed = {
inject:['openmct'],
props:['embed', 'entry'],
template: EmbedTemplate,
data: embedController.exposedData,
@ -81,7 +80,6 @@ function (
var notebookVue = Vue.extend({
template: NotebookTemplate,
provide: {openmct: self.openmct},
components: {
'notebook-entry': entryComponent,
'search': search.default

View File

@ -34,9 +34,7 @@ define([
'./plot/plugin',
'./telemetryTable/plugin',
'./staticRootPlugin/plugin',
'./notebook/plugin',
'./displayLayout/plugin',
'./folderView/plugin'
'./notebook/plugin'
], function (
_,
UTCTimeSystem,
@ -51,9 +49,7 @@ define([
PlotPlugin,
TelemetryTablePlugin,
StaticRootPlugin,
Notebook,
DisplayLayoutPlugin,
FolderView
Notebook
) {
var bundleMap = {
LocalStorage: 'platform/persistence/local',
@ -105,7 +101,7 @@ define([
*/
plugins.AutoflowView = AutoflowPlugin;
plugins.Conductor = TimeConductorPlugin.default;
plugins.Conductor = TimeConductorPlugin;
plugins.CouchDB = function (url) {
return function (openmct) {
@ -163,8 +159,6 @@ define([
plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin;
plugins.Notebook = Notebook;
plugins.DisplayLayout = DisplayLayoutPlugin.default;
plugins.FolderView = FolderView;
return plugins;
});

View File

@ -264,7 +264,7 @@ define([
this.applyStyle($('#widget', this.domElement), activeRule.getProperty('style'));
$('#widget', this.domElement).prop('title', activeRule.getProperty('message'));
$('#widgetLabel', this.domElement).html(activeRule.getProperty('label'));
$('#widgetLabel', this.domElement).removeClass().addClass('label widget-label c-summary-widget__label ' + activeRule.getProperty('icon'));
$('#widgetLabel', this.domElement).removeClass().addClass('label widget-label ' + activeRule.getProperty('icon'));
};
/**

View File

@ -18,7 +18,7 @@ define([
this.widget.title = datum.message;
this.label.title = datum.message;
this.label.innerHTML = datum.ruleLabel;
this.label.className = 'label widget-label c-summary-widget__label ' + datum.icon;
this.label.className = 'label widget-label ' + datum.icon;
};
SummaryWidgetView.prototype.render = function () {

View File

@ -8,7 +8,7 @@ define([
objectUtils
) {
const DEFAULT_VIEW_PRIORITY = 100;
/**
*
*/
@ -34,11 +34,7 @@ define([
},
editable: true,
priority: function (domainObject) {
if (domainObject.type === 'summary-widget') {
return Number.MAX_VALUE;
} else {
return DEFAULT_VIEW_PRIORITY;
}
return 1;
}
};
}

View File

@ -1,5 +1,5 @@
<div class="w-summary-widget s-status-no-data c-widget-wrapper u-contents">
<a class="t-summary-widget c-button c-summary-widget u-links u-fills-container">
<span class="label widget-label c-summary-widget__label">Loading...</span>
<div class="w-summary-widget s-status-no-data">
<a class="t-summary-widget l-summary-widget s-summary-widget labeled">
<span class="label widget-label">Loading...</span>
</a>
</div>

View File

@ -33,6 +33,30 @@ define([
) {
function TableConfigurationViewProvider(openmct) {
let instantiateService;
function isBeingEdited(object) {
let oldStyleObject = getOldStyleObject(object);
return oldStyleObject.hasCapability('editor') &&
oldStyleObject.getCapability('editor').inEditContext();
}
function getOldStyleObject(object) {
let oldFormatModel = objectUtils.toOldFormat(object);
let oldFormatId = objectUtils.makeKeyString(object.identifier);
return instantiate(oldFormatModel, oldFormatId);
}
function instantiate(model, id) {
if (!instantiateService) {
instantiateService = openmct.$injector.get('instantiate');
}
return instantiateService(model, id);
}
return {
key: 'table-configuration',
name: 'Telemetry Table Configuration',
@ -41,7 +65,8 @@ define([
return false;
}
let object = selection[0].context.item;
return object.type === 'table';
return object.type === 'table' &&
isBeingEdited(object);
},
view: function (selection) {
let component;
@ -61,7 +86,7 @@ define([
el: element
});
},
destroy: function () {
destroy: function (element) {
component.$destroy();
component = undefined;
}

View File

@ -23,10 +23,10 @@
define([
'lodash',
'EventEmitter',
'./TelemetryTableColumn'
'./TelemetryTableColumn',
], function (_, EventEmitter, TelemetryTableColumn) {
class TelemetryTableConfiguration extends EventEmitter {
class TelemetryTableConfiguration extends EventEmitter{
constructor(domainObject, openmct) {
super();
@ -37,8 +37,6 @@ define([
this.addColumnsForObject = this.addColumnsForObject.bind(this);
this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
this.objectMutated = this.objectMutated.bind(this);
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration()));
this.unlistenFromMutation = openmct.objects.observe(domainObject, '*', this.objectMutated);
}
@ -58,11 +56,11 @@ define([
* @param {*} object
*/
objectMutated(object) {
let oldConfiguration = this.domainObject.configuration;
//Synchronize domain object reference. Duplicate object otherwise change detection becomes impossible.
this.domainObject = object;
if (!_.eq(object.configuration, this.oldConfiguration)) {
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration()));
this.domainObject = JSON.parse(JSON.stringify(object));
if (!_.eq(object.configuration, oldConfiguration)){
this.emit('change', object.configuration);
}
}

View File

@ -1,12 +1,14 @@
<template>
<div class="c-properties" v-if="isEditing">
<div class="c-properties__header">Table Columns</div>
<ul class="c-properties__section">
<li class="c-properties__row" v-for="(title, key) in headers">
<div class="c-properties__label" title="Show or Hide Column"><label :for="key + 'ColumnControl'">{{title}}</label></div>
<div class="c-properties__value"><input type="checkbox" :id="key + 'ColumnControl'" :checked="configuration.hiddenColumns[key] !== true" @change="toggleColumn(key)"></div>
</li>
</ul>
<div class="grid-properties">
<!--form class="form" -->
<ul class="l-inspector-part">
<h2>Table Columns</h2>
<li class="grid-row" v-for="(title, key) in headers">
<div class="grid-cell label" title="Show or Hide Column"><label :for="key + 'ColumnControl'">{{title}}</label></div>
<div class="grid-cell value"><input type="checkbox" :id="key + 'ColumnControl'" :checked="configuration.hiddenColumns[key] !== true" @change="toggleColumn(key)"></div>
</li>
</ul>
<!--/form -->
</div>
</template>
@ -19,7 +21,6 @@ export default {
data() {
return {
headers: {},
isEditing: this.openmct.editor.isEditing(),
configuration: this.tableConfiguration.getConfiguration()
}
},
@ -40,14 +41,11 @@ export default {
removeObject(objectIdentifier) {
this.tableConfiguration.removeColumnsForObject(objectIdentifier, true);
this.updateHeaders(this.tableConfiguration.getAllHeaders());
},
toggleEdit(isEditing) {
this.isEditing = isEditing;
}
},
mounted() {
this.unlisteners = [];
this.openmct.editor.on('isEditing', this.toggleEdit);
let compositionCollection = this.openmct.composition.get(this.tableConfiguration.domainObject);
compositionCollection.load()
@ -64,7 +62,6 @@ export default {
},
destroyed() {
this.tableConfiguration.destroy();
this.openmct.editor.off('isEditing', this.toggleEdit);
this.unlisteners.forEach((unlisten) => unlisten());
}
}

View File

@ -9,7 +9,7 @@
</a>
</div>
<!-- Headers table -->
<div class="c-telemetry-table__headers-w js-table__headers-w">
<div class="c-table__headers-w js-table__headers-w">
<table class="c-table__headers c-telemetry-table__headers"
:style="{ 'max-width': totalWidth + 'px'}">
<thead>
@ -68,22 +68,48 @@
<style lang="scss">
@import "~styles/sass-base";
@import "~styles/table";
.c-telemetry-table {
// Table that displays telemetry in a scrolling body area
.c-table {
// Can be used by any type of table, scrolling, LAD, etc.
$min-w: 50px;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
overflow: hidden;
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
&__control-bar,
&__headers-w {
// Don't allow top level elements to grow or shrink
flex: 0 0 auto;
}
/******************************* ELEMENTS */
th, td {
display: block;
flex: 1 0 auto;
font-size: 0.7rem; // TEMP LEGACY TODO: refactor this when __main-container font-size is dealt with
white-space: nowrap;
min-width: $min-w;
padding: $tabularTdPadTB $tabularTdPadLR;
vertical-align: middle; // This is crucial to hiding f**king 4px height injected by browser by default
}
td {
color: $colorTelemFresh;
vertical-align: top;
}
&__control-bar {
margin-bottom: $interiorMarginSm;
}
/******************************* WRAPPERS */
&__headers-w {
// Wraps __headers table
flex: 0 0 auto;
background: $colorTabHeaderBg;
overflow: hidden;
}
@ -109,6 +135,65 @@
}
}
&__body {
// A table
tr {
&:not(:first-child) {
border-top: 1px solid $colorTabBorder;
}
}
}
/******************************* MODIFIERS */
&--filterable {
// TODO: discuss using the search.vue custom control here
.l-filter {
input[type="text"],
input[type="search"] {
$p: 20px;
transition: padding 200ms ease-in-out;
box-sizing: border-box;
padding-right: $p; // Fend off from icon
padding-left: $p; // Fend off from icon
width: 100%;
}
&.active {
// When user has typed something, hide the icon and collapse left padding
&:before {
opacity: 0;
}
input[type="text"],
input[type="search"] {
padding-left: $interiorMargin;
}
}
}
}
&--sortable {
.is-sorting {
&:after {
color: $colorIconLink;
content: $glyph-icon-arrow-tall-up;
font-family: symbolsfont;
font-size: 8px;
display: inline-block;
margin-left: $interiorMarginSm;
}
&.desc:after {
content: $glyph-icon-arrow-tall-down;
}
}
.is-sortable {
cursor: pointer;
}
}
}
.c-telemetry-table {
// Table that displays telemetry in a scrolling body area
/******************************* ELEMENTS */
&__scroll-forcer {
// Force horz scroll when needed; width set via JS
@ -166,6 +251,10 @@
}
}
.c-table__control-bar {
margin-bottom: $interiorMarginSm;
}
/******************************* LEGACY */
.s-status-taking-snapshot,
.overlay.snapshot {

View File

@ -1,436 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2018, 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.
*****************************************************************************/
<template>
<div class="c-conductor"
:class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode', panning ? 'status-panning' : '']">
<form class="u-contents" ref="conductorForm"
@submit="isFixed ? setBoundsFromView($event) : setOffsetsFromView($event)">
<ConductorModeIcon class="c-conductor__mode-icon"></ConductorModeIcon>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
v-if="isFixed">
<!-- Fixed start -->
<div class="c-conductor__start-fixed__label">Start</div>
<input class="c-input--datetime"
type="text" autocorrect="off" spellcheck="false"
ref="startDate"
v-model="formattedBounds.start"
@change="validateBounds('start', $event.target); setBoundsFromView()" />
<date-picker
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"></date-picker>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta"
v-if="!isFixed">
<!-- RT start -->
<div class="c-direction-indicator icon-minus"></div>
<input class="c-input--hrs-min-sec"
type="text" autocorrect="off"
spellcheck="false"
v-model="offsets.start"
@change="validateOffsets($event); setOffsetsFromView()">
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- Fixed end and RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
{{ isFixed ? 'End' : 'Updated' }}
</div>
<input class="c-input--datetime"
type="text" autocorrect="off" spellcheck="false"
v-model="formattedBounds.end"
:disabled="!isFixed"
ref="endDate"
@change="validateBounds('end', $event.target); setBoundsFromView()">
<date-picker
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
v-if="isFixed"></date-picker>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta"
v-if="!isFixed">
<!-- RT end -->
<div class="c-direction-indicator icon-plus"></div>
<input class="c-input--hrs-min-sec"
type="text"
autocorrect="off"
spellcheck="false"
v-model="offsets.end"
@change="validateOffsets($event); setOffsetsFromView()">
</div>
<conductor-axis
class="c-conductor__ticks"
:bounds="rawBounds"
@panAxis="setViewFromBounds"></conductor-axis>
<div class="c-conductor__controls">
<!-- Mode, time system menu buttons and duration slider -->
<ConductorMode class="c-conductor__mode-select"></ConductorMode>
<ConductorTimeSystem class="c-conductor__time-system-select"></ConductorTimeSystem>
</div>
<input type="submit" class="invisible">
</form>
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
/*********************************************** CONDUCTOR LAYOUT */
.c-conductor {
display: grid;
grid-column-gap: $interiorMargin;
grid-row-gap: $interiorMargin;
align-items: center;
// Default: fixed mode, desktop
grid-template-rows: 1fr 1fr;
grid-template-columns: 20px auto 1fr auto;
grid-template-areas:
"tc-mode-icon tc-start tc-ticks tc-end"
"tc-controls tc-controls tc-controls tc-controls";
&__mode-icon {
grid-area: tc-mode-icon;
}
&__start-fixed,
&__start-delta {
grid-area: tc-start;
display: flex;
}
&__end-fixed,
&__end-delta {
grid-area: tc-end;
display: flex;
justify-content: flex-end;
}
&__ticks {
grid-area: tc-ticks;
}
&__controls {
grid-area: tc-controls;
display: flex;
align-items: center;
> * + * {
margin-left: $interiorMargin;
}
}
[class*='-delta'] {
&:before {
content: $glyph-icon-clock;
font-family: symbolsfont;
}
}
&.is-realtime-mode {
grid-template-columns: 20px auto 1fr auto auto;
grid-template-areas:
"tc-mode-icon tc-start tc-ticks tc-updated tc-end"
"tc-controls tc-controls tc-controls tc-controls tc-controls";
.c-conductor__end-fixed {
grid-area: tc-updated;
}
}
body.phone.portrait & {
grid-row-gap: $interiorMargin;
grid-template-rows: auto auto auto;
grid-template-columns: 20px auto auto;
&__mode-icon {
grid-row: 1;
}
&__ticks,
&__zoom {
display: none;
}
&.is-fixed-mode {
[class*='__start-fixed'],
[class*='__end-fixed'] {
[class*='__label'] {
// Start and end are in separate columns; make the labels line up
width: 30px;
}
}
[class*='__end-input'] {
justify-content: flex-start;
}
grid-template-areas:
"tc-mode-icon tc-start tc-start"
"tc-mode-icon tc-end tc-end"
"tc-mode-icon tc-controls tc-controls";
}
&.is-realtime-mode {
grid-template-areas:
"tc-mode-icon tc-start tc-updated"
"tc-mode-icon tc-end tc-end"
"tc-mode-icon tc-controls tc-controls";
.c-conductor__end-fixed {
justify-content: flex-end;
}
}
}
}
.c-conductor-input {
color: $colorInputFg;
display: flex;
align-items: center;
justify-content: flex-start;
> * + * {
margin-left: $interiorMarginSm;
}
&:before {
// Realtime-mode clock icon symbol
margin-right: $interiorMarginSm;
}
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
input:invalid {
background: rgba($colorFormInvalid, 0.5);
}
}
.is-realtime-mode {
button {
@include themedButton($colorTimeBg);
color: $colorTimeFg;
&:hover {
background: $colorTimeHov !important;
color: $colorTimeFg !important;
}
}
.c-conductor-input {
&:before {
color: $colorTime;
}
}
.c-conductor__end-fixed {
// Displays last RT udpate
color: $colorTime;
input {
// Remove input look
background: none;
box-shadow: none;
color: $colorTime;
pointer-events: none;
}
}
}
</style>
<script>
import moment from 'moment';
import ConductorMode from './ConductorMode.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import DatePicker from './DatePicker.vue';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
const SECONDS = 1000;
const DAYS = 24 * 60 * 60 * SECONDS;
const YEARS = 365 * DAYS;
const RESIZE_POLL_INTERVAL = 200;
export default {
inject: ['openmct', 'configuration'],
components: {
ConductorMode,
ConductorTimeSystem,
DatePicker,
ConductorAxis,
ConductorModeIcon
},
data() {
let bounds = this.openmct.time.bounds();
let offsets = this.openmct.time.clockOffsets();
let timeSystem = this.openmct.time.timeSystem();
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
return {
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end)),
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
rawBounds: {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false
}
},
methods: {
setTimeSystem(timeSystem) {
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
setOffsetsFromView($event) {
if (this.$refs.conductorForm.checkValidity()){
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.openmct.time.clockOffsets({
start: startOffset,
end: endOffset
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
setBoundsFromView($event) {
if (this.$refs.conductorForm.checkValidity()){
let start = this.timeFormatter.parse(this.formattedBounds.start);
let end = this.timeFormatter.parse(this.formattedBounds.end);
this.openmct.time.bounds({
start: start,
end: end
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
setViewFromClock(clock) {
this.isFixed = clock === undefined;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
this.rawBounds.start = bounds.start;
this.rawBounds.end = bounds.end;
},
setViewFromOffsets(offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end));
},
validateBounds(startOrEnd, input) {
let validationResult = true;
if (!this.timeFormatter.validate(input.value)){
validationResult = 'Invalid date value';
} else {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
validationResult = this.openmct.time.validateBounds(boundsValues);
}
if (validationResult !== true){
input.setCustomValidity(validationResult);
} else {
input.setCustomValidity('');
}
},
validateOffsets(event) {
let input = event.target;
let validationResult = true;
if (!this.durationFormatter.validate(input.value)) {
validationResult = 'Invalid offset value';
} else {
let offsetValues = {
start: 0 - this.durationFormatter.parse(this.offsets.start),
end: this.durationFormatter.parse(this.offsets.end)
};
validationResult = this.openmct.time.validateOffsets(offsetValues);
}
if (validationResult !== true){
input.setCustomValidity(validationResult);
} else {
input.setCustomValidity('');
}
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
startDateSelected(date){
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateBounds('start', this.$refs.startDate);
this.setBoundsFromView();
},
endDateSelected(date){
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateBounds('end', this.$refs.endDate);
this.setBoundsFromView();
},
},
mounted() {
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('bounds', this.setViewFromBounds);
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.on('clockOffsets', this.setViewFromOffsets)
}
}
</script>

View File

@ -1,265 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2018, 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.
*****************************************************************************/
<template>
<div class="c-conductor-axis"
ref="axisHolder"
@mousedown="dragStart($event)">
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
.c-conductor-axis {
$h: 18px;
$tickYPos: ($h / 2) + 12px;
@include userSelectNone();
@include bgTicks($c: rgba($colorBodyFg, 0.4));
background-position: 0 50%;
background-size: 5px 2px;
border-radius: $controlCr;
height: $h;
svg {
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
> g {
// Overall Tick holder
transform: translateY($tickYPos);
path {
// Domain line
display: none;
}
g {
// Each tick. These move on drag.
line {
// Line beneath ticks
display: none;
}
}
}
text {
// Tick labels
fill: $colorBodyFg;
font-size: 1em;
paint-order: stroke;
font-weight: bold;
stroke: $colorBodyBg;
stroke-linecap: butt;
stroke-linejoin: bevel;
stroke-width: 6px;
}
}
body.desktop .is-fixed-mode & {
@include cursorGrab();
background-size: 3px 30%;
background-color: $colorBodyBgSubtle;
box-shadow: inset rgba(black, 0.4) 0 1px 1px;
transition: $transOut;
svg text {
fill: $colorBodyFg;
stroke: $colorBodyBgSubtle;
transition: $transOut;
}
&:hover,
&:active {
$c: $colorKeySubtle;
background-color: $c;
transition: $transIn;
svg text {
stroke: $c;
transition: $transIn;
}
}
}
.is-realtime-mode & {
$c: 1px solid rgba($colorTime, 0.7);
border-left: $c;
border-right: $c;
svg text {
fill: $colorTime;
}
}
}
</style>
<script>
import * as d3Selection from 'd3-selection';
import * as d3Axis from 'd3-axis';
import * as d3Scale from 'd3-scale';
import utcMultiTimeFormat from './utcMultiTimeFormat.js';
const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration';
const RESIZE_POLL_INTERVAL = 200;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
export default {
inject: ['openmct'],
props: {
bounds: Object
},
methods: {
setScale() {
let timeSystem = this.openmct.time.timeSystem();
let bounds = this.bounds;
if (timeSystem.isUTCBased) {
this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]);
} else {
this.xScale.domain([bounds.start, bounds.end]);
}
this.xAxis.scale(this.xScale);
this.xScale.range([PADDING, this.width - PADDING * 2]);
this.axisElement.call(this.xAxis);
if (this.width > 1800) {
this.xAxis.ticks(this.width / PIXELS_PER_TICK_WIDE);
} else {
this.xAxis.ticks(this.width / PIXELS_PER_TICK);
}
this.msPerPixel = (bounds.end - bounds.start) / this.width;
},
setViewFromTimeSystem(timeSystem) {
let format = this.getActiveFormatter();
let bounds = this.openmct.time.bounds();
//The D3 scale used depends on the type of time system as d3
// supports UTC out of the box.
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
} else {
this.xScale = d3Scale.scaleLinear();
}
this.xAxis.scale(this.xScale);
this.xAxis.tickFormat(utcMultiTimeFormat);
this.axisElement.call(this.xAxis);
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
let isFixed = this.openmct.time.clock() === undefined;
if (isFixed) {
return this.getFormatter(timeSystem.timeFormat);
} else {
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
}
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
dragStart($event){
let isFixed = this.openmct.time.clock() === undefined;
if (isFixed){
this.dragStartX = $event.clientX;
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.dragEnd, {
once: true
});
}
},
drag($event) {
if (!this.dragging){
this.dragging = true;
requestAnimationFrame(()=>{
let deltaX = $event.clientX - this.dragStartX;
let percX = deltaX / this.width;
let bounds = this.openmct.time.bounds();
let deltaTime = bounds.end - bounds.start;
let newStart = bounds.start - percX * deltaTime;
this.bounds = {
start: newStart,
end: newStart + deltaTime
};
this.$emit('panAxis', this.bounds);
this.dragging = false;
})
} else {
console.log('Rejected drag due to RAF cap');
}
},
dragEnd() {
document.removeEventListener('mousemove', this.drag);
this.openmct.time.bounds({
start: this.bounds.start,
end: this.bounds.end
});
},
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.width = this.$refs.axisHolder.clientWidth;
this.setScale();
}
}
},
watch: {
bounds: {
handler(bounds) {
this.setScale();
},
deep: true
}
},
mounted() {
let axisHolder = this.$refs.axisHolder;
let height = axisHolder.offsetHeight;
let vis = d3Selection.select(axisHolder)
.append("svg:svg")
.attr("width", "100%")
.attr("height", height);
this.width = this.$refs.axisHolder.clientWidth;
this.xAxis = d3Axis.axisTop();
this.dragging = false;
// draw x axis with labels. CSS is used to position them.
this.axisElement = vis.append("g");
this.setViewFromTimeSystem(this.openmct.time.timeSystem());
this.setScale();
//Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
destroyed() {
}
}
</script>

View File

@ -1,202 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2018, 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.
*****************************************************************************/
<template>
<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<button class="c-button--menu c-mode-button"
@click="toggleMenu($event)">
<span class="c-button__label">{{selectedMode.name}}</span>
</button>
<div class="c-menu c-super-menu c-conductor__mode-menu"
v-if="showMenu">
<div class="c-super-menu__menu">
<ul>
<li v-for="mode in modes"
:key="mode.key"
@click="setOption(mode)"
@mouseover="hoveredMode = mode"
@mouseleave="hoveredMode = {}"
class="menu-item-a"
:class="mode.cssClass">
{{mode.name}}
</li>
</ul>
</div>
<div class="c-super-menu__item-description">
<div :class="['l-item-description__icon', 'bg-' + hoveredMode.cssClass]"></div>
<div class="l-item-description__name">{{hoveredMode.name}}</div>
<div class="l-item-description__description">{{hoveredMode.description}}</div>
</div>
</div>
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
.c-conductor__mode-menu {
max-height: 80vh;
max-width: 500px;
min-height: 250px;
z-index: 70;
[class*="__icon"] {
filter: $colorKeyFilter;
}
[class*="__item-description"] {
min-width: 200px;
}
}
</style>
<script>
export default {
inject: ['openmct', 'configuration'],
data: function () {
let activeClock = this.openmct.time.clock();
if (activeClock !== undefined) {
//Create copy of active clock so the time API does not get reactified.
activeClock = Object.create(activeClock);
}
return {
selectedMode: this.getModeOptionForClock(activeClock),
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
modes: [],
hoveredMode: {},
showMenu: false
};
},
methods: {
loadClocksFromConfiguration() {
let clocks = this.configuration.menuOptions
.map(menuOption => menuOption.clock)
.filter(isDefinedAndUnique)
.map(this.getClock);
/*
* Populate the modes menu with metadata from the available clocks
* "Fixed Mode" is always first, and has no defined clock
*/
this.modes = [undefined]
.concat(clocks)
.map(this.getModeOptionForClock);
function isDefinedAndUnique(key, index, array) {
return key!== undefined && array.indexOf(key) === index;
}
},
getModeOptionForClock(clock) {
if (clock === undefined) {
return {
key: 'fixed',
name: 'Fixed Timespan Mode',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular'
}
} else {
return {
key: clock.key,
name: clock.name,
description: "Monitor streaming data in real-time. The Time " +
"Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock'
}
}
},
getClock(key) {
return this.openmct.time.getAllClocks().filter(function (clock) {
return clock.key === key;
})[0];
},
setOption(option) {
let clockKey = option.key;
if (clockKey === 'fixed') {
clockKey = undefined;
}
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.timeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
this.openmct.time.timeSystem(configuration.timeSystem, configuration.bounds);
}
if (clockKey === undefined) {
this.openmct.time.stopClock();
} else {
this.openmct.time.clock(clockKey, configuration.clockOffsets);
}
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock
},
timeSystem(config) {
return options.timeSystem === config.timeSystem
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
setViewFromClock(clock) {
this.selectedMode = this.getModeOptionForClock(clock);
},
toggleMenu(event) {
this.showMenu = !this.showMenu;
if (this.showMenu) {
document.addEventListener('click', this.toggleMenu, true);
} else {
document.removeEventListener('click', this.toggleMenu, true);
}
},
},
mounted: function () {
this.loadClocksFromConfiguration();
this.openmct.time.on('clock', this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('clock', this.setViewFromClock);
}
}
</script>

View File

@ -1,139 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2018, 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.
*****************************************************************************/
<template>
<div class="c-clock-symbol">
<div class="hand-little"></div>
<div class="hand-big"></div>
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
@keyframes clock-hands {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes clock-hands-sticky {
0% { transform: translate(-50%, -50%) rotate(0deg); }
7% { transform: translate(-50%, -50%) rotate(0deg); }
8% { transform: translate(-50%, -50%) rotate(30deg); }
15% { transform: translate(-50%, -50%) rotate(30deg); }
16% { transform: translate(-50%, -50%) rotate(60deg); }
24% { transform: translate(-50%, -50%) rotate(60deg); }
25% { transform: translate(-50%, -50%) rotate(90deg); }
32% { transform: translate(-50%, -50%) rotate(90deg); }
33% { transform: translate(-50%, -50%) rotate(120deg); }
40% { transform: translate(-50%, -50%) rotate(120deg); }
41% { transform: translate(-50%, -50%) rotate(150deg); }
49% { transform: translate(-50%, -50%) rotate(150deg); }
50% { transform: translate(-50%, -50%) rotate(180deg); }
57% { transform: translate(-50%, -50%) rotate(180deg); }
58% { transform: translate(-50%, -50%) rotate(210deg); }
65% { transform: translate(-50%, -50%) rotate(210deg); }
66% { transform: translate(-50%, -50%) rotate(240deg); }
74% { transform: translate(-50%, -50%) rotate(240deg); }
75% { transform: translate(-50%, -50%) rotate(270deg); }
82% { transform: translate(-50%, -50%) rotate(270deg); }
83% { transform: translate(-50%, -50%) rotate(300deg); }
90% { transform: translate(-50%, -50%) rotate(300deg); }
91% { transform: translate(-50%, -50%) rotate(330deg); }
99% { transform: translate(-50%, -50%) rotate(330deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
.c-clock-symbol {
$c: $colorBtnBg; //$colorObjHdrIc;
$d: 18px;
height: $d;
width: $d;
position: relative;
&:before {
font-family: symbolsfont;
color: $c;
content: $glyph-icon-brackets;
font-size: $d;
line-height: normal;
display: block;
width: 100%;
height: 100%;
z-index: 1;
}
// Clock hands
div[class*="hand"] {
$handW: 2px;
$handH: $d * 0.4;
animation-iteration-count: infinite;
animation-timing-function: linear;
transform-origin: bottom;
position: absolute;
height: $handW;
width: $handW;
left: 50%;
top: 50%;
z-index: 2;
&:before {
background: $c;
content: '';
display: block;
position: absolute;
width: 100%;
bottom: -1px;
}
&.hand-little {
z-index: 2;
animation-duration: 12s;
transform: translate(-50%, -50%) rotate(120deg);
&:before {
height: ceil($handH * 0.6);
}
}
&.hand-big {
z-index: 1;
animation-duration: 1s;
transform: translate(-50%, -50%);
&:before {
height: $handH;
}
}
}
// Modes
.is-realtime-mode &,
.is-lad-mode & {
&:before {
// Brackets icon
color: $colorTime;
}
div[class*="hand"] {
animation-name: clock-hands;
&:before {
background: $colorTime;
}
}
}
}
</style>

View File

@ -1,126 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2018, 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.
*****************************************************************************/
<template>
<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
v-if="selectedTimeSystem.name">
<button class="c-button--menu c-time-system-button"
:class="selectedTimeSystem.cssClass"
@click="toggleMenu($event)">
<span class="c-button__label">{{selectedTimeSystem.name}}</span>
</button>
<div class="c-menu" v-if="showMenu">
<ul>
<li @click="setTimeSystemFromView(timeSystem)"
v-for="timeSystem in timeSystems"
:key="timeSystem.key"
:class="timeSystem.cssClass">
{{timeSystem.name}}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
inject: ['openmct', 'configuration'],
data: function () {
let activeClock = this.openmct.time.clock();
return {
selectedTimeSystem: JSON.parse(JSON.stringify(this.openmct.time.timeSystem())),
timeSystems: this.getValidTimesystemsForClock(activeClock),
showMenu: false
};
},
methods: {
getValidTimesystemsForClock(clock) {
return this.configuration.menuOptions
.filter(menuOption => menuOption.clock === (clock && clock.key))
.map(menuOption => JSON.parse(JSON.stringify(this.openmct.time.timeSystems.get(menuOption.timeSystem))));
},
setTimeSystemFromView(timeSystem) {
if (timeSystem.key !== this.selectedTimeSystem.key) {
let activeClock = this.openmct.time.clock();
let configuration = this.getMatchingConfig({
clock: activeClock && activeClock.key,
timeSystem: timeSystem.key
});
if (activeClock === undefined) {
this.openmct.time.timeSystem(timeSystem.key, configuration.bounds);
} else {
this.openmct.time.timeSystem(timeSystem.key);
this.openmct.time.clockOffsets(configuration.clockOffsets);
}
}
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock
},
timeSystem(config) {
return options.timeSystem === config.timeSystem
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
toggleMenu(event) {
this.showMenu = !this.showMenu;
if (this.showMenu) {
document.addEventListener('click', this.toggleMenu, true);
} else {
document.removeEventListener('click', this.toggleMenu, true);
}
},
setViewFromTimeSystem(timeSystem) {
this.selectedTimeSystem = timeSystem;
},
setViewFromClock(clock) {
let activeClock = this.openmct.time.clock();
this.timeSystems = this.getValidTimesystemsForClock(activeClock);
}
},
mounted: function () {
this.openmct.time.on('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
},
destroyed: function () {
this.openmct.time.off('timeSystem', this.setViewFromTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
}
}
</script>

View File

@ -1,347 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2018, 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.
*****************************************************************************/
<template>
<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up c-datetime-picker__wrapper" ref="calendarHolder">
<a class="c-click-icon icon-calendar"
@click="togglePicker()"></a>
<div class="c-menu c-menu--mobile-modal c-datetime-picker"
v-if="showPicker">
<div class="c-datetime-picker__close-button">
<button class="c-click-icon icon-x-in-circle"
@click="togglePicker()"></button>
</div>
<div class="c-datetime-picker__pager c-pager l-month-year-pager">
<div class="c-pager__prev c-click-icon icon-arrow-left"
@click="changeMonth(-1)"></div>
<div class="c-pager__month-year">{{model.month}} {{model.year}}</div>
<div class="c-pager__next c-click-icon icon-arrow-right"
@click="changeMonth(1)"></div>
</div>
<div class="c-datetime-picker__calendar c-calendar">
<ul class="c-calendar__row--header l-cal-row">
<li v-for="day in ['Su','Mo','Tu','We','Th','Fr','Sa']"
:key="day">{{day}}</li>
</ul>
<ul class="c-calendar__row--body"
v-for="(row, index) in table"
:key="index">
<li v-for="(cell, index) in row"
:key="index"
@click="select(cell)"
:class="{ 'is-in-month': isInCurrentMonth(cell), selected: isSelected(cell) }">
<div class="c-calendar__day--prime">{{cell.day}}</div>
<div class="c-calendar__day--sub">{{cell.dayOfYear}}</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<style lang="scss">
@import "~styles/sass-base";
/******************************************************** PICKER */
.c-datetime-picker {
@include userSelectNone();
padding: $interiorMarginLg !important;
display: flex !important; // Override .c-menu display: block;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
&__close-button {
display: none; // Only show when body.phone, see below.
}
&__pager {
flex: 0 0 auto;
}
&__calendar {
border-top: 1px solid $colorInteriorBorder;
flex: 1 1 auto;
}
}
.c-pager {
display: grid;
grid-column-gap: $interiorMargin;
grid-template-rows: 1fr;
grid-template-columns: auto 1fr auto;
align-items: center;
.c-click-icon {
font-size: 0.8em;
}
&__month-year {
text-align: center;
}
}
/******************************************************** CALENDAR */
.c-calendar {
display: grid;
grid-template-columns: repeat(7, min-content);
grid-template-rows: auto;
grid-gap: 1px;
height: 100%;
$mutedOpacity: 0.5;
ul {
display: contents;
&[class*='--header'] {
pointer-events: none;
li {
opacity: $mutedOpacity;
}
}
}
li {
display: flex;
flex-direction: column;
justify-content: center !important;
padding: $interiorMargin;
&.is-in-month {
background: $colorMenuElementHilite;
}
}
&__day {
&--sub {
opacity: $mutedOpacity;
font-size: 0.8em;
}
}
}
/******************************************************** MOBILE */
body.phone {
.c-datetime-picker {
&.c-menu {
@include modalFullScreen();
}
&__close-button {
display: flex;
justify-content: flex-end;
}
}
.c-calendar {
grid-template-columns: repeat(7, auto);
}
}
</style>
<script>
import moment from 'moment';
const TIME_NAMES = {
'hours': "Hour",
'minutes': "Minute",
'seconds': "Second"
};
const MONTHS = moment.months();
const TIME_OPTIONS = (function makeRanges() {
let arr = [];
while (arr.length < 60) {
arr.push(arr.length);
}
return {
hours: arr.slice(0, 24),
minutes: arr,
seconds: arr
};
}());
export default {
inject: ['openmct'],
props: {
defaultDateTime: String,
formatter: Object
},
data: function () {
return {
showPicker: false,
picker: {
year: undefined,
month: undefined,
interacted: false
},
model: {
year: undefined,
month: undefined,
},
table: undefined,
date: undefined,
time: undefined
}
},
methods: {
generateTable() {
let m = moment.utc({ year: this.picker.year, month: this.picker.month }).day(0),
table = [],
row,
col;
for (row = 0; row < 6; row += 1) {
table.push([]);
for (col = 0; col < 7; col += 1) {
table[row].push({
year: m.year(),
month: m.month(),
day: m.date(),
dayOfYear: m.dayOfYear()
});
m.add(1, 'days'); // Next day!
}
}
return table;
},
updateViewForMonth() {
this.model.month = MONTHS[this.picker.month];
this.model.year = this.picker.year;
this.table = this.generateTable();
},
updateFromModel(defaultDateTime) {
let m;
m = moment.utc(defaultDateTime);
this.date = {
year: m.year(),
month: m.month(),
day: m.date()
};
this.time = {
hours: m.hour(),
minutes: m.minute(),
seconds: m.second()
};
// Zoom to that date in the picker, but
// only if the user hasn't interacted with it yet.
if (!this.picker.interacted) {
this.picker.year = m.year();
this.picker.month = m.month();
this.updateViewForMonth();
}
},
updateFromView() {
let m = moment.utc({
year: this.date.year,
month: this.date.month,
day: this.date.day,
hour: this.time.hours,
minute: this.time.minutes,
second: this.time.seconds
});
this.$emit('date-selected', m.valueOf());
},
isInCurrentMonth(cell) {
return cell.month === this.picker.month;
},
isSelected(cell) {
let date = this.date || {};
return cell.day === date.day &&
cell.month === date.month &&
cell.year === date.year;
},
select(cell) {
this.date = this.date || {};
this.date.month = cell.month;
this.date.year = cell.year;
this.date.day = cell.day;
this.updateFromView();
this.showPicker = false;
},
dateEquals(d1, d2) {
return d1.year === d2.year &&
d1.month === d2.month &&
d1.day === d2.day;
},
changeMonth(delta) {
this.picker.month += delta;
if (this.picker.month > 11) {
this.picker.month = 0;
this.picker.year += 1;
}
if (this.picker.month < 0) {
this.picker.month = 11;
this.picker.year -= 1;
}
this.picker.interacted = true;
this.updateViewForMonth();
},
nameFor(key) {
return TIME_NAMES[key];
},
optionsFor(key) {
return TIME_OPTIONS[key];
},
hidePicker(event) {
let path = event.composedPath();
if (path.indexOf(this.$refs.calendarHolder) === -1) {
this.showPicker = false;
}
},
togglePicker() {
this.showPicker = !this.showPicker;
if (this.showPicker) {
document.addEventListener('click', this.hidePicker, {
capture: true
});
}
}
},
mounted: function () {
this.updateFromModel(this.defaultDateTime);
this.updateViewForMonth();
},
destroyed: function () {
document.addEventListener('click', this.hidePicker, {
capture: true
});
}
}
</script>

View File

@ -20,108 +20,110 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Conductor from './Conductor.vue';
import Vue from 'vue';
define([], function () {
function isTruthy(a) {
return !!a;
}
function isTruthy(a) {
return !!a;
}
function validateMenuOption(menuOption, index) {
if (menuOption.clock && !menuOption.clockOffsets) {
return "clock-based menuOption at index " + index + " is " +
"missing required property 'clockOffsets'.";
function validateMenuOption(menuOption, index) {
if (menuOption.clock && !menuOption.clockOffsets) {
return "clock-based menuOption at index " + index + " is " +
"missing required property 'clockOffsets'.";
}
if (!menuOption.timeSystem) {
return "menuOption at index " + index + " is missing " +
"required property 'timeSystem'.";
}
if (!menuOption.bounds && !menuOption.clock) {
return "fixed-bounds menuOption at index " + index + " is " +
"missing required property 'bounds'";
}
}
if (!menuOption.timeSystem) {
return "menuOption at index " + index + " is missing " +
"required property 'timeSystem'.";
}
if (!menuOption.bounds && !menuOption.clock) {
return "fixed-bounds menuOption at index " + index + " is " +
"missing required property 'bounds'";
}
}
function validateConfiguration(config) {
if (config === undefined ||
config.menuOptions === undefined ||
config.menuOptions.length === 0) {
return "You must specify one or more 'menuOptions'.";
function validateConfiguration(config) {
if (config === undefined ||
config.menuOptions === undefined ||
config.menuOptions.length === 0) {
return "You must specify one or more 'menuOptions'.";
}
if (config.menuOptions.some(validateMenuOption)) {
return config.menuOptions.map(validateMenuOption)
.filter(isTruthy)
.join('\n');
}
return undefined;
}
if (config.menuOptions.some(validateMenuOption)) {
return config.menuOptions.map(validateMenuOption)
function validateRuntimeConfiguration(config, openmct) {
var systems = openmct.time.getAllTimeSystems()
.reduce(function (m, ts) {
m[ts.key] = ts;
return m;
}, {});
var clocks = openmct.time.getAllClocks()
.reduce(function (m, c) {
m[c.key] = c;
return m;
}, {});
return config.menuOptions.map(function (menuOption, index) {
if (menuOption.timeSystem && !systems[menuOption.timeSystem]) {
return "menuOption at index " + index + " specifies a " +
"timeSystem that does not exist: " + menuOption.timeSystem;
}
if (menuOption.clock && !clocks[menuOption.clock]) {
return "menuOption at index " + index + " specifies a " +
"clock that does not exist: " + menuOption.clock;
}
})
.filter(isTruthy)
.join('\n');
}
return undefined;
}
function validateRuntimeConfiguration(config, openmct) {
var systems = openmct.time.getAllTimeSystems()
.reduce(function (m, ts) {
m[ts.key] = ts;
return m;
}, {});
var clocks = openmct.time.getAllClocks()
.reduce(function (m, c) {
m[c.key] = c;
return m;
}, {});
return config.menuOptions.map(function (menuOption, index) {
if (menuOption.timeSystem && !systems[menuOption.timeSystem]) {
return "menuOption at index " + index + " specifies a " +
"timeSystem that does not exist: " + menuOption.timeSystem;
function throwConfigErrorIfExists(error) {
if (error) {
throw new Error("Invalid Time Conductor Configuration: \n" +
error + '\n' +
"https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor");
}
if (menuOption.clock && !clocks[menuOption.clock]) {
return "menuOption at index " + index + " specifies a " +
"clock that does not exist: " + menuOption.clock;
}
}).filter(isTruthy).join('\n');
}
function throwIfError(configResult) {
if (configResult) {
throw new Error("Invalid Time Conductor Configuration: \n" +
configResult + '\n' +
"https://github.com/nasa/openmct/blob/master/API.md#the-time-conductor");
}
}
function mountComponent(openmct, configuration) {
openmct.layout.conductorComponent = Object.create({
components: {
Conductor
},
template: "<conductor></conductor>",
provide: {
openmct: openmct,
configuration: configuration
}
});
}
return function (config) {
export default function (config){
throwConfigErrorIfExists(validateConfiguration(config));
let configResult = validateConfiguration(config);
throwIfError(configResult);
return function (openmct) {
return function (openmct) {
openmct.legacyExtension('constants', {
key: 'CONDUCTOR_CONFIG',
value: config,
priority: 'mandatory'
});
openmct.on('start', function () {
configResult = validateRuntimeConfiguration(config, openmct);
throwIfError(configResult);
openmct.legacyRegistry.enable('platform/features/conductor/core');
openmct.legacyRegistry.enable('platform/features/conductor/compatibility');
var defaults = config.menuOptions[0];
if (defaults.clock) {
openmct.time.clock(defaults.clock, defaults.clockOffsets);
openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds());
} else {
openmct.time.timeSystem(defaults.timeSystem, defaults.bounds);
}
openmct.on('start', function () {
mountComponent(openmct, config);
throwConfigErrorIfExists(validateRuntimeConfiguration(config, openmct));
});
/*
On app startup, default the conductor if not already set.
*/
if (openmct.time.timeSystem() !== undefined) {
return;
}
var defaults = config.menuOptions[0];
if (defaults.clock) {
openmct.time.clock(defaults.clock, defaults.clockOffsets);
openmct.time.timeSystem(defaults.timeSystem, openmct.time.bounds());
} else {
openmct.time.timeSystem(defaults.timeSystem, defaults.bounds);
}
});
};
};
};
});

View File

@ -1,66 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import moment from 'moment';
export default function multiFormat(date) {
var momentified = moment.utc(date);
/**
* Uses logic from d3 Time-Scales, v3 of the API. See
* https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Scales.md
*
* Licensed
*/
var format = [
[".SSS", function (m) {
return m.milliseconds();
}],
[":ss", function (m) {
return m.seconds();
}],
["HH:mm", function (m) {
return m.minutes();
}],
["HH:mm", function (m) {
return m.hours();
}],
["ddd DD", function (m) {
return m.days() &&
m.date() !== 1;
}],
["MMM DD", function (m) {
return m.date() !== 1;
}],
["MMMM", function (m) {
return m.month();
}],
["YYYY", function () {
return true;
}]
].filter(function (row) {
return row[1](momentified);
})[0][0];
if (format !== undefined) {
return moment.utc(date).format(format);
}
}

View File

@ -58,7 +58,7 @@ define(['EventEmitter'], function (EventEmitter) {
this.selected[0].element.classList.remove('s-selected');
}
if (this.selected[1] && this.selected[1].element) {
if (this.selected[1]) {
this.selected[1].element.classList.remove('s-selected-parent');
}
@ -66,7 +66,7 @@ define(['EventEmitter'], function (EventEmitter) {
selectable[0].element.classList.add('s-selected');
}
if (selectable[1] && selectable[1].element) {
if (selectable[1]) {
selectable[1].element.classList.add('s-selected-parent');
}
@ -132,7 +132,7 @@ define(['EventEmitter'], function (EventEmitter) {
}
return function () {
element.removeEventListener('click', capture, true);
element.removeEventListener('click', capture);
element.removeEventListener('click', selectCapture);
if (unlisten) {

View File

@ -1,330 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/************************************************** ESPRESSO THEME CONSTANTS */
@import "constants";
// Functions
@function buttonBg($c: $colorBtnBg) {
@return linear-gradient(lighten($c, 5%), $c);
}
// Constants
$fontBaseSize: 12px;
$smallCr: 2px;
$controlCr: 3px;
$basicCr: 4px;
// Base colors
$colorBodyBg: #393939;
$colorBodyFg: #aaa;
$colorGenBg: #222;
$colorHeadBg: #262626;
$colorHeadFg: $colorBodyFg;
$colorStatusBarBg: $colorHeadBg;
$colorStatusBarFg: $colorBodyFg;
$colorStatusBarFgHov: #aaa;
$colorKey: #0099cc;
$colorKeyFg: #fff;
$colorKeyHov: #00c0f6;
$colorKeyFilter: brightness(1) sepia(1) hue-rotate(145deg) saturate(6);
$colorKeyFilterHov: brightness(1) sepia(1) hue-rotate(145deg) saturate(7);
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc;
$colorAHov: #fff;
// Layout
$shellMainPad: 4px 0;
$shellPanePad: $interiorMargin, 7px; // TODO: Sync this with Snow theme
// Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #999;
$colorStatusDefault: #ccc;
$colorStatusInfo: #60ba7b;
$colorStatusAlert: #ffb66c;
$colorStatusError: #da0004;
$colorStatusBtnBg: #666;
// States
$colorPausedBg: #ff9900;
$colorPausedFg: #fff;
$colorOk: #33cc33;
// Base variations
$colorBodyBgSubtle: lighten($colorBodyBg, 5%);
$colorBodyBgSubtleHov: darken($colorKey, 50%);
$colorKeySubtle: darken($colorKey, 10%);
// Time Colors
$colorTime: #618cff;
$colorTimeBg: $colorTime;
$colorTimeFg: lighten($colorTimeBg, 30%);
$colorTimeHov: lighten($colorTime, 10%);
$colorTimeSubtle: darken($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
// Edit Colors
$editColor: #00c7c3;
$editColorFg: $colorBodyFg;
$browseBorderSelectableHov: 1px dotted rgba($colorBodyFg, 0.2);
$browseShdwSelectableHov: rgba($colorBodyFg, 0.2) 0 0 3px;
$browseBorderSelected: 1px solid rgba($colorBodyFg, 0.6);
$editBorderSelectable: 1px dotted rgba($editColor, 1);
$editBorderSelectableHov: 1px dashed rgba($editColor, 1);
$editBorderSelected: 1px solid $editColor;
$editBorderDrilledIn: 1px dashed #ff4d9a;
$colorGridLines: rgba($editColor, 0.2);
// UI elements
$colorIconAlias: #4af6f3;
$colorIconAliasForKeyFilter: #aaa;
$colorProgressBarOuter: rgba(#000, 0.1);
$colorProgressBarAmt: #0a0;
// Buttons and Controls
$colorBtnBg: lighten($colorBodyBg, 10%); // !
$colorBtnBgHov: lighten($colorBtnBg, 10%); // !
$colorBtnFg: lighten($colorBodyFg, 10%); // !
$colorBtnFgHov: $colorBtnFg;
$colorBtnMajorBg: $colorKey;
$colorBtnMajorBgHov: $colorKeyHov;
$colorBtnMajorFg: $colorKeyFg;
$colorBtnMajorFgHov: darken($colorBtnMajorFg, 10%);
$colorBtnCautionBg: #f16f6f;
$colorBtnCautionBgHov: #f1504e;
$colorBtnCautionFg: $colorBtnFg;
$colorClickIcon: $colorKey;
$colorClickIconBgHov: rgba($colorKey, 0.6);
$colorClickIconFgHov: $colorKeyHov;
// Menus
$colorMenuBg: lighten($colorBodyBg, 20%);
$colorMenuFg: lighten($colorBodyFg, 30%);
$colorMenuIc: lighten($colorKey, 15%);
$colorMenuHovBg: $colorMenuIc;
$colorMenuHovFg: lighten($colorMenuFg, 10%);
$colorMenuHovIc: $colorMenuHovFg;
$colorMenuElementHilite: lighten($colorMenuBg, 10%);
$shdwMenu: rgba(black, 0.5) 0 1px 5px;
$shdwMenuText: none;
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
// Palettes and Swatches
$paletteItemBorderOuterColorSelected: black;
$paletteItemBorderInnerColorSelected: white;
$paletteItemBorderInnerColor: rgba($paletteItemBorderOuterColorSelected, 0.3);
// Form colors
$colorCheck: $colorKey;
$colorFormRequired: $colorKey;
$colorFormValid: $colorOk;
$colorFormError: #990000;
$colorFormInvalid: #ff2200;
$colorFormFieldErrorBg: $colorFormError;
$colorFormFieldErrorFg: rgba(#fff, 0.6);
$colorFormLines: rgba(#000, 0.1);
$colorFormSectionHeader: rgba(#000, 0.05);
$colorInputBg: rgba(black, 0.2);
$colorInputFg: $colorBodyFg;
$colorInputPlaceholder: darken($colorBodyFg, 20%);
$colorFormText: darken($colorBodyFg, 10%);
$colorInputIcon: darken($colorBodyFg, 25%);
$colorFieldHint: lighten($colorBodyFg, 40%);
$shdwInput: inset rgba(black, 0.4) 0 0 1px;
$shdwInputHov: inset rgba(black, 0.7) 0 0 2px;
$shdwInputFoc: inset rgba(black, 0.8) 0 0.25px 3px;
// Inspector
$colorInspectorBg: lighten($colorBodyBg, 5%);
$colorInspectorFg: $colorBodyFg;
$colorInspectorPropName: darken($colorBodyFg, 20%);
$colorInspectorPropVal: lighten($colorInspectorFg, 15%);
$colorInspectorSectionHeaderBg: lighten($colorInspectorBg, 5%);
$colorInspectorSectionHeaderFg: lighten($colorInspectorBg, 40%);
// Overlay
$overlayColorBg: $colorMenuBg;
$overlayColorFg: $colorMenuFg;
$overlayButtonColorBg: lighten($overlayColorBg, 20%);
$overlayButtonColorFg: #fff;
$overlayCr: $interiorMarginLg;
// Indicator colors
$colorIndicatorAvailable: $colorKey;
$colorIndicatorDisabled: #444;
$colorIndicatorOn: $colorOk;
$colorIndicatorOff: #666;
// Limits and staleness colors//
$colorTelemFresh: lighten($colorBodyFg, 20%);
$colorTelemStale: darken($colorBodyFg, 20%);
$styleTelemStale: italic;
$colorLimitYellowBg: rgba(#ffaa00, 0.3);
$colorLimitYellowIc: #ffaa00;
$colorLimitRedBg: rgba(red, 0.3);
$colorLimitRedIc: red;
// Bubble colors
$colorInfoBubbleBg: $colorMenuBg;
$colorInfoBubbleFg: #666;
// Items
$colorItemBg: buttonBg($colorBtnBg);
$colorItemBgHov: buttonBg(lighten($colorBtnBg, 5%));
$colorListItemBg: transparent;
$colorListItemBgHov: rgba($colorKey, 0.1);
$colorItemFg: $colorBtnFg;
$colorItemFgDetails: darken($colorItemFg, 20%);
$shdwItemText: none;
// Tabular
$colorTabBorder: lighten($colorBodyBg, 10%);
$colorTabBodyBg: $colorBodyBg;
$colorTabBodyFg: lighten($colorBodyFg, 20%);
$colorTabHeaderBg: lighten($colorBodyBg, 10%);
$colorTabHeaderFg: lighten($colorBodyFg, 20%);
$colorTabHeaderBorder: $colorBodyBg;
// Plot
$colorPlotBg: rgba(black, 0.05);
$colorPlotFg: $colorBodyFg;
$colorPlotHash: black;
$opacityPlotHash: 0.2;
$stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: darken($colorPlotFg, 20%);
$legendCollapsedNameMaxW: 50%;
$legendHoverValueBg: rgba($colorBodyFg, 0.2);
// Tree
$colorTreeBg: transparent;
$colorItemTreeHoverBg: lighten($colorBodyBg, 10%);
$colorItemTreeHoverFg: lighten($colorBodyFg, 10%);
$colorItemTreeIcon: $colorKey; // Used
$colorItemTreeIconHover: $colorItemTreeIcon; // Used
$colorItemTreeFg: $colorBodyFg;
$colorItemTreeSelectedBg: darken($colorKey, 15%);
$colorItemTreeSelectedFg: $colorBodyBg;
$colorItemTreeEditingBg: $editColor;
$colorItemTreeEditingFg: $editColorFg;
$colorItemTreeVC: $colorBodyFg;
$colorItemTreeVCHover: $colorKey;
$shdwItemTreeIcon: none;
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;
// Scrollbar
$scrollbarTrackSize: 7px;
$scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;
$scrollbarTrackColorBg: rgba(#000, 0.2);
$scrollbarThumbColor: darken($colorBodyBg, 50%);
$scrollbarThumbColorHov: $colorKey;
$scrollbarThumbColorMenu: lighten($colorMenuBg, 10%);
$scrollbarThumbColorMenuHov: lighten($scrollbarThumbColorMenu, 2%);
// Splitter
$splitterHandleD: 2px;
$splitterHandleHitMargin: 4px;
$colorSplitterBaseBg: $colorBodyBg;
$colorSplitterBg: lighten($colorSplitterBaseBg, 10%);
$colorSplitterFg: $colorBodyBg;
$colorSplitterHover: $colorKey;
$colorSplitterActive: $colorKey;
$splitterBtnD: (16px, 35px); // height, width
$splitterBtnColorBg: $colorBtnBg;
$splitterBtnColorFg: #999;
$splitterBtnLabelColorFg: #666;
$splitterCollapsedBtnColorBg: #222;
$splitterCollapsedBtnColorFg: #666;
$splitterCollapsedBtnColorBgHov: $colorKey;
$splitterCollapsedBtnColorFgHov: $colorKeyFg;
// Mobile
$colorMobilePaneLeft: darken($colorBodyBg, 2%);
$colorMobilePaneLeftTreeItemBg: rgba($colorBodyFg, 0.1);
$colorMobilePaneLeftTreeItemFg: $colorItemTreeFg;
$colorMobileSelectListTreeItemBg: rgba(#000, 0.05);
// About Screen
$colorAboutLink: $colorKeySubtle;
// Loading
$colorLoadingFg: #776ba2;
$colorLoadingBg: rgba($colorLoadingFg, 0.1);
// Transitions
$transIn: all 50ms ease-in-out;
$transOut: all 250ms ease-in-out;
$transInBounce: all 200ms cubic-bezier(.47,.01,.25,1.5);
$transInBounceBig: all 300ms cubic-bezier(.2,1.6,.6,3);
// Discrete items, like Notebook entries, Widget rules
$createBtnTextTransform: uppercase;
@mixin discreteItem() {
background: rgba($colorBodyFg,0.1);
border: none;
border-radius: $controlCr;
.c-input-inline:hover {
background: $colorBodyBg;
}
}
@mixin discreteItemInnerElem() {
border: 1px solid rgba(#fff, 0.1);
border-radius: $controlCr;
}
@mixin themedButton($c: $colorBtnBg) {
background: linear-gradient(lighten($c, 5%), $c);
box-shadow: rgba(black, 0.5) 0 0 2px;
}
/**************************************************** NOT USED, LEAVE FOR NOW */
// Slider controls, not in use
/*
$sliderColorBase: $colorKey;
$sliderColorRangeHolder: rgba(black, 0.07);
$sliderColorRange: rgba($sliderColorBase, 0.2);
$sliderColorRangeHov: rgba($sliderColorBase, 0.4);
$sliderColorKnob: darken($sliderColorBase, 20%);
$sliderColorKnobHov: rgba($sliderColorBase, 0.7);
$sliderColorRangeValHovBg: $sliderColorRange;
$sliderColorRangeValHovFg: $colorBodyFg;
$sliderKnobW: 15px;
$sliderKnobR: 2px;
*/
// Content status
/*
$colorAlert: #ff3c00;
$colorWarningHi: #990000;
$colorWarningLo: #ff9900;
$colorDiagnostic: #a4b442;
$colorCommand: #3693bd;
$colorInfo: #2294a2;
$colorOk: #33cc33;
*/

View File

@ -1,61 +1,136 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/****************************************************** SNOW THEME CONSTANTS */
@import "constants";
// Functions
@function buttonBg($c: $colorBtnBg) {
@return $c;
@function pullForward($c: $colorBodyBg, $p: 20%) {
// For dark interfaces, lighter things come forward - opposite for light interfaces
@return darken($c, $p);
}
// Constants
$fontBaseSize: 12px;
$smallCr: 2px;
$controlCr: 3px;
$basicCr: 4px;
@function pushBack($c: $colorBodyBg, $p: 20%) {
// For dark interfaces, darker things move back - opposite for light interfaces
@return lighten($c, $p);
}
// Base colors
// Global
$fontBaseSize: 12px;
$colorBodyBg: #fcfcfc;
$colorBodyFg: #666;
$colorGenBg: #fff;
$colorHeadBg: #eee;
$colorHeadFg: $colorBodyFg;
$colorStatusBarBg: #000;
$colorStatusBarFg: #999;
$colorStatusBarFgHov: #aaa;
$colorKey: #0099cc;
$colorKeyFilter: brightness(0.9) sepia(1) hue-rotate(145deg) saturate(6);
$colorKeySelectedBg: $colorKey;
$colorKeyFg: #fff;
$colorKeyHov: #00c0f6;
$colorKeyFilter: brightness(0.9) sepia(1) hue-rotate(145deg) saturate(6);
$colorKeyFilterHov: brightness(1) sepia(1) hue-rotate(145deg) saturate(7);
$colorEditAreaBg: #eafaff;
$colorEditAreaFg: #4bb1c7;
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #999;
$colorAHov: $colorKey;
$contrastRatioPercent: 40%;
$hoverRatioPercent: 10%;
$basicCr: 4px;
$controlCr: 3px;
$smallCr: 2px;
$overlayCr: 11px;
$shdwTextSubtle: rgba(black, 0.2) 0 1px 2px;
// Layout
$shellMainPad: 4px 0;
$shellPanePad: 0 7px;
// Buttons and Controls
$btnPad: $interiorMargin, $interiorMargin * 1.25;
$colorBtnBg: #aaaaaa;
$colorBtnBgHov: pullForward($colorBtnBg, $hoverRatioPercent);
$colorBtnFg: #fff;
$colorBtnFgHov: $colorBtnFg;
$colorBtnIcon: #eee;
$colorBtnIconHov: $colorBtnFgHov;
$colorBtnMajorBg: $colorKey;
$colorBtnMajorBgHov: $colorKeyHov;
$colorBtnMajorFg: $colorKeyFg;
$colorBtnMajorFgHov: pushBack($colorBtnMajorFg, $hoverRatioPercent);
$colorClickIcon: $colorKey;
$colorClickIconHov: $colorKeyHov;
$colorToggleIcon: rgba($colorClickIcon, 0.5);
$colorToggleIconActive: $colorKey;
$colorToggleIconHov: rgba($colorToggleIconActive, 0.5);
$colorInvokeMenu: #000;
$contrastInvokeMenuPercent: 40%;
$shdwBtns: none;
$shdwBtnsOverlay: none;
$sliderColorBase: $colorKey;
$sliderColorRangeHolder: rgba(black, 0.07);
$sliderColorRange: rgba($sliderColorBase, 0.2);
$sliderColorRangeHov: rgba($sliderColorBase, 0.4);
$sliderColorKnob: pushBack($sliderColorBase, 20%);
$sliderColorKnobHov: rgba($sliderColorBase, 0.7);
$sliderColorRangeValHovBg: $sliderColorRange;
$sliderColorRangeValHovFg: $colorBodyFg;
$sliderKnobW: 15px;
$sliderKnobR: 2px;
$timeControllerToiLineColor: $colorBodyFg;
$timeControllerToiLineColorHov: #0052b5;
$colorTransLucBg: #666; // Used as a visual blocking element over variable backgrounds, like imagery
$createBtnTextTransform: uppercase;
// Foundation Colors
$colorAlt1: #776ba2;
$colorAlert: #ff3c00;
$colorWarningHi: #990000;
$colorWarningLo: #ff9900;
$colorDiagnostic: #a4b442;
$colorCommand: #3693bd;
$colorInfo: #2294a2;
$colorOk: #33cc33;
$colorIconLink: #49dedb;
$colorPausedBg: #ff9900;
$colorPausedFg: #fff;
$colorCreateBtn: $colorKey;
$colorGridLines: rgba(#000, 0.05);
$colorInvokeMenu: #fff;
$colorObjHdrTxt: $colorBodyFg;
$colorObjHdrIc: lighten($colorObjHdrTxt, 30%);
$colorTick: rgba(black, 0.2);
$colorSelectableSelectedPrimary: $colorKey;
$colorSelectableHov: rgba($colorBodyFg, 0.4);
// Menus
$colorMenuBg: pushBack($colorBodyBg, 10%);
$colorMenuFg: pullForward($colorMenuBg, 70%);
$colorMenuIc: $colorKey;
$colorMenuHovBg: $colorMenuIc; //pullForward($colorMenuBg, $hoverRatioPercent);
$colorMenuHovFg: $colorMenuBg;
$colorMenuHovIc: $colorMenuBg;
$shdwMenu: rgba(black, 0.5) 0 1px 5px;
$shdwMenuText: none;
$colorCreateMenuLgIcon: $colorKey;
$colorCreateMenuText: $colorBodyFg;
$menuItemPad: ($interiorMargin, nth($btnPad, 2));
// Form colors
$colorCheck: $colorKey;
$colorFormRequired: $colorKey;
$colorFormValid: $colorOk;
$colorFormError: $colorWarningHi;
$colorFormInvalid: #ff2200;
$colorFormFieldErrorBg: $colorFormError;
$colorFormFieldErrorFg: rgba(#fff, 0.6);
$colorFormLines: rgba(#000, 0.1);
$colorFormSectionHeader: rgba(#000, 0.05);
$colorInputBg: $colorGenBg;
$colorInputFg: $colorBodyFg;
$colorInputPlaceholder: pushBack($colorBodyFg, 20%);
$colorFormText: pushBack($colorBodyFg, 10%);
$colorInputIcon: pushBack($colorBodyFg, 25%);
$colorFieldHint: pullForward($colorBodyFg, 40%);
// Inspector
$colorInspectorBg: pullForward($colorBodyBg, 5%);
$colorInspectorFg: $colorBodyFg;
$colorInspectorPropName: pushBack($colorBodyFg, 20%);
$colorInspectorPropVal: pullForward($colorInspectorFg, 15%);
$colorInspectorSectionHeaderBg: pullForward($colorInspectorBg, 5%);
$colorInspectorSectionHeaderFg: pullForward($colorInspectorBg, 40%);
// Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #999;
@ -64,111 +139,12 @@ $colorStatusInfo: #60ba7b;
$colorStatusAlert: #ffb66c;
$colorStatusError: #da0004;
$colorStatusBtnBg: #666;
// States
$colorPausedBg: #ff9900;
$colorPausedFg: #fff;
$colorOk: #33cc33;
// Base variations
$colorBodyBgSubtle: darken($colorBodyBg, 5%);
$colorBodyBgSubtleHov: lighten($colorKey, 50%);
$colorKeySubtle: lighten($colorKey, 50%);
// Time Colors
$colorTime: #618cff;
$colorTimeBg: $colorTime;
$colorTimeFg: $colorBodyBg;
$colorTimeHov: lighten($colorTime, 5%);
$colorTimeSubtle: lighten($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
// Edit Colors
$editColor: #00c7c3;
$editColorFg: $colorBodyFg;
$browseBorderSelectableHov: 1px dotted rgba($colorBodyFg, 0.2);
$browseShdwSelectableHov: rgba($colorBodyFg, 0.2) 0 0 3px;
$browseBorderSelected: 1px solid rgba($colorBodyFg, 0.6);
$editBorderSelectable: 1px dotted rgba($editColor, 1);
$editBorderSelectableHov: 1px dashed rgba($editColor, 1);
$editBorderSelected: 1px solid $editColor;
$editBorderDrilledIn: 1px dashed #ff4d9a;
$colorGridLines: rgba($editColor, 0.2);
// UI elements
$colorIconAlias: #4af6f3;
$colorIconAliasForKeyFilter: #aaa;
$colorProgressBarOuter: rgba(#000, 0.1);
$colorProgressBarAmt: #0a0;
// Buttons and Controls
$colorBtnBg: #aaaaaa;
$colorBtnBgHov: darken($colorBtnBg, 10%);
$colorBtnFg: #fff;
$colorBtnFgHov: $colorBtnFg;
$colorBtnMajorBg: $colorKey;
$colorBtnMajorBgHov: $colorKeyHov;
$colorBtnMajorFg: $colorKeyFg;
$colorBtnMajorFgHov: lighten($colorBtnMajorFg, 10%);
$colorBtnCautionBg: #f16f6f;
$colorBtnCautionBgHov: #f1504e;
$colorBtnCautionFg: $colorBtnFg;
$colorClickIcon: $colorKey;
$colorClickIconBgHov: rgba($colorKey, 0.2);
$colorClickIconFgHov: $colorKeyHov;
// Menus
$colorMenuBg: lighten($colorBodyBg, 10%);
$colorMenuFg: darken($colorMenuBg, 70%);
$colorMenuIc: $colorKey;
$colorMenuHovBg: $colorMenuIc;
$colorMenuHovFg: $colorMenuBg;
$colorMenuHovIc: $colorMenuBg;
$colorMenuElementHilite: darken($colorMenuBg, 10%);
$shdwMenu: rgba(black, 0.5) 0 1px 5px;
$shdwMenuText: none;
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
// Palettes and Swatches
$paletteItemBorderOuterColorSelected: black;
$paletteItemBorderInnerColorSelected: white;
$paletteItemBorderInnerColor: rgba($paletteItemBorderOuterColorSelected, 0.3);
// Form colors
$colorCheck: $colorKey;
$colorFormRequired: $colorKey;
$colorFormValid: $colorOk;
$colorFormError: #990000;
$colorFormInvalid: #ff2200;
$colorFormFieldErrorBg: $colorFormError;
$colorFormFieldErrorFg: rgba(#fff, 0.6);
$colorFormLines: rgba(#000, 0.1);
$colorFormSectionHeader: rgba(#000, 0.05);
$colorInputBg: $colorGenBg;
$colorInputFg: $colorBodyFg;
$colorInputPlaceholder: lighten($colorBodyFg, 20%);
$colorFormText: lighten($colorBodyFg, 10%);
$colorInputIcon: lighten($colorBodyFg, 25%);
$colorFieldHint: darken($colorBodyFg, 40%);
$shdwInput: inset rgba(black, 0.7) 0 0 1px;
$shdwInputHov: inset rgba(black, 0.7) 0 0 2px;
$shdwInputFoc: inset rgba(black, 0.8) 0 0.25px 3px;
// Inspector
$colorInspectorBg: darken($colorBodyBg, 5%);
$colorInspectorFg: $colorBodyFg;
$colorInspectorPropName: lighten($colorBodyFg, 20%);
$colorInspectorPropVal: darken($colorInspectorFg, 15%);
$colorInspectorSectionHeaderBg: darken($colorInspectorBg, 5%);
$colorInspectorSectionHeaderFg: darken($colorInspectorBg, 40%);
// Overlay
$overlayColorBg: $colorMenuBg;
$overlayColorFg: $colorMenuFg;
$overlayButtonColorBg: $colorBtnBg;
$overlayButtonColorFg: $colorBtnFg;
$overlayCr: $interiorMarginLg;
$progressBarHOverlay: 15px;
$progressBarStripeW: 20px;
$shdwStatusIc: rgba(white, 0.8) 0 0px 5px;
$animPausedPulseDur: 1s;
// Indicator colors
$colorIndicatorAvailable: $colorKey;
@ -176,9 +152,13 @@ $colorIndicatorDisabled: #444;
$colorIndicatorOn: $colorOk;
$colorIndicatorOff: #666;
// Selects
$colorSelectBg: $colorBtnBg;
$colorSelectFg: $colorBtnFg;
// Limits and staleness colors//
$colorTelemFresh: darken($colorBodyFg, 20%);
$colorTelemStale: lighten($colorBodyFg, 20%);
$colorTelemFresh: pullForward($colorBodyFg, 20%);
$colorTelemStale: pushBack($colorBodyFg, 20%);
$styleTelemStale: italic;
$colorLimitYellowBg: rgba(#ffaa00, 0.3);
$colorLimitYellowIc: #ffaa00;
@ -188,22 +168,35 @@ $colorLimitRedIc: red;
// Bubble colors
$colorInfoBubbleBg: $colorMenuBg;
$colorInfoBubbleFg: #666;
$colorThumbsBubbleFg: pullForward($colorBodyFg, 10%);
$colorThumbsBubbleBg: pullForward($colorBodyBg, 10%);
// Overlay
$colorOvrBlocker: rgba(black, 0.7);//
$colorOvrBg: $colorBodyBg;
$colorOvrFg: $colorBodyFg;
$colorOvrBtnBg: pullForward($colorOvrBg, 40%);
$colorOvrBtnFg: #fff;
$colorFieldHintOverlay: pullForward($colorOvrBg, 40%);
$durLargeViewExpand: 250ms;
// Items
$colorItemBg: lighten($colorBtnBg, 20%);
$colorItemBgHov: lighten($colorItemBg, 5%);
$colorListItemBg: transparent;
$colorListItemBgHov: rgba($colorKey, 0.1);
$colorItemBg: #ddd;
$colorItemBgHov: pullForward($colorItemBg, $hoverRatioPercent * 0.7);
$colorItemFg: $colorBodyFg;
$colorItemFgDetails: lighten($colorItemFg, 15%);
$colorItemFgDetails: pushBack($colorItemFg, 15%);
$colorItemIc: $colorKey;
$colorItemSubIcons: $colorItemFgDetails;
$colorItemOpenIcon: $colorItemFgDetails;
$shdwItemText: none;
$colorItemBgSelected: $colorKey;
// Tabular
$colorTabBorder: darken($colorBodyBg, 10%);
$colorTabBorder: pullForward($colorBodyBg, 10%);
$colorTabBodyBg: $colorBodyBg;
$colorTabBodyFg: darken($colorBodyFg, 20%);
$colorTabHeaderBg: darken($colorBodyBg, 10%);
$colorTabHeaderFg: darken($colorBodyFg, 20%);
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
$colorTabHeaderBg: pullForward($colorBodyBg, 10%);
$colorTabHeaderFg: pullForward($colorBodyFg, 20%);
$colorTabHeaderBorder: $colorBodyBg;
// Plot
@ -213,23 +206,24 @@ $colorPlotHash: black;
$opacityPlotHash: 0.2;
$stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder;
$colorPlotLabelFg: lighten($colorPlotFg, 20%);
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
$legendCollapsedNameMaxW: 50%;
$legendHoverValueBg: rgba($colorBodyFg, 0.2);
// Tree
$colorTreeBg: transparent;
$colorItemTreeHoverBg: darken($colorBodyBg, 10%);
$colorItemTreeHoverFg: darken($colorBodyFg, 10%);
$colorTreeBg: #f0f0f0; // Used
$colorItemTreeHoverBg: pullForward($colorBodyBg, $hoverRatioPercent);
$colorItemTreeHoverFg: pullForward($colorBodyFg, $hoverRatioPercent);
$colorItemTreeIcon: $colorKey; // Used
$colorItemTreeIconHover: $colorItemTreeIcon; // Used
$colorItemTreeFg: $colorBodyFg;
$colorItemTreeSelectedBg: lighten($colorKey, 15%);
$colorItemTreeSelectedBg: pushBack($colorKey, 15%);
$colorItemTreeSelectedFg: $colorBodyBg;
$colorItemTreeEditingBg: $editColor;
$colorItemTreeEditingFg: $editColorFg;
$colorItemTreeEditingBg: #caf1ff;
$colorItemTreeEditingFg: $colorEditAreaFg;
$colorItemTreeVC: $colorBodyFg;
$colorItemTreeVCHover: $colorKey;
$colorItemTreeSelectedVC: $colorBodyBg;
$shdwItemTreeIcon: none;
// Images
@ -241,90 +235,75 @@ $scrollbarTrackShdw: rgba(#000, 0.2) 0 1px 2px;
$scrollbarTrackColorBg: rgba(#000, 0.2);
$scrollbarThumbColor: darken($colorBodyBg, 50%);
$scrollbarThumbColorHov: $colorKey;
$scrollbarThumbColorMenu: darken($colorMenuBg, 10%);
$scrollbarThumbColorMenuHov: darken($scrollbarThumbColorMenu, 2%);
$scrollbarThumbColorOverlay: darken($colorOvrBg, 50%);
$scrollbarThumbColorOverlayHov: $scrollbarThumbColorHov;
$scrollbarThumbColorMenu: pullForward($colorMenuBg, 10%);
$scrollbarThumbColorMenuHov: pullForward($scrollbarThumbColorMenu, 2%);
// Splitter
$splitterD: 7px;
$splitterHandleD: 2px;
$splitterHandleHitMargin: 4px;
$splitterGrippyD: ($splitterHandleD - 4, 75px, 50px); // thickness, length, min-length
$colorSplitterBaseBg: $colorBodyBg;
$colorSplitterBg: darken($colorSplitterBaseBg, 20%);
$colorSplitterBg: pullForward($colorSplitterBaseBg, 20%);
$colorSplitterFg: $colorBodyBg;
$colorSplitterHover: $colorKey;
$colorSplitterHover: $colorKey; // pullForward($colorSplitterBg, $hoverRatioPercent * 2);
$colorSplitterActive: $colorKey;
$splitterBtnD: (16px, 35px); // height, width
$splitterBtnColorBg: $colorBtnBg;
$splitterBtnColorFg: #ddd;
$splitterBtnLabelColorFg: #999;
$splitterCollapsedBtnColorBg: #ccc;
$splitterCollapsedBtnColorFg: #666;
$splitterCollapsedBtnColorBgHov: $colorKey;
$splitterCollapsedBtnColorFgHov: $colorKeyFg;
$splitterBtnColorBg: #eee;
$splitterBtnColorFg: #999;
$splitterBtnColorHoverBg: rgba($colorKey, 1);
$splitterBtnColorHoverFg: $colorBodyBg;
$colorSplitterGrippy: pullForward($colorSplitterBaseBg, 30%);
$splitterShdw: none;
$splitterEndCr: none;
// Minitabs
$colorMiniTabBg: $colorSplitterBg;
$colorMiniTabFg: pullForward($colorMiniTabBg, 30%);
$colorMiniTabBgHov: $colorSplitterHover;
$colorMiniTabFgHov: #fff;
// Mobile
$colorMobilePaneLeft: darken($colorBodyBg, 2%);
$colorMobilePaneLeftTreeItemBg: rgba($colorBodyFg, 0.1);
$colorMobilePaneLeftTreeItemBg: rgba($colorBodyFg, 0.1); //pullForward($colorMobilePaneLeft, 3%);
$colorMobilePaneLeftTreeItemFg: $colorItemTreeFg;
$colorMobileSelectListTreeItemBg: rgba(#000, 0.05);
// Datetime Picker, Calendar
$colorCalCellHovBg: $colorKey;
$colorCalCellHovFg: $colorKeyFg;
$colorCalCellSelectedBg: $colorItemTreeSelectedBg;
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
// Palettes
$colorPaletteFg: pullForward($colorMenuBg, 30%);
$colorPaletteSelected: #333;
$shdwPaletteFg: none;
$shdwPaletteSelected: inset 0 0 0 1px #fff;
// About Screen
$colorAboutLink: $colorKeySubtle;
$colorAboutLink: #84b3ff;
// Loading
$colorLoadingFg: #776ba2;
$colorLoadingFg: $colorAlt1;
$colorLoadingBg: rgba($colorLoadingFg, 0.1);
// Transitions
$transIn: all 50ms ease-in-out;
$transOut: all 250ms ease-in-out;
$transInBounce: all 200ms cubic-bezier(.47,.01,.25,1.5);
$transInBounceBig: all 300ms cubic-bezier(.2,1.6,.6,3);
$transIn: all 50ms ease-in;
$transOut: all 250ms ease-out;
// Discrete items, like Notebook entries, Widget rules
$createBtnTextTransform: uppercase;
@mixin discreteItem() {
background: rgba($colorBodyFg,0.1);
border: 1px solid $colorInteriorBorder;
border-radius: $controlCr;
.c-input-inline:hover {
background: $colorBodyBg;
}
}
@mixin discreteItemInnerElem() {
border: 1px solid $colorBodyBg;
border-radius: $controlCr;
}
@mixin themedButton($c: $colorBtnBg) {
background: $c;
}
/**************************************************** NOT USED, LEAVE FOR NOW */
// Slider controls, not in use
/*
$sliderColorBase: $colorKey;
$sliderColorRangeHolder: rgba(black, 0.07);
$sliderColorRange: rgba($sliderColorBase, 0.2);
$sliderColorRangeHov: rgba($sliderColorBase, 0.4);
$sliderColorKnob: lighten($sliderColorBase, 20%);
$sliderColorKnobHov: rgba($sliderColorBase, 0.7);
$sliderColorRangeValHovBg: $sliderColorRange;
$sliderColorRangeValHovFg: $colorBodyFg;
$sliderKnobW: 15px;
$sliderKnobR: 2px;
*/
// Content status
/*
$colorAlert: #ff3c00;
$colorWarningHi: #990000;
$colorWarningLo: #ff9900;
$colorDiagnostic: #a4b442;
$colorCommand: #3693bd;
$colorInfo: #2294a2;
$colorOk: #33cc33;
*/
border-radius: $controlCr; }

View File

@ -1,25 +1,3 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/************************** PATHS */
// Paths need to be relative to /platform/commonUI/theme/<theme-name>/css/ directory
$dirFonts: 'fonts/';
@ -40,21 +18,15 @@ $inputTextP: $inputTextPTopBtm $inputTextPLeftRight;
$menuLineH: 1.5rem;
$treeItemIndent: 16px;
$treeTypeIconW: 18px;
$overlayOuterMargin: 5%;
$overlayInnerMargin: 25px;
/*************** Items */
$itemPadLR: 5px;
$gridItemDesk: 175px;
$gridItemMobile: 32px;
$ueBrowseGridItemLg: 200px;
/*************** Tabular */
$tabularHeaderH: 22px;
$tabularTdPadLR: $itemPadLR;
$tabularTdPadTB: 2px;
/************************** MOBILE */
$mobileMenuIconD: 24px; // Used
$mobileTreeItemH: 35px; // Used
/************************** VISUAL */
$controlDisabledOpacity: 0.3;
@ -124,7 +96,7 @@ $glyph-icon-pointer-right: '\e1028';
$glyph-icon-refresh: '\e1029';
$glyph-icon-save: '\e1030';
$glyph-icon-sine: '\e1031';
$glyph-icon-font: '\e1032';
$glyph-icon-T: '\e1032';
$glyph-icon-thumbs-strip: '\e1033';
$glyph-icon-two-parts-both: '\e1034';
$glyph-icon-two-parts-one-only: '\e1035';
@ -141,7 +113,7 @@ $glyph-icon-frame-show: '\e1045';
$glyph-icon-frame-hide: '\e1046';
$glyph-icon-import: '\e1047';
$glyph-icon-export: '\e1048';
$glyph-icon-font-size: '\e1049';
$glyph-icon-minimize: '\e1049'; // 12px only
$glyph-icon-activity: '\e1100';
$glyph-icon-activity-mode: '\e1101';
$glyph-icon-autoflow-tabular: '\e1102';
@ -180,7 +152,7 @@ $glyph-icon-notebook: '\e1131';
$bg-icon-activity: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23666666' d='M288 32H160l160 160H174.872C152.74 153.742 111.377 128 64 128H0v256h64c47.377 0 88.74-25.742 110.872-64H320L160 480h128l224-224L288 32z'/%3e%3c/svg%3e");
$bg-icon-activity-mode: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23666666' d='M256 0C148.6 0 56.6 66.2 18.6 160H64c28.4 0 54 12.4 71.5 32H256l-96-96h128l160 160-160 160H160l96-96H135.5C118 339.6 92.4 352 64 352H18.6c38 93.8 129.9 160 237.4 160 141.4 0 256-114.6 256-256S397.4 0 256 0z'/%3e%3c/svg%3e");
$bg-icon-autoflow-tabular: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23666666' d='M96 0C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h32V0H96zM192 0h128v512H192zM416 0h-32v352h128V96c0-52.8-43.2-96-96-96z'/%3e%3c/svg%3e");
$bg-icon-clock: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23666666' d='M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0zm135 345c-6.4 11.1-18.3 18-31.2 18-6.3 0-12.5-1.7-18-4.8l-110.9-64-.1-.1c-.4-.2-.8-.5-1.2-.7l-.4-.3-.9-.6-.6-.5-.6-.5-.9-.7-.3-.3c-.4-.3-.7-.6-1.1-.9-2.5-2.3-4.7-5-6.5-7.9-.1-.2-.3-.5-.4-.7s-.3-.5-.4-.7c-1.6-3-2.9-6.2-3.6-9.6v-.1c-.1-.5-.2-.9-.3-1.4 0-.1 0-.3-.1-.4-.1-.3-.1-.7-.1-1.1s-.1-.5-.1-.8 0-.5-.1-.8-.1-.8-.1-1.1v-.5-1.4V81c0-19.9 16.1-36 36-36s36 16.1 36 36v161.2l92.9 53.6c17.1 10 22.9 32 13 49.2z'/%3e%3c/svg%3e");
$bg-icon-clock: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23666666' d='M256 0C114.62 0 0 114.62 0 256s114.62 256 256 256 256-114.62 256-256S397.38 0 256 0zm135 345a36 36 0 0 1-31.21 18 35.83 35.83 0 0 1-18-4.83l-110.85-64-.14-.08q-.6-.35-1.19-.73l-.43-.28-.93-.64-.63-.45-.63-.48-.85-.67-.32-.27c-.36-.3-.72-.61-1.07-.92a35.76 35.76 0 0 1-6.52-7.9c-.14-.23-.27-.47-.41-.7s-.29-.49-.43-.74a35.75 35.75 0 0 1-3.58-9.59v-.06c-.1-.46-.19-.92-.27-1.38 0-.14-.05-.28-.08-.42-.06-.35-.11-.71-.15-1.07s-.07-.52-.1-.79-.05-.51-.07-.77l-.09-1.12v-.52-1.39V81a36 36 0 0 1 36-36 36 36 0 0 1 36 36v161.22l92.85 53.61A36 36 0 0 1 391 345z' fill='%2300a14b'/%3e%3c/svg%3e");
$bg-icon-database: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23666666' d='M256 256C114.615 256 0 213.019 0 160v256c0 53.019 114.615 96 256 96s256-42.981 256-96V160c0 53.019-114.615 96-256 96z'/%3e%3cellipse fill='%23666666' cx='256' cy='96' rx='256' ry='96'/%3e%3c/svg%3e");
$bg-icon-database-query: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23666666' d='M341.76 409.643C316.369 423.871 287.118 432 256 432c-97.047 0-176-78.953-176-176S158.953 80 256 80s176 78.953 176 176c0 31.118-8.129 60.369-22.357 85.76l95.846 95.846C509.747 430.661 512 423.429 512 416V96c0-53.019-114.615-96-256-96S0 42.981 0 96v320c0 53.019 114.615 96 256 96 63.055 0 120.774-8.554 165.388-22.73l-79.628-79.627z'/%3e%3cpath fill='%23666666' d='M176 256c0 44.112 35.888 80 80 80s80-35.888 80-80-35.888-80-80-80-80 35.888-80 80z'/%3e%3c/svg%3e");
$bg-icon-dataset: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23666666' d='M448 96H288l-54.6-54.6-18.7-18.7C202.2 10.2 177.6 0 160 0H32C14.4 0 0 14.4 0 32v192c0-35.2 28.8-64 64-64h384c35.2 0 64 28.8 64 64v-64c0-35.2-28.8-64-64-64zM448 224H64c-35.2 0-64 28.8-64 64v160c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64zM160 448H96V288h64v160zm128 0h-64V288h64v160zm128 0h-64V288h64v160z'/%3e%3c/svg%3e");

View File

@ -1,84 +1,165 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/******************************************************** BUTTONS */
%c-control {
@include userSelectNone();
$fs: 1em;
display: inline-flex;
align-items: center;
font-size: $fs;
justify-content: start;
cursor: pointer;
&:before,
&:after {
font-family: symbolsfont;
display: block;
flex: 0 0 auto;
}
&:after {
font-size: 0.8em;
}
[class*="__label"] {
line-height: $fs; // Remove effect on top and bottom padding
font-size: $fs;
}
}
%c-button {
@extend %c-control;
background: $colorBtnBg;
border-radius: $controlCr;
color: $colorBtnFg;
padding: nth($btnPad, 1) nth($btnPad, 2);
&:hover {
background: $colorBtnBgHov;
color: $colorBtnFgHov;
}
&[class*="--major"] {
background: $colorBtnMajorBg;
color: $colorBtnMajorFg;
&:hover {
background: $colorBtnMajorBgHov;
color: $colorBtnMajorFgHov;
}
}
}
/********* Buttons */
// Optionally can include icon in :before via markup
button {
@include htmlInputReset();
}
.c-button,
.c-button--menu {
@include cButton();
button {
@extend %c-button;
}
.c-button--menu {
$m: $interiorMargin;
/********* Icon Buttons */
.c-icon-button {
// A clickable element that just includes the icon, no background
// Padding is included to facilitate a bigger hit area
// Make the icon bigger relative to its container
@extend %c-control;
$pLR: 3px;
$pTB: 3px;
border-radius: $controlCr;
color: $colorKey;
font-size: $fontBaseSize * 1.2;
padding: $pTB $pLR ;
&:hover {
background: rgba($colorKey, 0.2);
}
&:before {
font-size: 1.1em;
}
}
/********* Button Sets */
.c-button-set {
// Buttons are smashed together with minimal margin
// c-buttons don't have border-radius between buttons, creates a tool-button-strip look
// c-icon-buttons get grouped more closely together
// When one set is adjacent to another, provides a divider between
display: inline-flex;
&:before,
> * {
// Assume buttons are immediate descendants
flex: 0 0 auto;
&[class^="c-button"] {
// Only apply the following to buttons that have background, eg. c-button
border-radius: 0;
+ * {
margin-left: 1px;
}
&:first-child {
border-top-left-radius: $controlCr;
border-bottom-left-radius: $controlCr;
}
&:last-child {
border-top-right-radius: $controlCr;
border-bottom-right-radius: $controlCr;
}
}
}
+ .c-button-set {
$m: $interiorMarginSm;
&:before {
content: '';
display: block;
width: $m;
border-right: 1px solid $colorInteriorBorder;
margin-right: $m;
}
}
}
/********* Menu Buttons */
// Always includes :after dropdown arrow
// Optionally can include icon in :before
// Default menu position is down and to the right
// Apply c-menu-button--menus-up and c-menu-button--menus-left to --w wrapper element to modify menu position
.c-menu-button {
$m: $interiorMarginSm;
@extend %c-button;
&:before {
margin-right: $m;
}
&:after {
content: $glyph-icon-arrow-down;
font-family: symbolsfont;
margin-left: $m;
opacity: 0.5;
}
}
/********* Icon Buttons */
.c-click-icon {
@include cClickIcon();
&--menu {
&:after {
content: $glyph-icon-arrow-down;
font-family: symbolsfont;
font-size: 0.6em;
margin-left: floor($interiorMarginSm * 0.8);
opacity: 0.5;
}
}
&--swatched {
// Color control, show swatch element
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
> [class*='swatch'] {
box-shadow: inset rgba(black, 0.2) 0 0 1px;
flex: 0 0 auto;
height: 4px;
width: 100%;
margin-top: 1px;
&--w {
// Wraps c-menu-button, contains button and menu
.c-menu {
// Default position
top: 100%; left: 0;
}
&:before {
// Reduce size of icon to make a bit of room
flex: 1 1 auto;
font-size: 1.1em;
&.c-menu-button--menus-up {
.c-menu {
top: auto; bottom: 100%;
}
}
&.c-menu-button--menus-left {
.c-menu {
left: auto; right: 0;
}
}
}
}
@ -88,7 +169,7 @@ button {
// Provides a downward arrow icon that when clicked displays a context menu
// Always placed AFTER an element
.c-disclosure-button {
@include cClickIcon();
@extend .c-icon-button;
margin-left: $interiorMarginSm;
&:before {
@ -97,7 +178,7 @@ button {
font-size: 0.7em;
}
}
/********* Disclosure Triangle */
/********* Disclosure Triangle */
// Provides an arrow icon that when clicked expands an element to reveal its contents.
// Used in tree items. Always placed BEFORE an element.
.c-disclosure-triangle {
@ -127,95 +208,36 @@ button {
}
}
/******************************************************** FORM ELEMENTS */
input, textarea {
font-family: inherit;
font-weight: inherit;
letter-spacing: inherit;
}
/********* Inline inputs */
// A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus
.c-input-inline {
@include input-base();
border: 1px solid transparent;
display: block !important;
min-width: 0;
padding-left: 0;
padding-right: 0;
overflow: hidden;
transition: all 250ms ease;
white-space: nowrap;
input[type=text],
input[type=search],
input[type=number] {
@include reactive-input();
padding: $inputTextP;
&.numeric {
text-align: right;
}
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
margin-right: -5px !important;
margin-top: -1px !important;
}
.c-input {
&--datetime {
// Sized for values such as 2018-09-28 22:32:33.468Z
width: 160px;
&:not(:focus) {
text-overflow: ellipsis;
}
&--hrs-min-sec {
// Sized for values such as 00:25:00
width: 60px;
&:hover,
&:focus {
padding-left: $inputTextPLeftRight;
padding-right: $inputTextPLeftRight;
}
&-inline,
&--inline {
// A text input or contenteditable element that indicates edit affordance on hover and looks like an input on focus
@include reactive-input($bg: transparent);
box-shadow: none;
display: block !important;
min-width: 0;
padding-left: 0;
padding-right: 0;
overflow: hidden;
transition: all 250ms ease;
white-space: nowrap;
&:not(:focus) {
text-overflow: ellipsis;
}
&:hover,
&:focus {
background: $colorInputBg;
padding-left: $inputTextPLeftRight;
padding-right: $inputTextPLeftRight;
}
&:hover {
border-color: rgba($colorBodyFg, 0.2);
}
&--labeled {
// TODO: replace .c-labeled-input with this
// An input used in the Toolbar
// Assumes label is before the input
@include cControl();
input {
margin-left: $interiorMarginSm;
}
}
}
.c-labeled-input {
// An input used in the Toolbar
// Assumes label is before the input
@include cControl();
input {
margin-left: $interiorMarginSm;
}
}
/******************************************************** HYPERLINKS AND HYPERLINK BUTTONS */
.c-hyperlink {
&--link {
color: $colorKey;
}
&--button {
@include cButton();
&:focus {
@include nice-input($shdw: rgba(0, 0, 0, 0.6) 0 1px 3px);
border-color: transparent;
}
}
@ -232,11 +254,10 @@ input[type=number]::-webkit-outer-spin-button {
}
@mixin menuInner() {
color: $colorMenuFg;
li {
@include cControl();
justify-content: start;
@extend %c-control;
color: $colorMenuFg;
cursor: pointer;
display: flex;
padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
transition: $transIn;
@ -260,6 +281,11 @@ input[type=number]::-webkit-outer-spin-button {
.c-menu {
@include menuOuter();
@include menuInner();
li {
&:not(:first-child) {
border-top: 1px solid pullForward($colorMenuBg, 10%);
}
}
}
.c-super-menu {
@ -301,6 +327,11 @@ input[type=number]::-webkit-outer-spin-button {
justify-content: stretch;
.l-item-description {
&__icon,
&__description {
//flex: 1 1 50%;
}
&__name,
&__description {
margin-top: $interiorMarginLg;
@ -312,7 +343,6 @@ input[type=number]::-webkit-outer-spin-button {
}
&__name {
color: $colorMenuFg;
flex: 0 0 auto;
font-size: 1.25em;
}
@ -324,161 +354,3 @@ input[type=number]::-webkit-outer-spin-button {
}
}
/******************************************************** PALETTES */
.c-palette {
display: flex;
flex-flow: column nowrap;
&__items {
flex: 1 1 auto;
display: grid;
grid-template-columns: repeat(10, [col] auto );
grid-gap: 1px;
}
&__item {
$d: 16px;
border: 1px solid transparent;
cursor: pointer;
width: 16px; height: 16px;
transition: $transOut;
&:hover {
transition: $transIn;
$o: 0.7;
border-color: rgba($paletteItemBorderOuterColorSelected, $o);
box-shadow: inset rgba($paletteItemBorderInnerColorSelected, $o) 0 0 0 1px;
}
&.is-selected {
border-color: $paletteItemBorderOuterColorSelected !important;
border-width: 2px;
box-shadow: inset rgba($paletteItemBorderInnerColorSelected, 1) 0 0 0 1px;
}
}
&__item-none {
flex: 0 0 auto;
display: flex;
align-items: center;
margin-bottom: $interiorMarginSm;
.c-palette__item {
@include noColor();
border-color: $paletteItemBorderInnerColor;
margin-right: $interiorMarginSm;
}
}
}
/******************************************************** TOOLBAR */
.c-ctrl-wrapper {
@include cCtrlWrapper();
}
.c-toolbar,
.c-toolbar .c-ctrl-wrapper {
display: flex;
align-items: stretch;
}
.c-toolbar {
height: 24px; // Need to standardize the height
.c-click-icon {
@include cControl();
$pLR: $interiorMargin - 1;
$pTB: 2px;
color: $colorBodyFg;
padding: $pTB $pLR;
&--swatched {
padding-bottom: floor($pTB / 2);
width: 2em; // Standardize the width
}
&[class*='--caution'] {
&:before {
color: $colorBtnCautionBg;
}
&:hover {
background: rgba($colorBtnCautionBgHov, 0.2);
:before {
color: $colorBtnCautionBgHov;
}
}
}
}
.c-labeled-input {
font-size: 0.9em;
input[type='number'] {
width: 40px; // Number input sucks and must have size set using this method
}
+ .c-labeled-input {
margin-left: $interiorMargin;
}
}
}
/********* Button Sets */
.c-button-set {
// When one set is adjacent to another, provides a divider between
display: inline-flex;
flex: 0 0 auto;
> * {
// Assume buttons are immediate descendants
flex: 0 0 auto;
+ * {
// margin-left: $interiorMarginSm;
}
}
+ .c-button-set {
$m: $interiorMargin;
$b: 1px;
&:before {
content: '';
display: block;
width: $m + $b; // Allow for border
border-right: $b solid $colorInteriorBorder;
margin-right: $m;
}
}
&[class*='--strip'] {
// Buttons are smashed together with minimal margin
// c-buttons don't have border-radius between buttons, creates a tool-button-strip look
// c-click-icons get grouped more closely together
&[class^="c-button"] {
// Only apply the following to buttons that have background, eg. c-button
border-radius: 0;
+ * {
margin-left: 1px;
}
&:first-child {
border-top-left-radius: $controlCr;
border-bottom-left-radius: $controlCr;
}
&:last-child {
border-top-right-radius: $controlCr;
border-bottom-right-radius: $controlCr;
}
}
}
}
/***************************************************** SLIDERS */
.c-slider {
@include cControl();
> * + * { margin-left: $interiorMargin; }
}

View File

@ -1,25 +1,3 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/************************** FONTS */
@font-face {
/*
@ -92,16 +70,23 @@ body.desktop {
}
}
::-webkit-scrollbar-corner {
background: transparent;
.overlay ::-webkit-scrollbar-thumb {
background: $scrollbarThumbColorOverlay;
&:hover {
background: $scrollbarThumbColorOverlayHov;
}
}
.c-menu ::-webkit-scrollbar-thumb {
.menu ::-webkit-scrollbar-thumb {
background: $scrollbarThumbColorMenu;
&:hover {
background: $scrollbarThumbColorMenuHov;
}
}
::-webkit-scrollbar-corner {
background: transparent;
}
}
/************************** HTML ENTITIES */
@ -133,6 +118,22 @@ em {
font-style: normal;
}
input, textarea {
font-family: inherit;
font-weight: inherit;
letter-spacing: inherit;
}
input[type=text],
input[type=search],
input[type=number] {
@include nice-input();
padding: $inputTextP;
&.numeric {
text-align: right;
}
}
h1, h2, h3 {
letter-spacing: 0.04em;
margin: 0;
@ -168,6 +169,7 @@ li {
padding: 0;
}
/******************************************************** HAS */
// Local Controls: Controls placed in proximity to or overlaid on components and views
body.desktop .has-local-controls {
@ -378,8 +380,3 @@ a.disabled {
.t-imagery {
display: contents;
}
.t-frame-outer {
min-width: 200px;
min-height: 200px;
}

View File

@ -1,25 +1,3 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
@mixin glyphBefore($unicode, $family: 'symbolsfont') {
&:before {
content: $unicode;
@ -113,7 +91,7 @@
.icon-refresh { @include glyphBefore($glyph-icon-refresh); }
.icon-save { @include glyphBefore($glyph-icon-save); }
.icon-sine { @include glyphBefore($glyph-icon-sine); }
.icon-font { @include glyphBefore($glyph-icon-font); }
.icon-T { @include glyphBefore($glyph-icon-T); }
.icon-thumbs-strip { @include glyphBefore($glyph-icon-thumbs-strip); }
.icon-two-parts-both { @include glyphBefore($glyph-icon-two-parts-both); }
.icon-two-parts-one-only { @include glyphBefore($glyph-icon-two-parts-one-only); }
@ -130,7 +108,6 @@
.icon-frame-hide { @include glyphBefore($glyph-icon-frame-hide); }
.icon-import { @include glyphBefore($glyph-icon-import); }
.icon-export { @include glyphBefore($glyph-icon-export); }
.icon-font-size { @include glyphBefore($glyph-icon-font-size); }
.icon-activity { @include glyphBefore($glyph-icon-activity); }
.icon-activity-mode { @include glyphBefore($glyph-icon-activity-mode); }
.icon-autoflow-tabular { @include glyphBefore($glyph-icon-autoflow-tabular); }

View File

@ -31,20 +31,6 @@
width: $d;
}
@mixin isAlias() {
&:after {
color:$colorIconAlias;
content: $glyph-icon-link;
display: block;
font-family: symbolsfont;
position: absolute;
text-shadow: rgba(black, 0.5) 0 1px 4px;
top: auto; left: 0; bottom: 10%; right: auto;
transform-origin: left bottom;
transform: scale(0.5);
}
}
@mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) {
background-image: linear-gradient(-45deg,
rgba($c, $a) 25%, transparent 25%,
@ -67,37 +53,6 @@
background-size: $bgsize $bgsize;
}
@mixin noColor() {
// A "no fill/stroke" selection option. Used in palettes.
$c: red;
$s: 48%;
$e: 52%;
background-image: linear-gradient(-45deg,
transparent $s - 5%,
$c $s,
$c $e,
transparent $e + 5%
);
background-repeat: no-repeat;
background-size: contain;
}
@mixin bgTicks($c: $colorBodyFg, $repeatDir: 'x') {
$deg: 90deg;
@if ($repeatDir != 'x') {
$deg: 0deg;
$repeatDir: repeat-y;
} @else {
$repeatDir: repeat-x;
}
background-image: linear-gradient($deg,
$c 1px, transparent 1px,
transparent 100%
);
background-repeat: $repeatDir;
}
@mixin bgVertStripes($c: yellow, $a: 0.1, $d: 40px) {
@include background-image(linear-gradient(-90deg,
rgba($c, $a) 0%, rgba($c, $a) 50%,
@ -108,26 +63,33 @@
}
/************************** LAYOUT */
@mixin abs($m: 0) {
position: absolute;
top: $m; right: $m; bottom: $m; left: $m;
@mixin gridTwoColumn() {
display: grid;
grid-row-gap: 0;
grid-template-columns: 1fr 2fr;
align-items: start;
[class*="header"] {
border-radius: $smallCr;
background-color: $colorInspectorSectionHeaderBg;
color: $colorInspectorSectionHeaderFg;
margin: 0 0 $interiorMarginSm 0;
padding: $interiorMarginSm $interiorMargin;
&:not(:first-child) {
// Allow multiple headers within a component
margin-top: $interiorMarginLg;
}
}
[class*="span-all"],
[class*="header"] {
@include gridTwoColumnSpanCols();
}
}
@mixin propertiesHeader() {
border-radius: $smallCr;
background-color: $colorInspectorSectionHeaderBg;
color: $colorInspectorSectionHeaderFg;
font-weight: normal;
margin: 0 0 $interiorMarginSm 0;
padding: $interiorMarginSm $interiorMargin;
text-transform: uppercase;
}
@mixin modalFullScreen() {
// Optional modifier that makes a c-menu more mobile-friendly
position: fixed;
border-radius: 0;
top: 0; right: 0; bottom: 0; left: 0;
@mixin gridTwoColumnSpanCols() {
grid-column: 1 / 3;
}
/************************** TEXT */
@ -143,247 +105,43 @@
unicode-bidi:bidi-override;
}
/************************** CONTROLS, BUTTONS */
@mixin hover {
body.desktop & {
&:hover {
@content
}
}
@mixin test($c: #ffcc00, $a: 0.2) {
background-color: rgba($c, $a) !important;
}
@mixin htmlInputReset() {
/************************** CONTROLS, BUTTONS */
@mixin input-base() {
appearance: none;
background: transparent;
border: none;
border-radius: 0;
border-radius: $controlCr;
outline: none;
text-align: inherit;
&:focus {
outline: 0;
}
}
@mixin input-base() {
@include htmlInputReset();
border-radius: $controlCr;
&.error {
background: $colorFormFieldErrorBg;
background-color: $colorFormFieldErrorBg;
color: $colorFormFieldErrorFg;
}
}
@mixin nice-input($bg: $colorInputBg, $fg: $colorInputFg, $shdw: rgba(black, 0.5) 0 0 2px) {
@mixin nice-input($bg: $colorInputBg, $fg: $colorInputFg, $shdw: rgba(black, 0.5) 0 0px 2px) {
@include input-base();
background: $bg;
color: $fg;
box-shadow: inset $shdw;
}
@mixin reactive-input($bg: $colorInputBg, $fg: $colorInputFg) {
@include input-base();
background: $bg;
box-shadow: $shdwInput;
color: $fg;
&:focus {
box-shadow: $shdwInputFoc;
}
@include hover() {
&:not(:focus) {
box-shadow: $shdwInputHov;
}
}
}
@mixin button($bg: $colorBtnBg, $fg: $colorBtnFg, $radius: $controlCr, $shdw: none) {
// Is this being used? Remove if not.
background: $bg;
color: $fg;
border-radius: $radius;
box-shadow: $shdw;
}
@mixin cControl() {
$fs: 1em;
@include userSelectNone();
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
&:before,
&:after {
font-family: symbolsfont;
display: block;
flex: 0 0 auto;
}
&:after {
font-size: 0.8em;
}
[class*="__label"] {
@include ellipsize();
display: block;
line-height: $fs; // Remove effect on top and bottom padding
font-size: $fs;
}
}
@mixin cButton() {
@include cControl();
@include themedButton();
border-radius: $controlCr;
color: $colorBtnFg;
cursor: pointer;
padding: $interiorMargin floor($interiorMargin * 1.25);
@include hover() {
background: $colorBtnBgHov;
color: $colorBtnFgHov;
}
&[class*="--major"] {
background: $colorBtnMajorBg;
color: $colorBtnMajorFg;
@include hover() {
background: $colorBtnMajorBgHov;
color: $colorBtnMajorFgHov;
}
}
&[class*='--caution'] {
background: $colorBtnCautionBg;
color: $colorBtnCautionFg;
&:hover {
background: $colorBtnCautionBgHov;
}
}
}
@mixin cClickIcon() {
// A clickable element that just includes the icon, no background
// Padding is included to facilitate a bigger hit area
// Make the icon bigger relative to its container
@include cControl();
$pLR: 4px;
$pTB: 3px;
background: none;
box-shadow: none;
border-radius: $controlCr;
color: $colorKey;
cursor: pointer;
padding: $pTB $pLR ;
@include hover() {
background: $colorClickIconBgHov;
color: $colorClickIconFgHov;
}
&:before,
*:before {
// *:before handles any nested containers that may contain glyph elements
// Needed for c-togglebutton.
font-size: 1.2em;
}
}
@mixin cCtrlWrapper {
// Provides a wrapper around buttons and other controls
// Contains control and provides positioning context for contained menu/palette.
// Wraps --menu elements, contains button and menu
overflow: visible;
.c-menu {
// Default position of contained menu
top: 100%; left: 0;
}
&[class*='--menus-up'] {
.c-menu {
top: auto; bottom: 100%;
}
}
&[class*='--menus-left'] {
.c-menu {
left: auto; right: 0;
}
}
}
@mixin wrappedInput() {
// An input that is wrapped. Optionally includes a __label or icon element.
// Based on .c-search.
@include nice-input();
display: flex;
align-items: center;
padding-left: 4px;
padding-right: 4px;
&:before,
[class*='__label'] {
opacity: 0.5;
}
&:before {
// Adds an icon. Content defined in class.
direction: rtl; // Aligns glyph to right-hand side of container, for transition
display: block;
font-family: symbolsfont;
flex: 0 0 auto;
overflow: hidden;
padding: 2px 0; // Prevents clipping
transition: width 250ms ease;
width: 1em;
}
&:hover {
box-shadow: inset rgba(black, 0.8) 0 0px 2px;
&:before {
opacity: 0.9;
}
}
&--major {
padding: 4px;
}
&__input,
input[type='text'],
input[type='search'],
input[type='number'] {
background: none !important;
box-shadow: none !important; // !important needed to override default for [input]
flex: 1 1 auto;
padding-left: 2px !important;
padding-right: 2px !important;
min-width: 10px; // Must be set to allow input to collapse below browser min
}
&.is-active {
&:before {
padding: 2px 0px;
width: 0px;
}
}
}
/************************** MATH */
@function percentToDecimal($p) {
@return $p / 100%;
@ -403,17 +161,4 @@
@mixin userSelectNone() {
@include browserPrefix(user-select, none);
}
@mixin cursorGrab() {
cursor: grab;
cursor: -webkit-grab;
&:active {
cursor: grabbing;
cursor: -webkit-grabbing;
}
}
@mixin test($c: deeppink, $a: 0.3) {
background-color: rgba($c, $a) !important;
}
}

View File

@ -1,91 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
/******************************************************** TABLE */
.c-table {
// Can be used by any type of table, scrolling, LAD, etc.
$min-w: 50px;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
&__control-bar,
&__headers-w {
flex: 0 0 auto;
}
/******************************* ELEMENTS */
th, td {
white-space: nowrap;
min-width: $min-w;
padding: $tabularTdPadTB $tabularTdPadLR;
}
td {
color: $colorTelemFresh;
vertical-align: top;
}
&__control-bar {
margin-bottom: $interiorMarginSm;
}
[class*="__header"] {
background: $colorTabHeaderBg;
th {
&:not(:first-child) {
border-left: 1px solid $colorTabHeaderBorder;
}
}
}
&__body {
tr {
&:not(:first-child) {
border-top: 1px solid $colorTabBorder;
}
}
}
&--sortable {
.is-sorting {
&:after {
color: $colorIconAlias;
content: $glyph-icon-arrow-tall-up;
font-family: symbolsfont;
font-size: 8px;
display: inline-block;
margin-left: $interiorMarginSm;
}
&.desc:after {
content: $glyph-icon-arrow-tall-down;
}
}
.is-sortable {
cursor: pointer;
}
}
}

View File

@ -1,25 +1,3 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
@import "vendor/normalize.min.css";
@import "sass-base.scss";

View File

@ -2,7 +2,7 @@
"metadata": {
"name": "openmct-symbols-16px",
"lastOpened": 0,
"created": 1537817705550
"created": 1529545133464
},
"iconSets": [
{
@ -525,7 +525,7 @@
"tempChar": ""
},
{
"order": 149,
"order": 102,
"prevSize": 24,
"name": "icon-T",
"id": 84,
@ -660,21 +660,13 @@
"code": 921672,
"tempChar": ""
},
{
"order": 150,
"id": 119,
"name": "icon-font-size-alt1",
"prevSize": 24,
"code": 921673,
"tempChar": ""
},
{
"order": 37,
"prevSize": 24,
"name": "icon-activity",
"id": 32,
"code": 921856,
"tempChar": ""
"tempChar": ""
},
{
"order": 36,
@ -682,7 +674,7 @@
"name": "icon-activity-mode",
"id": 31,
"code": 921857,
"tempChar": ""
"tempChar": ""
},
{
"order": 52,
@ -690,7 +682,7 @@
"name": "icon-autoflow-tabular",
"id": 47,
"code": 921858,
"tempChar": ""
"tempChar": ""
},
{
"order": 55,
@ -698,7 +690,7 @@
"name": "icon-clock",
"id": 50,
"code": 921859,
"tempChar": ""
"tempChar": ""
},
{
"order": 58,
@ -706,7 +698,7 @@
"name": "icon-database",
"id": 53,
"code": 921860,
"tempChar": ""
"tempChar": ""
},
{
"order": 57,
@ -714,7 +706,7 @@
"name": "icon-database-query",
"id": 52,
"code": 921861,
"tempChar": ""
"tempChar": ""
},
{
"order": 17,
@ -722,7 +714,7 @@
"name": "icon-dataset",
"id": 12,
"code": 921862,
"tempChar": ""
"tempChar": ""
},
{
"order": 22,
@ -730,7 +722,7 @@
"name": "icon-datatable",
"id": 17,
"code": 921863,
"tempChar": ""
"tempChar": ""
},
{
"order": 59,
@ -738,7 +730,7 @@
"name": "icon-dictionary",
"id": 54,
"code": 921864,
"tempChar": ""
"tempChar": ""
},
{
"order": 62,
@ -746,7 +738,7 @@
"name": "icon-folder",
"id": 57,
"code": 921865,
"tempChar": ""
"tempChar": ""
},
{
"order": 66,
@ -754,7 +746,7 @@
"name": "icon-image",
"id": 61,
"code": 921872,
"tempChar": ""
"tempChar": ""
},
{
"order": 68,
@ -762,7 +754,7 @@
"name": "icon-layout",
"id": 63,
"code": 921873,
"tempChar": ""
"tempChar": ""
},
{
"order": 77,
@ -770,7 +762,7 @@
"name": "icon-object",
"id": 72,
"code": 921874,
"tempChar": ""
"tempChar": ""
},
{
"order": 78,
@ -778,7 +770,7 @@
"name": "icon-object-unknown",
"id": 73,
"code": 921875,
"tempChar": ""
"tempChar": ""
},
{
"order": 79,
@ -786,7 +778,7 @@
"name": "icon-packet",
"id": 74,
"code": 921876,
"tempChar": ""
"tempChar": ""
},
{
"order": 80,
@ -794,7 +786,7 @@
"name": "icon-page",
"id": 75,
"code": 921877,
"tempChar": ""
"tempChar": ""
},
{
"order": 135,
@ -802,7 +794,7 @@
"name": "icon-plot-overlay",
"prevSize": 24,
"code": 921878,
"tempChar": ""
"tempChar": ""
},
{
"order": 113,
@ -810,7 +802,7 @@
"name": "icon-plot-stacked",
"prevSize": 24,
"code": 921879,
"tempChar": ""
"tempChar": ""
},
{
"order": 10,
@ -818,7 +810,7 @@
"name": "icon-session",
"id": 5,
"code": 921880,
"tempChar": ""
"tempChar": ""
},
{
"order": 24,
@ -826,7 +818,7 @@
"name": "icon-tabular",
"id": 19,
"code": 921881,
"tempChar": ""
"tempChar": ""
},
{
"order": 7,
@ -834,7 +826,7 @@
"name": "icon-tabular-lad",
"id": 2,
"code": 921888,
"tempChar": ""
"tempChar": ""
},
{
"order": 6,
@ -842,7 +834,7 @@
"name": "icon-tabular-lad-set",
"id": 1,
"code": 921889,
"tempChar": ""
"tempChar": ""
},
{
"order": 8,
@ -850,7 +842,7 @@
"name": "icon-tabular-realtime",
"id": 3,
"code": 921890,
"tempChar": ""
"tempChar": ""
},
{
"order": 23,
@ -858,7 +850,7 @@
"name": "icon-tabular-scrolling",
"id": 18,
"code": 921891,
"tempChar": ""
"tempChar": ""
},
{
"order": 112,
@ -866,7 +858,7 @@
"name": "icon-telemetry",
"id": 86,
"code": 921892,
"tempChar": ""
"tempChar": ""
},
{
"order": 90,
@ -874,7 +866,7 @@
"name": "icon-telemetry-panel",
"id": 85,
"code": 921893,
"tempChar": ""
"tempChar": ""
},
{
"order": 93,
@ -882,15 +874,15 @@
"name": "icon-timeline",
"id": 88,
"code": 921894,
"tempChar": ""
"tempChar": ""
},
{
"order": 116,
"id": 101,
"name": "icon-timer-v15",
"name": "icon-timer-v1.5",
"prevSize": 24,
"code": 921895,
"tempChar": ""
"tempChar": ""
},
{
"order": 11,
@ -898,7 +890,7 @@
"name": "icon-topic",
"id": 6,
"code": 921896,
"tempChar": ""
"tempChar": ""
},
{
"order": 115,
@ -906,7 +898,7 @@
"name": "icon-box-with-dashed-lines",
"id": 29,
"code": 921897,
"tempChar": ""
"tempChar": ""
},
{
"order": 126,
@ -914,7 +906,7 @@
"name": "icon-summary-widget",
"prevSize": 24,
"code": 921904,
"tempChar": ""
"tempChar": ""
},
{
"order": 139,
@ -922,13 +914,13 @@
"name": "icon-notebook",
"prevSize": 24,
"code": 921905,
"tempChar": ""
"tempChar": ""
}
],
"metadata": {
"name": "openmct-symbols-16px",
"importSize": {
"width": 745,
"width": 512,
"height": 512
},
"designer": "Charles Hacskaylo",
@ -2368,7 +2360,7 @@
},
{
"paths": [
"M800 1024h224l-384-1024h-256l-384 1024h224l84-224h408zM380 608l132-352 132 352z"
"M0 0v256h128v-64h256v704h-192v128h640v-128h-192v-704h256v64h128v-256z"
],
"grid": 16,
"tags": [
@ -2376,15 +2368,9 @@
],
"defaultCode": 228,
"id": 84,
"attrs": [
{}
],
"isMulticolor": false,
"isMulticolor2": false,
"attrs": [],
"colorPermutations": {
"1161751207457516161751": [
{}
]
"1161751207457516161751": []
}
},
{
@ -2854,30 +2840,6 @@
]
}
},
{
"id": 119,
"paths": [
"M1226.4 320h-176l-76.22 203.24 77 205.34 87.22-232.58 90.74 242h-174.44l49.5 132h174.44l57.76 154h154l-264-704z",
"M384 0l-384 1024h224l84-224h408l84 224h224l-384-1024zM380 608l132-352 132 352z"
],
"attrs": [
{},
{}
],
"width": 1490,
"isMulticolor": false,
"isMulticolor2": false,
"grid": 16,
"tags": [
"icon-font-size-alt1"
],
"colorPermutations": {
"1161751207457516161751": [
{},
{}
]
}
},
{
"paths": [
"M576 64h-256l320 320h-290.256c-44.264-76.516-126.99-128-221.744-128h-128v512h128c94.754 0 177.48-51.484 221.744-128h290.256l-320 320h256l448-448-448-448z"
@ -3778,9 +3740,7 @@
"classSelector": ".ui-symbol",
"showMetrics": true,
"showMetadata": true,
"embed": false,
"noie8": true,
"ie7": false
"embed": false
},
"imagePref": {
"prefix": "icon-",

Some files were not shown because too many files have changed in this diff Show More