Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharles Hacskaylo <charles.f.hacskaylo@nasa.gov>2015-10-26 23:20:26 +0300
committerCharles Hacskaylo <charles.f.hacskaylo@nasa.gov>2015-10-26 23:20:26 +0300
commit6868bfd4e1284a339eaa0d43051701e9c3f0d38b (patch)
treeaeb67886c951f514b802efe6ca87ed6807bdfabd
parent95ea33b4411a8d49b558f03b003b7e28380bd86b (diff)
[Merge] Resolve conflicts in open155open155a
open #155 Conflicts: platform/commonUI/general/res/sass/user-environ/_layout.scss platform/commonUI/themes/espresso/res/css/theme-espresso.css platform/commonUI/themes/snow/res/css/theme-snow.css platform/commonUI/themes/snow/res/sass/_constants.scss
-rw-r--r--Procfile2
-rw-r--r--README.md15
-rw-r--r--docs/gendocs.js27
-rw-r--r--docs/src/architecture/Platform.md22
-rw-r--r--docs/src/guide/index.md2335
-rw-r--r--docs/src/index.html3
-rw-r--r--docs/src/tutorials/images/add-task.pngbin0 -> 23938 bytes
-rw-r--r--docs/src/tutorials/images/bar-plot-2.pngbin0 -> 39736 bytes
-rw-r--r--docs/src/tutorials/images/bar-plot-3.pngbin0 -> 42557 bytes
-rw-r--r--docs/src/tutorials/images/bar-plot-4.pngbin0 -> 31252 bytes
-rw-r--r--docs/src/tutorials/images/bar-plot.pngbin0 -> 35055 bytes
-rw-r--r--docs/src/tutorials/images/chrome.pngbin0 -> 143095 bytes
-rw-r--r--docs/src/tutorials/images/remove-task.pngbin0 -> 11357 bytes
-rw-r--r--docs/src/tutorials/images/telemetry-1.pngbin0 -> 16915 bytes
-rw-r--r--docs/src/tutorials/images/telemetry-2.pngbin0 -> 39792 bytes
-rw-r--r--docs/src/tutorials/images/telemetry-3.pngbin0 -> 51914 bytes
-rw-r--r--docs/src/tutorials/images/todo-edit.pngbin0 -> 25216 bytes
-rw-r--r--docs/src/tutorials/images/todo-list.pngbin0 -> 26034 bytes
-rw-r--r--docs/src/tutorials/images/todo-restyled.pngbin0 -> 25700 bytes
-rw-r--r--docs/src/tutorials/images/todo-selection.pngbin0 -> 32170 bytes
-rw-r--r--docs/src/tutorials/images/todo.pngbin0 -> 44219 bytes
-rw-r--r--docs/src/tutorials/index.md3055
-rw-r--r--example/generator/bundle.json6
-rw-r--r--example/generator/src/SinewaveTelemetryProvider.js8
-rw-r--r--example/generator/src/SinewaveTelemetrySeries.js (renamed from example/generator/src/SinewaveTelemetry.js)36
-rw-r--r--example/profiling/bundle.json6
-rw-r--r--example/profiling/src/DigestIndicator.js77
-rw-r--r--package.json3
-rw-r--r--platform/commonUI/browse/bundle.json5
-rw-r--r--platform/commonUI/browse/res/templates/browse-object.html9
-rw-r--r--platform/commonUI/browse/res/templates/browse.html4
-rw-r--r--platform/commonUI/browse/res/templates/create/create-button.html2
-rw-r--r--platform/commonUI/browse/src/creation/CreationService.js2
-rw-r--r--platform/commonUI/edit/res/templates/edit-object.html11
-rw-r--r--platform/commonUI/general/bundle.json29
-rw-r--r--platform/commonUI/general/res/sass/_constants.scss1
-rw-r--r--platform/commonUI/general/res/sass/_global.scss12
-rw-r--r--platform/commonUI/general/res/sass/_icons.scss8
-rw-r--r--platform/commonUI/general/res/sass/_mixins.scss23
-rw-r--r--platform/commonUI/general/res/sass/_views.scss3
-rw-r--r--platform/commonUI/general/res/sass/controls/_controls.scss146
-rw-r--r--platform/commonUI/general/res/sass/controls/_menus.scss290
-rw-r--r--platform/commonUI/general/res/sass/controls/_time-controller.scss179
-rw-r--r--platform/commonUI/general/res/sass/forms/_datetime.scss33
-rw-r--r--platform/commonUI/general/res/sass/forms/_selects.scss10
-rw-r--r--platform/commonUI/general/res/sass/helpers/_wait-spinner.scss73
-rw-r--r--platform/commonUI/general/res/sass/lists/_tabular.scss8
-rw-r--r--platform/commonUI/general/res/sass/plots/_plots-main.scss2
-rw-r--r--platform/commonUI/general/res/sass/search/_search.scss24
-rw-r--r--platform/commonUI/general/res/sass/tree/_tree.scss19
-rw-r--r--platform/commonUI/general/res/sass/user-environ/_frame.scss4
-rw-r--r--platform/commonUI/general/res/templates/controls/datetime-picker.html66
-rw-r--r--platform/commonUI/general/res/templates/controls/switcher.html2
-rw-r--r--platform/commonUI/general/res/templates/controls/time-controller.html159
-rw-r--r--platform/commonUI/general/src/controllers/DateTimePickerController.js202
-rw-r--r--platform/commonUI/general/src/controllers/TimeRangeController.js302
-rw-r--r--platform/commonUI/general/src/directives/MCTClickElsewhere.js77
-rw-r--r--platform/commonUI/general/src/directives/MCTPopup.js73
-rw-r--r--platform/commonUI/general/src/directives/MCTResize.js8
-rw-r--r--platform/commonUI/general/src/services/Popup.js89
-rw-r--r--platform/commonUI/general/src/services/PopupService.js127
-rw-r--r--platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js63
-rw-r--r--platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js237
-rw-r--r--platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js84
-rw-r--r--platform/commonUI/general/test/directives/MCTPopupSpec.js136
-rw-r--r--platform/commonUI/general/test/services/PopupServiceSpec.js98
-rw-r--r--platform/commonUI/general/test/services/PopupSpec.js74
-rw-r--r--platform/commonUI/general/test/suite.json6
-rw-r--r--platform/commonUI/inspect/bundle.json7
-rw-r--r--platform/commonUI/inspect/src/InfoConstants.js20
-rw-r--r--platform/commonUI/inspect/src/services/InfoService.js76
-rw-r--r--platform/commonUI/inspect/test/services/InfoServiceSpec.js140
-rw-r--r--platform/commonUI/themes/espresso/res/css/theme-espresso.css1559
-rw-r--r--platform/commonUI/themes/espresso/res/sass/_constants.scss39
-rw-r--r--platform/commonUI/themes/espresso/res/sass/_mixins.scss6
-rw-r--r--platform/commonUI/themes/snow/res/css/theme-snow.css1530
-rw-r--r--platform/commonUI/themes/snow/res/sass/_constants.scss37
-rw-r--r--platform/commonUI/themes/snow/res/sass/_mixins.scss1
-rw-r--r--platform/core/bundle.json1
-rw-r--r--platform/core/src/capabilities/MutationCapability.js21
-rw-r--r--platform/core/src/models/PersistedModelProvider.js23
-rw-r--r--platform/core/src/services/Throttle.js28
-rw-r--r--platform/core/test/models/PersistedModelProviderSpec.js65
-rw-r--r--platform/core/test/services/ThrottleSpec.js9
-rw-r--r--platform/entanglement/bundle.json11
-rw-r--r--platform/entanglement/src/actions/GoToOriginalAction.js62
-rw-r--r--platform/entanglement/src/capabilities/LocationCapability.js60
-rw-r--r--platform/entanglement/test/actions/GoToOriginalActionSpec.js95
-rw-r--r--platform/entanglement/test/capabilities/LocationCapabilitySpec.js91
-rw-r--r--platform/entanglement/test/suite.json4
-rw-r--r--platform/features/conductor/README.md9
-rw-r--r--platform/features/conductor/bundle.json46
-rw-r--r--platform/features/conductor/res/templates/time-conductor.html10
-rw-r--r--platform/features/conductor/src/ConductorRepresenter.js201
-rw-r--r--platform/features/conductor/src/ConductorService.js64
-rw-r--r--platform/features/conductor/src/ConductorTelemetryDecorator.js76
-rw-r--r--platform/features/conductor/src/TimeConductor.js103
-rw-r--r--platform/features/conductor/test/ConductorRepresenterSpec.js259
-rw-r--r--platform/features/conductor/test/ConductorServiceSpec.js58
-rw-r--r--platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js160
-rw-r--r--platform/features/conductor/test/TestTimeConductor.js50
-rw-r--r--platform/features/conductor/test/TimeConductorSpec.js78
-rw-r--r--platform/features/conductor/test/suite.json6
-rw-r--r--platform/features/layout/bundle.json5
-rw-r--r--platform/features/layout/res/templates/layout.html2
-rw-r--r--platform/features/layout/src/FixedController.js103
-rw-r--r--platform/features/layout/test/FixedControllerSpec.js46
-rw-r--r--platform/features/pages/res/iframe.html2
-rw-r--r--platform/features/plot/res/templates/plot.html2
-rw-r--r--platform/features/plot/src/PlotController.js35
-rw-r--r--platform/features/plot/src/SubPlot.js15
-rw-r--r--platform/features/plot/src/elements/PlotPanZoomStackGroup.js3
-rw-r--r--platform/features/plot/src/elements/PlotTickGenerator.js3
-rw-r--r--platform/features/plot/src/elements/PlotUpdater.js40
-rw-r--r--platform/features/plot/test/PlotControllerSpec.js30
-rw-r--r--platform/features/plot/test/SubPlotSpec.js9
-rw-r--r--platform/features/scrolling/src/RangeColumn.js2
-rw-r--r--platform/forms/res/templates/controls/color.html2
-rw-r--r--platform/forms/res/templates/controls/menu-button.html2
-rw-r--r--platform/persistence/elastic/bundle.json2
-rw-r--r--platform/persistence/elastic/src/ElasticSearchProvider.js302
-rw-r--r--platform/persistence/elastic/test/ElasticSearchProviderSpec.js216
-rw-r--r--platform/representation/bundle.json8
-rw-r--r--platform/representation/src/MCTRepresentation.js18
-rw-r--r--platform/representation/src/actions/ContextMenuAction.js56
-rw-r--r--platform/representation/test/MCTRepresentationSpec.js2
-rw-r--r--platform/representation/test/actions/ContextMenuActionSpec.js75
-rw-r--r--platform/search/bundle.json13
-rw-r--r--platform/search/res/templates/search.html38
-rw-r--r--platform/search/src/controllers/SearchController.js279
-rw-r--r--platform/search/src/services/GenericSearchProvider.js485
-rw-r--r--platform/search/src/services/GenericSearchWorker.js205
-rw-r--r--platform/search/src/services/SearchAggregator.js289
-rw-r--r--platform/search/test/controllers/SearchControllerSpec.js327
-rw-r--r--platform/search/test/services/GenericSearchProviderSpec.js470
-rw-r--r--platform/search/test/services/GenericSearchWorkerSpec.js297
-rw-r--r--platform/search/test/services/SearchAggregatorSpec.js301
-rw-r--r--platform/telemetry/src/TelemetryAggregator.js24
-rw-r--r--platform/telemetry/src/TelemetryHandle.js26
-rw-r--r--platform/telemetry/src/TelemetrySubscription.js54
-rw-r--r--test-main.js3
141 files changed, 13744 insertions, 3347 deletions
diff --git a/Procfile b/Procfile
index aa6094edf..1e13b4ae0 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1 @@
-web: node app.js --port $PORT --include example/localstorage \ No newline at end of file
+web: node app.js --port $PORT
diff --git a/README.md b/README.md
index 42cd06028..6a412412e 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,21 @@ This build will:
Run as `mvn clean install`.
+### Building Documentation
+
+Open MCT Web's documentation is generated by an
+[npm](https://www.npmjs.com/)-based build:
+
+* `npm install` _(only needs to run once)_
+* `npm run docs`
+
+Documentation will be generated in `target/docs`. Note that diagram
+generation is dependent on having [Cairo](http://cairographics.org/download/)
+installed; see
+[node-canvas](https://github.com/Automattic/node-canvas#installation)'s
+documentation for help with installation.
+
+
# Glossary
Certain terms are used throughout Open MCT Web with consistent meanings
diff --git a/docs/gendocs.js b/docs/gendocs.js
index 2fcda7214..cd61b9a9b 100644
--- a/docs/gendocs.js
+++ b/docs/gendocs.js
@@ -30,7 +30,8 @@
var CONSTANTS = {
DIAGRAM_WIDTH: 800,
DIAGRAM_HEIGHT: 500
- };
+ },
+ TOC_HEAD = "# Table of Contents";
GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined
(function () {
@@ -44,6 +45,7 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
split = require("split"),
stream = require("stream"),
nomnoml = require('nomnoml'),
+ toc = require("markdown-toc"),
Canvas = require('canvas'),
options = require("minimist")(process.argv.slice(2));
@@ -110,6 +112,9 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
done();
};
transform._flush = function (done) {
+ // Prepend table of contents
+ markdown =
+ [ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n");
this.push("<html><body>\n");
this.push(marked(markdown));
this.push("\n</body></html>\n");
@@ -133,8 +138,8 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
customRenderer.link = function (href, title, text) {
// ...but only if they look like relative paths
return (href || "").indexOf(":") === -1 && href[0] !== "/" ?
- renderer.link(href.replace(/\.md/, ".html"), title, text) :
- renderer.link.apply(renderer, arguments);
+ renderer.link(href.replace(/\.md/, ".html"), title, text) :
+ renderer.link.apply(renderer, arguments);
};
return customRenderer;
}
@@ -179,13 +184,17 @@ GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be define
glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) {
files.forEach(function (file) {
var destination = file.replace(options['in'], options.out),
- destPath = path.dirname(destination);
-
+ destPath = path.dirname(destination),
+ streamOptions = {};
+ if (file.match(/png$/)){
+ streamOptions.encoding = 'binary';
+ } else {
+ streamOptions.encoding = 'utf8';
+ }
+
mkdirp(destPath, function (err) {
- fs.createReadStream(file, { encoding: 'utf8' })
- .pipe(fs.createWriteStream(destination, {
- encoding: 'utf8'
- }));
+ fs.createReadStream(file, streamOptions)
+ .pipe(fs.createWriteStream(destination, streamOptions));
});
});
});
diff --git a/docs/src/architecture/Platform.md b/docs/src/architecture/Platform.md
index 80f9e487f..a59a6ebf9 100644
--- a/docs/src/architecture/Platform.md
+++ b/docs/src/architecture/Platform.md
@@ -35,16 +35,26 @@ in __any of these tiers__.
* _DOM_: The rendered HTML document, composed from HTML templates which
have been processed by AngularJS and will be updated by AngularJS
to reflect changes from the presentation layer. User interactions
- are initiated from here and invoke behavior in the presentation layer.
+ are initiated from here and invoke behavior in the presentation layer. HTML 
+ templates are written in Angular’s template syntax; see the [Angular documentation on templates](https://docs.angularjs.org/guide/templates)​. 
+ These describe the page as actually seen by the user. Conceptually, 
+ stylesheets (controlling the look­and­feel of the rendered templates) belong 
+ in this grouping as well. 
* [_Presentation layer_](#presentation-layer): The presentation layer
is responsible for updating (and providing information to update)
the displayed state of the application. The presentation layer consists
primarily of _controllers_ and _directives_. The presentation layer is
concerned with inspecting the information model and preparing it for
display.
-* [_Information model_](#information-model): The information model
- describes the state and behavior of the objects with which the user
- interacts.
+* [_Information model_](#information-model): ​Provides a common (within Open MCT 
+ Web) set of interfaces for dealing with “things” ­ domain objects ­ within the 
+ system. User­facing concerns in a Open MCT Web application are expressed as 
+ domain objects; examples include folders (used to organize other domain 
+ objects), layouts (used to build displays), or telemetry points (used as 
+ handles for streams of remote measurements.) These domain objects expose a 
+ common set of interfaces to allow reusable user interfaces to be built in the 
+ presentation and template tiers; the specifics of these behaviors are then 
+ mapped to interactions with underlying services. 
* [_Service infrastructure_](#service-infrastructure): The service
infrastructure is responsible for providing the underlying general
functionality needed to support the information model. This includes
@@ -52,7 +62,9 @@ in __any of these tiers__.
back-end.
* _Back-end_: The back-end is out of the scope of Open MCT Web, except
for the interfaces which are utilized by adapters participating in the
- service infrastructure.
+ service infrastructure. Includes the underlying persistence stores, telemetry 
+ streams, and so forth which the Open MCT Web client is being used to interact 
+ with.
## Application Start-up
diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md
index c575439d4..7b35dd66c 100644
--- a/docs/src/guide/index.md
+++ b/docs/src/guide/index.md
@@ -1,3 +1,2334 @@
-# Developer Guide
+# Open MCT Web Developer Guide
+Victor Woeltjen
-This is a placeholder for the developer guide.
+[victor.woeltjen@nasa.gov](mailto:victor.woeltjen@nasa.gov)
+
+September 23, 2015
+Document Version 1.1
+
+Date | Version | Summary of Changes | Author
+------------------- | --------- | ----------------------- | ---------------
+April 29, 2015 | 0 | Initial Draft | Victor Woeltjen
+May 12, 2015 | 0.1 | | Victor Woeltjen
+June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen
+October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry
+
+# Introduction
+The purpose of this guide is to familiarize software developers with the Open
+MCT Web platform.
+
+## What is Open MCT Web
+Open MCT Web is a platform for building user interface and display tools,
+developed at the NASA Ames Research Center in collaboration with teams at the
+Jet Propulsion Laboratory. It is written in HTML5, CSS3, and JavaScript, using
+[AngularJS](http://www.angularjs.org) as a framework. Its intended use is to
+create single-page web applications which integrate data and behavior from a
+variety of sources and domains.
+
+Open MCT Web has been developed to support the remote operation of space
+vehicles, so some of its features are specific to that task; however, it is
+flexible enough to be adapted to a variety of other application domains where a
+display tool oriented toward browsing, composing, and visualizing would be
+useful.
+
+Open MCT Web provides:
+
+* A common user interface paradigm which can be applied to a variety of domains
+and tasks. Open MCT Web is more than a widget toolkit - it provides a standard
+tree-on-the-left, view-on-the-right browsing environment which you customize by
+adding new browsable object types, visualizations, and back-end adapters.
+* A plugin framework and an extensible API for introducing new application
+features of a variety of types.
+* A set of general-purpose object types and visualizations, as well as some
+visualizations and infrastructure specific to telemetry display.
+
+## Client-Server Relationship
+Open MCT Web is client software - it runs entirely in the user's web browser. As
+such, it is largely 'server agnostic'; any web server capable of serving files
+from paths is capable of providing Open MCT Web.
+
+While Open MCT Web can be configured to run as a standalone client, this is
+rarely very useful. Instead, it is intended to be used as a display and
+interaction layer for information obtained from a variety of back-end services.
+Doing so requires authoring or utilizing adapter plugins which allow Open MCT
+Web to interact with these services.
+
+Typically, the pattern here is to provide a known interface that Open MCT Web
+can utilize, and implement it such that it interacts with whatever back-end
+provides the relevant information. Examples of back-ends that can be utilized in
+this fashion include databases for the persistence of user-created objects, or
+sources of telemetry data.
+
+See the [Architecture Guide](../architecture/index.md#Overview) for information
+on the client-server relationship.
+
+## Developing with Open MCT Web
+Building applications with Open MCT Web typically means authoring and utilizing
+a set of plugins which provide application-specific details about how Open MCT
+Web should behave.
+
+### Technologies
+
+Open MCT Web sources are written in JavaScript, with a number of configuration
+files written in JSON. Displayable components are written in HTML5 and CSS3.
+Open MCT Web is built using [AngularJS](http://www.angularjs.org) from Google. A
+good understanding of Angular is recommended for developers working with Open
+MCT Web.
+
+### Forking
+Open MCT Web does not currently have a single stand-alone artifact that can be
+used as a library. Instead, the recommended approach for creating a new
+application is to start by forking/branching Open MCT Web, and then adding new
+features from there. Put another way, Open MCT Web's source structure is built
+to serve as a template for specific applications.
+
+Forking in this manner should not require that you edit Open MCT Web's sources.
+The preferred approach is to create a new directory (peer to `index.html`) for
+the new application, then add new bundles (as described in the Framework
+chapter) within that directory.
+
+To initially clone the Open MCT Web repository:
+`git clone <repository URL> <local repo directory> -b open-master`
+
+To create a fork to begin working on a new application using Open MCT Web:
+
+ cd <local repo directory>
+ git checkout open-master
+ git checkout -b <new branch name>
+
+As a convention used internally, applications built using Open MCT Web have
+master branch names with an identifying prefix. For instance, if building an
+application called 'Foo', the last statement above would look like:
+
+ git checkout -b foo-master
+
+This convention is not enforced or understood by Open MCT Web in any way; it is
+mentioned here as a more general recommendation.
+
+# Overview
+
+Open MCT Web is implemented as a framework component which manages a set of
+other components. These components, called _bundles_, act as containers to group
+sets of related functionality; individual units of functionality are expressed
+within these bundles as _extensions_.
+
+Extensions declare dependencies on other extensions (either individually or
+categorically), and the framework provides actual extension instances at
+run-time to satisfy these declared dependency. This dependency injection
+approach allows software components which have been authored separately (e.g. as
+plugins) but to collaborate at run-time.
+
+Open MCT Web's framework layer is implemented on top of AngularJS's [dependency
+injection mechanism](https://docs.angularjs.org/guide/di) and is modelled after
+[OSGi](hhttp://www.osgi.org/) and its [Declarative Services component model](http://wiki.osgi.org/wiki/Declarative_Services).
+In particular, this is where the term _bundle_ comes from.
+
+## Framework Overview
+
+The framework's role in the application is to manage connections between
+bundles. All application-specific behavior is provided by individual bundles, or
+as the result of their collaboration.
+
+The framework is described in more detail in the [Framework Overview](../architecture/Framework.md#Overview) of the
+architecture guide.
+
+### Tiers
+While all bundles in a running Open MCT Web instance are effectively peers, it
+is useful to think of them as a tiered architecture, where each tier adds more
+specificity to the application.
+```nomnoml
+#direction: down
+[Plugins (Features external to OpenMCTWeb) *Bundle]->[<frame>OpenMCTWeb |
+[Application (Plots, layouts, ElasticSearch wrapper) *Bundle]->[Platform (Core API, common UI, infrastructure) *Bundle]
+[Platform (Core API, common UI, infrastructure) *Bundle]->[Framework (RequireJS, AngularJS, bundle loader)]]
+```
+
+* __Framework__ : This tier is responsible for wiring together the set of
+configured components (called _bundles_) together to instantiate the running
+application. It is responsible for mediating between AngularJS (in particular,
+its dependency injection mechanism) and RequireJS (to load scripts at run-time.)
+It additionally interprets bundle definitions (see explanation below, as well as
+further detail in the Framework chapter.) At this tier, we are at our most
+general: We know only that we are a plugin-based application.
+* __Platform__: Components in the Platform tier describe both the general user
+interface and corresponding developer-facing interfaces of Open MCT Web. This
+tier provides the general infrastructure for applications. It is less general
+than the framework tier, insofar as this tier introduces a specific user
+interface paradigm, but it is still non-specific as to what useful features
+will be provided. Although they can be removed or replaced easily, bundles
+provided by the Platform tier generally should not be thought of as optional.
+* __Application__: The application tier consists of components which utilize the
+infrastructure provided by the Platform to provide functionality which will (or
+could) be useful to specific applications built using Open MCT Web. These
+include adapters to specific persistence back-ends (such as ElasticSearch or
+CouchDB) as well as bundles which describe more user-facing features (such as
+_Plot_ views for visualizing time series data, or _Layout_ objects for
+display-building.) Bundles from this tier can be added or removed without
+compromising basic application functionality, with the caveat that at least one
+persistence adapter needs to be present.
+* __Plugins__: Conceptually, this tier is not so different from the application
+tier; it consists of bundles describing new features, back-end adapters, that
+are specific to the application being built on Open MCT Web. It is described as
+a separate tier here because it has one important distinction from the
+application tier: It consists of bundles that are not included with the platform
+(either authored anew for the specific application, or obtained from elsewhere.)
+
+Note that bundles in any tier can go off and consult back-end services. In
+practice, this responsibility is handled at the Application and/or Plugin tiers;
+Open MCT Web is built to be server-agnostic, so any back-end is considered an
+application-specific detail.
+
+## Platform Overview
+
+The "tiered" architecture described in the preceding text describes a way of
+thinking of and categorizing software components of a Open MCT Web application,
+as well as the framework layer's role in mediating between these components.
+Once the framework layer has wired these software components together, however,
+the application's logical architecture emerges.
+
+An overview of the logical architecture of the platform is given in the [Platform Architecture](../architecture/Platform.md#PlatformArchitecture)
+section of the Platform guide
+
+### Web Services
+
+As mentioned in the Introduction, Open MCT Web is a platform single-page
+applications which runs entirely in the browser. Most applications will want to
+additionally interact with server-side resources, to (for example) read
+telemetry data or store user-created objects. This interaction is handled by
+individual bundles using APIs which are supported in browser (such as
+`XMLHttpRequest`, typically wrapped by Angular's `$http`.)
+
+```nomnoml
+#direction: right
+[Web Service #1] <- [Web Browser]
+[Web Service #2] <- [Web Browser]
+[Web Service #3] <- [Web Browser]
+[<package> Web Browser |
+ [<package> Open MCT Web |
+ [Plugin Bundle #1]-->[Core API]
+ [Core API]<--[Plugin Bundle #2]
+ [Platform Bundle #1]-->[Core API]
+ [Platform Bundle #2]-->[Core API]
+ [Platform Bundle #3]-->[Core API]
+ [Core API]<--[Platform Bundle #4]
+ [Core API]<--[Platform Bundle #5]
+ [Core API]<--[Plugin Bundle #3]
+ ]
+ [Open MCT Web] ->[Browser APIs]
+]
+```
+
+This architectural approach ensures a loose coupling between applications built
+using Open MCT Web and the backends which support them.
+
+### Glossary
+
+Certain terms are used throughout Open MCT Web with consistent meanings or
+conventions. Other developer documentation, particularly in-line documentation,
+may presume an understanding of these terms.
+
+* __bundle__: A bundle is a removable, reusable grouping of software elements.
+The application is composed of bundles. Plug-ins are bundles.
+* __capability__: A JavaScript object which exposes dynamic behavior or
+non-persistent state associated with a domain object.
+* __category__: A machine-readable identifier for a group that something may
+belong to.
+* __composition __: In the context of a domain object, this refers to the set of
+other domain objects that compose or are contained by that object. A domain
+object's composition is the set of domain objects that should appear immediately
+ beneath it in a tree hierarchy. A domain object's composition is described in
+its model as an array of identifiers; its composition capability provides a
+means to retrieve the actual domain object instances associated with these
+identifiers asynchronously.
+* __description__: When used as an object property, this refers to the human-
+readable description of a thing; usually a single sentence or short paragraph.
+(Most often used in the context of extensions, domain object models, or other
+similar application-specific objects.)
+* __domain object __: A meaningful object to the user; a distinct thing in the
+work support by Open MCT Web. Anything that appears in the left-hand tree is a
+domain object.
+* __extension __: An extension is a unit of functionality exposed to the platform
+in a declarative fashion by a bundle. The term 'extension category' is used to
+distinguish types of extensions from specific extension instances.
+* __id__: A string which uniquely identifies a domain object.
+* __key__: When used as an object property, this refers to the machine-readable
+identifier for a specific thing in a set of things. (Most often used in the
+context of extensions or other similar application-specific object sets.) This
+term is chosen to avoid attaching ambiguous meanings to 'id'.
+* __model__: The persistent state associated with a domain object. A domain
+object's model is a JavaScript object which can be converted to JSON without
+losing information (that is, it contains no methods.)
+* __name__: When used as an object property, this refers to the human-readable
+name for a thing. (Most often used in the context of extensions, domain object
+models, or other similar application-specific objects.)
+* __navigation__: Refers to the current state of the application with respect to
+the user's expressed interest in a specific domain object; e.g. when a user
+clicks on a domain object in the tree, they are navigating to it, and it is
+thereafter considered the navigated object (until the user makes another such
+choice.) This term is used to distinguish navigation from selection, which
+occurs in an editing context.
+* __space__: A machine-readable name used to identify a persistence store.
+Interactions with persistence with generally involve a space parameter in some
+form, to distinguish multiple persistence stores from one another (for cases
+where there are multiple valid persistence locations available.)
+* __source__: A machine-readable name used to identify a source of telemetry
+data. Similar to "space", this allows multiple telemetry sources to operate
+side-by-side without conflicting.
+
+# Framework
+
+Open MCT Web is built on the [AngularJS framework]( http://www.angularjs.org ). A
+good understanding of that framework is recommended.
+
+Open MCT Web adds an extra layer on top of AngularJS to (a) generalize its
+dependency injection mechanism slightly, particularly to handle many-to-one
+relationships; and (b) handle script loading. Combined, these features become a
+plugin mechanism.
+
+This framework layer operates on two key concepts:
+
+* __Bundle:__ A bundle is a collection of related functionality that can be
+added to the application as a group. More concretely, a bundle is a directory
+containing a JSON file declaring its contents, as well as JavaScript sources,
+HTML templates, and other resources used to support that functionality. (The
+term bundle is borrowed from [OSGi](http://www.osgi.org/) - which has also
+inspired many of the concepts used in the framework layer. A familiarity with
+OSGi, particularly Declarative Services, may be useful when working with Open
+MCT Web.)
+* __Extension:__ An extension is an individual unit of functionality. Extensions
+are collected together in bundles, and may interact with other extensions.
+
+The framework layer, loaded and initiated from `index.html`, is the main point
+of entry for an application built on Open MCT Web. It is responsible for wiring
+together the application at run time (much of this responsibility is actually
+delegated to Angular); at a high-level, the framework does this by proceeding
+through four stages:
+
+1. __Loading definitions:__ JSON declarations are loaded for all bundles which
+will constitute the application, and wrapped in a useful API for subsequent
+stages.
+2. __Resolving extensions:__ Any scripts which provide implementations for
+extensions exposed by bundles are loaded, using Require.
+3. __Registering extensions__ Resolved extensions are registered with Angular,
+such that they can be used by the application at run-time. This stage includes
+both registration of Angular built-ins (directives, controllers, routes,
+constants, and services) as well as registration of non-Angular extensions.
+4. __Bootstrapping__ The Angular application is bootstrapped; at that point,
+Angular takes over and populates the body of the page using the extensions that
+have been registered.
+
+## Bundles
+
+The basic configurable unit of Open MCT Web is the _bundle_. This term has been
+used a bit already; now we'll get to a more formal definition.
+
+A bundle is a directory which contains:
+
+* A bundle definition; a file named `bundle.json`.
+* Subdirectories for sources, resources, and tests.
+* Optionally, a `README.md` Markdown file describing its contents (this is not
+used by Open MCT Web in any way, but it's a helpful convention to follow.)
+
+The bundle definition is the main point of entry for the bundle. The framework
+looks at this to determine which components need to be loaded and how they
+interact.
+
+A plugin in Open MCT Web is a bundle. The platform itself is also decomposed
+into bundles, each of which provides some category of functionality. The
+difference between a _bundle_ and a _plugin_ is purely a matter of the intended
+use; a plugin is just a bundle that is meant to be easily added or removed. When
+developing, it is typically more useful to think in terms of bundles.
+
+### Configuring Active Bundles
+
+To decide which bundles should be loaded, the framework loads a file named
+`bundles.json` (peer to the `index.html` file which serves the application) to
+determine which bundles should be loaded. This file should contain a single JSON
+array of strings, where each is the path to a bundle. These paths should not
+include bundle.json (this is implicit) or a trailing slash.
+
+For instance, if `bundles.json` contained:
+
+ [
+ "example/builtins",
+ "example/extensions"
+ ]
+
+...then the Open MCT Web framework would look for bundle definitions at
+`example/builtins/bundle.json` and `example/extensions/bundle.json`, relative
+to the path of `index.html`. No other bundles would be loaded.
+
+### Bundle Definition
+
+A bundle definition (the `bundle.json` file located within a bundle) contains a
+description of the bundle itself, as well as the information exposed by the
+bundle.
+
+This definition is expressed as a single JSON object with the following
+properties (all of which are optional, falling back to reasonable defaults):
+
+* `key`: A machine-readable name for the bundle. (Currently used only in
+logging.)
+* `name`: A human-readable name for the bundle. (Also only used in logging.)
+* `sources`: Names a directory in which source scripts (which will implement
+extensions) are located. Defaults to 'src'
+* `resources`: Names a directory in which resource files (such as HTML templates,
+images, CS files, and other non-JavaScript files needed by this bundle) are
+located. Defaults to 'res'
+* `libraries`: Names a directory in which third-party libraries are located.
+Defaults to 'lib'
+* `configuration`: A bundle's configuration object, which should be formatted as
+would be passed to require.config (see [RequireJS documentation](http://requirejs.org/docs/api.html ) );
+note that only paths and shim have been tested.
+* `extensions`: An object containing key-value pairs, where keys are extension
+categories, and values are extension definitions. See the section on Extensions
+for more information.
+
+For example, the bundle definition for example/policy looks like:
+
+ {
+ "name": "Example Policy",
+ "description": "Provides an example of using policies.",
+ "sources": "src",
+ "extensions": {
+ "policies": [
+ {
+ "implementation": "ExamplePolicy.js",
+ "category": "action"
+ }
+ ]
+ }
+ }
+
+### Bundle Directory Structure
+
+In addition to the directories defined in the bundle definition, a bundle will
+typically contain other directories not used at run-time. Additionally, some
+useful development scripts (such as the command line build and the test suite)
+expect this directory structure to be in use, and may ignore options chosen by
+`b undle.json`. It is recommended that the directory structure described below be
+used for new bundles.
+
+* `src`: Contains JavaScript sources for this bundle. May contain additional
+subdirectories to organize these sources; typically, these subdirectories are
+named to correspond to the extension categories they contain and/or support, but
+this is only a convention.
+* `res`: Contains other files needed by this bundle, such as HTML templates. May
+contain additional subdirectories to organize these sources.
+* `lib`: Contains JavaScript sources from third-party libraries. These are
+separated from bundle sources in order to ignore them during code style checking
+from the command line build.
+* `test`: Contains JavaScript sources implementing [Jasmine](http://jasmine.github.io/)
+tests, as well as a file named `suite.json` describing which files to test.
+Should have the same folder structure as the `src` directory; see the section on
+automated testing for more information.
+
+For example, the directory structure for bundle `platform/commonUI/about` looks
+like:
+
+ Platform
+ |
+ |-commonUI
+ |
+ +-about
+ |
+ |-res
+ |
+ |-src
+ |
+ |-test
+ |
+ |-bundle.json
+ |
+ +-README.md
+
+## Extensions
+
+While bundles provide groupings of related behaviors, the individual units of
+behavior are called extensions.
+
+Extensions belong to categories; an extension category is the machine-readable
+identifier used to identify groups of extensions. In the `extensions` property
+of a bundle definition, the keys are extension categories and the values are
+arrays of extension definitions.
+
+### General Extensions
+
+Extensions are intended as a general-purpose mechanism for adding new types of
+functionality to Open MCT Web.
+
+An extension category is registered with Angular under the name of the
+extension, plus a suffix of two square brackets; so, an Angular service (or,
+generally, any other extension) can access the full set of registered
+extensions, from all bundles, by including this string (e.g. `types[]` to get
+all type definitions) in a dependency declaration.
+
+As a convention, extension categories are given single-word, plural nouns for
+names within Open MCT Web (e.g. `types`.) This convention is not enforced by the
+platform in any way. For extension categories introduced by external plugins, it
+is recommended to prefix the extension category with a vendor identifier (or
+similar) followed by a dot, to avoid collisions.
+
+### Extension Definitions
+
+The properties used in extension definitions are typically unique to each
+category of extension; a few properties have standard interpretations by the
+platform.
+
+* `implementation`: Identifies a JavaScript source file (in the sources
+folder) which implements this extension. This JavaScript file is expected to
+contain an AMD module (see http://requirejs.org/docs/whyamd.html#amd ) which
+gives as its result a single constructor function.
+* `depends`: An array of dependencies needed by this extension; these will be
+passed on to Angular's [dependency injector](https://docs.angularjs.org/guide/di ) .
+By default, this is treated as an empty array. Note that depends does not make
+sense without `implementation` (since these dependencies will be passed to the
+implementation when it is instantiated.)
+* `priority`: A number or string indicating the priority order (see below) of
+this extension instance. Before an extension category is registered with
+AngularJS, the extensions of this category from all bundles will be concatenated
+into a single array, and then sorted by priority.
+
+Extensions do not need to have an implementation. If no implementation is
+provided, consumers of the extension category will receive the extension
+definition as a plain JavaScript object. Otherwise, they will receive the
+partialized (see below) constructor for that implementation, which will
+additionally have all properties from the extension definition attached.
+
+#### Partial Construction
+
+In general, extensions are intended to be implemented as constructor functions,
+which will be used elsewhere to instantiate new objects of that type. However,
+the Angular-supported method for dependency injection is (effectively)
+constructor-style injection; so, both declared dependencies and run-time
+arguments are competing for space in a constructor's arguments.
+
+To resolve this, the Open MCT Web framework registers extension instances in a
+partially constructed form. That is, the constructor exposed by the extension's
+implementation is effectively decomposed into two calls; the first takes the
+dependencies, and returns the constructor in its second form, which takes the
+remaining arguments.
+
+This means that, when writing implementations, the constructor function should
+be written to include all declared dependencies, followed by all run-time
+arguments. When using extensions, only the run-time arguments need to be
+provided.
+
+#### Priority
+
+Within each extension category, registration occurs in priority order. An
+extension's priority may be specified as a `priority` property in its extension
+definition; this may be a number, or a symbolic string. Extensions are
+registered in reverse order (highest-priority first), and symbolic strings are
+mapped to the numeric values as follows:
+
+* `fallback`: Negative infinity. Used for extensions that are not intended for
+use (that is, they are meant to be overridden) but are present as an option of
+last resort.
+* `default`: `-100`. Used for extensions that are expected to be overridden, but
+need a useful default.
+* `none`: `0`. Also used if no priority is specified, or if an unknown or
+malformed priority is specified.
+* `optional`: `100`. Used for extensions that are meant to be used, but may be
+overridden.
+* `preferred`: `1000`. Used for extensions that are specifically intended to be
+used, but still may be overridden in principle.
+* `mandatory`: Positive infinity. Used when an extension should definitely not
+be overridden.
+
+These symbolic names are chosen to support usage where many extensions may
+satisfy a given need, but only one may be used; in this case, as a convention it
+should be the lowest-ordered (highest-priority) extensions available. In other
+cases, a full set (or multi-element subset) of extensions may be desired, with a
+specific ordering; in these cases, it is preferable to specify priority
+numerically when declaring extensions, and to understand that extensions will be
+sorted according to these conventions when using them.
+
+### Angular Built-ins
+
+Several entities supported Angular are expressed and managed as extensions in
+Open MCT Web. Specifically, these extension categories are _directives_,
+_controllers_, _services_, _constants_, _runs_, and _routes_.
+
+#### Angular Directives
+
+New [directives]( https://docs.angularjs.org/guide/directive ) may be
+registered as extensions of the directives category. Implementations of
+directives in this category should take only dependencies as arguments, and
+should return a directive definition object.
+
+The directive's name should be provided as a key property of its extension
+definition, in camel-case format.
+
+#### Angular Controllers
+
+New [controllers]( https://docs.angularjs.org/guide/controller ) may be registered
+as extensions of the controllers category. The implementation is registered
+directly as the controller; its only constructor arguments are its declared
+dependencies.
+
+The directive's identifier should be provided as a key property of its extension
+definition.
+
+
+#### Angular Services
+
+New [services](https://docs.angularjs.org/guide/services ) may be registered as
+extensions of the services category. The implementation is registered via a
+[service call]( https://docs.angularjs.org/api/auto/service/$provide#service ), so
+it will be instantiated with the new operator.
+
+#### Angular Constants
+
+Constant values may be registered as extensions of the [ constants category](https://docs.angularjs.org/api/ng/type/angular.Module#constant ).
+These extensions have no implementation; instead, they should contain a property
+ key , which is the name under which the constant will be registered, and a
+property value , which is the constant value that will be registered.
+
+#### Angular Runs
+
+In some cases, you want to register code to run as soon as the application
+starts; these can be registered as extensions of the [ runs category](https://docs.angularjs.org/api/ng/type/angular.Module#run ).
+Implementations registered in this category will be invoked (with their declared
+dependencies) when the Open MCT Web application first starts. (Note that, in
+this case, the implementation is better thought of as just a function, as
+opposed to a constructor function.)
+
+#### Angular Routes
+
+Extensions of category `routes` will be registered with Angular's [route provider](https://docs.angularjs.org/api/ngRoute/provider/$routeProvider ).
+Extensions of this category have no implementations, and need only two
+properties in their definition:
+
+* `when`: The value that will be passed as the path argument to `$routeProvider.when`;
+specifically, the string that will appear in the trailing
+part of the URL corresponding to this route. This property may be omitted, in
+which case this extension instance will be treated as the default route.
+* `templateUrl`: A path to the template to render for this route. Specified as a
+path relative to the bundle's resource directory (`res` by default.)
+
+### Composite Services
+
+Composite services are described in the [relevant section](../architecture/Framework.md#Composite-Services)
+of the framework guide.
+
+A component should include the following properties in its extension definition:
+
+* `provides`: The symbolic identifier for the service that will be composed. The
+ fully-composed service will be registered with Angular under this name.
+* `type`: One of `provider`, `aggregator` or `decorator` (as above)
+
+In addition to any declared dependencies, _aggregators_ and _decorators_ both
+receive one more argument (immediately following declared dependencies) that is
+provided by the framework. For an aggregator, this will be an array of all
+providers of the same service (that is, with matching `provides` properties);
+for a decorator, this will be whichever provider, decorator, or aggregator is
+next in the sequence of decorators.
+
+Services exposed by the Open MCT Web platform are often declared as composite
+services, as this form is open for a variety of common modifications.
+
+# Core API
+
+Most of Open MCT Web's relevant API is provided and/or mediated by the
+framework; that is, much of developing for Open MCT Web is a matter of adding
+extensions which access other parts of the platform by means of dependency
+injection.
+
+The core bundle (`platform/core`) introduces a few additional object types meant
+to be passed along by other services.
+
+## Domain Objects
+
+Domain objects are the most fundamental component of Open MCT Web's information
+model. A domain object is some distinct thing relevant to a user's work flow,
+such as a telemetry channel, display, or similar. Open MCT Web is a tool for
+viewing, browsing, manipulating, and otherwise interacting with a graph of
+domain objects.
+
+A domain object should be conceived of as the union of the following:
+
+* __Identifier__: A machine-readable string that uniquely identifies the domain
+object within this application instance.
+* __Model__: The persistent state of the domain object. A domain object's model
+is a JavaScript object that can be losslessly converted to JSON.
+* __Capabilities__: Dynamic behavior associated with the domain object.
+Capabilities are JavaScript objects which provide additional methods for
+interacting with the domain objects which expose those capabilities. Not all
+domain objects expose all capabilities.
+
+At run-time, a domain object has the following interface:
+
+* `getId()`: Get the identifier for this domain object.
+* `getModel()`: Get the plain state associated with this domain object. This
+will return a JavaScript object that can be losslessly converted to JSON. Note
+that the model returned here can be modified directly but should not be;
+instead, use the mutation capability.
+* `getCapability(key)`: Get the specified capability associated with this domain
+object. This will return a JavaScript object whose interface is specific to the
+type of capability being requested. If the requested capability is not exposed
+by this domain object, this will return undefined .
+* `hasCapability(key)`: Shorthand for checking if a domain object exposes the
+requested capability.
+* `useCapability(key, arguments )`: Shorthand for
+`getCapability(key).invoke(arguments)`, with additional checking between calls.
+If the provided capability has no invoke method, the return value here functions
+as `getCapability` including returning `undefined` if the capability is not
+exposed.
+
+## Domain Object Actions
+
+An `Action` is behavior that can be performed upon/using a `DomainObject`. An
+Action has the following interface:
+
+* `perform()`: Do this action. For example, if one had an instance of a
+`RemoveAction` invoking its perform method would cause the domain object which
+exposed it to be removed from its container.
+* `getMetadata()`: Get metadata associated with this action. Returns an object
+containing:
+ * `name`: Human-readable name.
+ * `description`: Human-readable summary of this action.
+ * `glyph`: Single character to be displayed in Open MCT Web's icon font set.
+ * `context`: The context in which this action is being performed (see below)
+
+Action instances are typically obtained via a domain object's `action`
+capability.
+
+### Action Contexts
+
+An action context is a JavaScript object with the following properties:
+
+* `domainObject`: The domain object being acted upon.
+* `selectedObject`: Optional; the selection at the time of action (e.g. the
+dragged object in a drag-and-drop operation.)
+
+## Telemetry
+
+Telemetry series data in Open MCT Web is represented by a common interface, and
+packaged in a consistent manner to facilitate passing telemetry updates around
+multiple visualizations.
+
+### Telemetry Requests
+
+A telemetry request is a JavaScript object containing the following properties:
+
+* `source`: A machine-readable identifier for the source of this telemetry. This
+is useful when multiple distinct data sources are in use side-by-side.
+* `key`: A machine-readable identifier for a unique series of telemetry within
+that source.
+* _Note: This API is still under development; additional properties, such as
+start and end time, should be present in future versions of Open MCT Web._
+
+Additional properties may be included in telemetry requests which have specific
+interpretations for specific sources.
+
+### Telemetry Responses
+
+When returned from the `telemetryService` (see [Services](#Services) section),
+telemetry series data will be packaged in a `source -> key -> TelemetrySeries`
+fashion. That is, telemetry is passed in an object containing key-value pairs.
+Keys identify telemetry sources; values are objects containing additional
+key-value pairs. In this object, keys identify individual telemetry series (and
+match they `key` property from corresponding requests) and values are
+`TelemetrySeries` objects (see below.)
+
+### Telemetry Series
+
+A telemetry series is a specific sequence of data, typically associated with a
+specific instrument. Telemetry is modeled as an ordered sequence of domain and
+range values, where domain values must be non-decreasing but range values do
+not. (Typically, domain values are interpreted as UTC timestamps in milliseconds
+relative to the UNIX epoch.) A series must have at least one domain and one
+range, and may have more than one.
+
+Telemetry series data in Open MCT Web is expressed via the following
+`TelemetrySeries` interface:
+
+* `getPointCount()`: Returns the number of unique points/samples in this series.
+* `getDomainValue(index, [domain])`: Get the domain value at the specified index .
+If a second domain argument is provided, this is taken as a string identifier
+indicating which domain option (of, presumably, multiple) should be returned.
+* `getRangeValue(index, [range])`: Get the domain value at the specified index .
+If a second range argument is provided, this is taken as a string identifier
+indicating which range option (of, presumably, multiple) should be returned.
+
+### Telemetry Metadata
+
+Domain objects which have associated telemetry also expose metadata about that
+telemetry; this is retrievable via the `getMetadata()` of the telemetry
+capability. This will return a single JavaScript object containing the following
+properties:
+
+* `source`: The machine-readable identifier for the source of telemetry data for
+this object.
+* `key`: The machine-readable identifier for the individual telemetry series.
+* `domains`: An array of supported domains (see TelemetrySeries above.) Each
+domain should be expressed as an object which includes:
+ * `key`: Machine-readable identifier for this domain, as will be passed into
+ a getDomainValue(index, domain) call.
+ * `name`: Human-readable name for this domain.
+* `ranges`: An array of supported ranges; same format as domains .
+
+Note that this metadata is also used as the prototype for telemetry requests
+made using this capability.
+
+## Types
+A domain object's type is represented as a Type object, which has the following
+interface:
+
+* `getKey()`: Get the machine-readable identifier for this type.
+* `getName()`: Get the human-readable name for this type.
+* `getDescription()`: Get a human-readable summary of this type.
+* `getGlyph()`: Get the single character to be rendered as an icon for this type
+in Open MCT Web's custom font set.
+* `getInitialModel()`: Get a domain object model that represents the initial
+state (before user specification of properties) for domain objects of this type.
+* `getDefinition()`: Get the extension definition for this type, as a JavaScript
+object.
+* `instanceOf(type)`: Check if this type is (or inherits from) a specified type .
+This type can be either a string, in which case it is taken to be that type's
+ key , or it may be a `Type` instance.
+* `hasFeature(feature)`: Returns a boolean value indicating whether or not this
+type supports the specified feature, which is a symbolic string.
+* `getProperties()`: Get all properties associated with this type, expressed as
+an array of `TypeProperty` instances.
+
+### Type Features
+
+Features of a domain object type are expressed as symbolic string identifiers.
+They are defined in practice by usage; currently, the Open MCT Web platform only
+uses the creation feature to determine which domain object types should appear
+in the Create menu.
+
+### Type Properties
+
+Types declare the user-editable properties of their domain object instances in
+order to allow the forms which appear in the __Create__ and __Edit Properties__
+dialogs to be generated by the platform. A `TypeProperty` has the following interface:
+
+* `getValue(model)`: Get the current value for this property, as it appears in
+the provided domain object model.
+* `setValue(model, value)`: Set a new value for this property in the provided
+domain object model .
+* `getDefinition()`: Get the raw definition for this property as a JavaScript
+object (as it was declared in this type's extension definition.)
+
+# Extension Categories
+
+The information in this section is focused on registering new extensions of
+specific types; it does not contain a catalog of the extension instances of
+these categories provided by the platform. Relevant summaries there are provided
+in subsequent sections.
+
+## Actions Category
+
+An action is a thing that can be done to or using a domain object, typically as
+initiated by the user.
+
+An action's implementation:
+
+* Should take a single `context` argument in its constructor. (See Action
+Contexts, under Core API.)
+* Should provide a method `perform` which causes the behavior associated with
+the action to occur.
+* May provide a method `getMetadata` which provides metadata associated with
+the action. If omitted, one will be provided by the platform which includes
+metadata from the action's extension definition.
+* May provide a static method `appliesTo(context)` (that is, a function
+available as a property of the implementation's constructor itself), which will
+be used by the platform to filter out actions from contexts in which they are
+inherently inapplicable.
+
+An action's bundle definition (and/or `getMetadata()` return value) may include:
+
+* `category`: A string or array of strings identifying which category or
+categories an action falls into; used to determine when an action is displayed.
+Categories supported by the platform include:
+ * `contextual`: Actions in a context menu.
+ * `view-control`: Actions triggered by buttons in the top-right of Browse
+ view.
+* `key`: A machine-readable identifier for this action.
+* `name`: A human-readable name for this action (e.g. to show in a menu)
+* `description`: A human-readable summary of the behavior of this action.
+* `glyph`: A single character which will be rendered in Open MCT Web's custom
+font set as an icon for this action.
+
+## Capabilities Category
+
+Capabilities are exposed by domain objects (e.g. via the `getCapability` method)
+but most commonly originate as extensions of this category.
+
+Extension definitions for capabilities should include both an implementation,
+and a property named key whose value should be a string used as a
+machine-readable identifier for that capability, e.g. when passed as the
+argument to a domain object's `getCapability(key)` call.
+
+A capability's implementation should have methods specific to that capability;
+that is, there is no common format for capability implementations, aside from
+support for invocation via the `useCapability` shorthand.
+
+A capability's implementation will take a single argument (in addition to any
+declared dependencies), which is the domain object that will expose that
+capability.
+
+A capability's implementation may also expose a static method `appliesTo(model)`
+which should return a boolean value, and will be used by the platform to filter
+down capabilities to those which should be exposed by specific domain objects,
+based on their domain object models.
+
+## Controls Category
+
+Controls provide options for the `mct-control` directive.
+
+Six standard control types are included in the forms bundle:
+
+* `textfield`: An area to enter plain text.
+* `select`: A drop-down list of options.
+* `checkbox`: A box which may be checked/unchecked.
+* `color`: A color picker.
+* `button`: A button.
+* `datetime`: An input for UTC date/time entry; gives result as a UNIX
+timestamp, in milliseconds since start of 1970, UTC.
+
+New controls may be added as extensions of the controls category. Extensions of
+this category have two properties:
+
+* `key`: The symbolic name for this control (matched against the control field
+in rows of the form structure).
+* `templateUrl`: The URL to the control's Angular template, relative to the
+resources directory of the bundle which exposes the extension.
+
+Within the template for a control, the following variables will be included in
+scope:
+
+* `ngModel`: The model where form input will be stored. Notably we also need to
+look at field (see below) to determine which field in the model should be
+modified.
+* `ngRequired`: True if input is required.
+* `ngPattern`: The pattern to match against (for text entry)
+* `options`: The options for this control, as passed from the `options` property
+of an individual row definition.
+* `field`: Name of the field in `ngModel` which will hold the value for this
+control.
+
+## Gestures Category
+
+A _gesture_ is a user action which can be taken upon a representation of a
+domain object.
+
+Examples of gestures included in the platform are:
+
+* `drag`: For representations that can be used to initiate drag-and-drop
+composition.
+* `drop`: For representations that can be drop targets for drag-and-drop
+composition.
+* `menu`: For representations that can be used to pop up a context menu.
+
+Gesture definitions have a property `key` which is used as a machine-readable
+identifier for the gesture (e.g. `drag`, `drop`, `menu` above.)
+
+A gesture's implementation is instantiated once per representation that uses the
+gesture. This class will receive the jqLite-wrapped `mct-representation` element
+and the domain object being represented as arguments, and should do any
+necessary "wiring" (e.g. listening for events) during its constructor call. The
+gesture's implementation may also expose an optional `destroy()` method which
+will be called when the gesture should be removed, to avoid memory leaks by way
+of unremoved listeners.
+
+## Indicators Category
+
+An indicator is an element that should appear in the status area at the bottom
+of a running Open MCT Web client instance.
+
+### Standard Indicators
+
+Indicators which wish to appear in the common form of an icon-text pair should
+provide implementations with the following methods:
+
+* `getText()`: Provides the human-readable text that will be displayed for this
+indicator.
+* `getGlyph()`: Provides a single-character string that will be displayed as an
+icon in Open MCT Web's custom font set.
+* `getDescription()`: Provides a human-readable summary of the current state of
+this indicator; will be displayed in a tooltip on hover.
+* `getClass()`: Get a CSS class that will be applied to this indicator.
+* `getTextClass()`: Get a CSS class that will be applied to this indicator's
+text portion.
+* `getGlyphClass()`: Get a CSS class that will be applied to this indicator's
+icon portion.
+* `configure()`: If present, a configuration icon will appear to the right of
+this indicator, and clicking it will invoke this method.
+
+Note that all methods are optional, and are called directly from an Angular
+template, so they should be appropriate to run during digest cycles.
+
+### Custom Indicators
+
+Indicators which wish to have an arbitrary appearance (instead of following the
+icon-text convention commonly used) may specify a `template` property in their
+extension definition. The value of this property will be used as the `key` for
+an `mct-include` directive (so should refer to an extension of category
+ templates .) This template will be rendered to the status area. Indicators of
+this variety do not need to provide an implementation.
+
+## Licenses Category
+
+The extension category `licenses` can be used to add entries into the 'Licensing
+information' page, reachable from Open MCT Web's About dialog.
+
+Licenses may have the following properties, all of which are strings:
+
+* `name`: Human-readable name of the licensed component. (e.g. 'AngularJS'.)
+* `version`: Human-readable version of the licensed component. (e.g. '1.2.26'.)
+* `description`: Human-readable summary of the component.
+* `author`: Name or names of entities to which authorship should be attributed.
+* `copyright`: Copyright text to display for this component.
+* `link`: URL to full license text.
+
+## Policies Category
+
+Policies are used to handle decisions made using Open MCT Web's `policyService`;
+examples of these decisions are determining the applicability of certain
+actions, or checking whether or not a domain object of one type can contain a
+domain object of a different type. See the section on the Policies for an
+overview of Open MCT Web's policy model.
+
+A policy's extension definition should include:
+
+* `category`: The machine-readable identifier for the type of policy decision
+being supported here. For a list of categories supported by the platform, see
+the section on Policies. Plugins may introduce and utilize additional policy
+categories not in that list.
+* `message`: Optional; a human-readable message describing the policy, intended
+for display in situations where this specific policy has disallowed something.
+
+A policy's implementation should include a single method, `allow(candidate,
+context)`. The specific types used for `candidate` and `context` vary by policy
+category; in general, what is being asked is 'is this candidate allowed in this
+context?' This method should return a boolean value.
+
+Open MCT Web's policy model requires consensus; a policy decision is allowed
+when and only when all policies choose to allow it. As such, policies should
+generally be written to reject a certain case, and allow (by returning `true`)
+anything else.
+
+## Representations Category
+
+A representation is an Angular template used to display a domain object. The
+`representations` extension category is used to add options for the
+`mct-representation` directive.
+
+A representation definition should include the following properties:
+
+* `key`: The machine-readable name which identifies the representation.
+* `templateUrl`: The path to the representation's Angular template. This path is
+relative to the bundle's resources directory.
+* `uses`: Optional; an array of capability names. Indicates that this
+representation intends to use those capabilities of a domain object (via a
+`useCapability` call), and expects to find the latest results of that
+`useCapability` call in the scope of the presented template (under the same name
+as the capability itself.) Note that, if `useCapability` returns a promise, this
+will be resolved before being placed in the representation's scope.
+* `gestures`: An array of keys identifying gestures (see the `gestures`
+extension category) which should be available upon this representation. Examples
+of gestures include `drag` (for representations that should act as draggable
+sources for drag-drop operations) and `menu` (for representations which should
+show a domain-object-specific context menu on right-click.)
+
+### Representation Scope
+
+While _representations_ do not have implementations, per se, they do refer to
+Angular templates which need to interact with information (e.g. the domain
+object being represented) provided by the platform. This information is passed
+in through the template's scope, such that simple representations may be created
+by providing only templates. (More complex representations will need controllers
+which are referenced from templates. See [https://docs.angularjs.org/guide/controller ]()
+for more information on controllers in Angular.)
+
+A representation's scope will contain:
+
+* `domainObject`: The represented domain object.
+* `model`: The domain object's model.
+* `configuration`: An object containing configuration information for this
+representation (an empty object if there is no saved configuration.) The
+contents of this object are managed entirely by the view/representation which
+receives it.
+* `representation`: An empty object, useful as a 'scratch pad' for
+representation state.
+* `ngModel`: An object passed through the ng-model attribute of the
+`mct-representation` , if any.
+* `parameters`: An object passed through the parameters attribute of the
+`mct-representation`, if any.
+* Any capabilities requested by the uses property of the representation
+definition.
+
+## Representers Category
+
+The `representers` extension category is used to add additional behavior to the
+`mct-representation` directive. This extension category is intended primarily
+for use internal to the platform.
+
+Unlike _representations_, which describe specific ways to represent domain
+objects, _representers_ are used to modify or augment the process of
+representing domain objects in general. For example, support for the _gestures_
+extension category is added by a _representer_.
+
+A representer needs only provide an implementation. When an `mct-representation`
+is linked (see [https://docs.angularjs.org/guide/directive ]() or when the
+domain object being represented changes, a new _representer_ of each declared
+type is instantiated. The constructor arguments for a _representer_ are the same
+as the arguments to the link function in an Angular directive: `scope` the
+Angular scope for this representation; `element` the jqLite-wrapped
+`mct-representation` element, and `attrs` a set of key-value pairs of that
+element's attributes. _Representers_ may wish to populate the scope, attach
+event listeners to the element, etc.
+
+This implementation must provide a single method, `destroy()`, which will be
+invoked when the representer is no longer needed.
+
+## Roots Category
+
+The extension category `roots` is used to provide root-level domain object
+models. Root-level domain objects appear at the top-level of the tree hierarchy.
+For example, the _My Items_ folder is added as an extension of this category.
+
+Extensions of this category should have the following properties:
+
+* `id`: The machine-readable identifier for the domaiwn object being exposed.
+* `model`: The model, as a JSON object, for the domain object being exposed.
+
+## Stylesheets Category
+
+The stylesheets extension category is used to add CSS files to style the
+application. Extension definitions for this category should include one
+property:
+
+* `stylesheetUrl`: Path and filename, including extension, for the stylesheet to
+include. This path is relative to the bundle's resources folder (by default,
+`res`)
+
+To control the order of CSS files, use priority (see the section on Extension
+Definitions above.)
+
+## Templates Category
+
+The `templates` extension category is used to expose Angular templates under
+symbolic identifiers. These can then be utilized using the `mct-include`
+directive, which behaves similarly to `ng-include` except that it uses these
+symbolic identifiers instead of paths.
+
+A template's extension definition should include the following properties:
+
+* `key`: The machine-readable name which identifies this template, matched
+against the value given to the key attribute of the `mct-include` directive.
+* `templateUrl`: The path to the relevant Angular template. This path is
+relative to the bundle's resources directory.
+
+Note that, when multiple templates are present with the same key , the one with
+the highest priority will be used from `mct-include`. This behavior can be used
+to override templates exposed by the platform (to change the logo which appears
+in the bottom right, for instance.)
+
+Templates do not have implementations.
+
+## Types Category
+
+The types extension category describes types of domain objects which may
+appear within Open MCT Web.
+
+A type's extension definition should have the following properties:
+
+* `key`: The machine-readable identifier for this domain object type. Will be
+stored to and matched against the type property of domain object models.
+* `name`: The human-readable name for this domain object type.
+* `description`: A human-readable summary of this domain object type.
+* `glyph`: A single character to be rendered as an icon in Open MCT Web's custom
+font set.
+* `model`: A domain object model, used as the initial state for created domain
+objects of this type (before any properties are specified.)
+* `features`: Optional; an array of strings describing features of this domain
+object type. Currently, only creation is recognized by the platform; this is
+used to determine that this type should appear in the Create menu. More
+generally, this is used to support the `hasFeature(...)` method of the type
+capability.
+* `properties`: An array describing individual properties of this domain object
+(as should appear in the _Create_ or the _Edit Properties_ dialog.) Each
+property is described by an object containing the following properties:
+ * `control`: The key of the control (see `mct-control` and the `controls`
+ [extension category](#Controls)) to use for editing this property.
+ * `property`: A string which will be used as the name of the property in the
+ domain object's model that the value for this property should be stored
+ under. If this value should be stored in an object nested within the domain
+ object model, then property should be specified as an array of strings
+ identifying these nested objects and, finally, the property itself.
+ * other properties as appropriate for a control of this type (each
+ property's definition will also be passed in as the structure for its
+ control.) See documentation of mct-form for more detail on these
+ properties.
+
+Types do not have implementations.
+
+## Versions Category
+The versions extension category is used to introduce line items in Open MCT
+Web's About dialog. These should have the following properties:
+
+* `name`: The name of this line item, as should appear in the left-hand side of
+the list of version information in the About dialog.
+* `value`: The value which should appear to the right of the name in the About
+dialog.
+
+To control the ordering of line items within the About dialog, use `priority`.
+(See section on [Extension Definitions](#ExtensionDefinitions) above.)
+
+This extension category does not have implementations.
+
+## Views Category
+
+The views extension category is used to determine which options appear to the
+user as available views of domain objects of specific types. A view's extension
+definition has the same properties as a representation (and views can be
+utilized via `mct-representation`); additionally:
+
+* `name`: The human-readable name for this view type.
+* description : A human-readable summary of this view type.
+* `glyph`: A single character to be rendered as an icon in Open MCT Web's custom
+font set.
+* `type`: Optional; if present, this representation is only applicable for
+domain object's of this type.
+* `needs`: Optional array of strings; if present, this representation is only
+applicable for domain objects which have the capabilities identified by these
+strings.
+* `delegation`: Optional boolean, intended to be used in conjunction with
+`needs`; if present, allow required capabilities to be satisfied by means of
+capability delegation. (See [Delegation](#Delegation))
+* `toolbar`: Optional; a definition for the toolbar which may appear in a
+toolbar when using this view in Edit mode. This should be specified as a
+structure for mct-toolbar , with additional properties available for each item in
+that toolbar:
+ * `property`: A property name. This will refer to a property in the view's
+ current selection; that property on the selected object will be modifiable
+ as the `ng-model` of the displayed control in the toolbar. If the value of
+ the property is a function, it will be used as a getter-setter (called with
+ no arguments to use as a getter, called with a value to use as a setter.)
+ * `method`: A method to invoke (again, on the selected object) from the
+ toolbar control. Useful particularly for buttons (which don't edit a single
+ property, necessarily.)
+
+### View Scope
+
+Views do not have implementations, but do get the same properties in scope that
+are provided for `representations`.
+
+When a view is in Edit mode, this scope will additionally contain:
+
+* `commit()`: A function which can be invoked to mark any changes to the view's
+ configuration as ready to persist.
+* `selection`: An object representing the current selection state.
+
+#### Selection State
+
+A view's selection state is, conceptually, a set of JavaScript objects. The
+presence of methods/properties on these objects determine which toolbar controls
+are visible, and what state they manage and/or behavior they invoke.
+
+This set may contain up to two different objects: The _view proxy _, which is
+used to make changes to the view as a whole, and the _ selected object _, which is
+used to represent some state within the view. (Future versions of Open MCT Web
+may support multiple selected objects.)
+
+The `selection` object made available during Edit mode has the following
+methods:
+
+* `proxy([object])`: Get (or set, if called with an argument) the current view
+proxy.
+* `select(object)`: Make this object the selected object.
+* `deselect()`: Clear the currently selected object.
+* `get()`: Get the currently selected object. Returns undefined if there is no
+currently selected object.
+* `selected(object)`: Check if the JavaScript object is currently in the
+selection set. Returns true if the object is either the currently selected
+object, or the current view proxy.
+* `all()`: Get an array of all objects in the selection state. Will include
+either or both of the view proxy and selected object.
+
+# Directives
+
+Open MCT Web defines several Angular directives that are intended for use both
+internally within the platform, and by plugins.
+
+## Before Unload
+
+The `mct-before-unload` directive is used to listen for (and prompt for user
+confirmation) of navigation changes in the browser. This includes reloading,
+following links out of Open MCT Web, or changing routes. It is used to hook into
+both `onbeforeunload` event handling as well as route changes from within
+Angular.
+
+This directive is useable as an attribute. Its value should be an Angular
+expression. When an action that would trigger an unload and/or route change
+occurs, this Angular expression is evaluated. Its result should be a message to
+display to the user to confirm their navigation change; if this expression
+evaluates to a falsy value, no message will be displayed.
+
+## Chart
+
+The `mct-chart` directive is used to support drawing of simple charts. It is
+present to support the Plot view, and its functionality is limited to the
+functionality that is relevant for that view.
+
+This directive is used at the element level and takes one attribute, `draw`
+which is an Angular expression which will should evaluate to a drawing object.
+This drawing object should contain the following properties:
+
+* `dimensions`: The size, in logical coordinates, of the chart area. A
+two-element array or numbers.
+* `origin`: The position, in logical coordinates, of the lower-left corner of
+the chart area. A two-element array or numbers.
+* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is
+expressed as an object containing:
+ * `buffer`: A Float32Array containing points in the line, in logical
+ coordinates, in sequential x,y pairs.
+ * `color`: The color of the line, as a four-element RGBA array, where
+ each element is a number in the range of 0.0-1.0.
+ * `points`: The number of points in the line.
+* `boxes`: An array of rectangles to draw in the chart area. Each is an object
+containing:
+ * `start`: The first corner of the rectangle, as a two-element array of
+ numbers, in logical coordinates.
+ * `end`: The opposite corner of the rectangle, as a two-element array of
+ numbers, in logical coordinates. color : The color of the line, as a
+ four-element RGBA array, where each element is a number in the range of
+ 0.0-1.0.
+
+While `mct-chart` is intended to support plots specifically, it does perform
+some useful management of canvas objects (e.g. choosing between WebGL and Canvas
+2D APIs for drawing based on browser support) so its usage is recommended when
+its supported drawing primitives are sufficient for other charting tasks.
+
+## Container
+
+The `mct-container` is similar to the `mct-include` directive insofar as it allows
+templates to be referenced by symbolic keys instead of by URL. Unlike
+`mct-include` it supports transclusion.
+
+Unlike `mct-include` `mct-container` accepts a key as a plain string attribute,
+instead of as an Angular expression.
+
+## Control
+
+The `mct-control` directive is used to display user input elements. Several
+controls are included with the platform to wrap default input types. This
+directive is primarily intended for internal use by the `mct-form` and
+`mct-toolbar` directives.
+
+When using `mct-control` the attributes `ng-model` `ng-disabled`
+`ng-required` and `ng-pattern` may also be used. These have the usual meaning
+(as they would for an input element) except for `ng-model`; when used, it will
+actually be `ngModel[field]` (see below) that is two-way bound by this control.
+This allows `mct-control` elements to more easily delegate to other
+`mct-control` instances, and also facilitates usage for generated forms.
+
+This directive supports the following additional attributes, all specified as
+Angular expressions:
+
+* `key`: A machine-readable identifier for the specific type of control to
+display.
+* `options`: A set of options to display in this control.
+* `structure`: In practice, contains the definition object which describes this
+form row or toolbar item. Used to pass additional control-specific parameters.
+* `field`: The field in the `ngModel` under which to read/store the property
+associated with this control.
+
+## Drag
+
+The `mct-drag` directive is used to support drag-based gestures on HTML
+elements. Note that this is not 'drag' in the 'drag-and-drop' sense, but 'drag'
+in the more general 'mouse down, mouse move, mouse up' sense.
+
+This takes the form of three attributes:
+
+* `mct-drag`: An Angular expression to evaluate during drag movement.
+* `mct-drag-down`: An Angular expression to evaluate when the drag starts.
+* `mct-drag-up`: An Angular expression to evaluate when the drag ends.
+
+In each case, a variable `delta` will be provided to the expression; this is a
+two-element array or the horizontal and vertical pixel offset of the current
+mouse position relative to the mouse position where dragging began.
+
+## Form
+
+The `mct-form` directive is used to generate forms using a declarative structure,
+and to gather back user input. It is applicable at the element level and
+supports the following attributes:
+
+* `ng-model`: The object which should contain the full form input. Individual
+fields in this model are bound to individual controls; the names used for these
+fields are provided in the form structure (see below).
+* `structure`: The structure of the form; e.g. sections, rows, their names, and
+so forth. The value of this attribute should be an Angular expression.
+* `name`: The name in the containing scope under which to publish form
+"meta-state", e.g. `$valid` `$dirty` etc. This is as the behavior of `ng-form`.
+Passed as plain text in the attribute.
+
+### Form Structure
+
+Forms in Open MCT Web have a common structure to permit consistent display. A
+form is broken down into sections, which will be displayed in groups; each
+section is broken down into rows, each of which provides a control for a single
+property. Input from this form is two-way bound to the object passed via
+`ng-model`.
+
+A form's structure is represented by a JavaScript object in the following form:
+
+ {
+ "name": ... title to display for the form, as a string ...,
+ "sections": [
+ {
+ "name": ... title to display for the section ...,
+ "rows": [
+ {
+ "name": ... title to display for this row ...,
+ "control": ... symbolic key for the control ...,
+ "key": ... field name in ng-model ...
+ "pattern": ... optional, reg exp to match against ...
+ "required": ... optional boolean ...
+ "options": [
+ "name": ... name to display (e.g. in a select) ...,
+ "value": ... value to store in the model ...
+ ]
+ },
+ ... and other rows ...
+ ]
+ },
+ ... and other sections ...
+ ]
+ }
+
+Note that `pattern` may be specified as a string, to simplify storing for
+structures as JSON when necessary. The string should be given in a form
+appropriate to pass to a `RegExp` constructor.
+
+### Form Controls
+
+A few standard control types are included in the platform/forms bundle:
+
+* `textfield`: An area to enter plain text.
+* `select`: A drop-down list of options.
+* `checkbox`: A box which may be checked/unchecked.
+* `color`: A color picker.
+* `button`: A button.
+* `datetime`: An input for UTC date/time entry; gives result as a UNIX
+timestamp, in milliseconds since start of 1970, UTC.
+
+## Include
+
+The `mct-include` directive is similar to ng-include , except that it takes a
+symbolic identifier for a template instead of a URL. Additionally, templates
+included via mct-include will have an isolated scope.
+
+The directive should be used at the element level and supports the following
+attributes, all of which are specified as Angular expressions:
+
+* `key`: Machine-readable identifier for the template (of extension category
+templates ) to be displayed.
+* `ng-model`: _Optional_; will be passed into the template's scope as `ngModel`.
+Intended usage is for two-way bound user input.
+* `parameters`: _Optional_; will be passed into the template's scope as
+parameters. Intended usage is for template-specific display parameters.
+
+## Representation
+
+The `mct-representation` directive is used to include templates which
+specifically represent domain objects. Usage is similar to `mct-include`.
+
+The directive should be used at the element level and supports the following
+attributes, all of which are specified as Angular expressions:
+
+* `key`: Machine-readable identifier for the representation (of extension
+category _representations_ or _views_ ) to be displayed.
+* `mct-object`: The domain object being represented.
+* `ng-model`: Optional; will be passed into the template's scope as `ngModel`.
+Intended usage is for two-way bound user input.
+* `parameters`: Optional; will be passed into the template's scope as
+parameters . Intended usage is for template-specific display parameters.
+
+## Resize
+
+The `mct-resize` directive is used to monitor the size of an HTML element. It is
+specified as an attribute whose value is an Angular expression that will be
+evaluated when the size of the HTML element changes. This expression will be
+provided a single variable, `bounds` which is an object containing two
+properties, `width` and `height` describing the size in pixels of the element.
+
+When using this directive, an attribute `mct-resize-interval` may optionally be
+provided. Its value is an Angular expression describing the number of
+milliseconds to wait before next checking the size of the HTML element; this
+expression is evaluated when the directive is linked and reevaluated whenever
+the size is checked.
+
+## Scroll
+
+The `mct-scroll-x` and `mct-scroll-y` directives are used to both monitor and
+control the horizontal and vertical scroll bar state of an element,
+respectively. They are intended to be used as attributes whose values are
+assignable Angular expressions which two-way bind to the scroll bar state.
+
+## Toolbar
+
+The `mct-toolbar` directive is used to generate toolbars using a declarative
+structure, and to gather back user input. It is applicable at the element level
+and supports the following attributes:
+
+* `ng-model`: The object which should contain the full toolbar input. Individual
+fields in this model are bound to individual controls; the names used for these
+fields are provided in the form structure (see below).
+* `structure`: The structure of the toolbar; e.g. sections, rows, their names, and
+so forth. The value of this attribute should be an Angular expression.
+* `name`: The name in the containing scope under which to publish form
+"meta-state", e.g. `$valid`, `$dirty` etc. This is as the behavior of
+`ng-form`. Passed as plain text in the attribute.
+
+Toolbars support the same control options as forms.
+
+### Toolbar Structure
+
+A toolbar's structure is defined similarly to forms, except instead of rows
+there are items .
+
+ {
+ "name": ... title to display for the form, as a string ...,
+ "sections": [
+ {
+ "name": ... title to display for the section ...,
+ "items": [
+ {
+ "name": ... title to display for this row ...,
+ "control": ... symbolic key for the control ...,
+ "key": ... field name in ng-model ...
+ "pattern": ... optional, reg exp to match against ...
+ "required": ... optional boolean ...
+ "options": [
+ "name": ... name to display (e.g. in a select) ...,
+ "value": ... value to store in the model ...
+ ],
+ "disabled": ... true if control should be disabled ...
+ "size": ... size of the control (for textfields) ...
+ "click": ... function to invoke (for buttons) ...
+ "glyph": ... glyph to display (for buttons) ...
+ "text": ... text within control (for buttons) ...
+ },
+ ... and other rows ...
+ ]
+ },
+ ... and other sections ...
+ ]
+ }
+
+# Services
+
+The Open MCT Web platform provides a variety of services which can be retrieved
+and utilized via dependency injection. These services fall into two categories:
+
+* _Composite Services_ are defined by a set of components extensions; plugins may
+introduce additional components with matching interfaces to extend or augment
+the functionality of the composed service. (See the Framework section on
+Composite Services.)
+* _Other services_ which are defined as standalone service objects; these can be
+utilized by plugins but are not intended to be modified or augmented.
+
+## Composite Type Services
+
+This section describes the composite services exposed by Open MCT Web,
+specifically focusing on their interface and contract.
+
+In many cases, the platform will include a provider for a service which consumes
+a specific extension category; for instance, the `actionService` depends on
+`actions[]` and will expose available actions based on the rules defined for
+that extension category.
+
+In these cases, it will usually be simpler to add a new extension of a given
+category (e.g. of category `actions`) even when the same behavior could be
+introduced by a service component (e.g. an extension of category `components`
+where `provides` is `actionService` and `type` is `provider`.)
+
+Occasionally, the extension category does not provide enough expressive power to
+achieve a desired result. For instance, the Create menu is populated with
+`create` actions, where one such action exists for each creatable type. Since
+the framework does not provide a declarative means to introduce a new action per
+type declaratively, the platform implements this explicitly in an `actionService`
+component of type `provider`. Plugins may use a similar approach when the normal
+extension mechanism is insufficient to achieve a desired result.
+
+### Action Service
+
+The [Action Service](../architecture/platform#action-service) (`actionService`)
+provides `Action` instances which are applicable in specific contexts. See Core
+API for additional notes on the interface for actions. The `actionService` has
+the following interface:
+
+* `getActions(context)`: Returns an array of Action objects which are applicable
+in the specified action context.
+
+### Capability Service
+
+The [Capability Service](../architecture/platform#capability-service) (`capabilityService`)
+provides constructors for capabilities which will be exposed for a given domain
+object.
+
+The capabilityService has the following interface:
+
+* `getCapabilities(model)`: Returns a an object containing key-value pairs,
+representing capabilities which should be exposed by the domain object with this
+model. Keys in this object are the capability keys (as used in a
+`getCapability(...)` call) and values are either:
+ * Functions, in which case they will be used as constructors, which will
+ receive the domain object instance to which the capability applies as their
+ sole argument.The resulting object will be provided as the result of a
+ domain object's `getCapability(...)` call. Note that these instances are cached
+ by each object, but may be recreated when an object is mutated.
+ * Other objects, which will be used directly as the result of a domain
+ object's `getCapability(...)` call.
+
+### Dialog Service
+
+The `dialogService` provides a means for requesting user input via a modal
+dialog. It has the following interface:
+
+* `getUserInput(formStructure, formState)`: Prompt the user to fill out a form.
+The first argument describes the form's structure (as will be passed to
+ mct-form ) while the second argument contains the initial state of that form.
+This returns a Promise for the state of the form after the user has filled it
+in; this promise will be rejected if the user cancels input.
+* `getUserChoice(dialogStructure)`: Prompt the user to make a single choice from
+a set of options, which (in the platform implementation) will be expressed as
+buttons in the displayed dialog. Returns a Promise for the user's choice, which
+will be rejected if the user cancels input.
+
+### Dialog Structure
+
+The object passed as the `dialogStructure` to `getUserChoice` should have the
+following properties:
+
+* `title`: The title to display at the top of the dialog.
+* `hint`: Short message to display below the title.
+* `template`: Identifying key (as will be passed to mct-include ) for the
+template which will be used to populate the inner area of the dialog.
+* `model`: Model to pass in the ng-model attribute of mct-include .
+* `parameters`: Parameters to pass in the parameters attribute of mct-include .
+* `options`: An array of options describing each button at the bottom. Each
+option may have the following properties:
+ * `name`: Human-readable name to display in the button.
+ * `key`: Machine-readable key, to pass as the result of the resolved promise
+ when clicked.
+ * `description`: Description to show in tooltip on hover.
+
+### Domain Object Service
+
+The [Object Service](../architecture/platform.md#object-service) (`objectService`)
+provides domain object instances. It has the following interface:
+
+* `getObjects(ids)`: For the provided array of domain object identifiers,
+returns a Promise for an object containing key-value pairs, where keys are
+domain object identifiers and values are corresponding DomainObject instances.
+Note that the result may contain a superset or subset of the objects requested.
+
+### Gesture Service
+
+The `gestureService` is used to attach gestures (see extension category gestures)
+to representations. It has the following interface:
+
+* `attachGestures(element, domainObject, keys)`: Attach gestures specified by
+the provided gesture keys (an array of strings) to this jqLite-wrapped HTML
+element , which represents the specified domainObject . Returns an object with a
+single method `destroy()`, to be invoked when it is time to detach these
+gestures.
+
+### Model Service
+
+The [Model Service](../architecture/platform.md#model-service) (`modelService`)
+provides domain object models. It has the following interface:
+
+* `getModels(ids)`: For the provided array of domain object identifiers, returns
+a Promise for an object containing key-value pairs, where keys are domain object
+identifiers and values are corresponding domain object models. Note that the
+result may contain a superset or subset of the models requested.
+
+### Persistence Service
+
+The [Persistence Service](../architecture/platform.md#persistence-service) (`persistenceService`)
+provides the ability to load/store JavaScript objects
+(presumably serializing/deserializing to JSON in the process.) This is used
+primarily to store domain object models. It has the following interface:
+
+* `listSpaces()`: Returns a Promise for an array of strings identifying the
+different persistence spaces this service supports. Spaces are intended to be
+used to distinguish between different underlying persistence stores, to allow
+these to live side by side.
+* `listObjects()`: Returns a Promise for an array of strings identifying all
+documents stored in this persistence service.
+* `createObject(space, key, value)`: Create a new document in the specified
+persistence space , identified by the specified key , the contents of which shall
+match the specified value . Returns a promise that will be rejected if creation
+fails.
+* `readObject(space, key)`: Read an existing document in the specified
+persistence space , identified by the specified key . Returns a promise for the
+specified document; this promise will resolve to undefined if the document does
+not exist.
+* `updateObject(space, key, value)`: Update an existing document in the
+specified persistence space , identified by the specified key , such that its
+contents match the specified value . Returns a promise that will be rejected if
+the update fails.
+* `deleteObject(space, key)`: Delete an existing document from the specified
+persistence space , identified by the specified key . Returns a promise which will
+be rejected if deletion fails.
+
+### Policy Service
+
+The [Policy Service](../architecture/platform.md#policy-service) (`policyService`)
+may be used to determine whether or not certain behaviors are
+allowed within the application. It has the following interface:
+
+* `allow(category, candidate, context, [callback])`: Check if this decision
+should be allowed. Returns a boolean. Its arguments are interpreted as:
+ * `category`: A string identifying which kind of decision is being made. See
+ the [section on Categories](#PolicyCategories) for categories supported by
+ the platform; plugins may define and utilize policies of additional
+ categories, as well.
+ * `candidate`: An object representing the thing which shall or shall not be
+ allowed. Usually, this will be an instance of an extension of the category
+ defined above. This does need to be the case; additional policies which are
+ not specific to any extension may also be defined and consulted using unique
+ category identifiers. In this case, the type of the object delivered for the
+ candidate may be unique to the policy type.
+ * `context`: An object representing the context in which the decision is
+ occurring. Its contents are specific to each policy category.
+ * `callback`: Optional; a function to call if the policy decision is rejected.
+ This function will be called with the message string (which may be
+ undefined) of whichever individual policy caused the operation to fail.
+
+### Telemetry Service
+
+The [Telemetry Service](../architecture/platform.md#telemetry-service) (`telemetryService`)
+is used to acquire telemetry data. See the section on
+Telemetry in Core API for more information on how both the arguments and
+responses of this service are structured.
+
+When acquiring telemetry for display, it is recommended that the
+`telemetryHandler` service be used instead of this service. The
+`telemetryHandler` has additional support for subscribing to and requesting
+telemetry data associated with domain objects or groups of domain objects. See
+the [Other Services](#Other-Services) section for more information.
+
+The `telemetryService` has the following interface:
+
+* `requestTelemetry(requests)`: Issue a request for telemetry, matching the
+specified telemetry requests . Returns a _ Promise _ for a telemetry response
+object.
+* `subscribe(callback, requests)`: Subscribe to real-time updates for telemetry,
+matching the specified `requests`. The specified `callback` will be invoked with
+telemetry response objects as they become available. This method returns a
+function which can be invoked to terminate the subscription.
+
+### Type Service
+
+The [Type Service](../architecture/platform.md#type-service) (`typeService`) exposes
+domain object types. It has the following interface:
+
+* `listTypes()`: Returns all domain object types supported in the application,
+as an array of `Type` instances.
+* `getType(key)`: Returns the `Type` instance identified by the provided key, or
+undefined if no such type exists.
+
+### View Service
+
+The [View Service](../architecture/platform.md#view-service) (`viewService`) exposes
+definitions for views of domain objects. It has the following interface:
+
+* `getViews(domainObject)`: Get an array of extension definitions of category
+`views` which are valid and applicable to the specified `domainObject`.
+
+## Other Services
+
+### Drag and Drop
+
+The `dndService` provides information about the content of an active
+drag-and-drop gesture within the application. It is intended to complement the
+`DataTransfer` API of HTML5 drag-and-drop, by providing access to non-serialized
+JavaScript objects being dragged, as well as by permitting inspection during
+drag (which is normally prohibited by browsers for security reasons.)
+
+The `dndService` has the following methods:
+
+* `setData(key, value)`: Set drag data associated with a given type, specified
+by the `key` argument.
+* `getData(key)`: Get drag data associated with a given type, specified by the
+`key` argument.
+* `removeData(key)`: Clear drag data associated with a given type, specified by
+the `key` argument.
+
+### Navigation
+
+The _Navigation_ service provides information about the current navigation state
+of the application; that is, which object is the user currently viewing? This
+service merely tracks this state and notifies listeners; it does not take
+immediate action when navigation changes, although its listeners might.
+
+The `navigationService` has the following methods:
+
+* `getNavigation()`: Get the current navigation state. Returns a `DomainObject`.
+* `setNavigation(domainObject)`: Set the current navigation state. Returns a
+`DomainObject`.
+* `addListener(callback)`: Listen for changes in navigation state. The provided
+`callback` should be a `Function` which takes a single `DomainObject` as an
+argument.
+* `removeListener(callback)`: Stop listening for changes in navigation state.
+The provided `callback` should be a `Function` which has previously been passed
+to addListener .
+
+### Now
+
+The service now is a function which acts as a simple wrapper for `Date.now()`.
+It is present mainly so that this functionality may be more easily mocked in
+tests for scripts which use the current time.
+
+### Telemetry Formatter
+
+The _Telemetry Formatter_ is a utility for formatting domain and range values
+read from a telemetry series.
+
+`telemetryFormatter` has the following methods:
+
+* `formatDomainValue(value)`: Format the provided domain value (which will be
+assumed to be a timestamp) for display; returns a string.
+* `formatRangeValue(value)`: Format the provided range value (a number) for
+display; returns a string.
+
+### Telemetry Handler
+
+The _Telemetry Handler_ is a utility for retrieving telemetry data associated
+with domain objects; it is particularly useful for dealing with cases where the
+telemetry capability is delegated to contained objects (as occurs
+in _Telemetry Panels_.)
+
+The `telemetryHandler` has the following methods:
+
+* `handle(domainObject, callback, [lossless])`: Subscribe to and issue future
+requests for telemetry associated with the provided `domainObject`, invoking the
+provided callback function when streaming data becomes available. Returns a
+`TelemetryHandle` (see below.)
+
+#### Telemetry Handle
+
+A TelemetryHandle has the following methods:
+
+* `getTelemetryObjects()`: Get the domain objects (as a `DomainObject[]`) that
+have a telemetry capability and are being handled here. Note that these are
+looked up asynchronously, so this method may return an empty array if the
+initial lookup is not yet completed.
+* `promiseTelemetryObjects()`: As `getTelemetryObjects()`, but returns a Promise
+that will be fulfilled when the lookup is complete.
+* `unsubscribe()`: Unsubscribe to streaming telemetry updates associated with
+this handle.
+* `getDomainValue(domainObject)`: Get the most recent domain value received via
+a streaming update for the specified `domainObject`.
+* `getRangeValue(domainObject)`: Get the most recent range value received via a
+streaming update for the specified `domainObject`.
+* `getMetadata()`: Get metadata (as reported by the `getMetadata()` method of a
+telemetry capability) associated with telemetry-providing domain objects.
+Returns an array, which is in the same order as getTelemetryObjects() .
+* `request(request, callback)`: Issue a new request for historical telemetry
+data. The provided callback will be invoked when new data becomes available,
+which may occur multiple times (e.g. if there are multiple domain objects.) It
+will be invoked with the DomainObject for which a new series is available, and
+the TelemetrySeries itself, in that order.
+* `getSeries(domainObject)`: Get the latest `TelemetrySeries` (as resulted from
+a previous `request(...)` call) available for this domain object.
+
+
+# Models
+Domain object models in Open MCT Web are JavaScript objects describing the
+persistent state of the domain objects they describe. Their contents include a
+mix of commonly understood metadata attributes; attributes which are recognized
+by and/or determine the applicability of specific extensions; and properties
+specific to given types.
+
+## General Metadata
+
+Some properties of domain object models have a ubiquitous meaning through Open
+MCT Web and can be utilized directly:
+
+* `name`: The human-readable name of the domain object.
+
+## Extension-specific Properties
+
+Other properties of domain object models have specific meaning imposed by other
+extensions within the Open MCT Web platform.
+
+### Capability-specific Properties
+
+Some properties either trigger the presence/absence of certain capabilities, or
+are managed by specific capabilities:
+
+* `composition`: An array of domain object identifiers that represents the
+contents of this domain object (e.g. as will appear in the tree hierarchy.)
+Understood by the composition capability; the presence or absence of this
+property determines the presence or absence of that capability.
+* `modified`: The timestamp (in milliseconds since the UNIX epoch) of the last
+modification made to this domain object. Managed by the mutation capability.
+* `persisted`: The timestamp (in milliseconds since the UNIX epoch) of the last
+time when changes to this domain object were persisted. Managed by the
+ persistence capability.
+* `relationships`: An object containing key-value pairs, where keys are symbolic
+identifiers for relationship types, and values are arrays of domain object
+identifiers. Used by the relationship capability; the presence or absence of
+this property determines the presence or absence of that capability.
+* `telemetry`: An object which serves as a template for telemetry requests
+associated with this domain object (e.g. specifying `source` and `key`; see
+Telemetry Requests under Core API.) Used by the telemetry capability; the
+presence or absence of this property determines the presence or absence of that
+capability.
+* `type`: A string identifying the type of this domain object. Used by the `type`
+capability.
+
+### View Configurations
+
+Persistent configurations for specific views of domain objects are stored in the
+domain object model under the property configurations . This is an object
+containing key-value pairs, where keys identify the view, and values are objects
+containing view-specific (and view-managed) configuration properties.
+
+## Modifying Models
+When interacting with a domain object's model, it is possible to make
+modifications to it directly. __Don't!__ These changes may not be properly detected
+by the platform, meaning that other representations of the domain object may not
+be updated, changes may not be saved at the expected times, and generally, that
+unexpected behavior may occur. Instead, use the `mutation` capability.
+
+# Capabilities
+
+Dynamic behavior associated with a domain object is expressed as capabilities. A
+capability is a JavaScript object with an interface that is specific to the type
+of capability in use.
+
+Often, there is a relationship between capabilities and services. For instance,
+there is an action capability and an actionService , and there is a telemetry
+capability as well as a `telemetryService`. Typically, the pattern here is that
+the capability will utilize the service for the specific domain object.
+
+When interacting with domain objects, it is generally preferable to use a
+capability instead of a service when the option is available. Capability
+interfaces are typically easier to use and/or more powerful in these situations.
+Additionally, this usage provides a more robust substitutability mechanism; for
+instance, one could configure a plugin such that it provided a totally new
+implementation of a given capability which might not invoke the underlying
+service, while user code which interacts with capabilities remains indifferent
+to this detail.
+
+## Action Capability
+
+The `action` capability is present for all domain objects. It allows applicable
+`Action` instances to be retrieved and performed for specific domain objects.
+
+For example:
+ `domainObject.getCapability("action").perform("navigate"); `
+ ...will initiate a navigate action upon the domain object, if an action with
+ key "navigate" is defined.
+
+This capability has the following interface:
+* `getActions(context)`: Get the actions that are applicable in the specified
+action `context`; the capability will fill in the `domainObject` field of this
+context if necessary. If context is specified as a string, they will instead be
+used as the `key` of the action context. Returns an array of `Action` instances.
+* `perform(context)`: Perform an action. This will find and perform the first
+matching action available for the specified action context , filling in the
+`domainObject` field as necessary. If `context` is specified as a string, they
+will instead be used as the `key` of the action context. Returns a `Promise` for
+the result of the action that was performed, or `undefined` if no matching action
+was found.
+
+## Composition Capability
+
+The `composition` capability provides access to domain objects that are
+contained by this domain object. While the `composition` property of a domain
+object's model describes these contents (by their identifiers), the
+`composition` capability provides a means to load the corresponding
+`DomainObject` instances in the same order. The absence of this property in the
+model will result in the absence of this capability in the domain object.
+
+This capability has the following interface:
+
+* `invoke()`: Returns a `Promise` for an array of `DomainObject` instances.
+
+## Delegation Capability
+
+The delegation capability is used to communicate the intent of a domain object
+to delegate responsibilities, which would normally handled by other
+capabilities, to the domain objects in its composition.
+
+This capability has the following interface:
+
+* `getDelegates(key)`: Returns a Promise for an array of DomainObject instances,
+to which this domain object wishes to delegate the capability with the specified
+key .
+* `invoke(key)`: Alias of getDelegates(key) .
+* `doesDelegate(key)`: Returns true if the domain object does delegate the
+capability with the specified key .
+
+The platform implementation of the delegation capability inspects the domain
+object's type definition for a property delegates , whose value is an array of
+strings describing which capabilities domain objects of that type wish to
+delegate. If this property is not present, the delegation capability will not be
+present in domain objects of that type.
+
+## Editor Capability
+
+The editor capability is meant primarily for internal use by Edit mode, and
+helps to manage the behavior associated with exiting _Edit_ mode via _Save_ or
+_Cancel_. Its interface is not intended for general use. However,
+`domainObject.hasCapability(editor)` is a useful way of determining whether or
+not we are looking at an object in _Edit_ mode.
+
+## Mutation Capability
+
+The `mutation` capability provides a means by which the contents of a domain
+object's model can be modified. This capability is provided by the platform for
+all domain objects, and has the following interface:
+
+* `mutate(mutator, [timestamp])`: Modify the domain object's model using the
+specified `mutator` function. After changes are made, the `modified` property of
+the model will be updated with the specified `timestamp` if one was provided,
+or with the current system time.
+* `invoke(...)`: Alias of `mutate`.
+
+Changes to domain object models should only be made via the `mutation`
+capability; other platform behavior is likely to break (either by exhibiting
+undesired behavior, or failing to exhibit desired behavior) if models are
+modified by other means.
+
+### Mutator Function
+
+The mutator argument above is a function which will receive a cloned copy of the
+domain object's model as a single argument. It may return:
+
+* A `Promise` in which case the resolved value of the promise will be used to
+determine which of the following forms is used.
+* Boolean `false` in which case the mutation is cancelled.
+* A JavaScript object, in which case this object will be used as the new model
+for this domain object.
+* No value (or, equivalently, `undefined`), in which case the cloned copy
+(including any changes made in place by the mutator function) will be used as
+the new domain object model.
+
+## Persistence Capability
+
+The persistence capability provides a mean for interacting with the underlying
+persistence service which stores this domain object's model. It has the
+following interface:
+
+* `persist()`: Store the local version of this domain object, including any
+changes, to the persistence store. Returns a Promise for a boolean value, which
+will be true when the object was successfully persisted.
+* `refresh()`: Replace this domain object's model with the most recent version
+from persistence. Returns a Promise which will resolve when the change has
+completed.
+* `getSpace()`: Return the string which identifies the persistence space which
+stores this domain object.
+
+## Relationship Capability
+
+The relationship capability provides a means for accessing other domain objects
+with which this domain object has some typed relationship. It has the following
+interface:
+
+* `listRelationships()`: List all types of relationships exposed by this object.
+Returns an array of strings identifying the types of relationships.
+* `getRelatedObjects(relationship)`: Get all domain objects to which this domain
+object has the specified type of relationship, which is a string identifier
+(as above.) Returns a `Promise` for an array of `DomainObject` instances.
+
+The platform implementation of the `relationship` capability is present for domain
+objects which has a `relationships` property in their model, whose value is an
+object containing key-value pairs, where keys are strings identifying
+relationship types, and values are arrays of domain object identifiers.
+
+## Telemetry Capability
+
+The telemetry capability provides a means for accessing telemetry data
+associated with a domain object. It has the following interface:
+
+* `requestData([request])`: Request telemetry data for this specific domain
+object, using telemetry request parameters from the specified request if
+provided. This capability will fill in telemetry request properties as-needed
+for this domain object. Returns a `Promise` for a `TelemetrySeries`.
+* `subscribe(callback, [request])`: Subscribe to telemetry data updates for
+this specific domain object, using telemetry request parameters from the
+specified request if provided. This capability will fill in telemetry request
+properties as-needed for this domain object. The specified callback will be
+invoked with TelemetrySeries instances as they arrive. Returns a function which
+can be invoked to terminate the subscription, or undefined if no subscription
+could be obtained.
+* `getMetadata()`: Get metadata associated with this domain object's telemetry.
+
+The platform implementation of the `telemetry` capability is present for domain
+objects which has a `telemetry` property in their model and/or type definition;
+this object will serve as a template for telemetry requests made using this
+object, and will also be returned by `getMetadata()` above.
+
+## Type Capability
+The `type` capability exposes information about the domain object's type. It has
+the same interface as `Type`; see Core API.
+
+## View Capability
+
+The `view` capability exposes views which are applicable to a given domain
+object. It has the following interface:
+
+* `invoke()`: Returns an array of extension definitions for views which are
+applicable for this domain object.
+
+# Actions
+
+Actions are reusable processes/behaviors performed by users within the system,
+typically upon domain objects.
+
+## Action Categories
+
+The platform understands the following action categories (specifiable as the
+`category` parameter of an action's extension definition.)
+
+* `contextual`: Appears in context menus.
+* `view-control`: Appears in top-right area of view (as buttons) in Browse mode
+
+## Platform Actions
+The platform defines certain actions which can be utilized by way of a domain
+object's `action` capability. Unless otherwise specified, these act upon (and
+modify) the object described by the `domainObject` property of the action's
+context.
+
+* `cancel`: Cancel the current editing action (invoked from Edit mode.)
+* `compose`: Place an object in another object's composition. The object to be
+added should be provided as the `selectedObject` of the action context.
+* `edit`: Start editing an object (enter Edit mode.)
+* `fullscreen`: Enter full screen mode.
+* `navigate`: Make this object the focus of navigation (e.g. highlight it within
+the tree, display a view of it to the right.)
+* `properties`: Show the 'Edit Properties' dialog.
+* `remove`: Remove this domain object from its parent's composition. (The
+parent, in this case, is whichever other domain object exposed this object by
+way of its `composition` capability.)
+* `save`: Save changes (invoked from Edit mode.)
+* `window`: Open this object in a new window.
+
+# Policies
+
+Policies are consulted to determine when certain behavior in Open MCT Web is
+allowed. Policy questions are assigned to certain categories, which broadly
+describe the type of decision being made; within each category, policies have a
+candidate (the thing which may or may not be allowed) and, optionally, a context
+(describing, generally, the context in which the decision is occurring.)
+
+The types of objects passed for 'candidate' and 'context' vary by category;
+these types are documented below.
+
+## Policy Categories
+
+The platform understands the following policy categories (specifiable as the
+`category` parameter of an policy's extension definition.)
+
+* `action`: Determines whether or not a given action is allowable. The candidate
+argument here is an Action; the context is its action context object.
+* `composition`: Determines whether or not domain objects of a given type are
+allowed to contain domain objects of another type. The candidate argument here
+is the container's `Type`; the context argument is the `Type` of the object to be
+contained.
+* `view`: Determines whether or not a view is applicable for a domain object.
+The candidate argument is the view's extension definition; the context argument
+is the `DomainObject` to be viewed.
+
+# Build-Test-Deploy
+Open MCT Web is designed to support a broad variety of build and deployment
+options. The sources can be deployed in the same directory structure used during
+development. A few utilities are included to support development processes.
+
+## Command-line Build
+Open MCT Web includes a script for building via command line using Maven 3.0.4
+[https://maven.apache.org/]().
+
+Invoking mvn clean install will:
+
+* Check code style using JSLint. The build will fail if JSLint raises any warnings.
+* Run the test suite (see below.) The build will fail if any tests fail.
+* Populate version info (e.g. commit hash, build time.)
+* Produce a web archive (`.war`) artifact in the `target` directory.
+
+The produced artifact contains a subset of the repository's own folder
+hierarchy, omitting tests and example bundles.
+
+Note that an internet connection is required to run this build, in order to
+download build dependencies.
+
+## Test Suite
+
+Open MCT Web uses Jasmine [http://jasmine.github.io/]() for automated testing.
+The file `test.html` included at the top level of the source repository, can be
+run from the browser to perform tests for all active bundles, as defined in
+`bundle.json`.
+
+To define tests for a bundle:
+
+* Include a directory named `test` within that bundle.
+* In the `test` directory, include a file named `suite.json`. This will identify
+which scripts will be tested.
+* The file `suite.json` must contain a JSON array of strings, where each string
+is the name of a script to be tested. These names should include any directory
+paths to the script after (but not including) the `src` folder, and should not
+include the file's `.js` extension. (Note that while Open MCT Web's framework
+allows a different name to be chosen for the src directory, the test runner
+does not: This directory must be named `src` for the test runner to find it.)
+* For each script to be tested, a corresponding test script should be located in
+the bundle's `test` directory. This should include the suffix Spec at the end of
+the filename (but before the `.js` extension.) This test script should be an AMD
+module which uses the Jasmine API to declare its test behavior. It should
+declare an AMD dependency on the script to be tested, using a relative path.
+
+For example, if writing tests for a bundle at example/foo with two scripts:
+* `example/foo/src/controllers/FooController.js`
+* `example/foo/src/directives/FooDirective.js`
+
+First, these scripts should be identified in `example/foo/test/suite.json` e.g.
+with contents:`[ "controllers/FooController", "directives/FooDirective" ]`
+
+Then, scripts which describe these tests should be written. For example, test
+`example/foo/test/controllers/FooControllerSpec.js` could look like:
+
+ /*global define,Promise,describe,it,expect,beforeEach*/
+
+ define(
+ ["../../src/controllers/FooController"],
+ function (FooController) {
+ "use strict";
+
+
+ describe("The foo controller", function () {
+ it("does something", function () {
+ var controller = new FooController();
+ expect(controller.foo()).toEqual("foo");
+ });
+ });
+ }
+ );
+
+
+## Code Coverage
+
+In addition to running tests, the test runner will also capture code coverage
+information using [Blanket.JS](http://blanketjs.org/) and display this at the
+bottom of the screen. Currently, only statement coverage is displayed.
+
+## Deployment
+Open MCT Web is built to be flexible in terms of the deployment strategies it
+supports. In order to run in the browser, Open MCT Web needs:
+
+1. HTTP access to sources/resources for the framework, platform, and all active
+bundles.
+2. Access to any external services utilized by active bundles. (This means that
+external services need to support HTTP or some other web-accessible interface,
+like WebSockets.)
+
+Any HTTP server capable of serving flat files is sufficient for the first point.
+The command-line build also packages Open MCT Web into a `.war` file for easier
+deployment on containers such as Apache Tomcat.
+
+The second point may be less flexible, as it depends upon the specific services
+to be utilized by Open MCT Web. Because of this, it is often the set of external
+services (and the manner in which they are exposed) that determine how to deploy
+Open MCT Web.
+
+One important constraint to consider in this context is the browser's same
+origin policy. If external services are not on the same apparent host and port
+as the client (from the perspective of the browser) then access may be
+disallowed. There are two workarounds if this occurs:
+
+* Make the external service appear to be on the same host/port, either by
+actually deploying it there, or by proxying requests to it.
+* Enable CORS (cross-origin resource sharing) on the external service. This is
+only possible if the external service can be configured to support CORS. Care
+should be exercised if choosing this option to ensure that the chosen
+configuration does not create a security vulnerability.
+
+Examples of deployment strategies (and the conditions under which they make the
+most sense) include:
+
+* If the external services that Open MCT Web will utilize are all running on
+Apache Tomcat [https://tomcat.apache.org/](), then it makes sense to run Open
+MCT Web from the same Tomcat instance as a separate web application. The
+`.war` artifact produced by the command line build facilitates this deployment
+option. (See [https://tomcat.apache.org/tomcat-8.0-doc/deployer-howto.html() for
+general information on deploying in Tomcat.)
+* If a variety of external services will be running from a variety of
+hosts/ports, then it may make sense to use a web server that supports proxying,
+such as the Apache HTTP Server [http://httpd.apache.org/](). In this
+configuration, the HTTP server would be configured to proxy (or reverse proxy)
+requests at specific paths to the various external services, while providing
+Open MCT Web as flat files from a different path.
+* If a single server component is being developed to handle all server-side
+needs of an Open MCT Web instance, it can make sense to serve Open MCT Web (as
+flat files) from the same component using an embedded HTTP server such as Nancy
+[http://nancyfx.org/]().
+* If no external services are needed (or if the 'external services' will just
+be generating flat files to read) it makes sense to utilize a lightweight flat
+file HTTP server such as Lighttpd [http://www.lighttpd.net/](). In this
+configuration, Open MCT Web sources/resources would be placed at one path, while
+the files generated by the external service are placed at another path.
+* If all external services support CORS, it may make sense to have an HTTP
+server that is solely responsible for making Open MCT Web sources/resources
+available, and to have Open MCT Web contact these external services directly.
+Again, lightweight HTTP servers such as Lighttpd [http://www.lighttpd.net/]()
+are useful in this circumstance. The downside of this option is that additional
+configuration effort is required, both to enable CORS on the external services,
+and to ensure that Open MCT Web can correctly locate these services.
+
+Another important consideration is authentication. By design, Open MCT Web does
+not handle user authentication. Instead, this should typically be treated as a
+deployment-time concern, where authentication is handled by the HTTP server
+which provides Open MCT Web, or an external access management system.
+
+### Configuration
+In most of the deployment options above, some level of configuration is likely
+to be needed or desirable to make sure that bundles can reach the external
+services they need to reach. Most commonly this means providing the path or URL
+to an external service.
+
+Configurable parameters within Open MCT Web are specified via constants
+(literally, as extensions of the `constants` category) and accessed via
+dependency injection by the scripts which need them. Reasonable defaults for
+these constants are provided in the bundle where they are used. Plugins are
+encouraged to follow the same pattern.
+
+Constants may be specified in any bundle; if multiple constants are specified
+with the same `key` the highest-priority one will be used. This allows default
+values to be overridden by specifying constants with higher priority.
+
+This permits at least three configuration approaches:
+
+* Modify the constants defined in their original bundles when deploying. This is
+generally undesirable due to the amount of manual work required and potential
+for error, but is viable if there are a small number of constants to change.
+* Add a separate configuration bundle which overrides the values of these
+constants. This is particularly appropriate when multiple configurations (e.g.
+development, test, production) need to be managed easily; these can be swapped
+quickly by changing the set of active bundles in bundles.json.
+* Deploy Open MCT Web and its external services in such a fashion that the
+default paths to reach external services are all correct.
+
+### Configuration Constants
+
+The following configuration constants are recognized by Open MCT Web bundles:
+* CouchDB adapter - `platform/persistence/couch`
+ * `COUCHDB_PATH`: URL or path to the CouchDB database to be used for domain
+ object persistence. Should not include a trailing slash.
+* ElasticSearch adapter - `platform/persistence/elastic`
+ * `ELASTIC_ROOT`: URL or path to the ElasticSearch instance to be used for
+ domain object persistence. Should not include a trailing slash.
+ * `ELASTIC_PATH`: Path relative to the ElasticSearch instance where domain
+ object models should be persisted. Should take the form `<index>/<type>`. \ No newline at end of file
diff --git a/docs/src/index.html b/docs/src/index.html
index e84b40523..e80a6138b 100644
--- a/docs/src/index.html
+++ b/docs/src/index.html
@@ -29,8 +29,9 @@
Sections:
<ul>
<li><a href="api/">API</a></li>
- <li><a href="guide/">Developer Guide</a></li>
<li><a href="architecture/">Architecture Overview</a></li>
+ <li><a href="guide/">Developer Guide</a></li>
+ <li><a href="tutorials/">Tutorials</a></li>
</ul>
</body>
</html>
diff --git a/docs/src/tutorials/images/add-task.png b/docs/src/tutorials/images/add-task.png
new file mode 100644
index 000000000..7780365c5
--- /dev/null
+++ b/docs/src/tutorials/images/add-task.png
Binary files differ
diff --git a/docs/src/tutorials/images/bar-plot-2.png b/docs/src/tutorials/images/bar-plot-2.png
new file mode 100644
index 000000000..a32c2b76f
--- /dev/null
+++ b/docs/src/tutorials/images/bar-plot-2.png
Binary files differ
diff --git a/docs/src/tutorials/images/bar-plot-3.png b/docs/src/tutorials/images/bar-plot-3.png
new file mode 100644
index 000000000..0899984a3
--- /dev/null
+++ b/docs/src/tutorials/images/bar-plot-3.png
Binary files differ
diff --git a/docs/src/tutorials/images/bar-plot-4.png b/docs/src/tutorials/images/bar-plot-4.png
new file mode 100644
index 000000000..50a9091fa
--- /dev/null
+++ b/docs/src/tutorials/images/bar-plot-4.png
Binary files differ
diff --git a/docs/src/tutorials/images/bar-plot.png b/docs/src/tutorials/images/bar-plot.png
new file mode 100644
index 000000000..2f113d4c6
--- /dev/null
+++ b/docs/src/tutorials/images/bar-plot.png
Binary files differ
diff --git a/docs/src/tutorials/images/chrome.png b/docs/src/tutorials/images/chrome.png
new file mode 100644
index 000000000..1b9b7b80d
--- /dev/null
+++ b/docs/src/tutorials/images/chrome.png
Binary files differ
diff --git a/docs/src/tutorials/images/remove-task.png b/docs/src/tutorials/images/remove-task.png
new file mode 100644
index 000000000..015ec95ac
--- /dev/null
+++ b/docs/src/tutorials/images/remove-task.png
Binary files differ
diff --git a/docs/src/tutorials/images/telemetry-1.png b/docs/src/tutorials/images/telemetry-1.png
new file mode 100644
index 000000000..2a606e83c
--- /dev/null
+++ b/docs/src/tutorials/images/telemetry-1.png
Binary files differ
diff --git a/docs/src/tutorials/images/telemetry-2.png b/docs/src/tutorials/images/telemetry-2.png
new file mode 100644
index 000000000..0b34dd90f
--- /dev/null
+++ b/docs/src/tutorials/images/telemetry-2.png
Binary files differ
diff --git a/docs/src/tutorials/images/telemetry-3.png b/docs/src/tutorials/images/telemetry-3.png
new file mode 100644
index 000000000..c235b1d54
--- /dev/null
+++ b/docs/src/tutorials/images/telemetry-3.png
Binary files differ
diff --git a/docs/src/tutorials/images/todo-edit.png b/docs/src/tutorials/images/todo-edit.png
new file mode 100644
index 000000000..3c1ba3f5c
--- /dev/null
+++ b/docs/src/tutorials/images/todo-edit.png
Binary files differ
diff --git a/docs/src/tutorials/images/todo-list.png b/docs/src/tutorials/images/todo-list.png
new file mode 100644
index 000000000..48c84c63e
--- /dev/null
+++ b/docs/src/tutorials/images/todo-list.png
Binary files differ
diff --git a/docs/src/tutorials/images/todo-restyled.png b/docs/src/tutorials/images/todo-restyled.png
new file mode 100644
index 000000000..9fd7008c2
--- /dev/null
+++ b/docs/src/tutorials/images/todo-restyled.png
Binary files differ
diff --git a/docs/src/tutorials/images/todo-selection.png b/docs/src/tutorials/images/todo-selection.png
new file mode 100644
index 000000000..a0ff87514
--- /dev/null
+++ b/docs/src/tutorials/images/todo-selection.png
Binary files differ
diff --git a/docs/src/tutorials/images/todo.png b/docs/src/tutorials/images/todo.png
new file mode 100644
index 000000000..44a7b7b2e
--- /dev/null
+++ b/docs/src/tutorials/images/todo.png
Binary files differ
diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md
new file mode 100644
index 000000000..d07466aba
--- /dev/null
+++ b/docs/src/tutorials/index.md
@@ -0,0 +1,3055 @@
+# Open MCT Web Tutorials
+
+Victor Woeltjen
+victor.woeltjen@nasa.gov
+
+October 14, 2015
+Document Version 2.2
+
+Date | Version | Summary of Changes | Author
+---------------- | ------- | --------------------------------- | ---------------
+May 12, 2015 | 0 | Initial Draft | Victor Woeltjen
+June 4, 2015 | 1.0 | Name changes | Victor Woeltjen
+July 28, 2015 | 2.0 | Telemetry adapter tutorial | Victor Woeltjen
+July 31, 2015 | 2.1 | Clarify telemetry adapter details | Victor Woeltjen
+October 14, 2015 | 2.2 | Conversion to markdown | Andrew Henry
+
+# Introduction
+
+## This document
+This document contains a number of code examples in formatted code blocks. In
+many cases these code blocks are repeated in order to highlight code that has
+been added or removed as part of the tutorial. In these cases, any lines added
+will be indicated with a '+' at the start of the line. Any lines removed will
+be indicated with a '-'.
+
+## Setting Up Open MCT Web
+
+In this section, we will cover the steps necessary to get a minimal Open MCT Web
+developer environment up and running. Once we have this, we will be able to
+proceed with writing plugins as described in this tutorial.
+
+### Prerequisites
+
+This tutorial assumes you have the following software installed. Version numbers
+record what was used in writing this tutorial; the same steps should work with
+more recent versions, but this cannot be guaranteed.
+
+* Node.js v0.12.2: https://nodejs.org/
+* git v1.8.3.4: http://git-scm.com/
+* Google Chrome v42: https://www.google.com/chrome/
+* A text editor.
+
+Open MCT Web can be run without any of these tools, provided suitable
+alternatives are taken; see the [Open MCT Web Developer Guide](../guide/index.md)
+for a more general overview of how to run and deploy a Open MCT Web application.
+
+### Check out Open MCT Web Sources
+
+First step is to check out Open MCT Web from the source repository.
+
+`git clone https://github.com/nasa/openmctweb.git openmctweb`
+
+This will create a copy of the Open MCT Web source code repository in the folder
+`openmctweb` (relative to the path from which you ran the command.)
+If you have a repository URL, use that as the “path to repo” above. Alternately,
+if you received Open MCT Web as a git bundle, the path to that bundle on the
+local filesystem can be used instead.
+At this point, it will also be useful to branch off of Open MCT Web v0.6.2
+(which was used when writing these tutorials) to begin adding plugins.
+
+ cd openmctweb
+ git branch <my branch name> open-v0.6.2
+ git checkout <my branch name>
+
+### Configuring Persistence
+
+In its default configuration, Open MCT Web will try to use ElasticSearch
+(expected to be deployed at /elastic on the same HTTP server running Open MCT
+Web) to persist user-created domain objects. We don’t need that for these
+tutorials, so we will replace the ElasticSearch plugin with the example
+persistence plugin. This doesn’t actually persist, so anything we create within
+Open MCT Web will be lost on reload, but that’s fine for purposes of these
+tutorials.
+
+To change this configuration, edit bundles.json (at the top level of the Open
+MCT Web repository) and replace platform/persistence/elastic with
+example/persistence.
+
+#### Bundle Before
+
+ [
+ "platform/framework",
+ "platform/core",
+ "platform/representation",
+ "platform/commonUI/about",
+ "platform/commonUI/browse",
+ "platform/commonUI/edit",
+ "platform/commonUI/dialog",
+ "platform/commonUI/general",
+ "platform/containment",
+ "platform/telemetry",
+ "platform/features/layout",
+ "platform/features/pages",
+ "platform/features/plot",
+ "platform/features/scrolling",
+ "platform/forms",
+ "platform/persistence/queue",
+ -- "platform/persistence/elastic",
+ "platform/policy",
+
+ "example/generator"
+ ]
+__bundles.json__
+
+#### Bundle After
+
+ [
+ "platform/framework",
+ "platform/core",
+ "platform/representation",
+ "platform/commonUI/about",
+ "platform/commonUI/browse",
+ "platform/commonUI/edit",
+ "platform/commonUI/dialog",
+ "platform/commonUI/general",
+ "platform/containment",
+ "platform/telemetry",
+ "platform/features/layout",
+ "platform/features/pages",
+ "platform/features/plot",
+ "platform/features/scrolling",
+ "platform/forms",
+ "platform/persistence/queue",
+ "platform/policy",
+
+ ++ "example/persistence",
+ "example/generator"
+ ]
+__bundles.json__
+
+### Run a Web Server
+
+The next step is to run a web server so that you can view the Open MCT Web
+client (including the plugins you add to it) in browser. Any web server can
+be used for hosting OpenMCTWeb, and a trivial web server is provided in this
+package for the purposes of running the tutorials. The provided web server
+should not be used in a production environment
+
+To run the tutorial web server
+
+ node app.js
+
+### Viewing in Browser
+
+Once running, you should be able to view Open MCT Web from your browser at
+[http://localhost:8080/]() (assuming the web server is running on port 8080,
+and OpenMCTWeb is installed at the server's root path).
+[Google Chrome](https://www.google.com/chrome/) is recommended for these
+tutorials, as Chrome is Open MCT Web’s “test-to” browser. The browser cache
+can sometimes interfere with development (masking changes by
+using older versions of sources); to avoid this, it is easiest to run Chrome
+with Developer Tools expanded, and “Disable cache” selected from the Network
+tab, as shown below.
+
+![Chrome Developer Tools](images/chrome.png)
+
+# Tutorials
+
+These tutorials cover three of the common tasks in Open MCT Web:
+
+* The “to-do list” tutorial illustrates how to add a new application feature.
+* The “bar graph” tutorial illustrates how to add a new telemetry visualization.
+* The “data set reader” tutorial illustrates how to integrate with a telemetry
+backend.
+
+## To-do List
+
+The goal of this tutorial is to add a new application feature to Open MCT Web:
+To-do lists. Users should be able to create and manage these to track items that
+they need to do. This is modelled after the to-do lists at [http://todomvc.com/]().
+
+### Step 1-Create the Plugin
+
+The first step to adding a new feature to Open MCT Web is to create the plugin
+which will expose that feature. A plugin in Open MCT Web is represented by what
+is called a bundle; a bundle, in turn, is a directory which contains a file
+bundle.json, which in turn describes where other relevant sources & resources
+will be. The syntax of this file is described in more detail in the Open MCT Web
+Developer Guide.
+
+We will create this file in the directory tutorials/todo (we can hereafter refer
+to this plugin as tutorials/todo as well.) We will start with an “empty bundle”,
+one which exposes no extensions - which looks like:
+
+ {
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+
+ }
+ }
+
+__tutorials/todo/bundle.json__
+
+We will also include this in our list of active bundles.
+
+#### Before
+ [
+ "platform/framework",
+ "platform/core",
+ "platform/representation",
+ "platform/commonUI/about",
+ "platform/commonUI/browse",
+ "platform/commonUI/edit",
+ "platform/commonUI/dialog",
+ "platform/commonUI/general",
+ "platform/containment",
+ "platform/telemetry",
+ "platform/features/layout",
+ "platform/features/pages",
+ "platform/features/plot",
+ "platform/features/scrolling",
+ "platform/forms",
+ "platform/persistence/queue",
+ "platform/policy",
+
+ "example/persistence",
+ "example/generator"
+ ]
+__bundles.json__
+
+#### After
+ [
+ "platform/framework",
+ "platform/core",
+ "platform/representation",
+ "platform/commonUI/about",
+ "platform/commonUI/browse",
+ "platform/commonUI/edit",
+ "platform/commonUI/dialog",
+ "platform/commonUI/general",
+ "platform/containment",
+ "platform/telemetry",
+ "platform/features/layout",
+ "platform/features/pages",
+ "platform/features/plot",
+ "platform/features/scrolling",
+ "platform/forms",
+ "platform/persistence/queue",
+ "platform/policy",
+
+ "example/persistence",
+ "example/generator",
+
+ ++ "tutorials/todo"
+ ]
+
+__bundles.json__
+
+At this point, we can reload Open MCT Web. We haven’t introduced any new
+functionality, so we don’t see anything different, but if we run with logging
+enabled ([http://localhost:8080/?log=info]()) and check the browser console, we
+should see:
+
+`Resolving extensions for bundle tutorials/todo(To-do Plugin)`
+
+...which shows that our plugin has loaded.
+
+### Step 2-Add a Domain Object Type
+
+Features in a Open MCT Web application are most commonly expressed as domain
+objects and/or views thereof. A domain object is some thing that is relevant to
+the work that the Open MCT Web application is meant to support. Domain objects
+can be created, organized, edited, placed in layouts, and so forth. (For a
+deeper explanation of domain objects, see the Open MCT Web Developer Guide.)
+
+In the case of our to-do list feature, the to-do list itself is the thing we’ll
+want users to be able to create and edit. So, we will add that as a new type in
+our bundle definition:
+
+ {
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ ++ "types": [
+ ++ {
+ ++ "key": "example.todo",
+ ++ "name": "To-Do List",
+ ++ "glyph": "j",
+ ++ "description": "A list of things that need to be done.",
+ ++ "features": ["creation"]
+ ++ }
+ ]
+ }
+ }
+__tutorials/todo/bundle.json__
+
+What have we done here? We’ve stated that this bundle includes extensions of the
+category _types_, which is used to describe domain object types. Then, we’ve
+included a definition for one such extension, which is the to-do list object.
+
+Going through the properties we’ve defined:
+
+* The `key` of `example.todo` will be stored as the machine-readable name for
+domain objects of this type.
+* The `name` of “To-Do List” is the human-readable name for this type, and will
+be shown to users.
+* The `glyph` refers to a special character in Open MCT Web’s custom font set;
+this will be used as an icon.
+* The `description` is also human-readable, and will be used whenever a longer
+explanation of what this type is should be shown.
+* Finally, the `features` property describes some special features of objects of
+this type. Including `creation` here means that we want users to be able to
+create this (in other cases, we may wish to expose things as domain objects
+which aren’t user-created, in which case we would omit this.)
+
+If we reload Open MCT Web, we see that our new domain object type appears in the
+Create menu:
+
+![To-Do List](images/todo.png)
+
+At this point, our to-do list doesn’t do much of anything; we can create them
+and give them names, but they don’t have any specific functionality attached,
+because we haven’t defined any yet.
+
+### Step 3-Add a View
+
+In order to allow a to-do list to be used, we need to define and display its
+contents. In Open MCT Web, the pattern that the user expects is that they’ll
+click on an object in the left-hand tree, and see a visualization of it to the
+right; in Open MCT Web, these visualizations are called views.
+A view in Open MCT Web is defined by an Angular template. We’ll add that in the
+directory `tutorials/todo/res/templates` (`res` is, by default, the directory
+where bundle-related resources are kept, and `templates` is where HTML templates
+are stored by convention.)
+
+ <div>
+ <a href="">All</a>
+ <a href="">Incomplete</a>
+ <a href="">Complete</a>
+ </div>
+
+ <ul>
+ <li ng-repeat="task in model.tasks">
+ <input type="checkbox" ng-checked="task.completed">
+ {{task.description}}
+ </li>
+ </ul>
+__tutorials/todo/res/templates/todo.html__
+
+A summary of what’s included:
+
+* At the top, we have some buttons that we will later wire in to allow the user
+to filter down to either complete or incomplete tasks.
+* After that, we have a list of tasks. The scope variable `model` is the model
+of the domain object being viewed; this contains all of the persistent state
+associated with that object. This model is effectively just a JSON document, so
+we can choose what goes into it (so long as we take care not to collide with
+platform-defined properties; see the Open MCT Web Developer Guide.) Here, we
+assume that all tasks will be stored in a property `tasks`, and that each will be
+an object containing a `description` (the readable summary of the task) and a
+boolean `completed` flag.
+
+To expose this view in Open MCT Web, we need to declare it in our bundle
+definition:
+
+ {
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ "types": [
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "glyph": "j",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"]
+ }
+ ],
+ ++ "views": [
+ ++ {
+ ++ "key": "example.todo",
+ ++ "type": "example.todo",
+ ++ "glyph": "j",
+ ++ "name": "List",
+ ++ "templateUrl": "templates/todo.html"
+ ++ }
+ ++ ]
+ }
+ }
+__tutorials/todo/bundle.json__
+
+Here, we’ve added another extension, this time belonging to category `views`. It
+contains the following properties:
+
+* Its `key` is its machine-readable name; we’ve given it the same name here as
+the domain object type, but could have chosen any unique name.
+
+* The `type` property tells Open MCT Web that this view is only applicable to
+domain objects of that type. This means that we’ll see this view for To-do Lists
+that we create, but not for other domain objects (such as Folders.)
+
+* The `glyph` and `name` properties describe the icon and human-readable name
+for this view to display in the UI where needed (if multiple views are available
+for To-do Lists, the user will be able to choose one.)
+
+* Finally, the `templateUrl` points to the Angular template we wrote; this path is
+relative to the bundle’s `res` folder.
+
+This template looks like it should display tasks, but we don’t have any way for
+the user to create these yet. As a temporary workaround to test the view, we
+will specify an initial state for To-do List domain object models in the
+definition of that type.
+
+ {
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ "types": [
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "glyph": "j",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"],
+ ++ "model": {
+ ++ "tasks": [
+ ++ { "description": "Add a type", "completed": true },
+ ++ { "description": "Add a view" }
+ ++ ]
+ }
+ }
+ ],
+ "views": [
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "glyph": "j",
+ "name": "List",
+ "templateUrl": "templates/todo.html"
+ }
+ ]
+ }
+ }
+__tutorials/todo/bundle.json__
+
+Now, when To-do List objects are created in Open MCT Web, they will initially
+have the state described by that model property.
+
+If we reload Open MCT Web, create a To-do List, and navigate to it in the tree,
+we should now see:
+
+![To-Do List](images/todo-list.png)
+
+This looks roughly like what we want. We’ll handle styling later, so let’s work
+on adding functionality. Currently, the filter choices do nothing, and while the
+checkboxes can be checked/unchecked, we’re not actually making the changes in
+the domain object - if we click over to My Items and come back to our
+To-Do List, for instance, we’ll see that those check boxes have returned to
+their initial state.
+
+### Step 4-Add a Controller
+
+We need to do some scripting to add dynamic behavior to that view. In
+particular, we want to:
+
+* Filter by complete/incomplete status.
+* Change the completion state of tasks in the model.
+
+To do this, we will support this by adding an Angular controller. (See
+[https://docs.angularjs.org/guide/controller]() for an overview of controllers.)
+We will define that in an AMD module (see [http://requirejs.org/docs/whyamd.html]())
+in the directory `tutorials/todo/src/controllers` (`src` is, by default, the
+directory where bundle-related source code is kept, and controllers is where
+Angular controllers are stored by convention.)
+
+ define(function () {
+ function TodoController($scope) {
+ var showAll = true,
+ showCompleted;
+
+ // Persist changes made to a domain object's model
+ function persist() {
+ var persistence =
+ $scope.domainObject.getCapability('persistence');
+ return persistence && persistence.persist();
+ }
+
+ // Change which tasks are visible
+ $scope.setVisibility = function (all, completed) {
+ showAll = all;
+ showCompleted = completed;
+ };
+
+ // Toggle the completion state of a task
+ $scope.toggleCompletion = function (taskIndex) {
+ $scope.domainObject.useCapability('mutation', function (model) {
+ var task = model.tasks[taskIndex];
+ task.completed = !task.completed;
+ });
+ persist();
+ };
+
+ // Check whether a task should be visible
+ $scope.showTask = function (task) {
+ return showAll || (showCompleted === !!(task.completed));
+ };
+ }
+
+ return TodoController;
+ });
+__tutorials/todo/src/controllers/TodoController.js__
+
+Here, we’ve defined three new functions and placed them in our `$scope`, which
+will make them available from the template:
+
+* `setVisibility` changes which tasks are meant to be visible. The first argument
+is a boolean, which, if true, means we want to show everything; the second
+argument is the completion state we want to show (which is only relevant if the
+first argument is falsy.)
+
+* `toggleCompletion` changes whether or not a task is complete. We make the
+change via the domain object’s `mutation` capability, and then persist the
+change via its `persistence` capability. See the Open MCT Web Developer Guide
+for more information on these capabilities.
+
+* `showTask` is meant to be used to help decide if a task should be shown, based
+on the current visibility settings. It is true when we have decided to show
+everything, or when the completion state matches the state we’ve chosen. (Note
+the use of the double-not !! to coerce the completed flag to a boolean, for
+equality testing.)
+
+Note that these functions make reference to `$scope.domainObject;` this is the
+domain object being viewed, which is passed into the scope by Open MCT Web
+prior to our template being utilized.
+
+On its own, this controller merely exposes these functions; the next step is to
+use them from our template:
+
+ ++ <div ng-controller="TodoController">
+ <div>
+ ++ <a ng-click="setVisibility(true)">All</a>
+ ++ <a ng-click="setVisibility(false, false)">Incomplete</a>
+ ++ <a ng-click="setVisibility(false, true)">Complete</a>
+ </div>
+
+ <ul>
+ <li ng-repeat="task in model.tasks"
+ ++ ng-if="showTask(task)">
+ <input type="checkbox"
+ ng-checked="task.completed"
+ ++ ng-click="toggleCompletion($index)">
+ {{task.description}}
+ </li>
+ </ul>
+ ++ </div>
+__tutorials/todo/res/templates/todo.html__
+
+Summary of changes here:
+
+* First, we surround everything in a `div` which we use to utilize our
+`TodoController`. This `div` will also come in handy later for styling.
+* From our filters at the top, we change the visibility settings when a different
+option is clicked.
+* When showing tasks, we check with `showTask` to see if the task matches current
+filter settings.
+* Finally, when the checkbox for a task is clicked, we make the change in the
+model via `toggleCompletion`.
+
+If we were to try to run at this point, we’d run into problems because the
+`TodoController` has not been registered with Angular. We need to first declare
+it in our bundle definition, as an extension of category `controllers`:
+
+ {
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ "types": [
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "glyph": "j",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"],
+ "model": {
+ "tasks": [
+ { "description": "Add a type", "completed": true },
+ { "description": "Add a view" }
+ ]
+ }
+ }
+ ],
+ "views": [
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "glyph": "j",
+ "name": "List",
+ "templateUrl": "templates/todo.html"
+ }
+ ],
+ + "controllers": [
+ + {
+ + "key": "TodoController",
+ + "implementation": "controllers/TodoController.js",
+ + "depends": [ "$scope" ]
+ + }
+ + ]
+ }
+ }
+__tutorials/todo/bundle.json__
+
+In this extension definition we have:
+
+* A `key`, which again is a machine-readable identifier. This is the name that
+templates will reference.
+* An `implementation`, which refers to an AMD module. The path is relative to the
+`src` directory within the bundle.
+* The `depends` property declares the dependencies of this controller. Here, we
+want Angular to inject `$scope`, the current Angular scope (which, going back
+to our controller, is expected as our first argument.)
+
+If we reload the browser now, our To-do List looks much the same, but now we are
+able to filter down the visible list, and the changes we make will stick around
+if we go to My Items and come back.
+
+
+### Step 5-Support Editing
+
+We now have a somewhat-functional view of our To-Do List, but we’re still
+missing some important functionality: Adding and removing tasks!
+
+This is a good place to discuss the user interface style of Open MCT Web. Open
+MCT Web draws a distinction between “using” and “editing” a domain object; in
+general, you can only make changes to a domain object while in Edit mode, which
+is reachable from the button with a pencil icon. This distinction helps users
+keep these tasks separate.
+
+The distinction between “using” and “editing” may vary depending on what domain
+objects or views are being used. While it may be convenient for a developer to
+think of “editing” as “any changes made to a domain object,” in practice some of
+these activities will be thought of as “using.”
+
+For this tutorial we’ll consider checking/unchecking tasks as “using” To-Do
+Lists, and adding/removing tasks as “editing.” We’ve already implemented the
+“using” part, in this case, so let’s focus on editing.
+
+There are two new pieces of functionality we’ll want out of this step:
+
+* The ability to add new tasks.
+* The ability to remove existing tasks.
+
+An Editing user interface is typically handled in a tool bar associated with a
+view. The contents of this tool bar are defined declaratively in a view’s
+extension definition.
+
+ {
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ "types": [
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "glyph": "j",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"],
+ "model": {
+ "tasks": [
+ { "description": "Add a type", "completed": true },
+ { "description": "Add a view" }
+ ]
+ }
+ }
+ ],
+ "views": [
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "glyph": "j",
+ "name": "List",
+ "templateUrl": "templates/todo.html",
+ + "toolbar": {
+ + "sections": [
+ + {
+ + "items": [
+ + {
+ + "text": "Add Task",
+ + "glyph": "+",
+ + "method": "addTask",
+ + "control": "button"
+ + }
+ + ]
+ + },
+ + {
+ + "items": [
+ + {
+ + "glyph": "Z",
+ + "method": "removeTask",
+ + "control": "button"
+ + }
+ + ]
+ + }
+ + ]
+ + }
+ }
+ ],
+ "controllers": [
+ {
+ "key": "TodoController",
+ "implementation": "controllers/TodoController.js",
+ "depends": [ "$scope" ]
+ }
+ ]
+ }
+ }
+__tutorials/todo/bundle.json__
+
+What we’ve stated here is that the To-Do List’s view will have a toolbar which
+contains two sections (which will be visually separated by a divider), each of
+which contains one button. The first is a button labelled “Add Task” that will
+invoke an `addTask` method; the second is a button with a glyph (which will appear
+as a trash can in Open MCT Web’s custom font set) which will invoke a `removeTask`
+method. For more information on forms and tool bars in Open MCT Web, see the
+Open MCT Web Developer Guide.
+
+If we reload and run Open MCT Web, we won’t see any tool bar when we switch over
+to Edit mode. This is because the aforementioned methods are expected to be
+found on currently-selected elements; we haven’t done anything with selections
+in our view yet, so the Open MCT Web platform will filter this tool bar down to
+all the applicable controls, which means no controls at all.
+
+To support selection, we will need to make some changes to our controller:
+
+ define(function () {
+ + // Form to display when adding new tasks
+ + var NEW_TASK_FORM = {
+ + name: "Add a Task",
+ + sections: [{
+ + rows: [{
+ + name: 'Description',
+ + key: 'description',
+ + control: 'textfield',
+ + required: true
+ + }]
+ + }]
+ + };
+
+ + function TodoController($scope, dialogService) {
+ var showAll = true,
+ showCompleted;
+
+ // Persist changes made to a domain object's model
+ function persist() {
+ var persistence =
+ $scope.domainObject.getCapability('persistence');
+ return persistence && persistence.persist();
+ }
+
+ + // Remove a task
+ + function removeTaskAtIndex(taskIndex) {
+ + $scope.domainObject.useCapability('mutation', function
+ + (model) {
+ + model.tasks.splice(taskIndex, 1);
+ + });
+ + persist();
+ + }
+
+ + // Add a task
+ + function addNewTask(task) {
+ + $scope.domainObject.useCapability('mutation', function
+ + (model) {
+ + model.tasks.push(task);
+ + });
+ + persist();
+ + }
+
+ // Change which tasks are visible
+ $scope.setVisibility = function (all, completed) {
+ showAll = all;
+ showCompleted = completed;
+ };
+
+ // Toggle the completion state of a task
+ $scope.toggleCompletion = function (taskIndex) {
+ $scope.domainObject.useCapability('mutation', function (model) {
+ var task = model.tasks[taskIndex];
+ task.completed = !task.completed;
+ });
+ persist();
+ };
+
+ // Check whether a task should be visible
+ $scope.showTask = function (task) {
+ return showAll || (showCompleted === !!(task.completed));
+ };
+
+ // Handle selection state in edit mode
+ + if ($scope.selection) {
+ + // Expose the ability to select tasks
+ + $scope.selectTask = function (taskIndex) {
+ + $scope.selection.select({
+ + removeTask: function () {
+ + removeTaskAtIndex(taskIndex);
+ + $scope.selection.deselect();
+ + }
+ + });
+ + };
+
+ + // Expose a view-level selection proxy
+ + $scope.selection.proxy({
+ + addTask: function () {
+ + dialogService.getUserInput(NEW_TASK_FORM, {})
+ + .then(addNewTask);
+ + }
+ + });
+ + }
+ }
+
+ return TodoController;
+ });
+__tutorials/todo/src/controllers/TodoController.js__
+
+There are a few changes to pay attention to here. Let’s review them:
+
+* At the top, we describe the form that should be shown to the user when they
+click the _Add Task_ button. This form is described declaratively, and populates
+an object that has the same format as tasks in the `tasks` array of our
+To-Do List’s model.
+* We’ve added an argument to the `TodoController`: The `dialogService`, which is
+exposed by the Open MCT Web platform to handle showing dialogs.
+* Some utility functions for handling the actual adding and removing of tasks.
+These use the `mutation` capability to modify the tasks in the To-Do List’s
+model.
+* Finally, we check for the presence of a `selection` object in our scope. This
+object is provided by Edit mode to manage current selections for editing. When
+it is present, we expose a `selectTask` function to our scope to allow selecting
+individual tasks; when this occurs, we expose an object to `selection` which has
+a `removeTask` method, as expected by the tool bar we’ve defined. We additionally
+expose a view proxy, to handle view-level changes (e.g. not associated with any
+specific selected object); this has an `addTask` method, which again is expected
+by the tool bar we’ve defined.
+
+Additionally, we need to make changes to our template to select specific tasks
+in response to some user gesture. Here, we will select tasks when a user clicks
+the description.
+
+ <div ng-controller="TodoController">
+ <div>
+ <a ng-click="setVisibility(true)">All</a>
+ <a ng-click="setVisibility(false, false)">Incomplete</a>
+ <a ng-click="setVisibility(false, true)">Complete</a>
+ </div>
+
+ <ul>
+ <li ng-repeat="task in model.tasks"
+ ng-if="showTask(task)">
+ <input type="checkbox"
+ ng-checked="task.completed"
+ ng-click="toggleCompletion($index)">
+ + <span ng-click="selectTask($index)">
+ {{task.description}}
+ + </span>
+ </li>
+ </ul>
+ </div>
+__tutorials/todo/res/templates/todo.html__
+
+Finally, the `TodoController` uses the `dialogService` now, so we need to
+declare that dependency in its extension definition:
+
+ {
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ "types": [
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "glyph": "j",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"],
+ "model": {
+ "tasks": [
+ { "description": "Add a type", "completed": true },
+ { "description": "Add a view" }
+ ]
+ }
+ }
+ ],
+ "views": [
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "glyph": "j",
+ "name": "List",
+ "templateUrl": "templates/todo.html",
+ "toolbar": {
+ "sections": [
+ {
+ "items": [
+ {
+ "text": "Add Task",
+ "glyph": "+",
+ "method": "addTask",
+ "control": "button"
+ }
+ ]
+ },
+ {
+ "items": [
+ {
+ "glyph": "Z",
+ "method": "removeTask",
+ "control": "button"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "controllers": [
+ {
+ "key": "TodoController",
+ "implementation": "controllers/TodoController.js",
+ + "depends": [ "$scope", "dialogService" ]
+ }
+ ]
+ }
+ }
+__tutorials/todo/bundle.json__
+
+If we now reload Open MCT Web, we’ll be able to see the new functionality we’ve
+added. If we Create a new To-Do List, navigate to it, and click the button with
+the Pencil icon in the top-right, we’ll be in edit mode. We see, first, that our
+“Add Task” button appears in the tool bar:
+
+![Edit](images/todo-edit.png)
+
+If we click on this, we’ll get a dialog allowing us to add a new task:
+
+![Add task](images/add-task.png)
+
+Finally, if we click on the description of a specific task, we’ll see a new
+button appear, which we can then click on to remove that task:
+
+![Remove task](images/remove-task.png)
+
+As always in Edit mode, the user will be able to Save or Cancel any changes they have made.
+In terms of functionality, our To-Do List can do all the things we want, but the appearance is still lacking. In particular, we can’t distinguish our current filter choice or our current selection state.
+
+### Step 6-Customizing Look and Feel
+
+In this section, our goal is to:
+
+* Display the current filter choice.
+* Display the current task selection (when in Edit mode.)
+* Tweak the general aesthetics to our liking.
+* Get rid of those default tasks (we can create our own now.)
+
+To support the first two, we’ll need to expose some methods for checking these
+states in the controller:
+
+
+ define(function () {
+ // Form to display when adding new tasks
+ var NEW_TASK_FORM = {
+ name: "Add a Task",
+ sections: [{
+ rows: [{
+ name: 'Description',
+ key: 'description',
+ control: 'textfield',
+ required: true
+ }]
+ }]
+ };
+
+ function TodoController($scope, dialogService) {
+ var showAll = true,
+ showCompleted;
+
+ // Persist changes made to a domain object's model
+ function persist() {
+ var persistence =
+ $scope.domainObject.getCapability('persistence');
+ return persistence && persistence.persist();
+ }
+
+ // Remove a task
+ function removeTaskAtIndex(taskIndex) {
+ $scope.domainObject.useCapability('mutation', function (model) {
+ model.tasks.splice(taskIndex, 1);
+ });
+ persist();
+ }
+
+ // Add a task
+ function addNewTask(task) {
+ $scope.domainObject.useCapability('mutation', function (model) {
+ model.tasks.push(task);
+ });
+ persist();
+ }
+
+ // Change which tasks are visible
+ $scope.setVisibility = function (all, completed) {
+ showAll = all;
+ showCompleted = completed;
+ };
+
+ + // Check if current visibility settings match
+ + $scope.checkVisibility = function (all, completed) {
+ + return showAll ? all : (completed === showCompleted);
+ + };
+
+ // Toggle the completion state of a task
+ $scope.toggleCompletion = function (taskIndex) {
+ $scope.domainObject.useCapability('mutation', function (model) {
+ var task = model.tasks[taskIndex];
+ task.completed = !task.completed;
+ });
+ persist();
+ };
+
+ // Check whether a task should be visible
+ $scope.showTask = function (task) {
+ return showAll || (showCompleted === !!(task.completed));
+ };
+
+ // Handle selection state in edit mode
+ if ($scope.selection) {
+ // Expose the ability to select tasks
+ $scope.selectTask = function (taskIndex) {
+ $scope.selection.select({
+ removeTask: function () {
+ removeTaskAtIndex(taskIndex);
+ $scope.selection.deselect();
+ },
+ + taskIndex: taskIndex
+ });
+ };
+
+ + // Expose a check for current selection state
+ + $scope.isSelected = function (taskIndex) {
+ + return ($scope.selection.get() || {}).taskIndex ===
+ + taskIndex;
+ + };
+
+ // Expose a view-level selection proxy
+ $scope.selection.proxy({
+ addTask: function () {
+ dialogService.getUserInput(NEW_TASK_FORM, {})
+ .then(addNewTask);
+ }
+ });
+ }
+ }
+
+ return TodoController;
+ });
+__tutorials/todo/src/controllers/TodoController.js__
+
+A summary of these changes:
+
+* `checkVisibility` has the same arguments as `setVisibility`, but instead of
+making a change, it simply returns a boolean true/false indicating whether those
+settings are in effect. The logic reflects the fact that the second parameter is
+ignored when showing all.
+* To support checking for selection, the index of the currently-selected task is
+tracked as part of the selection object.
+* Finally, an isSelected function is exposed which checks if the indicated task
+is currently selected, using the index from above.
+
+Additionally, we will want to define some CSS rules in order to reflect these
+states visually, and to generally improve the appearance of our view. We add
+another file to the res directory of our bundle; this time, it is `css/todo.css`
+(with the `css` directory again being a convention.)
+
+ .example-todo div.example-button-group {
+ margin-top: 12px;
+ margin-bottom: 12px;
+ }
+
+ .example-todo .example-button-group a {
+ padding: 3px;
+ margin: 3px;
+ }
+
+ .example-todo .example-button-group a.selected {
+ border: 1px gray solid;
+ border-radius: 3px;
+ background: #444;
+ }
+
+ .example-todo .example-task-completed .example-task-description {
+ text-decoration: line-through;
+ opacity: 0.75;
+ }
+
+ .example-todo .example-task-description.selected {
+ background: #46A;
+ border-radius: 3px;
+ }
+
+ .example-todo .example-message {
+ font-style: italic;
+ }
+__tutorials/todo/res/css/todo.css__
+
+Here, we have defined classes and appearances for:
+
+* Our filter choosers (`example-button-group`).
+* Our selected and/or completed tasks (`example-task-description`).
+* A message, which we will add next, to display when there are no tasks
+(`example-message`).
+
+To include this CSS file in our running instance of Open MCT Web, we need to
+declare it in our bundle definition, this time as an extension of category
+`stylesheets`:
+
+ {
+ "name": "To-do Plugin",
+ "description": "Allows creating and editing to-do lists.",
+ "extensions": {
+ "types": [
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "glyph": "j",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"],
+ "model": {
+ "tasks": []
+ }
+ }
+ ],
+ "views": [
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "glyph": "j",
+ "name": "List",
+ "templateUrl": "templates/todo.html",
+ "toolbar": {
+ "sections": [
+ {
+ "items": [
+ {
+ "text": "Add Task",
+ "glyph": "+",
+ "method": "addTask",
+ "control": "button"
+ }
+ ]
+ },
+ {
+ "items": [
+ {
+ "glyph": "Z",
+ "method": "removeTask",
+ "control": "button"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "controllers": [
+ {
+ "key": "TodoController",
+ "implementation": "controllers/TodoController.js",
+ "depends": [ "$scope", "dialogService" ]
+ }
+ ],
+ + "stylesheets": [
+ + {
+ + "stylesheetUrl": "css/todo.css"
+ + }
+ + ]
+ }
+ }
+__tutorials/todo/bundle.json__
+
+Note that we’ve also removed our placeholder tasks from the `model` of the
+To-Do List’s type above; now To-Do Lists will start off empty.
+
+Finally, let’s utilize these changes from our view’s template:
+
+ + <div ng-controller="TodoController" class="example-todo">
+ + <div class="example-button-group">
+ + <a ng-class="{ selected: checkVisibility(true) }"
+ ng-click="setVisibility(true)">All</a>
+ + <a ng-class="{ selected: checkVisibility(false, false) }"
+ ng-click="setVisibility(false, false)">Incomplete</a>
+ + <a ng-class="{ selected: checkVisibility(false, true) }"
+ ng-click="setVisibility(false, true)">Complete</a>
+ </div>
+
+ <ul>
+ <li ng-repeat="task in model.tasks"
+ + ng-class="{ 'example-task-completed': task.completed }"
+ ng-if="showTask(task)">
+ <input type="checkbox"
+ ng-checked="task.completed"
+ ng-click="toggleCompletion($index)">
+ <span ng-click="selectTask($index)"
+ + ng-class="{ selected: isSelected($index) }"
+ + class="example-task-description">
+ {{task.description}}
+ </span>
+ </li>
+ </ul>
+ + <div ng-if="model.tasks.length < 1" class="example-message">
+ + There are no tasks to show.
+ + </div>
+ + </div>
+__tutorials/todo/res/templates/todo.html__
+
+Now, if we reload our page and create a new To-Do List, we will initially see:
+
+![Todo Restyled](images/todo-restyled.png)
+
+If we then go into Edit mode, add some tasks, and select one, it will now be
+much clearer what the current selection is (e.g. before we hit the remove button
+in the toolbar):
+
+![Todo Restyled](images/todo-selection.png)
+
+## Bar Graph
+
+In this tutorial, we will look at creating a bar graph plugin for visualizing
+telemetry data. Specifically, we want some bars that raise and lower to match
+the observed state of real-time telemetry; this is particularly useful for
+monitoring things like battery charge levels.
+It is recommended that the reader completes (or is familiar with) the To-Do
+List tutorial before completing this tutorial, as certain concepts discussed
+there will be addressed in more brevity here.
+
+### Step 1-Define the View
+
+Since the goal is to introduce a new view and expose it from a plugin, we will
+want to create a new bundle which declares an extension of category `views`.
+We’ll also be defining some custom styles, so we’ll include that extension as
+well. We’ll be creating this plugin in `tutorials/bargraph`, so our initial
+bundle definition looks like:
+
+ {
+ "name": "Bar Graph",
+ "description": "Provides the Bar Graph view of telemetry elements.",
+ "extensions": {
+ "views": [
+ {
+ "name": "Bar Graph",
+ "key": "example.bargraph",
+ "glyph": "H",
+ "templateUrl": "templates/bargraph.html",
+ "needs": [ "telemetry" ],
+ "delegation": true
+ }
+ ],
+ "stylesheets": [
+ {
+ "stylesheetUrl": "css/bargraph.css"
+ }
+ ]
+ }
+ }
+__tutorials/bargraph/bundle.json__
+
+The view definition should look familiar after the To-Do List tutorial, with
+some additions:
+
+* The `needs` property indicates that this view is only applicable to domain
+objects with a `telemetry` capability. This ensures that this view is available
+for telemetry points, but not for other objects (like folders.)
+* The `delegation` property indicates that the above constraint can be satisfied
+via capability delegation; that is, by domain objects which delegate the
+`telemetry` capability to their contained objects. This allows this view to be
+used for Telemetry Panel objects as well as for individual telemetry-providing
+domain objects.
+
+For this tutorial, we’ll assume that we’ve sketched out our template and CSS
+file ahead of time to describe the general look we want for the view. These
+look like:
+
+ <div class="example-bargraph">
+ <div class="example-tick-labels">
+ <div class="example-tick-label" style="bottom: 0%">High</div>
+ <div class="example-tick-label" style="bottom: 50%">Middle</div>
+ <div class="example-tick-label" style="bottom: 100%">Low</div>
+ </div>
+
+ <div class="example-graph-area">
+ <div style="left: 0; width: 33.3%;" class="example-bar-holder">
+ <div class="example-bar" style="top: 25%; bottom: 50%;">
+ </div>
+ </div>
+ <div style="left: 33.3%; width: 33.3%;" class="example-bar-holder">
+ <div class="example-bar" style="top: 40%; bottom: 10%;">
+ </div>
+ </div>
+ <div style="left: 66.6%; width: 33.3%;" class="example-bar-holder">
+ <div class="example-bar" style="top: 30%; bottom: 40%;">
+ </div>
+ </div>
+ <div style="bottom: 50%" class="example-graph-tick">
+ </div>
+ </div>
+
+ <div class="example-bar-labels">
+ <div style="left: 0; width: 33.3%;"
+ class="example-bar-holder example-label">
+ Label A
+ </div>
+ <div style="left: 33.3%; width: 33.3%;"
+ class="example-bar-holder example-label">
+ Label B
+ </div>
+ <div style="left: 66.6%; width: 33.3%;"
+ class="example-bar-holder example-label">
+ Label C
+ </div>
+ </div>
+ </div>
+__tutorials/bargraph/res/templates/bargraph.html__
+
+Here, three regions are defined. The first will be for tick labels along the
+vertical axis, showing the numeric value that certain heights correspond to. The
+second will be for the actual bar graphs themselves; three are included here.
+The third is for labels along the horizontal axis, which will indicate which
+bar corresponds to which telemetry point. Inline `style` attributes are used
+wherever dynamic positioning (handled by a script) is anticipated.
+The corresponding CSS file which styles and positions these elements:
+
+ .example-bargraph {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ mid-width: 160px;
+ min-height: 160px;
+ }
+
+ .example-bargraph .example-tick-labels {
+ position: absolute;
+ left: 0;
+ top: 24px;
+ bottom: 32px;
+ width: 72px;
+ font-size: 75%;
+ }
+
+ .example-bargraph .example-tick-label {
+ position: absolute;
+ right: 0;
+ height: 1em;
+ margin-bottom: -0.5em;
+ padding-right: 6px;
+ text-align: right;
+ }
+
+ .example-bargraph .example-graph-area {
+ position: absolute;
+ border: 1px gray solid;
+ left: 72px;
+ top: 24px;
+ bottom: 32px;
+ right: 0;
+ }
+
+ .example-bargraph .example-bar-labels {
+ position: absolute;
+ left: 72px;
+ bottom: 0;
+ right: 0;
+ height: 32px;
+ }
+
+ .example-bargraph .example-bar-holder {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ }
+
+ .example-bargraph .example-graph-tick {
+ position: absolute;
+ width: 100%;
+ height: 1px;
+ border-bottom: 1px gray dashed;
+ }
+
+ .example-bargraph .example-bar {
+ position: absolute;
+ background: darkcyan;
+ right: 4px;
+ left: 4px;
+ }
+
+ .example-bargraph .example-label {
+ text-align: center;
+ font-size: 85%;
+ padding-top: 6px;
+ }
+__tutorials/bargraph/res/css/bargraph.css__
+
+This is already enough that, if we add `“tutorials/bargraph”` to `bundles.json`,
+we should be able to run Open MCT Web and see our Bar Graph as an available view
+for domain objects which provide telemetry (such as the example
+_Sine Wave Generator_) as well as for _Telemetry Panel_ objects:
+
+![Bar Plot](images/bar-plot.png)
+
+This means that our remaining work will be to populate and position these
+elements based on the actual contents of the domain object.
+
+### Step 2-Add a Controller
+
+Our next step will be to begin dynamically populating this template’s contents.
+Specifically, our goals for this step will be to:
+
+* Show one bar per telemetry-providing domain object (for which we’ll be getting
+actual telemetry data in subsequent steps.)
+* Show correct labels for these objects at the bottom.
+* Show numeric labels on the left-hand side.
+
+Notably, we will not try to show telemetry data after this step.
+
+To support this, we will add a new controller which supports our Bar Graph view:
+
+ define(function () {
+ function BarGraphController($scope, telemetryHandler) {
+ var handle;
+
+ // Add min/max defaults
+ $scope.low = -1;
+ $scope.middle = 0;
+ $scope.high = 1;
+
+ // Convert value to a percent between 0-100, keeping values in points
+ $scope.toPercent = function (value) {
+ var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low);
+ return Math.min(100, Math.max(0, pct));
+ };
+
+ // Use the telemetryHandler to get telemetry objects here
+ handle = telemetryHandler.handle($scope.domainObject, function () {
+ $scope.telemetryObjects = handle.getTelemetryObjects();
+ $scope.barWidth =
+ 100 / Math.max(($scope.telemetryObjects).length, 1);
+ });
+
+ // Release subscriptions when scope is destroyed
+ $scope.$on('$destroy', handle.unsubscribe);
+ }
+
+ return BarGraphController;
+ });
+__tutorials/bargraph/src/controllers/BarGraphController.js__
+
+A summary of what we’ve done here:
+
+* We’re exposing some numeric values that will correspond to the _low_, _middle_,
+and _high_ end of the graph. (The `medium` attribute will be useful for
+positioning the middle line, which are graphs will ultimately descend down or
+push up from.)
+* Add a utility function which converts from numeric values to percentages. This
+will help support some positioning in the template.
+* Utilize the `telemetryHandler`, provided by the platform, to start listening
+to real-time telemetry updates. This will deal with most of the complexity of
+dealing with telemetry (e.g. differentiating between individual telemetry points
+and telemetry panels, monitoring latest values) and provide us with a useful
+interface for populating our view. The the Open MCT Web Developer Guide for more
+information on dealing with telemetry.
+
+Whenever the telemetry handler invokes its callbacks, we update the set of
+telemetry objects in view, as well as the width for each bar.
+
+We will also utilize this from our template:
+
+ <div class="example-bargraph">
+ <div class="example-tick-labels">
+ + <div ng-repeat="value in [low, middle, high] track by $index"
+ + class="example-tick-label"
+ + style="bottom: {{ toPercent(value) }}%">
+ + {{value}}
+ + </div>
+ </div>
+
+ <div class="example-graph-area">
+ + <div ng-repeat="telemetryObject in telemetryObjects"
+ + style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
+ + class="example-bar-holder">
+ <div class="example-bar"
+ style="top: 25%; bottom: 50%;">
+ </div>
+ + </div>
+ + <div style="bottom: {{ toPercent(middle) }}%"
+ class="example-graph-tick">
+ </div>
+ </div>
+
+ <div class="example-bar-labels">
+ + <div ng-repeat="telemetryObject in telemetryObjects"
+ + style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
+ + class="example-bar-holder example-label">
+ + <mct-representation key="'label'"
+ + mct-object="telemetryObject">
+ + </mct-representation>
+ + </div>
+ </div>
+ </div>
+__tutorials/bargraph/res/templates/bargraph.html__
+
+Summarizing these changes:
+
+* Utilize the exposed `low`, `middle`, and `high` values to populate our labels
+along the vertical axis. Additionally, use the `toPercent` function to position
+these from the bottom.
+* Replace our three hard-coded bars with a repeater that looks at the
+`telemetryObjects` exposed by the controller and adds one bar each.
+* Position the dashed tick-line using the `middle` value and the `toPercent`
+function, lining it up with its label to the left.
+* At the bottom, repeat a set of labels for the telemetry-providing domain
+objects, with matching alignment to the bars above. We use an existing
+representation, `label`, to make this easier.
+
+Finally, we expose our controller from our bundle definition. Note that the
+depends declaration includes both `$scope` as well as the `telemetryHandler`
+service we made use of.
+
+ {
+ "name": "Bar Graph",
+ "description": "Provides the Bar Graph view of telemetry elements.",
+ "extensions": {
+ "views": [
+ {
+ "name": "Bar Graph",
+ "key": "example.bargraph",
+ "glyph": "H",
+ "templateUrl": "templates/bargraph.html",
+ "needs": [ "telemetry" ],
+ "delegation": true
+ }
+ ],
+ "stylesheets": [
+ {
+ "stylesheetUrl": "css/bargraph.css"
+ }
+ ],
+ + "controllers": [
+ + {
+ + "key": "BarGraphController",
+ + "implementation": "controllers/BarGraphController.js",
+ + "depends": [ "$scope", "telemetryHandler" ]
+ + }
+ + ]
+ }
+ }
+__tutorials/bargraph/bundle.json__
+
+When we reload Open MCT Web, we are now able to see that our bar graph view
+correctly labels one bar per telemetry-providing domain object, as shown for
+this Telemetry Panel containing four Sine Wave Generators.
+
+![Bar Plot](images/bar-plot-2.png)
+
+### Step 3-Using Telemetry Data
+
+Now that our bar graph is labeled correctly, it’s time to start putting data
+into the view.
+
+First, let’s add expose some more functionality from our controller. To make it
+simple, we’ll expose the top and bottom for a bar graph for a given
+telemetry-providing domain object, as percentages.
+
+
+ define(function () {
+ function BarGraphController($scope, telemetryHandler) {
+ var handle;
+
+ // Add min/max defaults
+ $scope.low = -1;
+ $scope.middle = 0;
+ $scope.high = 1;
+
+ // Convert value to a percent between 0-100, keeping values in points
+ $scope.toPercent = function (value) {
+ var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low);
+ return Math.min(100, Math.max(0, pct));
+ };
+
+ // Get bottom and top (as percentages) for current value
+ + $scope.getBottom = function (telemetryObject) {
+ + var value = handle.getRangeValue(telemetryObject);
+ + return $scope.toPercent(Math.min($scope.middle, value));
+ + }
+ + $scope.getTop = function (telemetryObject) {
+ + var value = handle.getRangeValue(telemetryObject);
+ + return 100 - $scope.toPercent(Math.max($scope.middle, value));
+ + }
+
+ // Use the telemetryHandler to get telemetry objects here
+ handle = telemetryHandler.handle($scope.domainObject, function () {
+ $scope.telemetryObjects = handle.getTelemetryObjects();
+ $scope.barWidth =
+ 100 / Math.max(($scope.telemetryObjects).length, 1);
+ });
+
+ // Release subscriptions when scope is destroyed
+ $scope.$on('$destroy', handle.unsubscribe);
+ }
+
+ return BarGraphController;
+ });
+__tutorials/bargraph/src/controllers/BarGraphController.js__
+
+The `telemetryHandler` exposes a method to provide us with our latest data value
+(the `getRangeValue` method), and we already have a function to convert from a
+numeric value to a percentage within the view, so we just use those. The only
+slight complication is that we want our bar to move up or down from the middle
+value, so either of our top or bottom position for the bar itself could be
+either the middle line, or the data value. We let `Math.min` and `Math.max`
+decide this.
+
+Next, we utilize this functionality from the template:
+
+ <div class="example-bargraph">
+ <div class="example-tick-labels">
+ <div ng-repeat="value in [low, middle, high] track by $index"
+ class="example-tick-label"
+ style="bottom: {{ toPercent(value) }}%">
+ {{value}}
+ </div>
+ </div>
+
+ <div class="example-graph-area">
+ <div ng-repeat="telemetryObject in telemetryObjects"
+ style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
+ class="example-bar-holder">
+ <div class="example-bar"
+ + ng-style="{
+ + bottom: getBottom(telemetryObject) + '%',
+ + top: getTop(telemetryObject) + '%'
+ + }">
+ </div>
+ </div>
+ <div style="bottom: {{ toPercent(middle) }}%"
+ class="example-graph-tick">
+ </div>
+ </div>
+
+ <div class="example-bar-labels">
+ <div ng-repeat="telemetryObject in telemetryObjects"
+ style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
+ class="example-bar-holder example-label">
+ <mct-representation key="'label'"
+ mct-object="telemetryObject">
+ </mct-representation>
+ </div>
+ </div>
+ </div>
+__tutorials/bargraph/res/templates/bargraph.html__
+
+Here, we utilize the functions we just provided from the controller to position
+the bar, using an ng-style attribute.
+
+When we reload Open MCT Web, our bar graph view now looks like:
+
+![Bar Plot](images/bar-plot-3.png)
+
+### Step 4-View Configuration
+
+The default minimum and maximum values we’ve provided happen to make sense for
+sine waves, but what about other values? We want to provide the user with a
+means of configuring these boundaries.
+
+This is normally done via Edit mode. Since view configuration is a common
+problem, the Open MCT Web platform exposes a configuration object - called
+`configuration` - into our view’s scope. We can populate it as we please, and
+when we return to our view later, those changes will be persisted.
+
+First, let’s add a tool bar for changing these three values in Edit mode:
+
+ {
+ "name": "Bar Graph",
+ "description": "Provides the Bar Graph view of telemetry elements.",
+ "extensions": {
+ "views": [
+ {
+ "name": "Bar Graph",
+ "key": "example.bargraph",
+ "glyph": "H",
+ "templateUrl": "templates/bargraph.html",
+ "needs": [ "telemetry" ],
+ "delegation": true,
+ + "toolbar": {
+ + "sections": [
+ + {
+ + "items": [
+ + {
+ + "name": "Low",
+ + "property": "low",
+ + "required": true,
+ + "control": "textfield",
+ + "size": 4
+ + },
+ + {
+ + "name": "Middle",
+ + "property": "middle",
+ + "required": true,
+ + "control": "textfield",
+ + "size": 4
+ + },
+ + {
+ + "name": "High",
+ + "property": "high",
+ + "required": true,
+ + "control": "textfield",
+ + "size": 4
+ + }
+ + ]
+ + }
+ ]
+ }
+ }
+ ],
+ "stylesheets": [
+ {
+ "stylesheetUrl": "css/bargraph.css"
+ }
+ ],
+ "controllers": [
+ {
+ "key": "BarGraphController",
+ "implementation": "controllers/BarGraphController.js",
+ "depends": [ "$scope", "telemetryHandler" ]
+ }
+ ]
+ }
+ }
+__tutorials/bargraph/bundle.json__
+
+As we saw in to To-Do List plugin, a tool bar needs either a selected object or
+a view proxy to work from. We will add this to our controller, and additionally
+will start reading/writing those properties to the view’s `configuration`
+object.
+
+ define(function () {
+ function BarGraphController($scope, telemetryHandler) {
+ var handle;
+
+ + // Expose configuration constants directly in scope
+ + function exposeConfiguration() {
+ + $scope.low = $scope.configuration.low;
+ + $scope.middle = $scope.configuration.middle;
+ + $scope.high = $scope.configuration.high;
+ + }
+
+ + // Populate a default value in the configuration
+ + function setDefault(key, value) {
+ + if ($scope.configuration[key] === undefined) {
+ + $scope.configuration[key] = value;
+ + }
+ + }
+
+ + // Getter-setter for configuration properties (for view proxy)
+ + function getterSetter(property) {
+ + return function (value) {
+ + value = parseFloat(value);
+ + if (!isNaN(value)) {
+ + $scope.configuration[property] = value;
+ + exposeConfiguration();
+ + }
+ + return $scope.configuration[property];
+ + };
+ }
+
+ + // Add min/max defaults
+ + setDefault('low', -1);
+ + setDefault('middle', 0);
+ + setDefault('high', 1);
+ + exposeConfiguration($scope.configuration);
+
+ + // Expose view configuration options
+ + if ($scope.selection) {
+ + $scope.selection.proxy({
+ + low: getterSetter('low'),
+ + middle: getterSetter('middle'),
+ + high: getterSetter('high')
+ + });
+ + }
+
+ // Convert value to a percent between 0-100
+ $scope.toPercent = function (value) {
+ var pct = 100 * (value - $scope.low) /
+ ($scope.high - $scope.low);
+ return Math.min(100, Math.max(0, pct));
+ };
+
+ // Get bottom and top (as percentages) for current value
+ $scope.getBottom = function (telemetryObject) {
+ var value = handle.getRangeValue(telemetryObject);
+ return $scope.toPercent(Math.min($scope.middle, value));
+ }
+ $scope.getTop = function (telemetryObject) {
+ var value = handle.getRangeValue(telemetryObject);
+ return 100 - $scope.toPercent(Math.max($scope.middle, value));
+ }
+
+ // Use the telemetryHandler to get telemetry objects here
+ handle = telemetryHandler.handle($scope.domainObject, function () {
+ $scope.telemetryObjects = handle.getTelemetryObjects();
+ $scope.barWidth =
+ 100 / Math.max(($scope.telemetryObjects).length, 1);
+ });
+
+ // Release subscriptions when scope is destroyed
+ $scope.$on('$destroy', handle.unsubscribe);
+ }
+
+ return BarGraphController;
+ });
+__tutorials/bargraph/src/controllers/BarGraphController.js__
+
+A summary of these changes:
+
+* First, read `low`, `middle`, and `high` from the view configuration instead of
+initializing them to explicit values. This is placed into its own function,
+since it will be called a lot.
+* The function `setDefault` is included; it will be used to set the default
+values for `low`, `middle`, and `high` in the view configuration, but only if
+they aren’t present.
+* The tool bar will treat properties in a view proxy as getter-setters if
+they are functions; that is, they will be called with an argument to be used
+as a setter, and with no argument to use as a getter. We provide ourselves a
+function for making these getter-setters (since we’ll need three) that
+additionally handles some checking to ensure that these are actually numbers.
+* After that, we actually initialize both the view `configuration` object with
+defaults (if needed), and expose its state into the scope.
+* Finally, we expose a view proxy which will handle changes to `low`, `middle`,
+and `high` as entered by the user from the tool bar. This uses the
+getter-setters we defined previously.
+
+If we reload Open MCT Web and go to a Bar Graph view in Edit mode, we now see
+that we can change these bounds from the tool bar.
+
+![Bar plot](images/bar-plot-4.png)
+
+## Telemetry Adapter
+
+The goal of this tutorial is to demonstrate how to integrate Open MCT Web
+with an existing telemetry system.
+
+A summary of the steps we will take:
+
+* Expose the telemetry dictionary within the user interface.
+* Support subscription/unsubscription to real-time streaming data.
+* Support historical retrieval of telemetry data.
+
+### Step 0-Expose Your Telemetry
+
+As a precondition to integrating telemetry data into Open MCT Web, this
+information needs to be available over web-based interfaces. In practice,
+this will most likely mean exposing data over HTTP, or over WebSockets.
+For purposes of this tutorial, a simple node server is provided to stand
+in place of this existing telemetry system. It generates real-time data
+and exposes it over a WebSocket connection.
+
+
+ /*global require,process,console*/
+
+ var CONFIG = {
+ port: 8081,
+ dictionary: "dictionary.json",
+ interval: 1000
+ };
+
+ (function () {
+ "use strict";
+
+ var WebSocketServer = require('ws').Server,
+ fs = require('fs'),
+ wss = new WebSocketServer({ port: CONFIG.port }),
+ dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")),
+ spacecraft = {
+ "prop.fuel": 77,
+ "prop.thrusters": "OFF",
+ "comms.recd": 0,
+ "comms.sent": 0,
+ "pwr.temp": 245,
+ "pwr.c": 8.15,
+ "pwr.v": 30
+ },
+ histories = {},
+ listeners = [];
+
+ function updateSpacecraft() {
+ spacecraft["prop.fuel"] = Math.max(
+ 0,
+ spacecraft["prop.fuel"] -
+ (spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0)
+ );
+ spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985
+ + Math.random() * 0.25 + Math.sin(Date.now());
+ spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985;
+ spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3);
+ }
+
+ function generateTelemetry() {
+ var timestamp = Date.now(), sent = 0;
+ Object.keys(spacecraft).forEach(function (id) {
+ var state = { timestamp: timestamp, value: spacecraft[id] };
+ histories[id] = histories[id] || []; // Initialize
+ histories[id].push(state);
+ spacecraft["comms.sent"] += JSON.stringify(state).length;
+ });
+ listeners.forEach(function (listener) {
+ listener();
+ });
+ }
+
+ function update() {
+ updateSpacecraft();
+ generateTelemetry();
+ }
+
+ function handleConnection(ws) {
+ var subscriptions = {}, // Active subscriptions for this connection
+ handlers = { // Handlers for specific requests
+ dictionary: function () {
+ ws.send(JSON.stringify({
+ type: "dictionary",
+ value: dictionary
+ }));
+ },
+ subscribe: function (id) {
+ subscriptions[id] = true;
+ },
+ unsubscribe: function (id) {
+ delete subscriptions[id];
+ },
+ history: function (id) {
+ ws.send(JSON.stringify({
+ type: "history",
+ id: id,
+ value: histories[id]
+ }));
+ }
+ };
+
+ function notifySubscribers() {
+ Object.keys(subscriptions).forEach(function (id) {
+ var history = histories[id];
+ if (history) {
+ ws.send(JSON.stringify({
+ type: "data",
+ id: id,
+ value: history[history.length - 1]
+ }));
+ }
+ });
+ }
+
+ // Listen for requests
+ ws.on('message', function (message) {
+ var parts = message.split(' '),
+ handler = handlers[parts[0]];
+ if (handler) {
+ handler.apply(handlers, parts.slice(1));
+ }
+ });
+
+ // Stop sending telemetry updates for this connection when closed
+ ws.on('close', function () {
+ listeners = listeners.filter(function (listener) {
+ return listener !== notifySubscribers;
+ });
+ });
+
+ // Notify subscribers when telemetry is updated
+ listeners.push(notifySubscribers);
+ }
+
+ update();
+ setInterval(update, CONFIG.interval);
+
+ wss.on('connection', handleConnection);
+
+ console.log("Example spacecraft running on port ");
+ console.log("Press Enter to toggle thruster state.");
+ process.stdin.on('data', function (data) {
+ spacecraft['prop.thrusters'] =
+ (spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF";
+ console.log("Thrusters " + spacecraft["prop.thrusters"]);
+ });
+ }());
+__tutorial-server/app.js__
+
+For purposes of this tutorial, how this server has been implemented is
+not important; it has just enough functionality to resemble a WebSocket
+interface to a real telemetry system, and niceties such as error-handling
+have been omitted. (For more information on using WebSockets, both in the
+client and on the server,
+[https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API]() is an
+excellent starting point.)
+
+What does matter for this tutorial is the interfaces that are exposed. Once a
+WebSocket connection has been established to this server, it accepts plain text
+messages in the following formats, and issues JSON-formatted responses.
+
+The requests it handles are:
+
+* `dictionary`: Responds with a JSON response with the following fields:
+ * `type`: “dictionary”
+ * `value`: … the telemetry dictionary (see below) …
+* `subscribe <id>`: Subscribe to new telemetry data for the measurement with
+the provided identifier. The server will begin sending messages of the
+following form:
+ * `type`: “data”
+ * `id`: The identifier for the measurement.
+ * `value`: An object containing the actual measurement, in two fields:
+ * `timestamp`: A UNIX timestamp (in milliseconds) for the “measurement”
+ * `value`: The data value for the measurement (either a number, or a
+ string)
+* `unsubscribe <id>`: Stop receiving new data for the identified measurement.
+* `history <id>`: Request a history of all telemetry data for the identified
+measurement.
+ * `type`: “history”
+ * `id`: The identifier for the measurement.
+ * `value`: An array of objects containing the actual measurement, each of
+ which having two fields:
+ * `timestamp`: A UNIX timestamp (in milliseconds) for the “measurement”
+ * `value`: The data value for the measurement (either a number, or
+ a string)
+
+(Note that the term “measurement” is used to describe a distinct data series
+within this system; in other systems, these have been called channels,
+mnemonics, telemetry points, or other names. No preference is made here;
+Open MCT Web is easily adapted to use the terminology appropriate to your
+system.)
+Additionally, while running the server from the terminal we can toggle the
+state of the “spacecraft” by hitting enter; this will turn the “thrusters”
+on and off, having observable changes in telemetry.
+
+The telemetry dictionary referenced previously is contained in a separate file,
+used by the server. It uses a custom format and, for purposes of example,
+contains three “subsystems” containing a mix of numeric and string-based
+telemetry.
+
+ {
+ "name": "Example Spacecraft",
+ "identifier": "sc",
+ "subsystems": [
+ {
+ "name": "Propulsion",
+ "identifier": "prop",
+ "measurements": [
+ {
+ "name": "Fuel",
+ "identifier": "prop.fuel",
+ "units": "kilograms",
+ "type": "float"
+ },
+ {
+ "name": "Thrusters",
+ "identifier": "prop.thrusters",
+ "units": "None",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "Communications",
+ "identifier": "comms",
+ "measurements": [
+ {
+ "name": "Received",
+ "identifier": "comms.recd",
+ "units": "bytes",
+ "type": "integer"
+ },
+ {
+ "name": "Sent",
+ "identifier": "comms.sent",
+ "units": "bytes",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "Power",
+ "identifier": "pwr",
+ "measurements": [
+ {
+ "name": "Generator Temperature",
+ "identifier": "pwr.temp",
+ "units": "\u0080C",
+ "type": "float"
+ },
+ {
+ "name": "Generator Current",
+ "identifier": "pwr.c",
+ "units": "A",
+ "type": "float"
+ },
+ {
+ "name": "Generator Voltage",
+ "identifier": "pwr.v",
+ "units": "V",
+ "type": "float"
+ }
+ ]
+ }
+ ]
+ }
+__tutorial-server/dictionary.json__
+
+It should be noted that neither the interface for the example server nor the
+dictionary format are expected by Open MCT Web; rather, these are intended to
+stand in for some existing source of telemetry data to which we wish to adapt
+Open MCT Web.
+
+We can run this example server by:
+
+ cd tutorial-server
+ npm install ws
+ node app.js
+
+To verify that this is running and try out its interface, we can use a tool
+like [https://www.npmjs.com/package/wscat]():
+
+ wscat -c ws://localhost:8081
+ connected (press CTRL+C to quit)
+ > dictionary
+ < {"type":"dictionary","value":{"name":"Example Spacecraft","identifier":"sc","subsystems":[{"name":"Propulsion","identifier":"prop","measurements":[{"name":"Fuel","identifier":"prop.fuel","units":"kilograms","type":"float"},{"name":"Thrusters","identifier":"prop.thrusters","units":"None","type":"string"}]},{"name":"Communications","identifier":"comms","measurements":[{"name":"Received","identifier":"comms.recd","units":"bytes","type":"integer"},{"name":"Sent","identifier":"comms.sent","units":"bytes","type":"integer"}]},{"name":"Power","identifier":"pwr","measurements":[{"name":"Generator Temperature","identifier":"pwr.temp","units":"€C","type":"float"},{"name":"Generator Current","identifier":"pwr.c","units":"A","type":"float"},{"name":"Generator Voltage","identifier":"pwr.v","units":"V","type":"float"}]}]}}
+
+Now that the example server’s interface is reasonably well-understood, a plugin
+can be written to adapt Open MCT Web to utilize it.
+
+### Step 1-Add a Top-level Object
+
+Since Open MCT Web uses an “object-first” approach to accessing data, before
+we’ll be able to do anything with this new data source, we’ll need to have a
+way to explore the available measurements in the tree. In this step, we will
+add a top-level object which will serve as a container; in the next step, we
+will populate this with the contents of the telemetry dictionary (which we
+will retrieve from the server.)
+
+ {
+ "name": "Example Telemetry Adapter",
+ "extensions": {
+ "types": [
+ {
+ "name": "Spacecraft",
+ "key": "example.spacecraft",
+ "glyph": "o"
+ }
+ ],
+ "roots": [
+ {
+ "id": "example:sc",
+ "priority": "preferred",
+ "model": {
+ "type": "example.spacecraft",
+ "name": "My Spacecraft",
+ "composition": []
+ }
+ }
+ ]
+ }
+ }
+__tutorials/telemetry/bundle.json__
+
+Here, we’ve created our initial telemetry plugin. This exposes a new domain
+object type (the “Spacecraft”, which will be represented by the contents of the
+telemetry dictionary) and also adds one instance of it as a root-level object
+(by declaring an extension of category roots.) We have also set priority to
+preferred so that this shows up near the top, instead of below My Items.
+
+If we include this in our set of active bundles:
+
+ [
+ "platform/framework",
+ "platform/core",
+ "platform/representation",
+ "platform/commonUI/about",
+ "platform/commonUI/browse",
+ "platform/commonUI/edit",
+ "platform/commonUI/dialog",
+ "platform/commonUI/general",
+ "platform/containment",
+ "platform/telemetry",
+ "platform/features/layout",
+ "platform/features/pages",
+ "platform/features/plot",
+ "platform/features/scrolling",
+ "platform/forms",
+ "platform/persistence/queue",
+ "platform/policy",
+
+ "example/persistence",
+ "example/generator"
+ ]
+ [
+ "platform/framework",
+ "platform/core",
+ "platform/representation",
+ "platform/commonUI/about",
+ "platform/commonUI/browse",
+ "platform/commonUI/edit",
+ "platform/commonUI/dialog",
+ "platform/commonUI/general",
+ "platform/containment",
+ "platform/telemetry",
+ "platform/features/layout",
+ "platform/features/pages",
+ "platform/features/plot",
+ "platform/features/scrolling",
+ "platform/forms",
+ "platform/persistence/queue",
+ "platform/policy",
+
+ "example/persistence",
+ "example/generator",
+
+ + "tutorials/telemetry"
+ ]
+__bundles.json__
+
+...we will be able to reload Open MCT Web and see that it is present:
+
+![Telemetry](images/telemetry-1.png)
+
+Now, we have somewhere in the UI to put the contents of our telemetry
+dictionary.
+
+### Step 2-Expose the Telemetry Dictionary
+
+In order to expose the telemetry dictionary, we first need to read it from the
+server. Our first step will be to add a service that will handle interactions
+with the server; this will not be used by Open MCT Web directly, but will be
+used by subsequent components we add.
+
+ /*global define,WebSocket*/
+
+ define(
+ [],
+ function () {
+ "use strict";
+
+ function ExampleTelemetryServerAdapter($q, wsUrl) {
+ var ws = new WebSocket(wsUrl),
+ dictionary = $q.defer();
+
+ // Handle an incoming message from the server
+ ws.onmessage = function (event) {
+ var message = JSON.parse(event.data);
+
+ switch (message.type) {
+ case "dictionary":
+ dictionary.resolve(message.value);
+ break;
+ }
+ };
+
+ // Request dictionary once connection is established
+ ws.onopen = function () {
+ ws.send("dictionary");
+ };
+
+ return {
+ dictionary: function () {
+ return dictionary.promise;
+ }
+ };
+ }
+
+ return ExampleTelemetryServerAdapter;
+ }
+ );
+__tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__
+
+When created, this service initiates a connection to the server, and begins
+loading the dictionary. This will occur asynchronously, so the `dictionary()`
+method it exposes returns a `Promise` for the loaded dictionary
+(`dictionary.json` from above), using Angular’s `$q`
+(see [https://docs.angularjs.org/api/ng/service/$q]().) Note that error- and
+close-handling for this WebSocket connection have been omitted for brevity.
+
+Once the dictionary has been loaded, we will want to represent its contents
+as domain objects. Specifically, we want subsystems to appear as objects
+under My Spacecraft, and measurements to appear as objects within those
+subsystems. This means that we need to convert the data from the dictionary
+into domain object models, and expose these to Open MCT Web via a
+`modelService`.
+
+ /*global define*/
+
+ define(
+ function () {
+ "use strict";
+
+ var PREFIX = "example_tlm:",
+ FORMAT_MAPPINGS = {
+ float: "number",
+ integer: "number",
+ string: "string"
+ };
+
+ function ExampleTelemetryModelProvider(adapter, $q) {
+ var modelPromise, empty = $q.when({});
+
+ // Check if this model is in our dictionary (by prefix)
+ function isRelevant(id) {
+ return id.indexOf(PREFIX) === 0;
+ }
+
+ // Build a domain object identifier by adding a prefix
+ function makeId(element) {
+ return PREFIX + element.identifier;
+ }
+
+ // Create domain object models from this dictionary
+ function buildTaxonomy(dictionary) {
+ var models = {};
+
+ // Create & store a domain object model for a measurement
+ function addMeasurement(measurement) {
+ var format = FORMAT_MAPPINGS[measurement.type];
+ models[makeId(measurement)] = {
+ type: "example.measurement",
+ name: measurement.name,
+ telemetry: {
+ key: measurement.identifier,
+ ranges: [{
+ key: "value",
+ name: "Value",
+ units: measurement.units,
+ format: format
+ }]
+ }
+ };
+ }
+
+ // Create & store a domain object model for a subsystem
+ function addSubsystem(subsystem) {
+ var measurements =
+ (subsystem.measurements || []);
+ models[makeId(subsystem)] = {
+ type: "example.subsystem",
+ name: subsystem.name,
+ composition: measurements.map(makeId)
+ };
+ measurements.forEach(addMeasurement);
+ }
+
+ (dictionary.subsystems || []).forEach(addSubsystem);
+
+ return models;
+ }
+
+ // Begin generating models once the dictionary is available
+ modelPromise = adapter.dictionary().then(buildTaxonomy);
+
+ return {
+ getModels: function (ids) {
+ // Return models for the dictionary only when they
+ // are relevant to the request.
+ return ids.some(isRelevant) ? modelPromise : empty;
+ }
+ };
+ }
+
+ return ExampleTelemetryModelProvider;
+ }
+ );
+__tutorials/telemetry/src/ExampleTelemetryModelProvider.js__
+
+This script implements a `provider` for `modelService`; the `modelService` is a
+composite service, meaning that multiple such services can exist side by side.
+(For example, there is another `provider` for `modelService` that reads domain
+object models from the persistence store.)
+
+Here, we read the dictionary using the server adapter from above; since this
+will be loaded asynchronously, we use promise-chaining (see
+[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then#Chaining]())
+to take that result and build up an object mapping identifiers to new domain
+object models. This is returned from our `modelService`, but only when the
+request actually calls for identifiers that look like they’re from the
+dictionary. This means that loading other models is not blocked by loading the
+dictionary. (Note that the `modelService` contract allows us to return either a
+sub- or superset of the requested models, so it is fine to always return the
+whole dictionary.)
+
+Some notable points to call out here:
+
+* Every subsystem and every measurement from the dictionary has an `identifier`
+field declared. We use this as part of the domain object identifier, but we
+also prefix it with `example_tlm`:. This accomplishes a few things:
+ * We can easily tell whether an identifier is expected to be in the
+ dictionary or not.
+ * We avoid naming collisions with other model providers.
+ * Finally, Open MCT Web uses the colon prefix as a hint that this domain
+ object will not be in the persistence store.
+* A couple of new types are introduced here (in the `type` field of the domain
+object models we create); we will need to define these as extensions as well in
+order for them to display correctly.
+* The `composition` field of each subsystem contained the Open MCT Web
+identifiers of all the measurements in that subsystem. This `composition` field
+will be used by Open MCT Web to determine what domain objects contain other
+domain objects (e.g. to populate the tree.)
+* The `telemetry` field of each measurement will be used by Open MCT Web to
+understand how to request and interpret telemetry data for this object. The
+`key` is the machine-readable identifier for this measurement within the
+telemetry system; the `ranges` provide metadata about the values for this data.
+(A separate field, `domains`, provides metadata about timestamps or other
+ordering properties of the data, but this will be the same for all
+measurements, so we will define that later at the type level.)
+ * This field (whose contents will be merged atop the telemetry property we
+define at the type-level) will serve as a template for later `telemetry`
+requests to the `telemetryService`, so we’ll see the properties we define here
+again later in Steps 3 and 4.
+
+This allows our telemetry dictionary to be expressed as domain object models
+(and, in turn, as domain objects), but these objects still aren’t reachable. To
+fix this, we will need another script which will add these subsystems to the
+root-level object we added in Step 1.
+
+ /*global define*/
+
+ define(
+ function () {
+ "use strict";
+
+ var TAXONOMY_ID = "example:sc",
+ PREFIX = "example_tlm:";
+
+ function ExampleTelemetryInitializer(adapter, objectService) {
+ // Generate a domain object identifier for a dictionary element
+ function makeId(element) {
+ return PREFIX + element.identifier;
+ }
+
+ // When the dictionary is available, add all subsystems
+ // to the composition of My Spacecraft
+ function initializeTaxonomy(dictionary) {
+ // Get the top-level container for dictionary objects
+ // from a group of domain objects.
+ function getTaxonomyObject(domainObjects) {
+ return domainObjects[TAXONOMY_ID];
+ }
+
+ // Populate
+ function populateModel(taxonomyObject) {
+ return taxonomyObject.useCapability(
+ "mutation",
+ function (model) {
+ model.name =
+ dictionary.name;
+ model.composition =
+ dictionary.subsystems.map(makeId);
+ }
+ );
+ }
+
+ // Look up My Spacecraft, and populate it accordingly.
+ objectService.getObjects([TAXONOMY_ID])
+ .then(getTaxonomyObject)
+ .then(populateModel);
+ }
+
+ adapter.dictionary().then(initializeTaxonomy);
+ }
+
+ return ExampleTelemetryInitializer;
+ }
+ );
+__tutorials/telemetry/src/ExampleTelemetryInitializer.js__
+
+At the conclusion of Step 1, the top-level My Spacecraft object was empty. This
+script will wait for the dictionary to be loaded, then load My Spacecraft (by
+its identifier), and “mutate” it. The `mutation` capability allows changes to be
+made to a domain object’s model. Here, we take this top-level object, update its
+name to match what was in the dictionary, and set its `composition` to an array
+of domain object identifiers for all subsystems contained in the dictionary
+(using the same identifier prefix as before.)
+
+Finally, we wire in these changes by modifying our plugin’s `bundle.json` to
+provide metadata about how these pieces interact (both with each other, and
+with the platform):
+
+ {
+ "name": "Example Telemetry Adapter",
+ "extensions": {
+ "types": [
+ {
+ "name": "Spacecraft",
+ "key": "example.spacecraft",
+ "glyph": "o"
+ },
+ {
+ + "name": "Subsystem",
+ + "key": "example.subsystem",
+ + "glyph": "o",
+ + "model": { "composition": [] }
+ + },
+ + {
+ + "name": "Measurement",
+ + "key": "example.measurement",
+ + "glyph": "T",
+ + "model": { "telemetry": {} },
+ + "telemetry": {
+ + "source": "example.source",
+ + "domains": [
+ + {
+ + "name": "Time",
+ + "key": "timestamp"
+ + }
+ + ]
+ + }
+ + }
+ ],
+ "roots": [
+ {
+ "id": "example:sc",
+ "priority": "preferred",
+ "model": {
+ "type": "example.spacecraft",
+ "name": "My Spacecraft",
+ "composition": []
+ }
+ }
+ ],
+ + "services": [
+ + {
+ + "key": "example.adapter",
+ + "implementation": "ExampleTelemetryServerAdapter.js",
+ + "depends": [ "$q", "EXAMPLE_WS_URL" ]
+ + }
+ + ],
+ + "constants": [
+ + {
+ + "key": "EXAMPLE_WS_URL",
+ + "priority": "fallback",
+ + "value": "ws://localhost:8081"
+ + }
+ + ],
+ + "runs": [
+ + {
+ + "implementation": "ExampleTelemetryInitializer.js",
+ + "depends": [ "example.adapter", "objectService" ]
+ + }
+ + ],
+ + "components": [
+ + {
+ + "provides": "modelService",
+ + "type": "provider",
+ + "implementation": "ExampleTelemetryModelProvider.js",
+ + "depends": [ "example.adapter", "$q" ]
+ + }
+ + ]
+ }
+ }
+__tutorials/telemetry/bundle.json__
+
+A summary of what we’ve added here:
+
+* New type definitions have been added to represent Subsystems and Measurements,
+respectively.
+ * Measurements have a `telemetry` field; this is similar to the `telemetry`
+ field added in the model, but contains properties that will be common among
+ all Measurements. In particular, the `source` field will be used later as a
+ symbolic identifier for the telemetry data source.
+ * We have also added some “initial models” for these two types using the
+ `model` field. While domain objects of these types cannot be created via the
+ Create menu, some policies will look at initial models to predict what
+ capabilities domain objects of certain types would have, so we want to
+ ensure that Subsystems and Measurements will be recognized as having
+ `composition` and `telemetry` capabilities, respectively.
+* The adapter to the WebSocket server has been added as a service with the
+symbolic name `example.adapter`; it is depended-upon elsewhere within this
+plugin.
+* A constant, `EXAMPLE_WS_URL`, is defined, and depended-upon by
+`example.server`. Setting `priority` to `fallback` means this constant will be
+overridden if defined anywhere else, allowing configuration bundles to specify
+different URLs for the WebSocket connection.
+* The initializer script is registered using the `runs` category of extension,
+to ensure that this executes (and populates the contents of the top-level My
+Spacecraft object) once Open MCT Web is started.
+ * This depends upon the `example.adapter` service we exposed above, as well
+ as Angular’s `$q`; these services will be made available in the constructor
+ call.
+* Finally, the `modelService` provider which presents dictionary elements as
+domain object models is exposed. Since `modelService` is a composite service,
+this is registered under the extension category `components`.
+ * As with the initializer, this depends upon the `example.adapter` service
+ we exposed above, as well as Angular’s `$q`; these services will be made
+ available in the constructor call.
+
+Now if we run Open MCT Web (assuming our example telemetry server is also
+running) and expand our top-level node completely, we see the contents of our
+dictionary:
+
+![Telemetry 2](images/telemetry-2.png)
+
+
+Note that “My Spacecraft” has changed its name to “Example Spacecraft”, which
+is the name it had in the dictionary.
+
+### Step 3-Historical Telemetry
+
+After Step 2, we are able to see our dictionary in the user interface and click
+around our different measurements, but we don’t see any data. We need to give
+ourselves the ability to retrieve this data from the server. In this step, we
+will do so for the server’s historical telemetry.
+
+Our first step will be to add a method to our server adapter which allows us to
+send history requests to the server:
+
+ /*global define,WebSocket*/
+
+ define(
+ [],
+ function () {
+ "use strict";
+
+ function ExampleTelemetryServerAdapter($q, wsUrl) {
+ var ws = new WebSocket(wsUrl),
+ + histories = {},
+ dictionary = $q.defer();
+
+ // Handle an incoming message from the server
+ ws.onmessage = function (event) {
+ var message = JSON.parse(event.data);
+
+ switch (message.type) {
+ case "dictionary":
+ dictionary.resolve(message.value);
+ break;
+ + case "history":
+ + histories[message.id].resolve(message);
+ + delete histories[message.id];
+ + break;
+ }
+ };
+
+ // Request dictionary once connection is established
+ ws.onopen = function () {
+ ws.send("dictionary");
+ };
+
+ return {
+ dictionary: function () {
+ return dictionary.promise;
+ },
+ + history: function (id) {
+ + histories[id] = histories[id] || $q.defer();
+ + ws.send("history " + id);
+ + return histories[id].promise;
+ + }
+ };
+ }
+
+ return ExampleTelemetryServerAdapter;
+ }
+ );
+
+__tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__
+
+When the `history` method is called, a new request is issued to the server for
+historical telemetry, _unless_ a request for the same historical telemetry is
+still pending. Similarly, when historical telemetry arrives for a given
+identifier, the pending promise is resolved.
+
+This `history` method will be used by a `telemetryService` provider which we
+will implement:
+
+ /*global define*/
+
+ define(
+ ['./ExampleTelemetrySeries'],
+ function (ExampleTelemetrySeries) {
+ "use strict";
+
+ var SOURCE = "example.source";
+
+ function ExampleTelemetryProvider(adapter, $q) {
+ // Used to filter out requests for telemetry
+ // from some other source
+ function matchesSource(request) {
+ return (request.source === SOURCE);
+ }
+
+ return {
+ requestTelemetry: function (requests) {
+ var packaged = {},
+ relevantReqs = requests.filter(matchesSource);
+
+ // Package historical telemetry that has been received
+ function addToPackage(history) {
+ packaged[SOURCE][history.id] =
+ new ExampleTelemetrySeries(history.value);
+ }
+
+ // Retrieve telemetry for a specific measurement
+ function handleRequest(request) {
+ var key = request.key;
+ return adapter.history(key).then(addToPackage);
+ }
+
+ packaged[SOURCE] = {};
+ return $q.all(relevantReqs.map(handleRequest))
+ .then(function () { return packaged; });
+ },
+ subscribe: function (callback, requests) {
+ return function () {};
+ }
+ };
+ }
+
+ return ExampleTelemetryProvider;
+ }
+ );
+__tutorials/telemetry/src/ExampleTelemetryProvider.js__
+
+The `requestTelemetry` method of a `telemetryService` is expected to take an
+array of requests (each with `source` and `key` parameters, identifying the
+general source of data and the specific element within that source, respectively) and
+return a Promise for any telemetry data it knows of which satisfies those
+requests, packaged in a specific way. This packaging is as an object containing
+key-value pairs, where keys correspond to `source` properties of requests and
+values are key-value pairs, where keys correspond to `key` properties of requests
+and values are `TelemetrySeries` objects. (We will see our implementation
+below.)
+
+To do this, we create a container for our telemetry source, and consult the
+adapter to get telemetry histories for any relevant requests, then package
+them as they come in. The `$q.all` method is used to return a single Promise
+that will resolve only when all histories have been packaged. Promise-chaining
+is used to ensure that the resolved value will be the fully-packaged data.
+
+It is worth mentioning here that the `requests` we receive should look a little
+familiar. When Open MCT Web generates a `request` object associated with a
+domain object, it does so by merging together three JavaScript objects:
+
+* First, the `telemetry` property from that domain object’s type definition.
+* Second, the `telemetry` property from that domain object’s model.
+* Finally, the `request` object that was passed in via that domain object’s
+`telemetry` capability.
+
+As such, the `source` and `key` properties we observe here will come from the
+type definition and domain object model, respectively, as we specified them
+during Step 2. (Or, they might come from somewhere else entirely, if we have
+other telemetry-providing domain objects in our system; that is something we
+check for using the `source` property.)
+
+Finally, note that we also have a `subscribe` method, to satisfy the interface of
+`telemetryService`, but this `subscribe` method currently does nothing.
+
+This script uses an `ExampleTelemetrySeries` class, which looks like:
+
+ /*global define*/
+
+ define(
+ function () {
+ "use strict";
+
+ function ExampleTelemetrySeries(data) {
+ return {
+ getPointCount: function () {
+ return data.length;
+ },
+ getDomainValue: function (index) {
+ return (data[index] || {}).timestamp;
+ },
+ getRangeValue: function (index) {
+ return (data[index] || {}).value;
+ }
+ };
+ }
+
+ return ExampleTelemetrySeries;
+ }
+ );
+__tutorials/telemetry/src/ExampleTelemetrySeries.js__
+
+This takes the array of telemetry values (as returned by the server) and wraps
+it with the interface expected by the platform (the methods shown.)
+
+Finally, we expose this `telemetryService` provider declaratively:
+
+ {
+ "name": "Example Telemetry Adapter",
+ "extensions": {
+ "types": [
+ {
+ "name": "Spacecraft",
+ "key": "example.spacecraft",
+ "glyph": "o"
+ },
+ {
+ "name": "Subsystem",
+ "key": "example.subsystem",
+ "glyph": "o",
+ "model": { "composition": [] }
+ },
+ {
+ "name": "Measurement",
+ "key": "example.measurement",
+ "glyph": "T",
+ "model": { "telemetry": {} },
+ "telemetry": {
+ "source": "example.source",
+ "domains": [
+ {
+ "name": "Time",
+ "key": "timestamp"
+ }
+ ]
+ }
+ }
+ ],
+ "roots": [
+ {
+ "id": "example:sc",
+ "priority": "preferred",
+ "model": {
+ "type": "example.spacecraft",
+ "name": "My Spacecraft",
+ "composition": []
+ }
+ }
+ ],
+ "services": [
+ {
+ "key": "example.adapter",
+ "implementation": "ExampleTelemetryServerAdapter.js",
+ "depends": [ "$q", "EXAMPLE_WS_URL" ]
+ }
+ ],
+ "constants": [
+ {
+ "key": "EXAMPLE_WS_URL",
+ "priority": "fallback",
+ "value": "ws://localhost:8081"
+ }
+ ],
+ "runs": [
+ {
+ "implementation": "ExampleTelemetryInitializer.js",
+ "depends": [ "example.adapter", "objectService" ]
+ }
+ ],
+ "components": [
+ {
+ "provides": "modelService",
+ "type": "provider",
+ "implementation": "ExampleTelemetryModelProvider.js",
+ "depends": [ "example.adapter", "$q" ]
+ },
+ + {
+ + "provides": "telemetryService",
+ + "type": "provider",
+ + "implementation": "ExampleTelemetryProvider.js",
+ + "depends": [ "example.adapter", "$q" ]
+ + }
+ ]
+ }
+ }
+__tutorials/telemetry/bundle.json__
+
+Now, if we navigate to one of our numeric measurements, we should see a plot of
+its historical telemetry:
+
+![Telemetry](images/telemetry-3.png)
+
+We can now visualize our data, but it doesn’t update over time - we know the
+server is continually producing new data, but we have to click away and come
+back to see it. We can fix this by adding support for telemetry subscriptions.
+
+### Step 4-Real-time Telemetry
+
+Finally, we want to utilize the server’s ability to subscribe to telemetry
+from Open MCT Web. To do this, first we want to expose some new methods for
+this from our server adapter:
+
+ /*global define,WebSocket*/
+
+ define(
+ [],
+ function () {
+ "use strict";
+
+ function ExampleTelemetryServerAdapter($q, wsUrl) {
+ var ws = new WebSocket(wsUrl),
+ histories = {},
+ + listeners = [],
+ dictionary = $q.defer();
+
+ // Handle an incoming message from the server
+ ws.onmessage = function (event) {
+ var message = JSON.parse(event.data);
+
+ switch (message.type) {
+ case "dictionary":
+ dictionary.resolve(message.value);
+ break;
+ case "history":
+ histories[message.id].resolve(message);
+ delete histories[message.id];
+ break;
+ + case "data":
+ + listeners.forEach(function (listener) {
+ + listener(message);
+ + });
+ + break;
+ }
+ };
+
+ // Request dictionary once connection is established
+ ws.onopen = function () {
+ ws.send("dictionary");
+ };
+
+ return {
+ dictionary: function () {
+ return dictionary.promise;
+ },
+ history: function (id) {
+ histories[id] = histories[id] || $q.defer();
+ ws.send("history " + id);
+ return histories[id].promise;
+ },
+ + subscribe: function (id) {
+ + ws.send("subscribe " + id);
+ + },
+ + unsubscribe: function (id) {
+ + ws.send("unsubscribe " + id);
+ + },
+ + listen: function (callback) {
+ + listeners.push(callback);
+ + }
+ };
+ }
+
+ return ExampleTelemetryServerAdapter;
+ }
+ );
+__tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__
+
+Here, we have added `subscribe` and `unsubscribe` methods which issue the
+corresponding requests to the server. Seperately, we introduce the ability to
+listen for `data` messages as they come in: These will contain the data associated
+with these subscriptions.
+
+We then need only to utilize these methods from our `telemetryService`:
+
+ /*global define*/
+
+ define(
+ ['./ExampleTelemetrySeries'],
+ function (ExampleTelemetrySeries) {
+ "use strict";
+
+ var SOURCE = "example.source";
+
+ function ExampleTelemetryProvider(adapter, $q) {
+ + var subscribers = {};
+
+ // Used to filter out requests for telemetry
+ // from some other source
+ function matchesSource(request) {
+ return (request.source === SOURCE);
+ }
+
+ + // Listen for data, notify subscribers
+ + adapter.listen(function (message) {
+ + var packaged = {};
+ + packaged[SOURCE] = {};
+ + packaged[SOURCE][message.id] =
+ + new ExampleTelemetrySeries([message.value]);
+ + (subscribers[message.id] || []).forEach(function (cb) {
+ + cb(packaged);
+ + });
+ + });
+
+ return {
+ requestTelemetry: function (requests) {
+ var packaged = {},
+ relevantReqs = requests.filter(matchesSource);
+
+ // Package historical telemetry that has been received
+ function addToPackage(history) {
+ packaged[SOURCE][history.id] =
+ new ExampleTelemetrySeries(history.value);
+ }
+
+ // Retrieve telemetry for a specific measurement
+ function handleRequest(request) {
+ var key = request.key;
+ return adapter.history(key).then(addToPackage);
+ }
+
+ packaged[SOURCE] = {};
+ return $q.all(relevantReqs.map(handleRequest))
+ .then(function () { return packaged; });
+ },
+ subscribe: function (callback, requests) {
+ + var keys = requests.filter(matchesSource)
+ + .map(function (req) { return req.key; });
+ +
+ + function notCallback(cb) {
+ + return cb !== callback;
+ + }
+ +
+ + function unsubscribe(key) {
+ + subscribers[key] =
+ + (subscribers[key] || []).filter(notCallback);
+ + if (subscribers[key].length < 1) {
+ + adapter.unsubscribe(key);
+ + }
+ + }
+ +
+ + keys.forEach(function (key) {
+ + subscribers[key] = subscribers[key] || [];
+ + adapter.subscribe(key);
+ + subscribers[key].push(callback);
+ + });
+ +
+ + return function () {
+ + keys.forEach(unsubscribe);
+ + };
+ }
+ };
+ }
+
+ return ExampleTelemetryProvider;
+ }
+ );
+__tutorials/telemetry/src/ExampleTelemetryProvider.js__
+
+A quick summary of these changes:
+
+* First, we maintain current subscribers (callbacks) in an object containing
+key-value pairs, where keys are request key properties, and values are callback
+arrays.
+* We listen to new data coming in from the server adapter, and invoke any
+relevant callbacks when this happens. We package the data in the same manner
+that historical telemetry is packaged (even though in this case we are
+providing single-element series objects.)
+* Finally, in our `subscribe` method we add callbacks to the lists of active
+subscribers. This method is expected to return a function which terminates the
+subscription when called, so we do some work to remove subscribers in this
+situations. When our subscriber count for a given measurement drops to zero,
+we issue an unsubscribe request. (We don’t take any care to avoid issuing
+multiple subscribe requests to the server, because we happen to know that the
+server can handle this.)
+
+Running Open MCT Web again, we can still plot our historical telemetry - but
+now we also see that it updates in real-time as more data comes in from the
+server.
diff --git a/example/generator/bundle.json b/example/generator/bundle.json
index a13bbdc8f..cdb473695 100644
--- a/example/generator/bundle.json
+++ b/example/generator/bundle.json
@@ -34,6 +34,10 @@
{
"key": "time",
"name": "Time"
+ },
+ {
+ "key": "yesterday",
+ "name": "Yesterday"
}
],
"ranges": [
@@ -61,4 +65,4 @@
}
]
}
-} \ No newline at end of file
+}
diff --git a/example/generator/src/SinewaveTelemetryProvider.js b/example/generator/src/SinewaveTelemetryProvider.js
index 014510f67..c4062e659 100644
--- a/example/generator/src/SinewaveTelemetryProvider.js
+++ b/example/generator/src/SinewaveTelemetryProvider.js
@@ -25,8 +25,8 @@
* Module defining SinewaveTelemetryProvider. Created by vwoeltje on 11/12/14.
*/
define(
- ["./SinewaveTelemetry"],
- function (SinewaveTelemetry) {
+ ["./SinewaveTelemetrySeries"],
+ function (SinewaveTelemetrySeries) {
"use strict";
/**
@@ -45,7 +45,7 @@ define(
function generateData(request) {
return {
key: request.key,
- telemetry: new SinewaveTelemetry(request)
+ telemetry: new SinewaveTelemetrySeries(request)
};
}
@@ -112,4 +112,4 @@ define(
return SinewaveTelemetryProvider;
}
-); \ No newline at end of file
+);
diff --git a/example/generator/src/SinewaveTelemetry.js b/example/generator/src/SinewaveTelemetrySeries.js
index 6c255bf56..1e8403476 100644
--- a/example/generator/src/SinewaveTelemetry.js
+++ b/example/generator/src/SinewaveTelemetrySeries.js
@@ -29,35 +29,47 @@ define(
function () {
"use strict";
- var firstObservedTime = Date.now();
+ var ONE_DAY = 60 * 60 * 24,
+ firstObservedTime = Math.floor(Date.now() / 1000) - ONE_DAY;
/**
*
* @constructor
*/
- function SinewaveTelemetry(request) {
- var latestObservedTime = Date.now(),
- count = Math.floor((latestObservedTime - firstObservedTime) / 1000),
- period = request.period || 30,
- generatorData = {};
+ function SinewaveTelemetrySeries(request) {
+ var timeOffset = (request.domain === 'yesterday') ? ONE_DAY : 0,
+ latestTime = Math.floor(Date.now() / 1000) - timeOffset,
+ firstTime = firstObservedTime - timeOffset,
+ endTime = (request.end !== undefined) ?
+ Math.floor(request.end / 1000) : latestTime,
+ count = Math.min(endTime, latestTime) - firstTime,
+ period = +request.period || 30,
+ generatorData = {},
+ requestStart = (request.start === undefined) ? firstTime :
+ Math.max(Math.floor(request.start / 1000), firstTime),
+ offset = requestStart - firstTime;
+
+ if (request.size !== undefined) {
+ offset = Math.max(offset, count - request.size);
+ }
generatorData.getPointCount = function () {
- return count;
+ return count - offset;
};
generatorData.getDomainValue = function (i, domain) {
- return i * 1000 +
- (domain !== 'delta' ? firstObservedTime : 0);
+ return (i + offset) * 1000 + firstTime * 1000 -
+ (domain === 'yesterday' ? ONE_DAY : 0);
};
generatorData.getRangeValue = function (i, range) {
range = range || "sin";
- return Math[range](i * Math.PI * 2 / period);
+ return Math[range]((i + offset) * Math.PI * 2 / period);
};
return generatorData;
}
- return SinewaveTelemetry;
+ return SinewaveTelemetrySeries;
}
-); \ No newline at end of file
+);
diff --git a/example/profiling/bundle.json b/example/profiling/bundle.json
index b6090717d..25c1b1074 100644
--- a/example/profiling/bundle.json
+++ b/example/profiling/bundle.json
@@ -4,7 +4,11 @@
{
"implementation": "WatchIndicator.js",
"depends": ["$interval", "$rootScope"]
+ },
+ {
+ "implementation": "DigestIndicator.js",
+ "depends": ["$interval", "$rootScope"]
}
]
}
-} \ No newline at end of file
+}
diff --git a/example/profiling/src/DigestIndicator.js b/example/profiling/src/DigestIndicator.js
new file mode 100644
index 000000000..02fbc7a08
--- /dev/null
+++ b/example/profiling/src/DigestIndicator.js
@@ -0,0 +1,77 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * Displays the number of digests that have occurred since the
+ * indicator was first instantiated.
+ * @constructor
+ * @param $interval Angular's $interval
+ * @implements {Indicator}
+ */
+ function DigestIndicator($interval, $rootScope) {
+ var digests = 0,
+ displayed = 0,
+ start = Date.now();
+
+ function update() {
+ var secs = (Date.now() - start) / 1000;
+ displayed = Math.round(digests / secs);
+ }
+
+ function increment() {
+ digests += 1;
+ }
+
+ $rootScope.$watch(increment);
+
+ // Update state every second
+ $interval(update, 1000);
+
+ // Provide initial state, too
+ update();
+
+ return {
+ getGlyph: function () {
+ return ".";
+ },
+ getGlyphClass: function () {
+ return undefined;
+ },
+ getText: function () {
+ return displayed + " digests/sec";
+ },
+ getDescription: function () {
+ return "";
+ }
+ };
+ }
+
+ return DigestIndicator;
+
+ }
+);
diff --git a/package.json b/package.json
index a1ac01f43..c96642129 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
"split": "^1.0.0",
"mkdirp": "^0.5.1",
"nomnoml": "^0.0.3",
- "canvas": "^1.2.7"
+ "canvas": "^1.2.7",
+ "markdown-toc": "^0.11.7"
},
"scripts": {
"start": "node app.js",
diff --git a/platform/commonUI/browse/bundle.json b/platform/commonUI/browse/bundle.json
index e3284d99b..bc42a33c1 100644
--- a/platform/commonUI/browse/bundle.json
+++ b/platform/commonUI/browse/bundle.json
@@ -1,4 +1,9 @@
{
+ "configuration": {
+ "paths": {
+ "uuid": "uuid"
+ }
+ },
"extensions": {
"routes": [
{
diff --git a/platform/commonUI/browse/res/templates/browse-object.html b/platform/commonUI/browse/res/templates/browse-object.html
index ebe53b368..3bd6138da 100644
--- a/platform/commonUI/browse/res/templates/browse-object.html
+++ b/platform/commonUI/browse/res/templates/browse-object.html
@@ -42,9 +42,8 @@
</div>
</div>
- <div class='object-holder abs vscroll'>
- <mct-representation key="representation.selected.key"
- mct-object="representation.selected.key && domainObject">
- </mct-representation>
- </div>
+ <mct-representation key="representation.selected.key"
+ mct-object="representation.selected.key && domainObject"
+ class="abs object-holder">
+ </mct-representation>
</span>
diff --git a/platform/commonUI/browse/res/templates/browse.html b/platform/commonUI/browse/res/templates/browse.html
index e6255bcb5..9a1be7e77 100644
--- a/platform/commonUI/browse/res/templates/browse.html
+++ b/platform/commonUI/browse/res/templates/browse.html
@@ -28,7 +28,9 @@
<mct-split-pane class='contents abs' anchor='left'>
<div class='split-pane-component treeview pane left'>
<div class="holder abs l-mobile">
- <mct-representation key="'create-button'" mct-object="navigatedObject">
+ <mct-representation key="'create-button'"
+ mct-object="navigatedObject"
+ mct-device="desktop">
</mct-representation>
<div class='holder search-holder abs'
ng-class="{active: treeModel.search}">
diff --git a/platform/commonUI/browse/res/templates/create/create-button.html b/platform/commonUI/browse/res/templates/create/create-button.html
index 67a029411..a7b4ad96e 100644
--- a/platform/commonUI/browse/res/templates/create/create-button.html
+++ b/platform/commonUI/browse/res/templates/create/create-button.html
@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<div class="menu-element wrapper" ng-controller="ClickAwayController as createController">
- <div class="s-menu major create-btn" ng-click="createController.toggle()">
+ <div class="s-menu-btn major create-btn" ng-click="createController.toggle()">
<span class="ui-symbol icon type-icon">&#x2b;</span>
<span class="title-label">Create</span>
</div>
diff --git a/platform/commonUI/browse/src/creation/CreationService.js b/platform/commonUI/browse/src/creation/CreationService.js
index 667863ef2..2b059724b 100644
--- a/platform/commonUI/browse/src/creation/CreationService.js
+++ b/platform/commonUI/browse/src/creation/CreationService.js
@@ -25,7 +25,7 @@
* Module defining CreateService. Created by vwoeltje on 11/10/14.
*/
define(
- ["../../lib/uuid"],
+ ["uuid"],
function (uuid) {
"use strict";
diff --git a/platform/commonUI/edit/res/templates/edit-object.html b/platform/commonUI/edit/res/templates/edit-object.html
index c2089781a..71dc233a8 100644
--- a/platform/commonUI/edit/res/templates/edit-object.html
+++ b/platform/commonUI/edit/res/templates/edit-object.html
@@ -30,12 +30,11 @@
structure="toolbar.structure"
ng-model="toolbar.state">
</mct-toolbar>
- <div class='holder abs object-holder work-area'>
- <mct-representation key="representation.selected.key"
- toolbar="toolbar"
- mct-object="representation.selected.key && domainObject">
- </mct-representation>
- </div>
+ <mct-representation key="representation.selected.key"
+ toolbar="toolbar"
+ mct-object="representation.selected.key && domainObject"
+ class="holder abs object-holder work-area">
+ </mct-representation>
</div>
<mct-splitter></mct-splitter>
<div
diff --git a/platform/commonUI/general/bundle.json b/platform/commonUI/general/bundle.json
index ce3e5af71..2bd200b13 100644
--- a/platform/commonUI/general/bundle.json
+++ b/platform/commonUI/general/bundle.json
@@ -8,6 +8,11 @@
"key": "urlService",
"implementation": "services/UrlService.js",
"depends": [ "$location" ]
+ },
+ {
+ "key": "popupService",
+ "implementation": "services/PopupService.js",
+ "depends": [ "$document", "$window" ]
}
],
"runs": [
@@ -54,6 +59,16 @@
],
"controllers": [
{
+ "key": "TimeRangeController",
+ "implementation": "controllers/TimeRangeController.js",
+ "depends": [ "$scope", "now" ]
+ },
+ {
+ "key": "DateTimePickerController",
+ "implementation": "controllers/DateTimePickerController.js",
+ "depends": [ "$scope", "now" ]
+ },
+ {
"key": "TreeNodeController",
"implementation": "controllers/TreeNodeController.js",
"depends": [ "$scope", "$timeout" ]
@@ -119,11 +134,21 @@
"depends": [ "$document" ]
},
{
+ "key": "mctClickElsewhere",
+ "implementation": "directives/MCTClickElsewhere.js",
+ "depends": [ "$document" ]
+ },
+ {
"key": "mctResize",
"implementation": "directives/MCTResize.js",
"depends": [ "$timeout" ]
},
{
+ "key": "mctPopup",
+ "implementation": "directives/MCTPopup.js",
+ "depends": [ "$compile", "popupService" ]
+ },
+ {
"key": "mctScrollX",
"implementation": "directives/MCTScroll.js",
"depends": [ "$parse", "MCT_SCROLL_X_PROPERTY", "MCT_SCROLL_X_ATTRIBUTE" ]
@@ -226,6 +251,10 @@
{
"key": "selector",
"templateUrl": "templates/controls/selector.html"
+ },
+ {
+ "key": "datetime-picker",
+ "templateUrl": "templates/controls/datetime-picker.html"
}
],
"licenses": [
diff --git a/platform/commonUI/general/res/sass/_constants.scss b/platform/commonUI/general/res/sass/_constants.scss
index c3851a715..e7026e8e8 100644
--- a/platform/commonUI/general/res/sass/_constants.scss
+++ b/platform/commonUI/general/res/sass/_constants.scss
@@ -45,6 +45,7 @@ $ueEditToolBarH: 25px;
$ueBrowseLeftPaneW: 25%;
$ueEditLeftPaneW: 75%;
$treeSearchInputBarH: 25px;
+$ueTimeControlH: (33px, 20px, 20px);
// Overlay
$ovrTopBarH: 45px;
$ovrFooterH: 24px;
diff --git a/platform/commonUI/general/res/sass/_global.scss b/platform/commonUI/general/res/sass/_global.scss
index 3f2393871..45b868922 100644
--- a/platform/commonUI/general/res/sass/_global.scss
+++ b/platform/commonUI/general/res/sass/_global.scss
@@ -40,11 +40,11 @@
/************************** HTML ENTITIES */
a {
- color: #ccc;
+ color: $colorA;
cursor: pointer;
text-decoration: none;
&:hover {
- color: #fff;
+ color: $colorAHov;
}
}
@@ -125,6 +125,14 @@ mct-container {
text-align: center;
}
+.scrolling {
+ overflow: auto;
+}
+
+.vscroll {
+ overflow-y: auto;
+}
+
.no-margin {
margin: 0;
}
diff --git a/platform/commonUI/general/res/sass/_icons.scss b/platform/commonUI/general/res/sass/_icons.scss
index 1f6ec7a34..1208b3e7c 100644
--- a/platform/commonUI/general/res/sass/_icons.scss
+++ b/platform/commonUI/general/res/sass/_icons.scss
@@ -29,6 +29,9 @@
}
.ui-symbol {
+ &.type-icon {
+ color: $colorObjHdrIc;
+ }
&.icon {
color: $colorKey;
&.alert {
@@ -41,6 +44,9 @@
font-size: 1.65em;
}
}
+ &.icon-calendar:after {
+ content: "\e605";
+ }
}
.bar .ui-symbol {
@@ -52,7 +58,7 @@
display: inline-block;
}
-.s-menu .invoke-menu,
+.s-menu-btn .invoke-menu,
.icon.major .invoke-menu {
margin-left: $interiorMarginSm;
}
diff --git a/platform/commonUI/general/res/sass/_mixins.scss b/platform/commonUI/general/res/sass/_mixins.scss
index 14c56edfb..784b05466 100644
--- a/platform/commonUI/general/res/sass/_mixins.scss
+++ b/platform/commonUI/general/res/sass/_mixins.scss
@@ -364,9 +364,10 @@
/* This doesn't work on an element inside an element with absolute positioning that has height: auto */
//position: relative;
top: 50%;
- -webkit-transform: translateY(-50%);
- -ms-transform: translateY(-50%);
- transform: translateY(-50%);
+ @include webkitProp(transform, translateY(-50%));
+ //-webkit-transform: translateY(-50%);
+ //-ms-transform: translateY(-50%);
+ //transform: translateY(-50%);
}
@mixin verticalCenterBlock($holderH, $itemH) {
@@ -391,22 +392,8 @@
overflow-y: $showBar;
}
-@mixin wait-spinner($b: 5px, $c: $colorAlt1) {
- display: block;
- position: absolute;
- -webkit-animation: rotation .6s infinite linear;
- -moz-animation: rotation .6s infinite linear;
- -o-animation: rotation .6s infinite linear;
- animation: rotation .6s infinite linear;
- border-color: rgba($c, 0.25);
- border-top-color: rgba($c, 1.0);
- border-style: solid;
- border-width: $b;
- border-radius: 100%;
-}
-
@mixin test($c: #ffcc00, $a: 0.2) {
- background-color: rgba($c, $a);
+ background-color: rgba($c, $a) !important;
}
@mixin tmpBorder($c: #ffcc00, $a: 0.75) {
diff --git a/platform/commonUI/general/res/sass/_views.scss b/platform/commonUI/general/res/sass/_views.scss
index ef83e3c29..96c78f1dd 100644
--- a/platform/commonUI/general/res/sass/_views.scss
+++ b/platform/commonUI/general/res/sass/_views.scss
@@ -10,9 +10,6 @@
&.fixed {
font-size: 0.8em;
}
- &.scrolling {
- overflow: auto;
- }
.controls,
label,
.inline-block {
diff --git a/platform/commonUI/general/res/sass/controls/_controls.scss b/platform/commonUI/general/res/sass/controls/_controls.scss
index 0757e4dfe..f7457d9dd 100644
--- a/platform/commonUI/general/res/sass/controls/_controls.scss
+++ b/platform/commonUI/general/res/sass/controls/_controls.scss
@@ -177,7 +177,7 @@ label.checkbox.custom {
}
}
-.s-menu label.checkbox.custom {
+.s-menu-btn label.checkbox.custom {
margin-left: 5px;
}
@@ -349,49 +349,155 @@ label.checkbox.custom {
.slider {
$knobH: 100%; //14px;
- $knobW: 12px;
- $slotH: 50%;
.slot {
// @include border-radius($basicCr * .75);
- @include sliderTrack();
- height: $slotH;
+ //@include sliderTrack();
width: auto;
position: absolute;
- top: ($knobH - $slotH) / 2;
+ top: 0;
right: 0;
- bottom: auto;
+ bottom: 0;
left: 0;
}
.knob {
- @include btnSubtle();
- @include controlGrippy(rgba(black, 0.3), vertical, 1px, solid);
- cursor: ew-resize;
+ //@include btnSubtle();
+ //@include controlGrippy(rgba(black, 0.3), vertical, 1px, solid);
+ @include trans-prop-nice-fade(.25s);
+ background-color: $sliderColorKnob;
+ &:hover {
+ background-color: $sliderColorKnobHov;
+ }
position: absolute;
height: $knobH;
- width: $knobW;
+ width: $sliderKnobW;
top: 0;
auto: 0;
bottom: auto;
left: auto;
- &:before {
- top: 1px;
- bottom: 3px;
- left: ($knobW / 2) - 1;
- }
-
+ }
+ .knob-l {
+ @include border-left-radius($sliderKnobW);
+ cursor: w-resize;
+ }
+ .knob-r {
+ @include border-right-radius($sliderKnobW);
+ cursor: e-resize;
}
.range {
- background: rgba($colorKey, 0.6);
+ @include trans-prop-nice-fade(.25s);
+ background-color: $sliderColorRange;
cursor: ew-resize;
position: absolute;
- top: 0;
+ top: 0; //$tbOffset;
right: auto;
bottom: 0;
left: auto;
height: auto;
width: auto;
&:hover {
- background: rgba($colorKey, 0.7);
+ background-color: $sliderColorRangeHov;
+ }
+ }
+}
+
+/******************************************************** DATETIME PICKER */
+.l-datetime-picker {
+ $r1H: 15px;
+ @include user-select(none);
+ font-size: 0.8rem;
+ padding: $interiorMarginLg !important;
+ width: 230px;
+ .l-month-year-pager {
+ $pagerW: 20px;
+ //@include test();
+ //font-size: 0.8rem;
+ height: $r1H;
+ margin-bottom: $interiorMargin;
+ position: relative;
+ .pager,
+ .val {
+ //@include test(red);
+ @extend .abs;
+ }
+ .pager {
+ width: $pagerW;
+ @extend .ui-symbol;
+ &.prev {
+ right: auto;
+ &:before {
+ content: "\3c";
+ }
+ }
+ &.next {
+ left: auto;
+ text-align: right;
+ &:before {
+ content: "\3e";
+ }
+ }
+ }
+ .val {
+ text-align: center;
+ left: $pagerW + $interiorMargin;
+ right: $pagerW + $interiorMargin;
+ }
+ }
+ .l-calendar,
+ .l-time-selects {
+ border-top: 1px solid $colorInteriorBorder
+ }
+ .l-time-selects {
+ line-height: $formInputH;
+ }
+}
+
+/******************************************************** CALENDAR */
+.l-calendar {
+ $colorMuted: pushBack($colorMenuFg, 30%);
+ ul.l-cal-row {
+ @include display-flex;
+ @include flex-flow(row nowrap);
+ margin-top: 1px;
+ &:first-child {
+ margin-top: 0;
+ }
+ li {
+ @include flex(1 0);
+ //@include test();
+ margin-left: 1px;
+ padding: $interiorMargin;
+ text-align: center;
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+ &.l-header li {
+ color: $colorMuted;
+ }
+ &.l-body li {
+ @include trans-prop-nice(background-color, .25s);
+ cursor: pointer;
+ &.in-month {
+ background-color: $colorCalCellInMonthBg;
+ }
+ .sub {
+ color: $colorMuted;
+ font-size: 0.8em;
+ }
+ &.selected {
+ background: $colorCalCellSelectedBg;
+ color: $colorCalCellSelectedFg;
+ .sub {
+ color: inherit;
+ }
+ }
+ &:hover {
+ background-color: $colorCalCellHovBg;
+ color: $colorCalCellHovFg;
+ .sub {
+ color: inherit;
+ }
+ }
}
}
}
diff --git a/platform/commonUI/general/res/sass/controls/_menus.scss b/platform/commonUI/general/res/sass/controls/_menus.scss
index d9aecfd1c..49693b3c5 100644
--- a/platform/commonUI/general/res/sass/controls/_menus.scss
+++ b/platform/commonUI/general/res/sass/controls/_menus.scss
@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/******************************************************** MENU BUTTONS */
-.s-menu {
+.s-menu-btn {
// Formerly .btn-menu
@extend .s-btn;
span.l-click-area {
@@ -62,186 +62,192 @@
/******************************************************** MENUS THEMSELVES */
.menu-element {
- $bg: $colorMenuBg;
- $fg: $colorMenuFg;
- $ic: $colorMenuIc;
cursor: pointer;
position: relative;
- .menu {
- @include border-radius($basicCr);
- @include containerSubtle($bg, $fg);
- @include boxShdw($shdwMenu);
- @include txtShdw($shdwMenuText);
- display: block; // set to block via jQuery
- padding: $interiorMarginSm 0;
- position: absolute;
- z-index: 10;
- ul {
- @include menuUlReset();
- li {
- @include box-sizing(border-box);
- border-top: 1px solid lighten($bg, 20%);
- color: pullForward($bg, 60%);
- line-height: $menuLineH;
- padding: $interiorMarginSm $interiorMargin * 2 $interiorMarginSm ($interiorMargin * 2) + $treeTypeIconW;
- position: relative;
- white-space: nowrap;
- &:first-child {
- border: none;
- }
- &:hover {
- background: $colorMenuHovBg;
- color: $colorMenuHovFg;
- .icon {
- color: $colorMenuHovIc;
- }
- }
- .type-icon {
- left: $interiorMargin * 2;
- }
- }
- }
- }
+}
+
+.s-menu {
+ @include border-radius($basicCr);
+ @include containerSubtle($colorMenuBg, $colorMenuFg);
+ @include boxShdw($shdwMenu);
+ @include txtShdw($shdwMenuText);
+ padding: $interiorMarginSm 0;
+}
- .menu,
- .context-menu,
- .super-menu {
- pointer-events: auto;
- ul li {
- //padding-left: 25px;
- a {
- color: $fg;
+.menu {
+ @extend .s-menu;
+ display: block;
+ position: absolute;
+ z-index: 10;
+ ul {
+ @include menuUlReset();
+ li {
+ @include box-sizing(border-box);
+ border-top: 1px solid lighten($colorMenuBg, 20%);
+ color: pullForward($colorMenuBg, 60%);
+ line-height: $menuLineH;
+ padding: $interiorMarginSm $interiorMargin * 2 $interiorMarginSm ($interiorMargin * 2) + $treeTypeIconW;
+ position: relative;
+ white-space: nowrap;
+ &:first-child {
+ border: none;
}
- .icon {
- color: $ic;
+ &:hover {
+ background: $colorMenuHovBg;
+ color: $colorMenuHovFg;
+ .icon {
+ color: $colorMenuHovIc;
+ }
}
.type-icon {
- left: $interiorMargin;
- }
- &:hover .icon {
- //color: lighten($ic, 5%);
+ left: $interiorMargin * 2;
}
}
}
+}
+
+.menu,
+.context-menu,
+.super-menu {
+ pointer-events: auto;
+ ul li {
+ //padding-left: 25px;
+ a {
+ color: $colorMenuFg;
+ }
+ .icon {
+ color: $colorMenuIc;
+ }
+ .type-icon {
+ left: $interiorMargin;
+ }
+ &:hover .icon {
+ //color: lighten($colorMenuIc, 5%);
+ }
+ }
+}
- .checkbox-menu {
- // Used in search dropdown in tree
- @extend .context-menu;
- ul li {
- padding-left: 50px;
- .checkbox {
- $d: 0.7rem;
- position: absolute;
- left: $interiorMargin;
- top: ($menuLineH - $d) / 1.5;
- em {
+.checkbox-menu {
+ // Used in search dropdown in tree
+ @extend .context-menu;
+ ul li {
+ padding-left: 50px;
+ .checkbox {
+ $d: 0.7rem;
+ position: absolute;
+ left: $interiorMargin;
+ top: ($menuLineH - $d) / 1.5;
+ em {
+ height: $d;
+ width: $d;
+ &:before {
+ font-size: 7px !important;// $d/2;
height: $d;
width: $d;
- &:before {
- font-size: 7px !important;// $d/2;
- height: $d;
- width: $d;
- line-height: $d;
- }
+ line-height: $d;
}
}
- .type-icon {
- left: 25px;
- }
+ }
+ .type-icon {
+ left: 25px;
}
}
+}
- .super-menu {
- $w: 500px;
- $h: $w - 20;
- $plw: 50%;
- $prw: 50%;
- display: block;
- width: $w;
- height: $h;
- .contents {
- @include absPosDefault($interiorMargin);
+.super-menu {
+ $w: 500px;
+ $h: $w - 20;
+ $plw: 50%;
+ $prw: 50%;
+ display: block;
+ width: $w;
+ height: $h;
+ .contents {
+ @include absPosDefault($interiorMargin);
+ }
+ .pane {
+ @include box-sizing(border-box);
+ &.left {
+ //@include test();
+ border-right: 1px solid pullForward($colorMenuBg, 10%);
+ left: 0;
+ padding-right: $interiorMargin;
+ right: auto;
+ width: $plw;
+ overflow-x: hidden;
+ overflow-y: auto;
+ ul {
+ li {
+ @include border-radius($controlCr);
+ padding-left: 30px;
+ border-top: none;
+ }
+ }
}
- .pane {
- @include box-sizing(border-box);
- &.left {
- //@include test();
- border-right: 1px solid pullForward($colorMenuBg, 10%);
+ &.right {
+ //@include test(red);
+ left: auto;
+ right: 0;
+ padding: $interiorMargin * 5;
+ width: $prw;
+ }
+ }
+ .menu-item-description {
+ .desc-area {
+ &.icon {
+ $h: 150px;
+ color: $colorCreateMenuLgIcon;
+ position: relative;
+ font-size: 8em;
left: 0;
- padding-right: $interiorMargin;
- right: auto;
- width: $plw;
- overflow-x: hidden;
- overflow-y: auto;
- ul {
- li {
- @include border-radius($controlCr);
- padding-left: 30px;
- border-top: none;
- }
- }
+ height: $h;
+ line-height: $h;
+ margin-bottom: $interiorMargin * 5;
+ text-align: center;
}
- &.right {
- //@include test(red);
- left: auto;
- right: 0;
- padding: $interiorMargin * 5;
- width: $prw;
+ &.title {
+ color: $colorCreateMenuText;
+ font-size: 1.2em;
+ margin-bottom: 0.5em;
}
- }
- .menu-item-description {
- .desc-area {
- &.icon {
- $h: 150px;
- color: $colorCreateMenuLgIcon;
- position: relative;
- font-size: 8em;
- left: 0;
- height: $h;
- line-height: $h;
- margin-bottom: $interiorMargin * 5;
- text-align: center;
- }
- &.title {
- color: $colorCreateMenuText;
- font-size: 1.2em;
- margin-bottom: 0.5em;
- }
- &.description {
- //color: lighten($bg, 30%);
- color: $colorCreateMenuText;
- font-size: 0.8em;
- line-height: 1.5em;
- }
+ &.description {
+ //color: lighten($colorMenuBg, 30%);
+ color: $colorCreateMenuText;
+ font-size: 0.8em;
+ line-height: 1.5em;
}
}
}
- .context-menu {
- font-size: 0.80rem;
- }
+}
+.context-menu {
+ font-size: 0.80rem;
}
-.context-menu-holder {
- pointer-events: none;
+.context-menu-holder,
+.menu-holder {
position: absolute;
- height: 200px;
- width: 170px;
z-index: 70;
.context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%;
- .context-menu {
- }
}
- &.go-left .context-menu {
+ &.go-left .context-menu,
+ &.go-left .menu {
right: 0;
}
- &.go-up .context-menu {
+ &.go-up .context-menu,
+ &.go-up .menu {
bottom: 0;
}
}
+.context-menu-holder {
+ pointer-events: none;
+ height: 200px;
+ width: 170px;
+}
+
.btn-bar.right .menu,
.menus-to-left .menu {
left: auto;
diff --git a/platform/commonUI/general/res/sass/controls/_time-controller.scss b/platform/commonUI/general/res/sass/controls/_time-controller.scss
index 6991617ae..75f9c2a87 100644
--- a/platform/commonUI/general/res/sass/controls/_time-controller.scss
+++ b/platform/commonUI/general/res/sass/controls/_time-controller.scss
@@ -1,72 +1,155 @@
-.l-time-controller {
- $inputTxtW: 90px;
- $knobW: 9px;
- $r1H: 20px;
- $r2H: 30px;
- $r3H: 10px;
+@mixin toiLineHovEffects() {
+ //@include pulse(.25s);
+ &:before,
+ &:after {
+ background-color: $timeControllerToiLineColorHov;
+ }
+}
+
+.l-time-controller-visible {
+
+}
- position: relative;
- margin: $interiorMarginLg 0;
- min-width: 400px;
+mct-include.l-time-controller {
+ $minW: 500px;
+ $knobHOffset: 0px;
+ $knobM: ($sliderKnobW + $knobHOffset) * -1;
+ $rangeValPad: $interiorMargin;
+ $rangeValOffset: $sliderKnobW;
+ //$knobCr: $sliderKnobW;
+ $timeRangeSliderLROffset: 130px + $sliderKnobW + $rangeValOffset;
+ $r1H: nth($ueTimeControlH,1);
+ $r2H: nth($ueTimeControlH,2);
+ $r3H: nth($ueTimeControlH,3);
+
+ @include absPosDefault();
+ //@include test();
+ display: block;
+ top: auto;
+ height: $r1H + $r2H + $r3H + ($interiorMargin * 2);
+ min-width: $minW;
+ font-size: 0.8rem;
.l-time-range-inputs-holder,
.l-time-range-slider {
- font-size: 0.8em;
+ //font-size: 0.8em;
}
.l-time-range-inputs-holder,
.l-time-range-slider-holder,
.l-time-range-ticks-holder
{
- margin-bottom: $interiorMargin;
- position: relative;
+ //@include test();
+ @include absPosDefault(0, visible);
+ @include box-sizing(border-box);
+ top: auto;
}
.l-time-range-slider,
.l-time-range-ticks {
//@include test(red, 0.1);
@include absPosDefault(0, visible);
+ left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset;
}
.l-time-range-inputs-holder {
- height: $r1H;
- }
-
- .l-time-range-slider,
- .l-time-range-ticks {
- left: $inputTxtW; right: $inputTxtW;
-
+ //@include test(red);
+ height: $r1H; bottom: $r2H + $r3H + ($interiorMarginSm * 2);
+ padding-top: $interiorMargin;
+ border-top: 1px solid $colorInteriorBorder;
+ .type-icon {
+ font-size: 120%;
+ vertical-align: middle;
+ }
+ .l-time-range-input,
+ .l-time-range-inputs-elem {
+ margin-right: $interiorMargin;
+ .lbl {
+ color: $colorPlotLabelFg;
+ }
+ .ui-symbol.icon {
+ font-size: 11px;
+ width: 11px;
+ }
+ }
}
.l-time-range-slider-holder {
- height: $r2H;
+ //@include test(green);
+ height: $r2H; bottom: $r3H + ($interiorMarginSm * 1);
.range-holder {
@include box-shadow(none);
background: none;
border: none;
- height: 75%;
+ .range {
+ .toi-line {
+ $myC: $timeControllerToiLineColor;
+ $myW: 8px;
+ @include transform(translateX(50%));
+ position: absolute;
+ //@include test();
+ top: 0; right: 0; bottom: 0px; left: auto;
+ width: $myW;
+ height: auto;
+ z-index: 2;
+ &:before,
+ &:after {
+ background-color: $myC;
+ content: "";
+ position: absolute;
+ }
+ &:before {
+ // Vert line
+ top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1;
+ width: 2px;
+ //top: 0; right: 3px; bottom: 0; left: 3px;
+ }
+ &:after {
+ // Circle element
+ @include border-radius($myW);
+ @include transform(translateY(-50%));
+ top: 50%; right: 0; bottom: auto; left: 0;
+ width: auto;
+ height: $myW;
+ }
+ }
+ &:hover .toi-line {
+ @include toiLineHovEffects;
+ }
+ }
+ }
+ &:not(:active) {
+ //@include test(#ff00cc);
+ .knob,
+ .range {
+ @include transition-property(left, right);
+ @include transition-duration(500ms);
+ @include transition-timing-function(ease-in-out);
+ }
}
}
.l-time-range-ticks-holder {
height: $r3H;
.l-time-range-ticks {
- border-top: 1px solid $colorInteriorBorder;
+ border-top: 1px solid $colorTick;
.tick {
- background-color: $colorInteriorBorder;
+ background-color: $colorTick;
border:none;
+ height: 5px;
width: 1px;
margin-left: -1px;
+ position: absolute;
&:first-child {
margin-left: 0;
}
.l-time-range-tick-label {
- color: lighten($colorInteriorBorder, 20%);
- font-size: 0.7em;
+ @include webkitProp(transform, translateX(-50%));
+ color: $colorPlotLabelFg;
+ display: inline-block;
+ font-size: 0.9em;
position: absolute;
- margin-left: -0.5 * $tickLblW;
- text-align: center;
- top: $r3H;
- width: $tickLblW;
+ top: 8px;
+ white-space: nowrap;
z-index: 2;
}
}
@@ -74,31 +157,47 @@
}
.knob {
- width: $knobW;
+ z-index: 2;
.range-value {
- $w: 75px;
- //@include test();
+ //@include test($sliderColorRange);
+ @include trans-prop-nice-fade(.25s);
+ padding: 0 $rangeValOffset;
position: absolute;
- top: 50%;
- margin-top: -7px; // Label is 13px high
+ height: $r2H;
+ line-height: $r2H;
white-space: nowrap;
- width: $w;
}
&:hover .range-value {
- color: $colorKey;
+ color: $sliderColorKnobHov;
}
&.knob-l {
- margin-left: $knobW / -2;
+ //@include border-bottom-left-radius($knobCr); // MOVED TO _CONTROLS.SCSS
+ margin-left: $knobM;
.range-value {
text-align: right;
- right: $knobW + $interiorMargin;
+ right: $rangeValOffset;
}
}
&.knob-r {
- margin-right: $knobW / -2;
+ //@include border-bottom-right-radius($knobCr);
+ margin-right: $knobM;
.range-value {
- left: $knobW + $interiorMargin;
+ left: $rangeValOffset;
+ }
+ &:hover + .range-holder .range .toi-line {
+ @include toiLineHovEffects;
}
}
}
+}
+
+//.slot.range-holder {
+// background-color: $sliderColorRangeHolder;
+//}
+
+.s-time-range-val {
+ //@include test();
+ @include border-radius($controlCr);
+ background-color: $colorInputBg;
+ padding: 1px 1px 0 $interiorMargin;
} \ No newline at end of file
diff --git a/platform/commonUI/general/res/sass/forms/_datetime.scss b/platform/commonUI/general/res/sass/forms/_datetime.scss
index 8378fdbab..dbfacdeb0 100644
--- a/platform/commonUI/general/res/sass/forms/_datetime.scss
+++ b/platform/commonUI/general/res/sass/forms/_datetime.scss
@@ -19,39 +19,44 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
+@mixin complexFieldHolder($myW) {
+ width: $myW + $interiorMargin;
+ input[type="text"] {
+ width: $myW;
+ }
+}
+
.complex.datetime {
span {
display: inline-block;
margin-right: $interiorMargin;
}
-
+
+/*
.field-hints,
.fields {
}
+
.field-hints {
-
+
}
-
+ */
+
.fields {
margin-top: $interiorMarginSm 0;
padding: $interiorMarginSm 0;
}
.date {
- $myW: 80px;
- width: $myW + $interiorMargin;
- input {
- width: $myW;
- }
-
+ @include complexFieldHolder(80px);
+ }
+
+ .time.md {
+ @include complexFieldHolder(60px);
}
.time.sm {
- $myW: 40px;
- width: $myW + $interiorMargin;
- input {
- width: $myW;
- }
+ @include complexFieldHolder(40px);
}
} \ No newline at end of file
diff --git a/platform/commonUI/general/res/sass/forms/_selects.scss b/platform/commonUI/general/res/sass/forms/_selects.scss
index 2eda20dd6..027678369 100644
--- a/platform/commonUI/general/res/sass/forms/_selects.scss
+++ b/platform/commonUI/general/res/sass/forms/_selects.scss
@@ -21,10 +21,13 @@
*****************************************************************************/
.select {
@include btnSubtle($colorSelectBg);
- margin: 0 0 2px 2px; // Needed to avoid dropshadow from being clipped by parent containers
+ @if $shdwBtns != none {
+ margin: 0 0 2px 0; // Needed to avoid dropshadow from being clipped by parent containers
+ }
padding: 0 $interiorMargin;
overflow: hidden;
position: relative;
+ line-height: $formInputH;
select {
@include appearance(none);
@include box-sizing(border-box);
@@ -40,11 +43,8 @@
}
&:after {
@include contextArrow();
+ pointer-events: none;
color: rgba($colorSelectFg, percentToDecimal($contrastInvokeMenuPercent));
- //content:"v";
- //display: block;
- //font-family: 'symbolsfont';
- //pointer-events: none;
position: absolute;
right: $interiorMargin; top: 0;
}
diff --git a/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss b/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss
index f80c1f197..86c23a266 100644
--- a/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss
+++ b/platform/commonUI/general/res/sass/helpers/_wait-spinner.scss
@@ -19,24 +19,45 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-@-webkit-keyframes rotation {
- from {-webkit-transform: rotate(0deg);}
- to {-webkit-transform: rotate(359deg);}
+@include keyframes(rotation) {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(359deg); }
}
-@-moz-keyframes rotation {
- from {-moz-transform: rotate(0deg);}
- to {-moz-transform: rotate(359deg);}
+@mixin wait-spinner2($b: 5px, $c: $colorAlt1) {
+ @include keyframes(rotateCentered) {
+ 0% { transform: translateX(-50%) translateY(-50%) rotate(0deg); }
+ 100% { transform: translateX(-50%) translateY(-50%) rotate(359deg); }
+ }
+ @include animation-name(rotateCentered);
+ @include animation-duration(0.5s);
+ @include animation-iteration-count(infinite);
+ @include animation-timing-function(linear);
+ border-color: rgba($c, 0.25);
+ border-top-color: rgba($c, 1.0);
+ border-style: solid;
+ border-width: 5px;
+ @include border-radius(100%);
+ @include box-sizing(border-box);
+ display: block;
+ position: absolute;
+ height: 0; width: 0;
+ padding: 7%;
+ left: 50%; top: 50%;
}
-@-o-keyframes rotation {
- from {-o-transform: rotate(0deg);}
- to {-o-transform: rotate(359deg);}
-}
-
-@keyframes rotation {
- from {transform: rotate(0deg);}
- to {transform: rotate(359deg);}
+@mixin wait-spinner($b: 5px, $c: $colorAlt1) {
+ display: block;
+ position: absolute;
+ -webkit-animation: rotation .6s infinite linear;
+ -moz-animation: rotation .6s infinite linear;
+ -o-animation: rotation .6s infinite linear;
+ animation: rotation .6s infinite linear;
+ border-color: rgba($c, 0.25);
+ border-top-color: rgba($c, 1.0);
+ border-style: solid;
+ border-width: $b;
+ @include border-radius(100%);
}
.t-wait-spinner,
@@ -96,4 +117,28 @@
margin-top: 0 !important;
padding: 0 !important;
top: 0; left: 0;
+}
+
+.loading {
+ // Can be applied to any block element with height and width
+ pointer-events: none;
+ &:before,
+ &:after {
+ content: '';
+ }
+ &:before {
+ @include wait-spinner2(5px, $colorLoadingFg);
+ z-index: 10;
+ }
+ &:after {
+ @include absPosDefault();
+ background: $colorLoadingBg;
+ display: block;
+ z-index: 9;
+ }
+ &.tree-item:before {
+ padding: $menuLineH / 4;
+ border-width: 2px;
+ }
+
} \ No newline at end of file
diff --git a/platform/commonUI/general/res/sass/lists/_tabular.scss b/platform/commonUI/general/res/sass/lists/_tabular.scss
index 161e7511a..559f7324a 100644
--- a/platform/commonUI/general/res/sass/lists/_tabular.scss
+++ b/platform/commonUI/general/res/sass/lists/_tabular.scss
@@ -40,6 +40,11 @@ table {
thead, .thead {
border-bottom: 1px solid $colorTabHeaderBorder;
}
+
+ &:not(.fixed-header) tr th {
+ background-color: $colorTabHeaderBg;
+ }
+
tbody, .tbody {
display: table-row-group;
tr, .tr {
@@ -64,7 +69,6 @@ table {
display: table-cell;
}
th, .th {
- background-color: $colorTabHeaderBg;
border-left: 1px solid $colorTabHeaderBorder;
color: $colorTabHeaderFg;
padding: $tabularTdPadLR $tabularTdPadLR;
@@ -143,7 +147,7 @@ table {
position: absolute;
width: 100%;
height: $tabularHeaderH;
- background: rgba(#fff, 0.15);
+ background-color: $colorTabHeaderBg;
}
}
tbody, .tbody {
diff --git a/platform/commonUI/general/res/sass/plots/_plots-main.scss b/platform/commonUI/general/res/sass/plots/_plots-main.scss
index 67acb46e2..c1ce9fa92 100644
--- a/platform/commonUI/general/res/sass/plots/_plots-main.scss
+++ b/platform/commonUI/general/res/sass/plots/_plots-main.scss
@@ -89,7 +89,7 @@ $plotDisplayArea: ($legendH + $interiorMargin, 0, $xBarH + $interiorMargin, $yBa
.gl-plot-label,
.l-plot-label {
// @include test(yellow);
- color: lighten($colorBodyFg, 20%);
+ color: $colorPlotLabelFg;
position: absolute;
text-align: center;
// text-transform: uppercase;
diff --git a/platform/commonUI/general/res/sass/search/_search.scss b/platform/commonUI/general/res/sass/search/_search.scss
index 3aa349d6f..85fadc501 100644
--- a/platform/commonUI/general/res/sass/search/_search.scss
+++ b/platform/commonUI/general/res/sass/search/_search.scss
@@ -214,8 +214,6 @@
.search-scroll {
order: 3;
-
- //padding-right: $rightPadding;
margin-top: 4px;
// Adjustable scrolling size
@@ -227,28 +225,6 @@
.load-icon {
position: relative;
- &.loading {
- pointer-events: none;
- margin-left: $leftMargin;
-
- .title-label {
- // Text styling
- font-style: italic;
- font-size: .9em;
- opacity: 0.5;
-
- // Text positioning
- margin-left: $iconWidth + $leftMargin;
- line-height: 24px;
- }
- .wait-spinner {
- margin-left: $leftMargin;
- }
- }
-
- &:not(.loading) {
- cursor: pointer;
- }
}
.load-more-button {
diff --git a/platform/commonUI/general/res/sass/tree/_tree.scss b/platform/commonUI/general/res/sass/tree/_tree.scss
index 2c0343a6c..d64f456b2 100644
--- a/platform/commonUI/general/res/sass/tree/_tree.scss
+++ b/platform/commonUI/general/res/sass/tree/_tree.scss
@@ -83,7 +83,6 @@ ul.tree {
.icon {
&.l-icon-link,
&.l-icon-alert {
- //@include txtShdw($shdwItemTreeIcon);
position: absolute;
z-index: 2;
}
@@ -105,26 +104,12 @@ ul.tree {
@include absPosDefault();
display: block;
left: $runningItemW + ($interiorMargin * 3);
- //right: $treeContextTriggerW + $interiorMargin;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
- &.loading {
- pointer-events: none;
- .label {
- opacity: 0.5;
- .title-label {
- font-style: italic;
- }
- }
- .wait-spinner {
- margin-left: 14px;
- }
- }
-
&.selected {
background: $colorItemTreeSelectedBg;
color: $colorItemTreeSelectedFg;
@@ -142,9 +127,6 @@ ul.tree {
&:hover {
background: rgba($colorBodyFg, 0.1); //lighten($colorBodyBg, 5%);
color: pullForward($colorBodyFg, 20%);
- //.context-trigger {
- // display: block;
- //}
.icon {
color: $colorItemTreeIconHover;
}
@@ -158,7 +140,6 @@ ul.tree {
.context-trigger {
$h: 0.9rem;
- //display: none;
top: -1px;
position: absolute;
right: $interiorMarginSm;
diff --git a/platform/commonUI/general/res/sass/user-environ/_frame.scss b/platform/commonUI/general/res/sass/user-environ/_frame.scss
index d8b27d769..d1a38d401 100644
--- a/platform/commonUI/general/res/sass/user-environ/_frame.scss
+++ b/platform/commonUI/general/res/sass/user-environ/_frame.scss
@@ -47,7 +47,7 @@
}
&.frame-template {
.s-btn,
- .s-menu {
+ .s-menu-btn {
height: $ohH;
line-height: $ohH;
padding: 0 $interiorMargin;
@@ -56,7 +56,7 @@
}
}
- .s-menu:after {
+ .s-menu-btn:after {
font-size: 8px;
}
diff --git a/platform/commonUI/general/res/templates/controls/datetime-picker.html b/platform/commonUI/general/res/templates/controls/datetime-picker.html
new file mode 100644
index 000000000..bca8064b7
--- /dev/null
+++ b/platform/commonUI/general/res/templates/controls/datetime-picker.html
@@ -0,0 +1,66 @@
+<!--
+ 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 ng-controller="DateTimePickerController" class="l-datetime-picker s-datetime-picker s-menu">
+ <div class="holder">
+ <div class="l-month-year-pager">
+ <a class="pager prev" ng-click="changeMonth(-1)"></a>
+ <span class="val">{{month}} {{year}}</span>
+ <a class="pager next" ng-click="changeMonth(1)"></a>
+ </div>
+ <div class="l-calendar">
+ <ul class="l-cal-row l-header">
+ <li ng-repeat="day in ['Su','Mo','Tu','We','Th','Fr','Sa']">{{day}}</li>
+ </ul>
+ <ul class="l-cal-row l-body" ng-repeat="row in table">
+ <li ng-repeat="cell in row"
+ ng-click="select(cell)"
+ ng-class='{ "in-month": isInCurrentMonth(cell), selected: isSelected(cell) }'>
+ <div class="prime">{{cell.day}}</div>
+ <div class="sub">{{cell.dayOfYear}}</div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="l-time-selects complex datetime"
+ ng-show="options">
+ <div class="field-hints">
+ <span class="hint time md"
+ ng-repeat="key in ['hours', 'minutes', 'seconds']"
+ ng-if="options[key]">
+ {{nameFor(key)}}
+ </span>
+ </div>
+ <div>
+ <span class="field control time md"
+ ng-repeat="key in ['hours', 'minutes', 'seconds']"
+ ng-if="options[key]">
+ <div class='form-control select'>
+ <select size="1"
+ ng-model="time[key]"
+ ng-options="i for i in optionsFor(key)">
+ </select>
+ </div>
+ </span>
+ </div>
+ </div>
+</div>
diff --git a/platform/commonUI/general/res/templates/controls/switcher.html b/platform/commonUI/general/res/templates/controls/switcher.html
index c47263ddf..5e849f865 100644
--- a/platform/commonUI/general/res/templates/controls/switcher.html
+++ b/platform/commonUI/general/res/templates/controls/switcher.html
@@ -21,7 +21,7 @@
-->
<span ng-controller="ViewSwitcherController">
<div
- class="view-switcher menu-element s-menu"
+ class="view-switcher menu-element s-menu-btn"
ng-if="view.length > 1"
ng-controller="ClickAwayController as toggle"
>
diff --git a/platform/commonUI/general/res/templates/controls/time-controller.html b/platform/commonUI/general/res/templates/controls/time-controller.html
index fe1b6ff14..300e56c38 100644
--- a/platform/commonUI/general/res/templates/controls/time-controller.html
+++ b/platform/commonUI/general/res/templates/controls/time-controller.html
@@ -1,69 +1,108 @@
<!--
+ 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.
-NOTES
+ 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.
-Ticks:
-The thinking is to divide whatever the current time span is by 5,
-and assign values accordingly to 5 statically-positioned ticks. So the tick x-position is a static percentage
-of the total width available, and the labels change dynamically. This is consistent
-with our current approach to the time axis of plots.
-I'm keeping the number of ticks low so that when the view portal gets narrow,
-the tick labels won't collide with each other. For extra credit, add/remove ticks as the user resizes the view area.
-Note: this eval needs to be based on the whatever is containing the
-time-controller component, not the whole browser window.
-
-Range indicator and slider knobs:
-The left and right properties used in .slider .range-holder and the .knobs are
-CSS offsets from the left and right of their respective containers. You
-may want or need to calculate those positions as pure offsets from the start datetime
-(or left, as it were) and set them as left properties. No problem if so, but
-we'll need to tweak the CSS tiny bit to get the center of the knobs to line up
-properly on the range left and right bounds.
+ 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 ng-controller="TimeRangeController">
+ <div class="l-time-range-inputs-holder">
+ <span class="l-time-range-inputs-elem ui-symbol type-icon">&#x43;</span>
+ <span class="l-time-range-input" ng-controller="ToggleController as t1">
+ <!--<span class="lbl">Start</span>-->
+ <span class="s-btn time-range-start">
+ <input type="text"
+ ng-model="boundsModel.start"
+ ng-class="{ error: !boundsModel.startValid }">
+ </input>
+ <a class="ui-symbol icon icon-calendar" ng-click="t1.toggle()"></a>
+ <mct-popup ng-if="t1.isActive()">
+ <div mct-click-elsewhere="t1.setState(false)">
+ <mct-control key="'datetime-picker'"
+ ng-model="ngModel.outer"
+ field="'start'"
+ options="{ hours: true }">
+ </mct-control>
+ </div>
+ </mct-popup>
+ </span>
+ </span>
-<div ng-init="
-notes = 'Temporarily using an array to populate ticks so I can see what I\'m doing';
-ticks = [
-'00:00',
-'00:30',
-'01:00',
-'01:30',
-'02:00'
-];
-"></div>
+ <span class="l-time-range-inputs-elem lbl">to</span>
-<div class="l-time-controller">
- <div class="l-time-range-inputs-holder">
- Start: <input type="date" />
- End: <input type="date" />
- </div>
+ <span class="l-time-range-input" ng-controller="ToggleController as t2">
+ <!--<span class="lbl">End</span>-->
+ <span class="s-btn l-time-range-input">
+ <input type="text"
+ ng-model="boundsModel.end"
+ ng-class="{ error: !boundsModel.endValid }">
+ </input>
+ <a class="ui-symbol icon icon-calendar" ng-click="t2.toggle()">
+ </a>
+ <mct-popup ng-if="t2.isActive()">
+ <div mct-click-elsewhere="t2.setState(false)">
+ <mct-control key="'datetime-picker'"
+ ng-model="ngModel.outer"
+ field="'end'"
+ options="{ hours: true }">
+ </mct-control>
+ </div>
+ </mct-popup>
+ </span>&nbsp;
+ </span>
+ </div>
- <div class="l-time-range-slider-holder">
- <div class="l-time-range-slider">
- <div class="slider">
- <div class="slot range-holder">
- <div class="range" style="left: 0%; right: 30%;"></div>
- </div>
- <div class="knob knob-l" style="left: 0%;">
- <div class="range-value">05/22 14:46</div>
- </div>
- <div class="knob knob-r" style="right: 30%;">
- <div class="range-value">07/22 01:21</div>
- </div>
- </div>
- </div>
- </div>
+ <div class="l-time-range-slider-holder">
+ <div class="l-time-range-slider">
+ <div class="slider"
+ mct-resize="spanWidth = bounds.width">
+ <div class="knob knob-l"
+ mct-drag-down="startLeftDrag()"
+ mct-drag="leftDrag(delta[0])"
+ ng-style="{ left: startInnerPct }">
+ <div class="range-value">{{startInnerText}}</div>
+ </div>
+ <div class="knob knob-r"
+ mct-drag-down="startRightDrag()"
+ mct-drag="rightDrag(delta[0])"
+ ng-style="{ right: endInnerPct }">
+ <div class="range-value">{{endInnerText}}</div>
+ </div>
+ <div class="slot range-holder">
+ <div class="range"
+ mct-drag-down="startMiddleDrag()"
+ mct-drag="middleDrag(delta[0])"
+ ng-style="{ left: startInnerPct, right: endInnerPct}">
+ <div class="toi-line"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
- <div class="l-time-range-ticks-holder">
- <div class="l-time-range-ticks">
- <div
- ng-repeat="tick in ticks"
- ng-style="{ left: $index * 25 + '%' }"
- class="tick tick-x"
- >
- <span class="l-time-range-tick-label">{{tick}}</span>
- </div>
- </div>
- </div>
-</div> \ No newline at end of file
+ <div class="l-time-range-ticks-holder">
+ <div class="l-time-range-ticks">
+ <div
+ ng-repeat="tick in ticks"
+ ng-style="{ left: $index * (100 / (ticks.length - 1)) + '%' }"
+ class="tick tick-x"
+ >
+ <span class="l-time-range-tick-label">{{tick}}</span>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/platform/commonUI/general/src/controllers/DateTimePickerController.js b/platform/commonUI/general/src/controllers/DateTimePickerController.js
new file mode 100644
index 000000000..ac07d7755
--- /dev/null
+++ b/platform/commonUI/general/src/controllers/DateTimePickerController.js
@@ -0,0 +1,202 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,Promise*/
+
+define(
+ [ 'moment' ],
+ function (moment) {
+ 'use strict';
+
+ var TIME_NAMES = {
+ 'hours': "Hour",
+ 'minutes': "Minute",
+ 'seconds': "Second"
+ },
+ MONTHS = moment.months(),
+ TIME_OPTIONS = (function makeRanges() {
+ var arr = [];
+ while (arr.length < 60) {
+ arr.push(arr.length);
+ }
+ return {
+ hours: arr.slice(0, 24),
+ minutes: arr,
+ seconds: arr
+ };
+ }());
+
+ /**
+ * Controller to support the date-time picker.
+ *
+ * Adds/uses the following properties in scope:
+ * * `year`: Year being displayed in picker
+ * * `month`: Month being displayed
+ * * `table`: Table being displayed; array of arrays of
+ * * `day`: Day of month
+ * * `dayOfYear`: Day of year
+ * * `month`: Month associated with the day
+ * * `year`: Year associated with the day.
+ * * `date`: Date chosen
+ * * `year`: Year selected
+ * * `month`: Month selected (0-indexed)
+ * * `day`: Day of month selected
+ * * `time`: Chosen time (hours/minutes/seconds)
+ * * `hours`: Hours chosen
+ * * `minutes`: Minutes chosen
+ * * `seconds`: Seconds chosen
+ *
+ * Months are zero-indexed, day-of-months are one-indexed.
+ */
+ function DateTimePickerController($scope, now) {
+ var year,
+ month, // For picker state, not model state
+ interacted = false;
+
+ function generateTable() {
+ var m = moment.utc({ year: year, month: 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;
+ }
+
+ function updateScopeForMonth() {
+ $scope.month = MONTHS[month];
+ $scope.year = year;
+ $scope.table = generateTable();
+ }
+
+ function updateFromModel(ngModel) {
+ var m;
+
+ m = moment.utc(ngModel);
+
+ $scope.date = {
+ year: m.year(),
+ month: m.month(),
+ day: m.date()
+ };
+ $scope.time = {
+ hours: m.hour(),
+ minutes: m.minute(),
+ seconds: m.second()
+ };
+
+ //window.alert($scope.date.day + " " + ngModel);
+
+ // Zoom to that date in the picker, but
+ // only if the user hasn't interacted with it yet.
+ if (!interacted) {
+ year = m.year();
+ month = m.month();
+ updateScopeForMonth();
+ }
+ }
+
+ function updateFromView() {
+ var m = moment.utc({
+ year: $scope.date.year,
+ month: $scope.date.month,
+ day: $scope.date.day,
+ hour: $scope.time.hours,
+ minute: $scope.time.minutes,
+ second: $scope.time.seconds
+ });
+ $scope.ngModel[$scope.field] = m.valueOf();
+ }
+
+ $scope.isInCurrentMonth = function (cell) {
+ return cell.month === month;
+ };
+
+ $scope.isSelected = function (cell) {
+ var date = $scope.date || {};
+ return cell.day === date.day &&
+ cell.month === date.month &&
+ cell.year === date.year;
+ };
+
+ $scope.select = function (cell) {
+ $scope.date = $scope.date || {};
+ $scope.date.month = cell.month;
+ $scope.date.year = cell.year;
+ $scope.date.day = cell.day;
+ updateFromView();
+ };
+
+ $scope.dateEquals = function (d1, d2) {
+ return d1.year === d2.year &&
+ d1.month === d2.month &&
+ d1.day === d2.day;
+ };
+
+ $scope.changeMonth = function (delta) {
+ month += delta;
+ if (month > 11) {
+ month = 0;
+ year += 1;
+ }
+ if (month < 0) {
+ month = 11;
+ year -= 1;
+ }
+ interacted = true;
+ updateScopeForMonth();
+ };
+
+ $scope.nameFor = function (key) {
+ return TIME_NAMES[key];
+ };
+
+ $scope.optionsFor = function (key) {
+ return TIME_OPTIONS[key];
+ };
+
+ updateScopeForMonth();
+
+ // Ensure some useful default
+ $scope.ngModel[$scope.field] =
+ $scope.ngModel[$scope.field] === undefined ?
+ now() : $scope.ngModel[$scope.field];
+
+ $scope.$watch('ngModel[field]', updateFromModel);
+ $scope.$watchCollection('date', updateFromView);
+ $scope.$watchCollection('time', updateFromView);
+ }
+
+ return DateTimePickerController;
+ }
+);
diff --git a/platform/commonUI/general/src/controllers/TimeRangeController.js b/platform/commonUI/general/src/controllers/TimeRangeController.js
new file mode 100644
index 000000000..d4fb21be0
--- /dev/null
+++ b/platform/commonUI/general/src/controllers/TimeRangeController.js
@@ -0,0 +1,302 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,Promise*/
+
+define(
+ ['moment'],
+ function (moment) {
+ "use strict";
+
+ var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss",
+ TICK_SPACING_PX = 150;
+
+ /**
+ * @memberof platform/commonUI/general
+ * @constructor
+ */
+ function TimeConductorController($scope, now) {
+ var tickCount = 2,
+ innerMinimumSpan = 1000, // 1 second
+ outerMinimumSpan = 1000 * 60 * 60, // 1 hour
+ initialDragValue;
+
+ function formatTimestamp(ts) {
+ return moment.utc(ts).format(DATE_FORMAT);
+ }
+
+ function parseTimestamp(text) {
+ var m = moment.utc(text, DATE_FORMAT);
+ if (m.isValid()) {
+ return m.valueOf();
+ } else {
+ throw new Error("Could not parse " + text);
+ }
+ }
+
+ // From 0.0-1.0 to "0%"-"1%"
+ function toPercent(p) {
+ return (100 * p) + "%";
+ }
+
+ function updateTicks() {
+ var i, p, ts, start, end, span;
+ end = $scope.ngModel.outer.end;
+ start = $scope.ngModel.outer.start;
+ span = end - start;
+ $scope.ticks = [];
+ for (i = 0; i < tickCount; i += 1) {
+ p = i / (tickCount - 1);
+ ts = p * span + start;
+ $scope.ticks.push(formatTimestamp(ts));
+ }
+ }
+
+ function updateSpanWidth(w) {
+ tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2);
+ updateTicks();
+ }
+
+ function updateViewForInnerSpanFromModel(ngModel) {
+ var span = ngModel.outer.end - ngModel.outer.start;
+
+ // Expose readable dates for the knobs
+ $scope.startInnerText = formatTimestamp(ngModel.inner.start);
+ $scope.endInnerText = formatTimestamp(ngModel.inner.end);
+
+ // And positions for the knobs
+ $scope.startInnerPct =
+ toPercent((ngModel.inner.start - ngModel.outer.start) / span);
+ $scope.endInnerPct =
+ toPercent((ngModel.outer.end - ngModel.inner.end) / span);
+ }
+
+ function defaultBounds() {
+ var t = now();
+ return {
+ start: t - 24 * 3600 * 1000, // One day
+ end: t
+ };
+ }
+
+ function copyBounds(bounds) {
+ return { start: bounds.start, end: bounds.end };
+ }
+
+ function updateBoundsTextForProperty(ngModel, property) {
+ try {
+ if (!$scope.boundsModel[property] ||
+ parseTimestamp($scope.boundsModel[property]) !==
+ ngModel.outer[property]) {
+ $scope.boundsModel[property] =
+ formatTimestamp(ngModel.outer[property]);
+ }
+ } catch (e) {
+ // User-entered text is invalid, so leave it be
+ // until they fix it.
+ }
+ }
+
+ function updateBoundsText(ngModel) {
+ updateBoundsTextForProperty(ngModel, 'start');
+ updateBoundsTextForProperty(ngModel, 'end');
+ }
+
+ function updateViewFromModel(ngModel) {
+ var t = now();
+
+ ngModel = ngModel || {};
+ ngModel.outer = ngModel.outer || defaultBounds();
+ ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
+
+ // First, dates for the date pickers for outer bounds
+ updateBoundsText(ngModel);
+
+ // Then various updates for the inner span
+ updateViewForInnerSpanFromModel(ngModel);
+
+ // Stick it back is scope (in case we just set defaults)
+ $scope.ngModel = ngModel;
+
+ updateTicks();
+ }
+
+ function startLeftDrag() {
+ initialDragValue = $scope.ngModel.inner.start;
+ }
+
+ function startRightDrag() {
+ initialDragValue = $scope.ngModel.inner.end;
+ }
+
+ function startMiddleDrag() {
+ initialDragValue = {
+ start: $scope.ngModel.inner.start,
+ end: $scope.ngModel.inner.end
+ };
+ }
+
+ function toMillis(pixels) {
+ var span = $scope.ngModel.outer.end - $scope.ngModel.outer.start;
+ return (pixels / $scope.spanWidth) * span;
+ }
+
+ function clamp(value, low, high) {
+ return Math.max(low, Math.min(high, value));
+ }
+
+ function leftDrag(pixels) {
+ var delta = toMillis(pixels);
+ $scope.ngModel.inner.start = clamp(
+ initialDragValue + delta,
+ $scope.ngModel.outer.start,
+ $scope.ngModel.inner.end - innerMinimumSpan
+ );
+ updateViewFromModel($scope.ngModel);
+ }
+
+ function rightDrag(pixels) {
+ var delta = toMillis(pixels);
+ $scope.ngModel.inner.end = clamp(
+ initialDragValue + delta,
+ $scope.ngModel.inner.start + innerMinimumSpan,
+ $scope.ngModel.outer.end
+ );
+ updateViewFromModel($scope.ngModel);
+ }
+
+ function middleDrag(pixels) {
+ var delta = toMillis(pixels),
+ edge = delta < 0 ? 'start' : 'end',
+ opposite = delta < 0 ? 'end' : 'start';
+
+ // Adjust the position of the edge in the direction of drag
+ $scope.ngModel.inner[edge] = clamp(
+ initialDragValue[edge] + delta,
+ $scope.ngModel.outer.start,
+ $scope.ngModel.outer.end
+ );
+ // Adjust opposite knob to maintain span
+ $scope.ngModel.inner[opposite] = $scope.ngModel.inner[edge] +
+ initialDragValue[opposite] - initialDragValue[edge];
+
+ updateViewFromModel($scope.ngModel);
+ }
+
+ function updateOuterStart(t) {
+ var ngModel = $scope.ngModel;
+
+ ngModel.outer.start = t;
+
+ ngModel.outer.end = Math.max(
+ ngModel.outer.start + outerMinimumSpan,
+ ngModel.outer.end
+ );
+
+ ngModel.inner.start =
+ Math.max(ngModel.outer.start, ngModel.inner.start);
+ ngModel.inner.end = Math.max(
+ ngModel.inner.start + innerMinimumSpan,
+ ngModel.inner.end
+ );
+
+ updateViewForInnerSpanFromModel(ngModel);
+ updateTicks();
+ }
+
+ function updateOuterEnd(t) {
+ var ngModel = $scope.ngModel;
+
+ ngModel.outer.end = t;
+
+ ngModel.outer.start = Math.min(
+ ngModel.outer.end - outerMinimumSpan,
+ ngModel.outer.start
+ );
+
+ ngModel.inner.end =
+ Math.min(ngModel.outer.end, ngModel.inner.end);
+ ngModel.inner.start = Math.min(
+ ngModel.inner.end - innerMinimumSpan,
+ ngModel.inner.start
+ );
+
+ updateViewForInnerSpanFromModel(ngModel);
+ updateTicks();
+ }
+
+ function updateStartFromText(value) {
+ try {
+ updateOuterStart(parseTimestamp(value));
+ updateBoundsTextForProperty($scope.ngModel, 'end');
+ $scope.boundsModel.startValid = true;
+ } catch (e) {
+ $scope.boundsModel.startValid = false;
+ return;
+ }
+ }
+
+ function updateEndFromText(value) {
+ try {
+ updateOuterEnd(parseTimestamp(value));
+ updateBoundsTextForProperty($scope.ngModel, 'start');
+ $scope.boundsModel.endValid = true;
+ } catch (e) {
+ $scope.boundsModel.endValid = false;
+ return;
+ }
+ }
+
+ function updateStartFromPicker(value) {
+ updateOuterStart(value);
+ updateBoundsText($scope.ngModel);
+ }
+
+ function updateEndFromPicker(value) {
+ updateOuterEnd(value);
+ updateBoundsText($scope.ngModel);
+ }
+
+ $scope.startLeftDrag = startLeftDrag;
+ $scope.startRightDrag = startRightDrag;
+ $scope.startMiddleDrag = startMiddleDrag;
+ $scope.leftDrag = leftDrag;
+ $scope.rightDrag = rightDrag;
+ $scope.middleDrag = middleDrag;
+
+ $scope.state = false;
+ $scope.ticks = [];
+ $scope.boundsModel = {};
+
+ // Initialize scope to defaults
+ updateViewFromModel($scope.ngModel);
+
+ $scope.$watchCollection("ngModel", updateViewFromModel);
+ $scope.$watch("spanWidth", updateSpanWidth);
+ $scope.$watch("ngModel.outer.start", updateStartFromPicker);
+ $scope.$watch("ngModel.outer.end", updateEndFromPicker);
+ $scope.$watch("boundsModel.start", updateStartFromText);
+ $scope.$watch("boundsModel.end", updateEndFromText);
+ }
+
+ return TimeConductorController;
+ }
+);
diff --git a/platform/commonUI/general/src/directives/MCTClickElsewhere.js b/platform/commonUI/general/src/directives/MCTClickElsewhere.js
new file mode 100644
index 000000000..1bcdbbe6b
--- /dev/null
+++ b/platform/commonUI/general/src/directives/MCTClickElsewhere.js
@@ -0,0 +1,77 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ /**
+ * The `mct-click-elsewhere` directive will evaluate its
+ * associated expression whenever a `mousedown` occurs anywhere
+ * outside of the element that has the `mct-click-elsewhere`
+ * directive attached. This is useful for dismissing popups
+ * and the like.
+ */
+ function MCTClickElsewhere($document) {
+
+ // Link; install event handlers.
+ function link(scope, element, attrs) {
+ // Keep a reference to the body, to attach/detach
+ // mouse event handlers; mousedown and mouseup cannot
+ // only be attached to the element being linked, as the
+ // mouse may leave this element during the drag.
+ var body = $document.find('body');
+
+ function clickBody(event) {
+ var x = event.clientX,
+ y = event.clientY,
+ rect = element[0].getBoundingClientRect(),
+ xMin = rect.left,
+ xMax = xMin + rect.width,
+ yMin = rect.top,
+ yMax = yMin + rect.height;
+
+ if (x < xMin || x > xMax || y < yMin || y > yMax) {
+ scope.$eval(attrs.mctClickElsewhere);
+ }
+ }
+
+ body.on("mousedown", clickBody);
+ scope.$on("$destroy", function () {
+ body.off("mousedown", clickBody);
+ });
+ }
+
+ return {
+ // mct-drag only makes sense as an attribute
+ restrict: "A",
+ // Link function, to install event handlers
+ link: link
+ };
+ }
+
+ return MCTClickElsewhere;
+ }
+);
+
diff --git a/platform/commonUI/general/src/directives/MCTPopup.js b/platform/commonUI/general/src/directives/MCTPopup.js
new file mode 100644
index 000000000..d5ced4129
--- /dev/null
+++ b/platform/commonUI/general/src/directives/MCTPopup.js
@@ -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.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ function () {
+ 'use strict';
+
+ var TEMPLATE = "<div></div>";
+
+ /**
+ * The `mct-popup` directive may be used to display elements
+ * which "pop up" over other parts of the page. Typically, this is
+ * done in conjunction with an `ng-if` to control the visibility
+ * of the popup.
+ *
+ * Example of usage:
+ *
+ * <mct-popup ng-if="someExpr">
+ * <span>These are the contents of the popup!</span>
+ * </mct-popup>
+ *
+ * @constructor
+ * @memberof platform/commonUI/general
+ * @param $compile Angular's $compile service
+ * @param {platform/commonUI/general.PopupService} popupService
+ */
+ function MCTPopup($compile, popupService) {
+ function link(scope, element, attrs, ctrl, transclude) {
+ var div = $compile(TEMPLATE)(scope),
+ rect = element.parent()[0].getBoundingClientRect(),
+ position = [ rect.left, rect.top ],
+ popup = popupService.display(div, position);
+
+ transclude(function (clone) {
+ div.append(clone);
+ });
+
+ scope.$on('$destroy', function () {
+ popup.dismiss();
+ });
+ }
+
+ return {
+ restrict: "E",
+ transclude: true,
+ link: link,
+ scope: {}
+ };
+ }
+
+ return MCTPopup;
+ }
+);
diff --git a/platform/commonUI/general/src/directives/MCTResize.js b/platform/commonUI/general/src/directives/MCTResize.js
index 7f2d72280..f0fd8e0a6 100644
--- a/platform/commonUI/general/src/directives/MCTResize.js
+++ b/platform/commonUI/general/src/directives/MCTResize.js
@@ -58,6 +58,7 @@ define(
// Link; start listening for changes to an element's size
function link(scope, element, attrs) {
var lastBounds,
+ linking = true,
active = true;
// Determine how long to wait before the next update
@@ -74,7 +75,9 @@ define(
lastBounds.width !== bounds.width ||
lastBounds.height !== bounds.height) {
scope.$eval(attrs.mctResize, { bounds: bounds });
- scope.$apply(); // Trigger a digest
+ if (!linking) { // Avoid apply-in-a-digest
+ scope.$apply();
+ }
lastBounds = bounds;
}
}
@@ -101,6 +104,9 @@ define(
// Handle the initial callback
onInterval();
+
+ // Trigger scope.$apply on subsequent changes
+ linking = false;
}
return {
diff --git a/platform/commonUI/general/src/services/Popup.js b/platform/commonUI/general/src/services/Popup.js
new file mode 100644
index 000000000..6029ca29c
--- /dev/null
+++ b/platform/commonUI/general/src/services/Popup.js
@@ -0,0 +1,89 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ function () {
+ "use strict";
+
+ /**
+ * A popup is an element that has been displayed at a particular
+ * location within the page.
+ * @constructor
+ * @memberof platform/commonUI/general
+ * @param element the jqLite-wrapped element
+ * @param {object} styles an object containing key-value pairs
+ * of styles used to position the element.
+ */
+ function Popup(element, styles) {
+ this.styles = styles;
+ this.element = element;
+
+ element.css(styles);
+ }
+
+ /**
+ * Stop showing this popup.
+ */
+ Popup.prototype.dismiss = function () {
+ this.element.remove();
+ };
+
+ /**
+ * Check if this popup is positioned such that it appears to the
+ * left of its original location.
+ * @returns {boolean} true if the popup goes left
+ */
+ Popup.prototype.goesLeft = function () {
+ return !this.styles.left;
+ };
+
+ /**
+ * Check if this popup is positioned such that it appears to the
+ * right of its original location.
+ * @returns {boolean} true if the popup goes right
+ */
+ Popup.prototype.goesRight = function () {
+ return !this.styles.right;
+ };
+
+ /**
+ * Check if this popup is positioned such that it appears above
+ * its original location.
+ * @returns {boolean} true if the popup goes up
+ */
+ Popup.prototype.goesUp = function () {
+ return !this.styles.top;
+ };
+
+ /**
+ * Check if this popup is positioned such that it appears below
+ * its original location.
+ * @returns {boolean} true if the popup goes down
+ */
+ Popup.prototype.goesDown = function () {
+ return !this.styles.bottom;
+ };
+
+ return Popup;
+ }
+);
diff --git a/platform/commonUI/general/src/services/PopupService.js b/platform/commonUI/general/src/services/PopupService.js
new file mode 100644
index 000000000..f834609f2
--- /dev/null
+++ b/platform/commonUI/general/src/services/PopupService.js
@@ -0,0 +1,127 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./Popup'],
+ function (Popup) {
+ "use strict";
+
+ /**
+ * Displays popup elements at specific positions within the document.
+ * @memberof platform/commonUI/general
+ * @constructor
+ */
+ function PopupService($document, $window) {
+ this.$document = $document;
+ this.$window = $window;
+ }
+
+ /**
+ * Options controlling how the popup is displaed.
+ *
+ * @typedef PopupOptions
+ * @memberof platform/commonUI/general
+ * @property {number} [offsetX] the horizontal distance, in pixels,
+ * to offset the element in whichever direction it is
+ * displayed. Defaults to 0.
+ * @property {number} [offsetY] the vertical distance, in pixels,
+ * to offset the element in whichever direction it is
+ * displayed. Defaults to 0.
+ * @property {number} [marginX] the horizontal position, in pixels,
+ * after which to prefer to display the element to the left.
+ * If negative, this is relative to the right edge of the
+ * page. Defaults to half the window's width.
+ * @property {number} [marginY] the vertical position, in pixels,
+ * after which to prefer to display the element upward.
+ * If negative, this is relative to the right edge of the
+ * page. Defaults to half the window's height.
+ * @property {string} [leftClass] class to apply when shifting to the left
+ * @property {string} [rightClass] class to apply when shifting to the right
+ * @property {string} [upClass] class to apply when shifting upward
+ * @property {string} [downClass] class to apply when shifting downward
+ */
+
+ /**
+ * Display a popup at a particular location. The location chosen will
+ * be the corner of the element; the element will be positioned either
+ * to the left or the right of this point depending on available
+ * horizontal space, and will similarly be shifted upward or downward
+ * depending on available vertical space.
+ *
+ * @param element the jqLite-wrapped DOM element to pop up
+ * @param {number[]} position x,y position of the element, in
+ * pixel coordinates. Negative values are interpreted as
+ * relative to the right or bottom of the window.
+ * @param {PopupOptions} [options] additional options to control
+ * positioning of the popup
+ * @returns {platform/commonUI/general.Popup} the popup
+ */
+ PopupService.prototype.display = function (element, position, options) {
+ var $document = this.$document,
+ $window = this.$window,
+ body = $document.find('body'),
+ winDim = [ $window.innerWidth, $window.innerHeight ],
+ styles = { position: 'absolute' },
+ margin,
+ offset,
+ bubble;
+
+ function adjustNegatives(value, index) {
+ return value < 0 ? (value + winDim[index]) : value;
+ }
+
+ // Defaults
+ options = options || {};
+ offset = [
+ options.offsetX !== undefined ? options.offsetX : 0,
+ options.offsetY !== undefined ? options.offsetY : 0
+ ];
+ margin = [ options.marginX, options.marginY ].map(function (m, i) {
+ return m === undefined ? (winDim[i] / 2) : m;
+ }).map(adjustNegatives);
+
+ position = position.map(adjustNegatives);
+
+ if (position[0] > margin[0]) {
+ styles.right = (winDim[0] - position[0] + offset[0]) + 'px';
+ } else {
+ styles.left = (position[0] + offset[0]) + 'px';
+ }
+
+ if (position[1] > margin[1]) {
+ styles.bottom = (winDim[1] - position[1] + offset[1]) + 'px';
+ } else {
+ styles.top = (position[1] + offset[1]) + 'px';
+ }
+
+ // Add the menu to the body
+ body.append(element);
+
+ // Return a function to dismiss the bubble
+ return new Popup(element, styles);
+ };
+
+ return PopupService;
+ }
+);
+
diff --git a/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js b/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js
new file mode 100644
index 000000000..957df1b36
--- /dev/null
+++ b/platform/commonUI/general/test/controllers/DateTimePickerControllerSpec.js
@@ -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.
+ *****************************************************************************/
+/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
+
+define(
+ ["../../src/controllers/DateTimePickerController"],
+ function (DateTimePickerController) {
+ "use strict";
+
+ describe("The DateTimePickerController", function () {
+ var mockScope,
+ mockNow,
+ controller;
+
+ function fireWatch(expr, value) {
+ mockScope.$watch.calls.forEach(function (call) {
+ if (call.args[0] === expr) {
+ call.args[1](value);
+ }
+ });
+ }
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj(
+ "$scope",
+ [ "$apply", "$watch", "$watchCollection" ]
+ );
+ mockScope.ngModel = {};
+ mockScope.field = "testField";
+ mockNow = jasmine.createSpy('now');
+ controller = new DateTimePickerController(mockScope, mockNow);
+ });
+
+ it("watches the model that was passed in", function () {
+ expect(mockScope.$watch).toHaveBeenCalledWith(
+ "ngModel[field]",
+ jasmine.any(Function)
+ );
+ });
+
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js
new file mode 100644
index 000000000..91d3ecb9d
--- /dev/null
+++ b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js
@@ -0,0 +1,237 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
+
+define(
+ ["../../src/controllers/TimeRangeController", "moment"],
+ function (TimeRangeController, moment) {
+ "use strict";
+
+ var SEC = 1000,
+ MIN = 60 * SEC,
+ HOUR = 60 * MIN,
+ DAY = 24 * HOUR;
+
+ describe("The TimeRangeController", function () {
+ var mockScope,
+ mockNow,
+ controller;
+
+ function fireWatch(expr, value) {
+ mockScope.$watch.calls.forEach(function (call) {
+ if (call.args[0] === expr) {
+ call.args[1](value);
+ }
+ });
+ }
+
+ function fireWatchCollection(expr, value) {
+ mockScope.$watchCollection.calls.forEach(function (call) {
+ if (call.args[0] === expr) {
+ call.args[1](value);
+ }
+ });
+ }
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj(
+ "$scope",
+ [ "$apply", "$watch", "$watchCollection" ]
+ );
+ mockNow = jasmine.createSpy('now');
+ controller = new TimeRangeController(mockScope, mockNow);
+ });
+
+ it("watches the model that was passed in", function () {
+ expect(mockScope.$watchCollection)
+ .toHaveBeenCalledWith("ngModel", jasmine.any(Function));
+ });
+
+ describe("when dragged", function () {
+ beforeEach(function () {
+ mockScope.ngModel = {
+ outer: {
+ start: DAY * 1000,
+ end: DAY * 1001
+ },
+ inner: {
+ start: DAY * 1000 + HOUR * 3,
+ end: DAY * 1001 - HOUR * 3
+ }
+ };
+ mockScope.spanWidth = 1000;
+ fireWatch("spanWidth", mockScope.spanWidth);
+ fireWatchCollection("ngModel", mockScope.ngModel);
+ });
+
+ it("updates the start time for left drags", function () {
+ mockScope.startLeftDrag();
+ mockScope.leftDrag(250);
+ expect(mockScope.ngModel.inner.start)
+ .toEqual(DAY * 1000 + HOUR * 9);
+ });
+
+ it("updates the end time for right drags", function () {
+ mockScope.startRightDrag();
+ mockScope.rightDrag(-250);
+ expect(mockScope.ngModel.inner.end)
+ .toEqual(DAY * 1000 + HOUR * 15);
+ });
+
+ it("updates both start and end for middle drags", function () {
+ mockScope.startMiddleDrag();
+ mockScope.middleDrag(-125);
+ expect(mockScope.ngModel.inner).toEqual({
+ start: DAY * 1000,
+ end: DAY * 1000 + HOUR * 18
+ });
+ mockScope.middleDrag(250);
+ expect(mockScope.ngModel.inner).toEqual({
+ start: DAY * 1000 + HOUR * 6,
+ end: DAY * 1001
+ });
+ });
+
+ it("enforces a minimum inner span", function () {
+ mockScope.startRightDrag();
+ mockScope.rightDrag(-9999999);
+ expect(mockScope.ngModel.inner.end)
+ .toBeGreaterThan(mockScope.ngModel.inner.start);
+ });
+ });
+
+ describe("when outer bounds are changed", function () {
+ beforeEach(function () {
+ mockScope.ngModel = {
+ outer: {
+ start: DAY * 1000,
+ end: DAY * 1001
+ },
+ inner: {
+ start: DAY * 1000 + HOUR * 3,
+ end: DAY * 1001 - HOUR * 3
+ }
+ };
+ mockScope.spanWidth = 1000;
+ fireWatch("spanWidth", mockScope.spanWidth);
+ fireWatchCollection("ngModel", mockScope.ngModel);
+ });
+
+ it("enforces a minimum outer span", function () {
+ mockScope.ngModel.outer.end =
+ mockScope.ngModel.outer.start - DAY * 100;
+ fireWatch(
+ "ngModel.outer.end",
+ mockScope.ngModel.outer.end
+ );
+ expect(mockScope.ngModel.outer.end)
+ .toBeGreaterThan(mockScope.ngModel.outer.start);
+
+ mockScope.ngModel.outer.start =
+ mockScope.ngModel.outer.end + DAY * 100;
+ fireWatch(
+ "ngModel.outer.start",
+ mockScope.ngModel.outer.start
+ );
+ expect(mockScope.ngModel.outer.end)
+ .toBeGreaterThan(mockScope.ngModel.outer.start);
+ });
+
+ it("enforces a minimum inner span when outer span changes", function () {
+ mockScope.ngModel.outer.end =
+ mockScope.ngModel.outer.start - DAY * 100;
+ fireWatch(
+ "ngModel.outer.end",
+ mockScope.ngModel.outer.end
+ );
+ expect(mockScope.ngModel.inner.end)
+ .toBeGreaterThan(mockScope.ngModel.inner.start);
+ });
+
+ describe("by typing", function () {
+ it("updates models", function () {
+ var newStart = "1977-05-25 17:30:00",
+ newEnd = "2015-12-18 03:30:00";
+
+ mockScope.boundsModel.start = newStart;
+ fireWatch("boundsModel.start", newStart);
+ expect(mockScope.ngModel.outer.start)
+ .toEqual(moment.utc(newStart).valueOf());
+ expect(mockScope.boundsModel.startValid)
+ .toBeTruthy();
+
+ mockScope.boundsModel.end = newEnd;
+ fireWatch("boundsModel.end", newEnd);
+ expect(mockScope.ngModel.outer.end)
+ .toEqual(moment.utc(newEnd).valueOf());
+ expect(mockScope.boundsModel.endValid)
+ .toBeTruthy();
+ });
+
+ it("displays error state", function () {
+ var newStart = "Not a date",
+ newEnd = "Definitely not a date",
+ oldStart = mockScope.ngModel.outer.start,
+ oldEnd = mockScope.ngModel.outer.end;
+
+ mockScope.boundsModel.start = newStart;
+ fireWatch("boundsModel.start", newStart);
+ expect(mockScope.ngModel.outer.start)
+ .toEqual(oldStart);
+ expect(mockScope.boundsModel.startValid)
+ .toBeFalsy();
+
+ mockScope.boundsModel.end = newEnd;
+ fireWatch("boundsModel.end", newEnd);
+ expect(mockScope.ngModel.outer.end)
+ .toEqual(oldEnd);
+ expect(mockScope.boundsModel.endValid)
+ .toBeFalsy();
+ });
+
+ it("does not modify user input", function () {
+ // Don't want the controller "fixing" bad or
+ // irregularly-formatted input out from under
+ // the user's fingertips.
+ var newStart = "Not a date",
+ newEnd = "2015-3-3 01:02:04",
+ oldStart = mockScope.ngModel.outer.start,
+ oldEnd = mockScope.ngModel.outer.end;
+
+ mockScope.boundsModel.start = newStart;
+ fireWatch("boundsModel.start", newStart);
+ expect(mockScope.boundsModel.start)
+ .toEqual(newStart);
+
+ mockScope.boundsModel.end = newEnd;
+ fireWatch("boundsModel.end", newEnd);
+ expect(mockScope.boundsModel.end)
+ .toEqual(newEnd);
+ });
+ });
+ });
+
+
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js b/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js
new file mode 100644
index 000000000..9fa17763f
--- /dev/null
+++ b/platform/commonUI/general/test/directives/MCTClickElsewhereSpec.js
@@ -0,0 +1,84 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,jasmine*/
+
+define(
+ ["../../src/directives/MCTClickElsewhere"],
+ function (MCTClickElsewhere) {
+ "use strict";
+
+ var JQLITE_METHODS = [ "on", "off", "find", "parent" ];
+
+ describe("The mct-click-elsewhere directive", function () {
+ var mockDocument,
+ mockScope,
+ mockElement,
+ testAttrs,
+ mockBody,
+ mockParentEl,
+ testRect,
+ mctClickElsewhere;
+
+ function testEvent(x, y) {
+ return {
+ pageX: x,
+ pageY: y,
+ preventDefault: jasmine.createSpy("preventDefault")
+ };
+ }
+
+ beforeEach(function () {
+ mockDocument =
+ jasmine.createSpyObj("$document", JQLITE_METHODS);
+ mockScope =
+ jasmine.createSpyObj("$scope", [ "$eval", "$apply", "$on" ]);
+ mockElement =
+ jasmine.createSpyObj("element", JQLITE_METHODS);
+ mockBody =
+ jasmine.createSpyObj("body", JQLITE_METHODS);
+ mockParentEl =
+ jasmine.createSpyObj("parent", ["getBoundingClientRect"]);
+
+ testAttrs = {
+ mctClickElsewhere: "some Angular expression"
+ };
+ testRect = {
+ left: 20,
+ top: 42,
+ width: 60,
+ height: 75
+ };
+
+ mockDocument.find.andReturn(mockBody);
+
+ mctClickElsewhere = new MCTClickElsewhere(mockDocument);
+ mctClickElsewhere.link(mockScope, mockElement, testAttrs);
+ });
+
+ it("is valid as an attribute", function () {
+ expect(mctClickElsewhere.restrict).toEqual("A");
+ });
+
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/directives/MCTPopupSpec.js b/platform/commonUI/general/test/directives/MCTPopupSpec.js
new file mode 100644
index 000000000..2cd659818
--- /dev/null
+++ b/platform/commonUI/general/test/directives/MCTPopupSpec.js
@@ -0,0 +1,136 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,jasmine*/
+
+define(
+ ["../../src/directives/MCTPopup"],
+ function (MCTPopup) {
+ "use strict";
+
+ var JQLITE_METHODS = [ "on", "off", "find", "parent", "css", "append" ];
+
+ describe("The mct-popup directive", function () {
+ var mockCompile,
+ mockPopupService,
+ mockPopup,
+ mockScope,
+ mockElement,
+ testAttrs,
+ mockBody,
+ mockTransclude,
+ mockParentEl,
+ mockNewElement,
+ testRect,
+ mctPopup;
+
+ function testEvent(x, y) {
+ return {
+ pageX: x,
+ pageY: y,
+ preventDefault: jasmine.createSpy("preventDefault")
+ };
+ }
+
+ beforeEach(function () {
+ mockCompile =
+ jasmine.createSpy("$compile");
+ mockPopupService =
+ jasmine.createSpyObj("popupService", ["display"]);
+ mockPopup =
+ jasmine.createSpyObj("popup", ["dismiss"]);
+ mockScope =
+ jasmine.createSpyObj("$scope", [ "$eval", "$apply", "$on" ]);
+ mockElement =
+ jasmine.createSpyObj("element", JQLITE_METHODS);
+ mockBody =
+ jasmine.createSpyObj("body", JQLITE_METHODS);
+ mockTransclude =
+ jasmine.createSpy("transclude");
+ mockParentEl =
+ jasmine.createSpyObj("parent", ["getBoundingClientRect"]);
+ mockNewElement =
+ jasmine.createSpyObj("newElement", JQLITE_METHODS);
+
+ testAttrs = {
+ mctClickElsewhere: "some Angular expression"
+ };
+ testRect = {
+ left: 20,
+ top: 42,
+ width: 60,
+ height: 75
+ };
+
+ mockCompile.andCallFake(function () {
+ var mockFn = jasmine.createSpy();
+ mockFn.andReturn(mockNewElement);
+ return mockFn;
+ });
+ mockElement.parent.andReturn([mockParentEl]);
+ mockParentEl.getBoundingClientRect.andReturn(testRect);
+ mockPopupService.display.andReturn(mockPopup);
+
+ mctPopup = new MCTPopup(mockCompile, mockPopupService);
+
+ mctPopup.link(
+ mockScope,
+ mockElement,
+ testAttrs,
+ null,
+ mockTransclude
+ );
+ });
+
+ it("is valid as an element", function () {
+ expect(mctPopup.restrict).toEqual("E");
+ });
+
+ describe("creates an element which", function () {
+ it("displays as a popup", function () {
+ expect(mockPopupService.display).toHaveBeenCalledWith(
+ mockNewElement,
+ [ testRect.left, testRect.top ]
+ );
+ });
+
+ it("displays transcluded content", function () {
+ var mockClone =
+ jasmine.createSpyObj('clone', JQLITE_METHODS);
+ mockTransclude.mostRecentCall.args[0](mockClone);
+ expect(mockNewElement.append)
+ .toHaveBeenCalledWith(mockClone);
+ });
+
+ it("is removed when its containing scope is destroyed", function () {
+ expect(mockPopup.dismiss).not.toHaveBeenCalled();
+ mockScope.$on.calls.forEach(function (call) {
+ if (call.args[0] === '$destroy') {
+ call.args[1]();
+ }
+ });
+ expect(mockPopup.dismiss).toHaveBeenCalled();
+ });
+ });
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/services/PopupServiceSpec.js b/platform/commonUI/general/test/services/PopupServiceSpec.js
new file mode 100644
index 000000000..741d23bd3
--- /dev/null
+++ b/platform/commonUI/general/test/services/PopupServiceSpec.js
@@ -0,0 +1,98 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
+
+
+define(
+ ["../../src/services/PopupService"],
+ function (PopupService) {
+ 'use strict';
+
+ describe("PopupService", function () {
+ var mockDocument,
+ testWindow,
+ mockBody,
+ mockElement,
+ popupService;
+
+ beforeEach(function () {
+ mockDocument = jasmine.createSpyObj('$document', [ 'find' ]);
+ testWindow = { innerWidth: 1000, innerHeight: 800 };
+ mockBody = jasmine.createSpyObj('body', [ 'append' ]);
+ mockElement = jasmine.createSpyObj('element', [
+ 'css',
+ 'remove'
+ ]);
+
+ mockDocument.find.andCallFake(function (query) {
+ return query === 'body' && mockBody;
+ });
+
+ popupService = new PopupService(mockDocument, testWindow);
+ });
+
+ it("adds elements to the body of the document", function () {
+ popupService.display(mockElement, [ 0, 0 ]);
+ expect(mockBody.append).toHaveBeenCalledWith(mockElement);
+ });
+
+ describe("when positioned in appropriate quadrants", function () {
+ it("orients elements relative to the top-left", function () {
+ popupService.display(mockElement, [ 25, 50 ]);
+ expect(mockElement.css).toHaveBeenCalledWith({
+ position: 'absolute',
+ left: '25px',
+ top: '50px'
+ });
+ });
+
+ it("orients elements relative to the top-right", function () {
+ popupService.display(mockElement, [ 800, 50 ]);
+ expect(mockElement.css).toHaveBeenCalledWith({
+ position: 'absolute',
+ right: '200px',
+ top: '50px'
+ });
+ });
+
+ it("orients elements relative to the bottom-right", function () {
+ popupService.display(mockElement, [ 800, 650 ]);
+ expect(mockElement.css).toHaveBeenCalledWith({
+ position: 'absolute',
+ right: '200px',
+ bottom: '150px'
+ });
+ });
+
+ it("orients elements relative to the bottom-left", function () {
+ popupService.display(mockElement, [ 120, 650 ]);
+ expect(mockElement.css).toHaveBeenCalledWith({
+ position: 'absolute',
+ left: '120px',
+ bottom: '150px'
+ });
+ });
+ });
+
+ });
+ }
+);
diff --git a/platform/commonUI/general/test/services/PopupSpec.js b/platform/commonUI/general/test/services/PopupSpec.js
new file mode 100644
index 000000000..84d63953a
--- /dev/null
+++ b/platform/commonUI/general/test/services/PopupSpec.js
@@ -0,0 +1,74 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
+
+
+define(
+ ["../../src/services/Popup"],
+ function (Popup) {
+ 'use strict';
+
+ describe("Popup", function () {
+ var mockElement,
+ testStyles,
+ popup;
+
+ beforeEach(function () {
+ mockElement =
+ jasmine.createSpyObj('element', [ 'css', 'remove' ]);
+ testStyles = { left: '12px', top: '14px' };
+ popup = new Popup(mockElement, testStyles);
+ });
+
+ it("applies CSS styles when instantiated", function () {
+ expect(mockElement.css)
+ .toHaveBeenCalledWith(testStyles);
+ });
+
+ it("reports the orientation of the popup", function () {
+ var otherStyles = {
+ right: '12px',
+ bottom: '14px'
+ },
+ otherPopup = new Popup(mockElement, otherStyles);
+
+ expect(popup.goesLeft()).toBeFalsy();
+ expect(popup.goesRight()).toBeTruthy();
+ expect(popup.goesUp()).toBeFalsy();
+ expect(popup.goesDown()).toBeTruthy();
+
+ expect(otherPopup.goesLeft()).toBeTruthy();
+ expect(otherPopup.goesRight()).toBeFalsy();
+ expect(otherPopup.goesUp()).toBeTruthy();
+ expect(otherPopup.goesDown()).toBeFalsy();
+ });
+
+ it("removes elements when dismissed", function () {
+ expect(mockElement.remove).not.toHaveBeenCalled();
+ popup.dismiss();
+ expect(mockElement.remove).toHaveBeenCalled();
+ });
+
+ });
+
+ }
+);
diff --git a/platform/commonUI/general/test/suite.json b/platform/commonUI/general/test/suite.json
index 45c554d2c..0d19fbb9e 100644
--- a/platform/commonUI/general/test/suite.json
+++ b/platform/commonUI/general/test/suite.json
@@ -3,16 +3,22 @@
"controllers/BottomBarController",
"controllers/ClickAwayController",
"controllers/ContextMenuController",
+ "controllers/DateTimePickerController",
"controllers/GetterSetterController",
"controllers/SelectorController",
"controllers/SplitPaneController",
+ "controllers/TimeRangeController",
"controllers/ToggleController",
"controllers/TreeNodeController",
"controllers/ViewSwitcherController",
+ "directives/MCTClickElsewhere",
"directives/MCTContainer",
"directives/MCTDrag",
+ "directives/MCTPopup",
"directives/MCTResize",
"directives/MCTScroll",
+ "services/Popup",
+ "services/PopupService",
"services/UrlService",
"StyleSheetLoader"
]
diff --git a/platform/commonUI/inspect/bundle.json b/platform/commonUI/inspect/bundle.json
index bafeb851e..ed6858f13 100644
--- a/platform/commonUI/inspect/bundle.json
+++ b/platform/commonUI/inspect/bundle.json
@@ -45,13 +45,12 @@
"implementation": "services/InfoService.js",
"depends": [
"$compile",
- "$document",
- "$window",
"$rootScope",
+ "popupService",
"agentService"
]
}
- ],
+ ],
"constants": [
{
"key": "INFO_HOVER_DELAY",
@@ -66,4 +65,4 @@
}
]
}
-} \ No newline at end of file
+}
diff --git a/platform/commonUI/inspect/src/InfoConstants.js b/platform/commonUI/inspect/src/InfoConstants.js
index 4927de870..33a0865dd 100644
--- a/platform/commonUI/inspect/src/InfoConstants.js
+++ b/platform/commonUI/inspect/src/InfoConstants.js
@@ -31,13 +31,19 @@ define({
BUBBLE_TEMPLATE: "<mct-container key=\"bubble\" " +
"bubble-title=\"{{bubbleTitle}}\" " +
"bubble-layout=\"{{bubbleLayout}}\" " +
- "class=\"bubble-container\">" +
- "<mct-include key=\"bubbleTemplate\" ng-model=\"bubbleModel\">" +
+ "class=\"bubble-container\">" +
+ "<mct-include key=\"bubbleTemplate\" " +
+ "ng-model=\"bubbleModel\">" +
"</mct-include>" +
"</mct-container>",
- // Pixel offset for bubble, to align arrow position
- BUBBLE_OFFSET: [ 0, -26 ],
- // Max width and margins allowed for bubbles; defined in /platform/commonUI/general/res/sass/_constants.scss
- BUBBLE_MARGIN_LR: 10,
- BUBBLE_MAX_WIDTH: 300
+ // Options and classes for bubble
+ BUBBLE_OPTIONS: {
+ offsetX: 0,
+ offsetY: -26
+ },
+ BUBBLE_MOBILE_POSITION: [ 0, -25 ],
+ // Max width and margins allowed for bubbles;
+ // defined in /platform/commonUI/general/res/sass/_constants.scss
+ BUBBLE_MARGIN_LR: 10,
+ BUBBLE_MAX_WIDTH: 300
});
diff --git a/platform/commonUI/inspect/src/services/InfoService.js b/platform/commonUI/inspect/src/services/InfoService.js
index ff6d23cb5..eb929027a 100644
--- a/platform/commonUI/inspect/src/services/InfoService.js
+++ b/platform/commonUI/inspect/src/services/InfoService.js
@@ -27,18 +27,18 @@ define(
"use strict";
var BUBBLE_TEMPLATE = InfoConstants.BUBBLE_TEMPLATE,
- OFFSET = InfoConstants.BUBBLE_OFFSET;
+ MOBILE_POSITION = InfoConstants.BUBBLE_MOBILE_POSITION,
+ OPTIONS = InfoConstants.BUBBLE_OPTIONS;
/**
* Displays informative content ("info bubbles") for the user.
* @memberof platform/commonUI/inspect
* @constructor
*/
- function InfoService($compile, $document, $window, $rootScope, agentService) {
+ function InfoService($compile, $rootScope, popupService, agentService) {
this.$compile = $compile;
- this.$document = $document;
- this.$window = $window;
this.$rootScope = $rootScope;
+ this.popupService = popupService;
this.agentService = agentService;
}
@@ -55,53 +55,47 @@ define(
*/
InfoService.prototype.display = function (templateKey, title, content, position) {
var $compile = this.$compile,
- $document = this.$document,
- $window = this.$window,
$rootScope = this.$rootScope,
- body = $document.find('body'),
scope = $rootScope.$new(),
- winDim = [$window.innerWidth, $window.innerHeight],
- bubbleSpaceLR = InfoConstants.BUBBLE_MARGIN_LR + InfoConstants.BUBBLE_MAX_WIDTH,
- goLeft = position[0] > (winDim[0] - bubbleSpaceLR),
- goUp = position[1] > (winDim[1] / 2),
+ span = $compile('<span></span>')(scope),
+ bubbleSpaceLR = InfoConstants.BUBBLE_MARGIN_LR +
+ InfoConstants.BUBBLE_MAX_WIDTH,
+ options,
+ popup,
bubble;
-
+
+ options = Object.create(OPTIONS);
+ options.marginX = -bubbleSpaceLR;
+
+ // On a phone, bubble takes up more screen real estate,
+ // so position it differently (toward the bottom)
+ if (this.agentService.isPhone(navigator.userAgent)) {
+ position = MOBILE_POSITION;
+ options = {};
+ }
+
+ popup = this.popupService.display(span, position, options);
+
// Pass model & container parameters into the scope
scope.bubbleModel = content;
scope.bubbleTemplate = templateKey;
- scope.bubbleLayout = (goUp ? 'arw-btm' : 'arw-top') + ' ' +
- (goLeft ? 'arw-right' : 'arw-left');
scope.bubbleTitle = title;
+ // Style the bubble according to how it was positioned
+ scope.bubbleLayout = [
+ popup.goesUp() ? 'arw-btm' : 'arw-top',
+ popup.goesLeft() ? 'arw-right' : 'arw-left'
+ ].join(' ');
+ scope.bubbleLayout = 'arw-top arw-left';
- // Create the context menu
+ // Create the info bubble, now that we know how to
+ // point the arrow...
bubble = $compile(BUBBLE_TEMPLATE)(scope);
+ span.append(bubble);
- // Position the bubble
- bubble.css('position', 'absolute');
- if (this.agentService.isPhone(navigator.userAgent)) {
- bubble.css('right', '0px');
- bubble.css('left', '0px');
- bubble.css('top', 'auto');
- bubble.css('bottom', '25px');
- } else {
- if (goLeft) {
- bubble.css('right', (winDim[0] - position[0] + OFFSET[0]) + 'px');
- } else {
- bubble.css('left', position[0] + OFFSET[0] + 'px');
- }
- if (goUp) {
- bubble.css('bottom', (winDim[1] - position[1] + OFFSET[1]) + 'px');
- } else {
- bubble.css('top', position[1] + OFFSET[1] + 'px');
- }
- }
-
- // Add the menu to the body
- body.append(bubble);
-
- // Return a function to dismiss the bubble
- return function () {
- bubble.remove();
+ // Return a function to dismiss the info bubble
+ return function dismiss() {
+ popup.dismiss();
+ scope.$destroy();
};
};
diff --git a/platform/commonUI/inspect/test/services/InfoServiceSpec.js b/platform/commonUI/inspect/test/services/InfoServiceSpec.js
index e878afb26..f55d72d04 100644
--- a/platform/commonUI/inspect/test/services/InfoServiceSpec.js
+++ b/platform/commonUI/inspect/test/services/InfoServiceSpec.js
@@ -28,117 +28,85 @@ define(
describe("The info service", function () {
var mockCompile,
- mockDocument,
- testWindow,
mockRootScope,
+ mockPopupService,
mockAgentService,
- mockCompiledTemplate,
- testScope,
- mockBody,
- mockElement,
+ mockScope,
+ mockElements,
+ mockPopup,
service;
beforeEach(function () {
mockCompile = jasmine.createSpy('$compile');
- mockDocument = jasmine.createSpyObj('$document', ['find']);
- testWindow = { innerWidth: 1000, innerHeight: 100 };
mockRootScope = jasmine.createSpyObj('$rootScope', ['$new']);
mockAgentService = jasmine.createSpyObj('agentService', ['isMobile', 'isPhone']);
- mockCompiledTemplate = jasmine.createSpy('template');
- testScope = {};
- mockBody = jasmine.createSpyObj('body', ['append']);
- mockElement = jasmine.createSpyObj('element', ['css', 'remove']);
+ mockPopupService = jasmine.createSpyObj(
+ 'popupService',
+ ['display']
+ );
+ mockPopup = jasmine.createSpyObj('popup', [
+ 'dismiss',
+ 'goesLeft',
+ 'goesRight',
+ 'goesUp',
+ 'goesDown'
+ ]);
+
+ mockScope = jasmine.createSpyObj("scope", ["$destroy"]);
+ mockElements = [];
- mockDocument.find.andCallFake(function (tag) {
- return tag === 'body' ? mockBody : undefined;
+ mockPopupService.display.andReturn(mockPopup);
+ mockCompile.andCallFake(function () {
+ var mockCompiledTemplate = jasmine.createSpy('template'),
+ mockElement = jasmine.createSpyObj('element', [
+ 'css',
+ 'remove',
+ 'append'
+ ]);
+ mockCompiledTemplate.andReturn(mockElement);
+ mockElements.push(mockElement);
+ return mockCompiledTemplate;
});
- mockCompile.andReturn(mockCompiledTemplate);
- mockCompiledTemplate.andReturn(mockElement);
- mockRootScope.$new.andReturn(testScope);
+ mockRootScope.$new.andReturn(mockScope);
service = new InfoService(
mockCompile,
- mockDocument,
- testWindow,
mockRootScope,
+ mockPopupService,
mockAgentService
);
});
- it("creates elements and appends them to the body to display", function () {
- service.display('', '', {}, [0, 0]);
- expect(mockBody.append).toHaveBeenCalledWith(mockElement);
+ it("creates elements and displays them as popups", function () {
+ service.display('', '', {}, [123, 456]);
+ expect(mockPopupService.display).toHaveBeenCalledWith(
+ mockElements[0],
+ [ 123, 456 ],
+ jasmine.any(Object)
+ );
});
it("provides a function to remove displayed info bubbles", function () {
var fn = service.display('', '', {}, [0, 0]);
- expect(mockElement.remove).not.toHaveBeenCalled();
+ expect(mockPopup.dismiss).not.toHaveBeenCalled();
fn();
- expect(mockElement.remove).toHaveBeenCalled();
+ expect(mockPopup.dismiss).toHaveBeenCalled();
});
- describe("depending on mouse position", function () {
- // Positioning should vary based on quadrant in window,
- // which is 1000 x 100 in this test case.
- it("displays from the top-left in the top-left quadrant", function () {
- service.display('', '', {}, [250, 25]);
- expect(mockElement.css).toHaveBeenCalledWith(
- 'left',
- (250 + InfoConstants.BUBBLE_OFFSET[0]) + 'px'
- );
- expect(mockElement.css).toHaveBeenCalledWith(
- 'top',
- (25 + InfoConstants.BUBBLE_OFFSET[1]) + 'px'
- );
- });
-
- it("displays from the top-right in the top-right quadrant", function () {
- service.display('', '', {}, [700, 25]);
- expect(mockElement.css).toHaveBeenCalledWith(
- 'right',
- (300 + InfoConstants.BUBBLE_OFFSET[0]) + 'px'
- );
- expect(mockElement.css).toHaveBeenCalledWith(
- 'top',
- (25 + InfoConstants.BUBBLE_OFFSET[1]) + 'px'
- );
- });
-
- it("displays from the bottom-left in the bottom-left quadrant", function () {
- service.display('', '', {}, [250, 70]);
- expect(mockElement.css).toHaveBeenCalledWith(
- 'left',
- (250 + InfoConstants.BUBBLE_OFFSET[0]) + 'px'
- );
- expect(mockElement.css).toHaveBeenCalledWith(
- 'bottom',
- (30 + InfoConstants.BUBBLE_OFFSET[1]) + 'px'
- );
- });
-
- it("displays from the bottom-right in the bottom-right quadrant", function () {
- service.display('', '', {}, [800, 60]);
- expect(mockElement.css).toHaveBeenCalledWith(
- 'right',
- (200 + InfoConstants.BUBBLE_OFFSET[0]) + 'px'
- );
- expect(mockElement.css).toHaveBeenCalledWith(
- 'bottom',
- (40 + InfoConstants.BUBBLE_OFFSET[1]) + 'px'
- );
- });
-
- it("when on phone device, positioning is always on bottom", function () {
- mockAgentService.isPhone.andReturn(true);
- service = new InfoService(
- mockCompile,
- mockDocument,
- testWindow,
- mockRootScope,
- mockAgentService
- );
- service.display('', '', {}, [0, 0]);
- });
+ it("when on phone device, positions at bottom", function () {
+ mockAgentService.isPhone.andReturn(true);
+ service = new InfoService(
+ mockCompile,
+ mockRootScope,
+ mockPopupService,
+ mockAgentService
+ );
+ service.display('', '', {}, [123, 456]);
+ expect(mockPopupService.display).toHaveBeenCalledWith(
+ mockElements[0],
+ [ 0, -25 ],
+ jasmine.any(Object)
+ );
});
});
diff --git a/platform/commonUI/themes/espresso/res/css/theme-espresso.css b/platform/commonUI/themes/espresso/res/css/theme-espresso.css
index 13334809a..1be0ceb11 100644
--- a/platform/commonUI/themes/espresso/res/css/theme-espresso.css
+++ b/platform/commonUI/themes/espresso/res/css/theme-espresso.css
@@ -247,7 +247,7 @@ a.disabled {
/* line 42, ../../../../general/res/sass/_effects.scss */
.test {
- background-color: rgba(255, 204, 0, 0.2); }
+ background-color: rgba(255, 204, 0, 0.2) !important; }
@-moz-keyframes pulse {
0% {
@@ -314,7 +314,7 @@ a.disabled {
font-weight: normal;
font-style: normal; }
/* line 37, ../../../../general/res/sass/_global.scss */
-.ui-symbol {
+.ui-symbol, .l-datetime-picker .l-month-year-pager .pager {
font-family: 'symbolsfont'; }
/************************** HTML ENTITIES */
@@ -373,7 +373,8 @@ mct-container {
display: block; }
/* line 97, ../../../../general/res/sass/_global.scss */
-.abs, .s-menu span.l-click-area {
+.abs, .l-datetime-picker .l-month-year-pager .pager,
+.l-datetime-picker .l-month-year-pager .val, .s-menu-btn span.l-click-area {
position: absolute;
top: 0;
right: 0;
@@ -403,21 +404,29 @@ mct-container {
text-align: center; }
/* line 128, ../../../../general/res/sass/_global.scss */
+.scrolling {
+ overflow: auto; }
+
+/* line 132, ../../../../general/res/sass/_global.scss */
+.vscroll {
+ overflow-y: auto; }
+
+/* line 136, ../../../../general/res/sass/_global.scss */
.no-margin {
margin: 0; }
-/* line 132, ../../../../general/res/sass/_global.scss */
+/* line 140, ../../../../general/res/sass/_global.scss */
.ds {
-moz-box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px;
-webkit-box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px;
box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px; }
-/* line 136, ../../../../general/res/sass/_global.scss */
+/* line 144, ../../../../general/res/sass/_global.scss */
.hide,
.hidden {
display: none !important; }
-/* line 141, ../../../../general/res/sass/_global.scss */
+/* line 149, ../../../../general/res/sass/_global.scss */
.sep {
color: rgba(255, 255, 255, 0.2); }
@@ -443,7 +452,8 @@ mct-container {
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 26, ../../../../general/res/sass/_about.scss */
-.l-about.abs, .s-menu span.l-about.l-click-area {
+.l-about.abs, .l-datetime-picker .l-month-year-pager .l-about.pager,
+.l-datetime-picker .l-month-year-pager .l-about.val, .s-menu-btn span.l-about.l-click-area {
overflow: auto; }
/* line 31, ../../../../general/res/sass/_about.scss */
.l-about .l-logo-holder {
@@ -493,7 +503,7 @@ mct-container {
.s-about .s-logo-openmctweb {
background-image: url("../../../../general/res/images/logo-openmctweb-shdw.svg"); }
/* line 81, ../../../../general/res/sass/_about.scss */
- .s-about .s-btn, .s-about .s-menu {
+ .s-about .s-btn, .s-about .s-menu-btn {
line-height: 2em; }
/* line 85, ../../../../general/res/sass/_about.scss */
.s-about .l-licenses-software .l-license-software {
@@ -534,7 +544,8 @@ mct-container {
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 24, ../../../../general/res/sass/_text.scss */
-.abs.l-standalone, .s-menu span.l-standalone.l-click-area {
+.abs.l-standalone, .l-datetime-picker .l-month-year-pager .l-standalone.pager,
+.l-datetime-picker .l-month-year-pager .l-standalone.val, .s-menu-btn span.l-standalone.l-click-area {
padding: 5% 20%; }
/* line 29, ../../../../general/res/sass/_text.scss */
@@ -596,46 +607,52 @@ mct-container {
border-right: 5px solid transparent; }
/* line 32, ../../../../general/res/sass/_icons.scss */
-.ui-symbol.icon {
+.ui-symbol.type-icon, .l-datetime-picker .l-month-year-pager .type-icon.pager {
+ color: #cccccc; }
+/* line 35, ../../../../general/res/sass/_icons.scss */
+.ui-symbol.icon, .l-datetime-picker .l-month-year-pager .icon.pager {
color: #0099cc; }
- /* line 34, ../../../../general/res/sass/_icons.scss */
- .ui-symbol.icon.alert {
+ /* line 37, ../../../../general/res/sass/_icons.scss */
+ .ui-symbol.icon.alert, .l-datetime-picker .l-month-year-pager .icon.alert.pager {
color: #ff533a; }
- /* line 36, ../../../../general/res/sass/_icons.scss */
- .ui-symbol.icon.alert:hover {
+ /* line 39, ../../../../general/res/sass/_icons.scss */
+ .ui-symbol.icon.alert:hover, .l-datetime-picker .l-month-year-pager .icon.alert.pager:hover {
color: #ffaca0; }
- /* line 40, ../../../../general/res/sass/_icons.scss */
- .ui-symbol.icon.major {
+ /* line 43, ../../../../general/res/sass/_icons.scss */
+ .ui-symbol.icon.major, .l-datetime-picker .l-month-year-pager .icon.major.pager {
font-size: 1.65em; }
+/* line 47, ../../../../general/res/sass/_icons.scss */
+.ui-symbol.icon-calendar:after, .l-datetime-picker .l-month-year-pager .icon-calendar.pager:after {
+ content: "\e605"; }
-/* line 46, ../../../../general/res/sass/_icons.scss */
-.bar .ui-symbol {
+/* line 52, ../../../../general/res/sass/_icons.scss */
+.bar .ui-symbol, .bar .l-datetime-picker .l-month-year-pager .pager, .l-datetime-picker .l-month-year-pager .bar .pager {
display: inline-block; }
-/* line 50, ../../../../general/res/sass/_icons.scss */
+/* line 56, ../../../../general/res/sass/_icons.scss */
.invoke-menu {
text-shadow: none;
display: inline-block; }
-/* line 55, ../../../../general/res/sass/_icons.scss */
-.s-menu .invoke-menu,
+/* line 61, ../../../../general/res/sass/_icons.scss */
+.s-menu-btn .invoke-menu,
.icon.major .invoke-menu {
margin-left: 3px; }
-/* line 60, ../../../../general/res/sass/_icons.scss */
+/* line 66, ../../../../general/res/sass/_icons.scss */
.menu .type-icon,
.tree-item .type-icon,
.super-menu.menu .type-icon {
position: absolute; }
-/* line 70, ../../../../general/res/sass/_icons.scss */
+/* line 76, ../../../../general/res/sass/_icons.scss */
.l-icon-link:before {
content: "\f4"; }
-/* line 74, ../../../../general/res/sass/_icons.scss */
+/* line 80, ../../../../general/res/sass/_icons.scss */
.l-icon-alert {
display: none !important; }
- /* line 76, ../../../../general/res/sass/_icons.scss */
+ /* line 82, ../../../../general/res/sass/_icons.scss */
.l-icon-alert:before {
color: #ff533a;
content: "!"; }
@@ -1042,27 +1059,22 @@ mct-container {
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-@-webkit-keyframes rotation {
- from {
- -webkit-transform: rotate(0deg); }
- to {
- -webkit-transform: rotate(359deg); } }
@-moz-keyframes rotation {
- from {
- -moz-transform: rotate(0deg); }
- to {
- -moz-transform: rotate(359deg); } }
-@-o-keyframes rotation {
- from {
- -o-transform: rotate(0deg); }
- to {
- -o-transform: rotate(359deg); } }
+ 0% {
+ transform: rotate(0deg); }
+ 100% {
+ transform: rotate(359deg); } }
+@-webkit-keyframes rotation {
+ 0% {
+ transform: rotate(0deg); }
+ 100% {
+ transform: rotate(359deg); } }
@keyframes rotation {
- from {
+ 0% {
transform: rotate(0deg); }
- to {
+ 100% {
transform: rotate(359deg); } }
-/* line 42, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+/* line 63, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.t-wait-spinner,
.wait-spinner {
display: block;
@@ -1075,6 +1087,8 @@ mct-container {
border-top-color: #0099cc;
border-style: solid;
border-width: 0.5em;
+ -moz-border-radius: 100%;
+ -webkit-border-radius: 100%;
border-radius: 100%;
top: 50%;
left: 50%;
@@ -1085,7 +1099,7 @@ mct-container {
margin-top: -5%;
margin-left: -5%;
z-index: 2; }
- /* line 53, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ /* line 74, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.t-wait-spinner.inline,
.wait-spinner.inline {
display: inline-block !important;
@@ -1093,26 +1107,26 @@ mct-container {
position: relative !important;
vertical-align: middle; }
-/* line 61, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+/* line 82, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.l-wait-spinner-holder {
pointer-events: none;
position: absolute; }
- /* line 65, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ /* line 86, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.l-wait-spinner-holder.align-left .t-wait-spinner {
left: 0;
margin-left: 0; }
- /* line 70, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ /* line 91, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.l-wait-spinner-holder.full-size {
display: inline-block;
height: 100%;
width: 100%; }
- /* line 73, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ /* line 94, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.l-wait-spinner-holder.full-size .t-wait-spinner {
top: 0;
margin-top: 0;
padding: 30%; }
-/* line 82, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+/* line 103, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.treeview .wait-spinner {
display: block;
position: absolute;
@@ -1124,6 +1138,8 @@ mct-container {
border-top-color: #0099cc;
border-style: solid;
border-width: 0.25em;
+ -moz-border-radius: 100%;
+ -webkit-border-radius: 100%;
border-radius: 100%;
height: 10px;
width: 10px;
@@ -1132,7 +1148,7 @@ mct-container {
top: 2px;
left: 0; }
-/* line 91, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+/* line 112, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.wait-spinner.sm {
display: block;
position: absolute;
@@ -1144,6 +1160,8 @@ mct-container {
border-top-color: #0099cc;
border-style: solid;
border-width: 0.25em;
+ -moz-border-radius: 100%;
+ -webkit-border-radius: 100%;
border-radius: 100%;
height: 13px;
width: 13px;
@@ -1153,6 +1171,77 @@ mct-container {
top: 0;
left: 0; }
+/* line 122, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+.loading {
+ pointer-events: none; }
+ /* line 125, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ .loading:before, .loading:after {
+ content: ''; }
+ /* line 129, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ .loading:before {
+ -moz-animation-name: rotateCentered;
+ -webkit-animation-name: rotateCentered;
+ animation-name: rotateCentered;
+ -moz-animation-duration: 0.5s;
+ -webkit-animation-duration: 0.5s;
+ animation-duration: 0.5s;
+ -moz-animation-iteration-count: infinite;
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+ -moz-animation-timing-function: linear;
+ -webkit-animation-timing-function: linear;
+ animation-timing-function: linear;
+ border-color: rgba(255, 199, 0, 0.25);
+ border-top-color: #ffc700;
+ border-style: solid;
+ border-width: 5px;
+ -moz-border-radius: 100%;
+ -webkit-border-radius: 100%;
+ border-radius: 100%;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ display: block;
+ position: absolute;
+ height: 0;
+ width: 0;
+ padding: 7%;
+ left: 50%;
+ top: 50%;
+ z-index: 10; }
+@-moz-keyframes rotateCentered {
+ 0% {
+ transform: translateX(-50%) translateY(-50%) rotate(0deg); }
+ 100% {
+ transform: translateX(-50%) translateY(-50%) rotate(359deg); } }
+@-webkit-keyframes rotateCentered {
+ 0% {
+ transform: translateX(-50%) translateY(-50%) rotate(0deg); }
+ 100% {
+ transform: translateX(-50%) translateY(-50%) rotate(359deg); } }
+@keyframes rotateCentered {
+ 0% {
+ transform: translateX(-50%) translateY(-50%) rotate(0deg); }
+ 100% {
+ transform: translateX(-50%) translateY(-50%) rotate(359deg); } }
+ /* line 133, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ .loading:after {
+ overflow: hidden;
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ left: 0px;
+ width: auto;
+ height: auto;
+ background: rgba(153, 153, 153, 0.2);
+ display: block;
+ z-index: 9; }
+ /* line 139, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ .loading.tree-item:before {
+ padding: 0.375rem;
+ border-width: 2px; }
+
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@@ -1242,7 +1331,7 @@ mct-container {
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 25, ../../../../general/res/sass/controls/_buttons.scss */
-.s-btn, .s-menu {
+.s-btn, .s-menu-btn {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
@@ -1257,23 +1346,23 @@ mct-container {
padding: 0 7.5px;
font-size: 0.7rem; }
/* line 35, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn .icon, .s-menu .icon {
+ .s-btn .icon, .s-menu-btn .icon {
font-size: 0.8rem;
color: #0099cc; }
/* line 40, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn .title-label, .s-menu .title-label {
+ .s-btn .title-label, .s-menu-btn .title-label {
vertical-align: top; }
/* line 44, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.lg, .lg.s-menu {
+ .s-btn.lg, .lg.s-menu-btn {
font-size: 1rem; }
/* line 48, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.sm, .sm.s-menu {
+ .s-btn.sm, .sm.s-menu-btn {
padding: 0 5px; }
/* line 52, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.vsm, .vsm.s-menu {
+ .s-btn.vsm, .vsm.s-menu-btn {
padding: 0 2.5px; }
/* line 56, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.major, .major.s-menu {
+ .s-btn.major, .major.s-menu-btn {
background-color: #0099cc;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
@@ -1300,19 +1389,19 @@ mct-container {
-o-transition: background, 0.25s;
-webkit-transition: background, 0.25s;
transition: background, 0.25s;
- text-shadow: rgba(0, 0, 0, 0.3) 0 1px 1px; }
+ text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
- .s-btn.major .icon, .major.s-menu .icon {
+ .s-btn.major .icon, .major.s-menu-btn .icon {
color: #fff; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 294, ../../../../general/res/sass/_mixins.scss */
- .s-btn.major:not(.disabled):hover, .major.s-menu:not(.disabled):hover {
+ .s-btn.major:not(.disabled):hover, .major.s-menu-btn:not(.disabled):hover {
background: linear-gradient(#1ac6ff, #00bfff); }
/* line 296, ../../../../general/res/sass/_mixins.scss */
- .s-btn.major:not(.disabled):hover > .icon, .major.s-menu:not(.disabled):hover > .icon {
+ .s-btn.major:not(.disabled):hover > .icon, .major.s-menu-btn:not(.disabled):hover > .icon {
color: white; } }
/* line 62, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn:not(.major), .s-menu:not(.major) {
+ .s-btn:not(.major), .s-menu-btn:not(.major) {
background-color: #454545;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
@@ -1339,22 +1428,22 @@ mct-container {
-o-transition: background, 0.25s;
-webkit-transition: background, 0.25s;
transition: background, 0.25s;
- text-shadow: rgba(0, 0, 0, 0.3) 0 1px 1px; }
+ text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
- .s-btn:not(.major) .icon, .s-menu:not(.major) .icon {
+ .s-btn:not(.major) .icon, .s-menu-btn:not(.major) .icon {
color: #0099cc; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 294, ../../../../general/res/sass/_mixins.scss */
- .s-btn:not(.major):not(.disabled):hover, .s-menu:not(.major):not(.disabled):hover {
+ .s-btn:not(.major):not(.disabled):hover, .s-menu-btn:not(.major):not(.disabled):hover {
background: linear-gradient(#6b6b6b, #5e5e5e); }
/* line 296, ../../../../general/res/sass/_mixins.scss */
- .s-btn:not(.major):not(.disabled):hover > .icon, .s-menu:not(.major):not(.disabled):hover > .icon {
+ .s-btn:not(.major):not(.disabled):hover > .icon, .s-menu-btn:not(.major):not(.disabled):hover > .icon {
color: #33ccff; } }
/* line 71, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.pause-play .icon:before, .pause-play.s-menu .icon:before {
+ .s-btn.pause-play .icon:before, .pause-play.s-menu-btn .icon:before {
content: "\0000F1"; }
/* line 74, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.pause-play.paused, .pause-play.paused.s-menu {
+ .s-btn.pause-play.paused, .pause-play.paused.s-menu-btn {
background-color: #c56f01;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
@@ -1381,19 +1470,19 @@ mct-container {
-o-transition: background, 0.25s;
-webkit-transition: background, 0.25s;
transition: background, 0.25s;
- text-shadow: rgba(0, 0, 0, 0.3) 0 1px 1px; }
+ text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
- .s-btn.pause-play.paused .icon, .pause-play.paused.s-menu .icon {
+ .s-btn.pause-play.paused .icon, .pause-play.paused.s-menu-btn .icon {
color: #fff; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 294, ../../../../general/res/sass/_mixins.scss */
- .s-btn.pause-play.paused:not(.disabled):hover, .pause-play.paused.s-menu:not(.disabled):hover {
+ .s-btn.pause-play.paused:not(.disabled):hover, .pause-play.paused.s-menu-btn:not(.disabled):hover {
background: linear-gradient(#fe9815, #f88c01); }
/* line 296, ../../../../general/res/sass/_mixins.scss */
- .s-btn.pause-play.paused:not(.disabled):hover > .icon, .pause-play.paused.s-menu:not(.disabled):hover > .icon {
+ .s-btn.pause-play.paused:not(.disabled):hover > .icon, .pause-play.paused.s-menu-btn:not(.disabled):hover > .icon {
color: white; } }
/* line 76, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.pause-play.paused .icon, .pause-play.paused.s-menu .icon {
+ .s-btn.pause-play.paused .icon, .pause-play.paused.s-menu-btn .icon {
-moz-animation-name: pulse;
-webkit-animation-name: pulse;
animation-name: pulse;
@@ -1410,23 +1499,23 @@ mct-container {
-webkit-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out; }
/* line 78, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.pause-play.paused .icon :before, .pause-play.paused.s-menu .icon :before {
+ .s-btn.pause-play.paused .icon :before, .pause-play.paused.s-menu-btn .icon :before {
content: "\0000EF"; }
/* line 86, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.show-thumbs .icon:before, .show-thumbs.s-menu .icon:before {
+ .s-btn.show-thumbs .icon:before, .show-thumbs.s-menu-btn .icon:before {
content: "\000039"; }
/* line 92, ../../../../general/res/sass/controls/_buttons.scss */
.l-btn-set {
font-size: 0; }
/* line 98, ../../../../general/res/sass/controls/_buttons.scss */
- .l-btn-set .s-btn, .l-btn-set .s-menu {
+ .l-btn-set .s-btn, .l-btn-set .s-menu-btn {
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
margin-left: 1px; }
/* line 104, ../../../../general/res/sass/controls/_buttons.scss */
- .l-btn-set .first .s-btn, .l-btn-set .first .s-menu {
+ .l-btn-set .first .s-btn, .l-btn-set .first .s-menu-btn {
-moz-border-radius-topleft: 3px;
-webkit-border-top-left-radius: 3px;
border-top-left-radius: 3px;
@@ -1435,7 +1524,7 @@ mct-container {
border-bottom-left-radius: 3px;
margin-left: 0; }
/* line 111, ../../../../general/res/sass/controls/_buttons.scss */
- .l-btn-set .last .s-btn, .l-btn-set .last .s-menu {
+ .l-btn-set .last .s-btn, .l-btn-set .last .s-menu-btn {
-moz-border-radius-topright: 3px;
-webkit-border-top-right-radius: 3px;
border-top-right-radius: 3px;
@@ -1444,7 +1533,7 @@ mct-container {
border-bottom-right-radius: 3px; }
/* line 118, ../../../../general/res/sass/controls/_buttons.scss */
-.paused:not(.s-btn):not(.s-menu) {
+.paused:not(.s-btn):not(.s-menu-btn) {
border-color: #c56f01 !important;
color: #c56f01 !important; }
@@ -1704,7 +1793,7 @@ label.checkbox.custom {
margin-left: 0; }
/* line 180, ../../../../general/res/sass/controls/_controls.scss */
-.s-menu label.checkbox.custom {
+.s-menu-btn label.checkbox.custom {
margin-left: 5px; }
/* line 185, ../../../../general/res/sass/controls/_controls.scss */
@@ -1909,119 +1998,72 @@ label.checkbox.custom {
display: none; }
/******************************************************** SLIDERS */
-/* line 354, ../../../../general/res/sass/controls/_controls.scss */
+/* line 352, ../../../../general/res/sass/controls/_controls.scss */
.slider .slot {
- -moz-border-radius: 2px;
- -webkit-border-radius: 2px;
- border-radius: 2px;
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- -moz-box-shadow: inset rgba(0, 0, 0, 0.7) 0 1px 5px;
- -webkit-box-shadow: inset rgba(0, 0, 0, 0.7) 0 1px 5px;
- box-shadow: inset rgba(0, 0, 0, 0.7) 0 1px 5px;
- background-color: rgba(0, 0, 0, 0.4);
- height: 50%;
width: auto;
position: absolute;
- top: 25%;
+ top: 0;
right: 0;
- bottom: auto;
+ bottom: 0;
left: 0; }
-/* line 365, ../../../../general/res/sass/controls/_controls.scss */
+/* line 362, ../../../../general/res/sass/controls/_controls.scss */
.slider .knob {
- background-color: #333;
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- color: #999;
- display: inline-block;
- background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzQwNDA0MCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzMzMzMzMyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
- background-size: 100%;
- background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #404040), color-stop(100%, #333333));
- background-image: -moz-linear-gradient(#404040, #333333);
- background-image: -webkit-linear-gradient(#404040, #333333);
- background-image: linear-gradient(#404040, #333333);
- -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
- -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
- box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
- -moz-user-select: -moz-none;
- -ms-user-select: none;
- -webkit-user-select: none;
- user-select: none;
- -moz-transition: background, 0.25s;
- -o-transition: background, 0.25s;
- -webkit-transition: background, 0.25s;
- transition: background, 0.25s;
- text-shadow: rgba(0, 0, 0, 0.3) 0 1px 1px;
- cursor: ew-resize;
+ -moz-transition-property: visibility, opacity, background-color, border-color;
+ -o-transition-property: visibility, opacity, background-color, border-color;
+ -webkit-transition-property: visibility, opacity, background-color, border-color;
+ transition-property: visibility, opacity, background-color, border-color;
+ -moz-transition-duration: 0.25s;
+ -o-transition-duration: 0.25s;
+ -webkit-transition-duration: 0.25s;
+ transition-duration: 0.25s;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ background-color: rgba(0, 153, 204, 0.6);
position: absolute;
height: 100%;
- width: 12px;
+ width: 10px;
top: 0;
auto: 0;
bottom: auto;
left: auto; }
- /* line 289, ../../../../general/res/sass/_mixins.scss */
- .slider .knob .icon {
- color: #0099cc; }
- @media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
- /* line 294, ../../../../general/res/sass/_mixins.scss */
- .slider .knob:not(.disabled):hover {
- background: linear-gradient(#595959, #4d4d4d); }
- /* line 296, ../../../../general/res/sass/_mixins.scss */
- .slider .knob:not(.disabled):hover > .icon {
- color: #33ccff; } }
- /* line 176, ../../../../general/res/sass/_mixins.scss */
- .slider .knob:before {
- -moz-transition-property: "border-color";
- -o-transition-property: "border-color";
- -webkit-transition-property: "border-color";
- transition-property: "border-color";
- -moz-transition-duration: 0.75s;
- -o-transition-duration: 0.75s;
- -webkit-transition-duration: 0.75s;
- transition-duration: 0.75s;
- -moz-transition-timing-function: ease-in-out;
- -o-transition-timing-function: ease-in-out;
- -webkit-transition-timing-function: ease-in-out;
- transition-timing-function: ease-in-out;
- content: '';
- display: block;
- height: auto;
- pointer-events: none;
- position: absolute;
- z-index: 2;
- border-left: 1px solid rgba(0, 0, 0, 0.3);
- left: 2px;
- bottom: 5px;
- top: 5px; }
- /* line 198, ../../../../general/res/sass/_mixins.scss */
- .slider .knob:not(.disabled):hover:before {
- -moz-transition-property: "border-color";
- -o-transition-property: "border-color";
- -webkit-transition-property: "border-color";
- transition-property: "border-color";
- -moz-transition-duration: 25ms;
- -o-transition-duration: 25ms;
- -webkit-transition-duration: 25ms;
- transition-duration: 25ms;
- -moz-transition-timing-function: ease-in-out;
- -o-transition-timing-function: ease-in-out;
- -webkit-transition-timing-function: ease-in-out;
- transition-timing-function: ease-in-out;
- border-color: #0099cc; }
- /* line 376, ../../../../general/res/sass/controls/_controls.scss */
- .slider .knob:before {
- top: 1px;
- bottom: 3px;
- left: 5px; }
-/* line 383, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 367, ../../../../general/res/sass/controls/_controls.scss */
+ .slider .knob:hover {
+ background-color: #0099cc; }
+/* line 378, ../../../../general/res/sass/controls/_controls.scss */
+.slider .knob-l {
+ -moz-border-radius-topleft: 10px;
+ -webkit-border-top-left-radius: 10px;
+ border-top-left-radius: 10px;
+ -moz-border-radius-bottomleft: 10px;
+ -webkit-border-bottom-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+ cursor: w-resize; }
+/* line 382, ../../../../general/res/sass/controls/_controls.scss */
+.slider .knob-r {
+ -moz-border-radius-topright: 10px;
+ -webkit-border-top-right-radius: 10px;
+ border-top-right-radius: 10px;
+ -moz-border-radius-bottomright: 10px;
+ -webkit-border-bottom-right-radius: 10px;
+ border-bottom-right-radius: 10px;
+ cursor: e-resize; }
+/* line 386, ../../../../general/res/sass/controls/_controls.scss */
.slider .range {
- background: rgba(0, 153, 204, 0.6);
+ -moz-transition-property: visibility, opacity, background-color, border-color;
+ -o-transition-property: visibility, opacity, background-color, border-color;
+ -webkit-transition-property: visibility, opacity, background-color, border-color;
+ transition-property: visibility, opacity, background-color, border-color;
+ -moz-transition-duration: 0.25s;
+ -o-transition-duration: 0.25s;
+ -webkit-transition-duration: 0.25s;
+ transition-duration: 0.25s;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ background-color: rgba(0, 153, 204, 0.3);
cursor: ew-resize;
position: absolute;
top: 0;
@@ -2030,13 +2072,118 @@ label.checkbox.custom {
left: auto;
height: auto;
width: auto; }
- /* line 393, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 397, ../../../../general/res/sass/controls/_controls.scss */
.slider .range:hover {
- background: rgba(0, 153, 204, 0.7); }
+ background-color: rgba(0, 153, 204, 0.5); }
+
+/******************************************************** DATETIME PICKER */
+/* line 404, ../../../../general/res/sass/controls/_controls.scss */
+.l-datetime-picker {
+ -moz-user-select: -moz-none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+ font-size: 0.8rem;
+ padding: 10px !important;
+ width: 230px; }
+ /* line 410, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager {
+ height: 15px;
+ margin-bottom: 5px;
+ position: relative; }
+ /* line 422, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager {
+ width: 20px; }
+ /* line 425, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager.prev {
+ right: auto; }
+ /* line 427, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager.prev:before {
+ content: "\3c"; }
+ /* line 431, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager.next {
+ left: auto;
+ text-align: right; }
+ /* line 434, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager.next:before {
+ content: "\3e"; }
+ /* line 439, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .val {
+ text-align: center;
+ left: 25px;
+ right: 25px; }
+ /* line 445, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-calendar,
+ .l-datetime-picker .l-time-selects {
+ border-top: 1px solid rgba(153, 153, 153, 0.1); }
+ /* line 449, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-time-selects {
+ line-height: 22px; }
+
+/******************************************************** CALENDAR */
+/* line 457, ../../../../general/res/sass/controls/_controls.scss */
+.l-calendar ul.l-cal-row {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ margin-top: 1px; }
+ /* line 461, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row:first-child {
+ margin-top: 0; }
+ /* line 464, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row li {
+ -webkit-flex: 1 0;
+ flex: 1 0;
+ margin-left: 1px;
+ padding: 5px;
+ text-align: center; }
+ /* line 470, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row li:first-child {
+ margin-left: 0; }
+ /* line 474, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-header li {
+ color: #b3b3b3; }
+ /* line 477, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li {
+ -moz-transition-property: background-color;
+ -o-transition-property: background-color;
+ -webkit-transition-property: background-color;
+ transition-property: background-color;
+ -moz-transition-duration: 0.25s;
+ -o-transition-duration: 0.25s;
+ -webkit-transition-duration: 0.25s;
+ transition-duration: 0.25s;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ cursor: pointer; }
+ /* line 480, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li.in-month {
+ background-color: #616161; }
+ /* line 483, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li .sub {
+ color: #b3b3b3;
+ font-size: 0.8em; }
+ /* line 487, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li.selected {
+ background: #006080;
+ color: #cccccc; }
+ /* line 490, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li.selected .sub {
+ color: inherit; }
+ /* line 494, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li:hover {
+ background-color: #0099cc;
+ color: #fff; }
+ /* line 497, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li:hover .sub {
+ color: inherit; }
/******************************************************** BROWSER ELEMENTS */
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
- /* line 402, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 508, ../../../../general/res/sass/controls/_controls.scss */
::-webkit-scrollbar {
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
@@ -2051,7 +2198,7 @@ label.checkbox.custom {
height: 10px;
width: 10px; }
- /* line 411, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 517, ../../../../general/res/sass/controls/_controls.scss */
::-webkit-scrollbar-thumb {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzU5NTk1OSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzRkNGQ0ZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
background-size: 100%;
@@ -2065,7 +2212,7 @@ label.checkbox.custom {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box; }
- /* line 420, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 526, ../../../../general/res/sass/controls/_controls.scss */
::-webkit-scrollbar-thumb:hover {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzVlNWU1ZSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzUyNTI1MiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
background-size: 100%;
@@ -2074,7 +2221,7 @@ label.checkbox.custom {
background-image: -webkit-linear-gradient(#5e5e5e, #525252 20px);
background-image: linear-gradient(#5e5e5e, #525252 20px); }
- /* line 425, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 531, ../../../../general/res/sass/controls/_controls.scss */
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0.4); } }
/*****************************************************************************
@@ -2133,13 +2280,13 @@ label.checkbox.custom {
*****************************************************************************/
/******************************************************** MENU BUTTONS */
/* line 31, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu .icon {
+.s-menu-btn .icon {
font-size: 120%; }
/* line 35, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu .title-label {
+.s-menu-btn .title-label {
margin-left: 3px; }
/* line 39, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu:after {
+.s-menu-btn:after {
text-shadow: none;
content: '\76';
display: inline-block;
@@ -2148,14 +2295,14 @@ label.checkbox.custom {
vertical-align: top;
color: rgba(255, 255, 255, 0.2); }
/* line 46, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu.create-btn .title-label {
+.s-menu-btn.create-btn .title-label {
font-size: 1rem; }
/* line 54, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu .menu {
+.s-menu-btn .menu {
left: 0;
text-align: left; }
/* line 57, ../../../../general/res/sass/controls/_menus.scss */
- .s-menu .menu .ui-symbol.icon {
+ .s-menu-btn .menu .ui-symbol.icon, .s-menu-btn .menu .l-datetime-picker .l-month-year-pager .icon.pager, .l-datetime-picker .l-month-year-pager .s-menu-btn .menu .icon.pager {
width: 12px; }
/******************************************************** MENUS THEMSELVES */
@@ -2163,198 +2310,217 @@ label.checkbox.custom {
.menu-element {
cursor: pointer;
position: relative; }
- /* line 70, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu {
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
- background-color: #6e6e6e;
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- color: white;
- display: inline-block;
- background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzdhN2E3YSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzZlNmU2ZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
- background-size: 100%;
- background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7a7a7a), color-stop(100%, #6e6e6e));
- background-image: -moz-linear-gradient(#7a7a7a, #6e6e6e);
- background-image: -webkit-linear-gradient(#7a7a7a, #6e6e6e);
- background-image: linear-gradient(#7a7a7a, #6e6e6e);
- -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
- -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
- box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
- text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px;
- display: block;
- padding: 3px 0;
- position: absolute;
- z-index: 10; }
- /* line 79, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul {
+
+/* line 69, ../../../../general/res/sass/controls/_menus.scss */
+.s-menu, .menu {
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ background-color: #6e6e6e;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ color: white;
+ display: inline-block;
+ background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzdhN2E3YSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzZlNmU2ZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
+ background-size: 100%;
+ background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7a7a7a), color-stop(100%, #6e6e6e));
+ background-image: -moz-linear-gradient(#7a7a7a, #6e6e6e);
+ background-image: -webkit-linear-gradient(#7a7a7a, #6e6e6e);
+ background-image: linear-gradient(#7a7a7a, #6e6e6e);
+ -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
+ -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
+ box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px;
+ text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px;
+ padding: 3px 0; }
+
+/* line 77, ../../../../general/res/sass/controls/_menus.scss */
+.menu {
+ display: block;
+ position: absolute;
+ z-index: 10; }
+ /* line 82, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul {
+ margin: 0;
+ padding: 0; }
+ /* line 346, ../../../../general/res/sass/_mixins.scss */
+ .menu ul li {
+ list-style-type: none;
margin: 0;
padding: 0; }
- /* line 346, ../../../../general/res/sass/_mixins.scss */
- .menu-element .menu ul li {
- list-style-type: none;
- margin: 0;
- padding: 0; }
- /* line 81, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li {
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- border-top: 1px solid #a1a1a1;
- color: white;
- line-height: 1.5rem;
- padding: 3px 10px 3px 30px;
- position: relative;
- white-space: nowrap; }
- /* line 89, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li:first-child {
- border: none; }
- /* line 92, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li:hover {
- background: #878787;
- color: #fff; }
- /* line 95, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li:hover .icon {
- color: #fff; }
- /* line 99, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li .type-icon {
- left: 10px; }
- /* line 106, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu,
- .menu-element .context-menu,
- .menu-element .checkbox-menu,
- .menu-element .super-menu {
- pointer-events: auto; }
- /* line 112, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li a,
- .menu-element .context-menu ul li a,
- .menu-element .checkbox-menu ul li a,
- .menu-element .super-menu ul li a {
- color: white; }
- /* line 115, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li .icon,
- .menu-element .context-menu ul li .icon,
- .menu-element .checkbox-menu ul li .icon,
- .menu-element .super-menu ul li .icon {
- color: #24c8ff; }
- /* line 118, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li .type-icon,
- .menu-element .context-menu ul li .type-icon,
- .menu-element .checkbox-menu ul li .type-icon,
- .menu-element .super-menu ul li .type-icon {
- left: 5px; }
- /* line 130, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li {
- padding-left: 50px; }
- /* line 132, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li .checkbox {
- position: absolute;
- left: 5px;
- top: 0.53333rem; }
- /* line 137, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li .checkbox em {
- height: 0.7rem;
- width: 0.7rem; }
- /* line 140, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li .checkbox em:before {
- font-size: 7px !important;
- height: 0.7rem;
- width: 0.7rem;
- line-height: 0.7rem; }
- /* line 148, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li .type-icon {
- left: 25px; }
- /* line 154, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu {
- display: block;
- width: 500px;
- height: 480px; }
- /* line 162, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .contents {
- overflow: hidden;
- position: absolute;
- top: 5px;
- right: 5px;
- bottom: 5px;
- left: 5px;
- width: auto;
- height: auto; }
- /* line 165, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .pane {
+ /* line 84, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
- box-sizing: border-box; }
- /* line 167, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .pane.left {
- border-right: 1px solid #878787;
- left: 0;
- padding-right: 5px;
- right: auto;
- width: 50%;
- overflow-x: hidden;
- overflow-y: auto; }
- /* line 177, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .pane.left ul li {
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
- padding-left: 30px;
- border-top: none; }
- /* line 184, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .pane.right {
- left: auto;
- right: 0;
- padding: 25px;
- width: 50%; }
- /* line 194, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .menu-item-description .desc-area.icon {
+ box-sizing: border-box;
+ border-top: 1px solid #a1a1a1;
color: white;
+ line-height: 1.5rem;
+ padding: 3px 10px 3px 30px;
position: relative;
- font-size: 8em;
+ white-space: nowrap; }
+ /* line 92, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li:first-child {
+ border: none; }
+ /* line 95, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li:hover {
+ background: #878787;
+ color: #fff; }
+ /* line 98, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li:hover .icon {
+ color: #fff; }
+ /* line 102, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li .type-icon {
+ left: 10px; }
+
+/* line 109, ../../../../general/res/sass/controls/_menus.scss */
+.menu,
+.context-menu,
+.checkbox-menu,
+.super-menu {
+ pointer-events: auto; }
+ /* line 115, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li a,
+ .context-menu ul li a,
+ .checkbox-menu ul li a,
+ .super-menu ul li a {
+ color: white; }
+ /* line 118, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li .icon,
+ .context-menu ul li .icon,
+ .checkbox-menu ul li .icon,
+ .super-menu ul li .icon {
+ color: #24c8ff; }
+ /* line 121, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li .type-icon,
+ .context-menu ul li .type-icon,
+ .checkbox-menu ul li .type-icon,
+ .super-menu ul li .type-icon {
+ left: 5px; }
+
+/* line 133, ../../../../general/res/sass/controls/_menus.scss */
+.checkbox-menu ul li {
+ padding-left: 50px; }
+ /* line 135, ../../../../general/res/sass/controls/_menus.scss */
+ .checkbox-menu ul li .checkbox {
+ position: absolute;
+ left: 5px;
+ top: 0.53333rem; }
+ /* line 140, ../../../../general/res/sass/controls/_menus.scss */
+ .checkbox-menu ul li .checkbox em {
+ height: 0.7rem;
+ width: 0.7rem; }
+ /* line 143, ../../../../general/res/sass/controls/_menus.scss */
+ .checkbox-menu ul li .checkbox em:before {
+ font-size: 7px !important;
+ height: 0.7rem;
+ width: 0.7rem;
+ line-height: 0.7rem; }
+ /* line 151, ../../../../general/res/sass/controls/_menus.scss */
+ .checkbox-menu ul li .type-icon {
+ left: 25px; }
+
+/* line 157, ../../../../general/res/sass/controls/_menus.scss */
+.super-menu {
+ display: block;
+ width: 500px;
+ height: 480px; }
+ /* line 165, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .contents {
+ overflow: hidden;
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ bottom: 5px;
+ left: 5px;
+ width: auto;
+ height: auto; }
+ /* line 168, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .pane {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box; }
+ /* line 170, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .pane.left {
+ border-right: 1px solid #878787;
left: 0;
- height: 150px;
- line-height: 150px;
- margin-bottom: 25px;
- text-align: center; }
- /* line 205, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .menu-item-description .desc-area.title {
- color: white;
- font-size: 1.2em;
- margin-bottom: 0.5em; }
- /* line 210, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .menu-item-description .desc-area.description {
- color: white;
- font-size: 0.8em;
- line-height: 1.5em; }
- /* line 219, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .context-menu, .menu-element .checkbox-menu {
- font-size: 0.80rem; }
+ padding-right: 5px;
+ right: auto;
+ width: 50%;
+ overflow-x: hidden;
+ overflow-y: auto; }
+ /* line 180, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .pane.left ul li {
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ padding-left: 30px;
+ border-top: none; }
+ /* line 187, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .pane.right {
+ left: auto;
+ right: 0;
+ padding: 25px;
+ width: 50%; }
+ /* line 197, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .menu-item-description .desc-area.icon {
+ color: white;
+ position: relative;
+ font-size: 8em;
+ left: 0;
+ height: 150px;
+ line-height: 150px;
+ margin-bottom: 25px;
+ text-align: center; }
+ /* line 208, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .menu-item-description .desc-area.title {
+ color: white;
+ font-size: 1.2em;
+ margin-bottom: 0.5em; }
+ /* line 213, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .menu-item-description .desc-area.description {
+ color: white;
+ font-size: 0.8em;
+ line-height: 1.5em; }
-/* line 224, ../../../../general/res/sass/controls/_menus.scss */
-.context-menu-holder {
- pointer-events: none;
+/* line 222, ../../../../general/res/sass/controls/_menus.scss */
+.context-menu, .checkbox-menu {
+ font-size: 0.80rem; }
+
+/* line 226, ../../../../general/res/sass/controls/_menus.scss */
+.context-menu-holder,
+.menu-holder {
position: absolute;
- height: 200px;
- width: 170px;
z-index: 70; }
/* line 230, ../../../../general/res/sass/controls/_menus.scss */
- .context-menu-holder .context-menu-wrapper {
+ .context-menu-holder .context-menu-wrapper,
+ .menu-holder .context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%; }
- /* line 237, ../../../../general/res/sass/controls/_menus.scss */
- .context-menu-holder.go-left .context-menu, .context-menu-holder.go-left .menu-element .checkbox-menu, .menu-element .context-menu-holder.go-left .checkbox-menu {
+ /* line 235, ../../../../general/res/sass/controls/_menus.scss */
+ .context-menu-holder.go-left .context-menu, .context-menu-holder.go-left .checkbox-menu, .context-menu-holder.go-left .menu,
+ .menu-holder.go-left .context-menu,
+ .menu-holder.go-left .checkbox-menu,
+ .menu-holder.go-left .menu {
right: 0; }
- /* line 240, ../../../../general/res/sass/controls/_menus.scss */
- .context-menu-holder.go-up .context-menu, .context-menu-holder.go-up .menu-element .checkbox-menu, .menu-element .context-menu-holder.go-up .checkbox-menu {
+ /* line 239, ../../../../general/res/sass/controls/_menus.scss */
+ .context-menu-holder.go-up .context-menu, .context-menu-holder.go-up .checkbox-menu, .context-menu-holder.go-up .menu,
+ .menu-holder.go-up .context-menu,
+ .menu-holder.go-up .checkbox-menu,
+ .menu-holder.go-up .menu {
bottom: 0; }
/* line 245, ../../../../general/res/sass/controls/_menus.scss */
+.context-menu-holder {
+ pointer-events: none;
+ height: 200px;
+ width: 170px; }
+
+/* line 251, ../../../../general/res/sass/controls/_menus.scss */
.btn-bar.right .menu,
.menus-to-left .menu {
left: auto;
@@ -2774,24 +2940,25 @@ label.checkbox.custom {
.t-message-list .message-contents .l-message {
margin-right: 10px; } }
-/* line 1, ../../../../general/res/sass/controls/_time-controller.scss */
-.l-time-controller {
- position: relative;
- margin: 10px 0;
- min-width: 400px; }
- /* line 12, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-inputs-holder,
- .l-time-controller .l-time-range-slider {
- font-size: 0.8em; }
- /* line 17, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-inputs-holder,
- .l-time-controller .l-time-range-slider-holder,
- .l-time-controller .l-time-range-ticks-holder {
- margin-bottom: 5px;
- position: relative; }
- /* line 24, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-slider,
- .l-time-controller .l-time-range-ticks {
+/* line 13, ../../../../general/res/sass/controls/_time-controller.scss */
+mct-include.l-time-controller {
+ overflow: hidden;
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ left: 0px;
+ width: auto;
+ height: auto;
+ display: block;
+ top: auto;
+ height: 83px;
+ min-width: 500px;
+ font-size: 0.8rem; }
+ /* line 38, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder,
+ mct-include.l-time-controller .l-time-range-slider-holder,
+ mct-include.l-time-controller .l-time-range-ticks-holder {
overflow: visible;
position: absolute;
top: 0;
@@ -2799,77 +2966,196 @@ label.checkbox.custom {
bottom: 0;
left: 0;
width: auto;
- height: auto; }
- /* line 30, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-inputs-holder {
- height: 20px; }
- /* line 34, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-slider,
- .l-time-controller .l-time-range-ticks {
- left: 90px;
- right: 90px; }
- /* line 40, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-slider-holder {
- height: 30px; }
- /* line 42, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-slider-holder .range-holder {
+ height: auto;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ top: auto; }
+ /* line 47, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider,
+ mct-include.l-time-controller .l-time-range-ticks {
+ overflow: visible;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ width: auto;
+ height: auto;
+ left: 150px;
+ right: 150px; }
+ /* line 54, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder {
+ height: 33px;
+ bottom: 46px;
+ padding-top: 5px;
+ border-top: 1px solid rgba(153, 153, 153, 0.1); }
+ /* line 59, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder .type-icon {
+ font-size: 120%;
+ vertical-align: middle; }
+ /* line 63, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input,
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem {
+ margin-right: 5px; }
+ /* line 66, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input .lbl,
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem .lbl {
+ color: #666666; }
+ /* line 69, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input .ui-symbol.icon, mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input .l-datetime-picker .l-month-year-pager .icon.pager, .l-datetime-picker .l-month-year-pager mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input .icon.pager,
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem .ui-symbol.icon,
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem .l-datetime-picker .l-month-year-pager .icon.pager,
+ .l-datetime-picker .l-month-year-pager mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem .icon.pager {
+ font-size: 11px;
+ width: 11px; }
+ /* line 76, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder {
+ height: 20px;
+ bottom: 23px; }
+ /* line 79, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder {
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
background: none;
- border: none;
- height: 75%; }
- /* line 50, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder {
- height: 10px; }
- /* line 52, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder .l-time-range-ticks {
- border-top: 1px solid rgba(153, 153, 153, 0.1); }
- /* line 54, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick {
- background-color: rgba(153, 153, 153, 0.1);
+ border: none; }
+ /* line 84, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line {
+ -moz-transform: translateX(50%);
+ -ms-transform: translateX(50%);
+ -webkit-transform: translateX(50%);
+ transform: translateX(50%);
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0px;
+ left: auto;
+ width: 8px;
+ height: auto;
+ z-index: 2; }
+ /* line 94, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line:before, mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line:after {
+ background-color: #00c2ff;
+ content: "";
+ position: absolute; }
+ /* line 100, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line:before {
+ top: 0;
+ right: auto;
+ bottom: -10px;
+ left: 3px;
+ width: 2px; }
+ /* line 106, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line:after {
+ -moz-border-radius: 8px;
+ -webkit-border-radius: 8px;
+ border-radius: 8px;
+ -moz-transform: translateY(-50%);
+ -ms-transform: translateY(-50%);
+ -webkit-transform: translateY(-50%);
+ transform: translateY(-50%);
+ top: 50%;
+ right: 0;
+ bottom: auto;
+ left: 0;
+ width: auto;
+ height: 8px; }
+ /* line 3, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range:hover .toi-line:before, mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range:hover .toi-line:after {
+ background-color: #fff; }
+ /* line 122, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder:not(:active) .knob,
+ mct-include.l-time-controller .l-time-range-slider-holder:not(:active) .range {
+ -moz-transition-property: left, right;
+ -o-transition-property: left, right;
+ -webkit-transition-property: left, right;
+ transition-property: left, right;
+ -moz-transition-duration: 500ms;
+ -o-transition-duration: 500ms;
+ -webkit-transition-duration: 500ms;
+ transition-duration: 500ms;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out; }
+ /* line 131, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder {
+ height: 20px; }
+ /* line 133, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder .l-time-range-ticks {
+ border-top: 1px solid rgba(255, 255, 255, 0.2); }
+ /* line 135, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick {
+ background-color: rgba(255, 255, 255, 0.2);
border: none;
+ height: 5px;
width: 1px;
- margin-left: -1px; }
- /* line 59, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick:first-child {
+ margin-left: -1px;
+ position: absolute; }
+ /* line 142, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick:first-child {
margin-left: 0; }
- /* line 62, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick .l-time-range-tick-label {
- color: rgba(204, 204, 204, 0.1);
- font-size: 0.7em;
+ /* line 145, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick .l-time-range-tick-label {
+ transform: translateX(-50%);
+ -webkit-transform: translateX(-50%);
+ color: #666666;
+ display: inline-block;
+ font-size: 0.9em;
position: absolute;
- margin-left: -25px;
- text-align: center;
- top: 10px;
- width: 50px;
+ top: 8px;
+ white-space: nowrap;
z-index: 2; }
- /* line 76, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob {
- width: 9px; }
- /* line 78, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob .range-value {
+ /* line 159, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob {
+ z-index: 2; }
+ /* line 161, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob .range-value {
+ -moz-transition-property: visibility, opacity, background-color, border-color;
+ -o-transition-property: visibility, opacity, background-color, border-color;
+ -webkit-transition-property: visibility, opacity, background-color, border-color;
+ transition-property: visibility, opacity, background-color, border-color;
+ -moz-transition-duration: 0.25s;
+ -o-transition-duration: 0.25s;
+ -webkit-transition-duration: 0.25s;
+ transition-duration: 0.25s;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ padding: 0 10px;
position: absolute;
- top: 50%;
- margin-top: -7px;
- white-space: nowrap;
- width: 75px; }
- /* line 87, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob:hover .range-value {
+ height: 20px;
+ line-height: 20px;
+ white-space: nowrap; }
+ /* line 170, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob:hover .range-value {
color: #0099cc; }
- /* line 90, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob.knob-l {
- margin-left: -4.5px; }
- /* line 92, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob.knob-l .range-value {
+ /* line 173, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-l {
+ margin-left: -10px; }
+ /* line 176, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-l .range-value {
text-align: right;
- right: 14px; }
- /* line 97, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob.knob-r {
- margin-right: -4.5px; }
- /* line 99, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob.knob-r .range-value {
- left: 14px; }
+ right: 10px; }
+ /* line 181, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-r {
+ margin-right: -10px; }
+ /* line 184, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-r .range-value {
+ left: 10px; }
+ /* line 3, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-r:hover + .range-holder .range .toi-line:before, mct-include.l-time-controller .knob.knob-r:hover + .range-holder .range .toi-line:after {
+ background-color: #fff; }
+
+/* line 198, ../../../../general/res/sass/controls/_time-controller.scss */
+.s-time-range-val {
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ background-color: rgba(255, 255, 255, 0.1);
+ padding: 1px 1px 0 5px; }
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
@@ -3163,11 +3449,12 @@ textarea {
-o-transition: background, 0.25s;
-webkit-transition: background, 0.25s;
transition: background, 0.25s;
- text-shadow: rgba(0, 0, 0, 0.3) 0 1px 1px;
- margin: 0 0 2px 2px;
+ text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px;
+ margin: 0 0 2px 0;
padding: 0 5px;
overflow: hidden;
- position: relative; }
+ position: relative;
+ line-height: 22px; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
.select .icon {
color: #0099cc; }
@@ -3178,7 +3465,7 @@ textarea {
/* line 296, ../../../../general/res/sass/_mixins.scss */
.select:not(.disabled):hover > .icon {
color: #33ccff; } }
- /* line 28, ../../../../general/res/sass/forms/_selects.scss */
+ /* line 31, ../../../../general/res/sass/forms/_selects.scss */
.select select {
-moz-appearance: none;
-webkit-appearance: none;
@@ -3191,10 +3478,10 @@ textarea {
border: none !important;
padding: 4px 25px 2px 0px;
width: 120%; }
- /* line 37, ../../../../general/res/sass/forms/_selects.scss */
+ /* line 40, ../../../../general/res/sass/forms/_selects.scss */
.select select option {
margin: 5px 0; }
- /* line 41, ../../../../general/res/sass/forms/_selects.scss */
+ /* line 44, ../../../../general/res/sass/forms/_selects.scss */
.select:after {
text-shadow: none;
content: '\76';
@@ -3202,6 +3489,7 @@ textarea {
font-family: 'symbolsfont';
margin-left: 3px;
vertical-align: top;
+ pointer-events: none;
color: rgba(153, 153, 153, 0.2);
position: absolute;
right: 5px;
@@ -3263,7 +3551,7 @@ textarea {
.channel-selector .btns-add-remove {
margin-top: 150px; }
/* line 39, ../../../../general/res/sass/forms/_channel-selector.scss */
- .channel-selector .btns-add-remove .s-btn, .channel-selector .btns-add-remove .s-menu {
+ .channel-selector .btns-add-remove .s-btn, .channel-selector .btns-add-remove .s-menu-btn {
display: block;
margin-bottom: 5px;
text-align: center; }
@@ -3289,26 +3577,44 @@ textarea {
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-/* line 23, ../../../../general/res/sass/forms/_datetime.scss */
-.complex.datetime span {
- display: inline-block;
- margin-right: 5px; }
-/* line 36, ../../../../general/res/sass/forms/_datetime.scss */
-.complex.datetime .fields {
- margin-top: 3px 0;
- padding: 3px 0; }
-/* line 41, ../../../../general/res/sass/forms/_datetime.scss */
-.complex.datetime .date {
- width: 85px; }
- /* line 44, ../../../../general/res/sass/forms/_datetime.scss */
- .complex.datetime .date input {
- width: 80px; }
-/* line 50, ../../../../general/res/sass/forms/_datetime.scss */
-.complex.datetime .time.sm {
- width: 45px; }
- /* line 53, ../../../../general/res/sass/forms/_datetime.scss */
- .complex.datetime .time.sm input {
- width: 40px; }
+/* line 29, ../../../../general/res/sass/forms/_datetime.scss */
+.complex.datetime {
+ /*
+ .field-hints,
+ .fields {
+ }
+
+
+ .field-hints {
+
+ }
+ */ }
+ /* line 30, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime span {
+ display: inline-block;
+ margin-right: 5px; }
+ /* line 46, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .fields {
+ margin-top: 3px 0;
+ padding: 3px 0; }
+ /* line 51, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .date {
+ width: 85px; }
+ /* line 24, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .date input[type="text"] {
+ width: 80px; }
+ /* line 55, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .time.md {
+ width: 65px; }
+ /* line 24, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .time.md input[type="text"] {
+ width: 60px; }
+ /* line 59, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .time.sm {
+ width: 45px; }
+ /* line 24, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .time.sm input[type="text"] {
+ width: 40px; }
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
@@ -3420,8 +3726,10 @@ span.req {
.t-filter input.t-filter-input:not(.ng-dirty) + .t-a-clear {
display: none; }
/* line 42, ../../../../general/res/sass/forms/_filter.scss */
-.filter .icon.ui-symbol,
-.t-filter .icon.ui-symbol {
+.filter .icon.ui-symbol, .filter .l-datetime-picker .l-month-year-pager .icon.pager, .l-datetime-picker .l-month-year-pager .filter .icon.pager,
+.t-filter .icon.ui-symbol,
+.t-filter .l-datetime-picker .l-month-year-pager .icon.pager,
+.l-datetime-picker .l-month-year-pager .t-filter .icon.pager {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
@@ -3432,12 +3740,16 @@ span.req {
padding: 0px 5px;
vertical-align: middle; }
/* line 50, ../../../../general/res/sass/forms/_filter.scss */
- .filter .icon.ui-symbol:hover,
- .t-filter .icon.ui-symbol:hover {
+ .filter .icon.ui-symbol:hover, .filter .l-datetime-picker .l-month-year-pager .icon.pager:hover, .l-datetime-picker .l-month-year-pager .filter .icon.pager:hover,
+ .t-filter .icon.ui-symbol:hover,
+ .t-filter .l-datetime-picker .l-month-year-pager .icon.pager:hover,
+ .l-datetime-picker .l-month-year-pager .t-filter .icon.pager:hover {
background: rgba(255, 255, 255, 0.1); }
/* line 54, ../../../../general/res/sass/forms/_filter.scss */
-.filter .s-a-clear.ui-symbol,
-.t-filter .s-a-clear.ui-symbol {
+.filter .s-a-clear.ui-symbol, .filter .l-datetime-picker .l-month-year-pager .s-a-clear.pager, .l-datetime-picker .l-month-year-pager .filter .s-a-clear.pager,
+.t-filter .s-a-clear.ui-symbol,
+.t-filter .l-datetime-picker .l-month-year-pager .s-a-clear.pager,
+.l-datetime-picker .l-month-year-pager .t-filter .s-a-clear.pager {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
@@ -3461,8 +3773,10 @@ span.req {
text-align: center;
z-index: 5; }
/* line 74, ../../../../general/res/sass/forms/_filter.scss */
- .filter .s-a-clear.ui-symbol:hover,
- .t-filter .s-a-clear.ui-symbol:hover {
+ .filter .s-a-clear.ui-symbol:hover, .filter .l-datetime-picker .l-month-year-pager .s-a-clear.pager:hover, .l-datetime-picker .l-month-year-pager .filter .s-a-clear.pager:hover,
+ .t-filter .s-a-clear.ui-symbol:hover,
+ .t-filter .l-datetime-picker .l-month-year-pager .s-a-clear.pager:hover,
+ .l-datetime-picker .l-month-year-pager .t-filter .s-a-clear.pager:hover {
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60);
opacity: 0.6;
background-color: #0099cc; }
@@ -3538,33 +3852,49 @@ span.req {
.bar .icon.major {
margin-right: 5px; }
/* line 70, ../../../../general/res/sass/user-environ/_layout.scss */
-.bar.abs, .s-menu span.bar.l-click-area {
+.bar.abs, .l-datetime-picker .l-month-year-pager .bar.pager,
+.l-datetime-picker .l-month-year-pager .bar.val, .s-menu-btn span.bar.l-click-area {
text-wrap: none;
white-space: nowrap; }
/* line 73, ../../../../general/res/sass/user-environ/_layout.scss */
- .bar.abs.left, .s-menu span.bar.left.l-click-area,
+ .bar.abs.left, .l-datetime-picker .l-month-year-pager .bar.left.pager,
+ .l-datetime-picker .l-month-year-pager .bar.left.val, .s-menu-btn span.bar.left.l-click-area,
.bar.abs .left,
- .s-menu span.bar.l-click-area .left {
+ .l-datetime-picker .l-month-year-pager .bar.pager .left,
+ .l-datetime-picker .l-month-year-pager .bar.val .left,
+ .s-menu-btn span.bar.l-click-area .left {
width: 45%;
right: auto; }
/* line 78, ../../../../general/res/sass/user-environ/_layout.scss */
- .bar.abs.right, .s-menu span.bar.right.l-click-area,
+ .bar.abs.right, .l-datetime-picker .l-month-year-pager .bar.right.pager,
+ .l-datetime-picker .l-month-year-pager .bar.right.val, .s-menu-btn span.bar.right.l-click-area,
.bar.abs .right,
- .s-menu span.bar.l-click-area .right {
+ .l-datetime-picker .l-month-year-pager .bar.pager .right,
+ .l-datetime-picker .l-month-year-pager .bar.val .right,
+ .s-menu-btn span.bar.l-click-area .right {
width: 45%;
left: auto;
text-align: right; }
/* line 83, ../../../../general/res/sass/user-environ/_layout.scss */
- .bar.abs.right .icon.major, .s-menu span.bar.right.l-click-area .icon.major,
+ .bar.abs.right .icon.major, .l-datetime-picker .l-month-year-pager .bar.right.pager .icon.major,
+ .l-datetime-picker .l-month-year-pager .bar.right.val .icon.major, .s-menu-btn span.bar.right.l-click-area .icon.major,
.bar.abs .right .icon.major,
- .s-menu span.bar.l-click-area .right .icon.major {
+ .l-datetime-picker .l-month-year-pager .bar.pager .right .icon.major,
+ .l-datetime-picker .l-month-year-pager .bar.val .right .icon.major,
+ .s-menu-btn span.bar.l-click-area .right .icon.major {
margin-left: 15px; }
/* line 89, ../../../../general/res/sass/user-environ/_layout.scss */
- .bar.abs .l-flex .left, .s-menu span.bar.l-click-area .l-flex .left,
+ .bar.abs .l-flex .left, .l-datetime-picker .l-month-year-pager .bar.pager .l-flex .left,
+ .l-datetime-picker .l-month-year-pager .bar.val .l-flex .left, .s-menu-btn span.bar.l-click-area .l-flex .left,
.bar.abs .l-flex .right,
- .s-menu span.bar.l-click-area .l-flex .right, .bar.abs.l-flex .left, .s-menu span.bar.l-flex.l-click-area .left,
+ .l-datetime-picker .l-month-year-pager .bar.pager .l-flex .right,
+ .l-datetime-picker .l-month-year-pager .bar.val .l-flex .right,
+ .s-menu-btn span.bar.l-click-area .l-flex .right, .bar.abs.l-flex .left, .l-datetime-picker .l-month-year-pager .bar.l-flex.pager .left,
+ .l-datetime-picker .l-month-year-pager .bar.l-flex.val .left, .s-menu-btn span.bar.l-flex.l-click-area .left,
.bar.abs.l-flex .right,
- .s-menu span.bar.l-flex.l-click-area .right {
+ .l-datetime-picker .l-month-year-pager .bar.l-flex.pager .right,
+ .l-datetime-picker .l-month-year-pager .bar.l-flex.val .right,
+ .s-menu-btn span.bar.l-flex.l-click-area .right {
width: auto; }
/* line 98, ../../../../general/res/sass/user-environ/_layout.scss */
@@ -3737,10 +4067,16 @@ span.req {
overflow: auto;
top: 64px; }
/* line 267, ../../../../general/res/sass/user-environ/_layout.scss */
- .pane.items .object-browse-bar .left.abs, .pane.items .object-browse-bar .s-menu span.left.l-click-area, .s-menu .pane.items .object-browse-bar span.left.l-click-area,
+ .pane.items .object-browse-bar .left.abs, .pane.items .object-browse-bar .l-datetime-picker .l-month-year-pager .left.pager, .l-datetime-picker .l-month-year-pager .pane.items .object-browse-bar .left.pager,
+ .pane.items .object-browse-bar .l-datetime-picker .l-month-year-pager .left.val,
+ .l-datetime-picker .l-month-year-pager .pane.items .object-browse-bar .left.val, .pane.items .object-browse-bar .s-menu-btn span.left.l-click-area, .s-menu-btn .pane.items .object-browse-bar span.left.l-click-area,
.pane.items .object-browse-bar .right.abs,
- .pane.items .object-browse-bar .s-menu span.right.l-click-area,
- .s-menu .pane.items .object-browse-bar span.right.l-click-area {
+ .pane.items .object-browse-bar .l-datetime-picker .l-month-year-pager .right.pager,
+ .l-datetime-picker .l-month-year-pager .pane.items .object-browse-bar .right.pager,
+ .pane.items .object-browse-bar .l-datetime-picker .l-month-year-pager .right.val,
+ .l-datetime-picker .l-month-year-pager .pane.items .object-browse-bar .right.val,
+ .pane.items .object-browse-bar .s-menu-btn span.right.l-click-area,
+ .s-menu-btn .pane.items .object-browse-bar span.right.l-click-area {
top: auto; }
/* line 278, ../../../../general/res/sass/user-environ/_layout.scss */
.pane.items .object-holder {
@@ -3770,13 +4106,15 @@ span.req {
right: 3px; }
/* line 318, ../../../../general/res/sass/user-environ/_layout.scss */
-.object-browse-bar .s-btn, .object-browse-bar .s-menu,
+.object-browse-bar .s-btn, .object-browse-bar .s-menu-btn,
.top-bar .buttons-main .s-btn,
-.top-bar .buttons-main .s-menu,
+.top-bar .buttons-main .s-menu-btn,
.top-bar .s-menu,
+.top-bar .menu,
.tool-bar .s-btn,
+.tool-bar .s-menu-btn,
.tool-bar .s-menu,
-.tool-bar .s-menu {
+.tool-bar .menu {
height: 25px;
line-height: 25px;
vertical-align: top; }
@@ -4172,13 +4510,15 @@ span.req {
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 23, ../../../../general/res/sass/search/_search.scss */
-.abs.search-holder, .s-menu span.search-holder.l-click-area {
+.abs.search-holder, .l-datetime-picker .l-month-year-pager .search-holder.pager,
+.l-datetime-picker .l-month-year-pager .search-holder.val, .s-menu-btn span.search-holder.l-click-area {
height: 25px;
bottom: 0;
top: 23px;
z-index: 5; }
/* line 27, ../../../../general/res/sass/search/_search.scss */
- .abs.search-holder.active, .s-menu span.search-holder.active.l-click-area {
+ .abs.search-holder.active, .l-datetime-picker .l-month-year-pager .search-holder.active.pager,
+ .l-datetime-picker .l-month-year-pager .search-holder.active.val, .s-menu-btn span.search-holder.active.l-click-area {
height: auto;
bottom: 0; }
@@ -4316,27 +4656,10 @@ span.req {
height: auto;
max-height: 100%;
position: relative; }
- /* line 228, ../../../../general/res/sass/search/_search.scss */
+ /* line 226, ../../../../general/res/sass/search/_search.scss */
.search .search-scroll .load-icon {
position: relative; }
- /* line 230, ../../../../general/res/sass/search/_search.scss */
- .search .search-scroll .load-icon.loading {
- pointer-events: none;
- margin-left: 6px; }
- /* line 234, ../../../../general/res/sass/search/_search.scss */
- .search .search-scroll .load-icon.loading .title-label {
- font-style: italic;
- font-size: .9em;
- opacity: 0.5;
- margin-left: 26px;
- line-height: 24px; }
- /* line 244, ../../../../general/res/sass/search/_search.scss */
- .search .search-scroll .load-icon.loading .wait-spinner {
- margin-left: 6px; }
- /* line 249, ../../../../general/res/sass/search/_search.scss */
- .search .search-scroll .load-icon:not(.loading) {
- cursor: pointer; }
- /* line 254, ../../../../general/res/sass/search/_search.scss */
+ /* line 230, ../../../../general/res/sass/search/_search.scss */
.search .search-scroll .load-more-button {
margin-top: 5px 0;
font-size: 0.8em;
@@ -4448,36 +4771,50 @@ span.req {
.overlay .hint {
color: #b3b3b3; }
/* line 80, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .abs.top-bar, .overlay .s-menu span.top-bar.l-click-area, .s-menu .overlay span.top-bar.l-click-area {
+ .overlay .abs.top-bar, .overlay .l-datetime-picker .l-month-year-pager .top-bar.pager, .l-datetime-picker .l-month-year-pager .overlay .top-bar.pager,
+ .overlay .l-datetime-picker .l-month-year-pager .top-bar.val,
+ .l-datetime-picker .l-month-year-pager .overlay .top-bar.val, .overlay .s-menu-btn span.top-bar.l-click-area, .s-menu-btn .overlay span.top-bar.l-click-area {
height: 45px; }
/* line 84, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .abs.editor, .overlay .s-menu span.editor.l-click-area, .s-menu .overlay span.editor.l-click-area,
+ .overlay .abs.editor, .overlay .l-datetime-picker .l-month-year-pager .editor.pager, .l-datetime-picker .l-month-year-pager .overlay .editor.pager,
+ .overlay .l-datetime-picker .l-month-year-pager .editor.val,
+ .l-datetime-picker .l-month-year-pager .overlay .editor.val, .overlay .s-menu-btn span.editor.l-click-area, .s-menu-btn .overlay span.editor.l-click-area,
.overlay .abs.message-body,
- .overlay .s-menu span.message-body.l-click-area,
- .s-menu .overlay span.message-body.l-click-area {
+ .overlay .l-datetime-picker .l-month-year-pager .message-body.pager,
+ .l-datetime-picker .l-month-year-pager .overlay .message-body.pager,
+ .overlay .l-datetime-picker .l-month-year-pager .message-body.val,
+ .l-datetime-picker .l-month-year-pager .overlay .message-body.val,
+ .overlay .s-menu-btn span.message-body.l-click-area,
+ .s-menu-btn .overlay span.message-body.l-click-area {
top: 55px;
bottom: 34px;
left: 0;
right: 0;
overflow: auto; }
/* line 92, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .abs.editor .field.l-med input[type='text'], .overlay .s-menu span.editor.l-click-area .field.l-med input[type='text'], .s-menu .overlay span.editor.l-click-area .field.l-med input[type='text'],
+ .overlay .abs.editor .field.l-med input[type='text'], .overlay .l-datetime-picker .l-month-year-pager .editor.pager .field.l-med input[type='text'], .l-datetime-picker .l-month-year-pager .overlay .editor.pager .field.l-med input[type='text'],
+ .overlay .l-datetime-picker .l-month-year-pager .editor.val .field.l-med input[type='text'],
+ .l-datetime-picker .l-month-year-pager .overlay .editor.val .field.l-med input[type='text'], .overlay .s-menu-btn span.editor.l-click-area .field.l-med input[type='text'], .s-menu-btn .overlay span.editor.l-click-area .field.l-med input[type='text'],
.overlay .abs.message-body .field.l-med input[type='text'],
- .overlay .s-menu span.message-body.l-click-area .field.l-med input[type='text'],
- .s-menu .overlay span.message-body.l-click-area .field.l-med input[type='text'] {
+ .overlay .l-datetime-picker .l-month-year-pager .message-body.pager .field.l-med input[type='text'],
+ .l-datetime-picker .l-month-year-pager .overlay .message-body.pager .field.l-med input[type='text'],
+ .overlay .l-datetime-picker .l-month-year-pager .message-body.val .field.l-med input[type='text'],
+ .l-datetime-picker .l-month-year-pager .overlay .message-body.val .field.l-med input[type='text'],
+ .overlay .s-menu-btn span.message-body.l-click-area .field.l-med input[type='text'],
+ .s-menu-btn .overlay span.message-body.l-click-area .field.l-med input[type='text'] {
width: 100%; }
/* line 98, ../../../../general/res/sass/overlay/_overlay.scss */
.overlay .bottom-bar {
text-align: right; }
/* line 100, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .bottom-bar .s-btn, .overlay .bottom-bar .s-menu {
+ .overlay .bottom-bar .s-btn, .overlay .bottom-bar .s-menu-btn {
font-size: 95%;
height: 24px;
line-height: 24px;
margin-left: 5px;
padding: 0 15px; }
/* line 102, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .bottom-bar .s-btn:not(.major), .overlay .bottom-bar .s-menu:not(.major) {
+ .overlay .bottom-bar .s-btn:not(.major), .overlay .bottom-bar .s-menu-btn:not(.major) {
background-color: gray;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
@@ -4504,22 +4841,24 @@ span.req {
-o-transition: background, 0.25s;
-webkit-transition: background, 0.25s;
transition: background, 0.25s;
- text-shadow: rgba(0, 0, 0, 0.3) 0 1px 1px; }
+ text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
- .overlay .bottom-bar .s-btn:not(.major) .icon, .overlay .bottom-bar .s-menu:not(.major) .icon {
+ .overlay .bottom-bar .s-btn:not(.major) .icon, .overlay .bottom-bar .s-menu-btn:not(.major) .icon {
color: #fff; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 294, ../../../../general/res/sass/_mixins.scss */
- .overlay .bottom-bar .s-btn:not(.major):not(.disabled):hover, .overlay .bottom-bar .s-menu:not(.major):not(.disabled):hover {
+ .overlay .bottom-bar .s-btn:not(.major):not(.disabled):hover, .overlay .bottom-bar .s-menu-btn:not(.major):not(.disabled):hover {
background: linear-gradient(#a6a6a6, #999999); }
/* line 296, ../../../../general/res/sass/_mixins.scss */
- .overlay .bottom-bar .s-btn:not(.major):not(.disabled):hover > .icon, .overlay .bottom-bar .s-menu:not(.major):not(.disabled):hover > .icon {
+ .overlay .bottom-bar .s-btn:not(.major):not(.disabled):hover > .icon, .overlay .bottom-bar .s-menu-btn:not(.major):not(.disabled):hover > .icon {
color: white; } }
/* line 110, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .bottom-bar .s-btn:first-child, .overlay .bottom-bar .s-menu:first-child {
+ .overlay .bottom-bar .s-btn:first-child, .overlay .bottom-bar .s-menu-btn:first-child {
margin-left: 0; }
/* line 117, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .abs.bottom-bar, .overlay .s-menu span.bottom-bar.l-click-area, .s-menu .overlay span.bottom-bar.l-click-area {
+ .overlay .abs.bottom-bar, .overlay .l-datetime-picker .l-month-year-pager .bottom-bar.pager, .l-datetime-picker .l-month-year-pager .overlay .bottom-bar.pager,
+ .overlay .l-datetime-picker .l-month-year-pager .bottom-bar.val,
+ .l-datetime-picker .l-month-year-pager .overlay .bottom-bar.val, .overlay .s-menu-btn span.bottom-bar.l-click-area, .s-menu-btn .overlay span.bottom-bar.l-click-area {
top: auto;
right: 0;
bottom: 0;
@@ -4588,16 +4927,30 @@ span.req {
.overlay > .holder .editor .form .form-row > .label:after {
float: none; }
/* line 57, ../../../../general/res/sass/mobile/overlay/_overlay.scss */
- .overlay > .holder .contents .abs.top-bar, .overlay > .holder .contents .s-menu span.top-bar.l-click-area, .s-menu .overlay > .holder .contents span.top-bar.l-click-area,
+ .overlay > .holder .contents .abs.top-bar, .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .top-bar.pager, .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .top-bar.pager,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .top-bar.val,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .top-bar.val, .overlay > .holder .contents .s-menu-btn span.top-bar.l-click-area, .s-menu-btn .overlay > .holder .contents span.top-bar.l-click-area,
.overlay > .holder .contents .abs.editor,
- .overlay > .holder .contents .s-menu span.editor.l-click-area,
- .s-menu .overlay > .holder .contents span.editor.l-click-area,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .editor.pager,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .editor.pager,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .editor.val,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .editor.val,
+ .overlay > .holder .contents .s-menu-btn span.editor.l-click-area,
+ .s-menu-btn .overlay > .holder .contents span.editor.l-click-area,
.overlay > .holder .contents .abs.message-body,
- .overlay > .holder .contents .s-menu span.message-body.l-click-area,
- .s-menu .overlay > .holder .contents span.message-body.l-click-area,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .message-body.pager,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .message-body.pager,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .message-body.val,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .message-body.val,
+ .overlay > .holder .contents .s-menu-btn span.message-body.l-click-area,
+ .s-menu-btn .overlay > .holder .contents span.message-body.l-click-area,
.overlay > .holder .contents .abs.bottom-bar,
- .overlay > .holder .contents .s-menu span.bottom-bar.l-click-area,
- .s-menu .overlay > .holder .contents span.bottom-bar.l-click-area {
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .bottom-bar.pager,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .bottom-bar.pager,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .bottom-bar.val,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .bottom-bar.val,
+ .overlay > .holder .contents .s-menu-btn span.bottom-bar.l-click-area,
+ .s-menu-btn .overlay > .holder .contents span.bottom-bar.l-click-area {
top: auto;
right: auto;
bottom: auto;
@@ -4722,7 +5075,7 @@ ul.tree {
.search-result-item .label .type-icon .icon.l-icon-alert {
position: absolute;
z-index: 2; }
- /* line 90, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 89, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .label .type-icon .icon.l-icon-alert,
.search-result-item .label .type-icon .icon.l-icon-alert {
color: #ff533a;
@@ -4732,7 +5085,7 @@ ul.tree {
width: 8px;
top: 1px;
right: -2px; }
- /* line 96, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 95, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .label .type-icon .icon.l-icon-link,
.search-result-item .label .type-icon .icon.l-icon-link {
color: #49dedb;
@@ -4742,7 +5095,7 @@ ul.tree {
width: 8px;
left: -3px;
bottom: 0px; }
- /* line 104, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 103, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .label .title-label,
.search-result-item .label .title-label {
overflow: hidden;
@@ -4758,63 +5111,47 @@ ul.tree {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
- /* line 115, ../../../../general/res/sass/tree/_tree.scss */
- .tree-item.loading,
- .search-result-item.loading {
- pointer-events: none; }
- /* line 117, ../../../../general/res/sass/tree/_tree.scss */
- .tree-item.loading .label,
- .search-result-item.loading .label {
- opacity: 0.5; }
- /* line 119, ../../../../general/res/sass/tree/_tree.scss */
- .tree-item.loading .label .title-label,
- .search-result-item.loading .label .title-label {
- font-style: italic; }
- /* line 123, ../../../../general/res/sass/tree/_tree.scss */
- .tree-item.loading .wait-spinner,
- .search-result-item.loading .wait-spinner {
- margin-left: 14px; }
- /* line 128, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 113, ../../../../general/res/sass/tree/_tree.scss */
.tree-item.selected,
.search-result-item.selected {
background: #006080;
color: #cccccc; }
- /* line 131, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 116, ../../../../general/res/sass/tree/_tree.scss */
.tree-item.selected .view-control,
.search-result-item.selected .view-control {
color: rgba(255, 255, 255, 0.3); }
- /* line 134, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 119, ../../../../general/res/sass/tree/_tree.scss */
.tree-item.selected .label .type-icon,
.search-result-item.selected .label .type-icon {
color: #cccccc; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
- /* line 142, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 127, ../../../../general/res/sass/tree/_tree.scss */
.tree-item:not(.selected):hover,
.search-result-item:not(.selected):hover {
background: rgba(153, 153, 153, 0.1);
color: #cccccc; }
- /* line 148, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 130, ../../../../general/res/sass/tree/_tree.scss */
.tree-item:not(.selected):hover .icon,
.search-result-item:not(.selected):hover .icon {
color: #33ccff; } }
- /* line 155, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 137, ../../../../general/res/sass/tree/_tree.scss */
.tree-item:not(.loading),
.search-result-item:not(.loading) {
cursor: pointer; }
- /* line 159, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 141, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .context-trigger,
.search-result-item .context-trigger {
top: -1px;
position: absolute;
right: 3px; }
- /* line 165, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 146, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .context-trigger .invoke-menu,
.search-result-item .context-trigger .invoke-menu {
font-size: 0.75em;
height: 0.9rem;
line-height: 0.9rem; }
-/* line 174, ../../../../general/res/sass/tree/_tree.scss */
+/* line 155, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .label {
left: 15px; }
@@ -4899,12 +5236,14 @@ ul.tree {
.frame.child-frame.panel:hover {
border-color: rgba(179, 179, 179, 0.1); }
/* line 32, ../../../../general/res/sass/user-environ/_frame.scss */
-.frame > .object-header.abs, .s-menu .frame > span.object-header.l-click-area {
+.frame > .object-header.abs, .l-datetime-picker .l-month-year-pager .frame > .object-header.pager,
+.l-datetime-picker .l-month-year-pager .frame > .object-header.val, .s-menu-btn .frame > span.object-header.l-click-area {
font-size: 0.75em;
height: 16px;
line-height: 16px; }
/* line 38, ../../../../general/res/sass/user-environ/_frame.scss */
-.frame > .object-holder.abs, .s-menu .frame > span.object-holder.l-click-area {
+.frame > .object-holder.abs, .l-datetime-picker .l-month-year-pager .frame > .object-holder.pager,
+.l-datetime-picker .l-month-year-pager .frame > .object-holder.val, .s-menu-btn .frame > span.object-holder.l-click-area {
top: 21px; }
/* line 41, ../../../../general/res/sass/user-environ/_frame.scss */
.frame .contents {
@@ -4913,17 +5252,17 @@ ul.tree {
bottom: 5px;
left: 5px; }
/* line 49, ../../../../general/res/sass/user-environ/_frame.scss */
-.frame.frame-template .s-btn, .frame.frame-template .s-menu,
-.frame.frame-template .s-menu {
+.frame.frame-template .s-btn, .frame.frame-template .s-menu-btn,
+.frame.frame-template .s-menu-btn {
height: 16px;
line-height: 16px;
padding: 0 5px; }
/* line 54, ../../../../general/res/sass/user-environ/_frame.scss */
- .frame.frame-template .s-btn > span, .frame.frame-template .s-menu > span,
- .frame.frame-template .s-menu > span {
+ .frame.frame-template .s-btn > span, .frame.frame-template .s-menu-btn > span,
+ .frame.frame-template .s-menu-btn > span {
font-size: 0.65rem; }
/* line 59, ../../../../general/res/sass/user-environ/_frame.scss */
-.frame.frame-template .s-menu:after {
+.frame.frame-template .s-menu-btn:after {
font-size: 8px; }
/* line 63, ../../../../general/res/sass/user-environ/_frame.scss */
.frame.frame-template .view-switcher {
@@ -4984,7 +5323,9 @@ ul.tree {
.edit-mode .top-bar .buttons-main {
white-space: nowrap; }
/* line 52, ../../../../general/res/sass/user-environ/_top-bar.scss */
- .edit-mode .top-bar .buttons-main.abs, .edit-mode .top-bar .s-menu span.buttons-main.l-click-area, .s-menu .edit-mode .top-bar span.buttons-main.l-click-area {
+ .edit-mode .top-bar .buttons-main.abs, .edit-mode .top-bar .l-datetime-picker .l-month-year-pager .buttons-main.pager, .l-datetime-picker .l-month-year-pager .edit-mode .top-bar .buttons-main.pager,
+ .edit-mode .top-bar .l-datetime-picker .l-month-year-pager .buttons-main.val,
+ .l-datetime-picker .l-month-year-pager .edit-mode .top-bar .buttons-main.val, .edit-mode .top-bar .s-menu-btn span.buttons-main.l-click-area, .s-menu-btn .edit-mode .top-bar span.buttons-main.l-click-area {
bottom: auto;
left: auto; }
@@ -5219,37 +5560,41 @@ table {
table thead,
table .thead {
border-bottom: 1px solid #333; }
- /* line 43, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 44, ../../../../general/res/sass/lists/_tabular.scss */
+ .tabular:not(.fixed-header) tr th,
+ table:not(.fixed-header) tr th {
+ background-color: rgba(255, 255, 255, 0.1); }
+ /* line 48, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tbody, .tabular .tbody,
table tbody,
table .tbody {
display: table-row-group; }
- /* line 46, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 51, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tbody tr:hover, .tabular tbody .tr:hover, .tabular .tbody tr:hover, .tabular .tbody .tr:hover,
table tbody tr:hover,
table tbody .tr:hover,
table .tbody tr:hover,
table .tbody .tr:hover {
background: rgba(128, 128, 128, 0.1); }
- /* line 51, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 56, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr, .tabular .tr,
table tr,
table .tr {
display: table-row; }
- /* line 53, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 58, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr:first-child .td, .tabular .tr:first-child .td,
table tr:first-child .td,
table .tr:first-child .td {
border-top: none; }
- /* line 57, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 62, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr.group-header td, .tabular tr.group-header .td, .tabular .tr.group-header td, .tabular .tr.group-header .td,
table tr.group-header td,
table tr.group-header .td,
table .tr.group-header td,
table .tr.group-header .td {
- background-color: #404040;
- color: #a6a6a6; }
- /* line 63, ../../../../general/res/sass/lists/_tabular.scss */
+ background-color: rgba(242, 242, 242, 0.1);
+ color: #8c8c8c; }
+ /* line 68, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th, .tabular tr .th, .tabular tr td, .tabular tr .td, .tabular .tr th, .tabular .tr .th, .tabular .tr td, .tabular .tr .td,
table tr th,
table tr .th,
@@ -5260,26 +5605,25 @@ table {
table .tr td,
table .tr .td {
display: table-cell; }
- /* line 66, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 71, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th, .tabular tr .th, .tabular .tr th, .tabular .tr .th,
table tr th,
table tr .th,
table .tr th,
table .tr .th {
- background-color: #4d4d4d;
border-left: 1px solid #333;
- color: #b3b3b3;
+ color: #999;
padding: 5px 5px;
white-space: nowrap;
vertical-align: middle; }
- /* line 73, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 77, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th:first-child, .tabular tr .th:first-child, .tabular .tr th:first-child, .tabular .tr .th:first-child,
table tr th:first-child,
table tr .th:first-child,
table .tr th:first-child,
table .tr .th:first-child {
border-left: none; }
- /* line 77, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 81, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th.sort.sort:after, .tabular tr .th.sort.sort:after, .tabular .tr th.sort.sort:after, .tabular .tr .th.sort.sort:after,
table tr th.sort.sort:after,
table tr .th.sort.sort:after,
@@ -5291,21 +5635,21 @@ table {
content: "\ed";
display: inline-block;
margin-left: 3px; }
- /* line 85, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 89, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th.sort.sort.desc:after, .tabular tr .th.sort.sort.desc:after, .tabular .tr th.sort.sort.desc:after, .tabular .tr .th.sort.sort.desc:after,
table tr th.sort.sort.desc:after,
table tr .th.sort.sort.desc:after,
table .tr th.sort.sort.desc:after,
table .tr .th.sort.sort.desc:after {
content: "\ec"; }
- /* line 89, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 93, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th.sortable, .tabular tr .th.sortable, .tabular .tr th.sortable, .tabular .tr .th.sortable,
table tr th.sortable,
table tr .th.sortable,
table .tr th.sortable,
table .tr .th.sortable {
cursor: pointer; }
- /* line 93, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 97, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr td, .tabular tr .td, .tabular .tr td, .tabular .tr .td,
table tr td,
table tr .td,
@@ -5317,21 +5661,21 @@ table {
padding: 3px 5px;
word-wrap: break-word;
vertical-align: top; }
- /* line 100, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 104, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr td.numeric, .tabular tr .td.numeric, .tabular .tr td.numeric, .tabular .tr .td.numeric,
table tr td.numeric,
table tr .td.numeric,
table .tr td.numeric,
table .tr .td.numeric {
text-align: right; }
- /* line 103, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 107, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr td.s-cell-type-value, .tabular tr .td.s-cell-type-value, .tabular .tr td.s-cell-type-value, .tabular .tr .td.s-cell-type-value,
table tr td.s-cell-type-value,
table tr .td.s-cell-type-value,
table .tr td.s-cell-type-value,
table .tr .td.s-cell-type-value {
text-align: right; }
- /* line 105, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 109, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr td.s-cell-type-value .l-cell-contents, .tabular tr .td.s-cell-type-value .l-cell-contents, .tabular .tr td.s-cell-type-value .l-cell-contents, .tabular .tr .td.s-cell-type-value .l-cell-contents,
table tr td.s-cell-type-value .l-cell-contents,
table tr .td.s-cell-type-value .l-cell-contents,
@@ -5342,23 +5686,23 @@ table {
border-radius: 2px;
padding-left: 5px;
padding-right: 5px; }
- /* line 121, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 125, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.filterable tbody, .tabular.filterable .tbody,
table.filterable tbody,
table.filterable .tbody {
top: 44px; }
- /* line 124, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 128, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.filterable input[type="text"],
table.filterable input[type="text"] {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%; }
- /* line 130, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 134, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header,
table.fixed-header {
height: 100%; }
- /* line 132, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 136, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header thead, .tabular.fixed-header .thead,
.tabular.fixed-header tbody tr, .tabular.fixed-header .tbody .tr,
table.fixed-header thead,
@@ -5367,12 +5711,12 @@ table {
table.fixed-header .tbody .tr {
display: table;
table-layout: fixed; }
- /* line 137, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 141, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header thead, .tabular.fixed-header .thead,
table.fixed-header thead,
table.fixed-header .thead {
width: calc(100% - 10px); }
- /* line 139, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 143, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header thead:before, .tabular.fixed-header .thead:before,
table.fixed-header thead:before,
table.fixed-header .thead:before {
@@ -5382,8 +5726,8 @@ table {
position: absolute;
width: 100%;
height: 22px;
- background: rgba(255, 255, 255, 0.15); }
- /* line 149, ../../../../general/res/sass/lists/_tabular.scss */
+ background-color: rgba(255, 255, 255, 0.1); }
+ /* line 153, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header tbody, .tabular.fixed-header .tbody,
table.fixed-header tbody,
table.fixed-header .tbody {
@@ -5398,7 +5742,7 @@ table {
top: 22px;
display: block;
overflow-y: scroll; }
- /* line 157, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 161, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.t-event-messages td, .tabular.t-event-messages .td,
table.t-event-messages td,
table.t-event-messages .td {
@@ -5485,7 +5829,7 @@ table {
/* line 89, ../../../../general/res/sass/plots/_plots-main.scss */
.gl-plot .gl-plot-label,
.gl-plot .l-plot-label {
- color: #cccccc;
+ color: #666666;
position: absolute;
text-align: center; }
/* line 97, ../../../../general/res/sass/plots/_plots-main.scss */
@@ -5770,9 +6114,6 @@ table {
.l-view-section.fixed {
font-size: 0.8em; }
/* line 13, ../../../../general/res/sass/_views.scss */
- .l-view-section.scrolling {
- overflow: auto; }
- /* line 16, ../../../../general/res/sass/_views.scss */
.l-view-section .controls,
.l-view-section label,
.l-view-section .inline-block {
@@ -5835,7 +6176,7 @@ table {
-o-transition: background, 0.25s;
-webkit-transition: background, 0.25s;
transition: background, 0.25s;
- text-shadow: rgba(0, 0, 0, 0.3) 0 1px 1px;
+ text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px;
box-sizing: border-box;
cursor: pointer;
float: left;
@@ -5984,7 +6325,7 @@ table {
-o-transition: background, 0.25s;
-webkit-transition: background, 0.25s;
transition: background, 0.25s;
- text-shadow: rgba(0, 0, 0, 0.3) 0 1px 1px;
+ text-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px;
color: #80dfff; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
.items-holder .item.grid-item.selected .icon {
@@ -6137,7 +6478,7 @@ table {
.autoflow {
font-size: 0.75rem; }
/* line 32, ../../../../general/res/sass/_autoflow.scss */
- .autoflow:hover .l-autoflow-header .s-btn.change-column-width, .autoflow:hover .l-autoflow-header .change-column-width.s-menu {
+ .autoflow:hover .l-autoflow-header .s-btn.change-column-width, .autoflow:hover .l-autoflow-header .change-column-width.s-menu-btn {
-moz-transition-property: visibility, opacity, background-color, border-color;
-o-transition-property: visibility, opacity, background-color, border-color;
-webkit-transition-property: visibility, opacity, background-color, border-color;
@@ -6161,7 +6502,7 @@ table {
.autoflow .l-autoflow-header span {
vertical-align: middle; }
/* line 48, ../../../../general/res/sass/_autoflow.scss */
- .autoflow .l-autoflow-header .s-btn.change-column-width, .autoflow .l-autoflow-header .change-column-width.s-menu {
+ .autoflow .l-autoflow-header .s-btn.change-column-width, .autoflow .l-autoflow-header .change-column-width.s-menu-btn {
-moz-transition-property: visibility, opacity, background-color, border-color;
-o-transition-property: visibility, opacity, background-color, border-color;
-webkit-transition-property: visibility, opacity, background-color, border-color;
@@ -6438,7 +6779,7 @@ table {
left: 0;
z-index: 1; }
/* line 22, ../../../../general/res/sass/features/_time-display.scss */
- .l-time-display.l-timer .l-elem.l-value .ui-symbol.direction {
+ .l-time-display.l-timer .l-elem.l-value .ui-symbol.direction, .l-time-display.l-timer .l-elem.l-value .l-datetime-picker .l-month-year-pager .direction.pager, .l-datetime-picker .l-month-year-pager .l-time-display.l-timer .l-elem.l-value .direction.pager {
font-size: 0.8em; }
/* line 26, ../../../../general/res/sass/features/_time-display.scss */
.l-time-display.l-timer:hover .l-elem.l-value {
diff --git a/platform/commonUI/themes/espresso/res/sass/_constants.scss b/platform/commonUI/themes/espresso/res/sass/_constants.scss
index db68d1ab1..ac5cae86c 100644
--- a/platform/commonUI/themes/espresso/res/sass/_constants.scss
+++ b/platform/commonUI/themes/espresso/res/sass/_constants.scss
@@ -7,12 +7,14 @@ $colorKey: #0099cc;
$colorKeySelectedBg: #005177;
$colorKeyFg: #fff;
$colorInteriorBorder: rgba($colorBodyFg, 0.1);
+$colorA: #ccc;
+$colorAHov: #fff;
$contrastRatioPercent: 7%;
$basicCr: 3px;
$controlCr: 3px;
$smallCr: 2px;
-// Buttons
+// Buttons and Controls
$colorBtnBg: pullForward($colorBodyBg, $contrastRatioPercent); //
$colorBtnFg: $colorBodyFg;
$colorBtnMajorBg: $colorKey;
@@ -20,6 +22,18 @@ $colorBtnMajorFg: $colorKeyFg;
$colorBtnIcon: $colorKey;
$colorInvokeMenu: #fff;
$contrastInvokeMenuPercent: 20%;
+$shdwBtns: rgba(black, 0.2) 0 1px 2px;
+$sliderColorBase: $colorKey;
+$sliderColorRangeHolder: rgba(black, 0.1);
+$sliderColorRange: rgba($sliderColorBase, 0.3);
+$sliderColorRangeHov: rgba($sliderColorBase, 0.5);
+$sliderColorKnob: rgba($sliderColorBase, 0.6);
+$sliderColorKnobHov: $sliderColorBase;
+$sliderColorRangeValHovBg: rgba($sliderColorBase, 0.1);
+$sliderColorRangeValHovFg: $colorKeyFg;
+$sliderKnobW: nth($ueTimeControlH,2)/2;
+$timeControllerToiLineColor: #00c2ff;
+$timeControllerToiLineColorHov: #fff;
// General Colors
$colorAlt1: #ffc700;
@@ -32,6 +46,7 @@ $colorGridLines: rgba(#fff, 0.05);
$colorInvokeMenu: #fff;
$colorObjHdrTxt: $colorBodyFg;
$colorObjHdrIc: pullForward($colorObjHdrTxt, 20%);
+$colorTick: rgba(white, 0.2);
// Menu colors
$colorMenuBg: pullForward($colorBodyBg, 23%);
@@ -111,26 +126,27 @@ $colorItemBgSelected: $colorKey;
$colorTabBorder: pullForward($colorBodyBg, 10%);
$colorTabBodyBg: darken($colorBodyBg, 10%);
$colorTabBodyFg: lighten($colorTabBodyBg, 40%);
-$colorTabHeaderBg: lighten($colorBodyBg, 10%);
-$colorTabHeaderFg: lighten($colorTabHeaderBg, 40%);
+$colorTabHeaderBg: rgba(white, 0.1); // lighten($colorBodyBg, 10%);
+$colorTabHeaderFg: $colorBodyFg; //lighten($colorTabHeaderBg, 40%);
$colorTabHeaderBorder: $colorBodyBg;
// Plot
$colorPlotBg: rgba(black, 0.1);
$colorPlotFg: $colorBodyFg;
-$colorPlotHash: rgba(white, 0.2);
+$colorPlotHash: $colorTick;
$stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder;
+$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
// Tree
$colorItemTreeIcon: $colorKey;
$colorItemTreeIconHover: lighten($colorItemTreeIcon, 20%);
+$colorItemTreeVCHover: $colorAlt1;
$colorItemTreeFg: $colorBodyFg;
$colorItemTreeSelectedBg: pushBack($colorKey, 15%);
$colorItemTreeSelectedFg: pullForward($colorBodyFg, 20%);
$colorItemTreeVC: rgba(#fff, 0.3);
$colorItemTreeSelectedVC: $colorItemTreeVC;
-$colorItemTreeVCHover: $colorAlt1;
$shdwItemTreeIcon: 0.6;
// Scrollbar
@@ -151,5 +167,16 @@ $colorGrippyInteriorHover: $colorKey;
// Mobile
$colorMobilePaneLeft: darken($colorBodyBg, 5%);
+// Datetime Picker
+$colorCalCellHovBg: $colorKey;
+$colorCalCellHovFg: $colorKeyFg;
+$colorCalCellSelectedBg: $colorItemTreeSelectedBg;
+$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
+$colorCalCellInMonthBg: pushBack($colorMenuBg, 5%);
+
// About Screen
-$colorAboutLink: #84b3ff; \ No newline at end of file
+$colorAboutLink: #84b3ff;
+
+// Loading
+$colorLoadingBg: rgba($colorBodyFg, 0.2);
+$colorLoadingFg: $colorAlt1;
diff --git a/platform/commonUI/themes/espresso/res/sass/_mixins.scss b/platform/commonUI/themes/espresso/res/sass/_mixins.scss
index cf19f56e2..385b41a07 100644
--- a/platform/commonUI/themes/espresso/res/sass/_mixins.scss
+++ b/platform/commonUI/themes/espresso/res/sass/_mixins.scss
@@ -1,13 +1,13 @@
@mixin containerSubtle($bg: $colorBodyBg, $fg: $colorBodyFg, $hover: false) {
@include containerBase($bg, $fg);
@include background-image(linear-gradient(lighten($bg, 5%), $bg));
- @include boxShdwSubtle();
+ @include boxShdw($shdwBtns);
}
-@mixin btnSubtle($bg: $colorBodyBg, $bgHov: none, $fg: $colorBodyFg, $ic: $colorBtnIcon) {
+ @mixin btnSubtle($bg: $colorBodyBg, $bgHov: none, $fg: $colorBodyFg, $ic: $colorBtnIcon) {
@include containerSubtle($bg, $fg);
@include btnBase($bg, linear-gradient(lighten($bg, 15%), lighten($bg, 10%)), $fg, $ic);
- @include text-shadow(rgba(black, 0.3) 0 1px 1px);
+ @include text-shadow($shdwItemText);
}
@function pullForward($c: $colorBodyBg, $p: 20%) {
diff --git a/platform/commonUI/themes/snow/res/css/theme-snow.css b/platform/commonUI/themes/snow/res/css/theme-snow.css
index de35232d0..ed84a7854 100644
--- a/platform/commonUI/themes/snow/res/css/theme-snow.css
+++ b/platform/commonUI/themes/snow/res/css/theme-snow.css
@@ -247,7 +247,7 @@ a.disabled {
/* line 42, ../../../../general/res/sass/_effects.scss */
.test {
- background-color: rgba(255, 204, 0, 0.2); }
+ background-color: rgba(255, 204, 0, 0.2) !important; }
@-moz-keyframes pulse {
0% {
@@ -314,18 +314,18 @@ a.disabled {
font-weight: normal;
font-style: normal; }
/* line 37, ../../../../general/res/sass/_global.scss */
-.ui-symbol {
+.ui-symbol, .l-datetime-picker .l-month-year-pager .pager {
font-family: 'symbolsfont'; }
/************************** HTML ENTITIES */
/* line 42, ../../../../general/res/sass/_global.scss */
a {
- color: #ccc;
+ color: #999;
cursor: pointer;
text-decoration: none; }
/* line 46, ../../../../general/res/sass/_global.scss */
a:hover {
- color: #fff; }
+ color: #0099cc; }
/* line 51, ../../../../general/res/sass/_global.scss */
body, html {
@@ -373,7 +373,8 @@ mct-container {
display: block; }
/* line 97, ../../../../general/res/sass/_global.scss */
-.abs, .s-menu span.l-click-area {
+.abs, .l-datetime-picker .l-month-year-pager .pager,
+.l-datetime-picker .l-month-year-pager .val, .s-menu-btn span.l-click-area {
position: absolute;
top: 0;
right: 0;
@@ -403,21 +404,29 @@ mct-container {
text-align: center; }
/* line 128, ../../../../general/res/sass/_global.scss */
+.scrolling {
+ overflow: auto; }
+
+/* line 132, ../../../../general/res/sass/_global.scss */
+.vscroll {
+ overflow-y: auto; }
+
+/* line 136, ../../../../general/res/sass/_global.scss */
.no-margin {
margin: 0; }
-/* line 132, ../../../../general/res/sass/_global.scss */
+/* line 140, ../../../../general/res/sass/_global.scss */
.ds {
-moz-box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px;
-webkit-box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px;
box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px; }
-/* line 136, ../../../../general/res/sass/_global.scss */
+/* line 144, ../../../../general/res/sass/_global.scss */
.hide,
.hidden {
display: none !important; }
-/* line 141, ../../../../general/res/sass/_global.scss */
+/* line 149, ../../../../general/res/sass/_global.scss */
.sep {
color: rgba(255, 255, 255, 0.2); }
@@ -443,7 +452,8 @@ mct-container {
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 26, ../../../../general/res/sass/_about.scss */
-.l-about.abs, .s-menu span.l-about.l-click-area {
+.l-about.abs, .l-datetime-picker .l-month-year-pager .l-about.pager,
+.l-datetime-picker .l-month-year-pager .l-about.val, .s-menu-btn span.l-about.l-click-area {
overflow: auto; }
/* line 31, ../../../../general/res/sass/_about.scss */
.l-about .l-logo-holder {
@@ -493,7 +503,7 @@ mct-container {
.s-about .s-logo-openmctweb {
background-image: url("../../../../general/res/images/logo-openmctweb-shdw.svg"); }
/* line 81, ../../../../general/res/sass/_about.scss */
- .s-about .s-btn, .s-about .s-menu {
+ .s-about .s-btn, .s-about .s-menu-btn {
line-height: 2em; }
/* line 85, ../../../../general/res/sass/_about.scss */
.s-about .l-licenses-software .l-license-software {
@@ -534,7 +544,8 @@ mct-container {
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 24, ../../../../general/res/sass/_text.scss */
-.abs.l-standalone, .s-menu span.l-standalone.l-click-area {
+.abs.l-standalone, .l-datetime-picker .l-month-year-pager .l-standalone.pager,
+.l-datetime-picker .l-month-year-pager .l-standalone.val, .s-menu-btn span.l-standalone.l-click-area {
padding: 5% 20%; }
/* line 29, ../../../../general/res/sass/_text.scss */
@@ -596,48 +607,54 @@ mct-container {
border-right: 5px solid transparent; }
/* line 32, ../../../../general/res/sass/_icons.scss */
-.ui-symbol.icon {
+.ui-symbol.type-icon, .l-datetime-picker .l-month-year-pager .type-icon.pager {
+ color: #b3b3b3; }
+/* line 35, ../../../../general/res/sass/_icons.scss */
+.ui-symbol.icon, .l-datetime-picker .l-month-year-pager .icon.pager {
color: #0099cc; }
- /* line 34, ../../../../general/res/sass/_icons.scss */
- .ui-symbol.icon.alert {
- color: #ff533a; }
- /* line 36, ../../../../general/res/sass/_icons.scss */
- .ui-symbol.icon.alert:hover {
- color: #ffaca0; }
- /* line 40, ../../../../general/res/sass/_icons.scss */
- .ui-symbol.icon.major {
+ /* line 37, ../../../../general/res/sass/_icons.scss */
+ .ui-symbol.icon.alert, .l-datetime-picker .l-month-year-pager .icon.alert.pager {
+ color: #ff3c00; }
+ /* line 39, ../../../../general/res/sass/_icons.scss */
+ .ui-symbol.icon.alert:hover, .l-datetime-picker .l-month-year-pager .icon.alert.pager:hover {
+ color: #ff8a66; }
+ /* line 43, ../../../../general/res/sass/_icons.scss */
+ .ui-symbol.icon.major, .l-datetime-picker .l-month-year-pager .icon.major.pager {
font-size: 1.65em; }
+/* line 47, ../../../../general/res/sass/_icons.scss */
+.ui-symbol.icon-calendar:after, .l-datetime-picker .l-month-year-pager .icon-calendar.pager:after {
+ content: "\e605"; }
-/* line 46, ../../../../general/res/sass/_icons.scss */
-.bar .ui-symbol {
+/* line 52, ../../../../general/res/sass/_icons.scss */
+.bar .ui-symbol, .bar .l-datetime-picker .l-month-year-pager .pager, .l-datetime-picker .l-month-year-pager .bar .pager {
display: inline-block; }
-/* line 50, ../../../../general/res/sass/_icons.scss */
+/* line 56, ../../../../general/res/sass/_icons.scss */
.invoke-menu {
text-shadow: none;
display: inline-block; }
-/* line 55, ../../../../general/res/sass/_icons.scss */
-.s-menu .invoke-menu,
+/* line 61, ../../../../general/res/sass/_icons.scss */
+.s-menu-btn .invoke-menu,
.icon.major .invoke-menu {
margin-left: 3px; }
-/* line 60, ../../../../general/res/sass/_icons.scss */
+/* line 66, ../../../../general/res/sass/_icons.scss */
.menu .type-icon,
.tree-item .type-icon,
.super-menu.menu .type-icon {
position: absolute; }
-/* line 70, ../../../../general/res/sass/_icons.scss */
+/* line 76, ../../../../general/res/sass/_icons.scss */
.l-icon-link:before {
content: "\f4"; }
-/* line 74, ../../../../general/res/sass/_icons.scss */
+/* line 80, ../../../../general/res/sass/_icons.scss */
.l-icon-alert {
display: none !important; }
- /* line 76, ../../../../general/res/sass/_icons.scss */
+ /* line 82, ../../../../general/res/sass/_icons.scss */
.l-icon-alert:before {
- color: #ff533a;
+ color: #ff3c00;
content: "!"; }
/* line 13, ../../../../general/res/sass/_limits.scss */
@@ -1039,27 +1056,22 @@ mct-container {
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-@-webkit-keyframes rotation {
- from {
- -webkit-transform: rotate(0deg); }
- to {
- -webkit-transform: rotate(359deg); } }
@-moz-keyframes rotation {
- from {
- -moz-transform: rotate(0deg); }
- to {
- -moz-transform: rotate(359deg); } }
-@-o-keyframes rotation {
- from {
- -o-transform: rotate(0deg); }
- to {
- -o-transform: rotate(359deg); } }
+ 0% {
+ transform: rotate(0deg); }
+ 100% {
+ transform: rotate(359deg); } }
+@-webkit-keyframes rotation {
+ 0% {
+ transform: rotate(0deg); }
+ 100% {
+ transform: rotate(359deg); } }
@keyframes rotation {
- from {
+ 0% {
transform: rotate(0deg); }
- to {
+ 100% {
transform: rotate(359deg); } }
-/* line 42, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+/* line 63, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.t-wait-spinner,
.wait-spinner {
display: block;
@@ -1072,6 +1084,8 @@ mct-container {
border-top-color: #0099cc;
border-style: solid;
border-width: 0.5em;
+ -moz-border-radius: 100%;
+ -webkit-border-radius: 100%;
border-radius: 100%;
top: 50%;
left: 50%;
@@ -1082,7 +1096,7 @@ mct-container {
margin-top: -5%;
margin-left: -5%;
z-index: 2; }
- /* line 53, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ /* line 74, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.t-wait-spinner.inline,
.wait-spinner.inline {
display: inline-block !important;
@@ -1090,26 +1104,26 @@ mct-container {
position: relative !important;
vertical-align: middle; }
-/* line 61, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+/* line 82, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.l-wait-spinner-holder {
pointer-events: none;
position: absolute; }
- /* line 65, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ /* line 86, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.l-wait-spinner-holder.align-left .t-wait-spinner {
left: 0;
margin-left: 0; }
- /* line 70, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ /* line 91, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.l-wait-spinner-holder.full-size {
display: inline-block;
height: 100%;
width: 100%; }
- /* line 73, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ /* line 94, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.l-wait-spinner-holder.full-size .t-wait-spinner {
top: 0;
margin-top: 0;
padding: 30%; }
-/* line 82, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+/* line 103, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.treeview .wait-spinner {
display: block;
position: absolute;
@@ -1121,6 +1135,8 @@ mct-container {
border-top-color: #0099cc;
border-style: solid;
border-width: 0.25em;
+ -moz-border-radius: 100%;
+ -webkit-border-radius: 100%;
border-radius: 100%;
height: 10px;
width: 10px;
@@ -1129,7 +1145,7 @@ mct-container {
top: 2px;
left: 0; }
-/* line 91, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+/* line 112, ../../../../general/res/sass/helpers/_wait-spinner.scss */
.wait-spinner.sm {
display: block;
position: absolute;
@@ -1141,6 +1157,8 @@ mct-container {
border-top-color: #0099cc;
border-style: solid;
border-width: 0.25em;
+ -moz-border-radius: 100%;
+ -webkit-border-radius: 100%;
border-radius: 100%;
height: 13px;
width: 13px;
@@ -1150,6 +1168,77 @@ mct-container {
top: 0;
left: 0; }
+/* line 122, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+.loading {
+ pointer-events: none; }
+ /* line 125, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ .loading:before, .loading:after {
+ content: ''; }
+ /* line 129, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ .loading:before {
+ -moz-animation-name: rotateCentered;
+ -webkit-animation-name: rotateCentered;
+ animation-name: rotateCentered;
+ -moz-animation-duration: 0.5s;
+ -webkit-animation-duration: 0.5s;
+ animation-duration: 0.5s;
+ -moz-animation-iteration-count: infinite;
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+ -moz-animation-timing-function: linear;
+ -webkit-animation-timing-function: linear;
+ animation-timing-function: linear;
+ border-color: rgba(119, 107, 162, 0.25);
+ border-top-color: #776ba2;
+ border-style: solid;
+ border-width: 5px;
+ -moz-border-radius: 100%;
+ -webkit-border-radius: 100%;
+ border-radius: 100%;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ display: block;
+ position: absolute;
+ height: 0;
+ width: 0;
+ padding: 7%;
+ left: 50%;
+ top: 50%;
+ z-index: 10; }
+@-moz-keyframes rotateCentered {
+ 0% {
+ transform: translateX(-50%) translateY(-50%) rotate(0deg); }
+ 100% {
+ transform: translateX(-50%) translateY(-50%) rotate(359deg); } }
+@-webkit-keyframes rotateCentered {
+ 0% {
+ transform: translateX(-50%) translateY(-50%) rotate(0deg); }
+ 100% {
+ transform: translateX(-50%) translateY(-50%) rotate(359deg); } }
+@keyframes rotateCentered {
+ 0% {
+ transform: translateX(-50%) translateY(-50%) rotate(0deg); }
+ 100% {
+ transform: translateX(-50%) translateY(-50%) rotate(359deg); } }
+ /* line 133, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ .loading:after {
+ overflow: hidden;
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ left: 0px;
+ width: auto;
+ height: auto;
+ background: rgba(119, 107, 162, 0.1);
+ display: block;
+ z-index: 9; }
+ /* line 139, ../../../../general/res/sass/helpers/_wait-spinner.scss */
+ .loading.tree-item:before {
+ padding: 0.375rem;
+ border-width: 2px; }
+
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@@ -1239,7 +1328,7 @@ mct-container {
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 25, ../../../../general/res/sass/controls/_buttons.scss */
-.s-btn, .s-menu {
+.s-btn, .s-menu-btn {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
@@ -1254,23 +1343,23 @@ mct-container {
padding: 0 7.5px;
font-size: 0.7rem; }
/* line 35, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn .icon, .s-menu .icon {
+ .s-btn .icon, .s-menu-btn .icon {
font-size: 0.8rem;
color: #0099cc; }
/* line 40, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn .title-label, .s-menu .title-label {
+ .s-btn .title-label, .s-menu-btn .title-label {
vertical-align: top; }
/* line 44, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.lg, .lg.s-menu {
+ .s-btn.lg, .lg.s-menu-btn {
font-size: 1rem; }
/* line 48, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.sm, .sm.s-menu {
+ .s-btn.sm, .sm.s-menu-btn {
padding: 0 5px; }
/* line 52, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.vsm, .vsm.s-menu {
+ .s-btn.vsm, .vsm.s-menu-btn {
padding: 0 2.5px; }
/* line 56, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.major, .major.s-menu {
+ .s-btn.major, .major.s-menu-btn {
background-color: #0099cc;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
@@ -1290,17 +1379,17 @@ mct-container {
transition: background, 0.25s;
text-shadow: none; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
- .s-btn.major .icon, .major.s-menu .icon {
+ .s-btn.major .icon, .major.s-menu-btn .icon {
color: #fff; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 294, ../../../../general/res/sass/_mixins.scss */
- .s-btn.major:not(.disabled):hover, .major.s-menu:not(.disabled):hover {
+ .s-btn.major:not(.disabled):hover, .major.s-menu-btn:not(.disabled):hover {
background: deepskyblue; }
/* line 296, ../../../../general/res/sass/_mixins.scss */
- .s-btn.major:not(.disabled):hover > .icon, .major.s-menu:not(.disabled):hover > .icon {
+ .s-btn.major:not(.disabled):hover > .icon, .major.s-menu-btn:not(.disabled):hover > .icon {
color: white; } }
/* line 62, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn:not(.major), .s-menu:not(.major) {
+ .s-btn:not(.major), .s-menu-btn:not(.major) {
background-color: #969696;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
@@ -1320,20 +1409,20 @@ mct-container {
transition: background, 0.25s;
text-shadow: none; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
- .s-btn:not(.major) .icon, .s-menu:not(.major) .icon {
+ .s-btn:not(.major) .icon, .s-menu-btn:not(.major) .icon {
color: #eee; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 294, ../../../../general/res/sass/_mixins.scss */
- .s-btn:not(.major):not(.disabled):hover, .s-menu:not(.major):not(.disabled):hover {
+ .s-btn:not(.major):not(.disabled):hover, .s-menu-btn:not(.major):not(.disabled):hover {
background: #0099cc; }
/* line 296, ../../../../general/res/sass/_mixins.scss */
- .s-btn:not(.major):not(.disabled):hover > .icon, .s-menu:not(.major):not(.disabled):hover > .icon {
+ .s-btn:not(.major):not(.disabled):hover > .icon, .s-menu-btn:not(.major):not(.disabled):hover > .icon {
color: white; } }
/* line 71, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.pause-play .icon:before, .pause-play.s-menu .icon:before {
+ .s-btn.pause-play .icon:before, .pause-play.s-menu-btn .icon:before {
content: "\0000F1"; }
/* line 74, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.pause-play.paused, .pause-play.paused.s-menu {
+ .s-btn.pause-play.paused, .pause-play.paused.s-menu-btn {
background-color: #ff9900;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
@@ -1353,17 +1442,17 @@ mct-container {
transition: background, 0.25s;
text-shadow: none; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
- .s-btn.pause-play.paused .icon, .pause-play.paused.s-menu .icon {
+ .s-btn.pause-play.paused .icon, .pause-play.paused.s-menu-btn .icon {
color: #fff; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 294, ../../../../general/res/sass/_mixins.scss */
- .s-btn.pause-play.paused:not(.disabled):hover, .pause-play.paused.s-menu:not(.disabled):hover {
+ .s-btn.pause-play.paused:not(.disabled):hover, .pause-play.paused.s-menu-btn:not(.disabled):hover {
background: #ffad33; }
/* line 296, ../../../../general/res/sass/_mixins.scss */
- .s-btn.pause-play.paused:not(.disabled):hover > .icon, .pause-play.paused.s-menu:not(.disabled):hover > .icon {
+ .s-btn.pause-play.paused:not(.disabled):hover > .icon, .pause-play.paused.s-menu-btn:not(.disabled):hover > .icon {
color: white; } }
/* line 76, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.pause-play.paused .icon, .pause-play.paused.s-menu .icon {
+ .s-btn.pause-play.paused .icon, .pause-play.paused.s-menu-btn .icon {
-moz-animation-name: pulse;
-webkit-animation-name: pulse;
animation-name: pulse;
@@ -1380,23 +1469,23 @@ mct-container {
-webkit-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out; }
/* line 78, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.pause-play.paused .icon :before, .pause-play.paused.s-menu .icon :before {
+ .s-btn.pause-play.paused .icon :before, .pause-play.paused.s-menu-btn .icon :before {
content: "\0000EF"; }
/* line 86, ../../../../general/res/sass/controls/_buttons.scss */
- .s-btn.show-thumbs .icon:before, .show-thumbs.s-menu .icon:before {
+ .s-btn.show-thumbs .icon:before, .show-thumbs.s-menu-btn .icon:before {
content: "\000039"; }
/* line 92, ../../../../general/res/sass/controls/_buttons.scss */
.l-btn-set {
font-size: 0; }
/* line 98, ../../../../general/res/sass/controls/_buttons.scss */
- .l-btn-set .s-btn, .l-btn-set .s-menu {
+ .l-btn-set .s-btn, .l-btn-set .s-menu-btn {
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
margin-left: 1px; }
/* line 104, ../../../../general/res/sass/controls/_buttons.scss */
- .l-btn-set .first .s-btn, .l-btn-set .first .s-menu {
+ .l-btn-set .first .s-btn, .l-btn-set .first .s-menu-btn {
-moz-border-radius-topleft: 4px;
-webkit-border-top-left-radius: 4px;
border-top-left-radius: 4px;
@@ -1405,7 +1494,7 @@ mct-container {
border-bottom-left-radius: 4px;
margin-left: 0; }
/* line 111, ../../../../general/res/sass/controls/_buttons.scss */
- .l-btn-set .last .s-btn, .l-btn-set .last .s-menu {
+ .l-btn-set .last .s-btn, .l-btn-set .last .s-menu-btn {
-moz-border-radius-topright: 4px;
-webkit-border-top-right-radius: 4px;
border-top-right-radius: 4px;
@@ -1414,7 +1503,7 @@ mct-container {
border-bottom-right-radius: 4px; }
/* line 118, ../../../../general/res/sass/controls/_buttons.scss */
-.paused:not(.s-btn):not(.s-menu) {
+.paused:not(.s-btn):not(.s-menu-btn) {
border-color: #ff9900 !important;
color: #ff9900 !important; }
@@ -1674,7 +1763,7 @@ label.checkbox.custom {
margin-left: 0; }
/* line 180, ../../../../general/res/sass/controls/_controls.scss */
-.s-menu label.checkbox.custom {
+.s-menu-btn label.checkbox.custom {
margin-left: 5px; }
/* line 185, ../../../../general/res/sass/controls/_controls.scss */
@@ -1879,103 +1968,72 @@ label.checkbox.custom {
display: none; }
/******************************************************** SLIDERS */
-/* line 354, ../../../../general/res/sass/controls/_controls.scss */
+/* line 352, ../../../../general/res/sass/controls/_controls.scss */
.slider .slot {
- -moz-border-radius: 2px;
- -webkit-border-radius: 2px;
- border-radius: 2px;
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- -moz-box-shadow: inset rgba(0, 0, 0, 0.7) 0 1px 5px;
- -webkit-box-shadow: inset rgba(0, 0, 0, 0.7) 0 1px 5px;
- box-shadow: inset rgba(0, 0, 0, 0.7) 0 1px 5px;
- background-color: rgba(0, 0, 0, 0.1);
- height: 50%;
width: auto;
position: absolute;
- top: 25%;
+ top: 0;
right: 0;
- bottom: auto;
+ bottom: 0;
left: 0; }
-/* line 365, ../../../../general/res/sass/controls/_controls.scss */
+/* line 362, ../../../../general/res/sass/controls/_controls.scss */
.slider .knob {
- background-color: #969696;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- border-radius: 4px;
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- color: #fff;
- display: inline-block;
- -moz-user-select: -moz-none;
- -ms-user-select: none;
- -webkit-user-select: none;
- user-select: none;
- -moz-transition: background, 0.25s;
- -o-transition: background, 0.25s;
- -webkit-transition: background, 0.25s;
- transition: background, 0.25s;
- text-shadow: none;
- cursor: ew-resize;
+ -moz-transition-property: visibility, opacity, background-color, border-color;
+ -o-transition-property: visibility, opacity, background-color, border-color;
+ -webkit-transition-property: visibility, opacity, background-color, border-color;
+ transition-property: visibility, opacity, background-color, border-color;
+ -moz-transition-duration: 0.25s;
+ -o-transition-duration: 0.25s;
+ -webkit-transition-duration: 0.25s;
+ transition-duration: 0.25s;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ background-color: rgba(0, 153, 204, 0.5);
position: absolute;
height: 100%;
- width: 12px;
+ width: 10px;
top: 0;
auto: 0;
bottom: auto;
left: auto; }
- /* line 289, ../../../../general/res/sass/_mixins.scss */
- .slider .knob .icon {
- color: #eee; }
- /* line 176, ../../../../general/res/sass/_mixins.scss */
- .slider .knob:before {
- -moz-transition-property: "border-color";
- -o-transition-property: "border-color";
- -webkit-transition-property: "border-color";
- transition-property: "border-color";
- -moz-transition-duration: 0.75s;
- -o-transition-duration: 0.75s;
- -webkit-transition-duration: 0.75s;
- transition-duration: 0.75s;
- -moz-transition-timing-function: ease-in-out;
- -o-transition-timing-function: ease-in-out;
- -webkit-transition-timing-function: ease-in-out;
- transition-timing-function: ease-in-out;
- content: '';
- display: block;
- height: auto;
- pointer-events: none;
- position: absolute;
- z-index: 2;
- border-left: 1px solid rgba(0, 0, 0, 0.3);
- left: 2px;
- bottom: 5px;
- top: 5px; }
- /* line 198, ../../../../general/res/sass/_mixins.scss */
- .slider .knob:not(.disabled):hover:before {
- -moz-transition-property: "border-color";
- -o-transition-property: "border-color";
- -webkit-transition-property: "border-color";
- transition-property: "border-color";
- -moz-transition-duration: 25ms;
- -o-transition-duration: 25ms;
- -webkit-transition-duration: 25ms;
- transition-duration: 25ms;
- -moz-transition-timing-function: ease-in-out;
- -o-transition-timing-function: ease-in-out;
- -webkit-transition-timing-function: ease-in-out;
- transition-timing-function: ease-in-out;
- border-color: #fcfcfc; }
- /* line 376, ../../../../general/res/sass/controls/_controls.scss */
- .slider .knob:before {
- top: 1px;
- bottom: 3px;
- left: 5px; }
-/* line 383, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 367, ../../../../general/res/sass/controls/_controls.scss */
+ .slider .knob:hover {
+ background-color: rgba(0, 153, 204, 0.7); }
+/* line 378, ../../../../general/res/sass/controls/_controls.scss */
+.slider .knob-l {
+ -moz-border-radius-topleft: 10px;
+ -webkit-border-top-left-radius: 10px;
+ border-top-left-radius: 10px;
+ -moz-border-radius-bottomleft: 10px;
+ -webkit-border-bottom-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+ cursor: w-resize; }
+/* line 382, ../../../../general/res/sass/controls/_controls.scss */
+.slider .knob-r {
+ -moz-border-radius-topright: 10px;
+ -webkit-border-top-right-radius: 10px;
+ border-top-right-radius: 10px;
+ -moz-border-radius-bottomright: 10px;
+ -webkit-border-bottom-right-radius: 10px;
+ border-bottom-right-radius: 10px;
+ cursor: e-resize; }
+/* line 386, ../../../../general/res/sass/controls/_controls.scss */
.slider .range {
- background: rgba(0, 153, 204, 0.6);
+ -moz-transition-property: visibility, opacity, background-color, border-color;
+ -o-transition-property: visibility, opacity, background-color, border-color;
+ -webkit-transition-property: visibility, opacity, background-color, border-color;
+ transition-property: visibility, opacity, background-color, border-color;
+ -moz-transition-duration: 0.25s;
+ -o-transition-duration: 0.25s;
+ -webkit-transition-duration: 0.25s;
+ transition-duration: 0.25s;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ background-color: rgba(0, 153, 204, 0.2);
cursor: ew-resize;
position: absolute;
top: 0;
@@ -1984,13 +2042,118 @@ label.checkbox.custom {
left: auto;
height: auto;
width: auto; }
- /* line 393, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 397, ../../../../general/res/sass/controls/_controls.scss */
.slider .range:hover {
- background: rgba(0, 153, 204, 0.7); }
+ background-color: rgba(0, 153, 204, 0.4); }
+
+/******************************************************** DATETIME PICKER */
+/* line 404, ../../../../general/res/sass/controls/_controls.scss */
+.l-datetime-picker {
+ -moz-user-select: -moz-none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+ font-size: 0.8rem;
+ padding: 10px !important;
+ width: 230px; }
+ /* line 410, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager {
+ height: 15px;
+ margin-bottom: 5px;
+ position: relative; }
+ /* line 422, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager {
+ width: 20px; }
+ /* line 425, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager.prev {
+ right: auto; }
+ /* line 427, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager.prev:before {
+ content: "\3c"; }
+ /* line 431, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager.next {
+ left: auto;
+ text-align: right; }
+ /* line 434, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .pager.next:before {
+ content: "\3e"; }
+ /* line 439, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-month-year-pager .val {
+ text-align: center;
+ left: 25px;
+ right: 25px; }
+ /* line 445, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-calendar,
+ .l-datetime-picker .l-time-selects {
+ border-top: 1px solid rgba(102, 102, 102, 0.2); }
+ /* line 449, ../../../../general/res/sass/controls/_controls.scss */
+ .l-datetime-picker .l-time-selects {
+ line-height: 22px; }
+
+/******************************************************** CALENDAR */
+/* line 457, ../../../../general/res/sass/controls/_controls.scss */
+.l-calendar ul.l-cal-row {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-flow: row nowrap;
+ flex-flow: row nowrap;
+ margin-top: 1px; }
+ /* line 461, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row:first-child {
+ margin-top: 0; }
+ /* line 464, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row li {
+ -webkit-flex: 1 0;
+ flex: 1 0;
+ margin-left: 1px;
+ padding: 5px;
+ text-align: center; }
+ /* line 470, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row li:first-child {
+ margin-left: 0; }
+ /* line 474, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-header li {
+ color: #999999; }
+ /* line 477, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li {
+ -moz-transition-property: background-color;
+ -o-transition-property: background-color;
+ -webkit-transition-property: background-color;
+ transition-property: background-color;
+ -moz-transition-duration: 0.25s;
+ -o-transition-duration: 0.25s;
+ -webkit-transition-duration: 0.25s;
+ transition-duration: 0.25s;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ cursor: pointer; }
+ /* line 480, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li.in-month {
+ background-color: #f2f2f2; }
+ /* line 483, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li .sub {
+ color: #999999;
+ font-size: 0.8em; }
+ /* line 487, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li.selected {
+ background: #1ac6ff;
+ color: #fcfcfc; }
+ /* line 490, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li.selected .sub {
+ color: inherit; }
+ /* line 494, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li:hover {
+ background-color: #0099cc;
+ color: #fff; }
+ /* line 497, ../../../../general/res/sass/controls/_controls.scss */
+ .l-calendar ul.l-cal-row.l-body li:hover .sub {
+ color: inherit; }
/******************************************************** BROWSER ELEMENTS */
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
- /* line 402, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 508, ../../../../general/res/sass/controls/_controls.scss */
::-webkit-scrollbar {
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
@@ -2005,7 +2168,7 @@ label.checkbox.custom {
height: 10px;
width: 10px; }
- /* line 411, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 517, ../../../../general/res/sass/controls/_controls.scss */
::-webkit-scrollbar-thumb {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzg5ODk4OSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzdkN2Q3ZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
background-size: 100%;
@@ -2019,7 +2182,7 @@ label.checkbox.custom {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box; }
- /* line 420, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 526, ../../../../general/res/sass/controls/_controls.scss */
::-webkit-scrollbar-thumb:hover {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwYWNlNiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzAwOTljYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
background-size: 100%;
@@ -2028,7 +2191,7 @@ label.checkbox.custom {
background-image: -webkit-linear-gradient(#00ace6, #0099cc 20px);
background-image: linear-gradient(#00ace6, #0099cc 20px); }
- /* line 425, ../../../../general/res/sass/controls/_controls.scss */
+ /* line 531, ../../../../general/res/sass/controls/_controls.scss */
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0.1); } }
/*****************************************************************************
@@ -2087,13 +2250,13 @@ label.checkbox.custom {
*****************************************************************************/
/******************************************************** MENU BUTTONS */
/* line 31, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu .icon {
+.s-menu-btn .icon {
font-size: 120%; }
/* line 35, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu .title-label {
+.s-menu-btn .title-label {
margin-left: 3px; }
/* line 39, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu:after {
+.s-menu-btn:after {
text-shadow: none;
content: '\76';
display: inline-block;
@@ -2102,14 +2265,14 @@ label.checkbox.custom {
vertical-align: top;
color: rgba(255, 255, 255, 0.4); }
/* line 46, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu.create-btn .title-label {
+.s-menu-btn.create-btn .title-label {
font-size: 1rem; }
/* line 54, ../../../../general/res/sass/controls/_menus.scss */
-.s-menu .menu {
+.s-menu-btn .menu {
left: 0;
text-align: left; }
/* line 57, ../../../../general/res/sass/controls/_menus.scss */
- .s-menu .menu .ui-symbol.icon {
+ .s-menu-btn .menu .ui-symbol.icon, .s-menu-btn .menu .l-datetime-picker .l-month-year-pager .icon.pager, .l-datetime-picker .l-month-year-pager .s-menu-btn .menu .icon.pager {
width: 12px; }
/******************************************************** MENUS THEMSELVES */
@@ -2117,192 +2280,211 @@ label.checkbox.custom {
.menu-element {
cursor: pointer;
position: relative; }
- /* line 70, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu {
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- border-radius: 4px;
- background-color: white;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- border-radius: 4px;
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- color: #4d4d4d;
- display: inline-block;
- -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 5px;
- -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 5px;
- box-shadow: rgba(0, 0, 0, 0.5) 0 1px 5px;
- text-shadow: none;
- display: block;
- padding: 3px 0;
- position: absolute;
- z-index: 10; }
- /* line 79, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul {
+
+/* line 69, ../../../../general/res/sass/controls/_menus.scss */
+.s-menu, .menu {
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ background-color: white;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ color: #4d4d4d;
+ display: inline-block;
+ -moz-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 5px;
+ -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 1px 5px;
+ box-shadow: rgba(0, 0, 0, 0.5) 0 1px 5px;
+ text-shadow: none;
+ padding: 3px 0; }
+
+/* line 77, ../../../../general/res/sass/controls/_menus.scss */
+.menu {
+ display: block;
+ position: absolute;
+ z-index: 10; }
+ /* line 82, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul {
+ margin: 0;
+ padding: 0; }
+ /* line 346, ../../../../general/res/sass/_mixins.scss */
+ .menu ul li {
+ list-style-type: none;
margin: 0;
padding: 0; }
- /* line 346, ../../../../general/res/sass/_mixins.scss */
- .menu-element .menu ul li {
- list-style-type: none;
- margin: 0;
- padding: 0; }
- /* line 81, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li {
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
- border-top: 1px solid white;
- color: #666666;
- line-height: 1.5rem;
- padding: 3px 10px 3px 30px;
- position: relative;
- white-space: nowrap; }
- /* line 89, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li:first-child {
- border: none; }
- /* line 92, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li:hover {
- background: #e6e6e6;
- color: #4d4d4d; }
- /* line 95, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li:hover .icon {
- color: #0099cc; }
- /* line 99, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li .type-icon {
- left: 10px; }
- /* line 106, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu,
- .menu-element .context-menu,
- .menu-element .checkbox-menu,
- .menu-element .super-menu {
- pointer-events: auto; }
- /* line 112, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li a,
- .menu-element .context-menu ul li a,
- .menu-element .checkbox-menu ul li a,
- .menu-element .super-menu ul li a {
- color: #4d4d4d; }
- /* line 115, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li .icon,
- .menu-element .context-menu ul li .icon,
- .menu-element .checkbox-menu ul li .icon,
- .menu-element .super-menu ul li .icon {
- color: #0099cc; }
- /* line 118, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .menu ul li .type-icon,
- .menu-element .context-menu ul li .type-icon,
- .menu-element .checkbox-menu ul li .type-icon,
- .menu-element .super-menu ul li .type-icon {
- left: 5px; }
- /* line 130, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li {
- padding-left: 50px; }
- /* line 132, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li .checkbox {
- position: absolute;
- left: 5px;
- top: 0.53333rem; }
- /* line 137, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li .checkbox em {
- height: 0.7rem;
- width: 0.7rem; }
- /* line 140, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li .checkbox em:before {
- font-size: 7px !important;
- height: 0.7rem;
- width: 0.7rem;
- line-height: 0.7rem; }
- /* line 148, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .checkbox-menu ul li .type-icon {
- left: 25px; }
- /* line 154, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu {
- display: block;
- width: 500px;
- height: 480px; }
- /* line 162, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .contents {
- overflow: hidden;
- position: absolute;
- top: 5px;
- right: 5px;
- bottom: 5px;
- left: 5px;
- width: auto;
- height: auto; }
- /* line 165, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .pane {
+ /* line 84, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
- box-sizing: border-box; }
- /* line 167, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .pane.left {
- border-right: 1px solid #e6e6e6;
- left: 0;
- padding-right: 5px;
- right: auto;
- width: 50%;
- overflow-x: hidden;
- overflow-y: auto; }
- /* line 177, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .pane.left ul li {
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- border-radius: 4px;
- padding-left: 30px;
- border-top: none; }
- /* line 184, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .pane.right {
- left: auto;
- right: 0;
- padding: 25px;
- width: 50%; }
- /* line 194, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .menu-item-description .desc-area.icon {
- color: #0099cc;
+ box-sizing: border-box;
+ border-top: 1px solid white;
+ color: #666666;
+ line-height: 1.5rem;
+ padding: 3px 10px 3px 30px;
position: relative;
- font-size: 8em;
+ white-space: nowrap; }
+ /* line 92, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li:first-child {
+ border: none; }
+ /* line 95, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li:hover {
+ background: #e6e6e6;
+ color: #4d4d4d; }
+ /* line 98, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li:hover .icon {
+ color: #0099cc; }
+ /* line 102, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li .type-icon {
+ left: 10px; }
+
+/* line 109, ../../../../general/res/sass/controls/_menus.scss */
+.menu,
+.context-menu,
+.checkbox-menu,
+.super-menu {
+ pointer-events: auto; }
+ /* line 115, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li a,
+ .context-menu ul li a,
+ .checkbox-menu ul li a,
+ .super-menu ul li a {
+ color: #4d4d4d; }
+ /* line 118, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li .icon,
+ .context-menu ul li .icon,
+ .checkbox-menu ul li .icon,
+ .super-menu ul li .icon {
+ color: #0099cc; }
+ /* line 121, ../../../../general/res/sass/controls/_menus.scss */
+ .menu ul li .type-icon,
+ .context-menu ul li .type-icon,
+ .checkbox-menu ul li .type-icon,
+ .super-menu ul li .type-icon {
+ left: 5px; }
+
+/* line 133, ../../../../general/res/sass/controls/_menus.scss */
+.checkbox-menu ul li {
+ padding-left: 50px; }
+ /* line 135, ../../../../general/res/sass/controls/_menus.scss */
+ .checkbox-menu ul li .checkbox {
+ position: absolute;
+ left: 5px;
+ top: 0.53333rem; }
+ /* line 140, ../../../../general/res/sass/controls/_menus.scss */
+ .checkbox-menu ul li .checkbox em {
+ height: 0.7rem;
+ width: 0.7rem; }
+ /* line 143, ../../../../general/res/sass/controls/_menus.scss */
+ .checkbox-menu ul li .checkbox em:before {
+ font-size: 7px !important;
+ height: 0.7rem;
+ width: 0.7rem;
+ line-height: 0.7rem; }
+ /* line 151, ../../../../general/res/sass/controls/_menus.scss */
+ .checkbox-menu ul li .type-icon {
+ left: 25px; }
+
+/* line 157, ../../../../general/res/sass/controls/_menus.scss */
+.super-menu {
+ display: block;
+ width: 500px;
+ height: 480px; }
+ /* line 165, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .contents {
+ overflow: hidden;
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ bottom: 5px;
+ left: 5px;
+ width: auto;
+ height: auto; }
+ /* line 168, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .pane {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box; }
+ /* line 170, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .pane.left {
+ border-right: 1px solid #e6e6e6;
left: 0;
- height: 150px;
- line-height: 150px;
- margin-bottom: 25px;
- text-align: center; }
- /* line 205, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .menu-item-description .desc-area.title {
- color: #666;
- font-size: 1.2em;
- margin-bottom: 0.5em; }
- /* line 210, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .super-menu .menu-item-description .desc-area.description {
- color: #666;
- font-size: 0.8em;
- line-height: 1.5em; }
- /* line 219, ../../../../general/res/sass/controls/_menus.scss */
- .menu-element .context-menu, .menu-element .checkbox-menu {
- font-size: 0.80rem; }
+ padding-right: 5px;
+ right: auto;
+ width: 50%;
+ overflow-x: hidden;
+ overflow-y: auto; }
+ /* line 180, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .pane.left ul li {
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ padding-left: 30px;
+ border-top: none; }
+ /* line 187, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .pane.right {
+ left: auto;
+ right: 0;
+ padding: 25px;
+ width: 50%; }
+ /* line 197, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .menu-item-description .desc-area.icon {
+ color: #0099cc;
+ position: relative;
+ font-size: 8em;
+ left: 0;
+ height: 150px;
+ line-height: 150px;
+ margin-bottom: 25px;
+ text-align: center; }
+ /* line 208, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .menu-item-description .desc-area.title {
+ color: #666;
+ font-size: 1.2em;
+ margin-bottom: 0.5em; }
+ /* line 213, ../../../../general/res/sass/controls/_menus.scss */
+ .super-menu .menu-item-description .desc-area.description {
+ color: #666;
+ font-size: 0.8em;
+ line-height: 1.5em; }
-/* line 224, ../../../../general/res/sass/controls/_menus.scss */
-.context-menu-holder {
- pointer-events: none;
+/* line 222, ../../../../general/res/sass/controls/_menus.scss */
+.context-menu, .checkbox-menu {
+ font-size: 0.80rem; }
+
+/* line 226, ../../../../general/res/sass/controls/_menus.scss */
+.context-menu-holder,
+.menu-holder {
position: absolute;
- height: 200px;
- width: 170px;
z-index: 70; }
/* line 230, ../../../../general/res/sass/controls/_menus.scss */
- .context-menu-holder .context-menu-wrapper {
+ .context-menu-holder .context-menu-wrapper,
+ .menu-holder .context-menu-wrapper {
position: absolute;
height: 100%;
width: 100%; }
- /* line 237, ../../../../general/res/sass/controls/_menus.scss */
- .context-menu-holder.go-left .context-menu, .context-menu-holder.go-left .menu-element .checkbox-menu, .menu-element .context-menu-holder.go-left .checkbox-menu {
+ /* line 235, ../../../../general/res/sass/controls/_menus.scss */
+ .context-menu-holder.go-left .context-menu, .context-menu-holder.go-left .checkbox-menu, .context-menu-holder.go-left .menu,
+ .menu-holder.go-left .context-menu,
+ .menu-holder.go-left .checkbox-menu,
+ .menu-holder.go-left .menu {
right: 0; }
- /* line 240, ../../../../general/res/sass/controls/_menus.scss */
- .context-menu-holder.go-up .context-menu, .context-menu-holder.go-up .menu-element .checkbox-menu, .menu-element .context-menu-holder.go-up .checkbox-menu {
+ /* line 239, ../../../../general/res/sass/controls/_menus.scss */
+ .context-menu-holder.go-up .context-menu, .context-menu-holder.go-up .checkbox-menu, .context-menu-holder.go-up .menu,
+ .menu-holder.go-up .context-menu,
+ .menu-holder.go-up .checkbox-menu,
+ .menu-holder.go-up .menu {
bottom: 0; }
/* line 245, ../../../../general/res/sass/controls/_menus.scss */
+.context-menu-holder {
+ pointer-events: none;
+ height: 200px;
+ width: 170px; }
+
+/* line 251, ../../../../general/res/sass/controls/_menus.scss */
.btn-bar.right .menu,
.menus-to-left .menu {
left: auto;
@@ -2403,8 +2585,8 @@ label.checkbox.custom {
padding: 10px; }
/* line 94, ../../../../general/res/sass/controls/_messages.scss */
.message.error {
- background-color: rgba(255, 83, 58, 0.3);
- color: #ffaca0; }
+ background-color: rgba(255, 60, 0, 0.3);
+ color: #ff8a66; }
/* line 100, ../../../../general/res/sass/controls/_messages.scss */
.l-message-banner {
@@ -2722,24 +2904,25 @@ label.checkbox.custom {
.t-message-list .message-contents .l-message {
margin-right: 10px; } }
-/* line 1, ../../../../general/res/sass/controls/_time-controller.scss */
-.l-time-controller {
- position: relative;
- margin: 10px 0;
- min-width: 400px; }
- /* line 12, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-inputs-holder,
- .l-time-controller .l-time-range-slider {
- font-size: 0.8em; }
- /* line 17, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-inputs-holder,
- .l-time-controller .l-time-range-slider-holder,
- .l-time-controller .l-time-range-ticks-holder {
- margin-bottom: 5px;
- position: relative; }
- /* line 24, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-slider,
- .l-time-controller .l-time-range-ticks {
+/* line 13, ../../../../general/res/sass/controls/_time-controller.scss */
+mct-include.l-time-controller {
+ overflow: hidden;
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ left: 0px;
+ width: auto;
+ height: auto;
+ display: block;
+ top: auto;
+ height: 83px;
+ min-width: 500px;
+ font-size: 0.8rem; }
+ /* line 38, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder,
+ mct-include.l-time-controller .l-time-range-slider-holder,
+ mct-include.l-time-controller .l-time-range-ticks-holder {
overflow: visible;
position: absolute;
top: 0;
@@ -2747,77 +2930,196 @@ label.checkbox.custom {
bottom: 0;
left: 0;
width: auto;
- height: auto; }
- /* line 30, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-inputs-holder {
- height: 20px; }
- /* line 34, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-slider,
- .l-time-controller .l-time-range-ticks {
- left: 90px;
- right: 90px; }
- /* line 40, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-slider-holder {
- height: 30px; }
- /* line 42, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-slider-holder .range-holder {
+ height: auto;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ top: auto; }
+ /* line 47, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider,
+ mct-include.l-time-controller .l-time-range-ticks {
+ overflow: visible;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ width: auto;
+ height: auto;
+ left: 150px;
+ right: 150px; }
+ /* line 54, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder {
+ height: 33px;
+ bottom: 46px;
+ padding-top: 5px;
+ border-top: 1px solid rgba(102, 102, 102, 0.2); }
+ /* line 59, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder .type-icon {
+ font-size: 120%;
+ vertical-align: middle; }
+ /* line 63, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input,
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem {
+ margin-right: 5px; }
+ /* line 66, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input .lbl,
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem .lbl {
+ color: #999999; }
+ /* line 69, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input .ui-symbol.icon, mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input .l-datetime-picker .l-month-year-pager .icon.pager, .l-datetime-picker .l-month-year-pager mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-input .icon.pager,
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem .ui-symbol.icon,
+ mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem .l-datetime-picker .l-month-year-pager .icon.pager,
+ .l-datetime-picker .l-month-year-pager mct-include.l-time-controller .l-time-range-inputs-holder .l-time-range-inputs-elem .icon.pager {
+ font-size: 11px;
+ width: 11px; }
+ /* line 76, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder {
+ height: 20px;
+ bottom: 23px; }
+ /* line 79, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder {
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
background: none;
- border: none;
- height: 75%; }
- /* line 50, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder {
- height: 10px; }
- /* line 52, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder .l-time-range-ticks {
- border-top: 1px solid rgba(102, 102, 102, 0.2); }
- /* line 54, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick {
- background-color: rgba(102, 102, 102, 0.2);
+ border: none; }
+ /* line 84, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line {
+ -moz-transform: translateX(50%);
+ -ms-transform: translateX(50%);
+ -webkit-transform: translateX(50%);
+ transform: translateX(50%);
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0px;
+ left: auto;
+ width: 8px;
+ height: auto;
+ z-index: 2; }
+ /* line 94, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line:before, mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line:after {
+ background-color: #666;
+ content: "";
+ position: absolute; }
+ /* line 100, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line:before {
+ top: 0;
+ right: auto;
+ bottom: -10px;
+ left: 3px;
+ width: 2px; }
+ /* line 106, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range .toi-line:after {
+ -moz-border-radius: 8px;
+ -webkit-border-radius: 8px;
+ border-radius: 8px;
+ -moz-transform: translateY(-50%);
+ -ms-transform: translateY(-50%);
+ -webkit-transform: translateY(-50%);
+ transform: translateY(-50%);
+ top: 50%;
+ right: 0;
+ bottom: auto;
+ left: 0;
+ width: auto;
+ height: 8px; }
+ /* line 3, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range:hover .toi-line:before, mct-include.l-time-controller .l-time-range-slider-holder .range-holder .range:hover .toi-line:after {
+ background-color: #0052b5; }
+ /* line 122, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-slider-holder:not(:active) .knob,
+ mct-include.l-time-controller .l-time-range-slider-holder:not(:active) .range {
+ -moz-transition-property: left, right;
+ -o-transition-property: left, right;
+ -webkit-transition-property: left, right;
+ transition-property: left, right;
+ -moz-transition-duration: 500ms;
+ -o-transition-duration: 500ms;
+ -webkit-transition-duration: 500ms;
+ transition-duration: 500ms;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out; }
+ /* line 131, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder {
+ height: 20px; }
+ /* line 133, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder .l-time-range-ticks {
+ border-top: 1px solid rgba(0, 0, 0, 0.2); }
+ /* line 135, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick {
+ background-color: rgba(0, 0, 0, 0.2);
border: none;
+ height: 5px;
width: 1px;
- margin-left: -1px; }
- /* line 59, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick:first-child {
+ margin-left: -1px;
+ position: absolute; }
+ /* line 142, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick:first-child {
margin-left: 0; }
- /* line 62, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick .l-time-range-tick-label {
- color: rgba(153, 153, 153, 0.2);
- font-size: 0.7em;
+ /* line 145, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .l-time-range-ticks-holder .l-time-range-ticks .tick .l-time-range-tick-label {
+ transform: translateX(-50%);
+ -webkit-transform: translateX(-50%);
+ color: #999999;
+ display: inline-block;
+ font-size: 0.9em;
position: absolute;
- margin-left: -25px;
- text-align: center;
- top: 10px;
- width: 50px;
+ top: 8px;
+ white-space: nowrap;
z-index: 2; }
- /* line 76, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob {
- width: 9px; }
- /* line 78, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob .range-value {
+ /* line 159, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob {
+ z-index: 2; }
+ /* line 161, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob .range-value {
+ -moz-transition-property: visibility, opacity, background-color, border-color;
+ -o-transition-property: visibility, opacity, background-color, border-color;
+ -webkit-transition-property: visibility, opacity, background-color, border-color;
+ transition-property: visibility, opacity, background-color, border-color;
+ -moz-transition-duration: 0.25s;
+ -o-transition-duration: 0.25s;
+ -webkit-transition-duration: 0.25s;
+ transition-duration: 0.25s;
+ -moz-transition-timing-function: ease-in-out;
+ -o-transition-timing-function: ease-in-out;
+ -webkit-transition-timing-function: ease-in-out;
+ transition-timing-function: ease-in-out;
+ padding: 0 10px;
position: absolute;
- top: 50%;
- margin-top: -7px;
- white-space: nowrap;
- width: 75px; }
- /* line 87, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob:hover .range-value {
- color: #0099cc; }
- /* line 90, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob.knob-l {
- margin-left: -4.5px; }
- /* line 92, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob.knob-l .range-value {
+ height: 20px;
+ line-height: 20px;
+ white-space: nowrap; }
+ /* line 170, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob:hover .range-value {
+ color: rgba(0, 153, 204, 0.7); }
+ /* line 173, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-l {
+ margin-left: -10px; }
+ /* line 176, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-l .range-value {
text-align: right;
- right: 14px; }
- /* line 97, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob.knob-r {
- margin-right: -4.5px; }
- /* line 99, ../../../../general/res/sass/controls/_time-controller.scss */
- .l-time-controller .knob.knob-r .range-value {
- left: 14px; }
+ right: 10px; }
+ /* line 181, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-r {
+ margin-right: -10px; }
+ /* line 184, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-r .range-value {
+ left: 10px; }
+ /* line 3, ../../../../general/res/sass/controls/_time-controller.scss */
+ mct-include.l-time-controller .knob.knob-r:hover + .range-holder .range .toi-line:before, mct-include.l-time-controller .knob.knob-r:hover + .range-holder .range .toi-line:after {
+ background-color: #0052b5; }
+
+/* line 198, ../../../../general/res/sass/controls/_time-controller.scss */
+.s-time-range-val {
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ background-color: #fff;
+ padding: 1px 1px 0 5px; }
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
@@ -3103,14 +3405,14 @@ textarea {
-webkit-transition: background, 0.25s;
transition: background, 0.25s;
text-shadow: none;
- margin: 0 0 2px 2px;
padding: 0 5px;
overflow: hidden;
- position: relative; }
+ position: relative;
+ line-height: 22px; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
.select .icon {
color: #eee; }
- /* line 28, ../../../../general/res/sass/forms/_selects.scss */
+ /* line 31, ../../../../general/res/sass/forms/_selects.scss */
.select select {
-moz-appearance: none;
-webkit-appearance: none;
@@ -3123,10 +3425,10 @@ textarea {
border: none !important;
padding: 4px 25px 2px 0px;
width: 120%; }
- /* line 37, ../../../../general/res/sass/forms/_selects.scss */
+ /* line 40, ../../../../general/res/sass/forms/_selects.scss */
.select select option {
margin: 5px 0; }
- /* line 41, ../../../../general/res/sass/forms/_selects.scss */
+ /* line 44, ../../../../general/res/sass/forms/_selects.scss */
.select:after {
text-shadow: none;
content: '\76';
@@ -3134,6 +3436,7 @@ textarea {
font-family: 'symbolsfont';
margin-left: 3px;
vertical-align: top;
+ pointer-events: none;
color: rgba(102, 102, 102, 0.4);
position: absolute;
right: 5px;
@@ -3195,7 +3498,7 @@ textarea {
.channel-selector .btns-add-remove {
margin-top: 150px; }
/* line 39, ../../../../general/res/sass/forms/_channel-selector.scss */
- .channel-selector .btns-add-remove .s-btn, .channel-selector .btns-add-remove .s-menu {
+ .channel-selector .btns-add-remove .s-btn, .channel-selector .btns-add-remove .s-menu-btn {
display: block;
margin-bottom: 5px;
text-align: center; }
@@ -3221,26 +3524,44 @@ textarea {
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-/* line 23, ../../../../general/res/sass/forms/_datetime.scss */
-.complex.datetime span {
- display: inline-block;
- margin-right: 5px; }
-/* line 36, ../../../../general/res/sass/forms/_datetime.scss */
-.complex.datetime .fields {
- margin-top: 3px 0;
- padding: 3px 0; }
-/* line 41, ../../../../general/res/sass/forms/_datetime.scss */
-.complex.datetime .date {
- width: 85px; }
- /* line 44, ../../../../general/res/sass/forms/_datetime.scss */
- .complex.datetime .date input {
- width: 80px; }
-/* line 50, ../../../../general/res/sass/forms/_datetime.scss */
-.complex.datetime .time.sm {
- width: 45px; }
- /* line 53, ../../../../general/res/sass/forms/_datetime.scss */
- .complex.datetime .time.sm input {
- width: 40px; }
+/* line 29, ../../../../general/res/sass/forms/_datetime.scss */
+.complex.datetime {
+ /*
+ .field-hints,
+ .fields {
+ }
+
+
+ .field-hints {
+
+ }
+ */ }
+ /* line 30, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime span {
+ display: inline-block;
+ margin-right: 5px; }
+ /* line 46, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .fields {
+ margin-top: 3px 0;
+ padding: 3px 0; }
+ /* line 51, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .date {
+ width: 85px; }
+ /* line 24, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .date input[type="text"] {
+ width: 80px; }
+ /* line 55, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .time.md {
+ width: 65px; }
+ /* line 24, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .time.md input[type="text"] {
+ width: 60px; }
+ /* line 59, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .time.sm {
+ width: 45px; }
+ /* line 24, ../../../../general/res/sass/forms/_datetime.scss */
+ .complex.datetime .time.sm input[type="text"] {
+ width: 40px; }
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
@@ -3352,8 +3673,10 @@ span.req {
.t-filter input.t-filter-input:not(.ng-dirty) + .t-a-clear {
display: none; }
/* line 42, ../../../../general/res/sass/forms/_filter.scss */
-.filter .icon.ui-symbol,
-.t-filter .icon.ui-symbol {
+.filter .icon.ui-symbol, .filter .l-datetime-picker .l-month-year-pager .icon.pager, .l-datetime-picker .l-month-year-pager .filter .icon.pager,
+.t-filter .icon.ui-symbol,
+.t-filter .l-datetime-picker .l-month-year-pager .icon.pager,
+.l-datetime-picker .l-month-year-pager .t-filter .icon.pager {
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
@@ -3364,12 +3687,16 @@ span.req {
padding: 0px 5px;
vertical-align: middle; }
/* line 50, ../../../../general/res/sass/forms/_filter.scss */
- .filter .icon.ui-symbol:hover,
- .t-filter .icon.ui-symbol:hover {
+ .filter .icon.ui-symbol:hover, .filter .l-datetime-picker .l-month-year-pager .icon.pager:hover, .l-datetime-picker .l-month-year-pager .filter .icon.pager:hover,
+ .t-filter .icon.ui-symbol:hover,
+ .t-filter .l-datetime-picker .l-month-year-pager .icon.pager:hover,
+ .l-datetime-picker .l-month-year-pager .t-filter .icon.pager:hover {
background: rgba(255, 255, 255, 0.1); }
/* line 54, ../../../../general/res/sass/forms/_filter.scss */
-.filter .s-a-clear.ui-symbol,
-.t-filter .s-a-clear.ui-symbol {
+.filter .s-a-clear.ui-symbol, .filter .l-datetime-picker .l-month-year-pager .s-a-clear.pager, .l-datetime-picker .l-month-year-pager .filter .s-a-clear.pager,
+.t-filter .s-a-clear.ui-symbol,
+.t-filter .l-datetime-picker .l-month-year-pager .s-a-clear.pager,
+.l-datetime-picker .l-month-year-pager .t-filter .s-a-clear.pager {
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
@@ -3393,8 +3720,10 @@ span.req {
text-align: center;
z-index: 5; }
/* line 74, ../../../../general/res/sass/forms/_filter.scss */
- .filter .s-a-clear.ui-symbol:hover,
- .t-filter .s-a-clear.ui-symbol:hover {
+ .filter .s-a-clear.ui-symbol:hover, .filter .l-datetime-picker .l-month-year-pager .s-a-clear.pager:hover, .l-datetime-picker .l-month-year-pager .filter .s-a-clear.pager:hover,
+ .t-filter .s-a-clear.ui-symbol:hover,
+ .t-filter .l-datetime-picker .l-month-year-pager .s-a-clear.pager:hover,
+ .l-datetime-picker .l-month-year-pager .t-filter .s-a-clear.pager:hover {
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60);
opacity: 0.6;
background-color: #0099cc; }
@@ -3470,33 +3799,49 @@ span.req {
.bar .icon.major {
margin-right: 5px; }
/* line 70, ../../../../general/res/sass/user-environ/_layout.scss */
-.bar.abs, .s-menu span.bar.l-click-area {
+.bar.abs, .l-datetime-picker .l-month-year-pager .bar.pager,
+.l-datetime-picker .l-month-year-pager .bar.val, .s-menu-btn span.bar.l-click-area {
text-wrap: none;
white-space: nowrap; }
/* line 73, ../../../../general/res/sass/user-environ/_layout.scss */
- .bar.abs.left, .s-menu span.bar.left.l-click-area,
+ .bar.abs.left, .l-datetime-picker .l-month-year-pager .bar.left.pager,
+ .l-datetime-picker .l-month-year-pager .bar.left.val, .s-menu-btn span.bar.left.l-click-area,
.bar.abs .left,
- .s-menu span.bar.l-click-area .left {
+ .l-datetime-picker .l-month-year-pager .bar.pager .left,
+ .l-datetime-picker .l-month-year-pager .bar.val .left,
+ .s-menu-btn span.bar.l-click-area .left {
width: 45%;
right: auto; }
/* line 78, ../../../../general/res/sass/user-environ/_layout.scss */
- .bar.abs.right, .s-menu span.bar.right.l-click-area,
+ .bar.abs.right, .l-datetime-picker .l-month-year-pager .bar.right.pager,
+ .l-datetime-picker .l-month-year-pager .bar.right.val, .s-menu-btn span.bar.right.l-click-area,
.bar.abs .right,
- .s-menu span.bar.l-click-area .right {
+ .l-datetime-picker .l-month-year-pager .bar.pager .right,
+ .l-datetime-picker .l-month-year-pager .bar.val .right,
+ .s-menu-btn span.bar.l-click-area .right {
width: 45%;
left: auto;
text-align: right; }
/* line 83, ../../../../general/res/sass/user-environ/_layout.scss */
- .bar.abs.right .icon.major, .s-menu span.bar.right.l-click-area .icon.major,
+ .bar.abs.right .icon.major, .l-datetime-picker .l-month-year-pager .bar.right.pager .icon.major,
+ .l-datetime-picker .l-month-year-pager .bar.right.val .icon.major, .s-menu-btn span.bar.right.l-click-area .icon.major,
.bar.abs .right .icon.major,
- .s-menu span.bar.l-click-area .right .icon.major {
+ .l-datetime-picker .l-month-year-pager .bar.pager .right .icon.major,
+ .l-datetime-picker .l-month-year-pager .bar.val .right .icon.major,
+ .s-menu-btn span.bar.l-click-area .right .icon.major {
margin-left: 15px; }
/* line 89, ../../../../general/res/sass/user-environ/_layout.scss */
- .bar.abs .l-flex .left, .s-menu span.bar.l-click-area .l-flex .left,
+ .bar.abs .l-flex .left, .l-datetime-picker .l-month-year-pager .bar.pager .l-flex .left,
+ .l-datetime-picker .l-month-year-pager .bar.val .l-flex .left, .s-menu-btn span.bar.l-click-area .l-flex .left,
.bar.abs .l-flex .right,
- .s-menu span.bar.l-click-area .l-flex .right, .bar.abs.l-flex .left, .s-menu span.bar.l-flex.l-click-area .left,
+ .l-datetime-picker .l-month-year-pager .bar.pager .l-flex .right,
+ .l-datetime-picker .l-month-year-pager .bar.val .l-flex .right,
+ .s-menu-btn span.bar.l-click-area .l-flex .right, .bar.abs.l-flex .left, .l-datetime-picker .l-month-year-pager .bar.l-flex.pager .left,
+ .l-datetime-picker .l-month-year-pager .bar.l-flex.val .left, .s-menu-btn span.bar.l-flex.l-click-area .left,
.bar.abs.l-flex .right,
- .s-menu span.bar.l-flex.l-click-area .right {
+ .l-datetime-picker .l-month-year-pager .bar.l-flex.pager .right,
+ .l-datetime-picker .l-month-year-pager .bar.l-flex.val .right,
+ .s-menu-btn span.bar.l-flex.l-click-area .right {
width: auto; }
/* line 98, ../../../../general/res/sass/user-environ/_layout.scss */
@@ -3669,10 +4014,16 @@ span.req {
overflow: auto;
top: 64px; }
/* line 267, ../../../../general/res/sass/user-environ/_layout.scss */
- .pane.items .object-browse-bar .left.abs, .pane.items .object-browse-bar .s-menu span.left.l-click-area, .s-menu .pane.items .object-browse-bar span.left.l-click-area,
+ .pane.items .object-browse-bar .left.abs, .pane.items .object-browse-bar .l-datetime-picker .l-month-year-pager .left.pager, .l-datetime-picker .l-month-year-pager .pane.items .object-browse-bar .left.pager,
+ .pane.items .object-browse-bar .l-datetime-picker .l-month-year-pager .left.val,
+ .l-datetime-picker .l-month-year-pager .pane.items .object-browse-bar .left.val, .pane.items .object-browse-bar .s-menu-btn span.left.l-click-area, .s-menu-btn .pane.items .object-browse-bar span.left.l-click-area,
.pane.items .object-browse-bar .right.abs,
- .pane.items .object-browse-bar .s-menu span.right.l-click-area,
- .s-menu .pane.items .object-browse-bar span.right.l-click-area {
+ .pane.items .object-browse-bar .l-datetime-picker .l-month-year-pager .right.pager,
+ .l-datetime-picker .l-month-year-pager .pane.items .object-browse-bar .right.pager,
+ .pane.items .object-browse-bar .l-datetime-picker .l-month-year-pager .right.val,
+ .l-datetime-picker .l-month-year-pager .pane.items .object-browse-bar .right.val,
+ .pane.items .object-browse-bar .s-menu-btn span.right.l-click-area,
+ .s-menu-btn .pane.items .object-browse-bar span.right.l-click-area {
top: auto; }
/* line 278, ../../../../general/res/sass/user-environ/_layout.scss */
.pane.items .object-holder {
@@ -3702,13 +4053,15 @@ span.req {
right: 3px; }
/* line 318, ../../../../general/res/sass/user-environ/_layout.scss */
-.object-browse-bar .s-btn, .object-browse-bar .s-menu,
+.object-browse-bar .s-btn, .object-browse-bar .s-menu-btn,
.top-bar .buttons-main .s-btn,
-.top-bar .buttons-main .s-menu,
+.top-bar .buttons-main .s-menu-btn,
.top-bar .s-menu,
+.top-bar .menu,
.tool-bar .s-btn,
+.tool-bar .s-menu-btn,
.tool-bar .s-menu,
-.tool-bar .s-menu {
+.tool-bar .menu {
height: 25px;
line-height: 25px;
vertical-align: top; }
@@ -4104,13 +4457,15 @@ span.req {
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* line 23, ../../../../general/res/sass/search/_search.scss */
-.abs.search-holder, .s-menu span.search-holder.l-click-area {
+.abs.search-holder, .l-datetime-picker .l-month-year-pager .search-holder.pager,
+.l-datetime-picker .l-month-year-pager .search-holder.val, .s-menu-btn span.search-holder.l-click-area {
height: 25px;
bottom: 0;
top: 23px;
z-index: 5; }
/* line 27, ../../../../general/res/sass/search/_search.scss */
- .abs.search-holder.active, .s-menu span.search-holder.active.l-click-area {
+ .abs.search-holder.active, .l-datetime-picker .l-month-year-pager .search-holder.active.pager,
+ .l-datetime-picker .l-month-year-pager .search-holder.active.val, .s-menu-btn span.search-holder.active.l-click-area {
height: auto;
bottom: 0; }
@@ -4248,27 +4603,10 @@ span.req {
height: auto;
max-height: 100%;
position: relative; }
- /* line 228, ../../../../general/res/sass/search/_search.scss */
+ /* line 226, ../../../../general/res/sass/search/_search.scss */
.search .search-scroll .load-icon {
position: relative; }
- /* line 230, ../../../../general/res/sass/search/_search.scss */
- .search .search-scroll .load-icon.loading {
- pointer-events: none;
- margin-left: 6px; }
- /* line 234, ../../../../general/res/sass/search/_search.scss */
- .search .search-scroll .load-icon.loading .title-label {
- font-style: italic;
- font-size: .9em;
- opacity: 0.5;
- margin-left: 26px;
- line-height: 24px; }
- /* line 244, ../../../../general/res/sass/search/_search.scss */
- .search .search-scroll .load-icon.loading .wait-spinner {
- margin-left: 6px; }
- /* line 249, ../../../../general/res/sass/search/_search.scss */
- .search .search-scroll .load-icon:not(.loading) {
- cursor: pointer; }
- /* line 254, ../../../../general/res/sass/search/_search.scss */
+ /* line 230, ../../../../general/res/sass/search/_search.scss */
.search .search-scroll .load-more-button {
margin-top: 5px 0;
font-size: 0.8em;
@@ -4371,36 +4709,50 @@ span.req {
.overlay .hint {
color: #999999; }
/* line 80, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .abs.top-bar, .overlay .s-menu span.top-bar.l-click-area, .s-menu .overlay span.top-bar.l-click-area {
+ .overlay .abs.top-bar, .overlay .l-datetime-picker .l-month-year-pager .top-bar.pager, .l-datetime-picker .l-month-year-pager .overlay .top-bar.pager,
+ .overlay .l-datetime-picker .l-month-year-pager .top-bar.val,
+ .l-datetime-picker .l-month-year-pager .overlay .top-bar.val, .overlay .s-menu-btn span.top-bar.l-click-area, .s-menu-btn .overlay span.top-bar.l-click-area {
height: 45px; }
/* line 84, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .abs.editor, .overlay .s-menu span.editor.l-click-area, .s-menu .overlay span.editor.l-click-area,
+ .overlay .abs.editor, .overlay .l-datetime-picker .l-month-year-pager .editor.pager, .l-datetime-picker .l-month-year-pager .overlay .editor.pager,
+ .overlay .l-datetime-picker .l-month-year-pager .editor.val,
+ .l-datetime-picker .l-month-year-pager .overlay .editor.val, .overlay .s-menu-btn span.editor.l-click-area, .s-menu-btn .overlay span.editor.l-click-area,
.overlay .abs.message-body,
- .overlay .s-menu span.message-body.l-click-area,
- .s-menu .overlay span.message-body.l-click-area {
+ .overlay .l-datetime-picker .l-month-year-pager .message-body.pager,
+ .l-datetime-picker .l-month-year-pager .overlay .message-body.pager,
+ .overlay .l-datetime-picker .l-month-year-pager .message-body.val,
+ .l-datetime-picker .l-month-year-pager .overlay .message-body.val,
+ .overlay .s-menu-btn span.message-body.l-click-area,
+ .s-menu-btn .overlay span.message-body.l-click-area {
top: 55px;
bottom: 34px;
left: 0;
right: 0;
overflow: auto; }
/* line 92, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .abs.editor .field.l-med input[type='text'], .overlay .s-menu span.editor.l-click-area .field.l-med input[type='text'], .s-menu .overlay span.editor.l-click-area .field.l-med input[type='text'],
+ .overlay .abs.editor .field.l-med input[type='text'], .overlay .l-datetime-picker .l-month-year-pager .editor.pager .field.l-med input[type='text'], .l-datetime-picker .l-month-year-pager .overlay .editor.pager .field.l-med input[type='text'],
+ .overlay .l-datetime-picker .l-month-year-pager .editor.val .field.l-med input[type='text'],
+ .l-datetime-picker .l-month-year-pager .overlay .editor.val .field.l-med input[type='text'], .overlay .s-menu-btn span.editor.l-click-area .field.l-med input[type='text'], .s-menu-btn .overlay span.editor.l-click-area .field.l-med input[type='text'],
.overlay .abs.message-body .field.l-med input[type='text'],
- .overlay .s-menu span.message-body.l-click-area .field.l-med input[type='text'],
- .s-menu .overlay span.message-body.l-click-area .field.l-med input[type='text'] {
+ .overlay .l-datetime-picker .l-month-year-pager .message-body.pager .field.l-med input[type='text'],
+ .l-datetime-picker .l-month-year-pager .overlay .message-body.pager .field.l-med input[type='text'],
+ .overlay .l-datetime-picker .l-month-year-pager .message-body.val .field.l-med input[type='text'],
+ .l-datetime-picker .l-month-year-pager .overlay .message-body.val .field.l-med input[type='text'],
+ .overlay .s-menu-btn span.message-body.l-click-area .field.l-med input[type='text'],
+ .s-menu-btn .overlay span.message-body.l-click-area .field.l-med input[type='text'] {
width: 100%; }
/* line 98, ../../../../general/res/sass/overlay/_overlay.scss */
.overlay .bottom-bar {
text-align: right; }
/* line 100, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .bottom-bar .s-btn, .overlay .bottom-bar .s-menu {
+ .overlay .bottom-bar .s-btn, .overlay .bottom-bar .s-menu-btn {
font-size: 95%;
height: 24px;
line-height: 24px;
margin-left: 5px;
padding: 0 15px; }
/* line 102, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .bottom-bar .s-btn:not(.major), .overlay .bottom-bar .s-menu:not(.major) {
+ .overlay .bottom-bar .s-btn:not(.major), .overlay .bottom-bar .s-menu-btn:not(.major) {
background-color: #969696;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
@@ -4420,20 +4772,22 @@ span.req {
transition: background, 0.25s;
text-shadow: none; }
/* line 289, ../../../../general/res/sass/_mixins.scss */
- .overlay .bottom-bar .s-btn:not(.major) .icon, .overlay .bottom-bar .s-menu:not(.major) .icon {
+ .overlay .bottom-bar .s-btn:not(.major) .icon, .overlay .bottom-bar .s-menu-btn:not(.major) .icon {
color: #fff; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
/* line 294, ../../../../general/res/sass/_mixins.scss */
- .overlay .bottom-bar .s-btn:not(.major):not(.disabled):hover, .overlay .bottom-bar .s-menu:not(.major):not(.disabled):hover {
+ .overlay .bottom-bar .s-btn:not(.major):not(.disabled):hover, .overlay .bottom-bar .s-menu-btn:not(.major):not(.disabled):hover {
background: #7d7d7d; }
/* line 296, ../../../../general/res/sass/_mixins.scss */
- .overlay .bottom-bar .s-btn:not(.major):not(.disabled):hover > .icon, .overlay .bottom-bar .s-menu:not(.major):not(.disabled):hover > .icon {
+ .overlay .bottom-bar .s-btn:not(.major):not(.disabled):hover > .icon, .overlay .bottom-bar .s-menu-btn:not(.major):not(.disabled):hover > .icon {
color: white; } }
/* line 110, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .bottom-bar .s-btn:first-child, .overlay .bottom-bar .s-menu:first-child {
+ .overlay .bottom-bar .s-btn:first-child, .overlay .bottom-bar .s-menu-btn:first-child {
margin-left: 0; }
/* line 117, ../../../../general/res/sass/overlay/_overlay.scss */
- .overlay .abs.bottom-bar, .overlay .s-menu span.bottom-bar.l-click-area, .s-menu .overlay span.bottom-bar.l-click-area {
+ .overlay .abs.bottom-bar, .overlay .l-datetime-picker .l-month-year-pager .bottom-bar.pager, .l-datetime-picker .l-month-year-pager .overlay .bottom-bar.pager,
+ .overlay .l-datetime-picker .l-month-year-pager .bottom-bar.val,
+ .l-datetime-picker .l-month-year-pager .overlay .bottom-bar.val, .overlay .s-menu-btn span.bottom-bar.l-click-area, .s-menu-btn .overlay span.bottom-bar.l-click-area {
top: auto;
right: 0;
bottom: 0;
@@ -4502,16 +4856,30 @@ span.req {
.overlay > .holder .editor .form .form-row > .label:after {
float: none; }
/* line 57, ../../../../general/res/sass/mobile/overlay/_overlay.scss */
- .overlay > .holder .contents .abs.top-bar, .overlay > .holder .contents .s-menu span.top-bar.l-click-area, .s-menu .overlay > .holder .contents span.top-bar.l-click-area,
+ .overlay > .holder .contents .abs.top-bar, .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .top-bar.pager, .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .top-bar.pager,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .top-bar.val,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .top-bar.val, .overlay > .holder .contents .s-menu-btn span.top-bar.l-click-area, .s-menu-btn .overlay > .holder .contents span.top-bar.l-click-area,
.overlay > .holder .contents .abs.editor,
- .overlay > .holder .contents .s-menu span.editor.l-click-area,
- .s-menu .overlay > .holder .contents span.editor.l-click-area,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .editor.pager,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .editor.pager,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .editor.val,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .editor.val,
+ .overlay > .holder .contents .s-menu-btn span.editor.l-click-area,
+ .s-menu-btn .overlay > .holder .contents span.editor.l-click-area,
.overlay > .holder .contents .abs.message-body,
- .overlay > .holder .contents .s-menu span.message-body.l-click-area,
- .s-menu .overlay > .holder .contents span.message-body.l-click-area,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .message-body.pager,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .message-body.pager,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .message-body.val,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .message-body.val,
+ .overlay > .holder .contents .s-menu-btn span.message-body.l-click-area,
+ .s-menu-btn .overlay > .holder .contents span.message-body.l-click-area,
.overlay > .holder .contents .abs.bottom-bar,
- .overlay > .holder .contents .s-menu span.bottom-bar.l-click-area,
- .s-menu .overlay > .holder .contents span.bottom-bar.l-click-area {
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .bottom-bar.pager,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .bottom-bar.pager,
+ .overlay > .holder .contents .l-datetime-picker .l-month-year-pager .bottom-bar.val,
+ .l-datetime-picker .l-month-year-pager .overlay > .holder .contents .bottom-bar.val,
+ .overlay > .holder .contents .s-menu-btn span.bottom-bar.l-click-area,
+ .s-menu-btn .overlay > .holder .contents span.bottom-bar.l-click-area {
top: auto;
right: auto;
bottom: auto;
@@ -4635,17 +5003,17 @@ ul.tree {
.search-result-item .label .type-icon .icon.l-icon-alert {
position: absolute;
z-index: 2; }
- /* line 90, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 89, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .label .type-icon .icon.l-icon-alert,
.search-result-item .label .type-icon .icon.l-icon-alert {
- color: #ff533a;
+ color: #ff3c00;
font-size: 8px;
line-height: 8px;
height: 8px;
width: 8px;
top: 1px;
right: -2px; }
- /* line 96, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 95, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .label .type-icon .icon.l-icon-link,
.search-result-item .label .type-icon .icon.l-icon-link {
color: #49dedb;
@@ -4655,7 +5023,7 @@ ul.tree {
width: 8px;
left: -3px;
bottom: 0px; }
- /* line 104, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 103, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .label .title-label,
.search-result-item .label .title-label {
overflow: hidden;
@@ -4671,63 +5039,47 @@ ul.tree {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; }
- /* line 115, ../../../../general/res/sass/tree/_tree.scss */
- .tree-item.loading,
- .search-result-item.loading {
- pointer-events: none; }
- /* line 117, ../../../../general/res/sass/tree/_tree.scss */
- .tree-item.loading .label,
- .search-result-item.loading .label {
- opacity: 0.5; }
- /* line 119, ../../../../general/res/sass/tree/_tree.scss */
- .tree-item.loading .label .title-label,
- .search-result-item.loading .label .title-label {
- font-style: italic; }
- /* line 123, ../../../../general/res/sass/tree/_tree.scss */
- .tree-item.loading .wait-spinner,
- .search-result-item.loading .wait-spinner {
- margin-left: 14px; }
- /* line 128, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 113, ../../../../general/res/sass/tree/_tree.scss */
.tree-item.selected,
.search-result-item.selected {
background: #1ac6ff;
color: #fcfcfc; }
- /* line 131, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 116, ../../../../general/res/sass/tree/_tree.scss */
.tree-item.selected .view-control,
.search-result-item.selected .view-control {
color: #fcfcfc; }
- /* line 134, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 119, ../../../../general/res/sass/tree/_tree.scss */
.tree-item.selected .label .type-icon,
.search-result-item.selected .label .type-icon {
color: #fcfcfc; }
@media screen and (min-device-width: 800px) and (min-device-height: 1025px), screen and (min-device-width: 1025px) and (min-device-height: 800px) {
- /* line 142, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 127, ../../../../general/res/sass/tree/_tree.scss */
.tree-item:not(.selected):hover,
.search-result-item:not(.selected):hover {
background: rgba(102, 102, 102, 0.1);
color: #333333; }
- /* line 148, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 130, ../../../../general/res/sass/tree/_tree.scss */
.tree-item:not(.selected):hover .icon,
.search-result-item:not(.selected):hover .icon {
color: #0099cc; } }
- /* line 155, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 137, ../../../../general/res/sass/tree/_tree.scss */
.tree-item:not(.loading),
.search-result-item:not(.loading) {
cursor: pointer; }
- /* line 159, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 141, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .context-trigger,
.search-result-item .context-trigger {
top: -1px;
position: absolute;
right: 3px; }
- /* line 165, ../../../../general/res/sass/tree/_tree.scss */
+ /* line 146, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .context-trigger .invoke-menu,
.search-result-item .context-trigger .invoke-menu {
font-size: 0.75em;
height: 0.9rem;
line-height: 0.9rem; }
-/* line 174, ../../../../general/res/sass/tree/_tree.scss */
+/* line 155, ../../../../general/res/sass/tree/_tree.scss */
.tree-item .label {
left: 15px; }
@@ -4812,12 +5164,14 @@ ul.tree {
.frame.child-frame.panel:hover {
border-color: rgba(128, 128, 128, 0.2); }
/* line 32, ../../../../general/res/sass/user-environ/_frame.scss */
-.frame > .object-header.abs, .s-menu .frame > span.object-header.l-click-area {
+.frame > .object-header.abs, .l-datetime-picker .l-month-year-pager .frame > .object-header.pager,
+.l-datetime-picker .l-month-year-pager .frame > .object-header.val, .s-menu-btn .frame > span.object-header.l-click-area {
font-size: 0.75em;
height: 16px;
line-height: 16px; }
/* line 38, ../../../../general/res/sass/user-environ/_frame.scss */
-.frame > .object-holder.abs, .s-menu .frame > span.object-holder.l-click-area {
+.frame > .object-holder.abs, .l-datetime-picker .l-month-year-pager .frame > .object-holder.pager,
+.l-datetime-picker .l-month-year-pager .frame > .object-holder.val, .s-menu-btn .frame > span.object-holder.l-click-area {
top: 21px; }
/* line 41, ../../../../general/res/sass/user-environ/_frame.scss */
.frame .contents {
@@ -4826,17 +5180,17 @@ ul.tree {
bottom: 5px;
left: 5px; }
/* line 49, ../../../../general/res/sass/user-environ/_frame.scss */
-.frame.frame-template .s-btn, .frame.frame-template .s-menu,
-.frame.frame-template .s-menu {
+.frame.frame-template .s-btn, .frame.frame-template .s-menu-btn,
+.frame.frame-template .s-menu-btn {
height: 16px;
line-height: 16px;
padding: 0 5px; }
/* line 54, ../../../../general/res/sass/user-environ/_frame.scss */
- .frame.frame-template .s-btn > span, .frame.frame-template .s-menu > span,
- .frame.frame-template .s-menu > span {
+ .frame.frame-template .s-btn > span, .frame.frame-template .s-menu-btn > span,
+ .frame.frame-template .s-menu-btn > span {
font-size: 0.65rem; }
/* line 59, ../../../../general/res/sass/user-environ/_frame.scss */
-.frame.frame-template .s-menu:after {
+.frame.frame-template .s-menu-btn:after {
font-size: 8px; }
/* line 63, ../../../../general/res/sass/user-environ/_frame.scss */
.frame.frame-template .view-switcher {
@@ -4897,7 +5251,9 @@ ul.tree {
.edit-mode .top-bar .buttons-main {
white-space: nowrap; }
/* line 52, ../../../../general/res/sass/user-environ/_top-bar.scss */
- .edit-mode .top-bar .buttons-main.abs, .edit-mode .top-bar .s-menu span.buttons-main.l-click-area, .s-menu .edit-mode .top-bar span.buttons-main.l-click-area {
+ .edit-mode .top-bar .buttons-main.abs, .edit-mode .top-bar .l-datetime-picker .l-month-year-pager .buttons-main.pager, .l-datetime-picker .l-month-year-pager .edit-mode .top-bar .buttons-main.pager,
+ .edit-mode .top-bar .l-datetime-picker .l-month-year-pager .buttons-main.val,
+ .l-datetime-picker .l-month-year-pager .edit-mode .top-bar .buttons-main.val, .edit-mode .top-bar .s-menu-btn span.buttons-main.l-click-area, .s-menu-btn .edit-mode .top-bar span.buttons-main.l-click-area {
bottom: auto;
left: auto; }
@@ -5132,29 +5488,33 @@ table {
table thead,
table .thead {
border-bottom: 1px solid #fcfcfc; }
- /* line 43, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 44, ../../../../general/res/sass/lists/_tabular.scss */
+ .tabular:not(.fixed-header) tr th,
+ table:not(.fixed-header) tr th {
+ background-color: #e3e3e3; }
+ /* line 48, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tbody, .tabular .tbody,
table tbody,
table .tbody {
display: table-row-group; }
- /* line 46, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 51, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tbody tr:hover, .tabular tbody .tr:hover, .tabular .tbody tr:hover, .tabular .tbody .tr:hover,
table tbody tr:hover,
table tbody .tr:hover,
table .tbody tr:hover,
table .tbody .tr:hover {
background: rgba(51, 51, 51, 0.1); }
- /* line 51, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 56, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr, .tabular .tr,
table tr,
table .tr {
display: table-row; }
- /* line 53, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 58, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr:first-child .td, .tabular .tr:first-child .td,
table tr:first-child .td,
table .tr:first-child .td {
border-top: none; }
- /* line 57, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 62, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr.group-header td, .tabular tr.group-header .td, .tabular .tr.group-header td, .tabular .tr.group-header .td,
table tr.group-header td,
table tr.group-header .td,
@@ -5162,7 +5522,7 @@ table {
table .tr.group-header .td {
background-color: #efefef;
color: #404040; }
- /* line 63, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 68, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th, .tabular tr .th, .tabular tr td, .tabular tr .td, .tabular .tr th, .tabular .tr .th, .tabular .tr td, .tabular .tr .td,
table tr th,
table tr .th,
@@ -5173,26 +5533,25 @@ table {
table .tr td,
table .tr .td {
display: table-cell; }
- /* line 66, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 71, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th, .tabular tr .th, .tabular .tr th, .tabular .tr .th,
table tr th,
table tr .th,
table .tr th,
table .tr .th {
- background-color: #e3e3e3;
border-left: 1px solid #fcfcfc;
color: #333333;
padding: 5px 5px;
white-space: nowrap;
vertical-align: middle; }
- /* line 73, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 77, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th:first-child, .tabular tr .th:first-child, .tabular .tr th:first-child, .tabular .tr .th:first-child,
table tr th:first-child,
table tr .th:first-child,
table .tr th:first-child,
table .tr .th:first-child {
border-left: none; }
- /* line 77, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 81, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th.sort.sort:after, .tabular tr .th.sort.sort:after, .tabular .tr th.sort.sort:after, .tabular .tr .th.sort.sort:after,
table tr th.sort.sort:after,
table tr .th.sort.sort:after,
@@ -5204,21 +5563,21 @@ table {
content: "\ed";
display: inline-block;
margin-left: 3px; }
- /* line 85, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 89, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th.sort.sort.desc:after, .tabular tr .th.sort.sort.desc:after, .tabular .tr th.sort.sort.desc:after, .tabular .tr .th.sort.sort.desc:after,
table tr th.sort.sort.desc:after,
table tr .th.sort.sort.desc:after,
table .tr th.sort.sort.desc:after,
table .tr .th.sort.sort.desc:after {
content: "\ec"; }
- /* line 89, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 93, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr th.sortable, .tabular tr .th.sortable, .tabular .tr th.sortable, .tabular .tr .th.sortable,
table tr th.sortable,
table tr .th.sortable,
table .tr th.sortable,
table .tr .th.sortable {
cursor: pointer; }
- /* line 93, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 97, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr td, .tabular tr .td, .tabular .tr td, .tabular .tr .td,
table tr td,
table tr .td,
@@ -5230,21 +5589,21 @@ table {
padding: 3px 5px;
word-wrap: break-word;
vertical-align: top; }
- /* line 100, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 104, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr td.numeric, .tabular tr .td.numeric, .tabular .tr td.numeric, .tabular .tr .td.numeric,
table tr td.numeric,
table tr .td.numeric,
table .tr td.numeric,
table .tr .td.numeric {
text-align: right; }
- /* line 103, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 107, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr td.s-cell-type-value, .tabular tr .td.s-cell-type-value, .tabular .tr td.s-cell-type-value, .tabular .tr .td.s-cell-type-value,
table tr td.s-cell-type-value,
table tr .td.s-cell-type-value,
table .tr td.s-cell-type-value,
table .tr .td.s-cell-type-value {
text-align: right; }
- /* line 105, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 109, ../../../../general/res/sass/lists/_tabular.scss */
.tabular tr td.s-cell-type-value .l-cell-contents, .tabular tr .td.s-cell-type-value .l-cell-contents, .tabular .tr td.s-cell-type-value .l-cell-contents, .tabular .tr .td.s-cell-type-value .l-cell-contents,
table tr td.s-cell-type-value .l-cell-contents,
table tr .td.s-cell-type-value .l-cell-contents,
@@ -5255,23 +5614,23 @@ table {
border-radius: 3px;
padding-left: 5px;
padding-right: 5px; }
- /* line 121, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 125, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.filterable tbody, .tabular.filterable .tbody,
table.filterable tbody,
table.filterable .tbody {
top: 44px; }
- /* line 124, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 128, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.filterable input[type="text"],
table.filterable input[type="text"] {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%; }
- /* line 130, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 134, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header,
table.fixed-header {
height: 100%; }
- /* line 132, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 136, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header thead, .tabular.fixed-header .thead,
.tabular.fixed-header tbody tr, .tabular.fixed-header .tbody .tr,
table.fixed-header thead,
@@ -5280,12 +5639,12 @@ table {
table.fixed-header .tbody .tr {
display: table;
table-layout: fixed; }
- /* line 137, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 141, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header thead, .tabular.fixed-header .thead,
table.fixed-header thead,
table.fixed-header .thead {
width: calc(100% - 10px); }
- /* line 139, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 143, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header thead:before, .tabular.fixed-header .thead:before,
table.fixed-header thead:before,
table.fixed-header .thead:before {
@@ -5295,8 +5654,8 @@ table {
position: absolute;
width: 100%;
height: 22px;
- background: rgba(255, 255, 255, 0.15); }
- /* line 149, ../../../../general/res/sass/lists/_tabular.scss */
+ background-color: #e3e3e3; }
+ /* line 153, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.fixed-header tbody, .tabular.fixed-header .tbody,
table.fixed-header tbody,
table.fixed-header .tbody {
@@ -5311,7 +5670,7 @@ table {
top: 22px;
display: block;
overflow-y: scroll; }
- /* line 157, ../../../../general/res/sass/lists/_tabular.scss */
+ /* line 161, ../../../../general/res/sass/lists/_tabular.scss */
.tabular.t-event-messages td, .tabular.t-event-messages .td,
table.t-event-messages td,
table.t-event-messages .td {
@@ -5683,9 +6042,6 @@ table {
.l-view-section.fixed {
font-size: 0.8em; }
/* line 13, ../../../../general/res/sass/_views.scss */
- .l-view-section.scrolling {
- overflow: auto; }
- /* line 16, ../../../../general/res/sass/_views.scss */
.l-view-section .controls,
.l-view-section label,
.l-view-section .inline-block {
@@ -6025,7 +6381,7 @@ table {
.autoflow {
font-size: 0.75rem; }
/* line 32, ../../../../general/res/sass/_autoflow.scss */
- .autoflow:hover .l-autoflow-header .s-btn.change-column-width, .autoflow:hover .l-autoflow-header .change-column-width.s-menu {
+ .autoflow:hover .l-autoflow-header .s-btn.change-column-width, .autoflow:hover .l-autoflow-header .change-column-width.s-menu-btn {
-moz-transition-property: visibility, opacity, background-color, border-color;
-o-transition-property: visibility, opacity, background-color, border-color;
-webkit-transition-property: visibility, opacity, background-color, border-color;
@@ -6049,7 +6405,7 @@ table {
.autoflow .l-autoflow-header span {
vertical-align: middle; }
/* line 48, ../../../../general/res/sass/_autoflow.scss */
- .autoflow .l-autoflow-header .s-btn.change-column-width, .autoflow .l-autoflow-header .change-column-width.s-menu {
+ .autoflow .l-autoflow-header .s-btn.change-column-width, .autoflow .l-autoflow-header .change-column-width.s-menu-btn {
-moz-transition-property: visibility, opacity, background-color, border-color;
-o-transition-property: visibility, opacity, background-color, border-color;
-webkit-transition-property: visibility, opacity, background-color, border-color;
@@ -6326,7 +6682,7 @@ table {
left: 0;
z-index: 1; }
/* line 22, ../../../../general/res/sass/features/_time-display.scss */
- .l-time-display.l-timer .l-elem.l-value .ui-symbol.direction {
+ .l-time-display.l-timer .l-elem.l-value .ui-symbol.direction, .l-time-display.l-timer .l-elem.l-value .l-datetime-picker .l-month-year-pager .direction.pager, .l-datetime-picker .l-month-year-pager .l-time-display.l-timer .l-elem.l-value .direction.pager {
font-size: 0.8em; }
/* line 26, ../../../../general/res/sass/features/_time-display.scss */
.l-time-display.l-timer:hover .l-elem.l-value {
@@ -6353,5 +6709,5 @@ table {
vertical-align: top; }
/* line 3, ../sass/_controls.scss */
-.s-btn.major .title-label, .major.s-menu .title-label {
+.s-btn.major .title-label, .major.s-menu-btn .title-label {
text-transform: uppercase; }
diff --git a/platform/commonUI/themes/snow/res/sass/_constants.scss b/platform/commonUI/themes/snow/res/sass/_constants.scss
index 8a1d54642..5b961b013 100644
--- a/platform/commonUI/themes/snow/res/sass/_constants.scss
+++ b/platform/commonUI/themes/snow/res/sass/_constants.scss
@@ -7,12 +7,14 @@ $colorKey: #0099cc;
$colorKeySelectedBg: $colorKey;
$colorKeyFg: #fff;
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
+$colorA: #999;
+$colorAHov: $colorKey;
$contrastRatioPercent: 40%;
$basicCr: 4px;
$controlCr: $basicCr;
$smallCr: 3px;
-// Buttons
+// Buttons and Controls
$colorBtnBg: pullForward($colorBodyBg, $contrastRatioPercent);
$colorBtnFg: #fff;
$colorBtnMajorBg: $colorKey;
@@ -20,10 +22,22 @@ $colorBtnMajorFg: $colorKeyFg;
$colorBtnIcon: #eee;
$colorInvokeMenu: #000;
$contrastInvokeMenuPercent: 40%;
+$shdwBtns: none;
+$sliderColorBase: $colorKey;
+$sliderColorRangeHolder: rgba(black, 0.07);
+$sliderColorRange: rgba($sliderColorBase, 0.2);
+$sliderColorRangeHov: rgba($sliderColorBase, 0.4);
+$sliderColorKnob: rgba($sliderColorBase, 0.5);
+$sliderColorKnobHov: rgba($sliderColorBase, 0.7);
+$sliderColorRangeValHovBg: $sliderColorRange; //rgba($sliderColorBase, 0.1);
+$sliderColorRangeValHovFg: $colorBodyFg;
+$sliderKnobW: nth($ueTimeControlH,2)/2;
+$timeControllerToiLineColor: $colorBodyFg;
+$timeControllerToiLineColorHov: #0052b5;
// General Colors
-$colorAlt1: #ff6600;
-$colorAlert: #ff533a;
+$colorAlt1: #776ba2;
+$colorAlert: #ff3c00;
$colorIconLink: #49dedb;
$colorPausedBg: #ff9900;
$colorPausedFg: #fff;
@@ -32,6 +46,7 @@ $colorGridLines: rgba(#000, 0.05);
$colorInvokeMenu: #fff;
$colorObjHdrTxt: $colorBodyFg;
$colorObjHdrIc: pushBack($colorObjHdrTxt, 30%);
+$colorTick: rgba(black, 0.2);
// Menu colors
$colorMenuBg: pushBack($colorBodyBg, 10%);
@@ -118,9 +133,10 @@ $colorTabHeaderBorder: $colorBodyBg;
// Plot
$colorPlotBg: rgba(black, 0.05);
$colorPlotFg: $colorBodyFg;
-$colorPlotHash: rgba(black, 0.2);
+$colorPlotHash: $colorTick;
$stylePlotHash: dashed;
$colorPlotAreaBorder: $colorInteriorBorder;
+$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
// Tree
$colorItemTreeIcon: $colorKey;
@@ -151,5 +167,16 @@ $colorGrippyInteriorHover: $colorBodyBg;
// Mobile
$colorMobilePaneLeft: darken($colorBodyBg, 2%);
+// Datetime Picker, Calendar
+$colorCalCellHovBg: $colorKey;
+$colorCalCellHovFg: $colorKeyFg;
+$colorCalCellSelectedBg: $colorItemTreeSelectedBg;
+$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
+$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
+
// About Screen
-$colorAboutLink: #84b3ff; \ No newline at end of file
+$colorAboutLink: #84b3ff;
+
+// Loading
+$colorLoadingBg: rgba($colorLoadingFg, 0.1);
+$colorLoadingFg: $colorAlt1;
diff --git a/platform/commonUI/themes/snow/res/sass/_mixins.scss b/platform/commonUI/themes/snow/res/sass/_mixins.scss
index 2ff7fb269..e8ab65d5f 100644
--- a/platform/commonUI/themes/snow/res/sass/_mixins.scss
+++ b/platform/commonUI/themes/snow/res/sass/_mixins.scss
@@ -1,5 +1,6 @@
@mixin containerSubtle($bg: $colorBodyBg, $fg: $colorBodyFg) {
@include containerBase($bg, $fg);
+ @include boxShdw($shdwBtns);
}
@mixin btnSubtle($bg: $colorBtnBg, $bgHov: none, $fg: $colorBtnFg, $ic: $colorBtnIcon) {
diff --git a/platform/core/bundle.json b/platform/core/bundle.json
index 952daf557..330ac1c2b 100644
--- a/platform/core/bundle.json
+++ b/platform/core/bundle.json
@@ -66,6 +66,7 @@
"depends": [
"persistenceService",
"$q",
+ "now",
"PERSISTENCE_SPACE",
"ADDITIONAL_PERSISTENCE_SPACES"
]
diff --git a/platform/core/src/capabilities/MutationCapability.js b/platform/core/src/capabilities/MutationCapability.js
index b9f49ca96..1f08abda6 100644
--- a/platform/core/src/capabilities/MutationCapability.js
+++ b/platform/core/src/capabilities/MutationCapability.js
@@ -29,7 +29,8 @@ define(
function () {
"use strict";
- var TOPIC_PREFIX = "mutation:";
+ var GENERAL_TOPIC = "mutation",
+ TOPIC_PREFIX = "mutation:";
// Utility function to overwrite a destination object
// with the contents of a source object.
@@ -78,7 +79,11 @@ define(
* @implements {Capability}
*/
function MutationCapability(topic, now, domainObject) {
- this.mutationTopic = topic(TOPIC_PREFIX + domainObject.getId());
+ this.generalMutationTopic =
+ topic(GENERAL_TOPIC);
+ this.specificMutationTopic =
+ topic(TOPIC_PREFIX + domainObject.getId());
+
this.now = now;
this.domainObject = domainObject;
}
@@ -115,11 +120,17 @@ define(
// mutator function has a temporary copy to work with.
var domainObject = this.domainObject,
now = this.now,
- t = this.mutationTopic,
+ generalTopic = this.generalMutationTopic,
+ specificTopic = this.specificMutationTopic,
model = domainObject.getModel(),
clone = JSON.parse(JSON.stringify(model)),
useTimestamp = arguments.length > 1;
+ function notifyListeners(model) {
+ generalTopic.notify(domainObject);
+ specificTopic.notify(model);
+ }
+
// Function to handle copying values to the actual
function handleMutation(mutationResult) {
// If mutation result was undefined, just use
@@ -136,7 +147,7 @@ define(
copyValues(model, result);
}
model.modified = useTimestamp ? timestamp : now();
- t.notify(model);
+ notifyListeners(model);
}
// Report the result of the mutation
@@ -158,7 +169,7 @@ define(
* @memberof platform/core.MutationCapability#
*/
MutationCapability.prototype.listen = function (listener) {
- return this.mutationTopic.listen(listener);
+ return this.specificMutationTopic.listen(listener);
};
/**
diff --git a/platform/core/src/models/PersistedModelProvider.js b/platform/core/src/models/PersistedModelProvider.js
index a10f81817..c5e2927a9 100644
--- a/platform/core/src/models/PersistedModelProvider.js
+++ b/platform/core/src/models/PersistedModelProvider.js
@@ -39,14 +39,16 @@ define(
* @param {PersistenceService} persistenceService the service in which
* domain object models are persisted.
* @param $q Angular's $q service, for working with promises
+ * @param {function} now a function which provides the current time
* @param {string} space the name of the persistence space(s)
* from which models should be retrieved.
* @param {string} spaces additional persistence spaces to use
*/
- function PersistedModelProvider(persistenceService, $q, space, spaces) {
+ function PersistedModelProvider(persistenceService, $q, now, space, spaces) {
this.persistenceService = persistenceService;
this.$q = $q;
this.spaces = [space].concat(spaces || []);
+ this.now = now;
}
// Take the most recently modified model, for cases where
@@ -61,7 +63,9 @@ define(
PersistedModelProvider.prototype.getModels = function (ids) {
var persistenceService = this.persistenceService,
$q = this.$q,
- spaces = this.spaces;
+ spaces = this.spaces,
+ space = this.space,
+ now = this.now;
// Load a single object model from any persistence spaces
function loadModel(id) {
@@ -72,11 +76,24 @@ define(
});
}
+ // Ensure that models read from persistence have some
+ // sensible timestamp indicating they've been persisted.
+ function addPersistedTimestamp(model) {
+ if (model && (model.persisted === undefined)) {
+ model.persisted = model.modified !== undefined ?
+ model.modified : now();
+ }
+
+ return model;
+ }
+
// Package the result as id->model
function packageResult(models) {
var result = {};
ids.forEach(function (id, index) {
- result[id] = models[index];
+ if (models[index]) {
+ result[id] = addPersistedTimestamp(models[index]);
+ }
});
return result;
}
diff --git a/platform/core/src/services/Throttle.js b/platform/core/src/services/Throttle.js
index 3d68988d6..4b1ad3253 100644
--- a/platform/core/src/services/Throttle.js
+++ b/platform/core/src/services/Throttle.js
@@ -36,11 +36,16 @@ define(
*
* Returns a function that, when invoked, will invoke `fn` after
* `delay` milliseconds, only if no other invocations are pending.
- * The optional argument `apply` determines whether.
+ * The optional argument `apply` determines whether or not a
+ * digest cycle should be triggered.
*
* The returned function will itself return a `Promise` which will
* resolve to the returned value of `fn` whenever that is invoked.
*
+ * In cases where arguments are provided, only the most recent
+ * set of arguments will be passed on to the throttled function
+ * at the time it is executed.
+ *
* @returns {Function}
* @memberof platform/core
*/
@@ -56,12 +61,14 @@ define(
* @memberof platform/core.Throttle#
*/
return function (fn, delay, apply) {
- var activeTimeout;
+ var promise,
+ args = [];
- // Clear active timeout, so that next invocation starts
- // a new one.
- function clearActiveTimeout() {
- activeTimeout = undefined;
+ function invoke() {
+ // Clear the active timeout so a new one starts next time.
+ promise = undefined;
+ // Invoke the function with the latest supplied arguments.
+ return fn.apply(null, args);
}
// Defaults
@@ -69,14 +76,13 @@ define(
apply = apply || false;
return function () {
+ // Store arguments from this invocation
+ args = Array.prototype.slice.apply(arguments, [0]);
// Start a timeout if needed
- if (!activeTimeout) {
- activeTimeout = $timeout(fn, delay, apply);
- activeTimeout.then(clearActiveTimeout);
- }
+ promise = promise || $timeout(invoke, delay, apply);
// Return whichever timeout is active (to get
// a promise for the results of fn)
- return activeTimeout;
+ return promise;
};
};
}
diff --git a/platform/core/test/models/PersistedModelProviderSpec.js b/platform/core/test/models/PersistedModelProviderSpec.js
index 8dcb58a40..81769834b 100644
--- a/platform/core/test/models/PersistedModelProviderSpec.js
+++ b/platform/core/test/models/PersistedModelProviderSpec.js
@@ -35,6 +35,7 @@ define(
SPACE = "space0",
spaces = [ "space1" ],
modTimes,
+ mockNow,
provider;
function mockPromise(value) {
@@ -55,19 +56,33 @@ define(
beforeEach(function () {
modTimes = {};
mockQ = { when: mockPromise, all: mockAll };
- mockPersistenceService = {
- readObject: function (space, id) {
+ mockPersistenceService = jasmine.createSpyObj(
+ 'persistenceService',
+ [
+ 'createObject',
+ 'readObject',
+ 'updateObject',
+ 'deleteObject',
+ 'listSpaces',
+ 'listObjects'
+ ]
+ );
+ mockNow = jasmine.createSpy("now");
+
+ mockPersistenceService.readObject
+ .andCallFake(function (space, id) {
return mockPromise({
space: space,
id: id,
- modified: (modTimes[space] || {})[id]
+ modified: (modTimes[space] || {})[id],
+ persisted: 0
});
- }
- };
+ });
provider = new PersistedModelProvider(
mockPersistenceService,
mockQ,
+ mockNow,
SPACE,
spaces
);
@@ -81,12 +96,13 @@ define(
});
expect(models).toEqual({
- a: { space: SPACE, id: "a" },
- x: { space: SPACE, id: "x" },
- zz: { space: SPACE, id: "zz" }
+ a: { space: SPACE, id: "a", persisted: 0 },
+ x: { space: SPACE, id: "x", persisted: 0 },
+ zz: { space: SPACE, id: "zz", persisted: 0 }
});
});
+
it("reads object models from multiple spaces", function () {
var models;
@@ -99,9 +115,36 @@ define(
});
expect(models).toEqual({
- a: { space: SPACE, id: "a" },
- x: { space: 'space1', id: "x", modified: 12321 },
- zz: { space: SPACE, id: "zz" }
+ a: { space: SPACE, id: "a", persisted: 0 },
+ x: { space: 'space1', id: "x", modified: 12321, persisted: 0 },
+ zz: { space: SPACE, id: "zz", persisted: 0 }
+ });
+ });
+
+
+ it("ensures that persisted timestamps are present", function () {
+ var mockCallback = jasmine.createSpy("callback"),
+ testModels = {
+ a: { modified: 123, persisted: 1984, name: "A" },
+ b: { persisted: 1977, name: "B" },
+ c: { modified: 42, name: "C" },
+ d: { name: "D" }
+ };
+
+ mockPersistenceService.readObject.andCallFake(
+ function (space, id) {
+ return mockPromise(testModels[id]);
+ }
+ );
+ mockNow.andReturn(12321);
+
+ provider.getModels(Object.keys(testModels)).then(mockCallback);
+
+ expect(mockCallback).toHaveBeenCalledWith({
+ a: { modified: 123, persisted: 1984, name: "A" },
+ b: { persisted: 1977, name: "B" },
+ c: { modified: 42, persisted: 42, name: "C" },
+ d: { persisted: 12321, name: "D" }
});
});
diff --git a/platform/core/test/services/ThrottleSpec.js b/platform/core/test/services/ThrottleSpec.js
index bcaf2af36..3b361f70b 100644
--- a/platform/core/test/services/ThrottleSpec.js
+++ b/platform/core/test/services/ThrottleSpec.js
@@ -45,7 +45,9 @@ define(
// Verify precondition: Not called at throttle-time
expect(mockTimeout).not.toHaveBeenCalled();
expect(throttled()).toEqual(mockPromise);
- expect(mockTimeout).toHaveBeenCalledWith(mockFn, 0, false);
+ expect(mockFn).not.toHaveBeenCalled();
+ expect(mockTimeout)
+ .toHaveBeenCalledWith(jasmine.any(Function), 0, false);
});
it("schedules only one timeout at a time", function () {
@@ -59,10 +61,11 @@ define(
it("schedules additional invocations after resolution", function () {
var throttled = throttle(mockFn);
throttled();
- mockPromise.then.mostRecentCall.args[0](); // Resolve timeout
+ mockTimeout.mostRecentCall.args[0](); // Resolve timeout
throttled();
- mockPromise.then.mostRecentCall.args[0]();
+ mockTimeout.mostRecentCall.args[0]();
throttled();
+ mockTimeout.mostRecentCall.args[0]();
expect(mockTimeout.calls.length).toEqual(3);
});
});
diff --git a/platform/entanglement/bundle.json b/platform/entanglement/bundle.json
index 61c3d9053..d8cde0ada 100644
--- a/platform/entanglement/bundle.json
+++ b/platform/entanglement/bundle.json
@@ -30,6 +30,14 @@
"category": "contextual",
"implementation": "actions/LinkAction.js",
"depends": ["locationService", "linkService"]
+ },
+ {
+ "key": "follow",
+ "name": "Go To Original",
+ "description": "Go to the original, un-linked instance of this object.",
+ "glyph": "\u00F4",
+ "category": "contextual",
+ "implementation": "actions/GoToOriginalAction.js"
}
],
"components": [
@@ -52,7 +60,8 @@
"key": "location",
"name": "Location Capability",
"description": "Provides a capability for retrieving the location of an object based upon it's context.",
- "implementation": "capabilities/LocationCapability"
+ "implementation": "capabilities/LocationCapability",
+ "depends": [ "$q", "$injector" ]
}
],
"services": [
diff --git a/platform/entanglement/src/actions/GoToOriginalAction.js b/platform/entanglement/src/actions/GoToOriginalAction.js
new file mode 100644
index 000000000..9722915ad
--- /dev/null
+++ b/platform/entanglement/src/actions/GoToOriginalAction.js
@@ -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.
+ *****************************************************************************/
+
+/*global define */
+define(
+ function () {
+ "use strict";
+
+ /**
+ * Implements the "Go To Original" action, which follows a link back
+ * to an original instance of an object.
+ *
+ * @implements {Action}
+ * @constructor
+ * @private
+ * @memberof platform/entanglement
+ * @param {ActionContext} context the context in which the action
+ * will be performed
+ */
+ function GoToOriginalAction(context) {
+ this.domainObject = context.domainObject;
+ }
+
+ GoToOriginalAction.prototype.perform = function () {
+ return this.domainObject.getCapability("location").getOriginal()
+ .then(function (originalObject) {
+ var actionCapability =
+ originalObject.getCapability("action");
+ return actionCapability &&
+ actionCapability.perform("navigate");
+ });
+ };
+
+ GoToOriginalAction.appliesTo = function (context) {
+ var domainObject = context.domainObject;
+ return domainObject && domainObject.hasCapability("location")
+ && domainObject.getCapability("location").isLink();
+ };
+
+ return GoToOriginalAction;
+ }
+);
+
diff --git a/platform/entanglement/src/capabilities/LocationCapability.js b/platform/entanglement/src/capabilities/LocationCapability.js
index 17d678f57..27e1f74c7 100644
--- a/platform/entanglement/src/capabilities/LocationCapability.js
+++ b/platform/entanglement/src/capabilities/LocationCapability.js
@@ -1,3 +1,25 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
/*global define */
define(
@@ -12,12 +34,42 @@ define(
*
* @constructor
*/
- function LocationCapability(domainObject) {
+ function LocationCapability($q, $injector, domainObject) {
this.domainObject = domainObject;
+ this.$q = $q;
+ this.$injector = $injector;
return this;
}
/**
+ * Get an instance of this domain object in its original location.
+ *
+ * @returns {Promise.<DomainObject>} a promise for the original
+ * instance of this domain object
+ */
+ LocationCapability.prototype.getOriginal = function () {
+ var id;
+
+ if (this.isOriginal()) {
+ return this.$q.when(this.domainObject);
+ }
+
+ id = this.domainObject.getId();
+
+ this.objectService =
+ this.objectService || this.$injector.get("objectService");
+
+ // Assume that an object will be correctly contextualized when
+ // loaded directly from the object service; this is true
+ // so long as LocatingObjectDecorator is present, and that
+ // decorator is also contained in this bundle.
+ return this.objectService.getObjects([id])
+ .then(function (objects) {
+ return objects[id];
+ });
+ };
+
+ /**
* Set the primary location (the parent id) of the current domain
* object.
*
@@ -78,10 +130,6 @@ define(
return !this.isLink();
};
- function createLocationCapability(domainObject) {
- return new LocationCapability(domainObject);
- }
-
- return createLocationCapability;
+ return LocationCapability;
}
);
diff --git a/platform/entanglement/test/actions/GoToOriginalActionSpec.js b/platform/entanglement/test/actions/GoToOriginalActionSpec.js
new file mode 100644
index 000000000..40c2f213c
--- /dev/null
+++ b/platform/entanglement/test/actions/GoToOriginalActionSpec.js
@@ -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.
+ *****************************************************************************/
+
+/*global define,describe,beforeEach,it,jasmine,expect */
+
+define(
+ [
+ '../../src/actions/GoToOriginalAction',
+ '../DomainObjectFactory',
+ '../ControlledPromise'
+ ],
+ function (GoToOriginalAction, domainObjectFactory, ControlledPromise) {
+ 'use strict';
+
+ describe("The 'go to original' action", function () {
+ var testContext,
+ originalDomainObject,
+ mockLocationCapability,
+ mockOriginalActionCapability,
+ originalPromise,
+ action;
+
+ beforeEach(function () {
+ mockLocationCapability = jasmine.createSpyObj(
+ 'location',
+ [ 'isLink', 'isOriginal', 'getOriginal' ]
+ );
+ mockOriginalActionCapability = jasmine.createSpyObj(
+ 'action',
+ [ 'perform', 'getActions' ]
+ );
+ originalPromise = new ControlledPromise();
+ mockLocationCapability.getOriginal.andReturn(originalPromise);
+ mockLocationCapability.isLink.andReturn(true);
+ mockLocationCapability.isOriginal.andCallFake(function () {
+ return !mockLocationCapability.isLink();
+ });
+ testContext = {
+ domainObject: domainObjectFactory({
+ capabilities: {
+ location: mockLocationCapability
+ }
+ })
+ };
+ originalDomainObject = domainObjectFactory({
+ capabilities: {
+ action: mockOriginalActionCapability
+ }
+ });
+
+ action = new GoToOriginalAction(testContext);
+ });
+
+ it("is applicable to links", function () {
+ expect(GoToOriginalAction.appliesTo(testContext))
+ .toBeTruthy();
+ });
+
+ it("is not applicable to originals", function () {
+ mockLocationCapability.isLink.andReturn(false);
+ expect(GoToOriginalAction.appliesTo(testContext))
+ .toBeFalsy();
+ });
+
+ it("navigates to original objects when performed", function () {
+ expect(mockOriginalActionCapability.perform)
+ .not.toHaveBeenCalled();
+ action.perform();
+ originalPromise.resolve(originalDomainObject);
+ expect(mockOriginalActionCapability.perform)
+ .toHaveBeenCalledWith('navigate');
+ });
+
+ });
+ }
+);
diff --git a/platform/entanglement/test/capabilities/LocationCapabilitySpec.js b/platform/entanglement/test/capabilities/LocationCapabilitySpec.js
index 9cbfcc1be..442bfe20a 100644
--- a/platform/entanglement/test/capabilities/LocationCapabilitySpec.js
+++ b/platform/entanglement/test/capabilities/LocationCapabilitySpec.js
@@ -1,3 +1,25 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+
/*global define,describe,it,expect,beforeEach,jasmine */
define(
@@ -7,6 +29,7 @@ define(
'../ControlledPromise'
],
function (LocationCapability, domainObjectFactory, ControlledPromise) {
+ 'use strict';
describe("LocationCapability", function () {
@@ -14,13 +37,17 @@ define(
var locationCapability,
persistencePromise,
mutationPromise,
+ mockQ,
+ mockInjector,
+ mockObjectService,
domainObject;
beforeEach(function () {
domainObject = domainObjectFactory({
+ id: "testObject",
capabilities: {
context: {
- getParent: function() {
+ getParent: function () {
return domainObjectFactory({id: 'root'});
}
},
@@ -35,6 +62,11 @@ define(
}
});
+ mockQ = jasmine.createSpyObj("$q", ["when"]);
+ mockInjector = jasmine.createSpyObj("$injector", ["get"]);
+ mockObjectService =
+ jasmine.createSpyObj("objectService", ["getObjects"]);
+
persistencePromise = new ControlledPromise();
domainObject.capabilities.persistence.persist.andReturn(
persistencePromise
@@ -49,7 +81,11 @@ define(
}
);
- locationCapability = new LocationCapability(domainObject);
+ locationCapability = new LocationCapability(
+ mockQ,
+ mockInjector,
+ domainObject
+ );
});
it("returns contextual location", function () {
@@ -88,6 +124,57 @@ define(
expect(whenComplete).toHaveBeenCalled();
});
+ describe("when used to load an original instance", function () {
+ var objectPromise,
+ qPromise,
+ originalObjects,
+ mockCallback;
+
+ function resolvePromises() {
+ if (mockQ.when.calls.length > 0) {
+ qPromise.resolve(mockQ.when.mostRecentCall.args[0]);
+ }
+ if (mockObjectService.getObjects.calls.length > 0) {
+ objectPromise.resolve(originalObjects);
+ }
+ }
+
+ beforeEach(function () {
+ objectPromise = new ControlledPromise();
+ qPromise = new ControlledPromise();
+ originalObjects = {
+ testObject: domainObjectFactory()
+ };
+
+ mockInjector.get.andCallFake(function (key) {
+ return key === 'objectService' && mockObjectService;
+ });
+ mockObjectService.getObjects.andReturn(objectPromise);
+ mockQ.when.andReturn(qPromise);
+
+ mockCallback = jasmine.createSpy('callback');
+ });
+
+ it("provides originals directly", function () {
+ domainObject.model.location = 'root';
+ locationCapability.getOriginal().then(mockCallback);
+ expect(mockCallback).not.toHaveBeenCalled();
+ resolvePromises();
+ expect(mockCallback)
+ .toHaveBeenCalledWith(domainObject);
+ });
+
+ it("loads from the object service for links", function () {
+ domainObject.model.location = 'some-other-root';
+ locationCapability.getOriginal().then(mockCallback);
+ expect(mockCallback).not.toHaveBeenCalled();
+ resolvePromises();
+ expect(mockCallback)
+ .toHaveBeenCalledWith(originalObjects.testObject);
+ });
+ });
+
+
});
});
}
diff --git a/platform/entanglement/test/suite.json b/platform/entanglement/test/suite.json
index 12831b407..89c082f9c 100644
--- a/platform/entanglement/test/suite.json
+++ b/platform/entanglement/test/suite.json
@@ -1,5 +1,9 @@
[
"actions/AbstractComposeAction",
+ "actions/CopyAction",
+ "actions/GoToOriginalAction",
+ "actions/LinkAction",
+ "actions/MoveAction",
"services/CopyService",
"services/LinkService",
"services/MoveService",
diff --git a/platform/features/conductor/README.md b/platform/features/conductor/README.md
new file mode 100644
index 000000000..196093ae1
--- /dev/null
+++ b/platform/features/conductor/README.md
@@ -0,0 +1,9 @@
+Provides the time conductor, a control which appears at the
+bottom of the screen allowing telemetry start and end times
+to be modified.
+
+Note that the term "time controller" is generally preferred
+outside of the code base (e.g. in UI documents, issues, etc.);
+the term "time conductor" is being used in code to avoid
+confusion with "controllers" in the Model-View-Controller
+sense.
diff --git a/platform/features/conductor/bundle.json b/platform/features/conductor/bundle.json
new file mode 100644
index 000000000..de903cfb9
--- /dev/null
+++ b/platform/features/conductor/bundle.json
@@ -0,0 +1,46 @@
+{
+ "extensions": {
+ "representers": [
+ {
+ "implementation": "ConductorRepresenter.js",
+ "depends": [
+ "throttle",
+ "conductorService",
+ "$compile",
+ "views[]"
+ ]
+ }
+ ],
+ "components": [
+ {
+ "type": "decorator",
+ "provides": "telemetryService",
+ "implementation": "ConductorTelemetryDecorator.js",
+ "depends": [ "conductorService" ]
+ }
+ ],
+ "services": [
+ {
+ "key": "conductorService",
+ "implementation": "ConductorService.js",
+ "depends": [ "now", "TIME_CONDUCTOR_DOMAINS" ]
+ }
+ ],
+ "templates": [
+ {
+ "key": "time-conductor",
+ "templateUrl": "templates/time-conductor.html"
+ }
+ ],
+ "constants": [
+ {
+ "key": "TIME_CONDUCTOR_DOMAINS",
+ "value": [
+ { "key": "time", "name": "Time" },
+ { "key": "yesterday", "name": "Yesterday" }
+ ],
+ "comment": "Placeholder; to be replaced by inspection of available domains."
+ }
+ ]
+ }
+}
diff --git a/platform/features/conductor/res/templates/time-conductor.html b/platform/features/conductor/res/templates/time-conductor.html
new file mode 100644
index 000000000..4126652d5
--- /dev/null
+++ b/platform/features/conductor/res/templates/time-conductor.html
@@ -0,0 +1,10 @@
+<mct-include key="'time-controller'"
+ ng-model='ngModel.conductor'>
+</mct-include>
+<mct-control key="'select'"
+ ng-model='ngModel'
+ field="'domain'"
+ options="ngModel.options"
+ style="position: absolute; right: 0px; bottom: 46px;"
+ >
+</mct-control>
diff --git a/platform/features/conductor/src/ConductorRepresenter.js b/platform/features/conductor/src/ConductorRepresenter.js
new file mode 100644
index 000000000..0d9314ed2
--- /dev/null
+++ b/platform/features/conductor/src/ConductorRepresenter.js
@@ -0,0 +1,201 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ [],
+ function () {
+ "use strict";
+
+ var TEMPLATE = [
+ "<mct-include key=\"'time-conductor'\" ng-model='ngModel' class='l-time-controller'>",
+ "</mct-include>"
+ ].join(''),
+ THROTTLE_MS = 200,
+ GLOBAL_SHOWING = false;
+
+ /**
+ * The ConductorRepresenter attaches the universal time conductor
+ * to views.
+ *
+ * @implements {Representer}
+ * @constructor
+ * @memberof platform/features/conductor
+ * @param {Function} throttle a function used to reduce the frequency
+ * of function invocations
+ * @param {platform/features/conductor.ConductorService} conductorService
+ * service which provides the active time conductor
+ * @param $compile Angular's $compile
+ * @param {ViewDefinition[]} views all defined views
+ * @param {Scope} the scope of the representation
+ * @param element the jqLite-wrapped representation element
+ */
+ function ConductorRepresenter(
+ throttle,
+ conductorService,
+ $compile,
+ views,
+ scope,
+ element
+ ) {
+ this.throttle = throttle;
+ this.scope = scope;
+ this.conductorService = conductorService;
+ this.element = element;
+ this.views = views;
+ this.$compile = $compile;
+ }
+
+ // Update the time conductor from the scope
+ ConductorRepresenter.prototype.wireScope = function () {
+ var conductor = this.conductorService.getConductor(),
+ conductorScope = this.conductorScope(),
+ repScope = this.scope,
+ lastObservedBounds,
+ broadcastBounds;
+
+ // Combine start/end times into a single object
+ function bounds(start, end) {
+ return {
+ start: conductor.displayStart(),
+ end: conductor.displayEnd(),
+ domain: conductor.domain()
+ };
+ }
+
+ function boundsAreStable(newlyObservedBounds) {
+ return !lastObservedBounds ||
+ (lastObservedBounds.start === newlyObservedBounds.start &&
+ lastObservedBounds.end === newlyObservedBounds.end);
+ }
+
+ function updateConductorInner() {
+ var innerBounds = conductorScope.ngModel.conductor.inner;
+ conductor.displayStart(innerBounds.start);
+ conductor.displayEnd(innerBounds.end);
+ lastObservedBounds = lastObservedBounds || bounds();
+ broadcastBounds();
+ }
+
+ function updateDomain(value) {
+ conductor.domain(value);
+ repScope.$broadcast('telemetry:display:bounds', bounds(
+ conductor.displayStart(),
+ conductor.displayEnd(),
+ conductor.domain()
+ ));
+ }
+
+ // telemetry domain metadata -> option for a select control
+ function makeOption(domainOption) {
+ return {
+ name: domainOption.name,
+ value: domainOption.key
+ };
+ }
+
+ broadcastBounds = this.throttle(function () {
+ var newlyObservedBounds = bounds();
+
+ if (boundsAreStable(newlyObservedBounds)) {
+ repScope.$broadcast('telemetry:display:bounds', bounds());
+ lastObservedBounds = undefined;
+ } else {
+ lastObservedBounds = newlyObservedBounds;
+ broadcastBounds();
+ }
+ }, THROTTLE_MS);
+
+ conductorScope.ngModel = {};
+ conductorScope.ngModel.conductor =
+ { outer: bounds(), inner: bounds() };
+ conductorScope.ngModel.options =
+ conductor.domainOptions().map(makeOption);
+ conductorScope.ngModel.domain = conductor.domain();
+
+ conductorScope
+ .$watch('ngModel.conductor.inner.start', updateConductorInner);
+ conductorScope
+ .$watch('ngModel.conductor.inner.end', updateConductorInner);
+ conductorScope
+ .$watch('ngModel.domain', updateDomain);
+
+ repScope.$on('telemetry:view', updateConductorInner);
+ };
+
+ ConductorRepresenter.prototype.conductorScope = function (s) {
+ return (this.cScope = arguments.length > 0 ? s : this.cScope);
+ };
+
+ // Handle a specific representation of a specific domain object
+ ConductorRepresenter.prototype.represent = function represent(representation, representedObject) {
+ this.destroy();
+
+ if (this.views.indexOf(representation) !== -1 && !GLOBAL_SHOWING) {
+ // Track original states
+ this.originalHeight = this.element.css('height');
+ this.hadAbs = this.element.hasClass('abs');
+
+ // Create a new scope for the conductor
+ this.conductorScope(this.scope.$new());
+ this.wireScope();
+ this.conductorElement =
+ this.$compile(TEMPLATE)(this.conductorScope());
+ this.element.after(this.conductorElement[0]);
+ this.element.addClass('l-controls-visible l-time-controller-visible');
+ GLOBAL_SHOWING = true;
+ }
+ };
+
+ // Respond to the destruction of the current representation.
+ ConductorRepresenter.prototype.destroy = function destroy() {
+ // We may not have decided to show in the first place,
+ // so circumvent any unnecessary cleanup
+ if (!this.conductorElement) {
+ return;
+ }
+
+ // Restore the original size of the mct-representation
+ if (!this.hadAbs) {
+ this.element.removeClass('abs');
+ }
+ this.element.css('height', this.originalHeight);
+
+ // ...and remove the conductor
+ if (this.conductorElement) {
+ this.conductorElement.remove();
+ this.conductorElement = undefined;
+ }
+
+ // Finally, destroy its scope
+ if (this.conductorScope()) {
+ this.conductorScope().$destroy();
+ this.conductorScope(undefined);
+ }
+
+ GLOBAL_SHOWING = false;
+ };
+
+ return ConductorRepresenter;
+ }
+);
+
diff --git a/platform/features/conductor/src/ConductorService.js b/platform/features/conductor/src/ConductorService.js
new file mode 100644
index 000000000..3e281d2c1
--- /dev/null
+++ b/platform/features/conductor/src/ConductorService.js
@@ -0,0 +1,64 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ ['./TimeConductor'],
+ function (TimeConductor) {
+ 'use strict';
+
+ var ONE_DAY_IN_MS = 1000 * 60 * 60 * 24,
+ SIX_HOURS_IN_MS = ONE_DAY_IN_MS / 4;
+
+ /**
+ * Provides a single global instance of the time conductor, which
+ * controls both query ranges and displayed ranges for telemetry
+ * data.
+ *
+ * @constructor
+ * @memberof platform/features/conductor
+ * @param {Function} now a function which returns the current time
+ * as a UNIX timestamp, in milliseconds
+ */
+ function ConductorService(now, domains) {
+ var initialEnd =
+ Math.ceil(now() / SIX_HOURS_IN_MS) * SIX_HOURS_IN_MS;
+
+ this.conductor = new TimeConductor(
+ initialEnd - ONE_DAY_IN_MS,
+ initialEnd,
+ domains
+ );
+ }
+
+ /**
+ * Get the global instance of the time conductor.
+ * @returns {platform/features/conductor.TimeConductor} the
+ * time conductor
+ */
+ ConductorService.prototype.getConductor = function () {
+ return this.conductor;
+ };
+
+ return ConductorService;
+ }
+);
diff --git a/platform/features/conductor/src/ConductorTelemetryDecorator.js b/platform/features/conductor/src/ConductorTelemetryDecorator.js
new file mode 100644
index 000000000..ab2d958d7
--- /dev/null
+++ b/platform/features/conductor/src/ConductorTelemetryDecorator.js
@@ -0,0 +1,76 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+define(
+ function () {
+ 'use strict';
+
+ /**
+ * Decorates the `telemetryService` such that requests are
+ * mediated by the time conductor.
+ *
+ * @constructor
+ * @memberof platform/features/conductor
+ * @implements {TelemetryService}
+ * @param {platform/features/conductor.ConductorService} conductorServe
+ * the service which exposes the global time conductor
+ * @param {TelemetryService} telemetryService the decorated service
+ */
+ function ConductorTelemetryDecorator(conductorService, telemetryService) {
+ this.conductorService = conductorService;
+ this.telemetryService = telemetryService;
+ }
+
+ ConductorTelemetryDecorator.prototype.amendRequests = function (requests) {
+ var conductor = this.conductorService.getConductor(),
+ start = conductor.displayStart(),
+ end = conductor.displayEnd(),
+ domain = conductor.domain();
+
+ function amendRequest(request) {
+ request = request || {};
+ request.start = start;
+ request.end = end;
+ request.domain = domain;
+ return request;
+ }
+
+ return (requests || []).map(amendRequest);
+ };
+
+ ConductorTelemetryDecorator.prototype.requestTelemetry = function (requests) {
+ var self = this;
+ return this.telemetryService
+ .requestTelemetry(this.amendRequests(requests));
+ };
+
+ ConductorTelemetryDecorator.prototype.subscribe = function (callback, requests) {
+ var self = this;
+
+ return this.telemetryService
+ .subscribe(callback, this.amendRequests(requests));
+ };
+
+ return ConductorTelemetryDecorator;
+ }
+);
diff --git a/platform/features/conductor/src/TimeConductor.js b/platform/features/conductor/src/TimeConductor.js
new file mode 100644
index 000000000..0fa0403fd
--- /dev/null
+++ b/platform/features/conductor/src/TimeConductor.js
@@ -0,0 +1,103 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define*/
+
+/**
+ * The time conductor bundle adds a global control to the bottom of the
+ * outermost viewing area. This controls both the range for time-based
+ * queries and for time-based displays.
+ *
+ * @namespace platform/features/conductor
+ */
+define(
+ function () {
+ 'use strict';
+
+ /**
+ * Tracks the current state of the time conductor.
+ *
+ * @memberof platform/features/conductor
+ * @constructor
+ * @param {number} start the initial start time
+ * @param {number} end the initial end time
+ */
+ function TimeConductor(start, end, domains) {
+ this.range = { start: start, end: end };
+ this.domains = domains;
+ this.activeDomain = domains[0].key;
+ }
+
+ /**
+ * Get or set (if called with an argument) the start time for displays.
+ * @param {number} [value] the start time to set
+ * @returns {number} the start time
+ */
+ TimeConductor.prototype.displayStart = function (value) {
+ if (arguments.length > 0) {
+ this.range.start = value;
+ }
+ return this.range.start;
+ };
+
+ /**
+ * Get or set (if called with an argument) the end time for displays.
+ * @param {number} [value] the end time to set
+ * @returns {number} the end time
+ */
+ TimeConductor.prototype.displayEnd = function (value) {
+ if (arguments.length > 0) {
+ this.range.end = value;
+ }
+ return this.range.end;
+ };
+
+ /**
+ * Get available domain options which can be used to bound time
+ * selection.
+ * @returns {TelemetryDomain[]} available domains
+ */
+ TimeConductor.prototype.domainOptions = function () {
+ return this.domains;
+ };
+
+ /**
+ * Get or set (if called with an argument) the active domain.
+ * @param {string} [key] the key identifying the domain choice
+ * @returns {TelemetryDomain} the active telemetry domain
+ */
+ TimeConductor.prototype.domain = function (key) {
+ function matchesKey(domain) {
+ return domain.key === key;
+ }
+
+ if (arguments.length > 0) {
+ if (!this.domains.some(matchesKey)) {
+ throw new Error("Unknown domain " + key);
+ }
+ this.activeDomain = key;
+ }
+ return this.activeDomain;
+ };
+
+ return TimeConductor;
+ }
+);
diff --git a/platform/features/conductor/test/ConductorRepresenterSpec.js b/platform/features/conductor/test/ConductorRepresenterSpec.js
new file mode 100644
index 000000000..5d78c8a72
--- /dev/null
+++ b/platform/features/conductor/test/ConductorRepresenterSpec.js
@@ -0,0 +1,259 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,afterEach,jasmine*/
+
+define(
+ ["../src/ConductorRepresenter", "./TestTimeConductor"],
+ function (ConductorRepresenter, TestTimeConductor) {
+ "use strict";
+
+ var SCOPE_METHODS = [
+ '$on',
+ '$watch',
+ '$broadcast',
+ '$emit',
+ '$new',
+ '$destroy'
+ ],
+ ELEMENT_METHODS = [
+ 'hasClass',
+ 'addClass',
+ 'removeClass',
+ 'css',
+ 'after',
+ 'remove'
+ ];
+
+ describe("ConductorRepresenter", function () {
+ var mockThrottle,
+ mockConductorService,
+ mockCompile,
+ testViews,
+ mockScope,
+ mockElement,
+ mockConductor,
+ mockCompiledTemplate,
+ mockNewScope,
+ mockNewElement,
+ representer;
+
+ function fireWatch(scope, watch, value) {
+ scope.$watch.calls.forEach(function (call) {
+ if (call.args[0] === watch) {
+ call.args[1](value);
+ }
+ });
+ }
+
+ beforeEach(function () {
+ mockThrottle = jasmine.createSpy('throttle');
+ mockConductorService = jasmine.createSpyObj(
+ 'conductorService',
+ ['getConductor']
+ );
+ mockCompile = jasmine.createSpy('$compile');
+ testViews = [ { someKey: "some value" } ];
+ mockScope = jasmine.createSpyObj('scope', SCOPE_METHODS);
+ mockElement = jasmine.createSpyObj('element', ELEMENT_METHODS);
+ mockConductor = new TestTimeConductor();
+ mockCompiledTemplate = jasmine.createSpy('template');
+ mockNewScope = jasmine.createSpyObj('newScope', SCOPE_METHODS);
+ mockNewElement = jasmine.createSpyObj('newElement', ELEMENT_METHODS);
+ mockNewElement[0] = mockNewElement;
+
+ mockConductorService.getConductor.andReturn(mockConductor);
+ mockCompile.andReturn(mockCompiledTemplate);
+ mockCompiledTemplate.andReturn(mockNewElement);
+ mockScope.$new.andReturn(mockNewScope);
+ mockThrottle.andCallFake(function (fn) {
+ return fn;
+ });
+
+ representer = new ConductorRepresenter(
+ mockThrottle,
+ mockConductorService,
+ mockCompile,
+ testViews,
+ mockScope,
+ mockElement
+ );
+ });
+
+ afterEach(function () {
+ representer.destroy();
+ });
+
+ it("adds a conductor to views", function () {
+ representer.represent(testViews[0], {});
+ expect(mockElement.after).toHaveBeenCalledWith(mockNewElement);
+ });
+
+ it("adds nothing to non-view representations", function () {
+ representer.represent({ someKey: "something else" }, {});
+ expect(mockElement.after).not.toHaveBeenCalled();
+ });
+
+ it("removes the conductor when destroyed", function () {
+ representer.represent(testViews[0], {});
+ expect(mockNewElement.remove).not.toHaveBeenCalled();
+ representer.destroy();
+ expect(mockNewElement.remove).toHaveBeenCalled();
+ });
+
+ it("destroys any new scope created", function () {
+ representer.represent(testViews[0], {});
+ representer.destroy();
+ expect(mockNewScope.$destroy.calls.length)
+ .toEqual(mockScope.$new.calls.length);
+ });
+
+ it("exposes conductor state in scope", function () {
+ mockConductor.displayStart.andReturn(1977);
+ mockConductor.displayEnd.andReturn(1984);
+ mockConductor.domain.andReturn('d');
+ representer.represent(testViews[0], {});
+
+ expect(mockNewScope.ngModel.conductor).toEqual({
+ inner: { start: 1977, end: 1984, domain: 'd' },
+ outer: { start: 1977, end: 1984, domain: 'd' }
+ });
+ });
+
+ it("updates conductor state from scope", function () {
+ var testState = {
+ inner: { start: 42, end: 1984 },
+ outer: { start: -1977, end: 12321 }
+ };
+
+ representer.represent(testViews[0], {});
+
+ mockNewScope.ngModel.conductor = testState;
+
+ fireWatch(
+ mockNewScope,
+ 'ngModel.conductor.inner.start',
+ testState.inner.start
+ );
+ expect(mockConductor.displayStart).toHaveBeenCalledWith(42);
+
+ fireWatch(
+ mockNewScope,
+ 'ngModel.conductor.inner.end',
+ testState.inner.end
+ );
+ expect(mockConductor.displayEnd).toHaveBeenCalledWith(1984);
+ });
+
+ describe("when bounds are changing", function () {
+ var startWatch = "ngModel.conductor.inner.start",
+ endWatch = "ngModel.conductor.inner.end",
+ mockThrottledFn = jasmine.createSpy('throttledFn'),
+ testBounds;
+
+ function fireThrottledFn() {
+ mockThrottle.mostRecentCall.args[0]();
+ }
+
+ beforeEach(function () {
+ mockThrottle.andReturn(mockThrottledFn);
+ representer.represent(testViews[0], {});
+ testBounds = { start: 0, end: 1000 };
+ mockNewScope.ngModel.conductor.inner = testBounds;
+ mockConductor.displayStart.andCallFake(function () {
+ return testBounds.start;
+ });
+ mockConductor.displayEnd.andCallFake(function () {
+ return testBounds.end;
+ });
+ });
+
+ it("does not broadcast while bounds are changing", function () {
+ expect(mockScope.$broadcast).not.toHaveBeenCalled();
+ testBounds.start = 100;
+ fireWatch(mockNewScope, startWatch, testBounds.start);
+ testBounds.end = 500;
+ fireWatch(mockNewScope, endWatch, testBounds.end);
+ fireThrottledFn();
+ testBounds.start = 200;
+ fireWatch(mockNewScope, startWatch, testBounds.start);
+ testBounds.end = 400;
+ fireWatch(mockNewScope, endWatch, testBounds.end);
+ fireThrottledFn();
+ expect(mockScope.$broadcast).not.toHaveBeenCalled();
+ });
+
+ it("does broadcast when bounds have stabilized", function () {
+ expect(mockScope.$broadcast).not.toHaveBeenCalled();
+ testBounds.start = 100;
+ fireWatch(mockNewScope, startWatch, testBounds.start);
+ testBounds.end = 500;
+ fireWatch(mockNewScope, endWatch, testBounds.end);
+ fireThrottledFn();
+ fireWatch(mockNewScope, startWatch, testBounds.start);
+ fireWatch(mockNewScope, endWatch, testBounds.end);
+ fireThrottledFn();
+ expect(mockScope.$broadcast).toHaveBeenCalled();
+ });
+ });
+
+ it("exposes domain selection in scope", function () {
+ representer.represent(testViews[0], null);
+
+ expect(mockNewScope.ngModel.domain)
+ .toEqual(mockConductor.domain());
+ });
+
+ it("exposes domain options in scope", function () {
+ representer.represent(testViews[0], null);
+
+ mockConductor.domainOptions().forEach(function (option, i) {
+ expect(mockNewScope.ngModel.options[i].value)
+ .toEqual(option.key);
+ expect(mockNewScope.ngModel.options[i].name)
+ .toEqual(option.name);
+ });
+ });
+
+ it("updates domain selection from scope", function () {
+ var choice;
+ representer.represent(testViews[0], null);
+
+ // Choose a domain that isn't currently selected
+ mockNewScope.ngModel.options.forEach(function (option) {
+ if (option.value !== mockNewScope.ngModel.domain) {
+ choice = option.value;
+ }
+ });
+
+ expect(mockConductor.domain)
+ .not.toHaveBeenCalledWith(choice);
+
+ mockNewScope.ngModel.domain = choice;
+ fireWatch(mockNewScope, "ngModel.domain", choice);
+
+ expect(mockConductor.domain)
+ .toHaveBeenCalledWith(choice);
+ });
+
+ });
+ }
+);
diff --git a/platform/features/conductor/test/ConductorServiceSpec.js b/platform/features/conductor/test/ConductorServiceSpec.js
new file mode 100644
index 000000000..08080658a
--- /dev/null
+++ b/platform/features/conductor/test/ConductorServiceSpec.js
@@ -0,0 +1,58 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
+
+define(
+ ["../src/ConductorService"],
+ function (ConductorService) {
+ "use strict";
+
+ var TEST_NOW = 1020304050;
+
+ describe("ConductorService", function () {
+ var mockNow,
+ conductorService;
+
+ beforeEach(function () {
+ mockNow = jasmine.createSpy('now');
+ mockNow.andReturn(TEST_NOW);
+ conductorService = new ConductorService(mockNow, [
+ { key: "d1", name: "Domain #1" },
+ { key: "d2", name: "Domain #2" }
+ ]);
+ });
+
+ it("initializes a time conductor around the current time", function () {
+ var conductor = conductorService.getConductor();
+ expect(conductor.displayStart() <= TEST_NOW).toBeTruthy();
+ expect(conductor.displayEnd() >= TEST_NOW).toBeTruthy();
+ expect(conductor.displayEnd() > conductor.displayStart())
+ .toBeTruthy();
+ });
+
+ it("provides a single shared time conductor instance", function () {
+ expect(conductorService.getConductor())
+ .toBe(conductorService.getConductor());
+ });
+ });
+ }
+);
diff --git a/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js b/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js
new file mode 100644
index 000000000..6e768419c
--- /dev/null
+++ b/platform/features/conductor/test/ConductorTelemetryDecoratorSpec.js
@@ -0,0 +1,160 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
+
+
+define(
+ ["../src/ConductorTelemetryDecorator", "./TestTimeConductor"],
+ function (ConductorTelemetryDecorator, TestTimeConductor) {
+ "use strict";
+
+ describe("ConductorTelemetryDecorator", function () {
+ var mockTelemetryService,
+ mockConductorService,
+ mockConductor,
+ mockPromise,
+ mockSeries,
+ decorator;
+
+ function seriesIsInWindow(series) {
+ var i, v, inWindow = true;
+ for (i = 0; i < series.getPointCount(); i += 1) {
+ v = series.getDomainValue(i);
+ inWindow = inWindow && (v >= mockConductor.displayStart());
+ inWindow = inWindow && (v <= mockConductor.displayEnd());
+ }
+ return inWindow;
+ }
+
+ beforeEach(function () {
+ mockTelemetryService = jasmine.createSpyObj(
+ 'telemetryService',
+ [ 'requestTelemetry', 'subscribe' ]
+ );
+ mockConductorService = jasmine.createSpyObj(
+ 'conductorService',
+ ['getConductor']
+ );
+ mockConductor = new TestTimeConductor();
+ mockPromise = jasmine.createSpyObj(
+ 'promise',
+ ['then']
+ );
+ mockSeries = jasmine.createSpyObj(
+ 'series',
+ [ 'getPointCount', 'getDomainValue', 'getRangeValue' ]
+ );
+
+ mockTelemetryService.requestTelemetry.andReturn(mockPromise);
+ mockConductorService.getConductor.andReturn(mockConductor);
+
+ // Prepare test series; make sure it has a broad range of
+ // domain values, with at least some in the query-able range
+ mockSeries.getPointCount.andReturn(1000);
+ mockSeries.getDomainValue.andCallFake(function (i) {
+ var j = i - 500;
+ return j * j * j;
+ });
+
+ mockConductor.displayStart.andReturn(42);
+ mockConductor.displayEnd.andReturn(1977);
+ mockConductor.domain.andReturn("testDomain");
+
+ decorator = new ConductorTelemetryDecorator(
+ mockConductorService,
+ mockTelemetryService
+ );
+ });
+
+
+ describe("decorates historical requests", function () {
+ var request;
+
+ beforeEach(function () {
+ decorator.requestTelemetry([{ someKey: "some value" }]);
+ request = mockTelemetryService.requestTelemetry
+ .mostRecentCall.args[0][0];
+ });
+
+ it("with start times", function () {
+ expect(request.start).toEqual(mockConductor.displayStart());
+ });
+
+ it("with end times", function () {
+ expect(request.end).toEqual(mockConductor.displayEnd());
+ });
+
+ it("with domain selection", function () {
+ expect(request.domain).toEqual(mockConductor.domain());
+ });
+ });
+
+ describe("decorates subscription requests", function () {
+ var request;
+
+ beforeEach(function () {
+ var mockCallback = jasmine.createSpy('callback');
+ decorator.subscribe(mockCallback, [{ someKey: "some value" }]);
+ request = mockTelemetryService.subscribe
+ .mostRecentCall.args[1][0];
+ });
+
+ it("with start times", function () {
+ expect(request.start).toEqual(mockConductor.displayStart());
+ });
+
+ it("with end times", function () {
+ expect(request.end).toEqual(mockConductor.displayEnd());
+ });
+
+ it("with domain selection", function () {
+ expect(request.domain).toEqual(mockConductor.domain());
+ });
+ });
+
+ it("adds display start/end times & domain selection to historical requests", function () {
+ decorator.requestTelemetry([{ someKey: "some value" }]);
+ expect(mockTelemetryService.requestTelemetry)
+ .toHaveBeenCalledWith([{
+ someKey: "some value",
+ start: mockConductor.displayStart(),
+ end: mockConductor.displayEnd(),
+ domain: jasmine.any(String)
+ }]);
+ });
+
+ it("adds display start/end times & domain selection to subscription requests", function () {
+ var mockCallback = jasmine.createSpy('callback');
+ decorator.subscribe(mockCallback, [{ someKey: "some value" }]);
+ expect(mockTelemetryService.subscribe)
+ .toHaveBeenCalledWith(jasmine.any(Function), [{
+ someKey: "some value",
+ start: mockConductor.displayStart(),
+ end: mockConductor.displayEnd(),
+ domain: jasmine.any(String)
+ }]);
+ });
+
+
+ });
+ }
+);
diff --git a/platform/features/conductor/test/TestTimeConductor.js b/platform/features/conductor/test/TestTimeConductor.js
new file mode 100644
index 000000000..01fed0c8f
--- /dev/null
+++ b/platform/features/conductor/test/TestTimeConductor.js
@@ -0,0 +1,50 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,spyOn*/
+
+define(
+ ["../src/TimeConductor"],
+ function (TimeConductor) {
+ 'use strict';
+
+ function TestTimeConductor() {
+ var self = this;
+
+ TimeConductor.apply(this, [
+ 402514200000,
+ 444546000000,
+ [
+ { key: "domain0", name: "Domain #1" },
+ { key: "domain1", name: "Domain #2" }
+ ]
+ ]);
+
+ Object.keys(TimeConductor.prototype).forEach(function (method) {
+ spyOn(self, method).andCallThrough();
+ });
+ }
+
+ TestTimeConductor.prototype = TimeConductor.prototype;
+
+ return TestTimeConductor;
+ }
+);
diff --git a/platform/features/conductor/test/TimeConductorSpec.js b/platform/features/conductor/test/TimeConductorSpec.js
new file mode 100644
index 000000000..c9336a93b
--- /dev/null
+++ b/platform/features/conductor/test/TimeConductorSpec.js
@@ -0,0 +1,78 @@
+/*****************************************************************************
+ * Open MCT Web, Copyright (c) 2014-2015, United States Government
+ * as represented by the Administrator of the National Aeronautics and Space
+ * Administration. All rights reserved.
+ *
+ * Open MCT Web is licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ * Open MCT Web includes source code licensed under additional open source
+ * licenses. See the Open Source Licenses file (LICENSES.md) included with
+ * this source code distribution or the Licensing information page available
+ * at runtime from the About dialog for additional information.
+ *****************************************************************************/
+/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/
+
+define(
+ ["../src/TimeConductor"],
+ function (TimeConductor) {
+ "use strict";
+
+ describe("TimeConductor", function () {
+ var testStart,
+ testEnd,
+ testDomains,
+ conductor;
+
+ beforeEach(function () {
+ testStart = 42;
+ testEnd = 12321;
+ testDomains = [
+ { key: "d1", name: "Domain #1" },
+ { key: "d2", name: "Domain #2" }
+ ];
+ conductor = new TimeConductor(testStart, testEnd, testDomains);
+ });
+
+ it("provides accessors for query/display start/end times", function () {
+ expect(conductor.displayStart()).toEqual(testStart);
+ expect(conductor.displayEnd()).toEqual(testEnd);
+ });
+
+ it("provides setters for query/display start/end times", function () {
+ expect(conductor.displayStart(3)).toEqual(3);
+ expect(conductor.displayEnd(4)).toEqual(4);
+ expect(conductor.displayStart()).toEqual(3);
+ expect(conductor.displayEnd()).toEqual(4);
+ });
+
+ it("exposes domain options", function () {
+ expect(conductor.domainOptions()).toEqual(testDomains);
+ });
+
+ it("exposes the current domain choice", function () {
+ expect(conductor.domain()).toEqual(testDomains[0].key);
+ });
+
+ it("allows the domain choice to be changed", function () {
+ conductor.domain(testDomains[1].key);
+ expect(conductor.domain()).toEqual(testDomains[1].key);
+ });
+
+ it("throws an error on attempts to set an invalid domain", function () {
+ expect(function () {
+ conductor.domain("invalid-domain");
+ }).toThrow();
+ });
+
+ });
+ }
+);
diff --git a/platform/features/conductor/test/suite.json b/platform/features/conductor/test/suite.json
new file mode 100644
index 000000000..9343b8e42
--- /dev/null
+++ b/platform/features/conductor/test/suite.json
@@ -0,0 +1,6 @@
+[
+ "ConductorRepresenter",
+ "ConductorService",
+ "ConductorTelemetryDecorator",
+ "TimeConductor"
+]
diff --git a/platform/features/layout/bundle.json b/platform/features/layout/bundle.json
index c32ac5eee..df5bd9413 100644
--- a/platform/features/layout/bundle.json
+++ b/platform/features/layout/bundle.json
@@ -167,8 +167,9 @@
"$scope",
"$q",
"dialogService",
- "telemetrySubscriber",
- "telemetryFormatter"
+ "telemetryHandler",
+ "telemetryFormatter",
+ "throttle"
]
}
],
diff --git a/platform/features/layout/res/templates/layout.html b/platform/features/layout/res/templates/layout.html
index ca3a359db..1811f3523 100644
--- a/platform/features/layout/res/templates/layout.html
+++ b/platform/features/layout/res/templates/layout.html
@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
-<div style="width: 100%; height: 100%;"
+<div class="l-layout"
ng-controller="LayoutController as controller">
<div class='frame child-frame panel abs'
diff --git a/platform/features/layout/src/FixedController.js b/platform/features/layout/src/FixedController.js
index 2bece3539..77c655e2c 100644
--- a/platform/features/layout/src/FixedController.js
+++ b/platform/features/layout/src/FixedController.js
@@ -38,12 +38,13 @@ define(
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
- function FixedController($scope, $q, dialogService, telemetrySubscriber, telemetryFormatter) {
+ function FixedController($scope, $q, dialogService, telemetryHandler, telemetryFormatter, throttle) {
var self = this,
- subscription,
+ handle,
names = {}, // Cache names by ID
values = {}, // Cache values by ID
- elementProxiesById = {};
+ elementProxiesById = {},
+ maxDomainValue = Number.POSITIVE_INFINITY;
// Convert from element x/y/width/height to an
// appropriate ng-style argument, to position elements.
@@ -81,25 +82,52 @@ define(
return element.handles().map(generateDragHandle);
}
+ // Update the value displayed in elements of this telemetry object
+ function setDisplayedValue(telemetryObject, value, alarm) {
+ var id = telemetryObject.getId();
+ (elementProxiesById[id] || []).forEach(function (element) {
+ names[id] = telemetryObject.getModel().name;
+ values[id] = telemetryFormatter.formatRangeValue(value);
+ element.name = names[id];
+ element.value = values[id];
+ element.cssClass = alarm && alarm.cssClass;
+ });
+ }
+
+ // Update the displayed value for this object, from a specific
+ // telemetry series
+ function updateValueFromSeries(telemetryObject, telemetrySeries) {
+ var index = telemetrySeries.getPointCount() - 1,
+ limit = telemetryObject &&
+ telemetryObject.getCapability('limit'),
+ datum = telemetryObject && handle.getDatum(
+ telemetryObject,
+ index
+ );
+
+ if (index >= 0) {
+ setDisplayedValue(
+ telemetryObject,
+ telemetrySeries.getRangeValue(index),
+ limit && datum && limit.evaluate(datum)
+ );
+ }
+ }
+
// Update the displayed value for this object
function updateValue(telemetryObject) {
- var id = telemetryObject && telemetryObject.getId(),
- limit = telemetryObject &&
+ var limit = telemetryObject &&
telemetryObject.getCapability('limit'),
datum = telemetryObject &&
- subscription.getDatum(telemetryObject),
- alarm = limit && datum && limit.evaluate(datum);
-
- if (id) {
- (elementProxiesById[id] || []).forEach(function (element) {
- names[id] = telemetryObject.getModel().name;
- values[id] = telemetryFormatter.formatRangeValue(
- subscription.getRangeValue(telemetryObject)
- );
- element.name = names[id];
- element.value = values[id];
- element.cssClass = alarm && alarm.cssClass;
- });
+ handle.getDatum(telemetryObject);
+
+ if (telemetryObject &&
+ (handle.getDomainValue(telemetryObject) < maxDomainValue)) {
+ setDisplayedValue(
+ telemetryObject,
+ handle.getRangeValue(telemetryObject),
+ limit && datum && limit.evaluate(datum)
+ );
}
}
@@ -115,8 +143,8 @@ define(
// Update telemetry values based on new data available
function updateValues() {
- if (subscription) {
- subscription.getTelemetryObjects().forEach(updateValue);
+ if (handle) {
+ handle.getTelemetryObjects().forEach(updateValue);
}
}
@@ -178,22 +206,29 @@ define(
// Free up subscription to telemetry
function releaseSubscription() {
- if (subscription) {
- subscription.unsubscribe();
- subscription = undefined;
+ if (handle) {
+ handle.unsubscribe();
+ handle = undefined;
}
}
// Subscribe to telemetry updates for this domain object
function subscribe(domainObject) {
// Release existing subscription (if any)
- if (subscription) {
- subscription.unsubscribe();
+ if (handle) {
+ handle.unsubscribe();
}
// Make a new subscription
- subscription = domainObject &&
- telemetrySubscriber.subscribe(domainObject, updateValues);
+ handle = domainObject && telemetryHandler.handle(
+ domainObject,
+ updateValues
+ );
+ // Request an initial historical telemetry value
+ handle.request(
+ { size: 1 }, // Only need a single data point
+ updateValueFromSeries
+ );
}
// Handle changes in the object's composition
@@ -204,6 +239,17 @@ define(
subscribe($scope.domainObject);
}
+ // Trigger a new query for telemetry data
+ function updateDisplayBounds(event, bounds) {
+ maxDomainValue = bounds.end;
+ if (handle) {
+ handle.request(
+ { size: 1 }, // Only need a single data point
+ updateValueFromSeries
+ );
+ }
+ }
+
// Add an element to this view
function addElement(element) {
// Ensure that configuration field is populated
@@ -278,6 +324,9 @@ define(
// Position panes where they are dropped
$scope.$on("mctDrop", handleDrop);
+
+ // Respond to external bounds changes
+ $scope.$on("telemetry:display:bounds", updateDisplayBounds);
}
/**
diff --git a/platform/features/layout/test/FixedControllerSpec.js b/platform/features/layout/test/FixedControllerSpec.js
index b95bbc9eb..31d5e0665 100644
--- a/platform/features/layout/test/FixedControllerSpec.js
+++ b/platform/features/layout/test/FixedControllerSpec.js
@@ -30,10 +30,10 @@ define(
var mockScope,
mockQ,
mockDialogService,
- mockSubscriber,
+ mockHandler,
mockFormatter,
mockDomainObject,
- mockSubscription,
+ mockHandle,
mockEvent,
testGrid,
testModel,
@@ -78,9 +78,9 @@ define(
'$scope',
[ "$on", "$watch", "commit" ]
);
- mockSubscriber = jasmine.createSpyObj(
- 'telemetrySubscriber',
- [ 'subscribe' ]
+ mockHandler = jasmine.createSpyObj(
+ 'telemetryHandler',
+ [ 'handle' ]
);
mockQ = jasmine.createSpyObj('$q', ['when']);
mockDialogService = jasmine.createSpyObj(
@@ -95,9 +95,16 @@ define(
'domainObject',
[ 'getId', 'getModel', 'getCapability' ]
);
- mockSubscription = jasmine.createSpyObj(
+ mockHandle = jasmine.createSpyObj(
'subscription',
- [ 'unsubscribe', 'getTelemetryObjects', 'getRangeValue', 'getDatum' ]
+ [
+ 'unsubscribe',
+ 'getDomainValue',
+ 'getTelemetryObjects',
+ 'getRangeValue',
+ 'getDatum',
+ 'request'
+ ]
);
mockEvent = jasmine.createSpyObj(
'event',
@@ -116,13 +123,14 @@ define(
{ type: "fixed.telemetry", id: 'c', x: 1, y: 1 }
]};
- mockSubscriber.subscribe.andReturn(mockSubscription);
- mockSubscription.getTelemetryObjects.andReturn(
+ mockHandler.handle.andReturn(mockHandle);
+ mockHandle.getTelemetryObjects.andReturn(
testModel.composition.map(makeMockDomainObject)
);
- mockSubscription.getRangeValue.andCallFake(function (o) {
+ mockHandle.getRangeValue.andCallFake(function (o) {
return testValues[o.getId()];
});
+ mockHandle.getDomainValue.andReturn(12321);
mockFormatter.formatRangeValue.andCallFake(function (v) {
return "Formatted " + v;
});
@@ -137,7 +145,7 @@ define(
mockScope,
mockQ,
mockDialogService,
- mockSubscriber,
+ mockHandler,
mockFormatter
);
});
@@ -145,7 +153,7 @@ define(
it("subscribes when a domain object is available", function () {
mockScope.domainObject = mockDomainObject;
findWatch("domainObject")(mockDomainObject);
- expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
+ expect(mockHandler.handle).toHaveBeenCalledWith(
mockDomainObject,
jasmine.any(Function)
);
@@ -156,13 +164,13 @@ define(
// First pass - should simply should subscribe
findWatch("domainObject")(mockDomainObject);
- expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
- expect(mockSubscriber.subscribe.calls.length).toEqual(1);
+ expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
+ expect(mockHandler.handle.calls.length).toEqual(1);
// Object changes - should unsubscribe then resubscribe
findWatch("domainObject")(mockDomainObject);
- expect(mockSubscription.unsubscribe).toHaveBeenCalled();
- expect(mockSubscriber.subscribe.calls.length).toEqual(2);
+ expect(mockHandle.unsubscribe).toHaveBeenCalled();
+ expect(mockHandler.handle.calls.length).toEqual(2);
});
it("exposes visible elements based on configuration", function () {
@@ -255,7 +263,7 @@ define(
findWatch("model.composition")(mockScope.model.composition);
// Invoke the subscription callback
- mockSubscriber.subscribe.mostRecentCall.args[1]();
+ mockHandler.handle.mostRecentCall.args[1]();
// Get elements that controller is now exposing
elements = controller.getElements();
@@ -333,11 +341,11 @@ define(
// Make an object available
findWatch('domainObject')(mockDomainObject);
// Also verify precondition
- expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
+ expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
// Destroy the scope
findOn('$destroy')();
// Should have unsubscribed
- expect(mockSubscription.unsubscribe).toHaveBeenCalled();
+ expect(mockHandle.unsubscribe).toHaveBeenCalled();
});
it("exposes its grid size", function () {
diff --git a/platform/features/pages/res/iframe.html b/platform/features/pages/res/iframe.html
index a1a1c5cca..ba4fa67c7 100644
--- a/platform/features/pages/res/iframe.html
+++ b/platform/features/pages/res/iframe.html
@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
-<div class="abs l-iframe">
+<div class="l-iframe abs">
<iframe ng-controller="EmbeddedPageController as ctl"
ng-src="{{ctl.trust(model.url)}}">
</iframe>
diff --git a/platform/features/plot/res/templates/plot.html b/platform/features/plot/res/templates/plot.html
index 3a689d460..0a17de15c 100644
--- a/platform/features/plot/res/templates/plot.html
+++ b/platform/features/plot/res/templates/plot.html
@@ -119,7 +119,7 @@
<span class="ui-symbol icon">I</span>
</a>
- <div class="menu-element s-menu menus-to-left"
+ <div class="menu-element s-menu-btn menus-to-left"
ng-if="plot.getModeOptions().length > 1"
ng-controller="ClickAwayController as toggle">
diff --git a/platform/features/plot/src/PlotController.js b/platform/features/plot/src/PlotController.js
index a54fff83d..19aee9ca1 100644
--- a/platform/features/plot/src/PlotController.js
+++ b/platform/features/plot/src/PlotController.js
@@ -65,6 +65,7 @@ define(
subPlotFactory = new SubPlotFactory(telemetryFormatter),
cachedObjects = [],
updater,
+ lastBounds,
handle;
// Populate the scope with axis information (specifically, options
@@ -94,6 +95,17 @@ define(
}
}
+ // Change the displayable bounds
+ function setBasePanZoom(bounds) {
+ var start = bounds.start,
+ end = bounds.end;
+ if (updater) {
+ updater.setDomainBounds(start, end);
+ self.update();
+ }
+ lastBounds = bounds;
+ }
+
// Reinstantiate the plot updater (e.g. because we have a
// new subscription.) This will clear the plot.
function recreateUpdater() {
@@ -107,10 +119,15 @@ define(
handle,
($scope.axes[1].active || {}).key
);
+ // Keep any externally-provided bounds
+ if (lastBounds) {
+ setBasePanZoom(lastBounds);
+ }
}
// Handle new telemetry data in this plot
function updateValues() {
+ self.pending = false;
if (handle) {
setupModes(handle.getTelemetryObjects());
}
@@ -126,6 +143,7 @@ define(
// Display new historical data as it becomes available
function addHistoricalData(domainObject, series) {
+ self.pending = false;
updater.addHistorical(domainObject, series);
self.modeOptions.getModeHandler().plotTelemetry(updater);
self.update();
@@ -165,6 +183,14 @@ define(
}
}
+ // Respond to a display bounds change (requery for data)
+ function changeDisplayBounds(event, bounds) {
+ self.pending = true;
+ releaseSubscription();
+ subscribe($scope.domainObject);
+ setBasePanZoom(bounds);
+ }
+
this.modeOptions = new PlotModeOptions([], subPlotFactory);
this.updateValues = updateValues;
@@ -174,12 +200,19 @@ define(
.forEach(updateSubplot);
});
+ self.pending = true;
+
// Subscribe to telemetry when a domain object becomes available
$scope.$watch('domainObject', subscribe);
+ // Respond to external bounds changes
+ $scope.$on("telemetry:display:bounds", changeDisplayBounds);
+
// Unsubscribe when the plot is destroyed
$scope.$on("$destroy", releaseSubscription);
+ // Notify any external observers that a new telemetry view is here
+ $scope.$emit("telemetry:view");
}
/**
@@ -275,7 +308,7 @@ define(
PlotController.prototype.isRequestPending = function () {
// Placeholder; this should reflect request state
// when requesting historical telemetry
- return false;
+ return this.pending;
};
return PlotController;
diff --git a/platform/features/plot/src/SubPlot.js b/platform/features/plot/src/SubPlot.js
index 06b7f7bb0..dfcaadf35 100644
--- a/platform/features/plot/src/SubPlot.js
+++ b/platform/features/plot/src/SubPlot.js
@@ -64,6 +64,16 @@ define(
this.updateTicks();
}
+ /**
+ * Tests whether this subplot has domain data to show for the current pan/zoom level. Absence of domain data
+ * implies that there is no range data displayed either
+ * @returns {boolean} true if domain data exists for the current pan/zoom level
+ */
+ SubPlot.prototype.hasDomainData = function() {
+ return this.panZoomStack
+ && this.panZoomStack.getDimensions()[0] > 0;
+ };
+
// Utility function for filtering out empty strings.
function isNonEmpty(v) {
return typeof v === 'string' && v !== "";
@@ -253,7 +263,10 @@ define(
this.hovering = true;
this.subPlotBounds = $event.target.getBoundingClientRect();
this.mousePosition = this.toMousePosition($event);
- this.updateHoverCoordinates();
+ //If there is a domain to display, show hover coordinates, otherwise hover coordinates are meaningless
+ if (this.hasDomainData()) {
+ this.updateHoverCoordinates();
+ }
if (this.marqueeStart) {
this.updateMarqueeBox();
}
diff --git a/platform/features/plot/src/elements/PlotPanZoomStackGroup.js b/platform/features/plot/src/elements/PlotPanZoomStackGroup.js
index 3746958ec..e1b61a06e 100644
--- a/platform/features/plot/src/elements/PlotPanZoomStackGroup.js
+++ b/platform/features/plot/src/elements/PlotPanZoomStackGroup.js
@@ -143,8 +143,7 @@ define(
PlotPanZoomStackGroup.prototype.getDepth = function () {
// All stacks are kept in sync, so look up depth
// from the first one.
- return this.stacks.length > 0 ?
- this.stacks[0].getDepth() : 0;
+ return this.stacks.length > 0 ? this.stacks[0].getDepth() : 0;
};
/**
diff --git a/platform/features/plot/src/elements/PlotTickGenerator.js b/platform/features/plot/src/elements/PlotTickGenerator.js
index af1805095..f759b6bcd 100644
--- a/platform/features/plot/src/elements/PlotTickGenerator.js
+++ b/platform/features/plot/src/elements/PlotTickGenerator.js
@@ -53,7 +53,8 @@ define(
for (i = 0; i < count; i += 1) {
result.push({
- label: format(i * step + start)
+ //If data to show, display label for each tick line, otherwise show lines but suppress labels.
+ label: span > 0 ? format(i * step + start) : ''
});
}
diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js
index 851fa5609..d4b4ad3ee 100644
--- a/platform/features/plot/src/elements/PlotUpdater.js
+++ b/platform/features/plot/src/elements/PlotUpdater.js
@@ -141,10 +141,10 @@ define(
PlotUpdater.prototype.initializeDomainOffset = function (values) {
this.domainOffset =
((this.domainOffset === undefined) && (values.length > 0)) ?
- (values.reduce(function (a, b) {
- return (a || 0) + (b || 0);
- }, 0) / values.length) :
- this.domainOffset;
+ (values.reduce(function (a, b) {
+ return (a || 0) + (b || 0);
+ }, 0) / values.length) :
+ this.domainOffset;
};
// Expand range slightly so points near edges are visible
@@ -159,7 +159,10 @@ define(
// Update dimensions and origin based on extrema of plots
PlotUpdater.prototype.updateBounds = function () {
- var bufferArray = this.bufferArray;
+ var bufferArray = this.bufferArray,
+ priorDomainOrigin = this.origin[0],
+ priorDomainDimensions = this.dimensions[0];
+
if (bufferArray.length > 0) {
this.domainExtrema = bufferArray.map(function (lineBuffer) {
return lineBuffer.getDomainExtrema();
@@ -178,6 +181,18 @@ define(
// Enforce some minimum visible area
this.expandRange();
+ // Suppress domain changes when pinned
+ if (this.hasSpecificDomainBounds) {
+ this.origin[0] = priorDomainOrigin;
+ this.dimensions[0] = priorDomainDimensions;
+ if (this.following) {
+ this.origin[0] = Math.max(
+ this.domainExtrema[1] - this.dimensions[0],
+ this.origin[0]
+ );
+ }
+ }
+
// ...then enforce a fixed duration if needed
if (this.fixedDuration !== undefined) {
this.origin[0] = this.origin[0] + this.dimensions[0] -
@@ -282,6 +297,21 @@ define(
};
/**
+ * Set the start and end boundaries (usually time) for the
+ * domain axis of this updater.
+ */
+ PlotUpdater.prototype.setDomainBounds = function (start, end) {
+ this.fixedDuration = end - start;
+ this.origin[0] = start;
+ this.dimensions[0] = this.fixedDuration;
+
+ // Suppress follow behavior if we have windowed in on the past
+ this.hasSpecificDomainBounds = true;
+ this.following =
+ !this.domainExtrema || (end >= this.domainExtrema[1]);
+ };
+
+ /**
* Fill in historical data.
*/
PlotUpdater.prototype.addHistorical = function (domainObject, series) {
diff --git a/platform/features/plot/test/PlotControllerSpec.js b/platform/features/plot/test/PlotControllerSpec.js
index e6c79b4e5..dcae17792 100644
--- a/platform/features/plot/test/PlotControllerSpec.js
+++ b/platform/features/plot/test/PlotControllerSpec.js
@@ -45,11 +45,19 @@ define(
};
}
+ function fireEvent(name, args) {
+ mockScope.$on.calls.forEach(function (call) {
+ if (call.args[0] === name) {
+ call.args[1].apply(null, args || []);
+ }
+ });
+ }
+
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
- [ "$watch", "$on" ]
+ [ "$watch", "$on", "$emit" ]
);
mockFormatter = jasmine.createSpyObj(
"formatter",
@@ -87,6 +95,7 @@ define(
mockHandle.getMetadata.andReturn([{}]);
mockHandle.getDomainValue.andReturn(123);
mockHandle.getRangeValue.andReturn(42);
+ mockScope.domainObject = mockDomainObject;
controller = new PlotController(
mockScope,
@@ -212,7 +221,12 @@ define(
});
it("indicates if a request is pending", function () {
- // Placeholder; need to support requesting telemetry
+ mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
+ expect(controller.isRequestPending()).toBeTruthy();
+ mockHandle.request.mostRecentCall.args[1](
+ mockDomainObject,
+ mockSeries
+ );
expect(controller.isRequestPending()).toBeFalsy();
});
@@ -233,10 +247,20 @@ define(
// Also verify precondition
expect(mockHandle.unsubscribe).not.toHaveBeenCalled();
// Destroy the scope
- mockScope.$on.mostRecentCall.args[1]();
+ fireEvent("$destroy");
// Should have unsubscribed
expect(mockHandle.unsubscribe).toHaveBeenCalled();
});
+
+ it("requeries when displayable bounds change", function () {
+ mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
+ expect(mockHandle.request.calls.length).toEqual(1);
+ fireEvent("telemetry:display:bounds", [
+ {},
+ { start: 10, end: 100 }
+ ]);
+ expect(mockHandle.request.calls.length).toEqual(2);
+ });
});
}
);
diff --git a/platform/features/plot/test/SubPlotSpec.js b/platform/features/plot/test/SubPlotSpec.js
index 58cd19faa..f7a7b667e 100644
--- a/platform/features/plot/test/SubPlotSpec.js
+++ b/platform/features/plot/test/SubPlotSpec.js
@@ -157,6 +157,15 @@ define(
);
});
+ it ("indicates when there is domain data shown", function () {
+ expect(subplot.hasDomainData()).toEqual(true);
+ });
+
+ it ("indicates when there is no domain data shown", function () {
+ mockPanZoomStack.getDimensions.andReturn([0,0]);
+ expect(subplot.hasDomainData()).toEqual(false);
+ });
+
it("disallows marquee zoom when start and end Marquee is at the same position", function () {
expect(mockPanZoomStack.pushPanZoom).not.toHaveBeenCalled();
diff --git a/platform/features/scrolling/src/RangeColumn.js b/platform/features/scrolling/src/RangeColumn.js
index 637a68517..0b76fbfda 100644
--- a/platform/features/scrolling/src/RangeColumn.js
+++ b/platform/features/scrolling/src/RangeColumn.js
@@ -55,7 +55,7 @@ define(
var range = this.rangeMetadata.key,
limit = domainObject.getCapability('limit'),
value = datum[range],
- alarm = limit.evaluate(datum, range);
+ alarm = limit && limit.evaluate(datum, range);
return {
cssClass: alarm && alarm.cssClass,
diff --git a/platform/forms/res/templates/controls/color.html b/platform/forms/res/templates/controls/color.html
index 903b30d06..178a3bfd7 100644
--- a/platform/forms/res/templates/controls/color.html
+++ b/platform/forms/res/templates/controls/color.html
@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<div
- class="t-btn l-btn s-btn s-icon-btn s-menu menu-element t-color-palette"
+ class="t-btn l-btn s-btn s-icon-btn s-menu-btn menu-element t-color-palette"
ng-controller="ClickAwayController as toggle"
>
diff --git a/platform/forms/res/templates/controls/menu-button.html b/platform/forms/res/templates/controls/menu-button.html
index eb517c45e..dc7ab9e14 100644
--- a/platform/forms/res/templates/controls/menu-button.html
+++ b/platform/forms/res/templates/controls/menu-button.html
@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
-<div class="s-menu menu-element"
+<div class="s-menu-btn menu-element"
ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span>
diff --git a/platform/persistence/elastic/bundle.json b/platform/persistence/elastic/bundle.json
index f78d8504f..3e1383351 100644
--- a/platform/persistence/elastic/bundle.json
+++ b/platform/persistence/elastic/bundle.json
@@ -13,7 +13,7 @@
"provides": "searchService",
"type": "provider",
"implementation": "ElasticSearchProvider.js",
- "depends": [ "$http", "objectService", "ELASTIC_ROOT" ]
+ "depends": [ "$http", "ELASTIC_ROOT" ]
}
],
"constants": [
diff --git a/platform/persistence/elastic/src/ElasticSearchProvider.js b/platform/persistence/elastic/src/ElasticSearchProvider.js
index 604e3d0ed..84290d999 100644
--- a/platform/persistence/elastic/src/ElasticSearchProvider.js
+++ b/platform/persistence/elastic/src/ElasticSearchProvider.js
@@ -24,190 +24,122 @@
/**
* Module defining ElasticSearchProvider. Created by shale on 07/16/2015.
*/
-define(
- [],
- function () {
- "use strict";
-
- // JSLint doesn't like underscore-prefixed properties,
- // so hide them here.
- var ID = "_id",
- SCORE = "_score",
- DEFAULT_MAX_RESULTS = 100;
-
- /**
- * A search service which searches through domain objects in
- * the filetree using ElasticSearch.
- *
- * @constructor
- * @param $http Angular's $http service, for working with urls.
- * @param {ObjectService} objectService the service from which
- * domain objects can be gotten.
- * @param ROOT the constant `ELASTIC_ROOT` which allows us to
- * interact with ElasticSearch.
- */
- function ElasticSearchProvider($http, objectService, ROOT) {
- this.$http = $http;
- this.objectService = objectService;
- this.root = ROOT;
- }
-
- /**
- * Searches through the filetree for domain objects using a search
- * term. This is done through querying elasticsearch. Returns a
- * promise for a result object that has the format
- * {hits: searchResult[], total: number, timedOut: boolean}
- * where a searchResult has the format
- * {id: string, object: domainObject, score: number}
- *
- * Notes:
- * * The order of the results is from highest to lowest score,
- * as elsaticsearch determines them to be.
- * * Uses the fuzziness operator to get more results.
- * * More about this search's behavior at
- * https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html
- *
- * @param searchTerm The text input that is the query.
- * @param timestamp The time at which this function was called.
- * This timestamp is used as a unique identifier for this
- * query and the corresponding results.
- * @param maxResults (optional) The maximum number of results
- * that this function should return.
- * @param timeout (optional) The time after which the search should
- * stop calculations and return partial results. Elasticsearch
- * does not guarentee that this timeout will be strictly followed.
- */
- ElasticSearchProvider.prototype.query = function query(searchTerm, timestamp, maxResults, timeout) {
- var $http = this.$http,
- objectService = this.objectService,
- root = this.root,
- esQuery;
-
- function addFuzziness(searchTerm, editDistance) {
- if (!editDistance) {
- editDistance = '';
- }
-
- return searchTerm.split(' ').map(function (s) {
- // Don't add fuzziness for quoted strings
- if (s.indexOf('"') !== -1) {
- return s;
- } else {
- return s + '~' + editDistance;
- }
- }).join(' ');
- }
-
- // Currently specific to elasticsearch
- function processSearchTerm(searchTerm) {
- var spaceIndex;
-
- // Cut out any extra spaces
- while (searchTerm.substr(0, 1) === ' ') {
- searchTerm = searchTerm.substring(1, searchTerm.length);
- }
- while (searchTerm.substr(searchTerm.length - 1, 1) === ' ') {
- searchTerm = searchTerm.substring(0, searchTerm.length - 1);
- }
- spaceIndex = searchTerm.indexOf(' ');
- while (spaceIndex !== -1) {
- searchTerm = searchTerm.substring(0, spaceIndex) +
- searchTerm.substring(spaceIndex + 1, searchTerm.length);
- spaceIndex = searchTerm.indexOf(' ');
- }
-
- // Add fuzziness for completeness
- searchTerm = addFuzziness(searchTerm);
-
- return searchTerm;
- }
-
- // Processes results from the format that elasticsearch returns to
- // a list of searchResult objects, then returns a result object
- // (See documentation for query for object descriptions)
- function processResults(rawResults, timestamp) {
- var results = rawResults.data.hits.hits,
- resultsLength = results.length,
- ids = [],
- scores = {},
- searchResults = [],
- i;
-
- // Get the result objects' IDs
- for (i = 0; i < resultsLength; i += 1) {
- ids.push(results[i][ID]);
- }
-
- // Get the result objects' scores
- for (i = 0; i < resultsLength; i += 1) {
- scores[ids[i]] = results[i][SCORE];
- }
-
- // Get the domain objects from their IDs
- return objectService.getObjects(ids).then(function (objects) {
- var j,
- id;
-
- for (j = 0; j < resultsLength; j += 1) {
- id = ids[j];
-
- // Include items we can get models for
- if (objects[id].getModel) {
- // Format the results as searchResult objects
- searchResults.push({
- id: id,
- object: objects[id],
- score: scores[id]
- });
- }
- }
-
- return {
- hits: searchResults,
- total: rawResults.data.hits.total,
- timedOut: rawResults.data.timed_out
- };
- });
- }
-
-
- // Check to see if the user provided a maximum
- // number of results to display
- if (!maxResults) {
- // Else, we provide a default value.
- maxResults = DEFAULT_MAX_RESULTS;
- }
-
- // If the user input is empty, we want to have no search results.
- if (searchTerm !== '' && searchTerm !== undefined) {
- // Process the search term
- searchTerm = processSearchTerm(searchTerm);
-
- // Create the query to elasticsearch
- esQuery = root + "/_search/?q=" + searchTerm +
- "&size=" + maxResults;
- if (timeout) {
- esQuery += "&timeout=" + timeout;
- }
-
- // Get the data...
- return this.$http({
- method: "GET",
- url: esQuery
- }).then(function (rawResults) {
- // ...then process the data
- return processResults(rawResults, timestamp);
- }, function (err) {
- // In case of error, return nothing. (To prevent
- // infinite loading time.)
- return {hits: [], total: 0};
- });
- } else {
- return {hits: [], total: 0};
- }
- };
+define([
+
+], function (
+
+) {
+ "use strict";
+
+ var ID_PROPERTY = '_id',
+ SOURCE_PROPERTY = '_source',
+ SCORE_PROPERTY = '_score';
+
+ /**
+ * A search service which searches through domain objects in
+ * the filetree using ElasticSearch.
+ *
+ * @constructor
+ * @param $http Angular's $http service, for working with urls.
+ * @param ROOT the constant `ELASTIC_ROOT` which allows us to
+ * interact with ElasticSearch.
+ */
+ function ElasticSearchProvider($http, ROOT) {
+ this.$http = $http;
+ this.root = ROOT;
+ }
+ /**
+ * Search for domain objects using elasticsearch as a search provider.
+ *
+ * @param {String} searchTerm the term to search by.
+ * @param {Number} [maxResults] the max numer of results to return.
+ * @returns {Promise} promise for a modelResults object.
+ */
+ ElasticSearchProvider.prototype.query = function (searchTerm, maxResults) {
+ var searchUrl = this.root + '/_search/',
+ params = {},
+ provider = this;
+
+ searchTerm = this.cleanTerm(searchTerm);
+ searchTerm = this.fuzzyMatchUnquotedTerms(searchTerm);
+
+ params.q = searchTerm;
+ params.size = maxResults;
+
+ return this
+ .$http({
+ method: "GET",
+ url: searchUrl,
+ params: params
+ })
+ .then(function success(succesResponse) {
+ return provider.parseResponse(succesResponse);
+ }, function error(errorResponse) {
+ // Gracefully fail.
+ return {
+ hits: [],
+ total: 0
+ };
+ });
+ };
+
+
+ /**
+ * Clean excess whitespace from a search term and return the cleaned
+ * version.
+ *
+ * @private
+ * @param {string} the search term to clean.
+ * @returns {string} search terms cleaned of excess whitespace.
+ */
+ ElasticSearchProvider.prototype.cleanTerm = function (term) {
+ return term.trim().replace(/ +/g, ' ');
+ };
+
+ /**
+ * Add fuzzy matching markup to search terms that are not quoted.
+ *
+ * The following:
+ * hello welcome "to quoted village" have fun
+ * will become
+ * hello~ welcome~ "to quoted village" have~ fun~
+ *
+ * @private
+ */
+ ElasticSearchProvider.prototype.fuzzyMatchUnquotedTerms = function (query) {
+ var matchUnquotedSpaces = '\\s+(?=([^"]*"[^"]*")*[^"]*$)',
+ matcher = new RegExp(matchUnquotedSpaces, 'g');
+
+ return query
+ .replace(matcher, '~ ')
+ .replace(/$/, '~')
+ .replace(/"~+/, '"');
+ };
+
+ /**
+ * Parse the response from ElasticSearch and convert it to a
+ * modelResults object.
+ *
+ * @private
+ * @param response a ES response object from $http
+ * @returns modelResults
+ */
+ ElasticSearchProvider.prototype.parseResponse = function (response) {
+ var results = response.data.hits.hits,
+ searchResults = results.map(function (result) {
+ return {
+ id: result[ID_PROPERTY],
+ model: result[SOURCE_PROPERTY],
+ score: result[SCORE_PROPERTY]
+ };
+ });
+
+ return {
+ hits: searchResults,
+ total: response.data.hits.total
+ };
+ };
- return ElasticSearchProvider;
- }
-); \ No newline at end of file
+ return ElasticSearchProvider;
+});
diff --git a/platform/persistence/elastic/test/ElasticSearchProviderSpec.js b/platform/persistence/elastic/test/ElasticSearchProviderSpec.js
index decb1a5c9..f8337e086 100644
--- a/platform/persistence/elastic/test/ElasticSearchProviderSpec.js
+++ b/platform/persistence/elastic/test/ElasticSearchProviderSpec.js
@@ -19,97 +19,151 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-/*global define,describe,it,expect,beforeEach,jasmine*/
+/*global define,describe,it,expect,beforeEach,jasmine,spyOn,Promise,waitsFor*/
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
-define(
- ["../src/ElasticSearchProvider"],
- function (ElasticSearchProvider) {
- "use strict";
-
- // JSLint doesn't like underscore-prefixed properties,
- // so hide them here.
- var ID = "_id",
- SCORE = "_score";
-
- describe("The ElasticSearch search provider ", function () {
- var mockHttp,
- mockHttpPromise,
- mockObjectPromise,
- mockObjectService,
- mockDomainObject,
- provider,
- mockProviderResults;
+define([
+ '../src/ElasticSearchProvider'
+], function (
+ ElasticSearchProvider
+) {
+ 'use strict';
+ describe('ElasticSearchProvider', function () {
+ var $http,
+ ROOT,
+ provider;
+
+ beforeEach(function () {
+ $http = jasmine.createSpy('$http');
+ ROOT = 'http://localhost:9200';
+
+ provider = new ElasticSearchProvider($http, ROOT);
+ });
+
+ describe('query', function () {
beforeEach(function () {
- mockHttp = jasmine.createSpy("$http");
- mockHttpPromise = jasmine.createSpyObj(
- "promise",
- [ "then" ]
- );
- mockHttp.andReturn(mockHttpPromise);
- // allow chaining of promise.then().catch();
- mockHttpPromise.then.andReturn(mockHttpPromise);
-
- mockObjectService = jasmine.createSpyObj(
- "objectService",
- [ "getObjects" ]
- );
- mockObjectPromise = jasmine.createSpyObj(
- "promise",
- [ "then" ]
- );
- mockObjectService.getObjects.andReturn(mockObjectPromise);
-
- mockDomainObject = jasmine.createSpyObj(
- "domainObject",
- [ "getId", "getModel" ]
- );
-
- provider = new ElasticSearchProvider(mockHttp, mockObjectService, "");
- provider.query(' test "query" ', 0, undefined, 1000);
+ spyOn(provider, 'cleanTerm').andReturn('cleanedTerm');
+ spyOn(provider, 'fuzzyMatchUnquotedTerms').andReturn('fuzzy');
+ spyOn(provider, 'parseResponse').andReturn('parsedResponse');
+ $http.andReturn(Promise.resolve({}));
});
-
- it("sends a query to ElasticSearch", function () {
- expect(mockHttp).toHaveBeenCalled();
+
+ it('cleans terms and adds fuzzyness', function () {
+ provider.query('hello', 10);
+ expect(provider.cleanTerm).toHaveBeenCalledWith('hello');
+ expect(provider.fuzzyMatchUnquotedTerms)
+ .toHaveBeenCalledWith('cleanedTerm');
});
-
- it("gets data from ElasticSearch", function () {
- var data = {
- hits: {
- hits: [
- {},
- {}
- ],
- total: 0
+
+ it('calls through to $http', function () {
+ provider.query('hello', 10);
+ expect($http).toHaveBeenCalledWith({
+ method: 'GET',
+ params: {
+ q: 'fuzzy',
+ size: 10
},
- timed_out: false
- };
- data.hits.hits[0][ID] = 1;
- data.hits.hits[0][SCORE] = 1;
- data.hits.hits[1][ID] = 2;
- data.hits.hits[1][SCORE] = 2;
-
- mockProviderResults = mockHttpPromise.then.mostRecentCall.args[0]({data: data});
-
- expect(
- mockObjectPromise.then.mostRecentCall.args[0]({
- 1: mockDomainObject,
- 2: mockDomainObject
- }).hits.length
- ).toEqual(2);
+ url: 'http://localhost:9200/_search/'
+ });
});
-
- it("returns nothing for an empty string query", function () {
- expect(provider.query("").hits).toEqual([]);
+
+ it('gracefully fails when http fails', function () {
+ var promiseChainResolved = false;
+ $http.andReturn(Promise.reject());
+
+ provider
+ .query('hello', 10)
+ .then(function (results) {
+ expect(results).toEqual({
+ hits: [],
+ total: 0
+ });
+ promiseChainResolved = true;
+ });
+
+ waitsFor(function () {
+ return promiseChainResolved;
+ });
});
-
- it("returns something when there is an ElasticSearch error", function () {
- mockProviderResults = mockHttpPromise.then.mostRecentCall.args[1]();
- expect(mockProviderResults).toBeDefined();
+
+ it('parses and returns when http succeeds', function () {
+ var promiseChainResolved = false;
+ $http.andReturn(Promise.resolve('successResponse'));
+
+ provider
+ .query('hello', 10)
+ .then(function (results) {
+ expect(provider.parseResponse)
+ .toHaveBeenCalledWith('successResponse');
+ expect(results).toBe('parsedResponse');
+ promiseChainResolved = true;
+ });
+
+ waitsFor(function () {
+ return promiseChainResolved;
+ });
});
});
- }
-); \ No newline at end of file
+
+ it('can clean terms', function () {
+ expect(provider.cleanTerm(' asdhs ')).toBe('asdhs');
+ expect(provider.cleanTerm(' and some words'))
+ .toBe('and some words');
+ expect(provider.cleanTerm('Nice input')).toBe('Nice input');
+ });
+
+ it('can create fuzzy term matchers', function () {
+ expect(provider.fuzzyMatchUnquotedTerms('pwr dvc 43'))
+ .toBe('pwr~ dvc~ 43~');
+
+ expect(provider.fuzzyMatchUnquotedTerms(
+ 'hello welcome "to quoted village" have fun'
+ )).toBe(
+ 'hello~ welcome~ "to quoted village" have~ fun~'
+ );
+ });
+
+ it('can parse responses', function () {
+ var elasticSearchResponse = {
+ data: {
+ hits: {
+ total: 2,
+ hits: [
+ {
+ '_id': 'hit1Id',
+ '_source': 'hit1Model',
+ '_score': 0.56
+ },
+ {
+ '_id': 'hit2Id',
+ '_source': 'hit2Model',
+ '_score': 0.34
+ }
+ ]
+ }
+ }
+ };
+
+ expect(provider.parseResponse(elasticSearchResponse))
+ .toEqual({
+ hits: [
+ {
+ id: 'hit1Id',
+ model: 'hit1Model',
+ score: 0.56
+ },
+ {
+ id: 'hit2Id',
+ model: 'hit2Model',
+ score: 0.34
+ }
+ ],
+ total: 2
+ });
+ });
+ });
+
+});
diff --git a/platform/representation/bundle.json b/platform/representation/bundle.json
index 44656b0f4..331856a9a 100644
--- a/platform/representation/bundle.json
+++ b/platform/representation/bundle.json
@@ -54,7 +54,13 @@
{
"key": "menu",
"implementation": "actions/ContextMenuAction.js",
- "depends": [ "$compile", "$document", "$window", "$rootScope", "agentService" ]
+ "depends": [
+ "$compile",
+ "$document",
+ "$rootScope",
+ "popupService",
+ "agentService"
+ ]
}
]
}
diff --git a/platform/representation/src/MCTRepresentation.js b/platform/representation/src/MCTRepresentation.js
index 49b2ae0f5..98a814c36 100644
--- a/platform/representation/src/MCTRepresentation.js
+++ b/platform/representation/src/MCTRepresentation.js
@@ -136,6 +136,14 @@ define(
}
}
+ // Destroy (deallocate any resources associated with) any
+ // active representers.
+ function destroyRepresenters() {
+ activeRepresenters.forEach(function (activeRepresenter) {
+ activeRepresenter.destroy();
+ });
+ }
+
// General-purpose refresh mechanism; should set up the scope
// as appropriate for current representation key and
// domain object.
@@ -152,10 +160,8 @@ define(
// via the "inclusion" field
$scope.inclusion = representation && getPath(representation);
- // Any existing gestures are no longer valid; release them.
- activeRepresenters.forEach(function (activeRepresenter) {
- activeRepresenter.destroy();
- });
+ // Any existing representers are no longer valid; release them.
+ destroyRepresenters();
// Log if a key was given, but no matching representation
// was found.
@@ -209,6 +215,10 @@ define(
// model's "modified" field, by the mutation capability.
$scope.$watch("domainObject.getModel().modified", refreshCapabilities);
+ // Make sure any resources allocated by representers also get
+ // released.
+ $scope.$on("$destroy", destroyRepresenters);
+
// Do one initial refresh, so that we don't need another
// digest iteration just to populate the scope. Failure to
// do this can result in unstable digest cycles, which
diff --git a/platform/representation/src/actions/ContextMenuAction.js b/platform/representation/src/actions/ContextMenuAction.js
index 56e5fa33f..82e11713f 100644
--- a/platform/representation/src/actions/ContextMenuAction.js
+++ b/platform/representation/src/actions/ContextMenuAction.js
@@ -43,40 +43,52 @@ define(
* @constructor
* @param $compile Angular's $compile service
* @param $document the current document
- * @param $window the active window
* @param $rootScope Angular's root scope
- * @param actionContexr the context in which the action
+ * @param {platform/commonUI/general.PopupService} popupService
+ * @param actionContext the context in which the action
* should be performed
* @implements {Action}
*/
- function ContextMenuAction($compile, $document, $window, $rootScope, agentService, actionContext) {
+ function ContextMenuAction(
+ $compile,
+ $document,
+ $rootScope,
+ popupService,
+ agentService,
+ actionContext
+ ) {
this.$compile = $compile;
this.agentService = agentService;
this.actionContext = actionContext;
+ this.popupService = popupService;
this.getDocument = function () { return $document; };
- this.getWindow = function () { return $window; };
this.getRootScope = function () { return $rootScope; };
}
ContextMenuAction.prototype.perform = function () {
var $compile = this.$compile,
$document = this.getDocument(),
- $window = this.getWindow(),
$rootScope = this.getRootScope(),
actionContext = this.actionContext,
- winDim = [$window.innerWidth, $window.innerHeight],
- eventCoors = [actionContext.event.pageX, actionContext.event.pageY],
+ eventCoords = [
+ actionContext.event.pageX,
+ actionContext.event.pageY
+ ],
menuDim = GestureConstants.MCT_MENU_DIMENSIONS,
body = $document.find('body'),
scope = $rootScope.$new(),
- goLeft = eventCoors[0] + menuDim[0] > winDim[0],
- goUp = eventCoors[1] + menuDim[1] > winDim[1],
- initiatingEvent = this.agentService.isMobile() ? 'touchstart' : 'mousedown',
- menu;
+ initiatingEvent = this.agentService.isMobile() ?
+ 'touchstart' : 'mousedown',
+ menu,
+ popup;
// Remove the context menu
function dismiss() {
- menu.remove();
+ if (popup) {
+ popup.dismiss();
+ popup = undefined;
+ }
+ scope.$destroy();
body.off("mousedown", dismiss);
dismissExistingMenu = undefined;
}
@@ -91,21 +103,17 @@ define(
// Set up the scope, including menu positioning
scope.domainObject = actionContext.domainObject;
- scope.menuStyle = {};
- scope.menuStyle[goLeft ? "right" : "left"] =
- (goLeft ? (winDim[0] - eventCoors[0]) : eventCoors[0]) + 'px';
- scope.menuStyle[goUp ? "bottom" : "top"] =
- (goUp ? (winDim[1] - eventCoors[1]) : eventCoors[1]) + 'px';
- scope.menuClass = {
- "go-left": goLeft,
- "go-up": goUp,
- "context-menu-holder": true
- };
+ scope.menuClass = { "context-menu-holder": true };
// Create the context menu
menu = $compile(MENU_TEMPLATE)(scope);
- // Add the menu to the body
- body.append(menu);
+ popup = this.popupService.display(menu, eventCoords, {
+ marginX: -menuDim[0],
+ marginY: -menuDim[1]
+ });
+
+ scope.menuClass['go-left'] = popup.goesLeft();
+ scope.menuClass['go-up'] = popup.goesUp();
// Stop propagation so that clicks or touches on the menu do not close the menu
menu.on(initiatingEvent, function (event) {
diff --git a/platform/representation/test/MCTRepresentationSpec.js b/platform/representation/test/MCTRepresentationSpec.js
index 337f214a8..a50347df7 100644
--- a/platform/representation/test/MCTRepresentationSpec.js
+++ b/platform/representation/test/MCTRepresentationSpec.js
@@ -106,7 +106,7 @@ define(
mockSce.trustAsResourceUrl.andCallFake(function (url) {
return url;
});
- mockScope = jasmine.createSpyObj("scope", [ "$watch" ]);
+ mockScope = jasmine.createSpyObj("scope", [ "$watch", "$on" ]);
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS);
diff --git a/platform/representation/test/actions/ContextMenuActionSpec.js b/platform/representation/test/actions/ContextMenuActionSpec.js
index 233d6c6bf..ba24076fb 100644
--- a/platform/representation/test/actions/ContextMenuActionSpec.js
+++ b/platform/representation/test/actions/ContextMenuActionSpec.js
@@ -41,13 +41,14 @@ define(
mockMenu,
mockDocument,
mockBody,
- mockWindow,
+ mockPopupService,
mockRootScope,
mockAgentService,
mockScope,
mockElement,
mockDomainObject,
mockEvent,
+ mockPopup,
mockActionContext,
action;
@@ -57,36 +58,47 @@ define(
mockMenu = jasmine.createSpyObj("menu", JQLITE_FUNCTIONS);
mockDocument = jasmine.createSpyObj("$document", JQLITE_FUNCTIONS);
mockBody = jasmine.createSpyObj("body", JQLITE_FUNCTIONS);
- mockWindow = { innerWidth: MENU_DIMENSIONS[0] * 4, innerHeight: MENU_DIMENSIONS[1] * 4 };
+ mockPopupService =
+ jasmine.createSpyObj("popupService", ["display"]);
+ mockPopup = jasmine.createSpyObj("popup", [
+ "dismiss",
+ "goesLeft",
+ "goesUp"
+ ]);
mockRootScope = jasmine.createSpyObj("$rootScope", ["$new"]);
mockAgentService = jasmine.createSpyObj("agentService", ["isMobile"]);
- mockScope = {};
+ mockScope = jasmine.createSpyObj("scope", ["$destroy"]);
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS);
mockEvent = jasmine.createSpyObj("event", ["preventDefault", "stopPropagation"]);
- mockEvent.pageX = 0;
- mockEvent.pageY = 0;
+ mockEvent.pageX = 123;
+ mockEvent.pageY = 321;
mockCompile.andReturn(mockCompiledTemplate);
mockCompiledTemplate.andReturn(mockMenu);
mockDocument.find.andReturn(mockBody);
mockRootScope.$new.andReturn(mockScope);
+ mockPopupService.display.andReturn(mockPopup);
mockActionContext = {key: 'menu', domainObject: mockDomainObject, event: mockEvent};
action = new ContextMenuAction(
mockCompile,
mockDocument,
- mockWindow,
mockRootScope,
+ mockPopupService,
mockAgentService,
mockActionContext
);
});
- it(" adds a menu to the DOM when perform is called", function () {
+ it("displays a popup when performed", function () {
action.perform();
- expect(mockBody.append).toHaveBeenCalledWith(mockMenu);
+ expect(mockPopupService.display).toHaveBeenCalledWith(
+ mockMenu,
+ [ mockEvent.pageX, mockEvent.pageY ],
+ jasmine.any(Object)
+ );
});
it("prevents the default context menu behavior", function () {
@@ -94,29 +106,22 @@ define(
expect(mockEvent.preventDefault).toHaveBeenCalled();
});
- it("positions menus where clicked", function () {
- mockEvent.pageX = 10;
- mockEvent.pageY = 5;
- action.perform();
- expect(mockScope.menuStyle.left).toEqual("10px");
- expect(mockScope.menuStyle.top).toEqual("5px");
- expect(mockScope.menuStyle.right).toBeUndefined();
- expect(mockScope.menuStyle.bottom).toBeUndefined();
- expect(mockScope.menuClass['go-up']).toBeFalsy();
- expect(mockScope.menuClass['go-left']).toBeFalsy();
+ it("adds classes to menus based on position", function () {
+ var booleans = [ false, true ];
+
+ booleans.forEach(function (goLeft) {
+ booleans.forEach(function (goUp) {
+ mockPopup.goesLeft.andReturn(goLeft);
+ mockPopup.goesUp.andReturn(goUp);
+ action.perform();
+ expect(!!mockScope.menuClass['go-up'])
+ .toEqual(goUp);
+ expect(!!mockScope.menuClass['go-left'])
+ .toEqual(goLeft);
+ });
+ });
});
- it("repositions menus near the screen edge", function () {
- mockEvent.pageX = mockWindow.innerWidth - 10;
- mockEvent.pageY = mockWindow.innerHeight - 5;
- action.perform();
- expect(mockScope.menuStyle.right).toEqual("10px");
- expect(mockScope.menuStyle.bottom).toEqual("5px");
- expect(mockScope.menuStyle.left).toBeUndefined();
- expect(mockScope.menuStyle.top).toBeUndefined();
- expect(mockScope.menuClass['go-up']).toBeTruthy();
- expect(mockScope.menuClass['go-left']).toBeTruthy();
- });
it("removes a menu when body is clicked", function () {
// Show the menu
@@ -133,7 +138,7 @@ define(
});
// Menu should have been removed
- expect(mockMenu.remove).toHaveBeenCalled();
+ expect(mockPopup.dismiss).toHaveBeenCalled();
// Listener should have been detached from body
expect(mockBody.off).toHaveBeenCalledWith(
@@ -149,7 +154,7 @@ define(
// Verify precondition
expect(mockMenu.remove).not.toHaveBeenCalled();
- // Find and fire body's mousedown listener
+ // Find and fire menu's click listener
mockMenu.on.calls.forEach(function (call) {
if (call.args[0] === 'click') {
call.args[1]();
@@ -157,7 +162,7 @@ define(
});
// Menu should have been removed
- expect(mockMenu.remove).toHaveBeenCalled();
+ expect(mockPopup.dismiss).toHaveBeenCalled();
});
it("keeps a menu when menu is clicked", function () {
@@ -171,7 +176,7 @@ define(
});
// Menu should have been removed
- expect(mockMenu.remove).not.toHaveBeenCalled();
+ expect(mockPopup.dismiss).not.toHaveBeenCalled();
// Listener should have been detached from body
expect(mockBody.off).not.toHaveBeenCalled();
@@ -182,8 +187,8 @@ define(
action = new ContextMenuAction(
mockCompile,
mockDocument,
- mockWindow,
mockRootScope,
+ mockPopupService,
mockAgentService,
mockActionContext
);
@@ -194,6 +199,8 @@ define(
call.args[1](mockEvent);
}
});
+
+ expect(mockPopup.dismiss).not.toHaveBeenCalled();
});
});
}
diff --git a/platform/search/bundle.json b/platform/search/bundle.json
index 7ea153655..9e39e28d0 100644
--- a/platform/search/bundle.json
+++ b/platform/search/bundle.json
@@ -45,13 +45,20 @@
"provides": "searchService",
"type": "provider",
"implementation": "services/GenericSearchProvider.js",
- "depends": [ "$q", "$timeout", "objectService", "workerService", "GENERIC_SEARCH_ROOTS" ]
+ "depends": [
+ "$q",
+ "$log",
+ "modelService",
+ "workerService",
+ "topic",
+ "GENERIC_SEARCH_ROOTS"
+ ]
},
{
"provides": "searchService",
"type": "aggregator",
"implementation": "services/SearchAggregator.js",
- "depends": [ "$q" ]
+ "depends": [ "$q", "objectService" ]
}
],
"workers": [
@@ -61,4 +68,4 @@
}
]
}
-} \ No newline at end of file
+}
diff --git a/platform/search/res/templates/search.html b/platform/search/res/templates/search.html
index e65ab072a..225df353b 100644
--- a/platform/search/res/templates/search.html
+++ b/platform/search/res/templates/search.html
@@ -21,21 +21,16 @@
-->
<div class="search"
ng-controller="SearchController as controller">
-
+
<!-- Search bar -->
<div class="search-bar"
ng-controller="ClickAwayController as toggle">
-
+
<!-- Input field -->
<input class="search-input"
type="text"
ng-model="ngModel.input"
ng-keyup="controller.search()" />
- <!--mct-control key="'textfield'"
- class="search-input"
- ng-model="ngModel.input"
- ng-keyup="controller.search()">
- </mct-control-->
<!-- Search icon -->
<!-- ui symbols for search are 'd' and 'M' -->
@@ -43,20 +38,20 @@
ng-class="{content: !(ngModel.input === '' || ngModel.input === undefined)}">
M
</div>
-
+
<!-- Clear icon/button 'x' -->
<a class="ui-symbol clear-icon"
ng-class="{content: !(ngModel.input === '' || ngModel.input === undefined)}"
ng-click="ngModel.input = ''; controller.search()">
&#xe607;
</a>
-
+
<!-- Menu icon/button 'v' -->
<a class="ui-symbol menu-icon"
ng-click="toggle.toggle()">
v
</a>
-
+
<!-- Menu -->
<mct-representation key="'search-menu'"
class="menu-element search-menu-holder"
@@ -65,27 +60,24 @@
ng-click="toggle.setState(true)">
</mct-representation>
</div>
-
+
<!-- Active filter display -->
<div class="active-filter-display"
ng-class="{off: ngModel.filtersString === '' || ngModel.filtersString === undefined || !ngModel.search}"
ng-controller="SearchMenuController as menuController">
-
+
<a class="ui-symbol clear-filters-icon"
ng-click="ngModel.checkAll = true; menuController.checkAll()">
&#xe607;
</a>
Filtered by: {{ ngModel.filtersString }}
-
- <!--div class="filter-options">
- Filtered by: {{ ngModel.filtersString }}
- </div-->
+
</div>
-
+
<!-- This div exists to determine scroll bar location -->
<div class="search-scroll abs">
-
+
<!-- Results list -->
<div class="results">
<mct-representation key="'search-item'"
@@ -103,14 +95,14 @@
<span class="title-label">Loading...</span>
</div>
- <!-- Load more button -->
+ <!-- Load more button -->
<div ng-if="controller.areMore()">
<a class="load-more-button s-btn vsm"
- ng-click="controller.loadMore()">
+ ng-click="controller.loadMore()">
More Results
</a>
</div>
-
+
</div>
-
-</div> \ No newline at end of file
+
+</div>
diff --git a/platform/search/src/controllers/SearchController.js b/platform/search/src/controllers/SearchController.js
index 10cf056b4..629e49533 100644
--- a/platform/search/src/controllers/SearchController.js
+++ b/platform/search/src/controllers/SearchController.js
@@ -26,146 +26,155 @@
*/
define(function () {
"use strict";
-
- var INITIAL_LOAD_NUMBER = 20,
- LOAD_INCREMENT = 20;
-
+
+ /**
+ * Controller for search in Tree View.
+ *
+ * Filtering is currently buggy; it filters after receiving results from
+ * search providers, the downside of this is that it requires search
+ * providers to provide objects for all possible results, which is
+ * potentially a hit to persistence, thus can be very very slow.
+ *
+ * Ideally, filtering should be handled before loading objects from the persistence
+ * store, the downside to this is that filters must be applied to object
+ * models, not object instances.
+ *
+ * @constructor
+ * @param $scope
+ * @param searchService
+ */
function SearchController($scope, searchService) {
- // numResults is the amount of results to display. Will get increased.
- // fullResults holds the most recent complete searchService response object
- var numResults = INITIAL_LOAD_NUMBER,
- fullResults = {hits: []};
-
- // Scope variables are:
- // Variables used only in SearchController:
- // results, an array of searchResult objects
- // loading, whether search() is loading
- // ngModel.input, the text of the search query
- // ngModel.search, a boolean of whether to display search or the tree
- // Variables used also in SearchMenuController:
- // ngModel.filter, the function filter defined below
- // ngModel.types, an array of type objects
- // ngModel.checked, a dictionary of which type filter options are checked
- // ngModel.checkAll, a boolean of whether to search all types
- // ngModel.filtersString, a string list of what filters on the results are active
- $scope.results = [];
- $scope.loading = false;
-
-
- // Filters searchResult objects by type. Allows types that are
- // checked. (ngModel.checked['typekey'] === true)
- // If hits is not provided, will use fullResults.hits
- function filter(hits) {
- var newResults = [],
- i = 0;
-
- if (!hits) {
- hits = fullResults.hits;
- }
-
- // If checkAll is checked, search everything no matter what the other
- // checkboxes' statuses are. Otherwise filter the search by types.
- if ($scope.ngModel.checkAll) {
- newResults = fullResults.hits.slice(0, numResults);
- } else {
- while (newResults.length < numResults && i < hits.length) {
- // If this is of an acceptable type, add it to the list
- if ($scope.ngModel.checked[hits[i].object.getModel().type]) {
- newResults.push(fullResults.hits[i]);
- }
- i += 1;
- }
- }
-
- $scope.results = newResults;
- return newResults;
+ var controller = this;
+ this.$scope = $scope;
+ this.searchService = searchService;
+ this.numberToDisplay = this.RESULTS_PER_PAGE;
+ this.availabileResults = 0;
+ this.$scope.results = [];
+ this.$scope.loading = false;
+ this.pendingQuery = undefined;
+ this.$scope.ngModel.filter = function () {
+ return controller.onFilterChange.apply(controller, arguments);
+ };
+ }
+
+ SearchController.prototype.RESULTS_PER_PAGE = 20;
+
+ /**
+ * Returns true if there are more results than currently displayed for the
+ * for the current query and filters.
+ */
+ SearchController.prototype.areMore = function () {
+ return this.$scope.results.length < this.availableResults;
+ };
+
+ /**
+ * Display more results for the currently displayed query and filters.
+ */
+ SearchController.prototype.loadMore = function () {
+ this.numberToDisplay += this.RESULTS_PER_PAGE;
+ this.dispatchSearch();
+ };
+
+ /**
+ * Reset search results, then search for the query string specified in
+ * scope.
+ */
+ SearchController.prototype.search = function () {
+ var inputText = this.$scope.ngModel.input;
+
+ this.clearResults();
+
+ if (inputText) {
+ this.$scope.loading = true;
+ this.$scope.ngModel.search = true;
+ } else {
+ this.pendingQuery = undefined;
+ this.$scope.ngModel.search = false;
+ this.$scope.loading = false;
+ return;
+ }
+
+ this.dispatchSearch();
+ };
+
+ /**
+ * Dispatch a search to the search service if it hasn't already been
+ * dispatched.
+ *
+ * @private
+ */
+ SearchController.prototype.dispatchSearch = function () {
+ var inputText = this.$scope.ngModel.input,
+ controller = this,
+ queryId = inputText + this.numberToDisplay;
+
+ if (this.pendingQuery === queryId) {
+ return; // don't issue multiple queries for the same term.
}
-
- // Make function accessible from SearchMenuController
- $scope.ngModel.filter = filter;
-
- // For documentation, see search below
- function search(maxResults) {
- var inputText = $scope.ngModel.input;
-
- if (inputText !== '' && inputText !== undefined) {
- // We are starting to load.
- $scope.loading = true;
-
- // Update whether the file tree should be displayed
- // Hide tree only when starting search
- $scope.ngModel.search = true;
- }
-
- if (!maxResults) {
- // Reset 'load more'
- numResults = INITIAL_LOAD_NUMBER;
- }
-
- // Send the query
- searchService.query(inputText, maxResults).then(function (result) {
- // Store all the results before splicing off the front, so that
- // we can load more to display later.
- fullResults = result;
- $scope.results = filter(result.hits);
-
- // Update whether the file tree should be displayed
- // Reveal tree only when finishing search
- if (inputText === '' || inputText === undefined) {
- $scope.ngModel.search = false;
+
+ this.pendingQuery = queryId;
+
+ this
+ .searchService
+ .query(inputText, this.numberToDisplay, this.filterPredicate())
+ .then(function (results) {
+ if (controller.pendingQuery !== queryId) {
+ return; // another query in progress, so skip this one.
}
-
- // Now we are done loading.
- $scope.loading = false;
+ controller.onSearchComplete(results);
});
+ };
+
+ SearchController.prototype.filter = SearchController.prototype.onFilterChange;
+
+ /**
+ * Refilter results and update visible results when filters have changed.
+ */
+ SearchController.prototype.onFilterChange = function () {
+ this.pendingQuery = undefined;
+ this.search();
+ };
+
+ /**
+ * Returns a predicate function that can be used to filter object models.
+ *
+ * @private
+ */
+ SearchController.prototype.filterPredicate = function () {
+ if (this.$scope.ngModel.checkAll) {
+ return function () {
+ return true;
+ };
}
-
- return {
- /**
- * Search the filetree. Assumes that any search text will
- * be in ngModel.input
- *
- * @param maxResults (optional) The maximum number of results
- * that this function should return. If not provided, search
- * service default will be used.
- */
- search: search,
-
- /**
- * Checks to see if there are more search results to display. If the answer is
- * unclear, this function will err toward saying that there are more results.
- */
- areMore: function () {
- var i;
-
- // Check to see if any of the not displayed results are of an allowed type
- for (i = numResults; i < fullResults.hits.length; i += 1) {
- if ($scope.ngModel.checkAll || $scope.ngModel.checked[fullResults.hits[i].object.getModel().type]) {
- return true;
- }
- }
-
- // If none of the ones at hand are correct, there still may be more if we
- // re-search with a larger maxResults
- return fullResults.hits.length < fullResults.total;
- },
-
- /**
- * Increases the number of search results to display, and then
- * loads them, adding to the displayed results.
- */
- loadMore: function () {
- numResults += LOAD_INCREMENT;
-
- if (numResults > fullResults.hits.length && fullResults.hits.length < fullResults.total) {
- // Resend the query if we are out of items to display, but there are more to get
- search(numResults);
- } else {
- // Otherwise just take from what we already have
- $scope.results = filter(fullResults.hits);
- }
- }
+ var includeTypes = this.$scope.ngModel.checked;
+ return function (model) {
+ return !!includeTypes[model.type];
};
- }
+ };
+
+ /**
+ * Clear the search results.
+ *
+ * @private
+ */
+ SearchController.prototype.clearResults = function () {
+ this.$scope.results = [];
+ this.availableResults = 0;
+ this.numberToDisplay = this.RESULTS_PER_PAGE;
+ };
+
+
+ /**
+ * Update search results from given `results`.
+ *
+ * @private
+ */
+ SearchController.prototype.onSearchComplete = function (results) {
+ this.availableResults = results.total;
+ this.$scope.results = results.hits;
+ this.$scope.loading = false;
+ this.pendingQuery = undefined;
+ };
+
return SearchController;
});
diff --git a/platform/search/src/services/GenericSearchProvider.js b/platform/search/src/services/GenericSearchProvider.js
index 014d8d7fd..71dfe8c0e 100644
--- a/platform/search/src/services/GenericSearchProvider.js
+++ b/platform/search/src/services/GenericSearchProvider.js
@@ -19,251 +19,262 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-/*global define*/
+/*global define,setTimeout*/
/**
* Module defining GenericSearchProvider. Created by shale on 07/16/2015.
*/
-define(
- [],
- function () {
- "use strict";
-
- var DEFAULT_MAX_RESULTS = 100,
- DEFAULT_TIMEOUT = 1000,
- stopTime;
-
- /**
- * A search service which searches through domain objects in
- * the filetree without using external search implementations.
- *
- * @constructor
- * @param $q Angular's $q, for promise consolidation.
- * @param $timeout Angular's $timeout, for delayed function execution.
- * @param {ObjectService} objectService The service from which
- * domain objects can be gotten.
- * @param {WorkerService} workerService The service which allows
- * more easy creation of web workers.
- * @param {GENERIC_SEARCH_ROOTS} ROOTS An array of the root
- * domain objects' IDs.
- */
- function GenericSearchProvider($q, $timeout, objectService, workerService, ROOTS) {
- var indexed = {},
- pendingQueries = {},
- worker = workerService.run('genericSearchWorker');
-
- this.worker = worker;
- this.pendingQueries = pendingQueries;
- this.$q = $q;
- // pendingQueries is a dictionary with the key value pairs st
- // the key is the timestamp and the value is the promise
-
- // Tell the web worker to add a domain object's model to its list of items.
- function indexItem(domainObject) {
- var message;
-
- // undefined check
- if (domainObject && domainObject.getModel) {
- // Using model instead of whole domain object because
- // it's a JSON object.
- message = {
- request: 'index',
- model: domainObject.getModel(),
- id: domainObject.getId()
- };
- worker.postMessage(message);
- }
- }
-
-
- // Handles responses from the web worker. Namely, the results of
- // a search request.
- function handleResponse(event) {
- var ids = [],
- id;
-
- // If we have the results from a search
- if (event.data.request === 'search') {
- // Convert the ids given from the web worker into domain objects
- for (id in event.data.results) {
- ids.push(id);
- }
- objectService.getObjects(ids).then(function (objects) {
- var searchResults = [],
- id;
-
- // Create searchResult objects
- for (id in objects) {
- searchResults.push({
- object: objects[id],
- id: id,
- score: event.data.results[id]
- });
- }
-
- // Resove the promise corresponding to this
- pendingQueries[event.data.timestamp].resolve({
- hits: searchResults,
- total: event.data.total,
- timedOut: event.data.timedOut
- });
- });
- }
- }
-
- // Helper function for getItems(). Indexes the tree.
- function indexItems(nodes) {
- nodes.forEach(function (node) {
- var id = node && node.getId && node.getId();
-
- // If we have already indexed this item, stop here
- if (indexed[id]) {
- return;
- }
-
- // Index each item with the web worker
- indexItem(node);
- indexed[id] = true;
-
-
- // If this node has children, index those
- if (node && node.hasCapability && node.hasCapability('composition')) {
- // Make sure that this is async, so doesn't block up page
- $timeout(function () {
- // Get the children...
- node.useCapability('composition').then(function (children) {
- $timeout(function () {
- // ... then index the children
- if (children.constructor === Array) {
- indexItems(children);
- } else {
- indexItems([children]);
- }
- }, 0);
- });
- }, 0);
- }
-
- // Watch for changes to this item, in case it gets new children
- if (node && node.hasCapability && node.hasCapability('mutation')) {
- node.getCapability('mutation').listen(function (listener) {
- if (listener && listener.composition) {
- // If the node was mutated to have children, get the child domain objects
- objectService.getObjects(listener.composition).then(function (objectsById) {
- var objects = [],
- id;
-
- // Get each of the domain objects in objectsById
- for (id in objectsById) {
- objects.push(objectsById[id]);
- }
-
- indexItems(objects);
- });
- }
- });
- }
- });
- }
-
- // Converts the filetree into a list
- function getItems() {
- // Aquire root objects
- objectService.getObjects(ROOTS).then(function (objectsById) {
- var objects = [],
- id;
-
- // Get each of the domain objects in objectsById
- for (id in objectsById) {
- objects.push(objectsById[id]);
- }
-
- // Index all of the roots' descendents
- indexItems(objects);
- });
- }
-
- worker.onmessage = handleResponse;
-
- // Index the tree's contents once at the beginning
- getItems();
+define([
+
+], function (
+
+) {
+ "use strict";
+
+ /**
+ * A search service which searches through domain objects in
+ * the filetree without using external search implementations.
+ *
+ * @constructor
+ * @param $q Angular's $q, for promise consolidation.
+ * @param $log Anglar's $log, for logging.
+ * @param {ModelService} modelService the model service.
+ * @param {WorkerService} workerService the workerService.
+ * @param {TopicService} topic the topic service.
+ * @param {Array} ROOTS An array of object Ids to begin indexing.
+ */
+ function GenericSearchProvider($q, $log, modelService, workerService, topic, ROOTS) {
+ var provider = this;
+ this.$q = $q;
+ this.$log = $log;
+ this.modelService = modelService;
+
+ this.indexedIds = {};
+ this.idsToIndex = [];
+ this.pendingIndex = {};
+ this.pendingRequests = 0;
+
+ this.pendingQueries = {};
+
+ this.worker = this.startWorker(workerService);
+ this.indexOnMutation(topic);
+
+ ROOTS.forEach(function indexRoot(rootId) {
+ provider.scheduleForIndexing(rootId);
+ });
+
+
+ }
+
+ /**
+ * Maximum number of concurrent index requests to allow.
+ */
+ GenericSearchProvider.prototype.MAX_CONCURRENT_REQUESTS = 100;
+
+ /**
+ * Query the search provider for results.
+ *
+ * @param {String} input the string to search by.
+ * @param {Number} maxResults max number of results to return.
+ * @returns {Promise} a promise for a modelResults object.
+ */
+ GenericSearchProvider.prototype.query = function (
+ input,
+ maxResults
+ ) {
+
+ var queryId = this.dispatchSearch(input, maxResults),
+ pendingQuery = this.$q.defer();
+
+ this.pendingQueries[queryId] = pendingQuery;
+
+ return pendingQuery.promise;
+ };
+
+ /**
+ * Creates a search worker and attaches handlers.
+ *
+ * @private
+ * @param workerService
+ * @returns worker the created search worker.
+ */
+ GenericSearchProvider.prototype.startWorker = function (workerService) {
+ var worker = workerService.run('genericSearchWorker'),
+ provider = this;
+
+ worker.addEventListener('message', function (messageEvent) {
+ provider.onWorkerMessage(messageEvent);
+ });
+
+ return worker;
+ };
+
+ /**
+ * Listen to the mutation topic and re-index objects when they are
+ * mutated.
+ *
+ * @private
+ * @param topic the topicService.
+ */
+ GenericSearchProvider.prototype.indexOnMutation = function (topic) {
+ var mutationTopic = topic('mutation'),
+ provider = this;
+
+ mutationTopic.listen(function (mutatedObject) {
+ var id = mutatedObject.getId();
+ provider.indexedIds[id] = false;
+ provider.scheduleForIndexing(id);
+ });
+ };
+
+ /**
+ * Schedule an id to be indexed at a later date. If there are less
+ * pending requests then allowed, will kick off an indexing request.
+ *
+ * @private
+ * @param {String} id to be indexed.
+ */
+ GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
+ if (!this.indexedIds[id] && !this.pendingIndex[id]) {
+ this.indexedIds[id] = true;
+ this.pendingIndex[id] = true;
+ this.idsToIndex.push(id);
}
+ this.keepIndexing();
+ };
- /**
- * Searches through the filetree for domain objects which match
- * the search term. This function is to be used as a fallback
- * in the case where other search services are not avaliable.
- * Returns a promise for a result object that has the format
- * {hits: searchResult[], total: number, timedOut: boolean}
- * where a searchResult has the format
- * {id: string, object: domainObject, score: number}
- *
- * Notes:
- * * The order of the results is not guarenteed.
- * * A domain object qualifies as a match for a search input if
- * the object's name property contains any of the search terms
- * (which are generated by splitting the input at spaces).
- * * Scores are higher for matches that have more of the terms
- * as substrings.
- *
- * @param input The text input that is the query.
- * @param timestamp The time at which this function was called.
- * This timestamp is used as a unique identifier for this
- * query and the corresponding results.
- * @param maxResults (optional) The maximum number of results
- * that this function should return.
- * @param timeout (optional) The time after which the search should
- * stop calculations and return partial results.
- */
- GenericSearchProvider.prototype.query = function query(input, timestamp, maxResults, timeout) {
- var terms = [],
- searchResults = [],
- pendingQueries = this.pendingQueries,
- worker = this.worker,
- defer = this.$q.defer();
-
- // Tell the worker to search for items it has that match this searchInput.
- // Takes the searchInput, as well as a max number of results (will return
- // less than that if there are fewer matches).
- function workerSearch(searchInput, maxResults, timestamp, timeout) {
- var message = {
- request: 'search',
- input: searchInput,
- maxNumber: maxResults,
- timestamp: timestamp,
- timeout: timeout
- };
- worker.postMessage(message);
- }
-
- // If the input is nonempty, do a search
- if (input !== '' && input !== undefined) {
-
- // Allow us to access this promise later to resolve it later
- pendingQueries[timestamp] = defer;
-
- // Check to see if the user provided a maximum
- // number of results to display
- if (!maxResults) {
- // Else, we provide a default value
- maxResults = DEFAULT_MAX_RESULTS;
- }
- // Similarly, check if timeout was provided
- if (!timeout) {
- timeout = DEFAULT_TIMEOUT;
+ /**
+ * If there are less pending requests than concurrent requests, keep
+ * firing requests.
+ *
+ * @private
+ */
+ GenericSearchProvider.prototype.keepIndexing = function () {
+ while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS &&
+ this.idsToIndex.length
+ ) {
+ this.beginIndexRequest();
+ }
+ };
+
+ /**
+ * Pass an id and model to the worker to be indexed. If the model has
+ * composition, schedule those ids for later indexing.
+ *
+ * @private
+ * @param id a model id
+ * @param model a model
+ */
+ GenericSearchProvider.prototype.index = function (id, model) {
+ var provider = this;
+
+ this.worker.postMessage({
+ request: 'index',
+ model: model,
+ id: id
+ });
+
+ if (Array.isArray(model.composition)) {
+ model.composition.forEach(function (id) {
+ provider.scheduleForIndexing(id);
+ });
+ }
+ };
+
+ /**
+ * Pulls an id from the indexing queue, loads it from the model service,
+ * and indexes it. Upon completion, tells the provider to keep
+ * indexing.
+ *
+ * @private
+ */
+ GenericSearchProvider.prototype.beginIndexRequest = function () {
+ var idToIndex = this.idsToIndex.shift(),
+ provider = this;
+
+ this.pendingRequests += 1;
+ this.modelService
+ .getModels([idToIndex])
+ .then(function (models) {
+ delete provider.pendingIndex[idToIndex];
+ if (models[idToIndex]) {
+ provider.index(idToIndex, models[idToIndex]);
}
+ }, function () {
+ provider
+ .$log
+ .warn('Failed to index domain object ' + idToIndex);
+ })
+ .then(function () {
+ setTimeout(function () {
+ provider.pendingRequests -= 1;
+ provider.keepIndexing();
+ }, 0);
+ });
+ };
- // Send the query to the worker
- workerSearch(input, maxResults, timestamp, timeout);
+ /**
+ * Handle messages from the worker. Only really knows how to handle search
+ * results, which are parsed, transformed into a modelResult object, which
+ * is used to resolve the corresponding promise.
+ * @private
+ */
+ GenericSearchProvider.prototype.onWorkerMessage = function (event) {
+ if (event.data.request !== 'search') {
+ return;
+ }
- return defer.promise;
- } else {
- // Otherwise return an empty result
- return { hits: [], total: 0 };
- }
- };
+ var pendingQuery = this.pendingQueries[event.data.queryId],
+ modelResults = {
+ total: event.data.total
+ };
+ modelResults.hits = event.data.results.map(function (hit) {
+ return {
+ id: hit.item.id,
+ model: hit.item.model,
+ score: hit.matchCount
+ };
+ });
- return GenericSearchProvider;
- }
-); \ No newline at end of file
+ pendingQuery.resolve(modelResults);
+ delete this.pendingQueries[event.data.queryId];
+ };
+
+ /**
+ * @private
+ * @returns {Number} a unique, unusued query Id.
+ */
+ GenericSearchProvider.prototype.makeQueryId = function () {
+ var queryId = Math.ceil(Math.random() * 100000);
+ while (this.pendingQueries[queryId]) {
+ queryId = Math.ceil(Math.random() * 100000);
+ }
+ return queryId;
+ };
+
+ /**
+ * Dispatch a search query to the worker and return a queryId.
+ *
+ * @private
+ * @returns {Number} a unique query Id for the query.
+ */
+ GenericSearchProvider.prototype.dispatchSearch = function (
+ searchInput,
+ maxResults
+ ) {
+ var queryId = this.makeQueryId();
+
+ this.worker.postMessage({
+ request: 'search',
+ input: searchInput,
+ maxResults: maxResults,
+ queryId: queryId
+ });
+
+ return queryId;
+ };
+
+
+ return GenericSearchProvider;
+});
diff --git a/platform/search/src/services/GenericSearchWorker.js b/platform/search/src/services/GenericSearchWorker.js
index 57be98b42..928f66cab 100644
--- a/platform/search/src/services/GenericSearchWorker.js
+++ b/platform/search/src/services/GenericSearchWorker.js
@@ -26,133 +26,132 @@
*/
(function () {
"use strict";
-
+
// An array of objects composed of domain object IDs and models
// {id: domainObject's ID, model: domainObject's model}
- var indexedItems = [];
-
- // Helper function for serach()
- function convertToTerms(input) {
- var terms = input;
- // Shave any spaces off of the ends of the input
- while (terms.substr(0, 1) === ' ') {
- terms = terms.substring(1, terms.length);
- }
- while (terms.substr(terms.length - 1, 1) === ' ') {
- terms = terms.substring(0, terms.length - 1);
- }
-
- // Then split it at spaces and asterisks
- terms = terms.split(/ |\*/);
-
- // Remove any empty strings from the terms
- while (terms.indexOf('') !== -1) {
- terms.splice(terms.indexOf(''), 1);
- }
-
- return terms;
- }
-
- // Helper function for search()
- function scoreItem(item, input, terms) {
- var name = item.model.name.toLocaleLowerCase(),
- weight = 0.65,
- score = 0.0,
- i;
+ var indexedItems = [],
+ TERM_SPLITTER = /[ _\*]/;
- // Make the score really big if the item name and
- // the original search input are the same
- if (name === input) {
- score = 42;
- }
-
- for (i = 0; i < terms.length; i += 1) {
- // Increase the score if the term is in the item name
- if (name.indexOf(terms[i]) !== -1) {
- score += 1;
+ function indexItem(id, model) {
+ var vector = {
+ name: model.name
+ };
+ vector.cleanName = model.name.trim();
+ vector.lowerCaseName = vector.cleanName.toLocaleLowerCase();
+ vector.terms = vector.lowerCaseName.split(TERM_SPLITTER);
- // Add extra to the score if the search term exists
- // as its own term within the items
- if (name.split(' ').indexOf(terms[i]) !== -1) {
- score += 0.5;
- }
- }
- }
+ indexedItems.push({
+ id: id,
+ vector: vector,
+ model: model
+ });
+ }
- return score * weight;
+ // Helper function for search()
+ function convertToTerms(input) {
+ var query = {
+ exactInput: input
+ };
+ query.inputClean = input.trim();
+ query.inputLowerCase = query.inputClean.toLocaleLowerCase();
+ query.terms = query.inputLowerCase.split(TERM_SPLITTER);
+ query.exactTerms = query.inputClean.split(TERM_SPLITTER);
+ return query;
}
-
- /**
+
+ /**
* Gets search results from the indexedItems based on provided search
- * input. Returns matching results from indexedItems, as well as the
- * timestamp that was passed to it.
- *
+ * input. Returns matching results from indexedItems
+ *
* @param data An object which contains:
* * input: The original string which we are searching with
- * * maxNumber: The maximum number of search results desired
- * * timestamp: The time identifier from when the query was made
+ * * maxResults: The maximum number of search results desired
+ * * queryId: an id identifying this query, will be returned.
*/
function search(data) {
- // This results dictionary will have domain object ID keys which
- // point to the value the domain object's score.
- var results = {},
- input = data.input.toLocaleLowerCase(),
- terms = convertToTerms(input),
+ // This results dictionary will have domain object ID keys which
+ // point to the value the domain object's score.
+ var results,
+ input = data.input,
+ query = convertToTerms(input),
message = {
request: 'search',
results: {},
total: 0,
- timestamp: data.timestamp,
- timedOut: false
+ queryId: data.queryId
},
- score,
- i,
- id;
-
- // If the user input is empty, we want to have no search results.
- if (input !== '') {
- for (i = 0; i < indexedItems.length; i += 1) {
- // If this is taking too long, then stop
- if (Date.now() > data.timestamp + data.timeout) {
- message.timedOut = true;
- break;
+ matches = {};
+
+ if (!query.inputClean) {
+ // No search terms, no results;
+ return message;
+ }
+
+ // Two phases: find matches, then score matches.
+ // Idea being that match finding should be fast, so that future scoring
+ // operations process fewer objects.
+
+ query.terms.forEach(function findMatchingItems(term) {
+ indexedItems
+ .filter(function matchesItem(item) {
+ return item.vector.lowerCaseName.indexOf(term) !== -1;
+ })
+ .forEach(function trackMatch(matchedItem) {
+ if (!matches[matchedItem.id]) {
+ matches[matchedItem.id] = {
+ matchCount: 0,
+ item: matchedItem
+ };
+ }
+ matches[matchedItem.id].matchCount += 1;
+ });
+ });
+
+ // Then, score matching items.
+ results = Object
+ .keys(matches)
+ .map(function asMatches(matchId) {
+ return matches[matchId];
+ })
+ .map(function prioritizeExactMatches(match) {
+ if (match.item.vector.name === query.exactInput) {
+ match.matchCount += 100;
+ } else if (match.item.vector.lowerCaseName ===
+ query.inputLowerCase) {
+ match.matchCount += 50;
}
-
- // Score and add items
- score = scoreItem(indexedItems[i], input, terms);
- if (score > 0) {
- results[indexedItems[i].id] = score;
- message.total += 1;
+ return match;
+ })
+ .map(function prioritizeCompleteTermMatches(match) {
+ match.item.vector.terms.forEach(function (term) {
+ if (query.terms.indexOf(term) !== -1) {
+ match.matchCount += 0.5;
+ }
+ });
+ return match;
+ })
+ .sort(function compare(a, b) {
+ if (a.matchCount > b.matchCount) {
+ return -1;
}
- }
- }
-
- // Truncate results if there are more than maxResults
- if (message.total > data.maxResults) {
- i = 0;
- for (id in results) {
- message.results[id] = results[id];
- i += 1;
- if (i >= data.maxResults) {
- break;
+ if (a.matchCount < b.matchCount) {
+ return 1;
}
- }
- // TODO: This seems inefficient.
- } else {
- message.results = results;
- }
-
+ return 0;
+ });
+
+ message.total = results.length;
+ message.results = results
+ .slice(0, data.maxResults);
+
return message;
}
-
+
self.onmessage = function (event) {
if (event.data.request === 'index') {
- indexedItems.push({
- id: event.data.id,
- model: event.data.model
- });
+ indexItem(event.data.id, event.data.model);
} else if (event.data.request === 'search') {
self.postMessage(search(event.data));
}
};
-}()); \ No newline at end of file
+}());
diff --git a/platform/search/src/services/SearchAggregator.js b/platform/search/src/services/SearchAggregator.js
index 232409059..00988f81a 100644
--- a/platform/search/src/services/SearchAggregator.js
+++ b/platform/search/src/services/SearchAggregator.js
@@ -24,122 +24,201 @@
/**
* Module defining SearchAggregator. Created by shale on 07/16/2015.
*/
-define(
- [],
- function () {
- "use strict";
-
- var DEFUALT_TIMEOUT = 1000,
- DEFAULT_MAX_RESULTS = 100;
-
- /**
- * Allows multiple services which provide search functionality
- * to be treated as one.
- *
- * @constructor
- * @param $q Angular's $q, for promise consolidation.
- * @param {SearchProvider[]} providers The search providers to be
- * aggregated.
- */
- function SearchAggregator($q, providers) {
- this.$q = $q;
- this.providers = providers;
+define([
+
+], function (
+
+) {
+ "use strict";
+
+ /**
+ * Aggregates multiple search providers as a singular search provider.
+ * Search providers are expected to implement a `query` method which returns
+ * a promise for a `modelResults` object.
+ *
+ * The search aggregator combines the results from multiple providers,
+ * removes aggregates, and converts the results to domain objects.
+ *
+ * @constructor
+ * @param $q Angular's $q, for promise consolidation.
+ * @param objectService
+ * @param {SearchProvider[]} providers The search providers to be
+ * aggregated.
+ */
+ function SearchAggregator($q, objectService, providers) {
+ this.$q = $q;
+ this.objectService = objectService;
+ this.providers = providers;
+ }
+
+ /**
+ * If max results is not specified in query, use this as default.
+ */
+ SearchAggregator.prototype.DEFAULT_MAX_RESULTS = 100;
+
+ /**
+ * Because filtering isn't implemented inside each provider, the fudge
+ * factor is a multiplier on the number of results returned-- more results
+ * than requested will be fetched, and then will be filtered. This helps
+ * provide more predictable pagination when large numbers of results are
+ * returned but very few results match filters.
+ *
+ * If a provider level filter implementation is implemented in the future,
+ * remove this.
+ */
+ SearchAggregator.prototype.FUDGE_FACTOR = 5;
+
+ /**
+ * Sends a query to each of the providers. Returns a promise for
+ * a result object that has the format
+ * {hits: searchResult[], total: number}
+ * where a searchResult has the format
+ * {id: string, object: domainObject, score: number}
+ *
+ * @param {String} inputText The text input that is the query.
+ * @param {Number} maxResults (optional) The maximum number of results
+ * that this function should return. If not provided, a
+ * default of 100 will be used.
+ * @param {Function} [filter] if provided, will be called for every
+ * potential modelResult. If it returns false, the model result will be
+ * excluded from the search results.
+ * @returns {Promise} A Promise for a search result object.
+ */
+ SearchAggregator.prototype.query = function (
+ inputText,
+ maxResults,
+ filter
+ ) {
+
+ var aggregator = this,
+ resultPromises;
+
+ if (!maxResults) {
+ maxResults = this.DEFAULT_MAX_RESULTS;
}
- /**
- * Sends a query to each of the providers. Returns a promise for
- * a result object that has the format
- * {hits: searchResult[], total: number, timedOut: boolean}
- * where a searchResult has the format
- * {id: string, object: domainObject, score: number}
- *
- * @param inputText The text input that is the query.
- * @param maxResults (optional) The maximum number of results
- * that this function should return. If not provided, a
- * default of 100 will be used.
- */
- SearchAggregator.prototype.query = function queryAll(inputText, maxResults) {
- var $q = this.$q,
- providers = this.providers,
- i,
- timestamp = Date.now(),
- resultPromises = [];
-
- // Remove duplicate objects that have the same ID. Modifies the passed
- // array, and returns the number that were removed.
- function filterDuplicates(results, total) {
- var ids = {},
- numRemoved = 0,
- i;
-
- for (i = 0; i < results.length; i += 1) {
- if (ids[results[i].id]) {
- // If this result's ID is already there, remove the object
- results.splice(i, 1);
- numRemoved += 1;
-
- // Reduce loop index because we shortened the array
- i -= 1;
- } else {
- // Otherwise add the ID to the list of the ones we have seen
- ids[results[i].id] = true;
- }
- }
+ resultPromises = this.providers.map(function (provider) {
+ return provider.query(
+ inputText,
+ maxResults * aggregator.FUDGE_FACTOR
+ );
+ });
- return numRemoved;
- }
+ return this.$q
+ .all(resultPromises)
+ .then(function (providerResults) {
+ var modelResults = {
+ hits: [],
+ total: 0
+ };
- // Order the objects from highest to lowest score in the array.
- // Modifies the passed array, as well as returns the modified array.
- function orderByScore(results) {
- results.sort(function (a, b) {
- if (a.score > b.score) {
- return -1;
- } else if (b.score > a.score) {
- return 1;
- } else {
- return 0;
- }
+ providerResults.forEach(function (providerResult) {
+ modelResults.hits =
+ modelResults.hits.concat(providerResult.hits);
+ modelResults.total += providerResult.total;
});
- return results;
- }
- if (!maxResults) {
- maxResults = DEFAULT_MAX_RESULTS;
- }
+ modelResults = aggregator.orderByScore(modelResults);
+ modelResults = aggregator.applyFilter(modelResults, filter);
+ modelResults = aggregator.removeDuplicates(modelResults);
+
+ return aggregator.asObjectResults(modelResults);
+ });
+ };
- // Send the query to all the providers
- for (i = 0; i < providers.length; i += 1) {
- resultPromises.push(
- providers[i].query(inputText, timestamp, maxResults, DEFUALT_TIMEOUT)
- );
+ /**
+ * Order model results by score descending and return them.
+ */
+ SearchAggregator.prototype.orderByScore = function (modelResults) {
+ modelResults.hits.sort(function (a, b) {
+ if (a.score > b.score) {
+ return -1;
+ } else if (b.score > a.score) {
+ return 1;
+ } else {
+ return 0;
}
+ });
+ return modelResults;
+ };
+
+ /**
+ * Apply a filter to each model result, removing it from search results
+ * if it does not match.
+ */
+ SearchAggregator.prototype.applyFilter = function (modelResults, filter) {
+ if (!filter) {
+ return modelResults;
+ }
+ var initialLength = modelResults.hits.length,
+ finalLength,
+ removedByFilter;
+
+ modelResults.hits = modelResults.hits.filter(function (hit) {
+ return filter(hit.model);
+ });
- // Get promises for results arrays
- return $q.all(resultPromises).then(function (resultObjects) {
- var results = [],
- totalSum = 0,
- i;
+ finalLength = modelResults.hits.length;
+ removedByFilter = initialLength - finalLength;
+ modelResults.total -= removedByFilter;
- // Merge results
- for (i = 0; i < resultObjects.length; i += 1) {
- results = results.concat(resultObjects[i].hits);
- totalSum += resultObjects[i].total;
+ return modelResults;
+ };
+
+ /**
+ * Remove duplicate hits in a modelResults object, and decrement `total`
+ * each time a duplicate is removed.
+ */
+ SearchAggregator.prototype.removeDuplicates = function (modelResults) {
+ var includedIds = {};
+
+ modelResults.hits = modelResults
+ .hits
+ .filter(function alreadyInResults(hit) {
+ if (includedIds[hit.id]) {
+ modelResults.total -= 1;
+ return false;
}
- // Order by score first, so that when removing repeats we keep the higher scored ones
- orderByScore(results);
- totalSum -= filterDuplicates(results, totalSum);
-
- return {
- hits: results,
- total: totalSum,
- timedOut: resultObjects.some(function (obj) {
- return obj.timedOut;
- })
+ includedIds[hit.id] = true;
+ return true;
+ });
+
+ return modelResults;
+ };
+
+ /**
+ * Convert modelResults to objectResults by fetching them from the object
+ * service.
+ *
+ * @returns {Promise} for an objectResults object.
+ */
+ SearchAggregator.prototype.asObjectResults = function (modelResults) {
+ var objectIds = modelResults.hits.map(function (modelResult) {
+ return modelResult.id;
+ });
+
+ return this
+ .objectService
+ .getObjects(objectIds)
+ .then(function (objects) {
+
+ var objectResults = {
+ total: modelResults.total
};
+
+ objectResults.hits = modelResults
+ .hits
+ .map(function asObjectResult(hit) {
+ return {
+ id: hit.id,
+ object: objects[hit.id],
+ score: hit.score
+ };
+ });
+
+ return objectResults;
});
- };
+ };
- return SearchAggregator;
- }
-); \ No newline at end of file
+ return SearchAggregator;
+});
diff --git a/platform/search/test/controllers/SearchControllerSpec.js b/platform/search/test/controllers/SearchControllerSpec.js
index 720d9bd64..a755594d5 100644
--- a/platform/search/test/controllers/SearchControllerSpec.js
+++ b/platform/search/test/controllers/SearchControllerSpec.js
@@ -4,12 +4,12 @@
* 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.
+ * '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
+ * 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.
@@ -24,185 +24,162 @@
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
-define(
- ["../../src/controllers/SearchController"],
- function (SearchController) {
- "use strict";
-
- // These should be the same as the ones on the top of the search controller
- var INITIAL_LOAD_NUMBER = 20,
- LOAD_INCREMENT = 20;
-
- describe("The search controller", function () {
- var mockScope,
- mockSearchService,
- mockPromise,
- mockSearchResult,
- mockDomainObject,
- mockTypes,
- controller;
-
- function bigArray(size) {
- var array = [],
- i;
- for (i = 0; i < size; i += 1) {
- array.push(mockSearchResult);
- }
- return array;
+define([
+ '../../src/controllers/SearchController'
+], function (
+ SearchController
+) {
+ 'use strict';
+
+ describe('The search controller', function () {
+ var mockScope,
+ mockSearchService,
+ mockPromise,
+ mockSearchResult,
+ mockDomainObject,
+ mockTypes,
+ controller;
+
+ function bigArray(size) {
+ var array = [],
+ i;
+ for (i = 0; i < size; i += 1) {
+ array.push(mockSearchResult);
}
-
-
- beforeEach(function () {
- mockScope = jasmine.createSpyObj(
- "$scope",
- [ "$watch" ]
- );
- mockScope.ngModel = {};
- mockScope.ngModel.input = "test input";
- mockScope.ngModel.checked = {};
- mockScope.ngModel.checked['mock.type'] = true;
+ return array;
+ }
+
+
+ beforeEach(function () {
+ mockScope = jasmine.createSpyObj(
+ '$scope',
+ [ '$watch' ]
+ );
+ mockScope.ngModel = {};
+ mockScope.ngModel.input = 'test input';
+ mockScope.ngModel.checked = {};
+ mockScope.ngModel.checked['mock.type'] = true;
+ mockScope.ngModel.checkAll = true;
+
+ mockSearchService = jasmine.createSpyObj(
+ 'searchService',
+ [ 'query' ]
+ );
+ mockPromise = jasmine.createSpyObj(
+ 'promise',
+ [ 'then' ]
+ );
+ mockSearchService.query.andReturn(mockPromise);
+
+ mockTypes = [{key: 'mock.type', name: 'Mock Type', glyph: '?'}];
+
+ mockSearchResult = jasmine.createSpyObj(
+ 'searchResult',
+ [ '' ]
+ );
+ mockDomainObject = jasmine.createSpyObj(
+ 'domainObject',
+ [ 'getModel' ]
+ );
+ mockSearchResult.object = mockDomainObject;
+ mockDomainObject.getModel.andReturn({name: 'Mock Object', type: 'mock.type'});
+
+ controller = new SearchController(mockScope, mockSearchService, mockTypes);
+ controller.search();
+ });
+
+ it('has a default number of results per page', function () {
+ expect(controller.RESULTS_PER_PAGE).toBe(20);
+ });
+
+ it('sends queries to the search service', function () {
+ expect(mockSearchService.query).toHaveBeenCalledWith(
+ 'test input',
+ controller.RESULTS_PER_PAGE,
+ jasmine.any(Function)
+ );
+ });
+
+ describe('filter query function', function () {
+ it('returns true when all types allowed', function () {
mockScope.ngModel.checkAll = true;
-
- mockSearchService = jasmine.createSpyObj(
- "searchService",
- [ "query" ]
- );
- mockPromise = jasmine.createSpyObj(
- "promise",
- [ "then" ]
- );
- mockSearchService.query.andReturn(mockPromise);
-
- mockTypes = [{key: 'mock.type', name: 'Mock Type', glyph: '?'}];
-
- mockSearchResult = jasmine.createSpyObj(
- "searchResult",
- [ "" ]
- );
- mockDomainObject = jasmine.createSpyObj(
- "domainObject",
- [ "getModel" ]
- );
- mockSearchResult.object = mockDomainObject;
- mockDomainObject.getModel.andReturn({name: 'Mock Object', type: 'mock.type'});
-
- controller = new SearchController(mockScope, mockSearchService, mockTypes);
- controller.search();
- });
-
- it("sends queries to the search service", function () {
- expect(mockSearchService.query).toHaveBeenCalled();
- });
-
- it("populates the results with results from the search service", function () {
- expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
- mockPromise.then.mostRecentCall.args[0]({hits: []});
-
- expect(mockScope.results).toBeDefined();
- });
-
- it("is loading until the service's promise fufills", function () {
- // Send query
- controller.search();
- expect(mockScope.loading).toBeTruthy();
-
- // Then resolve the promises
- mockPromise.then.mostRecentCall.args[0]({hits: []});
- expect(mockScope.loading).toBeFalsy();
+ controller.onFilterChange();
+ var filterFn = mockSearchService.query.mostRecentCall.args[2];
+ expect(filterFn('askbfa')).toBe(true);
});
-
- it("displays only some results when there are many", function () {
- expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
- mockPromise.then.mostRecentCall.args[0]({hits: bigArray(100)});
-
- expect(mockScope.results).toBeDefined();
- expect(mockScope.results.length).toBeLessThan(100);
- });
-
- it("detects when there are more results", function () {
+ it('returns true only for matching checked types', function () {
mockScope.ngModel.checkAll = false;
-
- expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
- mockPromise.then.mostRecentCall.args[0]({
- hits: bigArray(INITIAL_LOAD_NUMBER + 5),
- total: INITIAL_LOAD_NUMBER + 5
- });
- // bigArray gives searchResults of type 'mock.type'
- mockScope.ngModel.checked['mock.type'] = false;
- mockScope.ngModel.checked['mock.type.2'] = true;
-
- expect(controller.areMore()).toBeFalsy();
-
- mockScope.ngModel.checked['mock.type'] = true;
-
- expect(controller.areMore()).toBeTruthy();
- });
-
- it("can load more results", function () {
- var oldSize;
-
- expect(mockPromise.then).toHaveBeenCalled();
- mockPromise.then.mostRecentCall.args[0]({
- hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1),
- total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
- });
- // These hits and total lengths are the case where the controller
- // DOES NOT have to re-search to load more results
- oldSize = mockScope.results.length;
-
- expect(controller.areMore()).toBeTruthy();
-
- controller.loadMore();
- expect(mockScope.results.length).toBeGreaterThan(oldSize);
- });
-
- it("can re-search to load more results", function () {
- var oldSize,
- oldCallCount;
-
- expect(mockPromise.then).toHaveBeenCalled();
- mockPromise.then.mostRecentCall.args[0]({
- hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT - 1),
- total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
- });
- // These hits and total lengths are the case where the controller
- // DOES have to re-search to load more results
- oldSize = mockScope.results.length;
- oldCallCount = mockPromise.then.callCount;
- expect(controller.areMore()).toBeTruthy();
-
- controller.loadMore();
- expect(mockPromise.then).toHaveBeenCalled();
- // Make sure that a NEW call to search has been made
- expect(oldCallCount).toBeLessThan(mockPromise.then.callCount);
- mockPromise.then.mostRecentCall.args[0]({
- hits: bigArray(INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1),
- total: INITIAL_LOAD_NUMBER + LOAD_INCREMENT + 1
- });
- expect(mockScope.results.length).toBeGreaterThan(oldSize);
+ controller.onFilterChange();
+ var filterFn = mockSearchService.query.mostRecentCall.args[2];
+ expect(filterFn({type: 'mock.type'})).toBe(true);
+ expect(filterFn({type: 'other.type'})).toBe(false);
});
-
- it("sets the ngModel.search flag", function () {
- // Flag should be true with nonempty input
- expect(mockScope.ngModel.search).toEqual(true);
-
- // Flag should be flase with empty input
- mockScope.ngModel.input = "";
- controller.search();
- mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
- expect(mockScope.ngModel.search).toEqual(false);
-
- // Both the empty string and undefined should be 'empty input'
- mockScope.ngModel.input = undefined;
- controller.search();
- mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
- expect(mockScope.ngModel.search).toEqual(false);
+ });
+
+ it('populates the results with results from the search service', function () {
+ expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
+ mockPromise.then.mostRecentCall.args[0]({hits: ['a']});
+
+ expect(mockScope.results.length).toBe(1);
+ expect(mockScope.results).toContain('a');
+ });
+
+ it('is loading until the service\'s promise fufills', function () {
+ expect(mockScope.loading).toBeTruthy();
+
+ // Then resolve the promises
+ mockPromise.then.mostRecentCall.args[0]({hits: []});
+ expect(mockScope.loading).toBeFalsy();
+ });
+
+ it('detects when there are more results', function () {
+ mockPromise.then.mostRecentCall.args[0]({
+ hits: bigArray(controller.RESULTS_PER_PAGE),
+ total: controller.RESULTS_PER_PAGE + 5
});
-
- it("has a default results list to filter from", function () {
- expect(mockScope.ngModel.filter()).toBeDefined();
+
+ expect(mockScope.results.length).toBe(controller.RESULTS_PER_PAGE);
+ expect(controller.areMore()).toBeTruthy();
+
+ controller.loadMore();
+
+ expect(mockSearchService.query).toHaveBeenCalledWith(
+ 'test input',
+ controller.RESULTS_PER_PAGE * 2,
+ jasmine.any(Function)
+ );
+
+ mockPromise.then.mostRecentCall.args[0]({
+ hits: bigArray(controller.RESULTS_PER_PAGE + 5),
+ total: controller.RESULTS_PER_PAGE + 5
});
+
+ expect(mockScope.results.length)
+ .toBe(controller.RESULTS_PER_PAGE + 5);
+
+ expect(controller.areMore()).toBe(false);
+ });
+
+ it('sets the ngModel.search flag', function () {
+ // Flag should be true with nonempty input
+ expect(mockScope.ngModel.search).toEqual(true);
+
+ // Flag should be flase with empty input
+ mockScope.ngModel.input = '';
+ controller.search();
+ mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
+ expect(mockScope.ngModel.search).toEqual(false);
+
+ // Both the empty string and undefined should be 'empty input'
+ mockScope.ngModel.input = undefined;
+ controller.search();
+ mockPromise.then.mostRecentCall.args[0]({hits: [], total: 0});
+ expect(mockScope.ngModel.search).toEqual(false);
+ });
+
+ it('attaches a filter function to scope', function () {
+ expect(mockScope.ngModel.filter).toEqual(jasmine.any(Function));
});
- }
-); \ No newline at end of file
+ });
+});
diff --git a/platform/search/test/services/GenericSearchProviderSpec.js b/platform/search/test/services/GenericSearchProviderSpec.js
index 2da7cd343..cc80e4210 100644
--- a/platform/search/test/services/GenericSearchProviderSpec.js
+++ b/platform/search/test/services/GenericSearchProviderSpec.js
@@ -19,175 +19,321 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-/*global define,describe,it,expect,beforeEach,jasmine*/
+/*global define,describe,it,expect,beforeEach,jasmine,Promise,spyOn,waitsFor,
+ runs*/
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
-define(
- ["../../src/services/GenericSearchProvider"],
- function (GenericSearchProvider) {
- "use strict";
-
- describe("The generic search provider ", function () {
- var mockQ,
- mockTimeout,
- mockDeferred,
- mockObjectService,
- mockObjectPromise,
- mockDomainObjects,
- mockCapability,
- mockCapabilityPromise,
- mockWorkerService,
- mockWorker,
- mockRoots = ['root1', 'root2'],
- provider,
- mockProviderResults;
+define([
+ "../../src/services/GenericSearchProvider"
+], function (
+ GenericSearchProvider
+) {
+ "use strict";
+ describe('GenericSearchProvider', function () {
+ var $q,
+ $log,
+ modelService,
+ models,
+ workerService,
+ worker,
+ topic,
+ mutationTopic,
+ ROOTS,
+ provider;
+
+ beforeEach(function () {
+ $q = jasmine.createSpyObj(
+ '$q',
+ ['defer']
+ );
+ $log = jasmine.createSpyObj(
+ '$log',
+ ['warn']
+ );
+ models = {};
+ modelService = jasmine.createSpyObj(
+ 'modelService',
+ ['getModels']
+ );
+ modelService.getModels.andReturn(Promise.resolve(models));
+ workerService = jasmine.createSpyObj(
+ 'workerService',
+ ['run']
+ );
+ worker = jasmine.createSpyObj(
+ 'worker',
+ [
+ 'postMessage',
+ 'addEventListener'
+ ]
+ );
+ workerService.run.andReturn(worker);
+ topic = jasmine.createSpy('topic');
+ mutationTopic = jasmine.createSpyObj(
+ 'mutationTopic',
+ ['listen']
+ );
+ topic.andReturn(mutationTopic);
+ ROOTS = [
+ 'mine'
+ ];
+
+ spyOn(GenericSearchProvider.prototype, 'scheduleForIndexing');
+
+ provider = new GenericSearchProvider(
+ $q,
+ $log,
+ modelService,
+ workerService,
+ topic,
+ ROOTS
+ );
+ });
+
+ it('listens for general mutation', function () {
+ expect(topic).toHaveBeenCalledWith('mutation');
+ expect(mutationTopic.listen)
+ .toHaveBeenCalledWith(jasmine.any(Function));
+ });
+
+ it('reschedules indexing when mutation occurs', function () {
+ var mockDomainObject =
+ jasmine.createSpyObj('domainObj', ['getId']);
+ mockDomainObject.getId.andReturn("some-id");
+ mutationTopic.listen.mostRecentCall.args[0](mockDomainObject);
+ expect(provider.scheduleForIndexing).toHaveBeenCalledWith('some-id');
+ });
+
+ it('starts indexing roots', function () {
+ expect(provider.scheduleForIndexing).toHaveBeenCalledWith('mine');
+ });
+
+ it('runs a worker', function () {
+ expect(workerService.run)
+ .toHaveBeenCalledWith('genericSearchWorker');
+ });
+
+ it('listens for messages from worker', function () {
+ expect(worker.addEventListener)
+ .toHaveBeenCalledWith('message', jasmine.any(Function));
+ spyOn(provider, 'onWorkerMessage');
+ worker.addEventListener.mostRecentCall.args[1]('mymessage');
+ expect(provider.onWorkerMessage).toHaveBeenCalledWith('mymessage');
+ });
+
+ it('has a maximum number of concurrent requests', function () {
+ expect(provider.MAX_CONCURRENT_REQUESTS).toBe(100);
+ });
+
+ describe('scheduleForIndexing', function () {
beforeEach(function () {
- var i;
-
- mockQ = jasmine.createSpyObj(
- "$q",
- [ "defer" ]
- );
- mockDeferred = jasmine.createSpyObj(
- "deferred",
- [ "resolve", "reject"]
- );
- mockDeferred.promise = "mock promise";
- mockQ.defer.andReturn(mockDeferred);
-
- mockTimeout = jasmine.createSpy("$timeout");
-
- mockObjectService = jasmine.createSpyObj(
- "objectService",
- [ "getObjects" ]
- );
- mockObjectPromise = jasmine.createSpyObj(
- "promise",
- [ "then", "catch" ]
- );
- mockObjectService.getObjects.andReturn(mockObjectPromise);
-
-
- mockWorkerService = jasmine.createSpyObj(
- "workerService",
- [ "run" ]
- );
- mockWorker = jasmine.createSpyObj(
- "worker",
- [ "postMessage" ]
- );
- mockWorkerService.run.andReturn(mockWorker);
-
- mockCapabilityPromise = jasmine.createSpyObj(
- "promise",
- [ "then", "catch" ]
- );
-
- mockDomainObjects = {};
- for (i = 0; i < 4; i += 1) {
- mockDomainObjects[i] = (
- jasmine.createSpyObj(
- "domainObject",
- [ "getId", "getModel", "hasCapability", "getCapability", "useCapability" ]
- )
- );
- mockDomainObjects[i].getId.andReturn(i);
- mockDomainObjects[i].getCapability.andReturn(mockCapability);
- mockDomainObjects[i].useCapability.andReturn(mockCapabilityPromise);
- }
- // Give the first object children
- mockDomainObjects[0].hasCapability.andReturn(true);
- mockCapability = jasmine.createSpyObj(
- "capability",
- [ "invoke", "listen" ]
- );
- mockCapability.invoke.andReturn(mockCapabilityPromise);
- mockDomainObjects[0].getCapability.andReturn(mockCapability);
-
- provider = new GenericSearchProvider(mockQ, mockTimeout, mockObjectService, mockWorkerService, mockRoots);
- });
-
- it("indexes tree on initialization", function () {
- expect(mockObjectService.getObjects).toHaveBeenCalled();
- expect(mockObjectPromise.then).toHaveBeenCalled();
-
- // Call through the root-getting part
- mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
-
- // Call through the children-getting part
- mockTimeout.mostRecentCall.args[0]();
- // Array argument indicates multiple children
- mockCapabilityPromise.then.mostRecentCall.args[0]([]);
- mockTimeout.mostRecentCall.args[0]();
- // Call again, but for single child
- mockCapabilityPromise.then.mostRecentCall.args[0]({});
- mockTimeout.mostRecentCall.args[0]();
-
- expect(mockWorker.postMessage).toHaveBeenCalled();
- });
-
- it("when indexing, listens for composition changes", function () {
- var mockListener = {composition: {}};
-
- // Call indexItems
- mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
-
- // Call through listening for changes
- expect(mockCapability.listen).toHaveBeenCalled();
- mockCapability.listen.mostRecentCall.args[0](mockListener);
- expect(mockObjectService.getObjects).toHaveBeenCalled();
- mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
- });
-
- it("sends search queries to the worker", function () {
- var timestamp = Date.now();
- provider.query(' test "query" ', timestamp, 1, 2);
- expect(mockWorker.postMessage).toHaveBeenCalledWith({
- request: "search",
- input: ' test "query" ',
- timestamp: timestamp,
- maxNumber: 1,
- timeout: 2
+ provider.scheduleForIndexing.andCallThrough();
+ spyOn(provider, 'keepIndexing');
+ });
+
+ it('tracks ids to index', function () {
+ expect(provider.indexedIds.a).not.toBeDefined();
+ expect(provider.pendingIndex.a).not.toBeDefined();
+ expect(provider.idsToIndex).not.toContain('a');
+ provider.scheduleForIndexing('a');
+ expect(provider.indexedIds.a).toBeDefined();
+ expect(provider.pendingIndex.a).toBeDefined();
+ expect(provider.idsToIndex).toContain('a');
+ });
+
+ it('calls keep indexing', function () {
+ provider.scheduleForIndexing('a');
+ expect(provider.keepIndexing).toHaveBeenCalled();
+ });
+ });
+
+ describe('keepIndexing', function () {
+ it('calls beginIndexRequest until at maximum', function () {
+ spyOn(provider, 'beginIndexRequest').andCallThrough();
+ provider.pendingRequests = 9;
+ provider.idsToIndex = ['a', 'b', 'c'];
+ provider.MAX_CONCURRENT_REQUESTS = 10;
+ provider.keepIndexing();
+ expect(provider.beginIndexRequest).toHaveBeenCalled();
+ expect(provider.beginIndexRequest.calls.length).toBe(1);
+ });
+
+ it('calls beginIndexRequest for all ids to index', function () {
+ spyOn(provider, 'beginIndexRequest').andCallThrough();
+ provider.pendingRequests = 0;
+ provider.idsToIndex = ['a', 'b', 'c'];
+ provider.MAX_CONCURRENT_REQUESTS = 10;
+ provider.keepIndexing();
+ expect(provider.beginIndexRequest).toHaveBeenCalled();
+ expect(provider.beginIndexRequest.calls.length).toBe(3);
+ });
+
+ it('does not index when at capacity', function () {
+ spyOn(provider, 'beginIndexRequest');
+ provider.pendingRequests = 10;
+ provider.idsToIndex.push('a');
+ provider.MAX_CONCURRENT_REQUESTS = 10;
+ provider.keepIndexing();
+ expect(provider.beginIndexRequest).not.toHaveBeenCalled();
+ });
+
+ it('does not index when no ids to index', function () {
+ spyOn(provider, 'beginIndexRequest');
+ provider.pendingRequests = 0;
+ provider.MAX_CONCURRENT_REQUESTS = 10;
+ provider.keepIndexing();
+ expect(provider.beginIndexRequest).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('index', function () {
+ it('sends index message to worker', function () {
+ var id = 'anId',
+ model = {};
+
+ provider.index(id, model);
+ expect(worker.postMessage).toHaveBeenCalledWith({
+ request: 'index',
+ id: id,
+ model: model
});
});
-
- it("gives an empty result for an empty query", function () {
- var timestamp = Date.now(),
- queryOutput;
-
- queryOutput = provider.query('', timestamp, 1, 2);
- expect(queryOutput.hits).toEqual([]);
- expect(queryOutput.total).toEqual(0);
-
- queryOutput = provider.query();
- expect(queryOutput.hits).toEqual([]);
- expect(queryOutput.total).toEqual(0);
- });
-
- it("handles responses from the worker", function () {
- var timestamp = Date.now(),
- event = {
- data: {
- request: "search",
- results: {
- 1: 1,
- 2: 2
+
+ it('schedules composed ids for indexing', function () {
+ var id = 'anId',
+ model = {composition: ['abc', 'def']};
+
+ provider.index(id, model);
+ expect(provider.scheduleForIndexing)
+ .toHaveBeenCalledWith('abc');
+ expect(provider.scheduleForIndexing)
+ .toHaveBeenCalledWith('def');
+ });
+ });
+
+ describe('beginIndexRequest', function () {
+
+ beforeEach(function () {
+ provider.pendingRequests = 0;
+ provider.pendingIds = {'abc': true};
+ provider.idsToIndex = ['abc'];
+ models.abc = {};
+ spyOn(provider, 'index');
+ });
+
+ it('removes items from queue', function () {
+ provider.beginIndexRequest();
+ expect(provider.idsToIndex.length).toBe(0);
+ });
+
+ it('tracks number of pending requests', function () {
+ provider.beginIndexRequest();
+ expect(provider.pendingRequests).toBe(1);
+ waitsFor(function () {
+ return provider.pendingRequests === 0;
+ });
+ runs(function () {
+ expect(provider.pendingRequests).toBe(0);
+ });
+ });
+
+ it('indexes objects', function () {
+ provider.beginIndexRequest();
+ waitsFor(function () {
+ return provider.pendingRequests === 0;
+ });
+ runs(function () {
+ expect(provider.index)
+ .toHaveBeenCalledWith('abc', models.abc);
+ });
+ });
+
+ });
+
+
+ it('can dispatch searches to worker', function () {
+ spyOn(provider, 'makeQueryId').andReturn(428);
+ expect(provider.dispatchSearch('searchTerm', 100))
+ .toBe(428);
+
+ expect(worker.postMessage).toHaveBeenCalledWith({
+ request: 'search',
+ input: 'searchTerm',
+ maxResults: 100,
+ queryId: 428
+ });
+ });
+
+ it('can generate queryIds', function () {
+ expect(provider.makeQueryId()).toEqual(jasmine.any(Number));
+ });
+
+ it('can query for terms', function () {
+ var deferred = {promise: {}};
+ spyOn(provider, 'dispatchSearch').andReturn(303);
+ $q.defer.andReturn(deferred);
+
+ expect(provider.query('someTerm', 100)).toBe(deferred.promise);
+ expect(provider.pendingQueries[303]).toBe(deferred);
+ });
+
+ describe('onWorkerMessage', function () {
+ var pendingQuery;
+ beforeEach(function () {
+ pendingQuery = jasmine.createSpyObj(
+ 'pendingQuery',
+ ['resolve']
+ );
+ provider.pendingQueries[143] = pendingQuery;
+ });
+
+ it('resolves pending searches', function () {
+ provider.onWorkerMessage({
+ data: {
+ request: 'search',
+ total: 2,
+ results: [
+ {
+ item: {
+ id: 'abc',
+ model: {id: 'abc'}
+ },
+ matchCount: 4
},
- total: 2,
- timedOut: false,
- timestamp: timestamp
- }
- };
-
- provider.query(' test "query" ', timestamp);
- mockWorker.onmessage(event);
- mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
- expect(mockDeferred.resolve).toHaveBeenCalled();
- });
-
+ {
+ item: {
+ id: 'def',
+ model: {id: 'def'}
+ },
+ matchCount: 2
+ }
+ ],
+ queryId: 143
+ }
+ });
+
+ expect(pendingQuery.resolve)
+ .toHaveBeenCalledWith({
+ total: 2,
+ hits: [{
+ id: 'abc',
+ model: {id: 'abc'},
+ score: 4
+ }, {
+ id: 'def',
+ model: {id: 'def'},
+ score: 2
+ }]
+ });
+
+ expect(provider.pendingQueries[143]).not.toBeDefined();
+
+ });
+
});
- }
-); \ No newline at end of file
+
+ });
+});
diff --git a/platform/search/test/services/GenericSearchWorkerSpec.js b/platform/search/test/services/GenericSearchWorkerSpec.js
index b95ec5a1b..20afb4c78 100644
--- a/platform/search/test/services/GenericSearchWorkerSpec.js
+++ b/platform/search/test/services/GenericSearchWorkerSpec.js
@@ -4,12 +4,12 @@
* 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.
+ * '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
+ * 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.
@@ -19,114 +19,205 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-/*global define,describe,it,expect,runs,waitsFor,beforeEach,jasmine,Worker,require*/
+/*global define,describe,it,expect,runs,waitsFor,beforeEach,jasmine,Worker,
+ require,afterEach*/
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
-define(
- [],
- function () {
- "use strict";
-
- describe("The generic search worker ", function () {
- // If this test fails, make sure this path is correct
- var worker = new Worker(require.toUrl('platform/search/src/services/GenericSearchWorker.js')),
- numObjects = 5;
-
- beforeEach(function () {
- var i;
- for (i = 0; i < numObjects; i += 1) {
- worker.postMessage(
- {
- request: "index",
- id: i,
- model: {
- name: "object " + i,
- id: i,
- type: "something"
- }
- }
- );
- }
- });
-
- it("searches can reach all objects", function () {
- var flag = false,
- workerOutput,
- resultsLength = 0;
-
- // Search something that should return all objects
- runs(function () {
- worker.postMessage(
- {
- request: "search",
- input: "object",
- maxNumber: 100,
- timestamp: Date.now(),
- timeout: 1000
- }
- );
- });
-
- worker.onmessage = function (event) {
- var id;
-
- workerOutput = event.data;
- for (id in workerOutput.results) {
- resultsLength += 1;
- }
- flag = true;
- };
-
- waitsFor(function () {
- return flag;
- }, "The worker should be searching", 1000);
-
- runs(function () {
- expect(workerOutput).toBeDefined();
- expect(resultsLength).toEqual(numObjects);
+define([
+
+], function (
+
+) {
+ 'use strict';
+
+ describe('GenericSearchWorker', function () {
+ // If this test fails, make sure this path is correct
+ var worker,
+ objectX,
+ objectY,
+ objectZ,
+ itemsToIndex,
+ onMessage,
+ data,
+ waitForResult;
+
+ beforeEach(function () {
+ worker = new Worker(
+ require.toUrl('platform/search/src/services/GenericSearchWorker.js')
+ );
+
+ objectX = {
+ id: 'x',
+ model: {name: 'object xx'}
+ };
+ objectY = {
+ id: 'y',
+ model: {name: 'object yy'}
+ };
+ objectZ = {
+ id: 'z',
+ model: {name: 'object zz'}
+ };
+ itemsToIndex = [
+ objectX,
+ objectY,
+ objectZ
+ ];
+
+ itemsToIndex.forEach(function (item) {
+ worker.postMessage({
+ request: 'index',
+ id: item.id,
+ model: item.model
});
});
-
- it("searches return only matches", function () {
- var flag = false,
- workerOutput,
- resultsLength = 0;
-
- // Search something that should return 1 object
- runs(function () {
- worker.postMessage(
- {
- request: "search",
- input: "2",
- maxNumber: 100,
- timestamp: Date.now(),
- timeout: 1000
- }
- );
- });
-
- worker.onmessage = function (event) {
- var id;
-
- workerOutput = event.data;
- for (id in workerOutput.results) {
- resultsLength += 1;
- }
- flag = true;
- };
-
+
+ onMessage = jasmine.createSpy('onMessage');
+ worker.addEventListener('message', onMessage);
+
+ waitForResult = function () {
waitsFor(function () {
- return flag;
- }, "The worker should be searching", 1000);
-
- runs(function () {
- expect(workerOutput).toBeDefined();
- expect(resultsLength).toEqual(1);
- expect(workerOutput.results[2]).toBeDefined();
+ if (onMessage.calls.length > 0) {
+ data = onMessage.calls[0].args[0].data;
+ return true;
+ }
+ return false;
});
+ };
+ });
+
+ afterEach(function () {
+ worker.terminate();
+ });
+
+ it('returns search results for partial term matches', function () {
+
+ worker.postMessage({
+ request: 'search',
+ input: 'obj',
+ maxResults: 100,
+ queryId: 123
+ });
+
+ waitForResult();
+
+ runs(function () {
+ expect(onMessage).toHaveBeenCalled();
+
+ expect(data.request).toBe('search');
+ expect(data.total).toBe(3);
+ expect(data.queryId).toBe(123);
+ expect(data.results.length).toBe(3);
+ expect(data.results[0].item.id).toBe('x');
+ expect(data.results[0].item.model).toEqual(objectX.model);
+ expect(data.results[0].matchCount).toBe(1);
+ expect(data.results[1].item.id).toBe('y');
+ expect(data.results[1].item.model).toEqual(objectY.model);
+ expect(data.results[1].matchCount).toBe(1);
+ expect(data.results[2].item.id).toBe('z');
+ expect(data.results[2].item.model).toEqual(objectZ.model);
+ expect(data.results[2].matchCount).toBe(1);
+ });
+ });
+
+ it('scores exact term matches higher', function () {
+ worker.postMessage({
+ request: 'search',
+ input: 'object',
+ maxResults: 100,
+ queryId: 234
+ });
+
+ waitForResult();
+
+ runs(function () {
+ expect(data.queryId).toBe(234);
+ expect(data.results.length).toBe(3);
+ expect(data.results[0].item.id).toBe('x');
+ expect(data.results[0].matchCount).toBe(1.5);
+ });
+ });
+
+ it('can find partial term matches', function () {
+ worker.postMessage({
+ request: 'search',
+ input: 'x',
+ maxResults: 100,
+ queryId: 345
+ });
+
+ waitForResult();
+
+ runs(function () {
+ expect(data.queryId).toBe(345);
+ expect(data.results.length).toBe(1);
+ expect(data.results[0].item.id).toBe('x');
+ expect(data.results[0].matchCount).toBe(1);
+ });
+ });
+
+ it('matches individual terms', function () {
+ worker.postMessage({
+ request: 'search',
+ input: 'x y z',
+ maxResults: 100,
+ queryId: 456
+ });
+
+ waitForResult();
+
+ runs(function () {
+ expect(data.queryId).toBe(456);
+ expect(data.results.length).toBe(3);
+ expect(data.results[0].item.id).toBe('x');
+ expect(data.results[0].matchCount).toBe(1);
+ expect(data.results[1].item.id).toBe('y');
+ expect(data.results[1].matchCount).toBe(1);
+ expect(data.results[2].item.id).toBe('z');
+ expect(data.results[1].matchCount).toBe(1);
+ });
+ });
+
+ it('scores exact matches highest', function () {
+ worker.postMessage({
+ request: 'search',
+ input: 'object xx',
+ maxResults: 100,
+ queryId: 567
+ });
+
+ waitForResult();
+
+ runs(function () {
+ expect(data.queryId).toBe(567);
+ expect(data.results.length).toBe(3);
+ expect(data.results[0].item.id).toBe('x');
+ expect(data.results[0].matchCount).toBe(103);
+ expect(data.results[1].matchCount).toBe(1.5);
+ expect(data.results[2].matchCount).toBe(1.5);
+ });
+ });
+
+ it('scores multiple term match above single match', function () {
+ worker.postMessage({
+ request: 'search',
+ input: 'obj x',
+ maxResults: 100,
+ queryId: 678
+ });
+
+ waitForResult();
+
+ runs(function () {
+ expect(data.queryId).toBe(678);
+ expect(data.results.length).toBe(3);
+ expect(data.results[0].item.id).toBe('x');
+ expect(data.results[0].matchCount).toBe(2);
+ expect(data.results[1].matchCount).toBe(1);
+ expect(data.results[2].matchCount).toBe(1);
});
});
- }
-); \ No newline at end of file
+ });
+});
diff --git a/platform/search/test/services/SearchAggregatorSpec.js b/platform/search/test/services/SearchAggregatorSpec.js
index 3205f0f9e..f8bee0dcc 100644
--- a/platform/search/test/services/SearchAggregatorSpec.js
+++ b/platform/search/test/services/SearchAggregatorSpec.js
@@ -19,83 +19,244 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
-/*global define,describe,it,expect,beforeEach,jasmine*/
+/*global define,describe,it,expect,beforeEach,jasmine,Promise,waitsFor,spyOn*/
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
-define(
- ["../../src/services/SearchAggregator"],
- function (SearchAggregator) {
- "use strict";
-
- describe("The search aggregator ", function () {
- var mockQ,
- mockPromise,
- mockProviders = [],
- aggregator,
- mockProviderResults = [],
- mockAggregatorResults,
- i;
-
- beforeEach(function () {
- mockQ = jasmine.createSpyObj(
- "$q",
- [ "all" ]
+define([
+ "../../src/services/SearchAggregator"
+], function (SearchAggregator) {
+ "use strict";
+
+ describe("SearchAggregator", function () {
+ var $q,
+ objectService,
+ providers,
+ aggregator;
+
+ beforeEach(function () {
+ $q = jasmine.createSpyObj(
+ '$q',
+ ['all']
+ );
+ $q.all.andReturn(Promise.resolve([]));
+ objectService = jasmine.createSpyObj(
+ 'objectService',
+ ['getObjects']
+ );
+ providers = [];
+ aggregator = new SearchAggregator($q, objectService, providers);
+ });
+
+ it("has a fudge factor", function () {
+ expect(aggregator.FUDGE_FACTOR).toBe(5);
+ });
+
+ it("has default max results", function () {
+ expect(aggregator.DEFAULT_MAX_RESULTS).toBe(100);
+ });
+
+ it("can order model results by score", function () {
+ var modelResults = {
+ hits: [
+ {score: 1},
+ {score: 23},
+ {score: 11}
+ ]
+ },
+ sorted = aggregator.orderByScore(modelResults);
+
+ expect(sorted.hits).toEqual([
+ {score: 23},
+ {score: 11},
+ {score: 1}
+ ]);
+ });
+
+ it('filters results without a function', function () {
+ var modelResults = {
+ hits: [
+ {thing: 1},
+ {thing: 2}
+ ],
+ total: 2
+ },
+ filtered = aggregator.applyFilter(modelResults);
+
+ expect(filtered.hits).toEqual([
+ {thing: 1},
+ {thing: 2}
+ ]);
+
+ expect(filtered.total).toBe(2);
+ });
+
+ it('filters results with a function', function () {
+ var modelResults = {
+ hits: [
+ {model: {thing: 1}},
+ {model: {thing: 2}},
+ {model: {thing: 3}}
+ ],
+ total: 3
+ },
+ filterFunc = function (model) {
+ return model.thing < 2;
+ },
+ filtered = aggregator.applyFilter(modelResults, filterFunc);
+
+ expect(filtered.hits).toEqual([
+ {model: {thing: 1}}
+ ]);
+ expect(filtered.total).toBe(1);
+ });
+
+ it('can remove duplicates', function () {
+ var modelResults = {
+ hits: [
+ {id: 15},
+ {id: 23},
+ {id: 14},
+ {id: 23}
+ ],
+ total: 4
+ },
+ deduped = aggregator.removeDuplicates(modelResults);
+
+ expect(deduped.hits).toEqual([
+ {id: 15},
+ {id: 23},
+ {id: 14}
+ ]);
+ expect(deduped.total).toBe(3);
+ });
+
+ it('can convert model results to object results', function () {
+ var modelResults = {
+ hits: [
+ {id: 123, score: 5},
+ {id: 234, score: 1}
+ ],
+ total: 2
+ },
+ objects = {
+ 123: '123-object-hey',
+ 234: '234-object-hello'
+ },
+ promiseChainComplete = false;
+
+ objectService.getObjects.andReturn(Promise.resolve(objects));
+
+ aggregator
+ .asObjectResults(modelResults)
+ .then(function (objectResults) {
+ expect(objectResults).toEqual({
+ hits: [
+ {id: 123, score: 5, object: '123-object-hey'},
+ {id: 234, score: 1, object: '234-object-hello'}
+ ],
+ total: 2
+ });
+ })
+ .then(function () {
+ promiseChainComplete = true;
+ });
+
+ waitsFor(function () {
+ return promiseChainComplete;
+ });
+ });
+
+ it('can send queries to providers', function () {
+ var provider = jasmine.createSpyObj(
+ 'provider',
+ ['query']
);
- mockPromise = jasmine.createSpyObj(
- "promise",
- [ "then" ]
+ provider.query.andReturn('i prooomise!');
+ providers.push(provider);
+
+ aggregator.query('find me', 123, 'filter');
+ expect(provider.query)
+ .toHaveBeenCalledWith(
+ 'find me',
+ 123 * aggregator.FUDGE_FACTOR
);
- for (i = 0; i < 3; i += 1) {
- mockProviders.push(
- jasmine.createSpyObj(
- "mockProvider" + i,
- [ "query" ]
- )
- );
- mockProviders[i].query.andReturn(mockPromise);
- }
- mockQ.all.andReturn(mockPromise);
-
- aggregator = new SearchAggregator(mockQ, mockProviders);
- aggregator.query();
-
- for (i = 0; i < mockProviders.length; i += 1) {
- mockProviderResults.push({
+ expect($q.all).toHaveBeenCalledWith(['i prooomise!']);
+ });
+
+ it('supplies max results when none is provided', function () {
+ var provider = jasmine.createSpyObj(
+ 'provider',
+ ['query']
+ );
+ providers.push(provider);
+ aggregator.query('find me');
+ expect(provider.query).toHaveBeenCalledWith(
+ 'find me',
+ aggregator.DEFAULT_MAX_RESULTS * aggregator.FUDGE_FACTOR
+ );
+ });
+
+ it('can combine responses from multiple providers', function () {
+ var providerResponses = [
+ {
+ hits: [
+ 'oneHit',
+ 'twoHit'
+ ],
+ total: 2
+ },
+ {
+ hits: [
+ 'redHit',
+ 'blueHit',
+ 'by',
+ 'Pete'
+ ],
+ total: 4
+ }
+ ],
+ promiseChainResolved = false;
+
+ $q.all.andReturn(Promise.resolve(providerResponses));
+ spyOn(aggregator, 'orderByScore').andReturn('orderedByScore!');
+ spyOn(aggregator, 'applyFilter').andReturn('filterApplied!');
+ spyOn(aggregator, 'removeDuplicates')
+ .andReturn('duplicatesRemoved!');
+ spyOn(aggregator, 'asObjectResults').andReturn('objectResults');
+
+ aggregator
+ .query('something', 10, 'filter')
+ .then(function (objectResults) {
+ expect(aggregator.orderByScore).toHaveBeenCalledWith({
hits: [
- {
- id: i,
- score: 42 - i
- },
- {
- id: i + 1,
- score: 42 - (2 * i)
- }
- ]
+ 'oneHit',
+ 'twoHit',
+ 'redHit',
+ 'blueHit',
+ 'by',
+ 'Pete'
+ ],
+ total: 6
});
- }
- mockAggregatorResults = mockPromise.then.mostRecentCall.args[0](mockProviderResults);
- });
-
- it("sends queries to all providers", function () {
- for (i = 0; i < mockProviders.length; i += 1) {
- expect(mockProviders[i].query).toHaveBeenCalled();
- }
- });
-
- it("filters out duplicate objects", function () {
- expect(mockAggregatorResults.hits.length).toEqual(mockProviders.length + 1);
- expect(mockAggregatorResults.total).not.toBeLessThan(mockAggregatorResults.hits.length);
- });
-
- it("orders results by score", function () {
- for (i = 1; i < mockAggregatorResults.hits.length; i += 1) {
- expect(mockAggregatorResults.hits[i].score)
- .not.toBeGreaterThan(mockAggregatorResults.hits[i - 1].score);
- }
+ expect(aggregator.applyFilter)
+ .toHaveBeenCalledWith('orderedByScore!', 'filter');
+ expect(aggregator.removeDuplicates)
+ .toHaveBeenCalledWith('filterApplied!');
+ expect(aggregator.asObjectResults)
+ .toHaveBeenCalledWith('duplicatesRemoved!');
+
+ expect(objectResults).toBe('objectResults');
+ })
+ .then(function () {
+ promiseChainResolved = true;
+ });
+
+ waitsFor(function () {
+ return promiseChainResolved;
});
-
});
- }
-); \ No newline at end of file
+
+ });
+});
diff --git a/platform/telemetry/src/TelemetryAggregator.js b/platform/telemetry/src/TelemetryAggregator.js
index fb4cf81fe..86257befb 100644
--- a/platform/telemetry/src/TelemetryAggregator.js
+++ b/platform/telemetry/src/TelemetryAggregator.js
@@ -32,6 +32,30 @@ define(
"use strict";
/**
+ * Describes a request for telemetry data. Note that responses
+ * may contain either a sub- or superset of the requested data.
+ * @typedef TelemetryRequest
+ * @property {string} source an identifier for the relevant
+ * source of telemetry data
+ * @property {string} key an identifier for the specific
+ * series of telemetry data provided by that source
+ * @property {number} [start] the earliest domain value of
+ * interest for that telemetry data; for time-based
+ * domains, this is in milliseconds since the start
+ * of 1970
+ * @property {number} [end] the latest domain value of interest
+ * for that telemetry data; for time-based domains,
+ * this is in milliseconds since 1970
+ * @property {string} [domain] the domain for the query; if
+ * omitted, this will be whatever the "normal"
+ * domain is for a given telemetry series (the
+ * first domain from its metadata)
+ * @property {number} [size] if set, indicates the maximum number
+ * of data points of interest for this request (more
+ * recent domain values will be preferred)
+ */
+
+ /**
* Request telemetry data.
* @param {TelemetryRequest[]} requests and array of
* requests to be handled
diff --git a/platform/telemetry/src/TelemetryHandle.js b/platform/telemetry/src/TelemetryHandle.js
index 145edfc5d..ff77d7b9e 100644
--- a/platform/telemetry/src/TelemetryHandle.js
+++ b/platform/telemetry/src/TelemetryHandle.js
@@ -79,8 +79,7 @@ define(
/**
* Change the request duration.
- * @param {object|number} request the duration of historical
- * data to look at; or, the request to issue
+ * @param {TelemetryRequest} request the request to issue
* @param {Function} [callback] a callback that will be
* invoked as new data becomes available, with the
* domain object for which new data is available.
@@ -107,6 +106,29 @@ define(
.then(issueRequests);
};
+ /**
+ * Get the latest telemetry datum for this domain object. This
+ * will be from real-time telemetry, unless an index is specified,
+ * in which case it will be pulled from the historical telemetry
+ * series at the specified index. If there is no latest available
+ * datum, this will return undefined.
+ *
+ * @param {DomainObject} domainObject the object of interest
+ * @param {number} [index] the index of the data of interest
+ * @returns {TelemetryDatum} the most recent datum
+ */
+ self.getDatum = function (telemetryObject, index) {
+ function makeNewDatum(series) {
+ return series ?
+ subscription.makeDatum(telemetryObject, series, index) :
+ undefined;
+ }
+
+ return typeof index !== 'number' ?
+ subscription.getDatum(telemetryObject) :
+ makeNewDatum(this.getSeries(telemetryObject));
+ };
+
return self;
}
diff --git a/platform/telemetry/src/TelemetrySubscription.js b/platform/telemetry/src/TelemetrySubscription.js
index 8b4d7d7a9..5dcab54b9 100644
--- a/platform/telemetry/src/TelemetrySubscription.js
+++ b/platform/telemetry/src/TelemetrySubscription.js
@@ -123,25 +123,6 @@ define(
telemetryCapability.getMetadata();
}
- // From a telemetry series, retrieve a single data point
- // containing all fields for domains/ranges
- function makeDatum(domainObject, series, index) {
- var metadata = lookupMetadata(domainObject),
- result = {};
-
- (metadata.domains || []).forEach(function (domain) {
- result[domain.key] =
- series.getDomainValue(index, domain.key);
- });
-
- (metadata.ranges || []).forEach(function (range) {
- result[range.key] =
- series.getRangeValue(index, range.key);
- });
-
- return result;
- }
-
// Update the latest telemetry data for a specific
// domain object. This will notify listeners.
function update(domainObject, series) {
@@ -160,7 +141,7 @@ define(
pool.put(domainObject.getId(), {
domain: series.getDomainValue(count - 1),
range: series.getRangeValue(count - 1),
- datum: makeDatum(domainObject, series, count - 1)
+ datum: self.makeDatum(domainObject, series, count - 1)
});
}
}
@@ -188,6 +169,11 @@ define(
function cacheObjectReferences(objects) {
self.telemetryObjects = objects;
self.metadatas = objects.map(lookupMetadata);
+
+ self.metadataById = {};
+ objects.forEach(function (obj, i) {
+ self.metadataById[obj.getId()] = self.metadatas[i];
+ });
// Fire callback, as this will be the first time that
// telemetry objects are available, or these objects
// will have changed.
@@ -241,6 +227,34 @@ define(
this.unlistenToMutation = addMutationListener();
}
+
+ /**
+ * From a telemetry series, retrieve a single data point
+ * containing all fields for domains/ranges
+ * @private
+ */
+ TelemetrySubscription.prototype.makeDatum = function (domainObject, series, index) {
+ var id = domainObject && domainObject.getId(),
+ metadata = (id && this.metadataById[id]) || {},
+ result = {};
+
+ (metadata.domains || []).forEach(function (domain) {
+ result[domain.key] =
+ series.getDomainValue(index, domain.key);
+ });
+
+ (metadata.ranges || []).forEach(function (range) {
+ result[range.key] =
+ series.getRangeValue(index, range.key);
+ });
+
+ return result;
+ };
+
+ /**
+ * Terminate all underlying subscriptions.
+ * @private
+ */
TelemetrySubscription.prototype.unsubscribeAll = function () {
var $q = this.$q;
return this.unsubscribePromise.then(function (unsubscribes) {
diff --git a/test-main.js b/test-main.js
index 46740a93b..18b5f2a0d 100644
--- a/test-main.js
+++ b/test-main.js
@@ -44,7 +44,8 @@ require.config({
paths: {
'es6-promise': 'platform/framework/lib/es6-promise-2.0.0.min',
- 'moment-duration-format': 'warp/clock/lib/moment-duration-format'
+ 'moment-duration-format': 'warp/clock/lib/moment-duration-format',
+ 'uuid': 'platform/commonUI/browse/lib/uuid'
},
// dynamically load all test files