@massimo-cassandro/minimo 1.6.1 → 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 +7 -2
- package/package.json +1 -1
- package/src/components/modal-alert/modal-alert.js +4 -1
- package/src/utilities/better-text.js +80 -0
- package/src/utilities/cookies.js +28 -0
- package/src/utilities/linked-dates.js +64 -0
- package/src/utilities/relative-date.js +153 -0
- package/src/utilities/truncate-string.js +22 -0
- /package/src/utilities/{escapeHTML.js → escape-html.js} +0 -0
package/index.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
|
|
2
2
|
// utilities
|
|
3
|
+
export { betterText } from './src/utilities/better-text.js';
|
|
3
4
|
export { classnames, classnamesNull } from './src/utilities/classnames.js';
|
|
4
5
|
export { createId } from './src/utilities/create-id.js';
|
|
5
6
|
export { domBuilder } from './src/utilities/dom-builder/dom-builder.js';
|
|
6
7
|
export { enableSubmitBtns } from './src/utilities/enable-submit-btns.js';
|
|
7
|
-
export { escapeHTML, unescapeHTML } from './src/utilities/
|
|
8
|
+
export { escapeHTML, unescapeHTML } from './src/utilities/escape-html.js';
|
|
9
|
+
export { getCookie, setCookie } from './src/utilities/cookies.js';
|
|
10
|
+
export { linkedDates } from './src/utilities/linked-dates.js';
|
|
11
|
+
export { relativeDate } from './src/utilities/relative-date.js';
|
|
8
12
|
export { sentenceCase } from './src/utilities/sentence-case.js';
|
|
9
13
|
export { stripTags } from './src/utilities/strip-tags.js';
|
|
10
14
|
export { titleCase } from './src/utilities/title-case.js';
|
|
15
|
+
export { truncateString } from './src/utilities/truncate-string.js';
|
|
11
16
|
export { ValidationError } from './src/utilities/validation-error-class.js';
|
|
12
17
|
|
|
13
18
|
// components
|
|
14
|
-
export {
|
|
19
|
+
export { modalAlert } from './src/components/modal-alert/modal-alert.js';
|
|
15
20
|
export { dismissAlerts } from './src/components/dismiss-alerts.js';
|
|
16
21
|
export { formResetSubmit } from './src/components/form-reset-submit.js';
|
|
17
22
|
export { innerNav } from './src/components/inner-nav/inner-nav.js';
|
package/package.json
CHANGED
|
@@ -39,7 +39,10 @@ import {defaults} from './defaults.js';
|
|
|
39
39
|
* @param {Record<string, string>} [marks=default_marks] - SVG icon map keyed by dialog type.
|
|
40
40
|
* @returns {void}
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
//TODO refactoring
|
|
44
|
+
|
|
45
|
+
export function modalAlert(params, custom_defaults = {}, marks = default_marks) {
|
|
43
46
|
|
|
44
47
|
try {
|
|
45
48
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Improves a text string by fixing punctuation spacing, multiple spaces,
|
|
5
|
+
* typographic quotes, non-breaking spaces after Italian particles/articles,
|
|
6
|
+
* and optionally enforcing the exact casing of custom words.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} str - input string to process
|
|
9
|
+
* @param {string[]} custom_words - list of words whose exact casing must be preserved (e.g. `['iPhone', 'macOS']`)
|
|
10
|
+
* @returns {string} processed string, or empty string if input is falsy
|
|
11
|
+
*/
|
|
12
|
+
export function betterText(str, custom_words = []) {
|
|
13
|
+
|
|
14
|
+
const particelle = [
|
|
15
|
+
'e', 'ed',
|
|
16
|
+
'di', 'a', 'da', 'in', 'per', 'con', 'su', 'per', 'tra', 'fra',
|
|
17
|
+
'il', 'lo', 'la', 'i', 'gli', 'le',
|
|
18
|
+
'un', 'uno', 'una',
|
|
19
|
+
'del', 'dello', 'della', 'dei', 'degli', 'delle',
|
|
20
|
+
'al', 'allo', 'alla', 'ai', 'agli', 'alle',
|
|
21
|
+
'dal', 'dallo', 'dalla', 'dai', 'dagli', 'dalle',
|
|
22
|
+
'nel', 'nello', 'nella', 'nei', 'negli', 'nelle',
|
|
23
|
+
'col', 'collo', 'colla', 'con', 'coi', 'cogli', 'colle',
|
|
24
|
+
'sul', 'sullo', 'sulla', 'sui', 'sugli', 'sulle',
|
|
25
|
+
|
|
26
|
+
// topographic terms
|
|
27
|
+
'C.so', 'Corso', 'Via', 'P.za', 'Piazza', 'L.go', 'Largo', 'V.le', 'Viale',
|
|
28
|
+
'De', 'San', 'c/o', 'Loc.',
|
|
29
|
+
|
|
30
|
+
// English
|
|
31
|
+
'the', 'a', 'an', 'at', 'out', 'of',
|
|
32
|
+
|
|
33
|
+
// words used in geonames
|
|
34
|
+
'isola', 'isole'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
if(str) {
|
|
38
|
+
str = str.trim();
|
|
39
|
+
|
|
40
|
+
// punctuation: remove space before, ensure single space after
|
|
41
|
+
// NOTE: must run before any HTML entity replacements to avoid conflicts with `;`
|
|
42
|
+
// (?!$): negative lookahead to skip punctuation at end of string
|
|
43
|
+
str = str.replace(/ +(,|;|\.|:|!|\?)/g, '$1');
|
|
44
|
+
str = str.replace(/(,|;|\.|:|!|\?)(?!$) +/g, '$1 ');
|
|
45
|
+
|
|
46
|
+
// collapse multiple spaces and tabs (including non-breaking spaces)
|
|
47
|
+
str = str.replace(/[ \t\u00A0]+/g, ' ');
|
|
48
|
+
|
|
49
|
+
// typographic quotes
|
|
50
|
+
str = str.replace(/(^| )"/g, '$1“') // opening double quotes
|
|
51
|
+
.replace(/"/g, '”') // remaining double quotes → closing
|
|
52
|
+
.replace(/(^| )'/g, '$1‘') // opening single quotes
|
|
53
|
+
.replace(/'/g, '’') // remaining single quotes and apostrophes
|
|
54
|
+
;
|
|
55
|
+
|
|
56
|
+
// remove space after apostrophe preceded by elided articles (l', un', d', all', ...)
|
|
57
|
+
str = str.replace(/((^| )(l|un|d|all|dell|nell|sull)('|')) /gi, '$1' );
|
|
58
|
+
|
|
59
|
+
// replace regular spaces with non-breaking spaces after Italian particles/articles
|
|
60
|
+
var re = new RegExp('\\b(' + particelle.join('|') + ')\\b +', 'gmi');
|
|
61
|
+
str = str.replace(re, function (match) {
|
|
62
|
+
return match.replace(/ +/g, '\u00A0');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// enforce exact casing of each particle
|
|
66
|
+
particelle.forEach(term => {
|
|
67
|
+
const re = new RegExp('\\b(' + term + ')\\b', 'gmi');
|
|
68
|
+
str = str.replace(re, term);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if(custom_words.length) {
|
|
72
|
+
custom_words.forEach(item => {
|
|
73
|
+
str = str.replace(new RegExp(`\\b${item}\\b`, 'gi'), item);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
str = '';
|
|
78
|
+
}
|
|
79
|
+
return str;
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the value of a cookie by name.
|
|
5
|
+
* @param {string} name
|
|
6
|
+
* @returns {string | undefined}
|
|
7
|
+
*/
|
|
8
|
+
export function getCookie(name) {
|
|
9
|
+
const value = `; ${document.cookie}`;
|
|
10
|
+
const parts = value.split(`; ${name}=`);
|
|
11
|
+
if (parts.length === 2) return parts.pop()?.split(';').shift();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sets a cookie.
|
|
17
|
+
* @param {object} params
|
|
18
|
+
* @param {string} params.name
|
|
19
|
+
* @param {string} params.value
|
|
20
|
+
* @param {string|null} [params.path=null]
|
|
21
|
+
* @param {number|null} [params.expire=null] - Max-Age in seconds
|
|
22
|
+
*/
|
|
23
|
+
export function setCookie({name, value, path = null, expire = null}) {
|
|
24
|
+
document.cookie = `${name}=${value}` +
|
|
25
|
+
(path ? `; path=${path}` : '') +
|
|
26
|
+
'; SameSite=Lax' +
|
|
27
|
+
(expire ? `; Max-Age=${expire}` : '');
|
|
28
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
File without changes
|