diff options
author | Charles Hacskaylo <charles.f.hacskaylo@nasa.gov> | 2015-10-26 23:20:26 +0300 |
---|---|---|
committer | Charles Hacskaylo <charles.f.hacskaylo@nasa.gov> | 2015-10-26 23:20:26 +0300 |
commit | 6868bfd4e1284a339eaa0d43051701e9c3f0d38b (patch) | |
tree | aeb67886c951f514b802efe6ca87ed6807bdfabd | |
parent | 95ea33b4411a8d49b558f03b003b7e28380bd86b (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
141 files changed, 13744 insertions, 3347 deletions
@@ -1 +1 @@ -web: node app.js --port $PORT --include example/localstorage
\ No newline at end of file +web: node app.js --port $PORT @@ -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 lookandfeel 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. Userfacing 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 Binary files differnew file mode 100644 index 000000000..7780365c5 --- /dev/null +++ b/docs/src/tutorials/images/add-task.png diff --git a/docs/src/tutorials/images/bar-plot-2.png b/docs/src/tutorials/images/bar-plot-2.png Binary files differnew file mode 100644 index 000000000..a32c2b76f --- /dev/null +++ b/docs/src/tutorials/images/bar-plot-2.png diff --git a/docs/src/tutorials/images/bar-plot-3.png b/docs/src/tutorials/images/bar-plot-3.png Binary files differnew file mode 100644 index 000000000..0899984a3 --- /dev/null +++ b/docs/src/tutorials/images/bar-plot-3.png diff --git a/docs/src/tutorials/images/bar-plot-4.png b/docs/src/tutorials/images/bar-plot-4.png Binary files differnew file mode 100644 index 000000000..50a9091fa --- /dev/null +++ b/docs/src/tutorials/images/bar-plot-4.png diff --git a/docs/src/tutorials/images/bar-plot.png b/docs/src/tutorials/images/bar-plot.png Binary files differnew file mode 100644 index 000000000..2f113d4c6 --- /dev/null +++ b/docs/src/tutorials/images/bar-plot.png diff --git a/docs/src/tutorials/images/chrome.png b/docs/src/tutorials/images/chrome.png Binary files differnew file mode 100644 index 000000000..1b9b7b80d --- /dev/null +++ b/docs/src/tutorials/images/chrome.png diff --git a/docs/src/tutorials/images/remove-task.png b/docs/src/tutorials/images/remove-task.png Binary files differnew file mode 100644 index 000000000..015ec95ac --- /dev/null +++ b/docs/src/tutorials/images/remove-task.png diff --git a/docs/src/tutorials/images/telemetry-1.png b/docs/src/tutorials/images/telemetry-1.png Binary files differnew file mode 100644 index 000000000..2a606e83c --- /dev/null +++ b/docs/src/tutorials/images/telemetry-1.png diff --git a/docs/src/tutorials/images/telemetry-2.png b/docs/src/tutorials/images/telemetry-2.png Binary files differnew file mode 100644 index 000000000..0b34dd90f --- /dev/null +++ b/docs/src/tutorials/images/telemetry-2.png diff --git a/docs/src/tutorials/images/telemetry-3.png b/docs/src/tutorials/images/telemetry-3.png Binary files differnew file mode 100644 index 000000000..c235b1d54 --- /dev/null +++ b/docs/src/tutorials/images/telemetry-3.png diff --git a/docs/src/tutorials/images/todo-edit.png b/docs/src/tutorials/images/todo-edit.png Binary files differnew file mode 100644 index 000000000..3c1ba3f5c --- /dev/null +++ b/docs/src/tutorials/images/todo-edit.png diff --git a/docs/src/tutorials/images/todo-list.png b/docs/src/tutorials/images/todo-list.png Binary files differnew file mode 100644 index 000000000..48c84c63e --- /dev/null +++ b/docs/src/tutorials/images/todo-list.png diff --git a/docs/src/tutorials/images/todo-restyled.png b/docs/src/tutorials/images/todo-restyled.png Binary files differnew file mode 100644 index 000000000..9fd7008c2 --- /dev/null +++ b/docs/src/tutorials/images/todo-restyled.png diff --git a/docs/src/tutorials/images/todo-selection.png b/docs/src/tutorials/images/todo-selection.png Binary files differnew file mode 100644 index 000000000..a0ff87514 --- /dev/null +++ b/docs/src/tutorials/images/todo-selection.png diff --git a/docs/src/tutorials/images/todo.png b/docs/src/tutorials/images/todo.png Binary files differnew file mode 100644 index 000000000..44a7b7b2e --- /dev/null +++ b/docs/src/tutorials/images/todo.png 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">+</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">C</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> + </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()">  </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()">  </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 |