diff --git a/bower.json b/bower.json index ed3dbaf899..369dcdc81b 100644 --- a/bower.json +++ b/bower.json @@ -22,6 +22,7 @@ "eventemitter3": "^1.2.0", "lodash": "3.10.1", "almond": "~0.3.2", - "html2canvas": "^0.4.1" + "html2canvas": "^0.4.1", + "moment-timezone": "^0.5.13" } } diff --git a/openmct.js b/openmct.js index 4c578b9b3f..8f116ea1a6 100644 --- a/openmct.js +++ b/openmct.js @@ -32,6 +32,7 @@ requirejs.config({ "html2canvas": "bower_components/html2canvas/build/html2canvas.min", "moment": "bower_components/moment/moment", "moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format", + "moment-timezone": "bower_components/moment-timezone/builds/moment-timezone-with-data", "saveAs": "bower_components/FileSaver.js/FileSaver.min", "screenfull": "bower_components/screenfull/dist/screenfull.min", "text": "bower_components/text/text", diff --git a/platform/commonUI/general/res/sass/controls/_controls.scss b/platform/commonUI/general/res/sass/controls/_controls.scss index 4c2c1866c9..74ce20abaf 100644 --- a/platform/commonUI/general/res/sass/controls/_controls.scss +++ b/platform/commonUI/general/res/sass/controls/_controls.scss @@ -307,6 +307,40 @@ textarea.lg { position: relative; height: 300px; } } } +/******************************************************** AUTOCOMPLETE */ +.autocomplete { + input { + width: 226px; + padding: 5px 0px 5px 7px; + } + .icon-arrow-down { + position: absolute; + top: 8px; + left: 210px; + font-size: 10px; + cursor: pointer; + } + .autocompleteOptions { + border: 1px solid $colorFormLines; + border-radius: 5px; + width: 224px; + max-height: 170px; + overflow-y: auto; + overflow-x: hidden; + li { + border: 1px solid $colorFormLines; + padding: 8px 0px 8px 5px; + .optionText { + cursor: pointer; + } + } + .optionPreSelected { + background-color: $colorInspectorSectionHeaderBg; + color: $colorInspectorSectionHeaderFg; + } + } +} + /******************************************************** OBJECT-HEADER */ .object-header { font-size: 1em; diff --git a/platform/features/clock/bundle.js b/platform/features/clock/bundle.js index 022f81f809..d3f98426bf 100644 --- a/platform/features/clock/bundle.js +++ b/platform/features/clock/bundle.js @@ -21,6 +21,7 @@ *****************************************************************************/ define([ + "moment-timezone", "./src/indicators/ClockIndicator", "./src/services/TickerService", "./src/controllers/ClockController", @@ -34,6 +35,7 @@ define([ "text!./res/templates/timer.html", 'legacyRegistry' ], function ( + MomentTimezone, ClockIndicator, TickerService, ClockController, @@ -226,13 +228,20 @@ define([ "cssClass": "l-inline" } ] + }, + { + "key": "timezone", + "name": "Timezone", + "control": "autocomplete", + "options": MomentTimezone.tz.names() } ], "model": { "clockFormat": [ "YYYY/MM/DD hh:mm:ss", "clock12" - ] + ], + "timezone": "UTC" } }, { diff --git a/platform/features/clock/src/controllers/ClockController.js b/platform/features/clock/src/controllers/ClockController.js index 9103fd5327..3c7dc7b216 100644 --- a/platform/features/clock/src/controllers/ClockController.js +++ b/platform/features/clock/src/controllers/ClockController.js @@ -20,9 +20,14 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define( - ['moment'], - function (moment) { +define([ + 'moment', + 'moment-timezone' + ], + function ( + moment, + momentTimezone + ) { /** * Controller for views of a Clock domain object. @@ -37,10 +42,13 @@ define( var lastTimestamp, unlisten, timeFormat, + zoneName, self = this; function update() { - var m = moment.utc(lastTimestamp); + var m = zoneName ? + moment.utc(lastTimestamp).tz(zoneName) : moment.utc(lastTimestamp); + self.zoneAbbr = m.zoneAbbr(); self.textValue = timeFormat && m.format(timeFormat); self.ampmValue = m.format("A"); // Just the AM or PM part } @@ -50,21 +58,23 @@ define( update(); } - function updateFormat(clockFormat) { + function updateModel(model) { var baseFormat; + if (model !== undefined) { + baseFormat = model.clockFormat[0]; - if (clockFormat !== undefined) { - baseFormat = clockFormat[0]; - - self.use24 = clockFormat[1] === 'clock24'; + self.use24 = model.clockFormat[1] === 'clock24'; timeFormat = self.use24 ? baseFormat.replace('hh', "HH") : baseFormat; - + // If wrong timezone is provided, the UTC will be used + zoneName = momentTimezone.tz.names().includes(model.timezone) ? + model.timezone : "UTC"; update(); } } - // Pull in the clock format from the domain object model - $scope.$watch('model.clockFormat', updateFormat); + + // Pull in the model (clockFormat and timezone) from the domain object model + $scope.$watch('model', updateModel); // Listen for clock ticks ... and stop listening on destroy unlisten = tickerService.listen(tick); @@ -76,7 +86,7 @@ define( * @returns {string} */ ClockController.prototype.zone = function () { - return "UTC"; + return this.zoneAbbr; }; /** diff --git a/platform/features/clock/test/controllers/ClockControllerSpec.js b/platform/features/clock/test/controllers/ClockControllerSpec.js index e48cc5c3ae..8774333d14 100644 --- a/platform/features/clock/test/controllers/ClockControllerSpec.js +++ b/platform/features/clock/test/controllers/ClockControllerSpec.js @@ -1,5 +1,5 @@ /***************************************************************************** - * Open MCT, Copyright (c) 2009-2016, United States Government + * Open MCT, Copyright (c) 2009-2017, United States Government * as represented by the Administrator of the National Aeronautics and Space * Administration. All rights reserved. * @@ -43,9 +43,9 @@ define( controller = new ClockController(mockScope, mockTicker); }); - it("watches for clock format from the domain object model", function () { + it("watches for model (clockFormat and timezone) from the domain object model", function () { expect(mockScope.$watch).toHaveBeenCalledWith( - "model.clockFormat", + "model", jasmine.any(Function) ); }); @@ -67,29 +67,35 @@ define( it("formats using the format string from the model", function () { mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP); - mockScope.$watch.mostRecentCall.args[1]([ - "YYYY-DDD hh:mm:ss", - "clock24" - ]); + mockScope.$watch.mostRecentCall.args[1]({ + "clockFormat": [ + "YYYY-DDD hh:mm:ss", + "clock24" + ], + "timezone": "Canada/Eastern" + }); - expect(controller.zone()).toEqual("UTC"); - expect(controller.text()).toEqual("2015-154 17:56:14"); + expect(controller.zone()).toEqual("EDT"); + expect(controller.text()).toEqual("2015-154 13:56:14"); expect(controller.ampm()).toEqual(""); }); it("formats 12-hour time", function () { mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP); - mockScope.$watch.mostRecentCall.args[1]([ - "YYYY-DDD hh:mm:ss", - "clock12" - ]); + mockScope.$watch.mostRecentCall.args[1]({ + "clockFormat": [ + "YYYY-DDD hh:mm:ss", + "clock12" + ], + "timezone": "" + }); expect(controller.zone()).toEqual("UTC"); expect(controller.text()).toEqual("2015-154 05:56:14"); expect(controller.ampm()).toEqual("PM"); }); - it("does not throw exceptions when clockFormat is undefined", function () { + it("does not throw exceptions when model is undefined", function () { mockTicker.listen.mostRecentCall.args[0](TEST_TIMESTAMP); expect(function () { mockScope.$watch.mostRecentCall.args[1](undefined); diff --git a/platform/forms/bundle.js b/platform/forms/bundle.js index 0f93e0f6bf..ab8ceca0da 100644 --- a/platform/forms/bundle.js +++ b/platform/forms/bundle.js @@ -24,10 +24,12 @@ define([ "./src/MCTForm", "./src/MCTToolbar", "./src/MCTControl", + "./src/controllers/AutocompleteController", "./src/controllers/DateTimeController", "./src/controllers/CompositeController", "./src/controllers/ColorController", "./src/controllers/DialogButtonController", + "text!./res/templates/controls/autocomplete.html", "text!./res/templates/controls/checkbox.html", "text!./res/templates/controls/datetime.html", "text!./res/templates/controls/select.html", @@ -45,10 +47,12 @@ define([ MCTForm, MCTToolbar, MCTControl, + AutocompleteController, DateTimeController, CompositeController, ColorController, DialogButtonController, + autocompleteTemplate, checkboxTemplate, datetimeTemplate, selectTemplate, @@ -87,6 +91,10 @@ define([ } ], "controls": [ + { + "key": "autocomplete", + "template": autocompleteTemplate + }, { "key": "checkbox", "template": checkboxTemplate @@ -137,6 +145,14 @@ define([ } ], "controllers": [ + { + "key": "AutocompleteController", + "implementation": AutocompleteController, + "depends": [ + "$scope", + "$element" + ] + }, { "key": "DateTimeController", "implementation": DateTimeController, diff --git a/platform/forms/res/templates/controls/autocomplete.html b/platform/forms/res/templates/controls/autocomplete.html new file mode 100644 index 0000000000..9649088848 --- /dev/null +++ b/platform/forms/res/templates/controls/autocomplete.html @@ -0,0 +1,46 @@ + + +
+ + +
+ +
+
\ No newline at end of file diff --git a/platform/forms/src/controllers/AutocompleteController.js b/platform/forms/src/controllers/AutocompleteController.js new file mode 100644 index 0000000000..46423ba728 --- /dev/null +++ b/platform/forms/src/controllers/AutocompleteController.js @@ -0,0 +1,138 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define( + [], + function () { + + /** + * Controller for the `autocomplete` form control. + * + * @memberof platform/forms + * @constructor + */ + function AutocompleteController($scope, $element) { + + var key = { + down: 40, + up: 38, + enter: 13 + }, + autocompleteInputElement = $element[0].getElementsByClassName('autocompleteInput')[0]; + + if ($scope.options[0].name) { + // If "options" include name, value pair + $scope.optionNames = $scope.options.map(function (opt) { + return opt.name; + }); + } else { + // If options is only an array of string. + $scope.optionNames = $scope.options; + } + + function fillInputWithIndexedOption() { + if ($scope.filteredOptions[$scope.optionIndex]) { + $scope.ngModel[$scope.field] = $scope.filteredOptions[$scope.optionIndex].name; + } + } + + function decrementOptionIndex() { + if ($scope.optionIndex === 0) { + $scope.optionIndex = $scope.filteredOptions.length; + } + $scope.optionIndex--; + fillInputWithIndexedOption(); + } + + function incrementOptionIndex() { + if ($scope.optionIndex === $scope.filteredOptions.length - 1) { + $scope.optionIndex = -1; + } + $scope.optionIndex++; + fillInputWithIndexedOption(); + } + + function fillInputWithString(string) { + $scope.hideOptions = true; + $scope.ngModel[$scope.field] = string; + } + + function showOptions(string) { + $scope.hideOptions = false; + $scope.filterOptions(string); + $scope.optionIndex = 0; + } + + $scope.keyDown = function ($event) { + if ($scope.filteredOptions) { + var keyCode = $event.keyCode; + switch (keyCode) { + case key.down: + incrementOptionIndex(); + break; + case key.up: + $event.preventDefault(); // Prevents cursor jumping back and forth + decrementOptionIndex(); + break; + case key.enter: + if ($scope.filteredOptions[$scope.optionIndex]) { + fillInputWithString($scope.filteredOptions[$scope.optionIndex].name); + } + } + } + }; + + $scope.filterOptions = function (string) { + $scope.hideOptions = false; + $scope.filteredOptions = $scope.optionNames.filter(function (option) { + return option.toLowerCase().indexOf(string.toLowerCase()) >= 0; + }).map(function (option, index) { + return { + optionId: index, + name: option + }; + }); + }; + + $scope.inputClicked = function () { + autocompleteInputElement.select(); + showOptions(autocompleteInputElement.value); + }; + + $scope.arrowClicked = function () { + autocompleteInputElement.select(); + showOptions(''); + }; + + $scope.fillInput = function (string) { + fillInputWithString(string); + }; + + $scope.optionMouseover = function (optionId) { + $scope.optionIndex = optionId; + }; + } + + return AutocompleteController; + + } +); diff --git a/platform/forms/test/controllers/AutocompleteControllerSpec.js b/platform/forms/test/controllers/AutocompleteControllerSpec.js new file mode 100644 index 0000000000..4edff63083 --- /dev/null +++ b/platform/forms/test/controllers/AutocompleteControllerSpec.js @@ -0,0 +1,69 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2017, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +define([ + "../../src/controllers/AutocompleteController", + "angular" +], function ( + AutocompleteController, + angular +) { + + describe("The autocomplete controller", function () { + var mockScope, + mockElement, + controller; + + beforeEach(function () { + mockScope = jasmine.createSpyObj("$scope", ["$watch"]); + mockScope.options = ['Asia/Dhaka', 'UTC', 'Toronto', 'Asia/Shanghai', 'Hotel California']; + mockScope.ngModel = [null, null, null, null, null]; + mockScope.field = 4; + mockElement = angular.element("
"); + controller = new AutocompleteController(mockScope, mockElement); + }); + + it("makes optionNames array equal to options if options is an array of string", function () { + expect(mockScope.optionNames).toEqual(mockScope.options); + }); + + it("filters options by returning array containing optionId and name", function () { + mockScope.filterOptions('Asia'); + var filteredOptions = [{ optionId : 0, name : 'Asia/Dhaka' }, + { optionId : 1, name : 'Asia/Shanghai' }]; + expect(mockScope.filteredOptions).toEqual(filteredOptions); + }); + + it("fills input with given string", function () { + var str = "UTC"; + mockScope.fillInput(str); + expect(mockScope.hideOptions).toEqual(true); + expect(mockScope.ngModel[mockScope.field]).toEqual(str); + }); + + it("sets a new optionIndex on mouse hover", function () { + mockScope.optionMouseover(1); + expect(mockScope.optionIndex).toEqual(1); + }); + + }); +}); diff --git a/test-main.js b/test-main.js index 901022ca66..d108b82631 100644 --- a/test-main.js +++ b/test-main.js @@ -58,6 +58,7 @@ requirejs.config({ "html2canvas": "bower_components/html2canvas/build/html2canvas.min", "moment": "bower_components/moment/moment", "moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format", + "moment-timezone": "bower_components/moment-timezone/builds/moment-timezone-with-data", "saveAs": "bower_components/FileSaver.js/FileSaver.min", "screenfull": "bower_components/screenfull/dist/screenfull.min", "text": "bower_components/text/text",