From 9d7d5dfba23a247ec234cfb8597d8a696e3d72d5 Mon Sep 17 00:00:00 2001 From: Arlen Johnson Date: Tue, 13 Sep 2022 19:28:37 -0400 Subject: [PATCH] Honor date-only dates using the date picker (CFM-107) (#49) * Align datepicker colors with Registry theme (CFM-107) * Honor date-only dates using the date picker (CFM-107) * Allow date fields to be specified as type 'date', 'dateTime', and 'endDateTime'. Ensure that the default times for endDateTime fields are set to '23:59:59'. (CFM-107) * Add the 'format' attribute to date/time fields to allow browsers to do simple client-side validation. Titles also included for accessibility. (CFM-107) * Extend the min and max dates to provide a wide range of years in the datepicker widget. (CFM-107) * Add aria attributes and screenreader features to time picker. (CFM-107) * Refactor FieldHelper to include a dateControl() wrapper function. Set the default year ranges for the datepicker. (CFM-107) * Minor update to comments. (CFM-107) * Clean up code conventions. (CFM-107) --- app/resources/locales/en_US/field.po | 9 + app/src/Lib/Enum/DateTypeEnum.php | 37 ++++ app/src/View/Helper/FieldHelper.php | 187 ++++++++++++------ app/templates/ApiUsers/fields.inc | 4 +- app/templates/ExternalIdentities/fields.inc | 2 +- .../ExternalIdentityRoles/fields.inc | 8 +- app/templates/GroupMembers/fields.inc | 5 +- app/templates/People/fields.inc | 2 +- app/templates/PersonRoles/fields.inc | 8 +- app/templates/element/datePicker.php | 22 ++- app/webroot/css/co-color.css | 6 +- .../datepicker/cm-datetimepicker.js | 56 ++++-- .../components/datepicker/cm-timepicker.js | 15 +- 13 files changed, 248 insertions(+), 113 deletions(-) create mode 100644 app/src/Lib/Enum/DateTypeEnum.php diff --git a/app/resources/locales/en_US/field.po b/app/resources/locales/en_US/field.po index 61c272bb5..28c608e54 100644 --- a/app/resources/locales/en_US/field.po +++ b/app/resources/locales/en_US/field.po @@ -98,6 +98,15 @@ msgstr "Created" msgid "datepicker.am" msgstr "AM" +msgid "datepicker.chooseTime" +msgstr "Choose time" + +msgid "datepicker.enterDate" +msgstr "Enter a date as YYYY-MM-DD" + +msgid "datepicker.enterDateTime" +msgstr "Enter a date as YYYY-MM-DD HH:MM:SS" + msgid "datepicker.hour" msgstr "Hour" diff --git a/app/src/Lib/Enum/DateTypeEnum.php b/app/src/Lib/Enum/DateTypeEnum.php new file mode 100644 index 000000000..1b2c6e1c7 --- /dev/null +++ b/app/src/Lib/Enum/DateTypeEnum.php @@ -0,0 +1,37 @@ +' . $this->Alert->alert($info, 'warning') . ''; @@ -66,25 +68,29 @@ public function banner(string $info) { * Emit a form control. * * @since COmanage Registry v5.0.0 - * @param string $fieldName Form field - * @param array $options FormHelper control options - * @param string $labelText Label text (fieldName language key used by default) - * @param array $config Custom FormHelper configuration options + * @param string $fieldName Form field + * @param array $options FormHelper control options + * @param string $labelText Label text (fieldName language key used by default) + * @param array $config Custom FormHelper configuration options + * @param string $ctrlCode Control code passed in from wrapper functions + * @param string $cssClass Start li css class passed in from wrapper functions * @return string HTML for control */ public function control(string $fieldName, array $options=[], string $labelText=null, - array $config=[]){ + array $config=[], + string $ctrlCode=null, + string $cssClass=''): string { $coptions = $options; $coptions['label'] = false; $coptions['readonly'] = !$this->editable || (isset($options['readonly']) && $options['readonly']); // Selects, Checkboxes, and Radio Buttons use "disabled" $coptions['disabled'] = $coptions['readonly']; - // Generate HTML for the control itself - $liClass = ""; + // Specify a class on the
  • form control wrapper + $liClass = $cssClass; // Remove prefix from field value if(isset($config['prefix'], $this->getView()->get('vv_obj')->$fieldName)) { @@ -94,50 +100,20 @@ public function control(string $fieldName, $vv_obj->$fieldName = $fieldValueTemp; $this->getView()->set('vv_obj', $vv_obj); } - - // Handle datetime controls specially - if($fieldName == 'valid_from' || $fieldName == 'valid_through') { - // Append the timezone to the label - $label = __d('field', $fieldName.".tz", [$this->_View->get('vv_tz')]); - - // A datetime field will be rendered as plain text input with adjacent date and time pickers - // that will interact with the field value. Allowing direct access to the input field is for - // accessibility purposes. - $coptions['class'] = 'form-control datepicker'; - $coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS'; // TODO: test for date-only inputs and send only the date - $coptions['id'] = str_replace("_", "-", $fieldName); - - $entity = $this->getView()->get('vv_obj'); - - $pickerDate = ''; - if(!empty($entity->$fieldName)) { - // Adjust the time back to the user's timezone - $coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->getView()->get('vv_tz')); - $pickerDate = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz')); - } - - $date_args = [ - 'fieldName' => $fieldName, - 'pickerDate' => $pickerDate - ]; - // Create a text field to hold our value. - $controlCode = $this->Form->text($fieldName, $coptions) - . $this->getView()->element('datePicker', $date_args); - - $liClass = "fields-datepicker"; - } else { - if($fieldName != 'status' - && !isset($options['empty']) - && (!isset($options['suppressBlank']) || !$options['suppressBlank'])) { - // Cause any select (except status) to render with a blank option, even - // if the field is required. This makes it clear when a value need to be set. - // Note this will be ignored for non-select controls. - $coptions['empty'] = true; - } - - $controlCode = $this->Form->control($fieldName, $coptions); + + if($fieldName != 'status' + && !isset($options['empty']) + && (!isset($options['suppressBlank']) || !$options['suppressBlank'])) { + // Cause any select (except status) to render with a blank option, even + // if the field is required. This makes it clear when a value need to be set. + // Note this will be ignored for non-select controls. + $coptions['empty'] = true; } + // Generate the form control or pass along the markup generated in a wrapper function + $controlCode = empty($ctrlCode) ? $this->Form->control($fieldName, $coptions) : $ctrlCode; + + // Required fields are usually determined by the model validator, but for // related models the view (currently) has to pass the field as required in // $options. For fields of the form model.0.field, if $options['required'] @@ -159,6 +135,90 @@ public function control(string $fieldName, . $this->endLine(); } + /** + * Emit a date/time form control. + * This is a wrapper function for $this->control() + * + * @since COmanage Registry v5.0.0 + * @param string $fieldName Form field + * @param string $dateType Standard, DateOnly, FromTime, ThroughTime + * + * @return string HTML for control + */ + + public function dateControl(string $fieldName, string $dateType=DateTypeEnum::Standard): string { + // A datetime field will be rendered as a plain text input with adjacent date and time pickers + // that will interact with the field value. Allowing direct access to the input field is for + // accessibility purposes. + + $pickerType = $dateType; + // Special-case the very common "valid_from" and "valid_through" fields so we won't need + // to specify their types in fields.inc. + if($fieldName == 'valid_from') { + $pickerType = DateTypeEnum::FromTime; + } + if($fieldName == 'valid_through') { + $pickerType = DateTypeEnum::ThroughTime; + } + + // Append the timezone to the label -- TODO: see that the timezone gets output to the display + $label = __d('field', $fieldName.".tz", [$this->_View->get('vv_tz')]); + + // Create the options array for the (text input) form control + $coptions = []; + $coptions['class'] = 'form-control datepicker'; + + if($pickerType == DateTypeEnum::DateOnly) { + $coptions['placeholder'] = 'YYYY-MM-DD'; + $coptions['pattern'] = '\d{4}-\d{2}-\d{2}'; + $coptions['title'] = __d('field', 'datepicker.enterDate'); + } else { + $coptions['placeholder'] = 'YYYY-MM-DD HH:MM:SS'; + $coptions['pattern'] = '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'; + $coptions['title'] = __d('field', 'datepicker.enterDateTime'); + } + $coptions['id'] = str_replace("_", "-", $fieldName); + + $entity = $this->getView()->get('vv_obj'); + + // Default the picker date to today + $now = FrozenTime::now(); + $pickerDate = $now->i18nFormat('yyyy-MM-dd'); + + // Get the existing values, if present + if(!empty($entity->$fieldName)) { + // Adjust the time back to the user's timezone + if($pickerType == DateTypeEnum::DateOnly) { + $coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz')); + } else { + $coptions['value'] = $entity->$fieldName->i18nFormat("yyyy-MM-dd HH:mm:ss", $this->getView()->get('vv_tz')); + } + $pickerDate = $entity->$fieldName->i18nFormat("yyyy-MM-dd", $this->getView()->get('vv_tz')); + } + + // Set the date picker floor year value (-100 years) + $pickerDateFT = new FrozenTime($pickerDate); + $pickerDateFT = $pickerDateFT->subYears(100); + $pickerFloor = $pickerDateFT->i18nFormat("yyyy-MM-dd"); + + $date_picker_args = [ + 'fieldName' => $fieldName, + 'pickerDate' => $pickerDate, + 'pickerType' => $pickerType, + 'pickerFloor' => $pickerFloor + ]; + + // Create a text field to hold our value and call the datePicker + $controlCode = $this->Form->text($fieldName, $coptions) + . $this->getView()->element('datePicker', $date_picker_args); + + // Specify a class on the
  • form control wrapper + $liClass = "fields-datepicker"; + + // Pass everything to the generic control() function + return $this->control($fieldName, $coptions, '', [], $controlCode, $liClass); + } + /** * End a set of form controls. * @@ -166,7 +226,7 @@ public function control(string $fieldName, * @return string Control Set end HTML */ - public function endControlSet() { + public function endControlSet(): string { $this->modelName = null; return "\n"; @@ -179,7 +239,7 @@ public function endControlSet() { * @return string Line end HTML */ - protected function endLine() { + protected function endLine(): string { return "
  • \n"; } @@ -191,7 +251,7 @@ protected function endLine() { * @return string Form Info HTML */ - protected function formInfoDiv(string $content) { + protected function formInfoDiv(string $content): string { return '
    ' . $content . '
    '; @@ -206,7 +266,7 @@ protected function formInfoDiv(string $content) { * @return string Form Info HTML */ - protected function formInfoWithPrefixDiv(string $context, string $prefix) { + protected function formInfoWithPrefixDiv(string $context, string $prefix): string { $div = '
    ' . PHP_EOL . '
    ' . PHP_EOL . '
    ' . PHP_EOL @@ -227,7 +287,7 @@ protected function formInfoWithPrefixDiv(string $context, string $prefix) { * @return string Form Name HTML */ - protected function formNameDiv(string $fieldName, string $labelText=null) { + protected function formNameDiv(string $fieldName, string $labelText=null): string { $label = $labelText; $desc = null; @@ -310,7 +370,10 @@ protected function formNameDiv(string $fieldName, string $labelText=null) { * @return string */ - public function statusControl(string $fieldName, string $status, array $link=[], string $labelText=null): string { + public function statusControl(string $fieldName, + string $status, + array $link=[], + string $labelText=null): string { $linkHtml = $status; if($link) { @@ -354,7 +417,11 @@ public function statusControl(string $fieldName, string $status, array $link=[], * @return string */ - public function startControlSet(string $modelName, string $action, bool $editable, array $reqFields, $entity=null) { + public function startControlSet(string $modelName, + string $action, + bool $editable, + array $reqFields, + $entity=null): string { $this->editable = $editable; $this->modelName = $modelName; $this->reqFields = $reqFields; @@ -371,7 +438,7 @@ public function startControlSet(string $modelName, string $action, bool $editabl * @return string */ - protected function startLine(string $class=null) { + protected function startLine(string $class=null): string { $ret = '
    * ' . __d('field', 'required') . ' diff --git a/app/templates/ApiUsers/fields.inc b/app/templates/ApiUsers/fields.inc index e4eb9bc02..80c0c0e00 100644 --- a/app/templates/ApiUsers/fields.inc +++ b/app/templates/ApiUsers/fields.inc @@ -56,9 +56,9 @@ if($vv_action == 'add' || $vv_action == 'edit') { print $this->Field->control('status', ['empty' => false]); - print $this->Field->control('valid_from'); + print $this->Field->dateControl('valid_from'); - print $this->Field->control('valid_through'); + print $this->Field->dateControl('valid_through'); print $this->Field->control('remote_ip'); diff --git a/app/templates/ExternalIdentities/fields.inc b/app/templates/ExternalIdentities/fields.inc index 961774364..04fa8c917 100644 --- a/app/templates/ExternalIdentities/fields.inc +++ b/app/templates/ExternalIdentities/fields.inc @@ -30,7 +30,7 @@ if($vv_action == 'add' || $vv_action == 'edit') { // XXX sync status? print $this->Field->control('status', ['empty' => false]); - print $this->Field->control('date_of_birth'); + print $this->Field->dateControl('date_of_birth', \App\Lib\Enum\DateTypeEnum::DateOnly); } // XXX This is a placeholder for canvas... maybe it should become a separate page diff --git a/app/templates/ExternalIdentityRoles/fields.inc b/app/templates/ExternalIdentityRoles/fields.inc index 1bd58b465..15e98947a 100644 --- a/app/templates/ExternalIdentityRoles/fields.inc +++ b/app/templates/ExternalIdentityRoles/fields.inc @@ -44,13 +44,9 @@ if($vv_action == 'add' || $vv_action == 'edit') { print $this->Field->control('manager_identifier', [], __d('field', 'manager')); -// XXX these need to render date pickers -// - we specifically have code in FieldHelper that checks for these two, but not date_of_birth -// - can FieldHelper introspect the date type rather than require a hard coded list of fields? -// though note valid_from/through uses special logic for 00:00:00 and 23:59:59 - print $this->Field->control('valid_from'); + print $this->Field->dateControl('valid_from'); - print $this->Field->control('valid_through'); + print $this->Field->dateControl('valid_through'); } // XXX This is a placeholder for canvas... maybe it should become a separate page diff --git a/app/templates/GroupMembers/fields.inc b/app/templates/GroupMembers/fields.inc index 559a946a2..6a9f07665 100644 --- a/app/templates/GroupMembers/fields.inc +++ b/app/templates/GroupMembers/fields.inc @@ -32,9 +32,8 @@ if($vv_action == 'add') { print $this->Form->hidden('person_id'); } -// XXX valid from should default to 00:00:00 time, valid through to 23:59:59 -print $this->Field->control('valid_from'); +print $this->Field->dateControl('valid_from'); -print $this->Field->control('valid_through'); +print $this->Field->dateControl('valid_through'); // XXX RFE: Add links to EIS or Nesting info \ No newline at end of file diff --git a/app/templates/People/fields.inc b/app/templates/People/fields.inc index ccb3e0a11..3b81e76f6 100644 --- a/app/templates/People/fields.inc +++ b/app/templates/People/fields.inc @@ -165,5 +165,5 @@ if($vv_action == 'add') { if($vv_action == 'add' || $vv_action == 'edit') { print $this->Field->control('status', ['empty' => false]); - print $this->Field->control('date_of_birth'); + print $this->Field->dateControl('date_of_birth', \App\Lib\Enum\DateTypeEnum::DateOnly); } \ No newline at end of file diff --git a/app/templates/PersonRoles/fields.inc b/app/templates/PersonRoles/fields.inc index b163efe3c..af8ad74bc 100644 --- a/app/templates/PersonRoles/fields.inc +++ b/app/templates/PersonRoles/fields.inc @@ -57,13 +57,9 @@ if($vv_action == 'add' || $vv_action == 'edit') { print $this->Field->statusControl($f.'_person_id', $fname, $flink, __d('field', $f)); } -// XXX these need to render date pickers -// - we specifically have code in FieldHelper that checks for these two, but not date_of_birth -// - can FieldHelper introspect the date type rather than require a hard coded list of fields? -// though note valid_from/through uses special logic for 00:00:00 and 23:59:59 - print $this->Field->control('valid_from'); + print $this->Field->dateControl('valid_from'); - print $this->Field->control('valid_through'); + print $this->Field->dateControl('valid_through'); } // XXX This is a placeholder for canvas... maybe it should become a separate page diff --git a/app/templates/element/datePicker.php b/app/templates/element/datePicker.php index 077242672..e04bc9b0e 100644 --- a/app/templates/element/datePicker.php +++ b/app/templates/element/datePicker.php @@ -1,7 +1,9 @@