diff --git a/bundles.json b/bundles.json index de005c5961..0486bdf24b 100644 --- a/bundles.json +++ b/bundles.json @@ -7,6 +7,7 @@ "platform/commonUI/edit", "platform/commonUI/dialog", "platform/commonUI/general", + "platform/commonUI/inspect", "platform/containment", "platform/telemetry", "platform/features/layout", @@ -16,7 +17,6 @@ "platform/forms", "platform/persistence/queue", "platform/policy", - "platform/entanglement", "example/persistence", "example/generator" diff --git a/platform/commonUI/browse/bundle.json b/platform/commonUI/browse/bundle.json index dcf1ba9a38..28d96c5ec2 100644 --- a/platform/commonUI/browse/bundle.json +++ b/platform/commonUI/browse/bundle.json @@ -57,7 +57,8 @@ { "key": "grid-item", "templateUrl": "templates/items/grid-item.html", - "uses": [ "type", "action" ] + "uses": [ "type", "action" ], + "gestures": [ "info", "menu" ] }, { "key": "object-header", diff --git a/platform/commonUI/general/bundle.json b/platform/commonUI/general/bundle.json index 2923d96c47..ead610c963 100644 --- a/platform/commonUI/general/bundle.json +++ b/platform/commonUI/general/bundle.json @@ -181,7 +181,7 @@ "key": "label", "templateUrl": "templates/label.html", "uses": [ "type" ], - "gestures": [ "drag", "menu" ] + "gestures": [ "drag", "menu", "info" ] }, { "key": "node", diff --git a/platform/commonUI/general/res/css/theme-espresso.css b/platform/commonUI/general/res/css/theme-espresso.css index 900558efd9..5a8bcac07a 100644 --- a/platform/commonUI/general/res/css/theme-espresso.css +++ b/platform/commonUI/general/res/css/theme-espresso.css @@ -328,6 +328,10 @@ span { */ } /* line 72, ../sass/_global.scss */ +mct-container { + display: block; } + +/* line 76, ../sass/_global.scss */ .abs, .btn-menu span.l-click-area { position: absolute; top: 0; @@ -337,51 +341,51 @@ span { height: auto; width: auto; } -/* line 82, ../sass/_global.scss */ +/* line 86, ../sass/_global.scss */ .code, .codehilite { font-family: "Lucida Console", monospace; font-size: 0.7em; line-height: 150%; white-space: pre; } -/* line 89, ../sass/_global.scss */ +/* line 93, ../sass/_global.scss */ .codehilite { background-color: rgba(255, 255, 255, 0.1); padding: 1em; } -/* line 95, ../sass/_global.scss */ +/* line 99, ../sass/_global.scss */ .align-right { text-align: right; } -/* line 99, ../sass/_global.scss */ +/* line 103, ../sass/_global.scss */ .centered { text-align: center; } -/* line 103, ../sass/_global.scss */ +/* line 107, ../sass/_global.scss */ .no-margin { margin: 0; } -/* line 107, ../sass/_global.scss */ +/* line 111, ../sass/_global.scss */ .colorKey { color: #0099cc; } -/* line 111, ../sass/_global.scss */ +/* line 115, ../sass/_global.scss */ .ds { -moz-box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px; -webkit-box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px; box-shadow: rgba(0, 0, 0, 0.7) 0 4px 10px 2px; } -/* line 115, ../sass/_global.scss */ +/* line 119, ../sass/_global.scss */ .hide, .hidden { display: none !important; } -/* line 121, ../sass/_global.scss */ +/* line 125, ../sass/_global.scss */ .paused:not(.s-btn):not(.icon-btn) { border-color: #c56f01 !important; color: #c56f01 !important; } -/* line 127, ../sass/_global.scss */ +/* line 131, ../sass/_global.scss */ .sep { color: rgba(255, 255, 255, 0.2); } @@ -2611,7 +2615,7 @@ label.checkbox.custom { position: absolute; height: 200px; width: 170px; - z-index: 59; } + z-index: 70; } /* line 172, ../sass/controls/_menus.scss */ .context-menu-holder .context-menu-wrapper { position: absolute; @@ -4215,137 +4219,101 @@ input[type="text"] { * at runtime from the About dialog for additional information. *****************************************************************************/ /* line 24, ../sass/helpers/_bubbles.scss */ -.l-bubble-wrapper { - position: absolute; - z-index: 70; } - /* line 27, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-bubble { - padding: 5px 10px; } - /* line 29, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-bubble .l-btn.close { - padding: 0 2px; - position: absolute; - right: 5px; - top: 5px; } - /* line 36, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .arw { - position: absolute; } - /* line 38, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .arw.arw-up { - bottom: 100%; } - /* line 42, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .arw.arw-down { - top: 100%; } - /* line 47, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-infobubble { +.l-infobubble-wrapper { + -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; + -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; + box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; + position: relative; + z-index: 50; } + /* line 29, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .l-infobubble { display: inline-block; - max-width: 250px; } - /* line 51, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-infobubble:before { + min-width: 100px; + max-width: 300px; + padding: 5px 10px; } + /* line 34, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .l-infobubble:before { content: ""; position: absolute; width: 0; height: 0; } - /* line 57, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-infobubble table { + /* line 40, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .l-infobubble table { width: 100%; } - /* line 60, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-infobubble table tr td { + /* line 43, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .l-infobubble table tr td { padding: 2px 0; vertical-align: top; } - /* line 67, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-infobubble table tr td.label { + /* line 50, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .l-infobubble table tr td.label { padding-right: 10px; white-space: nowrap; } - /* line 71, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-infobubble table tr td.value { + /* line 54, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .l-infobubble table tr td.value { white-space: nowrap; } - /* line 75, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-infobubble table tr td.align-wrap { + /* line 58, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .l-infobubble table tr td.align-wrap { white-space: normal; } - /* line 81, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper .l-infobubble .title { + /* line 64, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .l-infobubble .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 5px; } - /* line 88, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper.arw-left { + /* line 71, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-left { margin-left: 20px; } - /* line 90, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper.arw-left .l-infobubble::before { + /* line 73, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-left .l-infobubble::before { right: 100%; width: 0; height: 0; border-top: 6.66667px solid transparent; border-bottom: 6.66667px solid transparent; border-right: 10px solid #ddd; } - /* line 96, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper.arw-right { + /* line 79, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-right { margin-right: 20px; } - /* line 98, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper.arw-right .l-infobubble::before { + /* line 81, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-right .l-infobubble::before { left: 100%; width: 0; height: 0; border-top: 6.66667px solid transparent; border-bottom: 6.66667px solid transparent; border-left: 10px solid #ddd; } - /* line 105, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper.arw-top .l-infobubble::before { + /* line 88, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-top .l-infobubble::before { top: 20px; } - /* line 111, ../sass/helpers/_bubbles.scss */ - .l-bubble-wrapper.arw-btm .l-infobubble::before { + /* line 94, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-btm .l-infobubble::before { bottom: 20px; } - -/* line 121, ../sass/helpers/_bubbles.scss */ -.l-thumbsbubble-wrapper { - position: absolute; - left: 10px; - right: 10px; - height: 183px; - width: auto; } - /* line 128, ../sass/helpers/_bubbles.scss */ - .l-thumbsbubble-wrapper .l-thumbsbubble { - overflow: hidden; - position: absolute; - top: 0px; - right: 0px; - bottom: 0px; - left: 0px; - width: auto; - height: auto; } - /* line 130, ../sass/helpers/_bubbles.scss */ - .l-thumbsbubble-wrapper .l-thumbsbubble .l-image-thumbs-wrapper { - height: auto; - top: 5px !important; - right: 25px; - bottom: 5px !important; - left: 5px; } - /* line 135, ../sass/helpers/_bubbles.scss */ - .l-thumbsbubble-wrapper .arw { + /* line 99, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-down { + margin-bottom: 10px; } + /* line 101, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-down .l-infobubble::before { + left: 50%; + top: 100%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 7.5px solid #ddd; } + /* line 110, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper .arw { z-index: 2; } - /* line 140, ../sass/helpers/_bubbles.scss */ - .l-thumbsbubble-wrapper.arw-up .arw.arw-down, .l-thumbsbubble-wrapper.arw-down .arw.arw-up { + /* line 113, ../sass/helpers/_bubbles.scss */ + .l-infobubble-wrapper.arw-up .arw.arw-down, .l-infobubble-wrapper.arw-down .arw.arw-up { display: none; } -/* line 150, ../sass/helpers/_bubbles.scss */ -.s-bubble { - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; - -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; - -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; - box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; } - -/* line 156, ../sass/helpers/_bubbles.scss */ +/* line 120, ../sass/helpers/_bubbles.scss */ .l-thumbsbubble-wrapper .arw-up { width: 0; height: 0; border-left: 6.66667px solid transparent; border-right: 6.66667px solid transparent; border-bottom: 10px solid #4d4d4d; } -/* line 159, ../sass/helpers/_bubbles.scss */ +/* line 123, ../sass/helpers/_bubbles.scss */ .l-thumbsbubble-wrapper .arw-down { width: 0; height: 0; @@ -4353,27 +4321,33 @@ input[type="text"] { border-right: 6.66667px solid transparent; border-top: 10px solid #4d4d4d; } -/* line 163, ../sass/helpers/_bubbles.scss */ +/* line 127, ../sass/helpers/_bubbles.scss */ .s-infobubble { + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + -moz-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; + -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; + box-shadow: rgba(0, 0, 0, 0.4) 0 1px 5px; background: #ddd; color: #666; font-size: 0.8rem; } - /* line 168, ../sass/helpers/_bubbles.scss */ + /* line 134, ../sass/helpers/_bubbles.scss */ .s-infobubble .title { color: #333333; font-weight: bold; } - /* line 173, ../sass/helpers/_bubbles.scss */ + /* line 139, ../sass/helpers/_bubbles.scss */ .s-infobubble tr td { border-top: 1px solid #c4c4c4; font-size: 0.9em; } - /* line 177, ../sass/helpers/_bubbles.scss */ + /* line 143, ../sass/helpers/_bubbles.scss */ .s-infobubble tr:first-child td { border-top: none; } - /* line 181, ../sass/helpers/_bubbles.scss */ + /* line 147, ../sass/helpers/_bubbles.scss */ .s-infobubble .value { color: #333333; } -/* line 186, ../sass/helpers/_bubbles.scss */ +/* line 152, ../sass/helpers/_bubbles.scss */ .s-thumbsbubble { background: #4d4d4d; color: #b3b3b3; } diff --git a/platform/commonUI/general/res/sass/_constants.scss b/platform/commonUI/general/res/sass/_constants.scss index e5385b19ac..feab47efab 100644 --- a/platform/commonUI/general/res/sass/_constants.scss +++ b/platform/commonUI/general/res/sass/_constants.scss @@ -67,6 +67,8 @@ $colorLimitRed: #aa0000; $colorTelemFresh: #fff; $colorTelemStale: #888; $styleTelemState: italic; +$colorInfoBubbleFg: #666; +$colorInfoBubbleBg: #ddd; // Ratios $ltGamma: 20%; @@ -150,6 +152,9 @@ $imageThumbPad: 1px; // Bubbles $bubbleArwSize: 10px; $bubblePad: $interiorMargin; +$bubbleMinW: 100px; +$bubbleMaxW: 300px; + // Timing -$controlFadeMs: 100ms; +$controlFadeMs: 100ms; \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/_global.scss b/platform/commonUI/general/res/sass/_global.scss index 0ada165ec9..be0afc896d 100644 --- a/platform/commonUI/general/res/sass/_global.scss +++ b/platform/commonUI/general/res/sass/_global.scss @@ -69,6 +69,10 @@ span { */ } +mct-container { + display: block; +} + .abs { position: absolute; top: 0; diff --git a/platform/commonUI/general/res/sass/controls/_menus.scss b/platform/commonUI/general/res/sass/controls/_menus.scss index 01606d0c6d..cc6f65fb7a 100644 --- a/platform/commonUI/general/res/sass/controls/_menus.scss +++ b/platform/commonUI/general/res/sass/controls/_menus.scss @@ -168,7 +168,7 @@ position: absolute; height: 200px; width: 170px; - z-index: 59; + z-index: 70; .context-menu-wrapper { position: absolute; height: 100%; diff --git a/platform/commonUI/general/res/sass/helpers/_bubbles.scss b/platform/commonUI/general/res/sass/helpers/_bubbles.scss index 0dad9e115b..e9648523c4 100644 --- a/platform/commonUI/general/res/sass/helpers/_bubbles.scss +++ b/platform/commonUI/general/res/sass/helpers/_bubbles.scss @@ -21,33 +21,16 @@ *****************************************************************************/ //************************************************* LAYOUT -.l-bubble-wrapper { - position: absolute; - z-index: 70; - .l-bubble { - padding: $bubblePad $bubblePad*2; - .l-btn.close { - padding: 0 2px; - position: absolute; - right: $interiorMargin; - top: $interiorMargin; - } - } - .arw { - position: absolute; - &.arw-up { - bottom: 100%; - - } - &.arw-down { - top: 100%; - } - } - +.l-infobubble-wrapper { + $arwSize: 5px; + @include box-shadow(rgba(black, 0.4) 0 1px 5px); + position: relative; + z-index: 50; .l-infobubble { display: inline-block; - max-width: 250px; - //padding: 5px 10px; + min-width: $bubbleMinW; + max-width: $bubbleMaxW; + padding: 5px 10px; &:before { content:""; position: absolute; @@ -114,27 +97,17 @@ } &.arw-down { - - } -} - -.l-thumbsbubble-wrapper { - $closeBtnD: 15px; - position: absolute; - left: $interiorMarginLg; - right: $interiorMarginLg; - height: $imageThumbsWrapperH + $bubblePad*2 + $interiorMargin; - width: auto; - .l-thumbsbubble { - @include absPosDefault(); - .l-image-thumbs-wrapper { - height: auto; - top: $bubblePad !important; right: $closeBtnD + ($interiorMargin*2); bottom: $bubblePad !important; left: $bubblePad; + margin-bottom: $arwSize*2; + .l-infobubble::before { + left: 50%; + top: 100%; + margin-left: -1 * $arwSize; + border-left: $arwSize solid transparent; + border-right: $arwSize solid transparent; + border-top: ($arwSize * 1.5) solid $colorInfoBubbleBg; } } .arw { - //left: 50%; - //margin-left: $bubbleArwSize / -2; z-index: 2; } &.arw-up .arw.arw-down, @@ -143,15 +116,6 @@ //************************************************* LOOK AND FEEL -.s-bubble-wrapper { - //@include box-shadow(rgba(black, 0.4) 0 1px 5px); -} - -.s-bubble { - @include border-radius($basicCr); - @include box-shadow(rgba(black, 0.4) 0 1px 5px); -} - .l-thumbsbubble-wrapper { .arw-up { @include triangle('up', $bubbleArwSize, 1.5, $colorThumbsBubbleBg); @@ -162,6 +126,8 @@ } .s-infobubble { $emFg: darken($colorInfoBubbleFg, 20%); + @include border-radius($basicCr); + @include box-shadow(rgba(black, 0.4) 0 1px 5px); background: $colorInfoBubbleBg; color: $colorInfoBubbleFg; font-size: 0.8rem; diff --git a/platform/commonUI/inspect/bundle.json b/platform/commonUI/inspect/bundle.json index 16f2bdf89c..07506d0983 100644 --- a/platform/commonUI/inspect/bundle.json +++ b/platform/commonUI/inspect/bundle.json @@ -1,20 +1,50 @@ { "extensions": { - "types": [ + "templates": [ { - "key": "infobubble", - "name": "Info Bubble", - "glyph": "\u00EA", - "description": "Static markup for info bubbles", - "features": [ "creation" ] + "key": "info-table", + "templateUrl": "info-table.html" + }, + { + "key": "info-bubble", + "templateUrl": "info-bubble.html" } ], - "views": [ + "containers": [ { - "templateUrl": "infobubble.html", - "name": "Info Bubble", - "type": "infobubble", - "key": "infobubble" + "key": "bubble", + "templateUrl": "bubble.html", + "attributes": [ "bubbleTitle", "bubbleLayout" ], + "alias": "bubble" + } + ], + "gestures": [ + { + "key": "info", + "implementation": "gestures/InfoGesture.js", + "depends": [ + "$timeout", + "infoService", + "INFO_HOVER_DELAY" + ] + } + ], + "services": [ + { + "key": "infoService", + "implementation": "services/InfoService.js", + "depends": [ + "$compile", + "$document", + "$window", + "$rootScope" + ] + } + ], + "constants": [ + { + "key": "INFO_HOVER_DELAY", + "value": 500 } ] } diff --git a/platform/commonUI/inspect/res/bubble.html b/platform/commonUI/inspect/res/bubble.html new file mode 100644 index 0000000000..f528d6432f --- /dev/null +++ b/platform/commonUI/inspect/res/bubble.html @@ -0,0 +1,9 @@ +
+
+
+ {{bubble.bubbleTitle}} +
+ +
+
diff --git a/platform/commonUI/inspect/res/info-bubble.html b/platform/commonUI/inspect/res/info-bubble.html new file mode 100644 index 0000000000..1deeeade15 --- /dev/null +++ b/platform/commonUI/inspect/res/info-bubble.html @@ -0,0 +1,7 @@ + + + + diff --git a/platform/commonUI/inspect/res/info-table.html b/platform/commonUI/inspect/res/info-table.html new file mode 100644 index 0000000000..0c89a498d9 --- /dev/null +++ b/platform/commonUI/inspect/res/info-table.html @@ -0,0 +1,8 @@ + + + + + +
{{property.name}} + {{property.value}} +
diff --git a/platform/commonUI/inspect/src/InfoConstants.js b/platform/commonUI/inspect/src/InfoConstants.js new file mode 100644 index 0000000000..86570911c6 --- /dev/null +++ b/platform/commonUI/inspect/src/InfoConstants.js @@ -0,0 +1,35 @@ +/***************************************************************************** + * 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({ + BUBBLE_TEMPLATE: "" + + "" + + "" + + "", + // 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 +}); \ No newline at end of file diff --git a/platform/commonUI/inspect/src/gestures/InfoGesture.js b/platform/commonUI/inspect/src/gestures/InfoGesture.js new file mode 100644 index 0000000000..879cc2ac36 --- /dev/null +++ b/platform/commonUI/inspect/src/gestures/InfoGesture.js @@ -0,0 +1,121 @@ +/***************************************************************************** + * 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 `info` gesture displays domain object metadata in a + * bubble on hover. + * + * @constructor + * @param $timeout Angular's `$timeout` + * @param {InfoService} infoService a service which shows info bubbles + * @param {number} DELAY delay, in milliseconds, before bubble appears + * @param element jqLite-wrapped DOM element + * @param {DomainObject} domainObject the domain object for which to + * show information + */ + function InfoGesture($timeout, infoService, DELAY, element, domainObject) { + var dismissBubble, + pendingBubble, + mousePosition, + scopeOff; + + function trackPosition(event) { + // Record mouse position, so bubble can be shown at latest + // mouse position (not just where the mouse entered) + mousePosition = [ event.clientX, event.clientY ]; + } + + function hideBubble() { + // If a bubble is showing, dismiss it + if (dismissBubble) { + dismissBubble(); + element.off('mouseleave', hideBubble); + dismissBubble = undefined; + } + // If a bubble will be shown on a timeout, cancel that + if (pendingBubble) { + $timeout.cancel(pendingBubble); + element.off('mousemove', trackPosition); + element.off('mouseleave', hideBubble); + pendingBubble = undefined; + } + // Also clear mouse position so we don't have a ton of tiny + // arrays allocated while user mouses over things + mousePosition = undefined; + } + + function showBubble(event) { + trackPosition(event); + + // Also need to track position during hover + element.on('mousemove', trackPosition); + + // Show the bubble, after a suitable delay (if mouse has + // left before this time is up, this will be canceled.) + pendingBubble = $timeout(function () { + dismissBubble = infoService.display( + "info-table", + domainObject.getModel().name, + domainObject.useCapability('metadata'), + mousePosition + ); + element.off('mousemove', trackPosition); + pendingBubble = undefined; + }, DELAY); + + element.on('mouseleave', hideBubble); + } + + // Show bubble (on a timeout) on mouse over + element.on('mouseenter', showBubble); + + // Also make sure we dismiss bubble if representation is destroyed + // before the mouse actually leaves it + scopeOff = element.scope().$on('$destroy', hideBubble); + + return { + /** + * Detach any event handlers associated with this gesture. + * @memberof InfoGesture + * @method + */ + destroy: function () { + // Dismiss any active bubble... + hideBubble(); + // ...and detach listeners + element.off('mouseenter', showBubble); + scopeOff(); + } + }; + } + + return InfoGesture; + + } + +); diff --git a/platform/commonUI/inspect/src/services/InfoService.js b/platform/commonUI/inspect/src/services/InfoService.js new file mode 100644 index 0000000000..b67192ba30 --- /dev/null +++ b/platform/commonUI/inspect/src/services/InfoService.js @@ -0,0 +1,95 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define( + ['../InfoConstants'], + function (InfoConstants) { + "use strict"; + + var BUBBLE_TEMPLATE = InfoConstants.BUBBLE_TEMPLATE, + OFFSET = InfoConstants.BUBBLE_OFFSET; + + /** + * Displays informative content ("info bubbles") for the user. + * @constructor + */ + function InfoService($compile, $document, $window, $rootScope) { + + function display(templateKey, title, content, position) { + var 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), + bubble; + + // 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; + + // Create the context menu + bubble = $compile(BUBBLE_TEMPLATE)(scope); + + // Position the bubble + bubble.css('position', 'absolute'); + 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 { + /** + * Display an info bubble at the specified location. + * @param {string} templateKey template to place in bubble + * @param {string} title title for the bubble + * @param {*} content content to pass to the template, via + * `ng-model` + * @param {number[]} x,y position of the info bubble, in + * pixel coordinates. + * @returns {Function} a function that may be invoked to + * dismiss the info bubble + */ + display: display + }; + } + + return InfoService; + } +); diff --git a/platform/commonUI/inspect/test/gestures/InfoGestureSpec.js b/platform/commonUI/inspect/test/gestures/InfoGestureSpec.js new file mode 100644 index 0000000000..44f2b1e54b --- /dev/null +++ b/platform/commonUI/inspect/test/gestures/InfoGestureSpec.js @@ -0,0 +1,157 @@ +/***************************************************************************** + * 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/gestures/InfoGesture'], + function (InfoGesture) { + "use strict"; + + describe("The info gesture", function () { + var mockTimeout, + mockInfoService, + testDelay = 12321, + mockElement, + mockDomainObject, + mockScope, + mockOff, + testMetadata, + mockPromise, + mockHide, + gesture; + + function fireEvent(evt, value) { + mockElement.on.calls.forEach(function (call) { + if (call.args[0] === evt) { + call.args[1](value); + } + }); + } + + beforeEach(function () { + mockTimeout = jasmine.createSpy('$timeout'); + mockTimeout.cancel = jasmine.createSpy('cancel'); + mockInfoService = jasmine.createSpyObj( + 'infoService', + [ 'display' ] + ); + mockElement = jasmine.createSpyObj( + 'element', + [ 'on', 'off', 'scope', 'css' ] + ); + mockDomainObject = jasmine.createSpyObj( + 'domainObject', + [ 'getId', 'getCapability', 'useCapability', 'getModel' ] + ); + mockScope = jasmine.createSpyObj('$scope', [ '$on' ]); + mockOff = jasmine.createSpy('$off'); + testMetadata = [ { name: "Test name", value: "Test value" } ]; + mockPromise = jasmine.createSpyObj('promise', ['then']); + mockHide = jasmine.createSpy('hide'); + + mockDomainObject.getModel.andReturn({ name: "Test Object" }); + mockDomainObject.useCapability.andCallFake(function (c) { + return (c === 'metadata') ? testMetadata : undefined; + }); + mockElement.scope.andReturn(mockScope); + mockScope.$on.andReturn(mockOff); + mockTimeout.andReturn(mockPromise); + mockInfoService.display.andReturn(mockHide); + + gesture = new InfoGesture( + mockTimeout, + mockInfoService, + testDelay, + mockElement, + mockDomainObject + ); + }); + + it("listens for mouseenter on the representation", function () { + expect(mockElement.on) + .toHaveBeenCalledWith('mouseenter', jasmine.any(Function)); + }); + + it("displays an info bubble on a delay after mouseenter", function () { + fireEvent("mouseenter", { clientX: 1977, clientY: 42 }); + expect(mockTimeout) + .toHaveBeenCalledWith(jasmine.any(Function), testDelay); + mockTimeout.mostRecentCall.args[0](); + expect(mockInfoService.display).toHaveBeenCalledWith( + jasmine.any(String), + "Test Object", + testMetadata, + [ 1977, 42 ] + ); + }); + + it("does not display info bubble if mouse leaves too soon", function () { + fireEvent("mouseenter", { clientX: 1977, clientY: 42 }); + fireEvent("mouseleave", { clientX: 1977, clientY: 42 }); + expect(mockTimeout.cancel).toHaveBeenCalledWith(mockPromise); + expect(mockInfoService.display).not.toHaveBeenCalled(); + }); + + it("hides a shown bubble when mouse leaves", function () { + fireEvent("mouseenter", { clientX: 1977, clientY: 42 }); + mockTimeout.mostRecentCall.args[0](); + expect(mockHide).not.toHaveBeenCalled(); // verify precondition + fireEvent("mouseleave", {}); + expect(mockHide).toHaveBeenCalled(); + }); + + it("tracks mouse position", function () { + fireEvent("mouseenter", { clientX: 1977, clientY: 42 }); + fireEvent("mousemove", { clientX: 1999, clientY: 11 }); + fireEvent("mousemove", { clientX: 1984, clientY: 11 }); + mockTimeout.mostRecentCall.args[0](); + // Should have displayed at the latest observed mouse position + expect(mockInfoService.display).toHaveBeenCalledWith( + jasmine.any(String), + "Test Object", + testMetadata, + [ 1984, 11 ] + ); + }); + + it("hides shown bubbles when destroyed", function () { + fireEvent("mouseenter", { clientX: 1977, clientY: 42 }); + mockTimeout.mostRecentCall.args[0](); + expect(mockHide).not.toHaveBeenCalled(); // verify precondition + gesture.destroy(); + expect(mockHide).toHaveBeenCalled(); + }); + + it("detaches listeners when destroyed", function () { + fireEvent("mouseenter", { clientX: 1977, clientY: 42 }); + gesture.destroy(); + mockElement.on.calls.forEach(function (call) { + expect(mockElement.off).toHaveBeenCalledWith( + call.args[0], + call.args[1] + ); + }); + }); + + }); + } +); diff --git a/platform/commonUI/inspect/test/services/InfoServiceSpec.js b/platform/commonUI/inspect/test/services/InfoServiceSpec.js new file mode 100644 index 0000000000..858427fe84 --- /dev/null +++ b/platform/commonUI/inspect/test/services/InfoServiceSpec.js @@ -0,0 +1,131 @@ +/***************************************************************************** + * 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/InfoService', '../../src/InfoConstants'], + function (InfoService, InfoConstants) { + "use strict"; + + describe("The info service", function () { + var mockCompile, + mockDocument, + testWindow, + mockRootScope, + mockCompiledTemplate, + testScope, + mockBody, + mockElement, + service; + + beforeEach(function () { + mockCompile = jasmine.createSpy('$compile'); + mockDocument = jasmine.createSpyObj('$document', ['find']); + testWindow = { innerWidth: 1000, innerHeight: 100 }; + mockRootScope = jasmine.createSpyObj('$rootScope', ['$new']); + mockCompiledTemplate = jasmine.createSpy('template'); + testScope = {}; + mockBody = jasmine.createSpyObj('body', ['append']); + mockElement = jasmine.createSpyObj('element', ['css', 'remove']); + + mockDocument.find.andCallFake(function (tag) { + return tag === 'body' ? mockBody : undefined; + }); + mockCompile.andReturn(mockCompiledTemplate); + mockCompiledTemplate.andReturn(mockElement); + mockRootScope.$new.andReturn(testScope); + + service = new InfoService( + mockCompile, + mockDocument, + testWindow, + mockRootScope + ); + }); + + it("creates elements and appends them to the body to display", function () { + service.display('', '', {}, [0, 0]); + expect(mockBody.append).toHaveBeenCalledWith(mockElement); + }); + + it("provides a function to remove displayed info bubbles", function () { + var fn = service.display('', '', {}, [0, 0]); + expect(mockElement.remove).not.toHaveBeenCalled(); + fn(); + expect(mockElement.remove).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' + ); + }); + }); + + }); + } +); diff --git a/platform/commonUI/inspect/test/suite.json b/platform/commonUI/inspect/test/suite.json new file mode 100644 index 0000000000..82650e2dff --- /dev/null +++ b/platform/commonUI/inspect/test/suite.json @@ -0,0 +1,4 @@ +[ + "gestures/InfoGesture", + "services/InfoService" +] diff --git a/platform/core/bundle.json b/platform/core/bundle.json index 5a2727eb75..97ee2d262f 100644 --- a/platform/core/bundle.json +++ b/platform/core/bundle.json @@ -165,6 +165,10 @@ "implementation": "capabilities/PersistenceCapability.js", "depends": [ "persistenceService", "PERSISTENCE_SPACE" ] }, + { + "key": "metadata", + "implementation": "capabilities/MetadataCapability.js" + }, { "key": "mutation", "implementation": "capabilities/MutationCapability.js", diff --git a/platform/core/src/capabilities/MetadataCapability.js b/platform/core/src/capabilities/MetadataCapability.js new file mode 100644 index 0000000000..677dab0008 --- /dev/null +++ b/platform/core/src/capabilities/MetadataCapability.js @@ -0,0 +1,92 @@ +/*global define*/ + +define( + ['moment'], + function (moment) { + "use strict"; + + /** + * A piece of information about a domain object. + * @typedef {Object} MetadataProperty + * @property {string} name the human-readable name of this property + * @property {string} value the human-readable value of this property, + * for this specific domain object + */ + + var TIME_FORMAT = "YYYY-MM-DD HH:mm:ss"; + + /** + * Implements the `metadata` capability of a domain object, providing + * properties of that object for display. + * + * Usage: `domainObject.useCapability("metadata")` + * + * ...which will return an array of objects containing `name` and + * `value` properties describing that domain object (suitable for + * display.) + * + * @constructor + */ + function MetadataCapability(domainObject) { + var model = domainObject.getModel(); + + function hasDisplayableValue(metadataProperty) { + var t = typeof metadataProperty.value; + return (t === 'string' || t === 'number'); + } + + function formatTimestamp(timestamp) { + return typeof timestamp === 'number' ? + (moment.utc(timestamp).format(TIME_FORMAT) + " UTC") : + undefined; + } + + function getProperties() { + var type = domainObject.getCapability('type'); + + function lookupProperty(typeProperty) { + return { + name: typeProperty.getDefinition().name, + value: typeProperty.getValue(model) + }; + } + + return (type ? type.getProperties() : []).map(lookupProperty); + } + + function getCommonMetadata() { + var type = domainObject.getCapability('type'); + // Note that invalid values will be filtered out later + return [ + { + name: "Updated", + value: formatTimestamp(model.modified) + }, + { + name: "Type", + value: type && type.getName() + }, + { + name: "ID", + value: domainObject.getId() + } + ]; + } + + function getMetadata() { + return getProperties().concat(getCommonMetadata()) + .filter(hasDisplayableValue); + } + + return { + /** + * Get metadata about this object. + * @returns {MetadataProperty[]} metadata about this object + */ + invoke: getMetadata + }; + } + + return MetadataCapability; + } +); diff --git a/platform/core/test/capabilities/MetadataCapabilitySpec.js b/platform/core/test/capabilities/MetadataCapabilitySpec.js new file mode 100644 index 0000000000..272746f037 --- /dev/null +++ b/platform/core/test/capabilities/MetadataCapabilitySpec.js @@ -0,0 +1,101 @@ +/***************************************************************************** + * 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/capabilities/MetadataCapability'], + function (MetadataCapability) { + "use strict"; + + describe("The metadata capability", function () { + var mockDomainObject, + mockType, + mockProperties, + testModel, + metadata; + + function getCapability(key) { + return key === 'type' ? mockType : undefined; + } + + function findValue(properties, name) { + var i; + for (i = 0; i < properties.length; i += 1) { + if (properties[i].name === name) { + return properties[i].value; + } + } + } + + beforeEach(function () { + mockDomainObject = jasmine.createSpyObj( + 'domainObject', + ['getId', 'getCapability', 'useCapability', 'getModel'] + ); + mockType = jasmine.createSpyObj( + 'type', + ['getProperties', 'getName'] + ); + mockProperties = ['a', 'b', 'c'].map(function (k) { + var mockProperty = jasmine.createSpyObj( + 'property-' + k, + ['getValue', 'getDefinition'] + ); + mockProperty.getValue.andReturn("Value " + k); + mockProperty.getDefinition.andReturn({ name: "Property " + k}); + return mockProperty; + }); + testModel = { name: "" }; + + mockDomainObject.getId.andReturn("Test id"); + mockDomainObject.getModel.andReturn(testModel); + mockDomainObject.getCapability.andCallFake(getCapability); + mockDomainObject.useCapability.andCallFake(getCapability); + mockType.getProperties.andReturn(mockProperties); + mockType.getName.andReturn("Test type"); + + metadata = new MetadataCapability(mockDomainObject); + }); + + it("reads properties from the domain object model", function () { + metadata.invoke(); + mockProperties.forEach(function (mockProperty) { + expect(mockProperty.getValue).toHaveBeenCalledWith(testModel); + }); + }); + + it("reports type-specific properties", function () { + var properties = metadata.invoke(); + expect(findValue(properties, 'Property a')).toEqual("Value a"); + expect(findValue(properties, 'Property b')).toEqual("Value b"); + expect(findValue(properties, 'Property c')).toEqual("Value c"); + }); + + it("reports generic properties", function () { + var properties = metadata.invoke(); + expect(findValue(properties, 'ID')).toEqual("Test id"); + expect(findValue(properties, 'Type')).toEqual("Test type"); + }); + + }); + } +); diff --git a/platform/core/test/suite.json b/platform/core/test/suite.json index 36f3e81980..26749fa612 100644 --- a/platform/core/test/suite.json +++ b/platform/core/test/suite.json @@ -9,6 +9,7 @@ "capabilities/ContextualDomainObject", "capabilities/CoreCapabilityProvider", "capabilities/DelegationCapability", + "capabilities/MetadataCapability", "capabilities/MutationCapability", "capabilities/PersistenceCapability", "capabilities/RelationshipCapability", diff --git a/platform/forms/test/controllers/DateTimeControllerSpec.js b/platform/forms/test/controllers/DateTimeControllerSpec.js index 11c97ada07..230d6a7a33 100644 --- a/platform/forms/test/controllers/DateTimeControllerSpec.js +++ b/platform/forms/test/controllers/DateTimeControllerSpec.js @@ -84,6 +84,22 @@ define( // Should have cleared out the time stamp expect(mockScope.ngModel.test).toBeUndefined(); }); + + it("initializes form fields with values from ng-model", function () { + mockScope.ngModel = { test: 1417215313000 }; + mockScope.field = "test"; + mockScope.$watch.calls.forEach(function (call) { + if (call.args[0] === 'ngModel[field]') { + call.args[1](mockScope.ngModel.test); + } + }); + expect(mockScope.datetime).toEqual({ + date: "2014-332", + hour: "22", + min: "55", + sec: "13" + }); + }); }); } -); \ No newline at end of file +); diff --git a/platform/telemetry/src/TelemetryFormatter.js b/platform/telemetry/src/TelemetryFormatter.js index 0bb13fbf6d..853db642d7 100644 --- a/platform/telemetry/src/TelemetryFormatter.js +++ b/platform/telemetry/src/TelemetryFormatter.js @@ -22,8 +22,8 @@ /*global define,moment*/ define( - ['../lib/moment.min.js'], - function () { + ['moment'], + function (moment) { "use strict"; // Date format to use for domain values; in particular,