From 695ca5c0cf39d2dcecef44623c1fb2df2f829374 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 26 Nov 2014 08:01:00 -0800 Subject: [PATCH 01/18] [Forms] Bring in initial work on forms Bring in previous work on the forms component; this includes transitioned versions of specific form elements, and the mct-form direction which generates these. WTD-530 --- platform/forms/bundle.json | 42 +++++++++++++++++++ platform/forms/res/templates/_checkbox.html | 6 +++ platform/forms/res/templates/_checkboxes.html | 0 platform/forms/res/templates/_datetime.html | 24 +++++++++++ platform/forms/res/templates/_select.html | 7 ++++ platform/forms/res/templates/_selects.html | 5 +++ platform/forms/res/templates/_textfield.html | 8 ++++ platform/forms/res/templates/form.html | 38 +++++++++++++++++ platform/forms/res/templates/textfields.html | 7 ++++ platform/forms/src/MCTForm.js | 31 ++++++++++++++ 10 files changed, 168 insertions(+) create mode 100644 platform/forms/bundle.json create mode 100644 platform/forms/res/templates/_checkbox.html create mode 100644 platform/forms/res/templates/_checkboxes.html create mode 100644 platform/forms/res/templates/_datetime.html create mode 100644 platform/forms/res/templates/_select.html create mode 100644 platform/forms/res/templates/_selects.html create mode 100644 platform/forms/res/templates/_textfield.html create mode 100644 platform/forms/res/templates/form.html create mode 100644 platform/forms/res/templates/textfields.html create mode 100644 platform/forms/src/MCTForm.js diff --git a/platform/forms/bundle.json b/platform/forms/bundle.json new file mode 100644 index 0000000000..776d0ee596 --- /dev/null +++ b/platform/forms/bundle.json @@ -0,0 +1,42 @@ +{ + "name": "MCT Forms", + "description": "Form generator; includes directive and some controls.", + "extensions": { + "directives": [ + { + "key": "mctForm", + "implementation": "MCTForm.js" + } + ], + "templates": [ + { + "key": "_checkbox", + "templateUrl": "templates/_checkbox.html" + }, + { + "key": "_checkboxes", + "templateUrl": "templates/_checkboxes.html" + }, + { + "key": "_datetime", + "templateUrl": "templates/_datetime.html" + }, + { + "key": "_select", + "templateUrl": "templates/_select.html" + }, + { + "key": "_selects", + "templateUrl": "templates/_selects.html" + }, + { + "key": "_textfield", + "templateUrl": "templates/_textfield.html" + }, + { + "key": "_textfields", + "templateUrl": "templates/_textfields.html" + } + ] + } +} \ No newline at end of file diff --git a/platform/forms/res/templates/_checkbox.html b/platform/forms/res/templates/_checkbox.html new file mode 100644 index 0000000000..1412c4ee14 --- /dev/null +++ b/platform/forms/res/templates/_checkbox.html @@ -0,0 +1,6 @@ +
+ +
\ No newline at end of file diff --git a/platform/forms/res/templates/_checkboxes.html b/platform/forms/res/templates/_checkboxes.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/platform/forms/res/templates/_datetime.html b/platform/forms/res/templates/_datetime.html new file mode 100644 index 0000000000..55e0f7bdcb --- /dev/null +++ b/platform/forms/res/templates/_datetime.html @@ -0,0 +1,24 @@ +
+
+
+ Date + Hour + Min + Sec + Timezone +
+ +
+ + + + + + + +
+
+
\ No newline at end of file diff --git a/platform/forms/res/templates/_select.html b/platform/forms/res/templates/_select.html new file mode 100644 index 0000000000..e778d76450 --- /dev/null +++ b/platform/forms/res/templates/_select.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/platform/forms/res/templates/_selects.html b/platform/forms/res/templates/_selects.html new file mode 100644 index 0000000000..449051ed56 --- /dev/null +++ b/platform/forms/res/templates/_selects.html @@ -0,0 +1,5 @@ +
+ {#processedValues} + {#view key="_select"/} + {/processedValues} +
\ No newline at end of file diff --git a/platform/forms/res/templates/_textfield.html b/platform/forms/res/templates/_textfield.html new file mode 100644 index 0000000000..f351520231 --- /dev/null +++ b/platform/forms/res/templates/_textfield.html @@ -0,0 +1,8 @@ +
+ + + + + +
+ diff --git a/platform/forms/res/templates/form.html b/platform/forms/res/templates/form.html new file mode 100644 index 0000000000..b3dc0db27b --- /dev/null +++ b/platform/forms/res/templates/form.html @@ -0,0 +1,38 @@ +{{structure.name}} +
+ +
{{section.name}}
+
+ + + + + + + + + + +
+
{{row.name}}
+
+
+ +
+ {{item.control}} | Item.Key={{item.key}} | model={{model[item.key]}} + * + +
+
+
+
+
+
+ +CLICK! +
Model: {{model | json}}
+
\ No newline at end of file diff --git a/platform/forms/res/templates/textfields.html b/platform/forms/res/templates/textfields.html new file mode 100644 index 0000000000..39a01c489e --- /dev/null +++ b/platform/forms/res/templates/textfields.html @@ -0,0 +1,7 @@ +
+
+ {#processedValues} + {#view key="_textfield"/} {label} + {/processedValues} +
+
\ No newline at end of file diff --git a/platform/forms/src/MCTForm.js b/platform/forms/src/MCTForm.js new file mode 100644 index 0000000000..c0585d2a00 --- /dev/null +++ b/platform/forms/src/MCTForm.js @@ -0,0 +1,31 @@ +/*global define,Promise*/ + +/** + * Module defining MCTForm. Created by vwoeltje on 11/10/14. + */ +define( + [], + function () { + "use strict"; + + /** + * + * @constructor + */ + function MCTForm() { + var templatePath = [ + "platform/forms", //MCTForm.bundle.path, + "res", //MCTForm.bundle.resources, + "templates/form.html" + ].join("/"); + + return { + restrict: "E", + templateUrl: templatePath, + scope: { structure: "=", model: "=ngModel" } + }; + } + + return MCTForm; + } +); \ No newline at end of file From 9576673b8475f6def2264a570913d08bb6685fc3 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 26 Nov 2014 08:43:41 -0800 Subject: [PATCH 02/18] [Forms] Add mct-control directive Add an mct-control directive which can represent individual form controls. WTD-530. --- platform/forms/src/MCTControl.js | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 platform/forms/src/MCTControl.js diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js new file mode 100644 index 0000000000..e48b90629b --- /dev/null +++ b/platform/forms/src/MCTControl.js @@ -0,0 +1,49 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + function MCTControl(controls) { + var controlMap = {}; + + // Prepopulate controlMap for easy look up by key + controls.forEach(function (control) { + var path = [ + control.bundle.path, + control.bundle.resources, + control.templateUrl + ].join("/"); + controlMap[control.key] = path; + }); + + function controller($scope) { + $scope.$watch("key", function (key) { + // Pass the template URL to ng-include via scope. + $scope.inclusion = controlMap[$scope.key]; + }); + } + + return { + // Only show at the element level + restrict: "E", + + // Use the included controller to populate scope + controller: controller, + + // Use ng-include as a template; "inclusion" will be the real + // template path + template: '', + + // Pass through Angular's normal input field attributes + scope: { + ngModel: "=", + ngOptions: "=", + ngDisabled: "=", + ngPattern: "=" + } + }; + } + } +); \ No newline at end of file From 658d485ccc557fa34fbfb47e9d410a91bb77e585 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 26 Nov 2014 10:43:48 -0800 Subject: [PATCH 03/18] [Forms] Initial minimal functionality Initial minimal working implementation where a two-way binding between form and form user is observable. Notably, change ng-options to options, since ng-options is terminal (it breaks mct-control). WTD-530 --- platform/forms/bundle.json | 91 +++++++++++-------- .../res/templates/controls/checkbox.html | 7 ++ platform/forms/res/templates/form.html | 62 +++++++------ platform/forms/src/MCTControl.js | 29 +++++- platform/forms/src/MCTForm.js | 2 +- 5 files changed, 118 insertions(+), 73 deletions(-) create mode 100644 platform/forms/res/templates/controls/checkbox.html diff --git a/platform/forms/bundle.json b/platform/forms/bundle.json index 776d0ee596..782f2e4a2e 100644 --- a/platform/forms/bundle.json +++ b/platform/forms/bundle.json @@ -1,42 +1,53 @@ { - "name": "MCT Forms", - "description": "Form generator; includes directive and some controls.", - "extensions": { - "directives": [ - { - "key": "mctForm", - "implementation": "MCTForm.js" - } - ], - "templates": [ - { - "key": "_checkbox", - "templateUrl": "templates/_checkbox.html" - }, - { - "key": "_checkboxes", - "templateUrl": "templates/_checkboxes.html" - }, - { - "key": "_datetime", - "templateUrl": "templates/_datetime.html" - }, - { - "key": "_select", - "templateUrl": "templates/_select.html" - }, - { - "key": "_selects", - "templateUrl": "templates/_selects.html" - }, - { - "key": "_textfield", - "templateUrl": "templates/_textfield.html" - }, - { - "key": "_textfields", - "templateUrl": "templates/_textfields.html" - } - ] - } + "name": "MCT Forms", + "description": "Form generator; includes directive and some controls.", + "extensions": { + "directives": [ + { + "key": "mctForm", + "implementation": "MCTForm.js" + }, + { + "key": "mctControl", + "implementation": "MCTControl.js", + "depends": [ "controls[]" ] + } + ], + "controls": [ + { + "key": "checkbox", + "templateUrl": "templates/controls/checkbox.html" + } + ], + "templates": [ + { + "key": "_checkbox", + "templateUrl": "templates/_checkbox.html" + }, + { + "key": "_checkboxes", + "templateUrl": "templates/_checkboxes.html" + }, + { + "key": "_datetime", + "templateUrl": "templates/_datetime.html" + }, + { + "key": "_select", + "templateUrl": "templates/_select.html" + }, + { + "key": "_selects", + "templateUrl": "templates/_selects.html" + }, + { + "key": "_textfield", + "templateUrl": "templates/_textfield.html" + }, + { + "key": "_textfields", + "templateUrl": "templates/_textfields.html" + } + ] + } } \ No newline at end of file diff --git a/platform/forms/res/templates/controls/checkbox.html b/platform/forms/res/templates/controls/checkbox.html new file mode 100644 index 0000000000..a33eb4b86e --- /dev/null +++ b/platform/forms/res/templates/controls/checkbox.html @@ -0,0 +1,7 @@ + diff --git a/platform/forms/res/templates/form.html b/platform/forms/res/templates/form.html index b3dc0db27b..9b2b2c5d64 100644 --- a/platform/forms/res/templates/form.html +++ b/platform/forms/res/templates/form.html @@ -1,38 +1,42 @@ -{{structure.name}} +
+
-
{{section.name}}
+
+ {{section.name}} +
- - - - - - - - - - -
-
{{row.name}}
-
-
+
-
- {{item.control}} | Item.Key={{item.key}} | model={{model[item.key]}} - * - -
+
{{row.name}}
+
+
+ + +
+
+ +
+
- -CLICK! -
Model: {{model | json}}
-
\ No newline at end of file +
+ + \ No newline at end of file diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index e48b90629b..cbac1aedce 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -21,7 +21,9 @@ define( function controller($scope) { $scope.$watch("key", function (key) { // Pass the template URL to ng-include via scope. - $scope.inclusion = controlMap[$scope.key]; + $scope.inclusion = controlMap[key]; + console.log(key); + console.log($scope.inclusion); }); } @@ -36,14 +38,35 @@ define( // template path template: '', + // ngOptions is terminal, so we need to be higher priority + priority: 1000, + // Pass through Angular's normal input field attributes scope: { + // Used to choose which form control to use + key: "=", + + // The state of the form value itself ngModel: "=", - ngOptions: "=", + + // Enabled/disabled state ngDisabled: "=", - ngPattern: "=" + + // Pattern (for input fields) + ngPattern: "=", + + // Set of choices (if any) + options: "=", + + // Structure (subtree of Form Structure) + structure: "=", + + // Name, as in " Date: Wed, 26 Nov 2014 12:50:51 -0800 Subject: [PATCH 04/18] [Forms] Add more form controls Add remaining platform form controls; amend mct-form and mct-control directives to better communicate state. Begin working on problem of communicating validation back out of the form. WTD-530. --- platform/forms/bundle.json | 12 ++++++++ .../res/templates/controls/datetime.html | 28 +++++++++++++++++++ .../forms/res/templates/controls/select.html | 7 +++++ .../res/templates/controls/textfield.html | 7 +++++ platform/forms/res/templates/form.html | 14 ++++++---- platform/forms/src/MCTControl.js | 2 -- platform/forms/src/MCTForm.js | 15 +++++++++- 7 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 platform/forms/res/templates/controls/datetime.html create mode 100644 platform/forms/res/templates/controls/select.html create mode 100644 platform/forms/res/templates/controls/textfield.html diff --git a/platform/forms/bundle.json b/platform/forms/bundle.json index 782f2e4a2e..cfdbea6411 100644 --- a/platform/forms/bundle.json +++ b/platform/forms/bundle.json @@ -17,6 +17,18 @@ { "key": "checkbox", "templateUrl": "templates/controls/checkbox.html" + }, + { + "key": "datetime", + "templateUrl": "templates/controls/datetime.html" + }, + { + "key": "select", + "templateUrl": "templates/controls/select.html" + }, + { + "key": "textfield", + "templateUrl": "templates/controls/textfield.html" } ], "templates": [ diff --git a/platform/forms/res/templates/controls/datetime.html b/platform/forms/res/templates/controls/datetime.html new file mode 100644 index 0000000000..4978c76ecc --- /dev/null +++ b/platform/forms/res/templates/controls/datetime.html @@ -0,0 +1,28 @@ +
+
+ Date + Hour + Min + Sec + Timezone +
+ +
+ + + + + + + + + + + + + + UTC + +
+
+
\ No newline at end of file diff --git a/platform/forms/res/templates/controls/select.html b/platform/forms/res/templates/controls/select.html new file mode 100644 index 0000000000..01e32892e0 --- /dev/null +++ b/platform/forms/res/templates/controls/select.html @@ -0,0 +1,7 @@ + diff --git a/platform/forms/res/templates/controls/textfield.html b/platform/forms/res/templates/controls/textfield.html new file mode 100644 index 0000000000..420a7fbb7c --- /dev/null +++ b/platform/forms/res/templates/controls/textfield.html @@ -0,0 +1,7 @@ + + + + + diff --git a/platform/forms/res/templates/form.html b/platform/forms/res/templates/form.html index 9b2b2c5d64..c9b2148279 100644 --- a/platform/forms/res/templates/form.html +++ b/platform/forms/res/templates/form.html @@ -1,4 +1,4 @@ -
+
@@ -10,13 +10,18 @@ class="form-row validates" ng-class="{ required: row.required }"> -
{{row.name}}
+
+ {{row.name}} + + i + +
-
+
> @@ -26,7 +31,6 @@ diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index cbac1aedce..e564be239d 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -22,8 +22,6 @@ define( $scope.$watch("key", function (key) { // Pass the template URL to ng-include via scope. $scope.inclusion = controlMap[key]; - console.log(key); - console.log($scope.inclusion); }); } diff --git a/platform/forms/src/MCTForm.js b/platform/forms/src/MCTForm.js index f475aed5e8..5a342bd2fc 100644 --- a/platform/forms/src/MCTForm.js +++ b/platform/forms/src/MCTForm.js @@ -19,10 +19,23 @@ define( "templates/form.html" ].join("/"); + function controller($scope) { + $scope.$watch("mctForm", function (mctForm) { + if ($scope.name) { + $scope.$parent.mctForm = mctForm; + } + }); + } + return { restrict: "E", templateUrl: templatePath, - scope: { structure: "=", ngModel: "=ngModel" } + link: controller, + scope: { + structure: "=", + ngModel: "=", + name: "@" + } }; } From b31b4770d1c974f8c8461866f687fcbc5e395a8d Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 26 Nov 2014 13:08:57 -0800 Subject: [PATCH 05/18] [Forms] Communicate dirty state Communicate dirty state out of generated forms; WTD-530. --- bundles.json | 5 +-- .../res/templates/controls/checkbox.html | 4 +-- .../forms/res/templates/controls/select.html | 6 ++-- .../res/templates/controls/textfield.html | 4 +-- platform/forms/res/templates/form.html | 33 ++++++++++--------- platform/forms/src/MCTControl.js | 2 +- platform/forms/src/MCTForm.js | 3 +- 7 files changed, 31 insertions(+), 26 deletions(-) diff --git a/bundles.json b/bundles.json index d7edba5db7..74eca7c166 100644 --- a/bundles.json +++ b/bundles.json @@ -2,10 +2,11 @@ "platform/framework", "platform/core", "platform/representation", - "platform/commonUI/browse", - "platform/commonUI/edit", "platform/commonUI/dialog", "platform/commonUI/general", + "platform/forms", + + "example/forms", "example/persistence" ] \ No newline at end of file diff --git a/platform/forms/res/templates/controls/checkbox.html b/platform/forms/res/templates/controls/checkbox.html index a33eb4b86e..0152677ce6 100644 --- a/platform/forms/res/templates/controls/checkbox.html +++ b/platform/forms/res/templates/controls/checkbox.html @@ -1,7 +1,7 @@ diff --git a/platform/forms/res/templates/controls/select.html b/platform/forms/res/templates/controls/select.html index 01e32892e0..6d1186f625 100644 --- a/platform/forms/res/templates/controls/select.html +++ b/platform/forms/res/templates/controls/select.html @@ -1,7 +1,7 @@ diff --git a/platform/forms/res/templates/controls/textfield.html b/platform/forms/res/templates/controls/textfield.html index 420a7fbb7c..a09786337d 100644 --- a/platform/forms/res/templates/controls/textfield.html +++ b/platform/forms/res/templates/controls/textfield.html @@ -1,7 +1,7 @@ + ng-model="ngModel[field]" + name="mctControl"> diff --git a/platform/forms/res/templates/form.html b/platform/forms/res/templates/form.html index c9b2148279..33da4caf08 100644 --- a/platform/forms/res/templates/form.html +++ b/platform/forms/res/templates/form.html @@ -18,23 +18,26 @@
-
> - - +
+ + + +
- - + + + +
diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index e564be239d..ba3713492b 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -60,7 +60,7 @@ define( structure: "=", // Name, as in " Date: Wed, 26 Nov 2014 13:15:12 -0800 Subject: [PATCH 06/18] [Forms] Add validation classes Add classes for required, valid, and invalid, based on form state and on arguments passed into the form. WTD-530. --- platform/forms/res/templates/form.html | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/platform/forms/res/templates/form.html b/platform/forms/res/templates/form.html index 33da4caf08..0195e0b22b 100644 --- a/platform/forms/res/templates/form.html +++ b/platform/forms/res/templates/form.html @@ -6,9 +6,13 @@ {{section.name}}
-
+ +
{{row.name}} @@ -19,7 +23,7 @@
- + -
@@ -40,8 +43,8 @@
-
+
From d7c7e2835fbeff4ec9da620117361a4370608a6e Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 26 Nov 2014 13:20:07 -0800 Subject: [PATCH 07/18] [Forms] Let Angular force required rows Pass along ng-required values such that Angular may handle tracking of form validity based on the presence of required fields. WTD-530. --- platform/forms/res/templates/controls/select.html | 1 + platform/forms/res/templates/controls/textfield.html | 1 + platform/forms/src/MCTControl.js | 3 +++ 3 files changed, 5 insertions(+) diff --git a/platform/forms/res/templates/controls/select.html b/platform/forms/res/templates/controls/select.html index 6d1186f625..947ff3c6ff 100644 --- a/platform/forms/res/templates/controls/select.html +++ b/platform/forms/res/templates/controls/select.html @@ -1,6 +1,7 @@ diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index ba3713492b..b630b72182 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -50,6 +50,9 @@ define( // Enabled/disabled state ngDisabled: "=", + // Whether or not input is required + ngRequired: "=", + // Pattern (for input fields) ngPattern: "=", From bc3d1270d2899d18c70a6504dcc2bc91badbbb03 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Wed, 26 Nov 2014 13:23:30 -0800 Subject: [PATCH 08/18] [Forms] Simplify select model value Simplify select control's interaction with the model; only store the value associated with the option, not the name (which is just displayed for the user.) WTD-530. --- platform/forms/res/templates/controls/select.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/forms/res/templates/controls/select.html b/platform/forms/res/templates/controls/select.html index 947ff3c6ff..999c22c7e7 100644 --- a/platform/forms/res/templates/controls/select.html +++ b/platform/forms/res/templates/controls/select.html @@ -1,6 +1,6 @@ - - - - - - - - - - - - UTC - -
-
+ + +
+ + + + + + + + + + + + + + UTC + +
+
+ +
\ No newline at end of file diff --git a/platform/forms/src/controllers/DateTimeController.js b/platform/forms/src/controllers/DateTimeController.js new file mode 100644 index 0000000000..efd954da44 --- /dev/null +++ b/platform/forms/src/controllers/DateTimeController.js @@ -0,0 +1,34 @@ +/*global define*/ + +define( + [], + function () { + + function DateTimeController($scope) { + + function update() { + var date = $scope.datetime.date, + hour = $scope.datetime.hour, + min = $scope.datetime.min, + sec = $scope.datetime.sec; + + $scope.ngModel[$scope.field] = [ + date, + hour, + min, + sec + ].join("."); + } + + $scope.$watch("datetime.date", update); + $scope.$watch("datetime.hour", update); + $scope.$watch("datetime.min", update); + $scope.$watch("datetime.sec", update); + + $scope.datetime = {}; + } + + return DateTimeController; + + } +); \ No newline at end of file From aaa0ff768d6f67618bc9958fee53e395f0977e4d Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 13:35:24 -0800 Subject: [PATCH 10/18] [Forms] Get date-time as UNIX timestamp Get date-time as a UNIX timestamp (in milliseconds since start of 1970) add the model level for date-time controls. WTD-530. --- platform/forms/lib/moment.min.js | 6 ++++++ .../res/templates/controls/datetime.html | 12 ++++++++++++ .../src/controllers/DateTimeController.js | 19 +++++++++++-------- 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 platform/forms/lib/moment.min.js diff --git a/platform/forms/lib/moment.min.js b/platform/forms/lib/moment.min.js new file mode 100644 index 0000000000..9c1f3ade9f --- /dev/null +++ b/platform/forms/lib/moment.min.js @@ -0,0 +1,6 @@ +//! moment.js +//! version : 2.7.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +(function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function d(a,b){function c(){mb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}var d=!0;return j(function(){return d&&(c(),d=!1),b.apply(this,arguments)},b)}function e(a,b){return function(c){return m(a.call(this,c),b)}}function f(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function g(){}function h(a){z(a),j(this,a)}function i(a){var b=s(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._bubble()}function j(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function k(a){var b,c={};for(b in a)a.hasOwnProperty(b)&&Ab.hasOwnProperty(b)&&(c[b]=a[b]);return c}function l(a){return 0>a?Math.ceil(a):Math.floor(a)}function m(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&u(a[d])!==u(b[d]))&&g++;return g+f}function r(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=bc[a]||cc[b]||b}return a}function s(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=r(c),b&&(d[b]=a[c]));return d}function t(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}mb[b]=function(e,f){var g,h,i=mb.fn._lang[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=mb().utc().set(d,a);return i.call(mb.fn._lang,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function u(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function v(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function w(a,b,c){return bb(mb([a,11,31+b-c]),b,c).week}function x(a){return y(a)?366:365}function y(a){return a%4===0&&a%100!==0||a%400===0}function z(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[tb]<0||a._a[tb]>11?tb:a._a[ub]<1||a._a[ub]>v(a._a[sb],a._a[tb])?ub:a._a[vb]<0||a._a[vb]>23?vb:a._a[wb]<0||a._a[wb]>59?wb:a._a[xb]<0||a._a[xb]>59?xb:a._a[yb]<0||a._a[yb]>999?yb:-1,a._pf._overflowDayOfYear&&(sb>b||b>ub)&&(b=ub),a._pf.overflow=b)}function A(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function B(a){return a?a.toLowerCase().replace("_","-"):a}function C(a,b){return b._isUTC?mb(a).zone(b._offset||0):mb(a).local()}function D(a,b){return b.abbr=a,zb[a]||(zb[a]=new g),zb[a].set(b),zb[a]}function E(a){delete zb[a]}function F(a){var b,c,d,e,f=0,g=function(a){if(!zb[a]&&Bb)try{require("./lang/"+a)}catch(b){}return zb[a]};if(!a)return mb.fn._lang;if(!o(a)){if(c=g(a))return c;a=[a]}for(;f0;){if(c=g(e.slice(0,b).join("-")))return c;if(d&&d.length>=b&&q(e,d,!0)>=b-1)break;b--}f++}return mb.fn._lang}function G(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function H(a){var b,c,d=a.match(Fb);for(b=0,c=d.length;c>b;b++)d[b]=hc[d[b]]?hc[d[b]]:G(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function I(a,b){return a.isValid()?(b=J(b,a.lang()),dc[b]||(dc[b]=H(b)),dc[b](a)):a.lang().invalidDate()}function J(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Gb.lastIndex=0;d>=0&&Gb.test(a);)a=a.replace(Gb,c),Gb.lastIndex=0,d-=1;return a}function K(a,b){var c,d=b._strict;switch(a){case"Q":return Rb;case"DDDD":return Tb;case"YYYY":case"GGGG":case"gggg":return d?Ub:Jb;case"Y":case"G":case"g":return Wb;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?Vb:Kb;case"S":if(d)return Rb;case"SS":if(d)return Sb;case"SSS":if(d)return Tb;case"DDD":return Ib;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Mb;case"a":case"A":return F(b._l)._meridiemParse;case"X":return Pb;case"Z":case"ZZ":return Nb;case"T":return Ob;case"SSSS":return Lb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?Sb:Hb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Hb;case"Do":return Qb;default:return c=new RegExp(T(S(a.replace("\\","")),"i"))}}function L(a){a=a||"";var b=a.match(Nb)||[],c=b[b.length-1]||[],d=(c+"").match(_b)||["-",0,0],e=+(60*d[1])+u(d[2]);return"+"===d[0]?-e:e}function M(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[tb]=3*(u(b)-1));break;case"M":case"MM":null!=b&&(e[tb]=u(b)-1);break;case"MMM":case"MMMM":d=F(c._l).monthsParse(b),null!=d?e[tb]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[ub]=u(b));break;case"Do":null!=b&&(e[ub]=u(parseInt(b,10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=u(b));break;case"YY":e[sb]=mb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[sb]=u(b);break;case"a":case"A":c._isPm=F(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[vb]=u(b);break;case"m":case"mm":e[wb]=u(b);break;case"s":case"ss":e[xb]=u(b);break;case"S":case"SS":case"SSS":case"SSSS":e[yb]=u(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=L(b);break;case"dd":case"ddd":case"dddd":d=F(c._l).weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=u(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=mb.parseTwoDigitYear(b)}}function N(a){var c,d,e,f,g,h,i,j;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[sb],bb(mb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(j=F(a._l),g=j._week.dow,h=j._week.doy,d=b(c.gg,a._a[sb],bb(mb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=cb(d,e,f,h,g),a._a[sb]=i.year,a._dayOfYear=i.dayOfYear}function O(a){var c,d,e,f,g=[];if(!a._d){for(e=Q(a),a._w&&null==a._a[ub]&&null==a._a[tb]&&N(a),a._dayOfYear&&(f=b(a._a[sb],e[sb]),a._dayOfYear>x(f)&&(a._pf._overflowDayOfYear=!0),d=Z(f,0,a._dayOfYear),a._a[tb]=d.getUTCMonth(),a._a[ub]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];a._d=(a._useUTC?Z:Y).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()+a._tzm)}}function P(a){var b;a._d||(b=s(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],O(a))}function Q(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function R(a){if(a._f===mb.ISO_8601)return void V(a);a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=F(a._l),h=""+a._i,i=h.length,j=0;for(d=J(a._f,g).match(Fb)||[],b=0;b0&&a._pf.unusedInput.push(f),h=h.slice(h.indexOf(c)+c.length),j+=c.length),hc[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),M(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=i-j,h.length>0&&a._pf.unusedInput.push(h),a._isPm&&a._a[vb]<12&&(a._a[vb]+=12),a._isPm===!1&&12===a._a[vb]&&(a._a[vb]=0),O(a),z(a)}function S(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function T(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function U(a){var b,d,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;fg)&&(e=g,d=b));j(a,d||b)}function V(a){var b,c,d=a._i,e=Xb.exec(d);if(e){for(a._pf.iso=!0,b=0,c=Zb.length;c>b;b++)if(Zb[b][1].exec(d)){a._f=Zb[b][0]+(e[6]||" ");break}for(b=0,c=$b.length;c>b;b++)if($b[b][1].exec(d)){a._f+=$b[b][0];break}d.match(Nb)&&(a._f+="Z"),R(a)}else a._isValid=!1}function W(a){V(a),a._isValid===!1&&(delete a._isValid,mb.createFromInputFallback(a))}function X(b){var c=b._i,d=Cb.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?W(b):o(c)?(b._a=c.slice(0),O(b)):p(c)?b._d=new Date(+c):"object"==typeof c?P(b):"number"==typeof c?b._d=new Date(c):mb.createFromInputFallback(b)}function Y(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function Z(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function $(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function _(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function ab(a,b,c){var d=rb(Math.abs(a)/1e3),e=rb(d/60),f=rb(e/60),g=rb(f/24),h=rb(g/365),i=d0,i[4]=c,_.apply({},i)}function bb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=mb(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function cb(a,b,c,d,e){var f,g,h=Z(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:x(a-1)+g}}function db(b){var c=b._i,d=b._f;return null===c||d===a&&""===c?mb.invalid({nullInput:!0}):("string"==typeof c&&(b._i=c=F().preparse(c)),mb.isMoment(c)?(b=k(c),b._d=new Date(+c._d)):d?o(d)?U(b):R(b):X(b),new h(b))}function eb(a,b){var c,d;if(1===b.length&&o(b[0])&&(b=b[0]),!b.length)return mb();for(c=b[0],d=1;d=0?"+":"-";return b+m(Math.abs(a),6)},gg:function(){return m(this.weekYear()%100,2)},gggg:function(){return m(this.weekYear(),4)},ggggg:function(){return m(this.weekYear(),5)},GG:function(){return m(this.isoWeekYear()%100,2)},GGGG:function(){return m(this.isoWeekYear(),4)},GGGGG:function(){return m(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return u(this.milliseconds()/100)},SS:function(){return m(u(this.milliseconds()/10),2)},SSS:function(){return m(this.milliseconds(),3)},SSSS:function(){return m(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+m(u(a/60),2)+":"+m(u(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+m(u(a/60),2)+m(u(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ic=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];fc.length;)ob=fc.pop(),hc[ob+"o"]=f(hc[ob],ob);for(;gc.length;)ob=gc.pop(),hc[ob+ob]=e(hc[ob],2);for(hc.DDDD=e(hc.DDD,3),j(g.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=mb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=mb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return bb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),mb=function(b,d,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=d,g._l=e,g._strict=f,g._isUTC=!1,g._pf=c(),db(g)},mb.suppressDeprecationWarnings=!1,mb.createFromInputFallback=d("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i)}),mb.min=function(){var a=[].slice.call(arguments,0);return eb("isBefore",a)},mb.max=function(){var a=[].slice.call(arguments,0);return eb("isAfter",a)},mb.utc=function(b,d,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=d,g._strict=f,g._pf=c(),db(g).utc()},mb.unix=function(a){return mb(1e3*a)},mb.duration=function(a,b){var c,d,e,f=a,g=null;return mb.isDuration(a)?f={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(f={},b?f[b]=a:f.milliseconds=a):(g=Db.exec(a))?(c="-"===g[1]?-1:1,f={y:0,d:u(g[ub])*c,h:u(g[vb])*c,m:u(g[wb])*c,s:u(g[xb])*c,ms:u(g[yb])*c}):(g=Eb.exec(a))&&(c="-"===g[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},f={y:e(g[2]),M:e(g[3]),d:e(g[4]),h:e(g[5]),m:e(g[6]),s:e(g[7]),w:e(g[8])}),d=new i(f),mb.isDuration(a)&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},mb.version=pb,mb.defaultFormat=Yb,mb.ISO_8601=function(){},mb.momentProperties=Ab,mb.updateOffset=function(){},mb.relativeTimeThreshold=function(b,c){return ec[b]===a?!1:(ec[b]=c,!0)},mb.lang=function(a,b){var c;return a?(b?D(B(a),b):null===b?(E(a),a="en"):zb[a]||F(a),c=mb.duration.fn._lang=mb.fn._lang=F(a),c._abbr):mb.fn._lang._abbr},mb.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),F(a)},mb.isMoment=function(a){return a instanceof h||null!=a&&a.hasOwnProperty("_isAMomentObject")},mb.isDuration=function(a){return a instanceof i},ob=ic.length-1;ob>=0;--ob)t(ic[ob]);mb.normalizeUnits=function(a){return r(a)},mb.invalid=function(a){var b=mb.utc(0/0);return null!=a?j(b._pf,a):b._pf.userInvalidated=!0,b},mb.parseZone=function(){return mb.apply(null,arguments).parseZone()},mb.parseTwoDigitYear=function(a){return u(a)+(u(a)>68?1900:2e3)},j(mb.fn=h.prototype,{clone:function(){return mb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=mb(this).utc();return 00:!1},parsingFlags:function(){return j({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=I(this,a||mb.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a&&"string"==typeof b?mb.duration(isNaN(+b)?+a:+b,isNaN(+b)?b:a):"string"==typeof a?mb.duration(+b,a):mb.duration(a,b),n(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a&&"string"==typeof b?mb.duration(isNaN(+b)?+a:+b,isNaN(+b)?b:a):"string"==typeof a?mb.duration(+b,a):mb.duration(a,b),n(this,c,-1),this},diff:function(a,b,c){var d,e,f=C(a,this),g=6e4*(this.zone()-f.zone());return b=r(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-mb(this).startOf("month")-(f-mb(f).startOf("month")))/d,e-=6e4*(this.zone()-mb(this).startOf("month").zone()-(f.zone()-mb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:l(e)},from:function(a,b){return mb.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(mb(),a)},calendar:function(a){var b=a||mb(),c=C(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){return y(this.year())},isDST:function(){return this.zone()+mb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+mb(a).startOf(b)},isSame:function(a,b){return b=b||"ms",+this.clone().startOf(b)===+C(a,this).startOf(b)},min:d("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(a){return a=mb.apply(null,arguments),this>a?this:a}),max:d("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=mb.apply(null,arguments),a>this?this:a}),zone:function(a,b){var c=this._offset||0;return null==a?this._isUTC?c:this._d.getTimezoneOffset():("string"==typeof a&&(a=L(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,c!==a&&(!b||this._changeInProgress?n(this,mb.duration(c-a,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,mb.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?mb(a).zone():0,(this.zone()-a)%60===0},daysInMonth:function(){return v(this.year(),this.month())},dayOfYear:function(a){var b=rb((mb(this).startOf("day")-mb(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=bb(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=bb(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=bb(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this.day()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return w(this.year(),1,4)},weeksInYear:function(){var a=this._lang._week;return w(this.year(),a.dow,a.doy)},get:function(a){return a=r(a),this[a]()},set:function(a,b){return a=r(a),"function"==typeof this[a]&&this[a](b),this},lang:function(b){return b===a?this._lang:(this._lang=F(b),this)}}),mb.fn.millisecond=mb.fn.milliseconds=ib("Milliseconds",!1),mb.fn.second=mb.fn.seconds=ib("Seconds",!1),mb.fn.minute=mb.fn.minutes=ib("Minutes",!1),mb.fn.hour=mb.fn.hours=ib("Hours",!0),mb.fn.date=ib("Date",!0),mb.fn.dates=d("dates accessor is deprecated. Use date instead.",ib("Date",!0)),mb.fn.year=ib("FullYear",!0),mb.fn.years=d("years accessor is deprecated. Use year instead.",ib("FullYear",!0)),mb.fn.days=mb.fn.day,mb.fn.months=mb.fn.month,mb.fn.weeks=mb.fn.week,mb.fn.isoWeeks=mb.fn.isoWeek,mb.fn.quarters=mb.fn.quarter,mb.fn.toJSON=mb.fn.toISOString,j(mb.duration.fn=i.prototype,{_bubble:function(){var a,b,c,d,e=this._milliseconds,f=this._days,g=this._months,h=this._data;h.milliseconds=e%1e3,a=l(e/1e3),h.seconds=a%60,b=l(a/60),h.minutes=b%60,c=l(b/60),h.hours=c%24,f+=l(c/24),h.days=f%30,g+=l(f/30),h.months=g%12,d=l(g/12),h.years=d},weeks:function(){return l(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12)},humanize:function(a){var b=+this,c=ab(b,!a,this.lang());return a&&(c=this.lang().pastFuture(b,c)),this.lang().postformat(c)},add:function(a,b){var c=mb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=mb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=r(a),this[a.toLowerCase()+"s"]()},as:function(a){return a=r(a),this["as"+a.charAt(0).toUpperCase()+a.slice(1)+"s"]()},lang:mb.fn.lang,toIsoString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"}});for(ob in ac)ac.hasOwnProperty(ob)&&(kb(ob,ac[ob]),jb(ob.toLowerCase()));kb("Weeks",6048e5),mb.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},mb.lang("en",{ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),Bb?module.exports=mb:"function"==typeof define&&define.amd?(define("moment",function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(qb.moment=nb),mb}),lb(!0)):lb()}).call(this); \ No newline at end of file diff --git a/platform/forms/res/templates/controls/datetime.html b/platform/forms/res/templates/controls/datetime.html index 92efad64aa..8d33821e88 100644 --- a/platform/forms/res/templates/controls/datetime.html +++ b/platform/forms/res/templates/controls/datetime.html @@ -23,6 +23,10 @@ @@ -30,6 +34,10 @@ @@ -37,6 +45,10 @@ diff --git a/platform/forms/src/controllers/DateTimeController.js b/platform/forms/src/controllers/DateTimeController.js index efd954da44..2b29a13c85 100644 --- a/platform/forms/src/controllers/DateTimeController.js +++ b/platform/forms/src/controllers/DateTimeController.js @@ -1,23 +1,26 @@ /*global define*/ define( - [], + ["../../lib/moment.min"], function () { + var DATE_FORMAT = "YYYY-DDD"; + function DateTimeController($scope) { function update() { var date = $scope.datetime.date, hour = $scope.datetime.hour, min = $scope.datetime.min, - sec = $scope.datetime.sec; + sec = $scope.datetime.sec, + fullDateTime = moment.utc(date, DATE_FORMAT) + .hour(hour || 0) + .minute(min || 0) + .second(sec || 0); - $scope.ngModel[$scope.field] = [ - date, - hour, - min, - sec - ].join("."); + if (fullDateTime.isValid()) { + $scope.ngModel[$scope.field] = fullDateTime.valueOf(); + } } $scope.$watch("datetime.date", update); From 354327accc581f1515e41efb42cb9c8c6ed9ea88 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 14:08:33 -0800 Subject: [PATCH 11/18] [Forms] Support patterns Support patterns for text fields, using the ng-pattern directive. WTD-530. --- .../res/templates/controls/textfield.html | 1 + platform/forms/res/templates/form.html | 23 ++++++------ platform/forms/src/MCTForm.js | 35 +++++++++++++++++-- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/platform/forms/res/templates/controls/textfield.html b/platform/forms/res/templates/controls/textfield.html index f4f8096ba5..57e61d3613 100644 --- a/platform/forms/res/templates/controls/textfield.html +++ b/platform/forms/res/templates/controls/textfield.html @@ -3,6 +3,7 @@ diff --git a/platform/forms/res/templates/form.html b/platform/forms/res/templates/form.html index 0195e0b22b..5e5bbaef73 100644 --- a/platform/forms/res/templates/form.html +++ b/platform/forms/res/templates/form.html @@ -23,23 +23,26 @@
- - - + +
- + + structure="row" + field="item.key"> + {{item.name}}
diff --git a/platform/forms/src/MCTForm.js b/platform/forms/src/MCTForm.js index 00f7f7065f..6cdcb4deda 100644 --- a/platform/forms/src/MCTForm.js +++ b/platform/forms/src/MCTForm.js @@ -8,6 +8,8 @@ define( function () { "use strict"; + var MATCH_ALL = /^.*$/; + /** * * @constructor @@ -20,18 +22,47 @@ define( ].join("/"); function controller($scope) { + var regexps = [], + matchAll = /.*/; + + // ng-pattern seems to want a RegExp, and not a + // string (despite what documentation says) but + // we want form structure to be JSON-expressible, + // so we make RegExp's from strings as-needed + function getRegExp(pattern) { + // If undefined, don't apply a pattern + if (!pattern) { + return MATCH_ALL; + } + + // Just echo if it's already a regexp + if (pattern instanceof RegExp) { + return pattern; + } + + // Otherwise, assume a string + // Cache for easy lookup later (so we don't + // creat a new RegExp every digest cycle) + if (!regexps[pattern]) { + regexps[pattern] = new RegExp(pattern); + } + + return regexps[pattern]; + } + $scope.$watch("mctForm", function (mctForm) { - console.log(JSON.stringify(mctForm)); if ($scope.name) { $scope.$parent[$scope.name] = mctForm; } }); + + $scope.getRegExp = getRegExp; } return { restrict: "E", templateUrl: templatePath, - link: controller, + controller: controller, scope: { structure: "=", ngModel: "=", From 21f809645d9d6901106531fcd182e027fa7989a2 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 14:12:20 -0800 Subject: [PATCH 12/18] [Forms] Add Forms example Add example showing the use of generated forms, WTD-530. --- example/forms/bundle.json | 18 +++++ example/forms/res/templates/exampleForm.html | 19 +++++ example/forms/src/ExampleFormController.js | 79 ++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 example/forms/bundle.json create mode 100644 example/forms/res/templates/exampleForm.html create mode 100644 example/forms/src/ExampleFormController.js diff --git a/example/forms/bundle.json b/example/forms/bundle.json new file mode 100644 index 0000000000..9226e3780d --- /dev/null +++ b/example/forms/bundle.json @@ -0,0 +1,18 @@ +{ + "name": "Declarative Forms example", + "sources": "src", + "extensions": { + "controllers": [ + { + "key": "ExampleFormController", + "implementation": "ExampleFormController.js", + "depends": [ "$scope" ] + } + ], + "routes": [ + { + "templateUrl": "templates/exampleForm.html" + } + ] + } +} \ No newline at end of file diff --git a/example/forms/res/templates/exampleForm.html b/example/forms/res/templates/exampleForm.html new file mode 100644 index 0000000000..70c65f3ab4 --- /dev/null +++ b/example/forms/res/templates/exampleForm.html @@ -0,0 +1,19 @@ +
+ + + + +
    +
  • Dirty: {{aForm.$dirty}}
  • +
  • Valid: {{aForm.$valid}}
  • +
+ +
+
+
+    
+
+    
+
\ No newline at end of file diff --git a/example/forms/src/ExampleFormController.js b/example/forms/src/ExampleFormController.js new file mode 100644 index 0000000000..fd71122085 --- /dev/null +++ b/example/forms/src/ExampleFormController.js @@ -0,0 +1,79 @@ +/*global define*/ + +define( + [], + function () { + "use strict"; + + function ExampleFormController($scope) { + $scope.state = { + + }; + + $scope.form = { + name: "An example form.", + sections: [ + { + name: "First section", + rows: [ + { + name: "Check me", + control: "checkbox", + key: "checkMe" + }, + { + name: "Enter your name", + required: true, + control: "textfield", + key: "yourName" + }, + { + name: "Enter a number", + control: "textfield", + pattern: "^\\d+$", + key: "aNumber" + } + ] + }, + { + name: "Second section", + rows: [ + { + name: "Pick a date", + required: true, + description: "Enter date in form YYYY-DDD", + control: "datetime", + key: "aDate" + }, + { + name: "Choose something", + control: "select", + options: [ + { name: "Hats", value: "hats" }, + { name: "Bats", value: "bats" }, + { name: "Cats", value: "cats" }, + { name: "Mats", value: "mats" } + ], + key: "aChoice" + }, + { + name: "Choose something", + control: "select", + required: true, + options: [ + { name: "Hats", value: "hats" }, + { name: "Bats", value: "bats" }, + { name: "Cats", value: "cats" }, + { name: "Mats", value: "mats" } + ], + key: "aRequiredChoice" + } + ] + } + ] + }; + } + + return ExampleFormController; + } +); \ No newline at end of file From 7f59175313f4ae1154b4227642b565ea81aa1514 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 14:18:36 -0800 Subject: [PATCH 13/18] [Forms] Remove unused templates Remove unused templates (these have been reorganized into the controls directory.) WTD-530 --- platform/forms/res/templates/_checkbox.html | 6 ----- platform/forms/res/templates/_checkboxes.html | 0 platform/forms/res/templates/_datetime.html | 24 ------------------- platform/forms/res/templates/_select.html | 7 ------ platform/forms/res/templates/_selects.html | 5 ---- platform/forms/res/templates/_textfield.html | 8 ------- platform/forms/res/templates/textfields.html | 7 ------ 7 files changed, 57 deletions(-) delete mode 100644 platform/forms/res/templates/_checkbox.html delete mode 100644 platform/forms/res/templates/_checkboxes.html delete mode 100644 platform/forms/res/templates/_datetime.html delete mode 100644 platform/forms/res/templates/_select.html delete mode 100644 platform/forms/res/templates/_selects.html delete mode 100644 platform/forms/res/templates/_textfield.html delete mode 100644 platform/forms/res/templates/textfields.html diff --git a/platform/forms/res/templates/_checkbox.html b/platform/forms/res/templates/_checkbox.html deleted file mode 100644 index 1412c4ee14..0000000000 --- a/platform/forms/res/templates/_checkbox.html +++ /dev/null @@ -1,6 +0,0 @@ -
- -
\ No newline at end of file diff --git a/platform/forms/res/templates/_checkboxes.html b/platform/forms/res/templates/_checkboxes.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/platform/forms/res/templates/_datetime.html b/platform/forms/res/templates/_datetime.html deleted file mode 100644 index 55e0f7bdcb..0000000000 --- a/platform/forms/res/templates/_datetime.html +++ /dev/null @@ -1,24 +0,0 @@ -
-
-
- Date - Hour - Min - Sec - Timezone -
- -
- - - - - - - -
-
-
\ No newline at end of file diff --git a/platform/forms/res/templates/_select.html b/platform/forms/res/templates/_select.html deleted file mode 100644 index e778d76450..0000000000 --- a/platform/forms/res/templates/_select.html +++ /dev/null @@ -1,7 +0,0 @@ -
- -
diff --git a/platform/forms/res/templates/_selects.html b/platform/forms/res/templates/_selects.html deleted file mode 100644 index 449051ed56..0000000000 --- a/platform/forms/res/templates/_selects.html +++ /dev/null @@ -1,5 +0,0 @@ -
- {#processedValues} - {#view key="_select"/} - {/processedValues} -
\ No newline at end of file diff --git a/platform/forms/res/templates/_textfield.html b/platform/forms/res/templates/_textfield.html deleted file mode 100644 index f351520231..0000000000 --- a/platform/forms/res/templates/_textfield.html +++ /dev/null @@ -1,8 +0,0 @@ -
- - - - - -
- diff --git a/platform/forms/res/templates/textfields.html b/platform/forms/res/templates/textfields.html deleted file mode 100644 index 39a01c489e..0000000000 --- a/platform/forms/res/templates/textfields.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
- {#processedValues} - {#view key="_textfield"/} {label} - {/processedValues} -
-
\ No newline at end of file From a04b30a3839369002734cad54a06d7067d92e2cd Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 14:36:00 -0800 Subject: [PATCH 14/18] [Forms] Add clarifying comments Add in-line documentation to directives and controller used for the Forms component. WTD-530. --- platform/forms/src/MCTControl.js | 8 +++++ platform/forms/src/MCTForm.js | 33 ++++++++++++++++++- .../src/controllers/DateTimeController.js | 10 ++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/platform/forms/src/MCTControl.js b/platform/forms/src/MCTControl.js index b630b72182..2197ac4424 100644 --- a/platform/forms/src/MCTControl.js +++ b/platform/forms/src/MCTControl.js @@ -5,6 +5,14 @@ define( function () { "use strict"; + /** + * The mct-control will dynamically include the control + * for a form element based on a symbolic key. Individual + * controls are defined under the extension category + * `controls`; this allows plug-ins to introduce new form + * control types while still making use of the form + * generator to ensure an overall consistent form style. + */ function MCTControl(controls) { var controlMap = {}; diff --git a/platform/forms/src/MCTForm.js b/platform/forms/src/MCTForm.js index 6cdcb4deda..986c4d2ddc 100644 --- a/platform/forms/src/MCTForm.js +++ b/platform/forms/src/MCTForm.js @@ -11,6 +11,21 @@ define( var MATCH_ALL = /^.*$/; /** + * The mct-form directive allows generation of displayable + * forms based on a declarative description of the form's + * structure. + * + * This directive accepts three attributes: + * + * * `ng-model`: The model for the form; where user input + * where be stored. + * * `structure`: The declarative structure of the form. + * Describes what controls should be shown and where + * their values should be read/written in the model. + * * `name`: The name under which to expose the form's + * dirty/valid state. This is similar to ng-form's use + * of name, except this will be made available in the + * parent scope. * * @constructor */ @@ -50,6 +65,8 @@ define( return regexps[pattern]; } + // Publish the form state under the requested + // name in the parent scope $scope.$watch("mctForm", function (mctForm) { if ($scope.name) { $scope.$parent[$scope.name] = mctForm; @@ -60,12 +77,26 @@ define( } return { + // Only show at the element level restrict: "E", + + // Load the forms template templateUrl: templatePath, + + // Use the controller defined above to + // populate/respond to changes in scope controller: controller, + + // Initial an isolate scope scope: { - structure: "=", + + // The model: Where form input will actually go ngModel: "=", + + // Form structure; what sections/rows to show + structure: "=", + + // Name under which to publish the form name: "@" } }; diff --git a/platform/forms/src/controllers/DateTimeController.js b/platform/forms/src/controllers/DateTimeController.js index 2b29a13c85..d5a08859f1 100644 --- a/platform/forms/src/controllers/DateTimeController.js +++ b/platform/forms/src/controllers/DateTimeController.js @@ -6,8 +6,17 @@ define( var DATE_FORMAT = "YYYY-DDD"; + /** + * Controller for the `datetime` form control. + * This is a composite control; it includes multiple + * input fields but outputs a single timestamp (in + * milliseconds since start of 1970) to the ngModel. + * + * @constructor + */ function DateTimeController($scope) { + // Update the function update() { var date = $scope.datetime.date, hour = $scope.datetime.hour, @@ -23,6 +32,7 @@ define( } } + // Update value whenever any field changes. $scope.$watch("datetime.date", update); $scope.$watch("datetime.hour", update); $scope.$watch("datetime.min", update); From b60a5ae7c1b8375a783d984060750400db0a37f0 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 14:39:50 -0800 Subject: [PATCH 15/18] [Forms] Remove example forms bundle Remove example forms bundle from the list of active bundles, so that its route does not interfere with the routes provided by Browse and Edit mode in the running application. WTD-530. --- bundles.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles.json b/bundles.json index 74eca7c166..05f5dd4fb3 100644 --- a/bundles.json +++ b/bundles.json @@ -2,11 +2,11 @@ "platform/framework", "platform/core", "platform/representation", + "platform/commonUI/browse", + "platform/commonUI/edit", "platform/commonUI/dialog", "platform/commonUI/general", "platform/forms", - "example/forms", - "example/persistence" ] \ No newline at end of file From 29c5a7aabaa16893ba0a7deaa61754820110400d Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 14:40:35 -0800 Subject: [PATCH 16/18] [Forms] Add skeleton specs Add skeleton specs for scripts from the Forms component. WTD-530. --- platform/forms/test/MCTControlSpec.js | 12 ++++++++++++ platform/forms/test/MCTFormSpec.js | 12 ++++++++++++ .../forms/test/controllers/DateTimeControllerSpec.js | 12 ++++++++++++ platform/forms/test/suite.json | 5 +++++ 4 files changed, 41 insertions(+) create mode 100644 platform/forms/test/MCTControlSpec.js create mode 100644 platform/forms/test/MCTFormSpec.js create mode 100644 platform/forms/test/controllers/DateTimeControllerSpec.js create mode 100644 platform/forms/test/suite.json diff --git a/platform/forms/test/MCTControlSpec.js b/platform/forms/test/MCTControlSpec.js new file mode 100644 index 0000000000..39813afed6 --- /dev/null +++ b/platform/forms/test/MCTControlSpec.js @@ -0,0 +1,12 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/MCTControl"], + function (MCTControl) { + "use strict"; + + describe("The mct-control directive", function () { + + }); + } +); \ No newline at end of file diff --git a/platform/forms/test/MCTFormSpec.js b/platform/forms/test/MCTFormSpec.js new file mode 100644 index 0000000000..2d64065c29 --- /dev/null +++ b/platform/forms/test/MCTFormSpec.js @@ -0,0 +1,12 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../src/MCTForm"], + function (MCTForm) { + "use strict"; + + describe("The mct-form directive", function () { + + }); + } +); \ No newline at end of file diff --git a/platform/forms/test/controllers/DateTimeControllerSpec.js b/platform/forms/test/controllers/DateTimeControllerSpec.js new file mode 100644 index 0000000000..96039710d0 --- /dev/null +++ b/platform/forms/test/controllers/DateTimeControllerSpec.js @@ -0,0 +1,12 @@ +/*global define,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../../src/controllers/DateTimeController"], + function (DateTimeController) { + "use strict"; + + describe("The date-time directive", function () { + + }); + } +); \ No newline at end of file diff --git a/platform/forms/test/suite.json b/platform/forms/test/suite.json new file mode 100644 index 0000000000..b86d7b9178 --- /dev/null +++ b/platform/forms/test/suite.json @@ -0,0 +1,5 @@ +[ + "MCTControl", + "MCTForm", + "controllers/DateTimeController" +] \ No newline at end of file From 1bfc21270b80c6cd70d44b9c1af643003c4846db Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 15:33:40 -0800 Subject: [PATCH 17/18] [Forms] Fill in specs Fill in specs for scripts which support the mct-form and mct-control directives. WTD-530. --- platform/forms/src/MCTForm.js | 8 +- .../src/controllers/DateTimeController.js | 3 +- platform/forms/test/MCTControlSpec.js | 48 ++++++++++++ platform/forms/test/MCTFormSpec.js | 78 +++++++++++++++++++ .../controllers/DateTimeControllerSpec.js | 29 +++++++ 5 files changed, 161 insertions(+), 5 deletions(-) diff --git a/platform/forms/src/MCTForm.js b/platform/forms/src/MCTForm.js index 986c4d2ddc..8414938171 100644 --- a/platform/forms/src/MCTForm.js +++ b/platform/forms/src/MCTForm.js @@ -8,7 +8,8 @@ define( function () { "use strict"; - var MATCH_ALL = /^.*$/; + // Default ng-pattern; any non whitespace + var NON_WHITESPACE = /\S/; /** * The mct-form directive allows generation of displayable @@ -37,8 +38,7 @@ define( ].join("/"); function controller($scope) { - var regexps = [], - matchAll = /.*/; + var regexps = []; // ng-pattern seems to want a RegExp, and not a // string (despite what documentation says) but @@ -47,7 +47,7 @@ define( function getRegExp(pattern) { // If undefined, don't apply a pattern if (!pattern) { - return MATCH_ALL; + return NON_WHITESPACE; } // Just echo if it's already a regexp diff --git a/platform/forms/src/controllers/DateTimeController.js b/platform/forms/src/controllers/DateTimeController.js index d5a08859f1..c1387318d8 100644 --- a/platform/forms/src/controllers/DateTimeController.js +++ b/platform/forms/src/controllers/DateTimeController.js @@ -1,8 +1,9 @@ -/*global define*/ +/*global define,moment*/ define( ["../../lib/moment.min"], function () { + "use strict"; var DATE_FORMAT = "YYYY-DDD"; diff --git a/platform/forms/test/MCTControlSpec.js b/platform/forms/test/MCTControlSpec.js index 39813afed6..448892758a 100644 --- a/platform/forms/test/MCTControlSpec.js +++ b/platform/forms/test/MCTControlSpec.js @@ -6,6 +6,54 @@ define( "use strict"; describe("The mct-control directive", function () { + var testControls, + mockScope, + mctControl; + + beforeEach(function () { + testControls = [ + { + key: "abc", + bundle: { path: "a", resources: "b" }, + templateUrl: "c/template.html" + }, + { + key: "xyz", + bundle: { path: "x", resources: "y" }, + templateUrl: "z/template.html" + } + ]; + + mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]); + + mctControl = new MCTControl(testControls); + }); + + it("is restricted to the element level", function () { + expect(mctControl.restrict).toEqual("E"); + }); + + it("watches its passed key to choose a template", function () { + mctControl.controller(mockScope); + + expect(mockScope.$watch).toHaveBeenCalledWith( + "key", + jasmine.any(Function) + ); + }); + + it("changes its template dynamically", function () { + mctControl.controller(mockScope); + + mockScope.key = "xyz"; + mockScope.$watch.mostRecentCall.args[1]("xyz"); + + // Should have communicated the template path to + // ng-include via the "inclusion" field in scope + expect(mockScope.inclusion).toEqual( + "x/y/z/template.html" + ); + }); }); } diff --git a/platform/forms/test/MCTFormSpec.js b/platform/forms/test/MCTFormSpec.js index 2d64065c29..df30dbf4cc 100644 --- a/platform/forms/test/MCTFormSpec.js +++ b/platform/forms/test/MCTFormSpec.js @@ -6,6 +6,84 @@ define( "use strict"; describe("The mct-form directive", function () { + var mockScope, + mctForm; + + beforeEach(function () { + mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]); + mockScope.$parent = {}; + mctForm = new MCTForm(); + }); + + it("is restricted to elements", function () { + expect(mctForm.restrict).toEqual("E"); + }); + + it("watches for changes in form by name", function () { + // mct-form needs to watch for the form by name + // in order to convey changes in $valid, $dirty, etc + // up to the parent scope. + mctForm.controller(mockScope); + + expect(mockScope.$watch).toHaveBeenCalledWith( + "mctForm", + jasmine.any(Function) + ); + }); + + it("conveys form status to parent scope", function () { + var someState = { someKey: "some value" }; + mockScope.name = "someName"; + + mctForm.controller(mockScope); + + mockScope.$watch.mostRecentCall.args[1](someState); + + expect(mockScope.$parent.someName).toBe(someState); + }); + + it("allows strings to be converted to RegExps", function () { + // This is needed to support ng-pattern in the template + mctForm.controller(mockScope); + + // Should have added getRegExp to the scope, + // to convert strings to regular expressions + expect(mockScope.getRegExp("^\\d+$")).toEqual(/^\d+$/); + }); + + it("returns the same regexp instance for the same string", function () { + // Don't want new instances each digest cycle, for performance + var strRegExp = "^[a-z]\\d+$", + regExp; + + // Add getRegExp to scope + mctForm.controller(mockScope); + regExp = mockScope.getRegExp(strRegExp); + + // Same object instance each time... + expect(mockScope.getRegExp(strRegExp)).toBe(regExp); + expect(mockScope.getRegExp(strRegExp)).toBe(regExp); + }); + + it("passes RegExp objects through untouched", function () { + // Permit using forms to simply provide their own RegExp object + var regExp = /^\d+[a-d]$/; + + // Add getRegExp to scope + mctForm.controller(mockScope); + + // Should have added getRegExp to the scope, + // to convert strings to regular expressions + expect(mockScope.getRegExp(regExp)).toBe(regExp); + }); + + it("passes a non-whitespace regexp when no pattern is defined", function () { + // If no pattern is supplied, ng-pattern should match anything + mctForm.controller(mockScope); + expect(mockScope.getRegExp()).toEqual(/\S/); + expect(mockScope.getRegExp(undefined)).toEqual(/\S/); + }); + }); } diff --git a/platform/forms/test/controllers/DateTimeControllerSpec.js b/platform/forms/test/controllers/DateTimeControllerSpec.js index 96039710d0..3b47068f15 100644 --- a/platform/forms/test/controllers/DateTimeControllerSpec.js +++ b/platform/forms/test/controllers/DateTimeControllerSpec.js @@ -6,6 +6,35 @@ define( "use strict"; describe("The date-time directive", function () { + var mockScope, + controller; + + beforeEach(function () { + mockScope = jasmine.createSpyObj("$scope", [ "$watch" ]); + controller = new DateTimeController(mockScope); + }); + + it("watches for changes in fields", function () { + ["date", "hour", "min", "sec"].forEach(function (fieldName) { + expect(mockScope.$watch).toHaveBeenCalledWith( + "datetime." + fieldName, + jasmine.any(Function) + ); + }); + }); + + it("converts date-time input into a timestamp", function () { + mockScope.ngModel = {}; + mockScope.field = "test"; + mockScope.datetime.date = "2014-332"; + mockScope.datetime.hour = 22; + mockScope.datetime.min = 55; + mockScope.datetime.sec = 13; + + mockScope.$watch.mostRecentCall.args[1](); + + expect(mockScope.ngModel.test).toEqual(1417215313000); + }); }); } From 3a7f35487d0a1740bb1ea6306292fc64f086e519 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Fri, 28 Nov 2014 15:58:19 -0800 Subject: [PATCH 18/18] [Forms] Add usage documentation Add documentation for using the mct-form directive introduced for WTD-530. --- platform/forms/README.md | 90 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/platform/forms/README.md b/platform/forms/README.md index cc81b4ca86..d435b023fd 100644 --- a/platform/forms/README.md +++ b/platform/forms/README.md @@ -1,3 +1,87 @@ -This bundle contains a general implementation of forms in Open MCT Web. -This allows forms to be expressed using a reasonably concise declarative -syntax, and rendered as Angular templates in a consistent fashion. +# Overview + +This bundle contains a general implementation of forms in Open MCT Web. +This allows forms to be expressed using a reasonably concise declarative +syntax, and rendered as Angular templates in a consistent fashion. + +# Usage + +To include a form with a declarative definition, use the `mct-form` +directive, e.g.: + + + + +The attributes utilized by this form are as follows: + +* `ng-model`: The object which should contain the full form input. Individual + fields in this model are bound to individual controls; the names used for + these fields are provided in the form structure (see below). +* `structure`: The structure of the form; e.g. sections, rows, their names, + and so forth. The value of this attribute should be an Angular expression. +* `name`: The name in the containing scope under which to publish form + "meta-state", e.g. `$valid`, `$dirty`, etc. This is as the behavior of + `ng-form`. Passed as plain text in the attribute. + +## Form structure + +A form's structure is described as a JavaScript object in the following form: + + { + "name": ... title to display for the form, as a string ..., + "sections": [ + { + "name": ... title to display for the section ..., + "rows": [ + { + "name": ... title to display for this row ..., + "control": ... symbolic key for the control ..., + "key": ... field name in ng-model ... + "pattern": ... optional, reg exp to match against ... + "required": ... optional boolean ... + "options": [ + "name": ... name to display (e.g. in a select) ..., + "value": ... value to store in the model ... + ] + }, + ... and other rows ... + ] + }, + ... and other sections ... + ] + } + +Note that `pattern` may be specified as a string, to simplify storing +for structures as JSON when necessary. The string should be given in +a form appropriate to pass to a `RegExp` constructor. + +## Adding controls + +Four standard control types are included in the forms bundle: + +* `textfield`: An area to enter plain text. +* `select`: A drop-down list of options. +* `checkbox`: A box which may be checked/unchecked. +* `datetime`: An input for UTC date/time entry; gives result as a + UNIX timestamp, in milliseconds since start of 1970, UTC. + +New controls may be added as extensions of the `controls` category. +Extensions of this category have two properites: + +* `key`: The symbolic name for this control (matched against the + `control` field in rows of the form structure). +* `templateUrl`: The URL to the control's Angular template, relative + to the resources directory of the bundle which exposes the extension. + +Within the template for a control, the following variables will be +included in scope: + +* `ngModel`: The model where form input will be stored. Notably we + also need to look at `field` (see below) to determine which field + in the model should be modified. +* `ngRequired`: True if input is required. +* `ngPattern`: The pattern to match against (for text entry.) +* `options`: The options for this control, as passed from the + `options` property of an individual row. +* `field`: Name of the field in `ngModel` which will hold the value + for this control. \ No newline at end of file