@yuuvis/client-framework 3.19.0 → 3.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/fesm2022/yuuvis-client-framework-datepicker.mjs +90 -34
  2. package/fesm2022/yuuvis-client-framework-datepicker.mjs.map +1 -1
  3. package/fesm2022/yuuvis-client-framework-forms.mjs +14 -5
  4. package/fesm2022/yuuvis-client-framework-forms.mjs.map +1 -1
  5. package/fesm2022/yuuvis-client-framework-object-details.mjs +2 -1
  6. package/fesm2022/yuuvis-client-framework-object-details.mjs.map +1 -1
  7. package/fesm2022/yuuvis-client-framework-object-flavor.mjs +2 -1
  8. package/fesm2022/yuuvis-client-framework-object-flavor.mjs.map +1 -1
  9. package/fesm2022/yuuvis-client-framework-object-form.mjs +3 -1
  10. package/fesm2022/yuuvis-client-framework-object-form.mjs.map +1 -1
  11. package/fesm2022/yuuvis-client-framework-object-relationship.mjs +9 -3
  12. package/fesm2022/yuuvis-client-framework-object-relationship.mjs.map +1 -1
  13. package/fesm2022/yuuvis-client-framework-object-summary.mjs +4 -1
  14. package/fesm2022/yuuvis-client-framework-object-summary.mjs.map +1 -1
  15. package/fesm2022/yuuvis-client-framework-renderer.mjs +22 -11
  16. package/fesm2022/yuuvis-client-framework-renderer.mjs.map +1 -1
  17. package/fesm2022/yuuvis-client-framework-smart-search.mjs +54 -14
  18. package/fesm2022/yuuvis-client-framework-smart-search.mjs.map +1 -1
  19. package/lib/assets/i18n/en.json +4 -4
  20. package/package.json +6 -6
  21. package/types/yuuvis-client-framework-datepicker.d.ts +16 -1
  22. package/types/yuuvis-client-framework-object-form.d.ts +6 -0
  23. package/types/yuuvis-client-framework-renderer.d.ts +17 -4
  24. package/types/yuuvis-client-framework-smart-search.d.ts +29 -6
@@ -395,7 +395,11 @@ class DateInputElementComponent {
395
395
  this.disabled = isDisabled;
396
396
  }
397
397
  _isValidInput(v) {
398
- const n = parseInt(v);
398
+ // digits only - parseInt alone would accept trailing garbage like '1x' and
399
+ // silently reduce it to 1
400
+ if (!/^\d+$/.test(`${v ?? ''}`))
401
+ return false;
402
+ const n = parseInt(`${v}`, 10);
399
403
  return this._isNumber(n) && (!this.maxValue || n <= this.maxValue) && (!this.minValue || n >= this.minValue);
400
404
  }
401
405
  validate(c) {
@@ -1067,6 +1071,7 @@ class DateInputComponent {
1067
1071
  this.date = null;
1068
1072
  this._disabled = false;
1069
1073
  this.invalid = signal(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
1074
+ this._dateInvalid = false;
1070
1075
  this._refreshPlaceholder = true;
1071
1076
  this._locale = this.datepickerService.DEFAULT_LANGUAGE;
1072
1077
  this._withTime = false;
@@ -1117,12 +1122,20 @@ class DateInputComponent {
1117
1122
  }
1118
1123
  });
1119
1124
  this._formStateSub = this.dateInputForm.statusChanges.subscribe({
1120
- next: (v) => {
1121
- this._setInvalidInputError(v === 'INVALID');
1125
+ next: () => {
1126
+ this._updateInvalidState();
1122
1127
  }
1123
1128
  });
1124
1129
  }
1125
- _setInvalidInputError(isInvalid) {
1130
+ /**
1131
+ * Reflects both sources of invalidity: a structurally incomplete/invalid form
1132
+ * (missing or out of range parts) and input that does not describe a real
1133
+ * calendar date (e.g. 31st of February). Both have to be taken into account
1134
+ * here because the form group may well be VALID while the entered parts still
1135
+ * do not add up to an existing date.
1136
+ */
1137
+ _updateInvalidState() {
1138
+ const isInvalid = this.dateInputForm?.status === 'INVALID' || this._dateInvalid;
1126
1139
  this.invalid.set(isInvalid);
1127
1140
  if (isInvalid) {
1128
1141
  this.datepickerService.setErrors({
@@ -1134,39 +1147,78 @@ class DateInputComponent {
1134
1147
  }
1135
1148
  }
1136
1149
  _checkAndPropagateForm() {
1137
- if (this.dateInputForm?.status === 'VALID') {
1138
- this.date = this._formToDate(this.dateInputForm);
1139
- this.datepickerService.setValue(this.date);
1150
+ if (this.dateInputForm?.status !== 'VALID') {
1151
+ // incomplete or out of range input, the status subscription reports the error
1152
+ return;
1140
1153
  }
1141
- }
1142
- _formToDate(g) {
1143
- // starting with a year of '0000' because there could be negative values
1144
- // that would break the date creation using string parameter
1145
- let dateString = `0000-${g.controls['month'].value}-${g.controls['day'].value}`;
1146
- if (this.withTime) {
1147
- const strHour = g.controls['hour'].value;
1148
- let hour = strHour?.length ? parseInt(strHour) : -1;
1149
- if (this.hour12) {
1150
- hour = this.currentDayPeriod === 'pm' && hour < 12 ? hour + 12 : hour;
1151
- hour = this.currentDayPeriod === 'am' && hour === 12 ? 0 : hour;
1152
- hour = hour === 24 ? 0 : hour;
1153
- }
1154
- dateString += `T${hour < 10 ? `0${hour}` : `${hour}`}:${g.controls['minute'].value}:00`;
1154
+ const parsed = this._formToDate(this.dateInputForm);
1155
+ if (!parsed.empty && !parsed.date) {
1156
+ // input that does not describe an existing date (e.g. 31.02.). Keep what the user
1157
+ // typed so it can be corrected, but do not propagate a silently shifted date
1158
+ this._dateInvalid = true;
1159
+ this._updateInvalidState();
1160
+ return;
1155
1161
  }
1156
- const date = new Date(dateString);
1157
- let isValidDateObject = !!date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
1158
- if (isValidDateObject) {
1159
- // set the year independently
1160
- const parsedYear = parseInt(g.controls['year'].value);
1161
- if (!isNaN(parsedYear)) {
1162
- date.setFullYear(parsedYear);
1163
- }
1164
- else {
1165
- isValidDateObject = false;
1166
- }
1162
+ this._dateInvalid = false;
1163
+ this.date = parsed.date;
1164
+ this.datepickerService.setValue(this.date);
1165
+ this._updateInvalidState();
1166
+ }
1167
+ _formToDate(group) {
1168
+ const keys = this.withTime ? ['day', 'month', 'year', 'hour', 'minute'] : ['day', 'month', 'year'];
1169
+ if (keys.every((key) => !`${group.controls[key].value ?? ''}`.length))
1170
+ return { date: null, empty: true };
1171
+ const invalid = { date: null, empty: false };
1172
+ const day = this._toInt(group.controls['day'].value);
1173
+ const month = this._toInt(group.controls['month'].value);
1174
+ // the year may be negative because of a locale year offset (e.g. locale 'th')
1175
+ const year = this._toInt(group.controls['year'].value, true);
1176
+ if (day === null || month === null || year === null)
1177
+ return invalid;
1178
+ if (month < 1 || month > 12 || day < 1 || day > this._daysInMonth(year, month))
1179
+ return invalid;
1180
+ const time = this._formToTime(group);
1181
+ if (!time)
1182
+ return invalid;
1183
+ // setFullYear with all three arguments assigns them at once, so no intermediate
1184
+ // rollover can happen, and it also covers years outside the parsable range
1185
+ const date = new Date(0);
1186
+ date.setFullYear(year, month - 1, day);
1187
+ date.setHours(time.hour, time.minute, 0, 0);
1188
+ // guard against any remaining shift (e.g. a local time skipped by a DST change)
1189
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
1190
+ ? { date, empty: false }
1191
+ : invalid;
1192
+ }
1193
+ _formToTime(group) {
1194
+ if (!this.withTime)
1195
+ return { hour: 0, minute: 0 };
1196
+ let hour = this._toInt(group.controls['hour'].value);
1197
+ const minute = this._toInt(group.controls['minute'].value);
1198
+ if (hour === null || minute === null)
1199
+ return null;
1200
+ if (this.hour12) {
1201
+ hour = this.currentDayPeriod === 'pm' && hour < 12 ? hour + 12 : hour;
1202
+ hour = this.currentDayPeriod === 'am' && hour === 12 ? 0 : hour;
1203
+ hour = hour === 24 ? 0 : hour;
1167
1204
  }
1168
- this._setInvalidInputError(!isValidDateObject);
1169
- return isValidDateObject ? date : null;
1205
+ return hour > 23 || minute > 59 ? null : { hour, minute };
1206
+ }
1207
+ /**
1208
+ * Strict integer parse - unlike parseInt it rejects values with trailing
1209
+ * garbage ('1x') so those cannot slip through as a valid date part.
1210
+ */
1211
+ _toInt(value, allowNegative = false) {
1212
+ const str = `${value ?? ''}`;
1213
+ if (!(allowNegative ? /^-?\d+$/ : /^\d+$/).test(str))
1214
+ return null;
1215
+ return parseInt(str, 10);
1216
+ }
1217
+ _daysInMonth(year, month) {
1218
+ // day 0 of the following month is the last day of the given month
1219
+ const last = new Date(0);
1220
+ last.setFullYear(year, month, 0);
1221
+ return last.getDate();
1170
1222
  }
1171
1223
  _focusInput(offset) {
1172
1224
  const inputs = this._getFocusableElements();
@@ -1291,6 +1343,10 @@ class DateInputComponent {
1291
1343
  this.dateInputForm.patchValue(this._getPatch(vc.value), {
1292
1344
  emitEvent: false
1293
1345
  });
1346
+ // the value comes from the outside (calendar, model), so any previously
1347
+ // entered non existing date is gone now
1348
+ this._dateInvalid = false;
1349
+ this._updateInvalidState();
1294
1350
  // this.dateInputForm?.updateValueAndValidity()
1295
1351
  }
1296
1352
  }));