@massimo-cassandro/minimo 1.6.1 → 1.7.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.
package/index.js CHANGED
@@ -1,17 +1,24 @@
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';
6
+ export { disableBtnsOnSubmit } from './src/utilities/disable-btns-on-submit.js';
5
7
  export { domBuilder } from './src/utilities/dom-builder/dom-builder.js';
6
8
  export { enableSubmitBtns } from './src/utilities/enable-submit-btns.js';
7
- export { escapeHTML, unescapeHTML } from './src/utilities/escapeHTML.js';
9
+ export { escapeHTML, unescapeHTML } from './src/utilities/escape-html.js';
10
+ export { getCookie, setCookie } from './src/utilities/cookies.js';
11
+ export { linkedDates } from './src/utilities/linked-dates.js';
12
+ export { randomId } from './src/utilities/random-id.js';
13
+ export { relativeDate } from './src/utilities/relative-date.js';
8
14
  export { sentenceCase } from './src/utilities/sentence-case.js';
9
15
  export { stripTags } from './src/utilities/strip-tags.js';
10
16
  export { titleCase } from './src/utilities/title-case.js';
17
+ export { truncateString } from './src/utilities/truncate-string.js';
11
18
  export { ValidationError } from './src/utilities/validation-error-class.js';
12
19
 
13
20
  // components
14
- export { default as modalAlert } from './src/components/modal-alert/modal-alert.js';
21
+ export { modalAlert } from './src/components/modal-alert/modal-alert.js';
15
22
  export { dismissAlerts } from './src/components/dismiss-alerts.js';
16
23
  export { formResetSubmit } from './src/components/form-reset-submit.js';
17
24
  export { innerNav } from './src/components/inner-nav/inner-nav.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.6.1",
3
+ "version": "1.7.1",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -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
- export default function (params, custom_defaults = {}, marks = default_marks) {
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,19 @@
1
+ /**
2
+ * Disables all `[type=submit]` and `[type=button]` buttons on form submit.
3
+ *
4
+ * Applies to all `<form>` elements in the document except those with
5
+ * `data-disable-submit="false"`.
6
+ *
7
+ * @returns {void}
8
+ */
9
+ export function disableBtnsOnSubmit() {
10
+ document.querySelectorAll('form:not([data-disable-submit=false])').forEach( el => {
11
+ el.addEventListener('submit', () => {
12
+ /** @type {NodeListOf<HTMLButtonElement|HTMLInputElement>} */
13
+ const btns = el.querySelectorAll('[type=submit], [type=button]');
14
+ btns.forEach(btn => {
15
+ btn.disabled = true;
16
+ });
17
+ });
18
+ });
19
+ }
@@ -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,13 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Generates a random ID string.
5
+ * @returns {string} Base64-encoded random string
6
+ */
7
+ export function randomId() {
8
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_';
9
+ const bytes = crypto.getRandomValues(new Uint8Array(12));
10
+ return chars[bytes[0] % 52] // first item is from first 52 chars (A-Za-z)
11
+ + Array.from(bytes.slice(1), b => chars[b % 63]).join('');
12
+ }
13
+
@@ -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