@massimo-cassandro/minimo 1.6.0 → 1.7.0
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.
- package/index.js +10 -3
- package/package.json +4 -2
- package/src/components/autocomplete/autocomplete-engine.js +70 -64
- package/src/components/autocomplete/check-autocomplete.js +10 -2
- package/src/components/dismiss-alerts.js +9 -2
- package/src/components/form-reset-submit.js +12 -4
- package/src/components/inner-nav/inner-nav.js +5 -1
- package/src/components/modal-alert/customization-templates/custom-modal-alert.js +4 -0
- package/src/components/modal-alert/defaults.js +6 -3
- package/src/components/modal-alert/modal-alert.js +62 -21
- package/src/components/modal-popup/modal-popup.js +51 -29
- package/src/components/overlay/overlay.js +13 -1
- package/src/components/sf-macro/sf-macro.js +23 -12
- package/src/components/snackbar/snackbar.js +32 -17
- package/src/components/spinner/spinner.js +6 -2
- package/src/components/unsplash-page/fetch-unsplash-data.js +25 -2
- package/src/components/unsplash-page/img-breakpoints.js +11 -0
- package/src/components/unsplash-page/unsplash-page.js +64 -40
- package/src/css/forms/forms.css +4 -0
- package/src/utilities/better-text.js +80 -0
- package/src/utilities/classnames.js +14 -1
- package/src/utilities/cookies.js +28 -0
- package/src/utilities/create-id.js +19 -0
- package/src/utilities/dom-builder/dom-builder.js +84 -75
- package/src/utilities/dom-builder/domBuilderBasicSetup.js +12 -9
- package/src/utilities/dom-builder/parseDomString.js +31 -29
- package/src/utilities/enable-submit-btns.js +7 -1
- package/src/utilities/{escapeHTML.js → escape-html.js} +13 -0
- package/src/utilities/linked-dates.js +64 -0
- package/src/utilities/relative-date.js +153 -0
- package/src/utilities/sentence-case.js +5 -2
- package/src/utilities/strip-tags.js +8 -0
- package/src/utilities/title-case.js +6 -2
- package/src/utilities/truncate-string.js +22 -0
- package/src/utilities/validation-error-class.js +14 -7
- package/src/web-components/s-datatable-component/s-datatable-component.js +1 -0
- package/src/web-components/s-datatable-component/src/parse-cols.js +1 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Links date/datetime-local input fields so that one field constrains
|
|
5
|
+
* the min/max of another.
|
|
6
|
+
*
|
|
7
|
+
* The `data-max` attribute on a date field must contain the `id` of the field
|
|
8
|
+
* whose value sets the `max` constraint of the current field (start date).
|
|
9
|
+
* The `data-min` attribute on a date field must contain the `id` of the field
|
|
10
|
+
* whose value sets the `min` constraint of the current field (end date).
|
|
11
|
+
*
|
|
12
|
+
* If the linked field is disabled, the constraint is removed on focus
|
|
13
|
+
* and restored when the linked field is re-enabled.
|
|
14
|
+
*
|
|
15
|
+
* @param {Document|Element} context - root element to search within (defaults to `document`)
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
export function linkedDates(context = document) {
|
|
19
|
+
|
|
20
|
+
// start date fields: constrained by a max value from a linked field
|
|
21
|
+
context.querySelectorAll('input[type="date"][data-max], input[type="datetime-local"][data-max]').forEach(el => {
|
|
22
|
+
const maxId = /** @type {HTMLInputElement} */ (el).dataset.max;
|
|
23
|
+
let linked_field = maxId ? document.getElementById(maxId) : null;
|
|
24
|
+
|
|
25
|
+
if(linked_field) {
|
|
26
|
+
el.setAttribute('max', /** @type {HTMLInputElement} */ (linked_field).value);
|
|
27
|
+
|
|
28
|
+
linked_field.addEventListener('change', () => {
|
|
29
|
+
el.setAttribute('max', /** @type {HTMLInputElement} */ (linked_field).value);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// remove max if the linked field is disabled; restore it when re-focused enabled
|
|
33
|
+
el.addEventListener('focus', () => {
|
|
34
|
+
if(/** @type {HTMLInputElement} */ (linked_field).disabled) {
|
|
35
|
+
el.removeAttribute('max');
|
|
36
|
+
} else {
|
|
37
|
+
el.setAttribute('max', /** @type {HTMLInputElement} */ (linked_field).value);
|
|
38
|
+
}
|
|
39
|
+
}, false);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// end date fields: constrained by a min value from a linked field
|
|
44
|
+
context.querySelectorAll('input[type="date"][data-min], input[type="datetime-local"][data-min]').forEach(el => {
|
|
45
|
+
const minId = /** @type {HTMLInputElement} */ (el).dataset.min;
|
|
46
|
+
let linked_field = minId ? document.getElementById(minId) : null;
|
|
47
|
+
|
|
48
|
+
if(linked_field) {
|
|
49
|
+
linked_field.addEventListener('change', () => {
|
|
50
|
+
el.setAttribute('min', /** @type {HTMLInputElement} */ (linked_field).value);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// remove min if the linked field is disabled; restore it when re-focused enabled
|
|
54
|
+
el.addEventListener('focus', () => {
|
|
55
|
+
if(/** @type {HTMLInputElement} */ (linked_field).disabled) {
|
|
56
|
+
el.removeAttribute('min');
|
|
57
|
+
} else {
|
|
58
|
+
el.setAttribute('min', /** @type {HTMLInputElement} */ (linked_field).value);
|
|
59
|
+
}
|
|
60
|
+
}, false);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// Displays a date in relative form to the current date if within the set limits.
|
|
4
|
+
// Relative dates are automatically updated every minute.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} RelativeDateOptions
|
|
8
|
+
* @property {boolean} [useRelativeTime=true] - Show times near the date as "in xx minutes" (before) or "now" (within `relativeTimeMinutesAfter`)
|
|
9
|
+
* @property {string} [nowString] - String to display when the date is right now
|
|
10
|
+
* @property {number} [relativeTimeMinutesBefore] - Minutes before the date within which to show "in xx minutes"
|
|
11
|
+
* @property {number} [relativeTimeMinutesAfter] - Minutes after the date within which to show the now string
|
|
12
|
+
* @property {boolean} [relativeTimeShowTime] - Show the actual time alongside relative date labels
|
|
13
|
+
* @property {string} [relativeTimeShowTimeMarkup] - HTML markup for the time portion; `@@time@@` is replaced with the localized time string
|
|
14
|
+
* @property {boolean} [firstLetterUpperCase] - Capitalize the first letter of the returned string
|
|
15
|
+
* @property {Intl.DateTimeFormatOptions} [dateFormat] - Format for non-relative dates (beyond day after tomorrow)
|
|
16
|
+
* @property {Intl.DateTimeFormatOptions} [timeFormat] - Format for time displayed in relative date labels
|
|
17
|
+
* @property {string[]} [relativeStrings] - Labels for yesterday / today / tomorrow / day after tomorrow (in chronological order)
|
|
18
|
+
* @property {string} [locale] - Locale string for date/time formatting
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns an HTML `<time>` element string displaying the date in relative or absolute form.
|
|
23
|
+
* Relative dates are auto-refreshed every minute via a shared `setInterval`.
|
|
24
|
+
*
|
|
25
|
+
* @param {Date | string} date - Date to display (Date object or ISO string)
|
|
26
|
+
* @param {RelativeDateOptions} [options={}]
|
|
27
|
+
* @returns {string} HTML `<time>` element as a string
|
|
28
|
+
*
|
|
29
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// TODO: Add option to include start time in relative dates
|
|
33
|
+
// TODO: alternative solution in place of `@@time@@` placeholder
|
|
34
|
+
// TODO reconsider and evaluate the use of Intl.RelativeTimeFormat Intl.RelativeTimeFormat
|
|
35
|
+
|
|
36
|
+
export function relativeDate(date, options = {}) {
|
|
37
|
+
|
|
38
|
+
/** @type {Required<RelativeDateOptions>} */
|
|
39
|
+
const default_options = {
|
|
40
|
+
useRelativeTime: true,
|
|
41
|
+
nowString: 'adesso',
|
|
42
|
+
relativeTimeMinutesBefore: 30,
|
|
43
|
+
relativeTimeMinutesAfter: 30,
|
|
44
|
+
relativeTimeShowTime: true,
|
|
45
|
+
relativeTimeShowTimeMarkup: ' <span class="time-info">(alle @@time@@)</span>',
|
|
46
|
+
firstLetterUpperCase: true,
|
|
47
|
+
dateFormat: {
|
|
48
|
+
weekday: 'short',
|
|
49
|
+
year: 'numeric',
|
|
50
|
+
month: 'short',
|
|
51
|
+
day: 'numeric',
|
|
52
|
+
hour12: false,
|
|
53
|
+
hour: '2-digit',
|
|
54
|
+
minute: '2-digit'
|
|
55
|
+
},
|
|
56
|
+
timeFormat: {
|
|
57
|
+
hour12: false,
|
|
58
|
+
hour: '2-digit',
|
|
59
|
+
minute: '2-digit'
|
|
60
|
+
},
|
|
61
|
+
relativeStrings: [ // NB: in chronological order
|
|
62
|
+
'ieri alle', 'oggi alle', 'domani alle', 'dopodomani alle'
|
|
63
|
+
],
|
|
64
|
+
locale: 'it-IT'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let isRelative = false;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {Date | string} dateInput
|
|
71
|
+
* @param {RelativeDateOptions} [overrideOptions={}]
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
const parseDate = (dateInput, overrideOptions = {}) => {
|
|
75
|
+
|
|
76
|
+
const opts = { ...default_options, ...overrideOptions };
|
|
77
|
+
|
|
78
|
+
const d = typeof dateInput === 'string' ? new Date(dateInput) : dateInput
|
|
79
|
+
, now = new Date()
|
|
80
|
+
, minutesDiff = (d.getTime() - now.getTime()) / 60000 // difference between the two dates in minutes
|
|
81
|
+
, daysDiff = (
|
|
82
|
+
new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0).getTime() -
|
|
83
|
+
new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0).getTime()
|
|
84
|
+
) / 86400000
|
|
85
|
+
, rtf = new Intl.RelativeTimeFormat('it', { style: 'long' })
|
|
86
|
+
;
|
|
87
|
+
|
|
88
|
+
/** @type {string} */
|
|
89
|
+
let result;
|
|
90
|
+
|
|
91
|
+
if (opts.useRelativeTime && Math.floor(Math.abs(minutesDiff)) === 0) {
|
|
92
|
+
|
|
93
|
+
result = opts.nowString;
|
|
94
|
+
isRelative = true;
|
|
95
|
+
|
|
96
|
+
} else if (opts.useRelativeTime && (
|
|
97
|
+
(Math.abs(minutesDiff) <= opts.relativeTimeMinutesAfter && minutesDiff < 0) ||
|
|
98
|
+
(minutesDiff > 0 && minutesDiff <= opts.relativeTimeMinutesBefore)
|
|
99
|
+
)) {
|
|
100
|
+
|
|
101
|
+
// formatToParts + reduce needed to strip decimal fractions from the value
|
|
102
|
+
const parts = rtf.formatToParts(minutesDiff, 'minute').reduce((acc, curr) => {
|
|
103
|
+
if (curr.type !== 'decimal') { // decimal separator
|
|
104
|
+
if (curr.type === 'fraction') { // fractional part
|
|
105
|
+
const intPart = acc.find(i => i.type === 'integer');
|
|
106
|
+
if (intPart) intPart.value += `.${curr.value}`;
|
|
107
|
+
} else {
|
|
108
|
+
acc.push(curr);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return acc;
|
|
112
|
+
}, /** @type {Intl.RelativeTimeFormatPart[]} */ ([]));
|
|
113
|
+
|
|
114
|
+
result = parts.map(i => i.type !== 'literal' ? Math.round(+i.value) : i.value).join('') +
|
|
115
|
+
(opts.relativeTimeShowTime ? opts.relativeTimeShowTimeMarkup?.replace('@@time@@', d.toLocaleString(opts.locale, opts.timeFormat)) : '');
|
|
116
|
+
|
|
117
|
+
isRelative = true;
|
|
118
|
+
|
|
119
|
+
// between -1 day and +2 days
|
|
120
|
+
} else if (daysDiff >= -1 && daysDiff <= 2) {
|
|
121
|
+
|
|
122
|
+
result = opts.relativeStrings[daysDiff + 1] + ' ' + d.toLocaleString(opts.locale, opts.timeFormat);
|
|
123
|
+
isRelative = daysDiff === 0;
|
|
124
|
+
|
|
125
|
+
} else {
|
|
126
|
+
result = d.toLocaleString(opts.locale, opts.dateFormat);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (opts.firstLetterUpperCase && result) {
|
|
130
|
+
result = result.charAt(0).toUpperCase() + result.slice(1).toLowerCase();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return `<time datetime="${d.toISOString()}"${isRelative ? ` data-relative-date-opts="${encodeURIComponent(JSON.stringify(overrideOptions))}"` : ''}>${result}</time>`;
|
|
134
|
+
|
|
135
|
+
}; // end parseDate
|
|
136
|
+
|
|
137
|
+
const result = parseDate(date, options);
|
|
138
|
+
|
|
139
|
+
if (isRelative && !(/** @type {any} */ (window)).relativeDateUpd) {
|
|
140
|
+
/** @type {any} */ (window).relativeDateUpd = true;
|
|
141
|
+
setInterval(() => {
|
|
142
|
+
document.querySelectorAll('time[data-relative-date-opts]').forEach(item => {
|
|
143
|
+
const datetime = item.getAttribute('datetime');
|
|
144
|
+
const optsAttr = /** @type {HTMLElement} */ (item).dataset.relativeDateOpts;
|
|
145
|
+
if (datetime && optsAttr) {
|
|
146
|
+
item.outerHTML = parseDate(datetime, JSON.parse(decodeURIComponent(optsAttr)));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}, 60000);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Converts a string to sentence case (first letter uppercase, rest lowercase).
|
|
3
|
+
* @param {string | null | undefined} str
|
|
4
|
+
* @returns {string | null | undefined}
|
|
5
|
+
*/
|
|
3
6
|
export function sentenceCase(str) {
|
|
4
7
|
if(str) {
|
|
5
8
|
return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Converts a string to title case (first letter of each word uppercase, rest lowercase).
|
|
3
|
+
* @param {string | null | undefined} str
|
|
4
|
+
* @returns {string | null | undefined}
|
|
5
|
+
*/
|
|
2
6
|
export function titleCase(str) {
|
|
3
7
|
|
|
4
8
|
if(str) {
|
|
@@ -11,4 +15,4 @@ export function titleCase(str) {
|
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
return str;
|
|
14
|
-
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Truncates a string to the desired length adding an option suffix
|
|
6
|
+
* from https://www.codegrepper.com/code-examples/javascript/javascript+truncate+string+full+word
|
|
7
|
+
*
|
|
8
|
+
* @param {string} str - string yo be truncated
|
|
9
|
+
* @param {number} maxLength - truncated string max length
|
|
10
|
+
* @param {string} suffix
|
|
11
|
+
* @returns {string|undefined} truncated string, or undefined if str is falsy
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export function truncateString(str, maxLength, suffix) {
|
|
16
|
+
|
|
17
|
+
suffix = suffix || '…';
|
|
18
|
+
if(str) {
|
|
19
|
+
return str.length < maxLength ? str :
|
|
20
|
+
`${str.substring(0, str.substring(0, maxLength - suffix.length).lastIndexOf(' '))}${suffix}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// throw new ValidationError('Sono presenti valori duplicati', campiErrore );
|
|
1
|
+
// usage:
|
|
2
|
+
// throw new ValidationError('Duplicate values found', errorFields);
|
|
4
3
|
|
|
5
4
|
/*
|
|
6
|
-
|
|
5
|
+
example:
|
|
7
6
|
|
|
8
7
|
const form = document.getElementById('form-__xxxx___');
|
|
9
8
|
form.addEventListener('submit', e => {
|
|
@@ -13,7 +12,7 @@ form.addEventListener('submit', e => {
|
|
|
13
12
|
form.classList.add('was-validated');
|
|
14
13
|
try {
|
|
15
14
|
if( ... ) {
|
|
16
|
-
throw new ValidationError('
|
|
15
|
+
throw new ValidationError('__message__', [errorFields, ...] );
|
|
17
16
|
}
|
|
18
17
|
} catch( error ) {
|
|
19
18
|
e.preventDefault();
|
|
@@ -35,10 +34,18 @@ form.addEventListener('submit', e => {
|
|
|
35
34
|
});
|
|
36
35
|
*/
|
|
37
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Custom error class for form validation failures.
|
|
39
|
+
* Extends the native Error with an array of invalid form fields.
|
|
40
|
+
*/
|
|
38
41
|
export class ValidationError extends Error {
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} message - Error message.
|
|
44
|
+
* @param {Element[]} fields - Array of invalid form field elements.
|
|
45
|
+
*/
|
|
39
46
|
constructor(message, fields) {
|
|
40
|
-
super(message);
|
|
47
|
+
super(message);
|
|
41
48
|
this.name = 'ValidationError';
|
|
42
|
-
this.fields = fields;
|
|
49
|
+
this.fields = fields;
|
|
43
50
|
}
|
|
44
51
|
}
|