@sb1/indeks-web 0.17.0 → 0.18.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/CHANGELOG.md +46 -0
- package/dist/npm/index.d.ts +36 -2
- package/dist/npm/index.js +992 -17
- package/dist/npm/lib/components/combobox/IxCombobox.d.ts +1 -0
- package/dist/npm/lib/components/date-field/IxDateField.d.ts +67 -0
- package/dist/npm/lib/components/field/formats.d.ts +55 -7
- package/dist/npm/lib/components/phone-number-field/IxPhoneNumberField.d.ts +33 -0
- package/dist/npm/lib/components/phone-number-field/countries.d.ts +27 -0
- package/dist/npm/lib/components/progress-bar/IxProgressBar.d.ts +1 -0
- package/dist/npm/lib/utils/cn.d.ts +20 -0
- package/package.json +1 -1
package/dist/npm/index.js
CHANGED
|
@@ -287,7 +287,7 @@ var COLLATOR = new Intl.Collator("no", {
|
|
|
287
287
|
sensitivity: "base",
|
|
288
288
|
usage: "search"
|
|
289
289
|
});
|
|
290
|
-
var IS_TOUCH = typeof navigator !== "undefined" && /iphone|ipad|ipod|android/i.test(navigator.userAgent);
|
|
290
|
+
var IS_TOUCH$1 = typeof navigator !== "undefined" && /iphone|ipad|ipod|android/i.test(navigator.userAgent);
|
|
291
291
|
var IxCombobox = class extends HTMLElement {
|
|
292
292
|
constructor(..._args) {
|
|
293
293
|
super(..._args);
|
|
@@ -355,6 +355,9 @@ var IxCombobox = class extends HTMLElement {
|
|
|
355
355
|
this._arrowHint = null;
|
|
356
356
|
this._results = null;
|
|
357
357
|
}
|
|
358
|
+
focus(options) {
|
|
359
|
+
this._input?.focus(options);
|
|
360
|
+
}
|
|
358
361
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
359
362
|
if (!this.isConnected) return;
|
|
360
363
|
if (name === "disabled" || name === "readonly") this._syncControlState();
|
|
@@ -423,7 +426,7 @@ var IxCombobox = class extends HTMLElement {
|
|
|
423
426
|
if (this._toggle) {
|
|
424
427
|
this._toggle.type = "button";
|
|
425
428
|
this._toggle.setAttribute("tabindex", "-1");
|
|
426
|
-
if (IS_TOUCH) this._toggle.setAttribute("aria-hidden", "true");
|
|
429
|
+
if (IS_TOUCH$1) this._toggle.setAttribute("aria-hidden", "true");
|
|
427
430
|
}
|
|
428
431
|
if (this._noHits) {
|
|
429
432
|
this._noHitsText = (this.getAttribute("data-no-hits-text") || this._noHits.textContent || "").trim();
|
|
@@ -757,6 +760,7 @@ var IxCombobox = class extends HTMLElement {
|
|
|
757
760
|
}
|
|
758
761
|
}
|
|
759
762
|
_emitChange() {
|
|
763
|
+
this._select?.dispatchEvent(new Event("change", { bubbles: true }));
|
|
760
764
|
this.dispatchEvent(new CustomEvent("change", { bubbles: true }));
|
|
761
765
|
}
|
|
762
766
|
_syncControlState() {
|
|
@@ -898,13 +902,18 @@ function compileTokens(pattern) {
|
|
|
898
902
|
/**
|
|
899
903
|
* Lager en formatter fra en pattern-streng.
|
|
900
904
|
*
|
|
901
|
-
* `format` fyller slots med matchende tegn fra den rå verdien og setter inn
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
905
|
+
* `format` fyller slots med matchende tegn fra den rå verdien og setter inn en
|
|
906
|
+
* separator så snart gruppen foran er fylt OG resten av verdien er konsumert —
|
|
907
|
+
* separatoren dukker altså opp med én gang gruppen er komplett ("24" → "24.",
|
|
908
|
+
* "2412" → "24.12."), ikke først når neste siffer skrives. En separator legges
|
|
909
|
+
* aldri til for en halvfylt gruppe, og heller ikke når vi stoppet fordi neste
|
|
910
|
+
* tegn ikke passet sloten (da ville vi doblet separatoren ved innliming av
|
|
911
|
+
* allerede formatert tekst). Tegn som ikke passer neste slot — enten fordi
|
|
912
|
+
* brukeren skrev flere tegn enn
|
|
913
|
+
* patternet har plasser, eller skrev noe som ikke hører hjemme (bokstav i et
|
|
914
|
+
* sifferfelt) — droppes IKKE: den ledende delen som passer formateres, og resten
|
|
915
|
+
* legges uformatert på til slutt i samme rekkefølge brukeren skrev. Alt brukeren
|
|
916
|
+
* skriver forblir synlig.
|
|
908
917
|
*
|
|
909
918
|
* `parse` er tapsfri: den fjerner kun separator-tegnene patternet definerer
|
|
910
919
|
* (literal-tokens) og beholder alt annet i rekkefølge — ingen cap på antall
|
|
@@ -935,11 +944,74 @@ function createPatternFormatter(pattern) {
|
|
|
935
944
|
pendingLiterals = "";
|
|
936
945
|
i++;
|
|
937
946
|
}
|
|
938
|
-
|
|
947
|
+
const trailing = i >= chars.length ? pendingLiterals : "";
|
|
948
|
+
return out + trailing + chars.slice(i).join("");
|
|
939
949
|
}
|
|
940
950
|
};
|
|
941
951
|
}
|
|
942
952
|
/**
|
|
953
|
+
* Kanonisk dato-parser — den ENE sannheten for hvordan en dato-streng tolkes.
|
|
954
|
+
* Både ix-field sin synlige formatering og IxDateField sin ISO-utleding går
|
|
955
|
+
* gjennom denne, så de aldri kan drifte fra hverandre.
|
|
956
|
+
*
|
|
957
|
+
* Komplett dato → nullutfylte deler; ufullstendig/tvetydig → `null`.
|
|
958
|
+
*
|
|
959
|
+
* - Med punktum: nøyaktig 3 rent-numeriske segmenter, dag/måned 1-2 sifre,
|
|
960
|
+
* år 2 eller 4 sifre. Dag/måned nullutfylles; 2-sifret år utvides til `20xx`
|
|
961
|
+
* (`"26"` → `"2026"`). `"1.1.2026"` og `"1.1.26"` → begge 01/01/2026.
|
|
962
|
+
* - Uten punktum: nøyaktig 8 sifre tolkes som ddmmåååå (`"01012026"`). Annen
|
|
963
|
+
* lengde uten skilletegn (f.eks. `"112026"`) er tvetydig — vi kan ikke vite
|
|
964
|
+
* hvor grensene går uten punktum — og regnes ufullstendig → `null`.
|
|
965
|
+
* - Bokstaver, ekstra punktum eller feil segmentantall → `null`.
|
|
966
|
+
*/
|
|
967
|
+
function parseDate(display) {
|
|
968
|
+
const trimmed = display.trim();
|
|
969
|
+
if (trimmed.includes(".")) {
|
|
970
|
+
const segments = trimmed.split(".");
|
|
971
|
+
if (segments.length !== 3) return null;
|
|
972
|
+
const [day, month, year] = segments;
|
|
973
|
+
if (!/^\d{1,2}$/.test(day) || !/^\d{1,2}$/.test(month) || !/^(\d{2}|\d{4})$/.test(year)) return null;
|
|
974
|
+
const fullYear = year.length === 2 ? `20${year}` : year;
|
|
975
|
+
return {
|
|
976
|
+
day: day.padStart(2, "0"),
|
|
977
|
+
month: month.padStart(2, "0"),
|
|
978
|
+
year: fullYear
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
if (/^\d{8}$/.test(trimmed)) return {
|
|
982
|
+
day: trimmed.slice(0, 2),
|
|
983
|
+
month: trimmed.slice(2, 4),
|
|
984
|
+
year: trimmed.slice(4, 8)
|
|
985
|
+
};
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
/** Komplett dato → visning `dd.mm.åååå` (nullutfylt); ellers strengen verbatim. */
|
|
989
|
+
function normalizeDateDisplay(display) {
|
|
990
|
+
const parts = parseDate(display);
|
|
991
|
+
return parts ? `${parts.day}.${parts.month}.${parts.year}` : display;
|
|
992
|
+
}
|
|
993
|
+
/** Komplett dato → ISO `åååå-mm-dd`; ellers tom streng. */
|
|
994
|
+
function isoFromDate(display) {
|
|
995
|
+
const parts = parseDate(display);
|
|
996
|
+
return parts ? `${parts.year}-${parts.month}-${parts.day}` : "";
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Formatter for datofelt. `parse` er identitet — punktumet er en meningsfull
|
|
1000
|
+
* tastet grense, ikke en separator å fjerne (trivielt tapsfri). `format`
|
|
1001
|
+
* nullutfyller til `dd.mm.åååå` når datoen er komplett, ellers vises alt verbatim.
|
|
1002
|
+
*
|
|
1003
|
+
* Invariant-unntak: `parse(format(raw)) === raw` gjelder IKKE her, fordi
|
|
1004
|
+
* nullutfylling (`"1"` → `"01"`) ikke er reversibel. Blur-syklusen krever i
|
|
1005
|
+
* stedet at `format` er idempotent på allerede-normaliserte verdier —
|
|
1006
|
+
* `format("01.01.2026") === "01.01.2026"` — som holder.
|
|
1007
|
+
*/
|
|
1008
|
+
function createDateFormatter() {
|
|
1009
|
+
return {
|
|
1010
|
+
parse: (display) => display,
|
|
1011
|
+
format: (raw) => normalizeDateDisplay(raw)
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
943
1015
|
* Beløp er ikke en fast-lengde pattern-maske: lengden varierer og tusenskillet
|
|
944
1016
|
* forskyver seg mens man skriver. Derfor en egen formatter.
|
|
945
1017
|
*
|
|
@@ -1001,6 +1073,19 @@ function registerFormat(name, formatter) {
|
|
|
1001
1073
|
function resolveFormat(name) {
|
|
1002
1074
|
return registry.get(name);
|
|
1003
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Navnene på de innebygde formatvariantene. Eksporteres slik at typelaget
|
|
1078
|
+
* (indeks-react dupliserer denne listen for autocomplete på `format`) kan holdes
|
|
1079
|
+
* i synk via en sync-test — React importerer ikke web-runtime ellers.
|
|
1080
|
+
*/
|
|
1081
|
+
var BUILTIN_FORMAT_NAMES = [
|
|
1082
|
+
"phone",
|
|
1083
|
+
"amount",
|
|
1084
|
+
"account",
|
|
1085
|
+
"orgnr",
|
|
1086
|
+
"ssn",
|
|
1087
|
+
"date"
|
|
1088
|
+
];
|
|
1004
1089
|
registerFormat("phone", {
|
|
1005
1090
|
...createPatternFormatter("000 00 000"),
|
|
1006
1091
|
live: true
|
|
@@ -1009,10 +1094,7 @@ registerFormat("ssn", {
|
|
|
1009
1094
|
...createPatternFormatter("000000 00000"),
|
|
1010
1095
|
live: true
|
|
1011
1096
|
});
|
|
1012
|
-
registerFormat("date",
|
|
1013
|
-
...createPatternFormatter("00.00.0000"),
|
|
1014
|
-
live: true
|
|
1015
|
-
});
|
|
1097
|
+
registerFormat("date", createDateFormatter());
|
|
1016
1098
|
registerFormat("account", {
|
|
1017
1099
|
...createPatternFormatter("0000 00 00000"),
|
|
1018
1100
|
live: true
|
|
@@ -1029,6 +1111,216 @@ registerFormat("amount", {
|
|
|
1029
1111
|
live: true
|
|
1030
1112
|
});
|
|
1031
1113
|
//#endregion
|
|
1114
|
+
//#region lib/components/date-field/IxDateField.ts
|
|
1115
|
+
/**
|
|
1116
|
+
* IxDateField — datofelt: formatert tekst-input (dd.mm.åååå) + native OS-kalender.
|
|
1117
|
+
*
|
|
1118
|
+
* ## Designfilosofi
|
|
1119
|
+
*
|
|
1120
|
+
* Hybrid av to mønstre. `ix-date-field` ligger NØSTET inne i `ix-field` (som
|
|
1121
|
+
* `ix-combobox`), og `ix-field` gjør ARIA-limet: kobler label↔input,
|
|
1122
|
+
* aria-describedby, feilmelding, OG formateringen (`data-format="date"` →
|
|
1123
|
+
* normaliserer til dd.mm.åååå på blur; brukeren kan taste `1.1.2026` like gjerne
|
|
1124
|
+
* som `01.01.2026`). `ix-date-field` dupliserer ingenting av dette. Dens ENESTE
|
|
1125
|
+
* jobb er å gjøre feltet til et datofelt:
|
|
1126
|
+
*
|
|
1127
|
+
* 1. GENERERE to instrumentelle elementer inn i `.ix-text-field` (idempotent):
|
|
1128
|
+
* - `.ix-date-field__toggle` kalenderknapp (CSS gir ikonet)
|
|
1129
|
+
* - `.ix-date-field__native` overlagt gjennomsiktig `<input type="date">`
|
|
1130
|
+
* som bærer ISO-verdien, gir min/max-validering og åpner OS-kalenderen
|
|
1131
|
+
* 2. To-veis synk mellom den synlige dd.mm.åååå-inputen og den native ISO-inputen.
|
|
1132
|
+
* 3. Speile min/max/disabled/readonly ned til den native inputen.
|
|
1133
|
+
* 4. Knapp-klikk → `native.showPicker?.()`; tap/klikk treffer uansett den
|
|
1134
|
+
* gjennomsiktige native-inputen som ligger over knappen (mobil).
|
|
1135
|
+
*
|
|
1136
|
+
* ## Verdi-kontrakt
|
|
1137
|
+
*
|
|
1138
|
+
* Utad er verdien ISO (`åååå-mm-dd`): den native inputen bærer den, `change`
|
|
1139
|
+
* emitter den, og `name` settes på den native inputen så form-innsending gir ISO.
|
|
1140
|
+
* Den synlige inputen viser `dd.mm.åååå` og har bevisst INGEN `name` (ville
|
|
1141
|
+
* kollidert med ix-field sin formaterings-speiling ved innsending).
|
|
1142
|
+
*
|
|
1143
|
+
* ## Delelementer (light DOM)
|
|
1144
|
+
* - .ix-text-field > input → synlig dd.mm.åååå-input (forfatteren)
|
|
1145
|
+
* - .ix-date-field__toggle → kalenderknapp (WC genererer)
|
|
1146
|
+
* - .ix-date-field__native (input[date]) → skjult ISO-input (WC genererer)
|
|
1147
|
+
*
|
|
1148
|
+
* @example
|
|
1149
|
+
* <ix-field>
|
|
1150
|
+
* <label>Fødselsdato</label>
|
|
1151
|
+
* <span data-field="description">dd.mm.åååå</span>
|
|
1152
|
+
* <ix-date-field data-open-label="Åpne kalender" name="fodt">
|
|
1153
|
+
* <div class="ix-text-field">
|
|
1154
|
+
* <input inputmode="numeric" data-format="date" />
|
|
1155
|
+
* </div>
|
|
1156
|
+
* </ix-date-field>
|
|
1157
|
+
* <span data-field="error"></span>
|
|
1158
|
+
* </ix-field>
|
|
1159
|
+
*/
|
|
1160
|
+
var IS_TOUCH = typeof navigator !== "undefined" && /iphone|ipad|ipod|android/i.test(navigator.userAgent);
|
|
1161
|
+
function digitsFromIso(iso) {
|
|
1162
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
1163
|
+
if (!match) return "";
|
|
1164
|
+
const [, year, month, day] = match;
|
|
1165
|
+
return `${day}${month}${year}`;
|
|
1166
|
+
}
|
|
1167
|
+
function displayFromDigits(digits) {
|
|
1168
|
+
return `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4, 8)}`;
|
|
1169
|
+
}
|
|
1170
|
+
var IxDateField = class extends HTMLElement {
|
|
1171
|
+
constructor(..._args) {
|
|
1172
|
+
super(..._args);
|
|
1173
|
+
_defineProperty(this, "_input", null);
|
|
1174
|
+
_defineProperty(this, "_toggle", null);
|
|
1175
|
+
_defineProperty(this, "_native", null);
|
|
1176
|
+
_defineProperty(this, "_syncing", false);
|
|
1177
|
+
_defineProperty(this, "_lastEmitted", "");
|
|
1178
|
+
_defineProperty(this, "_onVisibleInput", null);
|
|
1179
|
+
_defineProperty(this, "_onNativeInput", null);
|
|
1180
|
+
_defineProperty(this, "_onToggleClick", null);
|
|
1181
|
+
}
|
|
1182
|
+
static get observedAttributes() {
|
|
1183
|
+
return [
|
|
1184
|
+
"min",
|
|
1185
|
+
"max",
|
|
1186
|
+
"disabled",
|
|
1187
|
+
"readonly",
|
|
1188
|
+
"value",
|
|
1189
|
+
"name",
|
|
1190
|
+
"data-open-label"
|
|
1191
|
+
];
|
|
1192
|
+
}
|
|
1193
|
+
connectedCallback() {
|
|
1194
|
+
this._wire();
|
|
1195
|
+
}
|
|
1196
|
+
disconnectedCallback() {
|
|
1197
|
+
if (this._input && this._onVisibleInput) this._input.removeEventListener("input", this._onVisibleInput);
|
|
1198
|
+
if (this._native && this._onNativeInput) {
|
|
1199
|
+
this._native.removeEventListener("input", this._onNativeInput);
|
|
1200
|
+
this._native.removeEventListener("change", this._onNativeInput);
|
|
1201
|
+
}
|
|
1202
|
+
if (this._toggle && this._onToggleClick) this._toggle.removeEventListener("click", this._onToggleClick);
|
|
1203
|
+
this._onVisibleInput = null;
|
|
1204
|
+
this._onNativeInput = null;
|
|
1205
|
+
this._onToggleClick = null;
|
|
1206
|
+
this._input = null;
|
|
1207
|
+
this._toggle = null;
|
|
1208
|
+
this._native = null;
|
|
1209
|
+
}
|
|
1210
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
1211
|
+
if (!this.isConnected) return;
|
|
1212
|
+
if (name === "min" || name === "max" || name === "disabled" || name === "readonly") this._syncControlState();
|
|
1213
|
+
else if (name === "name") {
|
|
1214
|
+
if (this._native) this._native.name = newValue ?? "";
|
|
1215
|
+
} else if (name === "data-open-label") this._syncToggleLabel();
|
|
1216
|
+
else if (name === "value") this._seedFromIso(newValue ?? "");
|
|
1217
|
+
}
|
|
1218
|
+
_wire() {
|
|
1219
|
+
const field = this.querySelector(".ix-text-field");
|
|
1220
|
+
if (!field) return;
|
|
1221
|
+
this._input = field.querySelector("input:not(.ix-date-field__native)");
|
|
1222
|
+
if (!this._input) return;
|
|
1223
|
+
this._native = field.querySelector("input.ix-date-field__native");
|
|
1224
|
+
if (!this._native) {
|
|
1225
|
+
const native = document.createElement("input");
|
|
1226
|
+
native.type = "date";
|
|
1227
|
+
native.className = "ix-date-field__native";
|
|
1228
|
+
native.tabIndex = -1;
|
|
1229
|
+
native.setAttribute("aria-hidden", "true");
|
|
1230
|
+
field.appendChild(native);
|
|
1231
|
+
this._native = native;
|
|
1232
|
+
}
|
|
1233
|
+
this._toggle = field.querySelector("button.ix-date-field__toggle");
|
|
1234
|
+
if (!this._toggle) {
|
|
1235
|
+
const toggle = document.createElement("button");
|
|
1236
|
+
toggle.type = "button";
|
|
1237
|
+
toggle.className = "ix-date-field__toggle";
|
|
1238
|
+
field.insertBefore(toggle, this._native);
|
|
1239
|
+
this._toggle = toggle;
|
|
1240
|
+
}
|
|
1241
|
+
const hostName = this.getAttribute("name");
|
|
1242
|
+
const visibleName = this._input.getAttribute("name");
|
|
1243
|
+
const name = hostName ?? visibleName ?? "";
|
|
1244
|
+
if (name) this._native.name = name;
|
|
1245
|
+
if (visibleName) this._input.removeAttribute("name");
|
|
1246
|
+
this.toggleAttribute("data-touch", IS_TOUCH);
|
|
1247
|
+
this._syncToggleLabel();
|
|
1248
|
+
this._syncControlState();
|
|
1249
|
+
this._onVisibleInput = () => this._handleVisibleInput();
|
|
1250
|
+
this._input.addEventListener("input", this._onVisibleInput);
|
|
1251
|
+
this._onNativeInput = (e) => this._handleNativeInput(e);
|
|
1252
|
+
this._native.addEventListener("input", this._onNativeInput);
|
|
1253
|
+
this._native.addEventListener("change", this._onNativeInput);
|
|
1254
|
+
this._onToggleClick = () => this._openPicker();
|
|
1255
|
+
this._toggle.addEventListener("click", this._onToggleClick);
|
|
1256
|
+
const initialIso = this.getAttribute("value");
|
|
1257
|
+
if (initialIso) this._seedFromIso(initialIso);
|
|
1258
|
+
else if (this._input.value) {
|
|
1259
|
+
this._syncing = true;
|
|
1260
|
+
this._native.value = isoFromDate(this._input.value);
|
|
1261
|
+
this._syncing = false;
|
|
1262
|
+
}
|
|
1263
|
+
this._lastEmitted = this._native.value;
|
|
1264
|
+
}
|
|
1265
|
+
_handleVisibleInput() {
|
|
1266
|
+
if (this._syncing || !this._input || !this._native) return;
|
|
1267
|
+
this._syncing = true;
|
|
1268
|
+
this._native.value = isoFromDate(this._input.value);
|
|
1269
|
+
this._syncing = false;
|
|
1270
|
+
this._emitIfChanged();
|
|
1271
|
+
}
|
|
1272
|
+
_handleNativeInput(e) {
|
|
1273
|
+
e.stopPropagation();
|
|
1274
|
+
if (this._syncing || !this._input || !this._native) return;
|
|
1275
|
+
const digits = digitsFromIso(this._native.value);
|
|
1276
|
+
this._syncing = true;
|
|
1277
|
+
this._input.value = digits ? displayFromDigits(digits) : "";
|
|
1278
|
+
this._input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1279
|
+
this._syncing = false;
|
|
1280
|
+
this._emitIfChanged();
|
|
1281
|
+
}
|
|
1282
|
+
_emitIfChanged() {
|
|
1283
|
+
const iso = this._native?.value ?? "";
|
|
1284
|
+
if (iso === this._lastEmitted) return;
|
|
1285
|
+
this._lastEmitted = iso;
|
|
1286
|
+
this.dispatchEvent(new CustomEvent("change", { bubbles: true }));
|
|
1287
|
+
}
|
|
1288
|
+
_seedFromIso(iso) {
|
|
1289
|
+
if (!this._native || !this._input) return;
|
|
1290
|
+
this._syncing = true;
|
|
1291
|
+
this._native.value = iso;
|
|
1292
|
+
const digits = digitsFromIso(this._native.value);
|
|
1293
|
+
this._input.value = digits ? displayFromDigits(digits) : "";
|
|
1294
|
+
this._syncing = false;
|
|
1295
|
+
this._emitIfChanged();
|
|
1296
|
+
}
|
|
1297
|
+
_openPicker() {
|
|
1298
|
+
try {
|
|
1299
|
+
this._native?.showPicker();
|
|
1300
|
+
} catch {}
|
|
1301
|
+
}
|
|
1302
|
+
_syncControlState() {
|
|
1303
|
+
if (!this._native) return;
|
|
1304
|
+
this._native.min = this.getAttribute("min") ?? "";
|
|
1305
|
+
this._native.max = this.getAttribute("max") ?? "";
|
|
1306
|
+
this._native.disabled = this.hasAttribute("disabled");
|
|
1307
|
+
this._native.readOnly = this.hasAttribute("readonly");
|
|
1308
|
+
}
|
|
1309
|
+
_syncToggleLabel() {
|
|
1310
|
+
if (!this._toggle) return;
|
|
1311
|
+
const label = this.getAttribute("data-open-label");
|
|
1312
|
+
if (label) this._toggle.setAttribute("aria-label", label);
|
|
1313
|
+
else this._toggle.removeAttribute("aria-label");
|
|
1314
|
+
if (IS_TOUCH) {
|
|
1315
|
+
this._toggle.setAttribute("aria-hidden", "true");
|
|
1316
|
+
this._toggle.tabIndex = -1;
|
|
1317
|
+
} else {
|
|
1318
|
+
this._toggle.removeAttribute("aria-hidden");
|
|
1319
|
+
this._toggle.tabIndex = 0;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
//#endregion
|
|
1032
1324
|
//#region lib/components/field/IxField.ts
|
|
1033
1325
|
/**
|
|
1034
1326
|
* IxField — tilgjengelighetslim mellom label, input og hjelpetekster.
|
|
@@ -1336,7 +1628,7 @@ var IxField = class extends HTMLElement {
|
|
|
1336
1628
|
listeners.push([type, fn]);
|
|
1337
1629
|
};
|
|
1338
1630
|
if (formatter.live) {
|
|
1339
|
-
on("input", () => this._maskInPlace());
|
|
1631
|
+
on("input", (e) => this._maskInPlace(e));
|
|
1340
1632
|
control.value = formatter.format(initialRaw);
|
|
1341
1633
|
} else {
|
|
1342
1634
|
on("focus", () => {
|
|
@@ -1360,7 +1652,7 @@ var IxField = class extends HTMLElement {
|
|
|
1360
1652
|
this._activeControl = null;
|
|
1361
1653
|
};
|
|
1362
1654
|
}
|
|
1363
|
-
_maskInPlace() {
|
|
1655
|
+
_maskInPlace(event) {
|
|
1364
1656
|
const formatter = this._activeFormatter;
|
|
1365
1657
|
const control = this._activeControl;
|
|
1366
1658
|
if (!formatter || !control) return;
|
|
@@ -1375,6 +1667,7 @@ var IxField = class extends HTMLElement {
|
|
|
1375
1667
|
newCaret = i;
|
|
1376
1668
|
break;
|
|
1377
1669
|
}
|
|
1670
|
+
if (!(event?.inputType?.startsWith("delete") ?? false)) while (newCaret < next.length && !formatter.parse(next[newCaret])) newCaret++;
|
|
1378
1671
|
if (next !== display) control.value = next;
|
|
1379
1672
|
control.setSelectionRange(newCaret, newCaret);
|
|
1380
1673
|
if (this._formatMirror) this._formatMirror.value = raw;
|
|
@@ -1641,6 +1934,669 @@ var IxIcon = class extends HTMLElement {
|
|
|
1641
1934
|
}
|
|
1642
1935
|
};
|
|
1643
1936
|
//#endregion
|
|
1937
|
+
//#region lib/components/phone-number-field/countries.ts
|
|
1938
|
+
var NORDIC = /* @__PURE__ */ new Set([
|
|
1939
|
+
"SE",
|
|
1940
|
+
"DK",
|
|
1941
|
+
"FI",
|
|
1942
|
+
"IS"
|
|
1943
|
+
]);
|
|
1944
|
+
var COUNTRIES = [
|
|
1945
|
+
{
|
|
1946
|
+
iso: "NO",
|
|
1947
|
+
code: "47",
|
|
1948
|
+
name: {
|
|
1949
|
+
nb: "Norge",
|
|
1950
|
+
nn: "Noreg",
|
|
1951
|
+
en: "Norway"
|
|
1952
|
+
}
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
iso: "SE",
|
|
1956
|
+
code: "46",
|
|
1957
|
+
name: {
|
|
1958
|
+
nb: "Sverige",
|
|
1959
|
+
nn: "Sverige",
|
|
1960
|
+
en: "Sweden"
|
|
1961
|
+
}
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
iso: "DK",
|
|
1965
|
+
code: "45",
|
|
1966
|
+
name: {
|
|
1967
|
+
nb: "Danmark",
|
|
1968
|
+
nn: "Danmark",
|
|
1969
|
+
en: "Denmark"
|
|
1970
|
+
}
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
iso: "FI",
|
|
1974
|
+
code: "358",
|
|
1975
|
+
name: {
|
|
1976
|
+
nb: "Finland",
|
|
1977
|
+
nn: "Finland",
|
|
1978
|
+
en: "Finland"
|
|
1979
|
+
}
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
iso: "IS",
|
|
1983
|
+
code: "354",
|
|
1984
|
+
name: {
|
|
1985
|
+
nb: "Island",
|
|
1986
|
+
nn: "Island",
|
|
1987
|
+
en: "Iceland"
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
iso: "BE",
|
|
1992
|
+
code: "32",
|
|
1993
|
+
name: {
|
|
1994
|
+
nb: "Belgia",
|
|
1995
|
+
nn: "Belgia",
|
|
1996
|
+
en: "Belgium"
|
|
1997
|
+
}
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
iso: "EE",
|
|
2001
|
+
code: "372",
|
|
2002
|
+
name: {
|
|
2003
|
+
nb: "Estland",
|
|
2004
|
+
nn: "Estland",
|
|
2005
|
+
en: "Estonia"
|
|
2006
|
+
}
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
iso: "FR",
|
|
2010
|
+
code: "33",
|
|
2011
|
+
name: {
|
|
2012
|
+
nb: "Frankrike",
|
|
2013
|
+
nn: "Frankrike",
|
|
2014
|
+
en: "France"
|
|
2015
|
+
}
|
|
2016
|
+
},
|
|
2017
|
+
{
|
|
2018
|
+
iso: "GR",
|
|
2019
|
+
code: "30",
|
|
2020
|
+
name: {
|
|
2021
|
+
nb: "Hellas",
|
|
2022
|
+
nn: "Hellas",
|
|
2023
|
+
en: "Greece"
|
|
2024
|
+
}
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
iso: "IN",
|
|
2028
|
+
code: "91",
|
|
2029
|
+
name: {
|
|
2030
|
+
nb: "India",
|
|
2031
|
+
nn: "India",
|
|
2032
|
+
en: "India"
|
|
2033
|
+
}
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
iso: "IE",
|
|
2037
|
+
code: "353",
|
|
2038
|
+
name: {
|
|
2039
|
+
nb: "Irland",
|
|
2040
|
+
nn: "Irland",
|
|
2041
|
+
en: "Ireland"
|
|
2042
|
+
}
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
iso: "IT",
|
|
2046
|
+
code: "39",
|
|
2047
|
+
name: {
|
|
2048
|
+
nb: "Italia",
|
|
2049
|
+
nn: "Italia",
|
|
2050
|
+
en: "Italy"
|
|
2051
|
+
}
|
|
2052
|
+
},
|
|
2053
|
+
{
|
|
2054
|
+
iso: "HR",
|
|
2055
|
+
code: "385",
|
|
2056
|
+
name: {
|
|
2057
|
+
nb: "Kroatia",
|
|
2058
|
+
nn: "Kroatia",
|
|
2059
|
+
en: "Croatia"
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
iso: "LV",
|
|
2064
|
+
code: "371",
|
|
2065
|
+
name: {
|
|
2066
|
+
nb: "Latvia",
|
|
2067
|
+
nn: "Latvia",
|
|
2068
|
+
en: "Latvia"
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
iso: "LT",
|
|
2073
|
+
code: "370",
|
|
2074
|
+
name: {
|
|
2075
|
+
nb: "Litauen",
|
|
2076
|
+
nn: "Litauen",
|
|
2077
|
+
en: "Lithuania"
|
|
2078
|
+
}
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
iso: "NL",
|
|
2082
|
+
code: "31",
|
|
2083
|
+
name: {
|
|
2084
|
+
nb: "Nederland",
|
|
2085
|
+
nn: "Nederland",
|
|
2086
|
+
en: "Netherlands"
|
|
2087
|
+
}
|
|
2088
|
+
},
|
|
2089
|
+
{
|
|
2090
|
+
iso: "PL",
|
|
2091
|
+
code: "48",
|
|
2092
|
+
name: {
|
|
2093
|
+
nb: "Polen",
|
|
2094
|
+
nn: "Polen",
|
|
2095
|
+
en: "Poland"
|
|
2096
|
+
}
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
iso: "PT",
|
|
2100
|
+
code: "351",
|
|
2101
|
+
name: {
|
|
2102
|
+
nb: "Portugal",
|
|
2103
|
+
nn: "Portugal",
|
|
2104
|
+
en: "Portugal"
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
iso: "RO",
|
|
2109
|
+
code: "40",
|
|
2110
|
+
name: {
|
|
2111
|
+
nb: "Romania",
|
|
2112
|
+
nn: "Romania",
|
|
2113
|
+
en: "Romania"
|
|
2114
|
+
}
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
iso: "RU",
|
|
2118
|
+
code: "7",
|
|
2119
|
+
name: {
|
|
2120
|
+
nb: "Russland",
|
|
2121
|
+
nn: "Russland",
|
|
2122
|
+
en: "Russia"
|
|
2123
|
+
}
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
iso: "ES",
|
|
2127
|
+
code: "34",
|
|
2128
|
+
name: {
|
|
2129
|
+
nb: "Spania",
|
|
2130
|
+
nn: "Spania",
|
|
2131
|
+
en: "Spain"
|
|
2132
|
+
}
|
|
2133
|
+
},
|
|
2134
|
+
{
|
|
2135
|
+
iso: "GB",
|
|
2136
|
+
code: "44",
|
|
2137
|
+
name: {
|
|
2138
|
+
nb: "Storbritannia",
|
|
2139
|
+
nn: "Storbritannia",
|
|
2140
|
+
en: "United Kingdom"
|
|
2141
|
+
}
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
iso: "CH",
|
|
2145
|
+
code: "41",
|
|
2146
|
+
name: {
|
|
2147
|
+
nb: "Sveits",
|
|
2148
|
+
nn: "Sveits",
|
|
2149
|
+
en: "Switzerland"
|
|
2150
|
+
}
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
iso: "DE",
|
|
2154
|
+
code: "49",
|
|
2155
|
+
name: {
|
|
2156
|
+
nb: "Tyskland",
|
|
2157
|
+
nn: "Tyskland",
|
|
2158
|
+
en: "Germany"
|
|
2159
|
+
}
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
iso: "US",
|
|
2163
|
+
code: "1",
|
|
2164
|
+
name: {
|
|
2165
|
+
nb: "USA",
|
|
2166
|
+
nn: "USA",
|
|
2167
|
+
en: "United States"
|
|
2168
|
+
}
|
|
2169
|
+
},
|
|
2170
|
+
{
|
|
2171
|
+
iso: "AT",
|
|
2172
|
+
code: "43",
|
|
2173
|
+
name: {
|
|
2174
|
+
nb: "Østerrike",
|
|
2175
|
+
nn: "Austerrike",
|
|
2176
|
+
en: "Austria"
|
|
2177
|
+
}
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
iso: "TR",
|
|
2181
|
+
code: "90",
|
|
2182
|
+
name: {
|
|
2183
|
+
nb: "Tyrkia",
|
|
2184
|
+
nn: "Tyrkia",
|
|
2185
|
+
en: "Turkey"
|
|
2186
|
+
}
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
iso: "UA",
|
|
2190
|
+
code: "380",
|
|
2191
|
+
name: {
|
|
2192
|
+
nb: "Ukraina",
|
|
2193
|
+
nn: "Ukraina",
|
|
2194
|
+
en: "Ukraine"
|
|
2195
|
+
}
|
|
2196
|
+
},
|
|
2197
|
+
{
|
|
2198
|
+
iso: "CN",
|
|
2199
|
+
code: "86",
|
|
2200
|
+
name: {
|
|
2201
|
+
nb: "Kina",
|
|
2202
|
+
nn: "Kina",
|
|
2203
|
+
en: "China"
|
|
2204
|
+
}
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
iso: "JP",
|
|
2208
|
+
code: "81",
|
|
2209
|
+
name: {
|
|
2210
|
+
nb: "Japan",
|
|
2211
|
+
nn: "Japan",
|
|
2212
|
+
en: "Japan"
|
|
2213
|
+
}
|
|
2214
|
+
},
|
|
2215
|
+
{
|
|
2216
|
+
iso: "BR",
|
|
2217
|
+
code: "55",
|
|
2218
|
+
name: {
|
|
2219
|
+
nb: "Brasil",
|
|
2220
|
+
nn: "Brasil",
|
|
2221
|
+
en: "Brazil"
|
|
2222
|
+
}
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
iso: "AU",
|
|
2226
|
+
code: "61",
|
|
2227
|
+
name: {
|
|
2228
|
+
nb: "Australia",
|
|
2229
|
+
nn: "Australia",
|
|
2230
|
+
en: "Australia"
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
];
|
|
2234
|
+
/**
|
|
2235
|
+
* Bygger standard landliste for en gitt `locale`. Rekkefølge: Norge først,
|
|
2236
|
+
* deretter Norden (Sverige, Danmark, Finland, Island), deretter resten
|
|
2237
|
+
* alfabetisk etter lokalisert landnavn (locale-bevisst `Intl.Collator`, som
|
|
2238
|
+
* håndterer æ/ø/å).
|
|
2239
|
+
*
|
|
2240
|
+
* @default locale = 'nb'
|
|
2241
|
+
*/
|
|
2242
|
+
function getDefaultCountries(locale = "nb") {
|
|
2243
|
+
const collator = new Intl.Collator(locale);
|
|
2244
|
+
const toOption = (c) => ({
|
|
2245
|
+
value: c.code,
|
|
2246
|
+
label: `+${c.code}`,
|
|
2247
|
+
description: c.name[locale]
|
|
2248
|
+
});
|
|
2249
|
+
const norway = COUNTRIES.filter((c) => c.iso === "NO");
|
|
2250
|
+
const nordics = COUNTRIES.filter((c) => NORDIC.has(c.iso)).sort((a, b) => collator.compare(a.name[locale], b.name[locale]));
|
|
2251
|
+
const rest = COUNTRIES.filter((c) => c.iso !== "NO" && !NORDIC.has(c.iso)).sort((a, b) => collator.compare(a.name[locale], b.name[locale]));
|
|
2252
|
+
return [
|
|
2253
|
+
...norway,
|
|
2254
|
+
...nordics,
|
|
2255
|
+
...rest
|
|
2256
|
+
].map(toOption);
|
|
2257
|
+
}
|
|
2258
|
+
/** Normaliserer en `data-locale`-attributt til en gyldig `CountryLocale`. */
|
|
2259
|
+
function normalizeLocale(value) {
|
|
2260
|
+
return value === "nn" || value === "en" ? value : "nb";
|
|
2261
|
+
}
|
|
2262
|
+
//#endregion
|
|
2263
|
+
//#region lib/components/phone-number-field/IxPhoneNumberField.ts
|
|
2264
|
+
/**
|
|
2265
|
+
* IxPhoneNumberField — telefonnummer med landvelger.
|
|
2266
|
+
*
|
|
2267
|
+
* ## Designfilosofi
|
|
2268
|
+
*
|
|
2269
|
+
* Komponenten er en sammensatt gruppe: den binder sammen en <ix-combobox>
|
|
2270
|
+
* (landkode) og et <ix-field>-tekstfelt (nummer) under én felles label og én
|
|
2271
|
+
* felles valideringsmelding. Den reimplementerer verken combobox- eller
|
|
2272
|
+
* formaterings-oppførsel — <ix-combobox> og <ix-field> gjør fortsatt sin egen
|
|
2273
|
+
* interne ARIA-kabling.
|
|
2274
|
+
*
|
|
2275
|
+
* HTML/WC er primærkilden: forfatteren skriver minimal markup (en TOM
|
|
2276
|
+
* <ix-combobox> holder), og komponenten fyller landlista, setter forhåndsvalg
|
|
2277
|
+
* og stamper nummer-feltets standardattributter. React er et tynt lag som kun
|
|
2278
|
+
* videresender config som data-*-attributter.
|
|
2279
|
+
*
|
|
2280
|
+
* Speiler <ix-checkbox-group>/<ix-radio-group>-mønsteret, med disse forskjellene:
|
|
2281
|
+
* de to «kontrollene» er en <ix-combobox> og et nummer-<input> (ikke en liste av
|
|
2282
|
+
* like radio/checkbox), og disabled/readonly/required propageres til begge disse.
|
|
2283
|
+
*
|
|
2284
|
+
* ## Hva komponenten gjør
|
|
2285
|
+
*
|
|
2286
|
+
* 1. Setter role="group" på host slik at skjermlesere annonserer gruppen og
|
|
2287
|
+
* leser legend-teksten som gruppens tilgjengelige navn. (Det finnes ingen
|
|
2288
|
+
* egen rolle for et sammensatt felt — "group" er korrekt.)
|
|
2289
|
+
*
|
|
2290
|
+
* 2. Kobler legend via aria-labelledby — genererer ID på [data-field="legend"]
|
|
2291
|
+
* om nødvendig og peker host til den.
|
|
2292
|
+
*
|
|
2293
|
+
* 3. Setter aria-describedby på host med IDer til description og error
|
|
2294
|
+
* (description alltid først). Hopper over elementer som mangler.
|
|
2295
|
+
*
|
|
2296
|
+
* 4. Setter aria-live="polite" på [data-field="error"] og observerer
|
|
2297
|
+
* textContent-endringer: ikke-tomt innhold → aria-invalid="true" på host;
|
|
2298
|
+
* tomt → aria-invalid fjernes. aria-invalid settes kun på host — det er
|
|
2299
|
+
* gruppens validitet som telles.
|
|
2300
|
+
*
|
|
2301
|
+
* 5. Propagerer disabled/readonly/required til begge kontrollene. Egne
|
|
2302
|
+
* (per-kontroll) verdier bevares via WeakMaps slik at group-toggle ikke
|
|
2303
|
+
* overstyrer dem.
|
|
2304
|
+
*
|
|
2305
|
+
* 6. Fyller landvelgeren: injiserer .ix-combobox__option-markup i den tomme
|
|
2306
|
+
* <ix-combobox>-lista fra en innebygd, lokaliserbar landliste (eller en
|
|
2307
|
+
* egendefinert `data-countries`-JSON), og setter aria-selected på
|
|
2308
|
+
* forhåndsvalgt land (data-country-code / data-default-country-code).
|
|
2309
|
+
*
|
|
2310
|
+
* 7. Stamper nummer-feltets standardattributter (type=tel, inputmode=numeric,
|
|
2311
|
+
* autocomplete=tel-national, data-format=phone) om de mangler.
|
|
2312
|
+
*
|
|
2313
|
+
* 8. Utleder data-state (error/readonly/disabled) for CSS.
|
|
2314
|
+
*
|
|
2315
|
+
* 9. Lytter på childList slik at kontroller som React rendrer inn etter mount
|
|
2316
|
+
* (conditional rendering) fanges opp og re-wires.
|
|
2317
|
+
*
|
|
2318
|
+
* ## Ingen Shadow DOM
|
|
2319
|
+
*
|
|
2320
|
+
* Komponenten bruker light DOM slik at CSS fra @sb1/indeks-css når alle
|
|
2321
|
+
* delelementer direkte, og aria-koblingene (aria-labelledby/-describedby) ikke
|
|
2322
|
+
* krysser en shadow-grense.
|
|
2323
|
+
*
|
|
2324
|
+
* @example
|
|
2325
|
+
* <ix-phone-number-field data-locale="nb" data-default-country-code="47">
|
|
2326
|
+
* <span data-field="legend">Mobilnummer</span>
|
|
2327
|
+
* <span data-field="description">Vi bruker det kun til SMS-varsling</span>
|
|
2328
|
+
* <div data-field="items">
|
|
2329
|
+
* <ix-combobox data-field="country" class="ix-combobox" data-no-hits-text="Ingen treff">
|
|
2330
|
+
* <div class="ix-text-field">
|
|
2331
|
+
* <input class="ix-text-field__input" aria-label="Landkode" />
|
|
2332
|
+
* <button type="button" class="ix-combobox__toggle" aria-label="Vis land"></button>
|
|
2333
|
+
* </div>
|
|
2334
|
+
* <div class="ix-combobox__listbox" hidden></div>
|
|
2335
|
+
* </ix-combobox>
|
|
2336
|
+
* <ix-field data-field="number">
|
|
2337
|
+
* <div class="ix-text-field">
|
|
2338
|
+
* <input aria-label="Telefonnummer" />
|
|
2339
|
+
* </div>
|
|
2340
|
+
* </ix-field>
|
|
2341
|
+
* </div>
|
|
2342
|
+
* <span data-field="error"></span>
|
|
2343
|
+
* </ix-phone-number-field>
|
|
2344
|
+
*/
|
|
2345
|
+
var phoneNumberFieldCounter = 0;
|
|
2346
|
+
var IxPhoneNumberField = class extends HTMLElement {
|
|
2347
|
+
constructor(..._args) {
|
|
2348
|
+
super(..._args);
|
|
2349
|
+
_defineProperty(this, "_errorObserver", null);
|
|
2350
|
+
_defineProperty(this, "_childObserver", null);
|
|
2351
|
+
_defineProperty(this, "_instanceId", 0);
|
|
2352
|
+
_defineProperty(this, "_filling", false);
|
|
2353
|
+
_defineProperty(this, "_ownDisabled", /* @__PURE__ */ new WeakMap());
|
|
2354
|
+
_defineProperty(this, "_ownReadonly", /* @__PURE__ */ new WeakMap());
|
|
2355
|
+
_defineProperty(this, "_ownRequired", /* @__PURE__ */ new WeakMap());
|
|
2356
|
+
}
|
|
2357
|
+
static get observedAttributes() {
|
|
2358
|
+
return [
|
|
2359
|
+
"disabled",
|
|
2360
|
+
"readonly",
|
|
2361
|
+
"required",
|
|
2362
|
+
"data-locale",
|
|
2363
|
+
"data-countries",
|
|
2364
|
+
"data-country-code",
|
|
2365
|
+
"data-default-country-code"
|
|
2366
|
+
];
|
|
2367
|
+
}
|
|
2368
|
+
connectedCallback() {
|
|
2369
|
+
this._wire();
|
|
2370
|
+
}
|
|
2371
|
+
disconnectedCallback() {
|
|
2372
|
+
this._errorObserver?.disconnect();
|
|
2373
|
+
this._errorObserver = null;
|
|
2374
|
+
this._childObserver?.disconnect();
|
|
2375
|
+
this._childObserver = null;
|
|
2376
|
+
}
|
|
2377
|
+
attributeChangedCallback(attr, _oldValue, _newValue) {
|
|
2378
|
+
if (!this.isConnected) return;
|
|
2379
|
+
if (attr === "disabled" || attr === "readonly") this._syncHostStateToControls();
|
|
2380
|
+
else if (attr === "required") this._syncRequired();
|
|
2381
|
+
else if (attr === "data-locale" || attr === "data-countries") this._refillCombobox();
|
|
2382
|
+
else if (attr === "data-country-code" || attr === "data-default-country-code") this._applyPreselection();
|
|
2383
|
+
}
|
|
2384
|
+
_wire() {
|
|
2385
|
+
this.setAttribute("role", "group");
|
|
2386
|
+
this._instanceId = ++phoneNumberFieldCounter;
|
|
2387
|
+
const legend = this.querySelector(":scope > [data-field=\"legend\"]");
|
|
2388
|
+
if (legend) {
|
|
2389
|
+
if (!legend.id) legend.id = `ix-phone-number-field-legend-${this._instanceId}`;
|
|
2390
|
+
this.setAttribute("aria-labelledby", legend.id);
|
|
2391
|
+
}
|
|
2392
|
+
this._wireDescribedBy();
|
|
2393
|
+
this._fillCombobox();
|
|
2394
|
+
this._stampNumberDefaults();
|
|
2395
|
+
this._syncHostStateToControls();
|
|
2396
|
+
this._syncRequired();
|
|
2397
|
+
this._syncDataState();
|
|
2398
|
+
this._childObserver = new MutationObserver((mutations) => {
|
|
2399
|
+
if (this._filling) return;
|
|
2400
|
+
if (mutations.some((m) => Array.from(m.addedNodes).some((n) => this._containsControl(n)) || Array.from(m.removedNodes).some((n) => this._containsControl(n)))) {
|
|
2401
|
+
this._fillCombobox();
|
|
2402
|
+
this._stampNumberDefaults();
|
|
2403
|
+
this._syncHostStateToControls();
|
|
2404
|
+
this._syncRequired();
|
|
2405
|
+
}
|
|
2406
|
+
if (mutations.some((m) => Array.from(m.addedNodes).some((n) => this._containsGroupField(n)) || Array.from(m.removedNodes).some((n) => this._containsGroupField(n)))) this._wireDescribedBy();
|
|
2407
|
+
});
|
|
2408
|
+
this._childObserver.observe(this, {
|
|
2409
|
+
childList: true,
|
|
2410
|
+
subtree: true
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
_wireDescribedBy() {
|
|
2414
|
+
const description = this.querySelector(":scope > [data-field=\"description\"]");
|
|
2415
|
+
const error = this.querySelector(":scope > [data-field=\"error\"]");
|
|
2416
|
+
const describedBy = [];
|
|
2417
|
+
if (description) {
|
|
2418
|
+
if (!description.id) description.id = `ix-phone-number-field-desc-${this._instanceId}`;
|
|
2419
|
+
describedBy.push(description.id);
|
|
2420
|
+
}
|
|
2421
|
+
this._errorObserver?.disconnect();
|
|
2422
|
+
this._errorObserver = null;
|
|
2423
|
+
if (error) {
|
|
2424
|
+
if (!error.id) error.id = `ix-phone-number-field-error-${this._instanceId}`;
|
|
2425
|
+
describedBy.push(error.id);
|
|
2426
|
+
error.setAttribute("aria-live", "polite");
|
|
2427
|
+
this._errorObserver = new MutationObserver(() => {
|
|
2428
|
+
this._syncInvalid(error);
|
|
2429
|
+
});
|
|
2430
|
+
this._errorObserver.observe(error, {
|
|
2431
|
+
childList: true,
|
|
2432
|
+
characterData: true,
|
|
2433
|
+
subtree: true
|
|
2434
|
+
});
|
|
2435
|
+
this._syncInvalid(error);
|
|
2436
|
+
} else this.removeAttribute("aria-invalid");
|
|
2437
|
+
if (describedBy.length > 0) this.setAttribute("aria-describedby", describedBy.join(" "));
|
|
2438
|
+
else this.removeAttribute("aria-describedby");
|
|
2439
|
+
}
|
|
2440
|
+
_containsControl(node) {
|
|
2441
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
2442
|
+
const el = node;
|
|
2443
|
+
if (el.matches?.("ix-combobox, input")) return true;
|
|
2444
|
+
return !!el.querySelector?.("ix-combobox, input");
|
|
2445
|
+
}
|
|
2446
|
+
_containsGroupField(node) {
|
|
2447
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
2448
|
+
const el = node;
|
|
2449
|
+
const selector = "[data-field=\"description\"], [data-field=\"error\"]";
|
|
2450
|
+
if (el.matches?.(selector)) return true;
|
|
2451
|
+
return !!el.querySelector?.(selector);
|
|
2452
|
+
}
|
|
2453
|
+
_combobox() {
|
|
2454
|
+
return this.querySelector("ix-combobox");
|
|
2455
|
+
}
|
|
2456
|
+
_numberInput() {
|
|
2457
|
+
const numberField = this.querySelector("[data-field=\"number\"]");
|
|
2458
|
+
if (numberField) return numberField.querySelector("input");
|
|
2459
|
+
const combobox = this._combobox();
|
|
2460
|
+
return Array.from(this.querySelectorAll("input")).find((input) => !combobox?.contains(input)) ?? null;
|
|
2461
|
+
}
|
|
2462
|
+
_syncInvalid(error) {
|
|
2463
|
+
if (!!error.textContent?.trim()) this.setAttribute("aria-invalid", "true");
|
|
2464
|
+
else this.removeAttribute("aria-invalid");
|
|
2465
|
+
this._syncDataState();
|
|
2466
|
+
}
|
|
2467
|
+
_syncDataState() {
|
|
2468
|
+
const state = !!this.querySelector(":scope > [data-field=\"error\"]")?.textContent?.trim() ? "error" : this.hasAttribute("readonly") ? "readonly" : this.hasAttribute("disabled") ? "disabled" : null;
|
|
2469
|
+
if (state) this.setAttribute("data-state", state);
|
|
2470
|
+
else this.removeAttribute("data-state");
|
|
2471
|
+
}
|
|
2472
|
+
_syncHostStateToControls() {
|
|
2473
|
+
const isDisabled = this.hasAttribute("disabled");
|
|
2474
|
+
const isReadonly = this.hasAttribute("readonly");
|
|
2475
|
+
const combobox = this._combobox();
|
|
2476
|
+
if (combobox) this._applyState(combobox, isDisabled, isReadonly, "attribute");
|
|
2477
|
+
const input = this._numberInput();
|
|
2478
|
+
if (input) this._applyState(input, isDisabled, isReadonly, "property");
|
|
2479
|
+
this._syncDataState();
|
|
2480
|
+
}
|
|
2481
|
+
_syncRequired() {
|
|
2482
|
+
const on = this.hasAttribute("required");
|
|
2483
|
+
const comboboxInput = this._combobox()?.querySelector("input") ?? null;
|
|
2484
|
+
if (comboboxInput) this._applyFlag(comboboxInput, "required", on, this._ownRequired, "property");
|
|
2485
|
+
const numberInput = this._numberInput();
|
|
2486
|
+
if (numberInput) this._applyFlag(numberInput, "required", on, this._ownRequired, "property");
|
|
2487
|
+
}
|
|
2488
|
+
_applyState(control, isDisabled, isReadonly, mode) {
|
|
2489
|
+
this._applyFlag(control, "disabled", isDisabled, this._ownDisabled, mode);
|
|
2490
|
+
this._applyFlag(control, "readonly", isReadonly, this._ownReadonly, mode);
|
|
2491
|
+
}
|
|
2492
|
+
_applyFlag(control, flag, groupOn, ownStore, mode) {
|
|
2493
|
+
const prop = flag === "readonly" ? "readOnly" : flag;
|
|
2494
|
+
const read = () => mode === "attribute" ? control.hasAttribute(flag) : !!control[prop];
|
|
2495
|
+
const write = (value) => {
|
|
2496
|
+
if (mode === "attribute") if (value) control.setAttribute(flag, "");
|
|
2497
|
+
else control.removeAttribute(flag);
|
|
2498
|
+
else control[prop] = value;
|
|
2499
|
+
};
|
|
2500
|
+
if (groupOn) {
|
|
2501
|
+
if (!ownStore.has(control)) ownStore.set(control, read());
|
|
2502
|
+
write(true);
|
|
2503
|
+
} else if (ownStore.has(control)) {
|
|
2504
|
+
write(ownStore.get(control));
|
|
2505
|
+
ownStore.delete(control);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
_fillCombobox() {
|
|
2509
|
+
if (this._filling) return;
|
|
2510
|
+
const listbox = this._listbox();
|
|
2511
|
+
if (!listbox) return;
|
|
2512
|
+
if (listbox.querySelector(".ix-combobox__option")) return;
|
|
2513
|
+
const options = this._resolveCountryOptions();
|
|
2514
|
+
if (options.length === 0) return;
|
|
2515
|
+
const preselect = this._preselectValue();
|
|
2516
|
+
this._filling = true;
|
|
2517
|
+
try {
|
|
2518
|
+
const frag = document.createDocumentFragment();
|
|
2519
|
+
for (const opt of options) frag.appendChild(this._buildOption(opt, opt.value === preselect));
|
|
2520
|
+
listbox.setAttribute("data-ix-injected", "");
|
|
2521
|
+
listbox.appendChild(frag);
|
|
2522
|
+
} finally {
|
|
2523
|
+
this._filling = false;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
_refillCombobox() {
|
|
2527
|
+
const listbox = this._listbox();
|
|
2528
|
+
if (!listbox || !listbox.hasAttribute("data-ix-injected")) {
|
|
2529
|
+
this._fillCombobox();
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
this._filling = true;
|
|
2533
|
+
try {
|
|
2534
|
+
for (const opt of Array.from(listbox.querySelectorAll(".ix-combobox__option"))) opt.remove();
|
|
2535
|
+
} finally {
|
|
2536
|
+
this._filling = false;
|
|
2537
|
+
}
|
|
2538
|
+
listbox.removeAttribute("data-ix-injected");
|
|
2539
|
+
this._fillCombobox();
|
|
2540
|
+
}
|
|
2541
|
+
_applyPreselection() {
|
|
2542
|
+
const listbox = this._listbox();
|
|
2543
|
+
if (!listbox) return;
|
|
2544
|
+
const preselect = this._preselectValue();
|
|
2545
|
+
this._filling = true;
|
|
2546
|
+
try {
|
|
2547
|
+
for (const opt of Array.from(listbox.querySelectorAll(".ix-combobox__option"))) if (preselect !== null && opt.getAttribute("data-value") === preselect) opt.setAttribute("aria-selected", "true");
|
|
2548
|
+
else opt.removeAttribute("aria-selected");
|
|
2549
|
+
} finally {
|
|
2550
|
+
this._filling = false;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
_buildOption(opt, selected) {
|
|
2554
|
+
const el = document.createElement("div");
|
|
2555
|
+
el.className = "ix-combobox__option";
|
|
2556
|
+
el.setAttribute("data-value", opt.value);
|
|
2557
|
+
if (selected) el.setAttribute("aria-selected", "true");
|
|
2558
|
+
const check = document.createElement("span");
|
|
2559
|
+
check.className = "ix-combobox__option-check";
|
|
2560
|
+
check.setAttribute("aria-hidden", "true");
|
|
2561
|
+
el.appendChild(check);
|
|
2562
|
+
const label = document.createElement("span");
|
|
2563
|
+
label.className = "ix-combobox__option-label";
|
|
2564
|
+
label.textContent = opt.label;
|
|
2565
|
+
el.appendChild(label);
|
|
2566
|
+
if (opt.description) {
|
|
2567
|
+
const desc = document.createElement("span");
|
|
2568
|
+
desc.className = "ix-combobox__option-description";
|
|
2569
|
+
desc.textContent = opt.description;
|
|
2570
|
+
el.appendChild(desc);
|
|
2571
|
+
}
|
|
2572
|
+
return el;
|
|
2573
|
+
}
|
|
2574
|
+
_preselectValue() {
|
|
2575
|
+
return this.getAttribute("data-country-code") ?? this.getAttribute("data-default-country-code");
|
|
2576
|
+
}
|
|
2577
|
+
_resolveCountryOptions() {
|
|
2578
|
+
const raw = this.getAttribute("data-countries");
|
|
2579
|
+
if (raw) try {
|
|
2580
|
+
const parsed = JSON.parse(raw);
|
|
2581
|
+
if (Array.isArray(parsed) && parsed.every((o) => o && typeof o.value === "string" && typeof o.label === "string")) return parsed;
|
|
2582
|
+
} catch {}
|
|
2583
|
+
return getDefaultCountries(normalizeLocale(this.getAttribute("data-locale")));
|
|
2584
|
+
}
|
|
2585
|
+
_listbox() {
|
|
2586
|
+
return this._combobox()?.querySelector(".ix-combobox__listbox") ?? null;
|
|
2587
|
+
}
|
|
2588
|
+
_stampNumberDefaults() {
|
|
2589
|
+
const input = this._numberInput();
|
|
2590
|
+
if (!input) return;
|
|
2591
|
+
for (const [attr, value] of Object.entries({
|
|
2592
|
+
type: "tel",
|
|
2593
|
+
inputmode: "numeric",
|
|
2594
|
+
autocomplete: "tel-national",
|
|
2595
|
+
"data-format": "phone"
|
|
2596
|
+
})) if (!input.hasAttribute(attr)) input.setAttribute(attr, value);
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2599
|
+
//#endregion
|
|
1644
2600
|
//#region lib/utils/platform.ts
|
|
1645
2601
|
/**
|
|
1646
2602
|
* Plattform-deteksjon for web components.
|
|
@@ -2338,6 +3294,22 @@ var IxTabPanel = class extends HTMLElement {
|
|
|
2338
3294
|
}
|
|
2339
3295
|
};
|
|
2340
3296
|
//#endregion
|
|
3297
|
+
//#region lib/utils/cn.ts
|
|
3298
|
+
function cn(...inputs) {
|
|
3299
|
+
const klasser = [];
|
|
3300
|
+
for (const input of inputs) {
|
|
3301
|
+
if (!input) continue;
|
|
3302
|
+
if (typeof input === "string" || typeof input === "number") klasser.push(String(input));
|
|
3303
|
+
else if (Array.isArray(input)) {
|
|
3304
|
+
const nøstet = cn(...input);
|
|
3305
|
+
if (nøstet) klasser.push(nøstet);
|
|
3306
|
+
} else if (typeof input === "object") {
|
|
3307
|
+
for (const nøkkel in input) if (input[nøkkel]) klasser.push(nøkkel);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
return klasser.join(" ");
|
|
3311
|
+
}
|
|
3312
|
+
//#endregion
|
|
2341
3313
|
//#region lib/modal/modal.ts
|
|
2342
3314
|
var ATTR_OPEN = "data-modal-open";
|
|
2343
3315
|
var ATTR_CLOSE = "data-modal-close";
|
|
@@ -2530,10 +3502,13 @@ customElements.define("ix-icon", IxIcon);
|
|
|
2530
3502
|
customElements.define("ix-radio-group", IxRadioGroup);
|
|
2531
3503
|
customElements.define("ix-checkbox-group", IxCheckboxGroup);
|
|
2532
3504
|
customElements.define("ix-combobox", IxCombobox);
|
|
3505
|
+
customElements.define("ix-date-field", IxDateField);
|
|
3506
|
+
customElements.define("ix-phone-number-field", IxPhoneNumberField);
|
|
2533
3507
|
customElements.define("ix-progress-bar", IxProgressBar);
|
|
2534
3508
|
customElements.define("ix-tabs", IxTabs);
|
|
2535
3509
|
customElements.define("ix-tab-list", IxTabList);
|
|
2536
3510
|
customElements.define("ix-tab", IxTab);
|
|
2537
3511
|
customElements.define("ix-tab-panel", IxTabPanel);
|
|
3512
|
+
globalThis.cn = cn;
|
|
2538
3513
|
//#endregion
|
|
2539
|
-
export { COMMON_ICON_NAMES, IxCheckboxGroup, IxCombobox, IxField, IxIcon, IxProgressBar, IxRadioGroup, IxTab, IxTabList, IxTabPanel, IxTabs, amountFormatterForLocale, createAmountFormatter, createPatternFormatter, registerFormat, resolveFormat };
|
|
3514
|
+
export { BUILTIN_FORMAT_NAMES, COMMON_ICON_NAMES, IxCheckboxGroup, IxCombobox, IxDateField, IxField, IxIcon, IxPhoneNumberField, IxProgressBar, IxRadioGroup, IxTab, IxTabList, IxTabPanel, IxTabs, amountFormatterForLocale, cn, createAmountFormatter, createPatternFormatter, registerFormat, resolveFormat };
|