Merged from master

This commit is contained in:
Andrew Henry
2015-11-04 20:30:49 -08:00
145 changed files with 14832 additions and 5432 deletions

View File

@@ -1,4 +1,9 @@
{
"configuration": {
"paths": {
"uuid": "uuid"
}
},
"extensions": {
"routes": [
{

View File

@@ -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>

View File

@@ -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}">

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<div class="menu-element wrapper" ng-controller="ClickAwayController as createController">
<div class="s-menu major create-btn" ng-click="createController.toggle()">
<div class="s-menu-btn major create-btn" ng-click="createController.toggle()">
<span class="ui-symbol icon type-icon">&#x2b;</span>
<span class="title-label">Create</span>
</div>

View File

@@ -25,7 +25,7 @@
* Module defining CreateService. Created by vwoeltje on 11/10/14.
*/
define(
["../../lib/uuid"],
["uuid"],
function (uuid) {
"use strict";

View File

@@ -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

View File

@@ -8,6 +8,11 @@
"key": "urlService",
"implementation": "services/UrlService.js",
"depends": [ "$location" ]
},
{
"key": "popupService",
"implementation": "services/PopupService.js",
"depends": [ "$document", "$window" ]
}
],
"runs": [
@@ -53,6 +58,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",
@@ -118,11 +133,21 @@
"implementation": "directives/MCTDrag.js",
"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",
@@ -226,6 +251,10 @@
{
"key": "selector",
"templateUrl": "templates/controls/selector.html"
},
{
"key": "datetime-picker",
"templateUrl": "templates/controls/datetime-picker.html"
}
],
"licenses": [

View File

@@ -45,6 +45,7 @@ $ueEditToolBarH: 25px;
$ueBrowseLeftPaneW: 25%;
$ueEditLeftPaneW: 75%;
$treeSearchInputBarH: 25px;
$ueTimeControlH: (33px, 20px, 20px);
// Overlay
$ovrTopBarH: 45px;
$ovrFooterH: 24px;
@@ -105,3 +106,8 @@ $dirImgs: $dirCommonRes + 'images/';
/************************** TIMINGS */
$controlFadeMs: 100ms;
/************************** LIMITS */
$glyphLimit: '\e603';
$glyphLimitUpr: '\0000eb';
$glyphLimitLwr: '\0000ee';

View File

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

View File

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

View File

@@ -1,26 +1,39 @@
@mixin limit($bg, $ic, $glyph) {
background: $bg !important;
//color: $fg !important;
&:before {
//@include pulse(1000ms);
color: $ic;
content: $glyph;
}
@mixin limitGlyph($iconColor, $glyph: $glyphLimit) {
&:before {
color: $iconColor;
content: $glyph;
font-family: symbolsfont;
font-size: 0.8em;
display: inline;
margin-right: $interiorMarginSm;
}
}
[class*="s-limit"] {
//white-space: nowrap;
&:before {
display: inline-block;
font-family: symbolsfont;
font-size: 0.75em;
font-style: normal !important;
margin-right: $interiorMarginSm;
vertical-align: middle;
}
.s-limit-red { background: $colorLimitRedBg !important; }
.s-limit-yellow { background: $colorLimitYellowBg !important; }
// Handle limit when applied to a tr
tr[class*="s-limit"] {
&.s-limit-red td:first-child {
@include limitGlyph($colorLimitRedIc);
}
&.s-limit-yellow td:first-child {
@include limitGlyph($colorLimitYellowIc);
}
&.s-limit-upr td:first-child:before { content:$glyphLimitUpr; }
&.s-limit-lwr td:first-child:before { content:$glyphLimitLwr; }
}
.s-limit-upr-red { @include limit($colorLimitRedBg, $colorLimitRedIc, "\0000eb"); };
.s-limit-upr-yellow { @include limit($colorLimitYellowBg, $colorLimitYellowIc, "\0000ed"); };
.s-limit-lwr-yellow { @include limit($colorLimitYellowBg, $colorLimitYellowIc, "\0000ec"); };
.s-limit-lwr-red { @include limit($colorLimitRedBg, $colorLimitRedIc, "\0000ee"); };
// Handle limit when applied directly to a non-tr element
// Assume this is applied to the element that displays the limit value
:not(tr)[class*="s-limit"] {
&.s-limit-red {
@include limitGlyph($colorLimitRedIc);
}
&.s-limit-yellow {
@include limitGlyph($colorLimitYellowIc);
}
&.s-limit-upr:before { content:$glyphLimitUpr; }
&.s-limit-lwr:before { content:$glyphLimitLwr; }
}

View File

@@ -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) {

View File

@@ -10,9 +10,6 @@
&.fixed {
font-size: 0.8em;
}
&.scrolling {
overflow: auto;
}
.controls,
label,
.inline-block {

View File

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

View File

@@ -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;
}
}
}
}
}
.menu,
.context-menu,
.super-menu {
pointer-events: auto;
ul li {
//padding-left: 25px;
a {
color: $fg;
}
.icon {
color: $ic;
}
.type-icon {
left: $interiorMargin;
}
&:hover .icon {
//color: lighten($ic, 5%);
}
}
}
.s-menu {
@include border-radius($basicCr);
@include containerSubtle($colorMenuBg, $colorMenuFg);
@include boxShdw($shdwMenu);
@include txtShdw($shdwMenuText);
padding: $interiorMarginSm 0;
}
.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;
line-height: $d;
}
}
}
.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);
}
.pane {
.menu {
@extend .s-menu;
display: block;
position: absolute;
z-index: 10;
ul {
@include menuUlReset();
li {
@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;
}
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;
}
&:hover {
background: $colorMenuHovBg;
color: $colorMenuHovFg;
.icon {
color: $colorMenuHovIc;
}
}
&.right {
//@include test(red);
left: auto;
right: 0;
padding: $interiorMargin * 5;
width: $prw;
.type-icon {
left: $interiorMargin * 2;
}
}
.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;
}
}
}
}
.context-menu {
font-size: 0.80rem;
}
}
.context-menu-holder {
pointer-events: none;
.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 {
height: $d;
width: $d;
&:before {
font-size: 7px !important;// $d/2;
height: $d;
width: $d;
line-height: $d;
}
}
}
.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);
}
.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;
}
}
}
&.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;
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($colorMenuBg, 30%);
color: $colorCreateMenuText;
font-size: 0.8em;
line-height: 1.5em;
}
}
}
}
.context-menu {
font-size: 0.80rem;
}
.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;

View File

@@ -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;
}
}
position: relative;
margin: $interiorMarginLg 0;
min-width: 400px;
.l-time-controller-visible {
}
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;
}

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -30,22 +30,22 @@
}
.holder-all {
$myM: 0; // $interiorMarginSm;
top: $myM;
right: $myM;
bottom: $myM;
left: $myM;
$myM: 0; // $interiorMarginSm;
top: $myM;
right: $myM;
bottom: $myM;
left: $myM;
}
.browse-area,
.edit-area,
.editor {
position: absolute;
position: absolute;
}
//.editor {
// @include border-radius($basicCr * 1.5);
//}
.editor {
@include border-radius($basicCr * 1.5);
}
.contents {
$myM: 0; //$interiorMargin;
@@ -68,8 +68,8 @@
margin-right: $interiorMargin;
}
&.abs {
text-wrap: none;
white-space: nowrap;
text-wrap: none;
white-space: nowrap;
&.left,
.left {
width: 45%;
@@ -95,67 +95,51 @@
}
.user-environ {
.browse-area,
.edit-area,
.editor {
top: $bodyMargin + $ueTopBarH + ($interiorMargin);
right: $bodyMargin;
bottom: $ueFooterH + $bodyMargin;
left: $bodyMargin;
}
.browse-area,
.edit-area,
.editor {
top: $bodyMargin + $ueTopBarH + ($interiorMargin);
right: $bodyMargin;
bottom: $ueFooterH + $bodyMargin;
left: $bodyMargin;
}
.browse-area,
.edit-area {
> .contents {
left: 0;
right: 0;
}
}
.browse-area,
.edit-area {
> .contents {
left: 0;
right: 0;
}
}
.edit-area {
$tbH: $btnToolbarH + $interiorMargin;
top: $bodyMargin + $ueTopBarEditH + ($interiorMargin);
.tool-bar {
bottom: auto;
height: $tbH;
line-height: $btnToolbarH;
}
.work-area {
top: $tbH + $interiorMargin * 2;
}
}
.edit-area {
$tbH: $btnToolbarH + $interiorMargin;
top: $bodyMargin + $ueTopBarEditH + ($interiorMargin);
.tool-bar {
bottom: auto;
height: $tbH;
line-height: $btnToolbarH;
}
.work-area {
top: $tbH + $interiorMargin * 2;
}
}
// from _bottom-bar.scss
.ue-bottom-bar {
@include absPosDefault(0);// New status bar design
top: auto;
height: $ueFooterH;
line-height: $ueFooterH - ($interiorMargin * 2);
background: $colorFooterBg;
color: lighten($colorBodyBg, 30%);
font-size: .7rem;
.status-holder {
@include box-sizing(border-box);
@include absPosDefault($interiorMargin);
@include ellipsize();
//line-height: $ueFooterH - ($interiorMargin * 2);
right: 120px;
text-transform: uppercase;
z-index: 1;
}
.app-logo {
@include box-sizing(border-box);
@include absPosDefault($interiorMargin);
cursor: pointer;
left: auto;
width: $ueAppLogoW;
z-index: 2;
&.logo-openmctweb {
background: url($dirImgs + 'logo-openmctweb.svg') no-repeat center center;
}
}
}
.ue-bottom-bar {
//@include absPosDefault($bodyMargin);
@include absPosDefault(0); // New status bar design
top: auto;
height: $ueFooterH;
.status-holder {
//right: $ueAppLogoW + $bodyMargin; New status bar design
z-index: 1;
}
.app-logo {
left: auto;
width: $ueAppLogoW;
z-index: 2;
}
}
}
.cols {
@@ -241,89 +225,89 @@
}
}
.pane {
position: absolute;
&.treeview.left {
.create-btn-holder {
bottom: auto; top: 0;
height: $ueTopBarH;
.wrapper.menu-element {
position: absolute;
bottom: $interiorMargin;
}
}
.search-holder {
top: $ueTopBarH + $interiorMarginLg;
}
.tree-holder {
overflow: auto;
top: $ueTopBarH + $interiorMarginLg + $treeSearchInputBarH + $interiorMargin;
}
.create-btn-holder {
bottom: auto;
top: 0;
height: $ueTopBarH;
.wrapper.menu-element {
position: absolute;
bottom: $interiorMargin;
}
}
.search-holder {
top: $ueTopBarH + $interiorMarginLg;
}
.tree-holder {
overflow: auto;
top: $ueTopBarH + $interiorMarginLg + $treeSearchInputBarH + $interiorMargin;
}
}
&.items {
.object-browse-bar {
.left.abs,
.right.abs {
top: auto;
}
//.left.abs {
// @include tmpBorder(green);
//}
//.right.abs {
// @include tmpBorder(red);
//}
}
.object-holder {
top: $ueTopBarH + $interiorMarginLg;
.left.abs,
.right.abs {
top: auto;
}
}
}
.object-holder {
overflow: auto;
}
}
.split-layout {
&.horizontal {
// Slides up and down
>.pane {
// @include test();
margin-top: $interiorMargin;
&:first-child {
margin-top: 0;
}
}
}
&.vertical {
// Slides left and right
>.pane {
// @include test();
margin-left: $interiorMargin;
>.holder {
left: 0;
right: 0;
}
&:first-child {
margin-left: 0;
.holder {
right: $interiorMarginSm;
}
}
}
&.horizontal {
// Slides up and down
> .pane {
// @include test();
margin-top: $interiorMargin;
&:first-child {
margin-top: 0;
}
}
}
&.vertical {
// Slides left and right
> .pane {
// @include test();
margin-left: $interiorMargin;
> .holder {
left: 0;
right: 0;
}
&:first-child {
margin-left: 0;
.holder {
right: $interiorMarginSm;
}
}
}
}
}
}
.object-holder {
overflow: hidden; // Contained objects need to handle their own overflow now
top: $ueTopBarH + $interiorMarginLg;
> ng-include {
@include absPosDefault(0, auto);
}
&.l-controls-visible {
&.l-time-controller-visible {
bottom: nth($ueTimeControlH,1) + nth($ueTimeControlH,2) +nth($ueTimeControlH,3) + ($interiorMargin * 3);
}
}
}
.object-browse-bar .s-btn,
.top-bar .buttons-main .s-btn,
.top-bar .s-menu,
.top-bar .s-menu-btn,
.tool-bar .s-btn,
.tool-bar .s-menu {
$h: $btnToolbarH;
height: $h;
line-height: $h;
vertical-align: top;
.tool-bar .s-menu-btn {
$h: $btnToolbarH;
height: $h;
line-height: $h;
vertical-align: top;
}
.object-browse-bar,
@@ -334,33 +318,29 @@
}
.object-browse-bar {
//@include test(blue);
@include absPosDefault(0, visible);
@include box-sizing(border-box);
height: $ueTopBarH;
line-height: $ueTopBarH;
white-space: nowrap;
//@include test(blue);
@include absPosDefault(0, visible);
@include box-sizing(border-box);
height: $ueTopBarH;
line-height: $ueTopBarH;
white-space: nowrap;
.left {
padding-right: $interiorMarginLg * 2;
.l-back {
display: inline-block;
float: left;
margin-right: $interiorMarginLg;
}
}
.left {
padding-right: $interiorMarginLg * 2;
.l-back {
display: inline-block;
float: left;
margin-right: $interiorMarginLg;
}
}
}
.l-flex {
@include webkitVal('display', 'flex');
@include webkitProp('flex-flow', 'row nowrap');
.left {
//@include test(red);
@include webkitProp(flex, '1 1 0');
padding-right: $interiorMarginLg;
}
}
.vscroll {
overflow-y: auto;
}
@include webkitVal('display', 'flex');
@include webkitProp('flex-flow', 'row nowrap');
.left {
//@include test(red);
@include webkitProp(flex, '1 1 0');
padding-right: $interiorMarginLg;
}
}

View File

@@ -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>

View File

@@ -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"
>

View File

@@ -1,69 +1,108 @@
<!--
Open MCT Web, Copyright (c) 2014-2015, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
NOTES
Open MCT Web is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Ticks:
The thinking is to divide whatever the current time span is by 5,
and assign values accordingly to 5 statically-positioned ticks. So the tick x-position is a static percentage
of the total width available, and the labels change dynamically. This is consistent
with our current approach to the time axis of plots.
I'm keeping the number of ticks low so that when the view portal gets narrow,
the tick labels won't collide with each other. For extra credit, add/remove ticks as the user resizes the view area.
Note: this eval needs to be based on the whatever is containing the
time-controller component, not the whole browser window.
Range indicator and slider knobs:
The left and right properties used in .slider .range-holder and the .knobs are
CSS offsets from the left and right of their respective containers. You
may want or need to calculate those positions as pure offsets from the start datetime
(or left, as it were) and set them as left properties. No problem if so, but
we'll need to tweak the CSS tiny bit to get the center of the knobs to line up
properly on the range left and right bounds.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT Web includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="TimeRangeController">
<div class="l-time-range-inputs-holder">
<span class="l-time-range-inputs-elem ui-symbol type-icon">&#x43;</span>
<span class="l-time-range-input" ng-controller="ToggleController as t1">
<!--<span class="lbl">Start</span>-->
<span class="s-btn time-range-start">
<input type="text"
ng-model="boundsModel.start"
ng-class="{ error: !boundsModel.startValid }">
</input>
<a class="ui-symbol icon icon-calendar" ng-click="t1.toggle()"></a>
<mct-popup ng-if="t1.isActive()">
<div mct-click-elsewhere="t1.setState(false)">
<mct-control key="'datetime-picker'"
ng-model="ngModel.outer"
field="'start'"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>
</span>
<div ng-init="
notes = 'Temporarily using an array to populate ticks so I can see what I\'m doing';
ticks = [
'00:00',
'00:30',
'01:00',
'01:30',
'02:00'
];
"></div>
<span class="l-time-range-inputs-elem lbl">to</span>
<div class="l-time-controller">
<div class="l-time-range-inputs-holder">
Start: <input type="date" />
End: <input type="date" />
</div>
<span class="l-time-range-input" ng-controller="ToggleController as t2">
<!--<span class="lbl">End</span>-->
<span class="s-btn l-time-range-input">
<input type="text"
ng-model="boundsModel.end"
ng-class="{ error: !boundsModel.endValid }">
</input>
<a class="ui-symbol icon icon-calendar" ng-click="t2.toggle()">
</a>
<mct-popup ng-if="t2.isActive()">
<div mct-click-elsewhere="t2.setState(false)">
<mct-control key="'datetime-picker'"
ng-model="ngModel.outer"
field="'end'"
options="{ hours: true }">
</mct-control>
</div>
</mct-popup>
</span>&nbsp;
</span>
</div>
<div class="l-time-range-slider-holder">
<div class="l-time-range-slider">
<div class="slider">
<div class="slot range-holder">
<div class="range" style="left: 0%; right: 30%;"></div>
</div>
<div class="knob knob-l" style="left: 0%;">
<div class="range-value">05/22 14:46</div>
</div>
<div class="knob knob-r" style="right: 30%;">
<div class="range-value">07/22 01:21</div>
</div>
</div>
</div>
</div>
<div class="l-time-range-slider-holder">
<div class="l-time-range-slider">
<div class="slider"
mct-resize="spanWidth = bounds.width">
<div class="knob knob-l"
mct-drag-down="startLeftDrag()"
mct-drag="leftDrag(delta[0])"
ng-style="{ left: startInnerPct }">
<div class="range-value">{{startInnerText}}</div>
</div>
<div class="knob knob-r"
mct-drag-down="startRightDrag()"
mct-drag="rightDrag(delta[0])"
ng-style="{ right: endInnerPct }">
<div class="range-value">{{endInnerText}}</div>
</div>
<div class="slot range-holder">
<div class="range"
mct-drag-down="startMiddleDrag()"
mct-drag="middleDrag(delta[0])"
ng-style="{ left: startInnerPct, right: endInnerPct}">
<div class="toi-line"></div>
</div>
</div>
</div>
</div>
</div>
<div class="l-time-range-ticks-holder">
<div class="l-time-range-ticks">
<div
ng-repeat="tick in ticks"
ng-style="{ left: $index * 25 + '%' }"
class="tick tick-x"
>
<span class="l-time-range-tick-label">{{tick}}</span>
</div>
</div>
</div>
</div>
<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>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*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;
}
);

View File

@@ -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 {

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*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)
);
});
});
}
);

View File

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

View File

@@ -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");
});
});
}
);

View File

@@ -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();
});
});
});
}
);

View File

@@ -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'
});
});
});
});
}
);

View File

@@ -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();
});
});
}
);

View File

@@ -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"
]

View File

@@ -45,13 +45,12 @@
"implementation": "services/InfoService.js",
"depends": [
"$compile",
"$document",
"$window",
"$rootScope",
"popupService",
"agentService"
]
}
],
],
"constants": [
{
"key": "INFO_HOVER_DELAY",
@@ -66,4 +65,4 @@
}
]
}
}
}

View File

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

View File

@@ -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();
};
};

View File

@@ -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'
]);
mockDocument.find.andCallFake(function (tag) {
return tag === 'body' ? mockBody : undefined;
mockScope = jasmine.createSpyObj("scope", ["$destroy"]);
mockElements = [];
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)
);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -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,16 +126,17 @@ $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;
@@ -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;
$colorAboutLink: #84b3ff;
// Loading
$colorLoadingBg: rgba($colorBodyFg, 0.2);
$colorLoadingFg: $colorAlt1;

View File

@@ -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%) {

File diff suppressed because it is too large Load Diff

View File

@@ -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%);
@@ -57,20 +72,6 @@ $colorInputBg: $colorGenBg;
$colorInputFg: $colorBodyFg;
$colorFormText: pushBack($colorBodyFg, 10%);
$colorInputIcon: pushBack($colorBodyFg, 25%);
// Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #fff;
$colorStatusDefault: #ccc;
$colorStatusInfo: #60ba7b;
$colorStatusAlert: #ffb66c;
$colorStatusError: #c96b68;
$colorProgressBarOuter: rgba(#000, 0.1);
$colorProgressBarAmt: #0a0;
$progressBarHOverlay: 15px;
$progressBarStripeW: 20px;
$shdwStatusIc: rgba(white, 0.8) 0 0px 5px;
// Selects
$colorSelectBg: #ddd;
$colorSelectFg: $colorBodyFg;
@@ -118,9 +119,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 +153,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;
$colorAboutLink: #84b3ff;
// Loading
$colorLoadingFg: $colorAlt1;
$colorLoadingBg: rgba($colorLoadingFg, 0.1);

View File

@@ -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) {

View File

@@ -66,6 +66,7 @@
"depends": [
"persistenceService",
"$q",
"now",
"PERSISTENCE_SPACE",
"ADDITIONAL_PERSISTENCE_SPACES"
]

View File

@@ -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);
};
/**

View File

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

View File

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

View File

@@ -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" }
});
});

View File

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

View File

@@ -31,6 +31,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": [
@@ -53,7 +61,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": [

View File

@@ -0,0 +1,62 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*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;
}
);

View File

@@ -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,11 +34,41 @@ 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;
}
);

View File

@@ -0,0 +1,95 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*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');
});
});
}
);

View File

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

View File

@@ -1,6 +1,9 @@
[
"actions/AbstractComposeAction",
"actions/CopyAction",
"actions/GoToOriginalAction",
"actions/LinkAction",
"actions/MoveAction",
"services/CopyService",
"services/LinkService",
"services/MoveService",

View File

@@ -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.

View File

@@ -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."
}
]
}
}

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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());
});
});
}
);

View File

@@ -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)
}]);
});
});
}
);

View File

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

View File

@@ -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();
});
});
}
);

View File

@@ -0,0 +1,6 @@
[
"ConductorRepresenter",
"ConductorService",
"ConductorTelemetryDecorator",
"TimeConductor"
]

View File

@@ -167,8 +167,9 @@
"$scope",
"$q",
"dialogService",
"telemetrySubscriber",
"telemetryFormatter"
"telemetryHandler",
"telemetryFormatter",
"throttle"
]
}
],

View File

@@ -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'

View File

@@ -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 displayed value for this object
function updateValue(telemetryObject) {
var id = telemetryObject && telemetryObject.getId(),
// 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 &&
subscription.getDatum(telemetryObject),
alarm = limit && datum && limit.evaluate(datum);
datum = telemetryObject && handle.getDatum(
telemetryObject,
index
);
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;
});
if (index >= 0) {
setDisplayedValue(
telemetryObject,
telemetrySeries.getRangeValue(index),
limit && datum && limit.evaluate(datum)
);
}
}
// Update the displayed value for this object
function updateValue(telemetryObject) {
var limit = telemetryObject &&
telemetryObject.getCapability('limit'),
datum = telemetryObject &&
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);
}
/**

View File

@@ -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 () {

View File

@@ -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>

View File

@@ -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">

View File

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

View File

@@ -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();
}

View File

@@ -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;
};
/**

View File

@@ -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) : ''
});
}

View File

@@ -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,12 @@ define(
// Update dimensions and origin based on extrema of plots
PlotUpdater.prototype.updateBounds = function () {
var bufferArray = this.bufferArray;
var bufferArray = this.bufferArray.filter(function (lineBuffer) {
return lineBuffer.getLength() > 0; // Ignore empty lines
}),
priorDomainOrigin = this.origin[0],
priorDomainDimensions = this.dimensions[0];
if (bufferArray.length > 0) {
this.domainExtrema = bufferArray.map(function (lineBuffer) {
return lineBuffer.getDomainExtrema();
@@ -178,6 +183,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] -
@@ -281,6 +298,21 @@ define(
return this.bufferArray;
};
/**
* 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.
*/

View File

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

View File

@@ -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();

View File

@@ -202,6 +202,38 @@ define(
expect(updater.getDimensions()[1]).toBeGreaterThan(20);
});
describe("when no data is initially available", function () {
beforeEach(function () {
testDomainValues = {};
testRangeValues = {};
updater = new PlotUpdater(
mockSubscription,
testDomain,
testRange,
1350 // Smaller max size for easier testing
);
});
it("has no line data", function () {
// Either no lines, or empty lines are fine
expect(updater.getLineBuffers().map(function (lineBuffer) {
return lineBuffer.getLength();
}).reduce(function (a, b) {
return a + b;
}, 0)).toEqual(0);
});
it("determines initial domain bounds from first available data", function () {
testDomainValues.a = 123;
testRangeValues.a = 456;
updater.update();
expect(updater.getOrigin()[0]).toEqual(jasmine.any(Number));
expect(updater.getOrigin()[1]).toEqual(jasmine.any(Number));
expect(isNaN(updater.getOrigin()[0])).toBeFalsy();
expect(isNaN(updater.getOrigin()[1])).toBeFalsy();
});
});
});
}
);

View File

@@ -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,

View File

@@ -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"
>

View File

@@ -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>

View File

@@ -13,7 +13,7 @@
"provides": "searchService",
"type": "provider",
"implementation": "ElasticSearchProvider.js",
"depends": [ "$http", "objectService", "ELASTIC_ROOT" ]
"depends": [ "$http", "ELASTIC_ROOT" ]
}
],
"constants": [

View File

@@ -24,190 +24,122 @@
/**
* Module defining ElasticSearchProvider. Created by shale on 07/16/2015.
*/
define(
[],
function () {
"use strict";
define([
// 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;
}
], function (
/**
* 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 = '';
}
) {
"use strict";
return searchTerm.split(' ').map(function (s) {
// Don't add fuzziness for quoted strings
if (s.indexOf('"') !== -1) {
return s;
} else {
return s + '~' + editDistance;
}
}).join(' ');
}
var ID_PROPERTY = '_id',
SOURCE_PROPERTY = '_source',
SCORE_PROPERTY = '_score';
// 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};
}
};
return ElasticSearchProvider;
/**
* 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;
});

View File

@@ -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";
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;
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;
});
});
});
}
);
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
});
});
});
});

View File

@@ -54,7 +54,13 @@
{
"key": "menu",
"implementation": "actions/ContextMenuAction.js",
"depends": [ "$compile", "$document", "$window", "$rootScope", "agentService" ]
"depends": [
"$compile",
"$document",
"$rootScope",
"popupService",
"agentService"
]
}
]
}

View File

@@ -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

View File

@@ -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) {

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