@stimulus-plumbers/controllers 0.2.7 → 0.2.9

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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Map of E.164 country calling code → expected local digit count.
3
+ * Key: calling code string (without '+').
4
+ * Value: local number digit count (excluding the country code).
5
+ *
6
+ * Example: '1' → 10 means USA/Canada use a 10-digit local number
7
+ * (3-digit area code + 7-digit subscriber number).
8
+ *
9
+ * Extend this object to add formatting support for additional countries.
10
+ */
11
+ const DIALING_CODE_LOCAL_DIGITS = {
12
+ 1: 10, // USA, Canada, and NANP countries
13
+ };
14
+
15
+ /** Strips all non-digit characters */
16
+ const STRIP_NON_DIGITS = /\D/g;
17
+
18
+ /** Matches an E.164 international phone number: + followed by 7–15 digits */
19
+ const E164_PATTERN = /^\+\d{7,15}$/;
20
+
21
+ export const PhoneFormatter = {
22
+ /**
23
+ * Converts raw input to canonical form.
24
+ * If input starts with '+', produces E.164 (+digits); otherwise strips to digits only.
25
+ * e.g. '(555) 123-4567' → '5551234567', '+1 555 123 4567' → '+15551234567'
26
+ * @param {string} raw - Raw input (may contain spaces, dashes, parentheses, etc.)
27
+ * @returns {string} Canonical string, or '' for non-string input
28
+ */
29
+ normalize(raw) {
30
+ if (typeof raw !== 'string') return '';
31
+ const hasPlus = raw.trimStart().startsWith('+');
32
+ const digits = raw.replace(STRIP_NON_DIGITS, '');
33
+ return hasPlus ? `+${digits}` : digits;
34
+ },
35
+
36
+ /**
37
+ * Validates the canonical phone number.
38
+ * Accepts E.164 format or a local number whose digit count appears in DIALING_CODE_LOCAL_DIGITS.
39
+ * @param {string} value - Canonical value from normalize()
40
+ * @returns {boolean}
41
+ */
42
+ validate(value) {
43
+ if (typeof value !== 'string') return false;
44
+ if (E164_PATTERN.test(value)) return true;
45
+ const digits = value.replace(STRIP_NON_DIGITS, '');
46
+ return Object.values(DIALING_CODE_LOCAL_DIGITS).includes(digits.length);
47
+ },
48
+
49
+ /**
50
+ * Formats a canonical phone number for display.
51
+ * Recognises numbers matching entries in DIALING_CODE_LOCAL_DIGITS.
52
+ * e.g. '5551234567' → '(555) 123-4567', '+15551234567' → '+1 (555) 123-4567'
53
+ * Returns value unchanged for unrecognised international numbers.
54
+ * @param {string} value - Canonical value from normalize()
55
+ * @returns {string} Formatted display string, or '' for non-string input
56
+ */
57
+ format(value) {
58
+ if (typeof value !== 'string') return '';
59
+ const digits = value.replace(STRIP_NON_DIGITS, '');
60
+ for (const [code, localDigits] of Object.entries(DIALING_CODE_LOCAL_DIGITS)) {
61
+ if (digits.length === localDigits) {
62
+ // Local number without country code prefix
63
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
64
+ }
65
+ const totalWithCode = localDigits + code.length;
66
+ if (digits.length === totalWithCode && digits.startsWith(code)) {
67
+ const local = digits.slice(code.length);
68
+ return `+${code} (${local.slice(0, 3)}) ${local.slice(3, 6)}-${local.slice(6)}`;
69
+ }
70
+ }
71
+ return value;
72
+ },
73
+ };
@@ -0,0 +1,10 @@
1
+ export const PlainFormatter = {
2
+ normalize(raw) {
3
+ if (typeof raw !== 'string') return '';
4
+ return raw;
5
+ },
6
+
7
+ validate() {
8
+ return true;
9
+ },
10
+ };
@@ -0,0 +1,62 @@
1
+ /** Matches a 24-hour time: HH:MM */
2
+ const H24_PATTERN = /^([01]?\d|2[0-3]):([0-5]\d)$/;
3
+
4
+ export const TimeFormatter = {
5
+ /**
6
+ * Converts raw input to canonical 24-hour form: HH:MM.
7
+ * Accepts HH:MM (24h) and h:mm AM/PM (12h).
8
+ * @param {string} raw
9
+ * @returns {string} "HH:MM" or "" for invalid input
10
+ */
11
+ normalize(raw) {
12
+ if (typeof raw !== 'string') return '';
13
+ const trimmed = raw.trim();
14
+
15
+ if (H24_PATTERN.test(trimmed)) {
16
+ const [h, m] = trimmed.split(':');
17
+ return `${String(parseInt(h, 10)).padStart(2, '0')}:${m}`;
18
+ }
19
+
20
+ // h:mm AM/PM
21
+ const ampm = trimmed.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
22
+ if (ampm) {
23
+ let h = parseInt(ampm[1], 10);
24
+ const m = ampm[2];
25
+ const period = ampm[3].toUpperCase();
26
+ if (period === 'AM') h = h === 12 ? 0 : h;
27
+ else h = h === 12 ? 12 : h + 12;
28
+ if (h > 23 || parseInt(m, 10) > 59) return '';
29
+ return `${String(h).padStart(2, '0')}:${m}`;
30
+ }
31
+
32
+ return '';
33
+ },
34
+
35
+ /**
36
+ * Validates that the value is a parseable time.
37
+ * @param {string} value
38
+ * @returns {boolean}
39
+ */
40
+ validate(value) {
41
+ return TimeFormatter.normalize(value) !== '';
42
+ },
43
+
44
+ /**
45
+ * Formats a canonical HH:MM value for display.
46
+ * @param {string} value - Canonical "HH:MM" from normalize()
47
+ * @param {Object} [opts={}]
48
+ * @param {string} [opts.format='h12'] - 'h12' or 'h24'
49
+ * @returns {string}
50
+ */
51
+ format(value, opts = {}) {
52
+ if (typeof value !== 'string') return '';
53
+ const match = value.match(/^(\d{2}):(\d{2})$/);
54
+ if (!match) return value;
55
+ const h = parseInt(match[1], 10);
56
+ const m = match[2];
57
+ if (opts.format === 'h24') return `${String(h).padStart(2, '0')}:${m}`;
58
+ const period = h < 12 ? 'AM' : 'PM';
59
+ const displayH = h % 12 || 12;
60
+ return `${displayH}:${m} ${period}`;
61
+ },
62
+ };
@@ -3,8 +3,10 @@
3
3
  */
4
4
 
5
5
  export { initCalendar } from './calendar';
6
+ export { initComboboxDropdown } from './combobox_dropdown';
6
7
  export { attachContentLoader } from './content_loader';
7
8
  export { attachDismisser } from './dismisser';
8
9
  export { attachFlipper } from './flipper';
10
+ export { attachFormatter } from './formatter';
9
11
  export { attachShifter } from './shifter';
10
12
  export { attachVisibility } from './visibility';