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 f286f362e9..a826466ac4 100644 --- a/platform/commonUI/general/res/sass/controls/_controls.scss +++ b/platform/commonUI/general/res/sass/controls/_controls.scss @@ -297,6 +297,39 @@ 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; + } + .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 c7b941c1d9..99eddef550 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", @@ -32,6 +33,7 @@ define([ "text!./res/templates/timer.html", 'legacyRegistry' ], function ( + MomentTimezone, ClockIndicator, TickerService, ClockController, @@ -200,13 +202,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..945be23f87 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 = zoneName ? m.zoneAbbr() : "UTC"; 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; - - update(); + // 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 8bcc1ea7a1..c723962571 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", @@ -44,10 +46,12 @@ define([ MCTForm, MCTToolbar, MCTControl, + AutocompleteController, DateTimeController, CompositeController, ColorController, DialogButtonController, + autocompleteTemplate, checkboxTemplate, datetimeTemplate, selectTemplate, @@ -85,6 +89,10 @@ define([ } ], "controls": [ + { + "key": "autocomplete", + "template": autocompleteTemplate + }, { "key": "checkbox", "template": checkboxTemplate @@ -131,6 +139,13 @@ define([ } ], "controllers": [ + { + "key": "AutocompleteController", + "implementation": AutocompleteController, + "depends": [ + "$scope" + ] + }, { "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..14022916a8 --- /dev/null +++ b/platform/forms/res/templates/controls/autocomplete.html @@ -0,0 +1,43 @@ + + +
+ + +
+ +
+
\ 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..d7fd7baa8d --- /dev/null +++ b/platform/forms/src/controllers/AutocompleteController.js @@ -0,0 +1,128 @@ +/***************************************************************************** + * 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) { + + var key = { + down: 40, + up: 38, + enter: 13 + } + + 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 decrementOptionIndex() { + if($scope.optionIndex === 0) { + $scope.optionIndex = $scope.filteredOptions.length; + } + $scope.optionIndex--; + } + + function incrementOptionIndex() { + if($scope.optionIndex === $scope.filteredOptions.length-1) { + $scope.optionIndex = -1; + } + $scope.optionIndex++; + } + + function fillInputWithString(string) { + $scope.hideOptions = true; + // Hard coded!! + $scope.ngModel[4] = string; + } + + function fillInputWithIndexedOption() { + $scope.ngModel[4] = $scope.filteredOptions[$scope.optionIndex].name; + } + + $scope.keyDown = function($event) { + if($scope.filteredOptions) { + var keyCode = $event.keyCode; + switch(keyCode) { + case key.down: + incrementOptionIndex(); + fillInputWithIndexedOption(); + break; + case key.up: + $event.preventDefault(); // Prevents cursor jumping back and forth + decrementOptionIndex(); + fillInputWithIndexedOption(); + 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($event) { + var target = $event.target; + target.select(); + $scope.hideOptions = false; + $scope.filterOptions(target.value); + $scope.optionIndex = 0; + } + + $scope.fillInput = function(string) { + fillInputWithString(string); + } + + $scope.optionMouseover = function(optionId) { + $scope.optionIndex = optionId; + } + } + + return AutocompleteController; + + } +); \ No newline at end of file diff --git a/platform/forms/test/controllers/AutocompleteControllerSpec.js b/platform/forms/test/controllers/AutocompleteControllerSpec.js new file mode 100644 index 0000000000..7de87a2b6d --- /dev/null +++ b/platform/forms/test/controllers/AutocompleteControllerSpec.js @@ -0,0 +1,63 @@ +/***************************************************************************** + * 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"], + function (AutocompleteController) { + + describe("The autocomplete controller", function () { + var mockScope, + 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]; + controller = new AutocompleteController(mockScope); + }); + + 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[4]).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",