@@ -31,7 +31,8 @@ export type EuroPlateUI = {
3131} ;
3232
3333export type EuroPlateOptions = {
34- input : HTMLInputElement ; // REQUIRED
34+ input ?: HTMLInputElement ; // era required → ora opzionale (se usi wrapper)
35+ wrapper ?: string | HTMLElement | false ; // 👈 selector, nodo o false (default)
3536 ui ?: EuroPlateUI ; // opzionale (UI pronta)
3637 allowedCountries ?: string [ ] ; // default: tutte
3738 mode ?: "AUTO" | string ; // default: "AUTO"
@@ -43,6 +44,14 @@ export type EuroPlateOptions = {
4344 logger ?: Logger ; // opzionale
4445 deps ?: { inputmask ?: any } ; // opzionale (se non c’è su window)
4546 debug ?: boolean ; // default: false
47+ // 👉 Autoload delle dipendenze (true di default)
48+ autoLoadDeps ?: {
49+ inputmask ?: boolean ; // default true
50+ } ;
51+ // 👉 CDN override (se vuoi cambiare l’URL)
52+ cdn ?: {
53+ inputmask ?: string ; // default jsDelivr UMD
54+ } ;
4655 i18n ?: I18nCode ; // <— NEW (default: 'AUTO')
4756} ;
4857
@@ -57,7 +66,6 @@ type DictScalarKey = "auto" | "placeholderAuto" | "valid" | "invalid" | "checked
5766
5867// Nomi paese localizzati (facoltativi per ciascun cc)
5968type CountryNameDict = Partial < Record < CountryKey , string > > ;
60-
6169const DICT : Record <
6270 Lang ,
6371 {
@@ -75,17 +83,78 @@ const DICT: Record<
7583 valid : "Valida" ,
7684 invalid : "Non valida" ,
7785 checked : "Controllati" ,
78- countries : { IT : "Italia" , FR : "Francia" , DE : "Germania" , ES : "Spagna" , NL : "Paesi Bassi" } ,
86+ countries : {
87+ IT : "Italia" ,
88+ UK : "Regno Unito" ,
89+ DE : "Germania" ,
90+ FR : "Francia" ,
91+ ES : "Spagna" ,
92+ PT : "Portogallo" ,
93+ NL : "Paesi Bassi" ,
94+ BE : "Belgio" ,
95+ CH : "Svizzera" ,
96+ AT : "Austria" ,
97+ IE : "Irlanda" ,
98+ LU : "Lussemburgo" ,
99+ DK : "Danimarca" ,
100+ SE : "Svezia" ,
101+ NO : "Norvegia" ,
102+ FI : "Finlandia" ,
103+ PL : "Polonia" ,
104+ CZ : "Cechia" ,
105+ SK : "Slovacchia" ,
106+ HU : "Ungheria" ,
107+ RO : "Romania" ,
108+ BG : "Bulgaria" ,
109+ SI : "Slovenia" ,
110+ HR : "Croazia" ,
111+ GR : "Grecia" ,
112+ LT : "Lituania" ,
113+ LV : "Lettonia" ,
114+ EE : "Estonia" ,
115+ UA : "Ucraina" ,
116+ } ,
79117 } ,
80118 en : {
81119 auto : "Auto (All)" ,
82120 placeholderAuto : "AA 999 AA / AA-999-AA / 9999 AAA" ,
83121 valid : "Valid" ,
84122 invalid : "Invalid" ,
85123 checked : "Checked" ,
86- countries : { IT : "Italy" , FR : "France" , DE : "Germany" , ES : "Spain" , NL : "Netherlands" } ,
124+ countries : {
125+ IT : "Italy" ,
126+ UK : "United Kingdom" ,
127+ DE : "Germany" ,
128+ FR : "France" ,
129+ ES : "Spain" ,
130+ PT : "Portugal" ,
131+ NL : "Netherlands" ,
132+ BE : "Belgium" ,
133+ CH : "Switzerland" ,
134+ AT : "Austria" ,
135+ IE : "Ireland" ,
136+ LU : "Luxembourg" ,
137+ DK : "Denmark" ,
138+ SE : "Sweden" ,
139+ NO : "Norway" ,
140+ FI : "Finland" ,
141+ PL : "Poland" ,
142+ CZ : "Czechia" ,
143+ SK : "Slovakia" ,
144+ HU : "Hungary" ,
145+ RO : "Romania" ,
146+ BG : "Bulgaria" ,
147+ SI : "Slovenia" ,
148+ HR : "Croatia" ,
149+ GR : "Greece" ,
150+ LT : "Lithuania" ,
151+ LV : "Latvia" ,
152+ EE : "Estonia" ,
153+ UA : "Ukraine" ,
154+ } ,
87155 } ,
88156} ;
157+
89158function pickLang ( code : I18nCode ) : Lang {
90159 if ( code === "IT" ) return "it" ;
91160 if ( code === "EN" ) return "en" ;
@@ -122,7 +191,8 @@ export type EuroPlateInstance = {
122191export function createEuroPlate ( EuroMod : any , opts : EuroPlateOptions ) : EuroPlateInstance {
123192 const {
124193 i18n = "AUTO" ,
125- input,
194+ //input,
195+ wrapper = false , // 👈 AGGIUNTO: selector | HTMLElement | false
126196 ui = { } ,
127197 allowedCountries,
128198 mode = "AUTO" ,
@@ -141,18 +211,59 @@ export function createEuroPlate(EuroMod: any, opts: EuroPlateOptions): EuroPlate
141211 debug = false ,
142212 } = opts || ( { } as EuroPlateOptions ) ;
143213 let lang : Lang = pickLang ( opts ?. i18n ?? "AUTO" ) ;
144- // const lang: Lang = pickLang(opts?.i18n ?? "AUTO");
145- const button = ui . button ?? undefined ;
146- const dropdown = ui . dropdown ?? undefined ;
147- const flagIcon = ui . flagIcon ?? undefined ;
148- const flagLabel = ui . flagLabel ?? undefined ;
149- const statusEl = ui . status ?? undefined ;
150214
215+ // riferimenti UI locali (li riempiremo da wrapper oppure da opts.ui)
216+ let input ! : HTMLInputElement ; // ← verrà assegnato
217+ let button : HTMLElement | undefined = ui . button ?? undefined ;
218+ let dropdown : HTMLElement | undefined = ui . dropdown ?? undefined ;
219+ let flagIcon : HTMLElement | undefined = ui . flagIcon ?? undefined ;
220+ let flagLabel : HTMLElement | undefined = ui . flagLabel ?? undefined ;
221+ let statusEl : HTMLElement | undefined = ui . status ?? undefined ;
222+
223+ // ----- AUTO-BUILD DOM SE wrapper È TRUTHY -----
224+ if ( wrapper ) {
225+ const root : HTMLElement | null =
226+ typeof wrapper === "string" ? document . querySelector ( wrapper ) : wrapper ;
227+
228+ if ( ! root ) throw new Error ( `Wrapper non trovato: ${ String ( wrapper ) } ` ) ;
229+
230+ root . classList . add ( "plate-iti-wrapper" ) ;
231+ root . innerHTML = `
232+ <div class="plate-iti">
233+ <button class="flag-btn" type="button" aria-haspopup="listbox" aria-expanded="false">
234+ <div class="iti__flag-box"><div class="iti__flag iti__auto-eu"></div></div>
235+ <span class="flag-label">${ t ( lang , "auto" ) } </span>
236+ <svg width="14" height="14" viewBox="0 0 20 20" fill="none" aria-hidden="true" style="margin-left:4px">
237+ <path d="M6 8l4 4 4-4" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
238+ </svg>
239+ </button>
240+ <input class="plate-input" placeholder="${ t ( lang , "placeholderAuto" ) } " autocomplete="off" />
241+ <div class="dropdown" role="listbox" aria-label="Select country"></div>
242+ </div>
243+ <div class="status"></div>
244+ ` ;
245+
246+ // bind elementi generati
247+ button = root . querySelector ( ".flag-btn" ) as HTMLElement ;
248+ flagIcon = root . querySelector ( ".iti__flag" ) as HTMLElement ;
249+ flagLabel = root . querySelector ( ".flag-label" ) as HTMLElement ;
250+ dropdown = root . querySelector ( ".dropdown" ) as HTMLElement ;
251+ input = root . querySelector ( ".plate-input" ) as HTMLInputElement ;
252+ statusEl = root . querySelector ( ".status" ) as HTMLElement ;
253+
254+ // aggiorna anche opts per retro-compat (se altrove li leggi da opts)
255+ ( opts as any ) . ui = { button, flagIcon, flagLabel, dropdown, status : statusEl } ;
256+ ( opts as any ) . input = input ;
257+ } else {
258+ // nessun wrapper → usa ciò che arriva da fuori (comportamento attuale)
259+ input = ( opts as any ) . input as HTMLInputElement ;
260+ }
261+
262+ // guard finali
151263 if ( ! EuroMod ?. validatePlate || ! EuroMod ?. getInputMask ) {
152264 throw new Error ( "EuroMod mancante o incompleto" ) ;
153265 }
154- if ( ! input ) throw new Error ( "opts.input è richiesto" ) ;
155-
266+ if ( ! input ) throw new Error ( "Devi passare `input` o `wrapper`." ) ;
156267 // logger morbido
157268 let DBG = ! ! debug ;
158269 const log : Logger = {
@@ -173,8 +284,40 @@ export function createEuroPlate(EuroMod: any, opts: EuroPlateOptions): EuroPlate
173284 } ,
174285 } ;
175286
176- const IM = deps ?. inputmask ?? ( globalThis as any ) . Inputmask ;
177- if ( ! IM ) log . warn ?.( "Inputmask non trovato: la maschera non sarà applicata" ) ;
287+ // ---- Inputmask fallback loader ---------------------------------------------
288+ let IM : any = deps ?. inputmask ?? ( globalThis as any ) . Inputmask ;
289+ let imReady = ! ! IM ;
290+ const wantAutoload = opts . autoLoadDeps ?. inputmask !== false ; // default: true
291+
292+ const imCdnUrl =
293+ opts . cdn ?. inputmask ?? "https://cdn.jsdelivr.net/npm/inputmask@5.0.9/dist/inputmask.min.js" ;
294+
295+ // Carica UMD async se non presente e autoload attivo
296+ if ( ! IM && wantAutoload ) {
297+ const script = document . createElement ( "script" ) ;
298+ script . src = imCdnUrl ;
299+ script . async = true ;
300+ script . crossOrigin = "anonymous" ;
301+ script . onload = ( ) => {
302+ IM = ( globalThis as any ) . Inputmask ;
303+ imReady = ! ! IM ;
304+ log . info ?.( "Inputmask caricato da CDN" ) ;
305+ // se siamo già su un paese fisso, prova a (ri)applicare la maschera
306+ if ( IM && selected !== "AUTO" ) {
307+ try {
308+ applyMaskDebounced ( input , selected ) ;
309+ } catch { }
310+ }
311+ } ;
312+ script . onerror = ( ) => {
313+ log . warn ?.( "Caricamento Inputmask fallito (CDN)" ) ;
314+ } ;
315+ document . head . appendChild ( script ) ;
316+ }
317+
318+ function hasIM ( ) : boolean {
319+ return ! ! IM ;
320+ }
178321
179322 const {
180323 supportedCountries,
@@ -249,6 +392,10 @@ export function createEuroPlate(EuroMod: any, opts: EuroPlateOptions): EuroPlate
249392 . replace ( / 9 \{ ( \d + ) \} / g, ( _ , n ) => "9" . repeat ( + n ) ) ;
250393
251394 const applyMaskDebounced = debounce ( ( inputEl : HTMLInputElement , country : string ) => {
395+ if ( ! hasIM ( ) ) {
396+ log . warn ?.( "Inputmask non disponibile: salto applyMask" ) ;
397+ return ;
398+ }
252399 if ( ! IM ) return ;
253400 if ( ! inputEl || ! country || country === "AUTO" ) {
254401 hardClearMaskDebounced ( inputEl ) ;
0 commit comments