diff --git a/app/src/View/Helper/FieldHelper.php b/app/src/View/Helper/FieldHelper.php
index 6a09398f0..8d50b4f41 100644
--- a/app/src/View/Helper/FieldHelper.php
+++ b/app/src/View/Helper/FieldHelper.php
@@ -302,7 +302,9 @@ public function dateField(string $fieldName,
// Initialize
$dateFormat = $dateType === DateTypeEnum::DateOnly ? 'yyyy-MM-dd' : 'yyyy-MM-dd HH:mm:ss';
$dateTitle = $dateType === DateTypeEnum::DateOnly ? 'datepicker.enterDate' : 'datepicker.enterDateTime';
- $datePattern = $dateType === DateTypeEnum::DateOnly ? '\d{4}-\d{2}-\d{2}' : '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}';
+ $datePattern = $dateType === DateTypeEnum::DateOnly ?
+ '\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])' :
+ '\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]) (0\d|1\d|2[0-3]):(0\d|[1-5]\d):(0\d|[1-5]\d)';
$queryParams = $this->getView()->getRequest()->getQueryParams();
$date_object = match(true) {
@@ -332,8 +334,8 @@ public function dateField(string $fieldName,
default => $dateType
};
- // Append the timezone to the label
- $coptions['class'] = 'form-control datepicker';
+ // Set the attributes for the field
+ $coptions['class'] = 'form-control datepicker ' . $pickerType;
$coptions['placeholder'] = $dateFormat;
$coptions['pattern'] = $datePattern;
$coptions['title'] = __d('field', $dateTitle);
diff --git a/app/templates/element/datePicker.php b/app/templates/element/datePicker.php
index 6eda46363..a1f40bb0a 100644
--- a/app/templates/element/datePicker.php
+++ b/app/templates/element/datePicker.php
@@ -66,7 +66,7 @@
pm: "= __d('field', 'datepicker.pm') ?>",
choosetime: "= __d('field', 'datepicker.chooseTime') ?>"
}
- }
+ };
},
components: {
CmDateTimePicker
@@ -90,7 +90,7 @@
// is registered and destroyed as the component is mounted and unmounted.
app.directive("clickout", {
mounted(el, binding, vnode) {
- el.clickOutEvent = function (event) {
+ el.clickOutEvent = function(event) {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event, el);
}
@@ -105,3 +105,38 @@
app.mount("#= $pickerId ?>-container");
+
+
+ = $vv_tz->getName() ?>
+
+
+
+
+ = $pickerType == 'dateonly' ? __d('field','datepicker.enterDate') : __d('field','datepicker.enterDateTime') ?>
+
+
diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css
index c07b7a294..7580fafcb 100644
--- a/app/webroot/css/co-base.css
+++ b/app/webroot/css/co-base.css
@@ -1785,8 +1785,12 @@ ul.fields li.fields-datepicker {
ul.fields li.fields-datepicker .field-info {
width: auto;
display: flex;
+ flex-wrap: wrap;
align-items: center;
}
+ul.fields li.fields-datepicker .datepicker-message {
+ flex-basis: 100%;
+}
ul.form-list input.datepicker {
width: auto;
}
@@ -1861,6 +1865,11 @@ ul.form-list .cm-time-picker-vals li {
.cm-time-picker-colon {
padding: 0 1em;
}
+.cm-tz {
+ font-size: 0.9em;
+ margin-left: 0.5em;
+ color: var(--cmg-color-txt-soft);
+}
/* Autocomplete */
.cm-autocomplete-panel {
overflow-y: auto;
@@ -2349,6 +2358,9 @@ address {
.invalid-feedback {
font-size: 1em;
}
+input[type='text'].invalid {
+ border: 1px solid var(--cmg-color-highlight-001) !important;
+}
.warn-level-a,
.warn-level-a td {
background-color: var(--cmg-color-highlight-012);
diff --git a/app/webroot/js/comanage/comanage.js b/app/webroot/js/comanage/comanage.js
index 41af5426e..a47e6635e 100644
--- a/app/webroot/js/comanage/comanage.js
+++ b/app/webroot/js/comanage/comanage.js
@@ -424,3 +424,57 @@ function setApplicationState(value, elem, reload= false) {
}
});
}
+
+// Validate a date and attempt autocompletion by type. This is used primarily with keyboard input.
+// dateVal - (string) date value from date input field
+// dateType - (string) type as defined in DateTypeEnum: standard, dateonly, fromtime, throughtime
+// regExPattern - (string) the regular expression pattern held in the field's pattern attribute
+// messageContainerId - (string) the ID of the field message container (for warnings to the user)
+function validateDateFormat(dateVal,dateType,regExPattern,fieldId) {
+ const dateValTrimmed = dateVal.trim();
+ const regexDateField = new RegExp(regExPattern);
+ const regexDateOnly = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
+ const regexDateOnlyCompact = /^\d{4}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$/;
+
+ const showInvalid = () => {
+ $('#' + fieldId + '-msg').show();
+ $('#' + fieldId).addClass('invalid');
+ };
+
+ const expandCompactYmd = (compactYmd) =>
+ compactYmd.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
+
+ // empty is always OK
+ if(dateValTrimmed === '') {
+ return dateValTrimmed;
+ }
+
+ // already matches the field pattern, accept as-is
+ if(regexDateField.test(dateValTrimmed)) {
+ return dateValTrimmed;
+ }
+
+ const isDateOnly = (dateType === 'dateonly');
+
+ // compact date (yyyymmdd) can be auto-corrected
+ if(regexDateOnlyCompact.test(dateValTrimmed)) {
+ const expanded = expandCompactYmd(dateValTrimmed);
+
+ if(isDateOnly) {
+ return expanded;
+ }
+
+ const endTime = (dateType === 'throughtime') ? ' 23:59:59' : ' 00:00:00';
+ return expanded + endTime;
+ }
+
+ // yyyy-mm-dd can be accepted for datetime fields (append time)
+ if(!isDateOnly && regexDateOnly.test(dateValTrimmed)) {
+ const endTime = (dateType === 'throughtime') ? ' 23:59:59' : ' 00:00:00';
+ return dateValTrimmed + endTime;
+ }
+
+ // anything else is invalid
+ showInvalid();
+ return dateValTrimmed;
+}
\ No newline at end of file
diff --git a/app/webroot/js/comanage/components/datepicker/cm-datetimepicker.js b/app/webroot/js/comanage/components/datepicker/cm-datetimepicker.js
index c0a4eabf3..4d3b79b0e 100644
--- a/app/webroot/js/comanage/components/datepicker/cm-datetimepicker.js
+++ b/app/webroot/js/comanage/components/datepicker/cm-datetimepicker.js
@@ -71,6 +71,7 @@ export default {
dateField.value = dateTime.join(' ');
}
}
+ this.clearExternalValidationMessages(this.target);
},
formatDate(date) {
let formattedDate = date.getFullYear();
@@ -116,8 +117,25 @@ export default {
if(type == 'minute') {
time[1] = val;
}
+ // Do some basic validation
+ let minutesAndSecondsRegex = /^(0\d|[1-5]\d)$/;
+ if(time[1] === undefined) {
+ // The minutes are missing (including the colon). Restore them.
+ time.push('00');
+ } else if(!minutesAndSecondsRegex.test(time[1])) {
+ // The minutes are invalid. Set them to zero.
+ time[1] = '00';
+ }
+ if(time[2] === undefined) {
+ // The seconds are missing (including the colon). Restore them.
+ time.push('00');
+ } else if(!minutesAndSecondsRegex.test(time[2])) {
+ // The seconds are invalid. Set them to zero.
+ time[2] = '00';
+ }
dateTime[1] = time.join(':');
dateField.value = dateTime.join(' ');
+ this.clearExternalValidationMessages(this.target);
},
formatToday() {
const today = new Date();
@@ -127,6 +145,12 @@ export default {
today.setSeconds(0);
const formattedToday = this.formatDate(today);
return formattedToday.split(' ');
+ },
+ clearExternalValidationMessages(fieldId) {
+ // Turn off validation messages that may have been revealed on the input field
+ // which is outside of this component. (We'll use jQuery for simplicity.)
+ $('#' + fieldId + '-msg').hide();
+ $('#' + fieldId).removeClass('invalid');
}
},
mounted() {