@softpak/components 20.6.11 → 20.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.
@@ -279,8 +279,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImpor
279
279
  // }
280
280
 
281
281
  class SpxInputCycleComponent {
282
- constructor(cdr) {
282
+ setT(fn, ms = 0) {
283
+ const id = window.setTimeout(() => {
284
+ this.pendingTimers.delete(id);
285
+ if (!this.suppressRefocus)
286
+ fn();
287
+ }, ms);
288
+ this.pendingTimers.add(id);
289
+ return id;
290
+ }
291
+ clearPendingTimers() {
292
+ this.pendingTimers.forEach((id) => clearTimeout(id));
293
+ this.pendingTimers.clear();
294
+ }
295
+ constructor(cdr, hostRef) {
283
296
  this.cdr = cdr;
297
+ this.hostRef = hostRef;
298
+ // ===== Inputs/signals (unchanged) =====
284
299
  this.mappedReadonly = computed(() => (this.spxReadonly() ? true : false), ...(ngDevMode ? [{ debugName: "mappedReadonly" }] : []));
285
300
  this.spxSpeedDial = input([], ...(ngDevMode ? [{ debugName: "spxSpeedDial" }] : []));
286
301
  this.spxName = input(...(ngDevMode ? [undefined, { debugName: "spxName" }] : []));
@@ -314,6 +329,9 @@ class SpxInputCycleComponent {
314
329
  this.internalIncompleteVer = signal(0, ...(ngDevMode ? [{ debugName: "internalIncompleteVer" }] : []));
315
330
  this.lastSkippedVer = -1;
316
331
  this.completeInput = new EventEmitter();
332
+ // ===== NEW: Focus suppression + safe timers =====
333
+ this.suppressRefocus = false;
334
+ this.pendingTimers = new Set();
317
335
  /** Externe wijzigingen via model() → sync naar interne state + DOM */
318
336
  this._reactToValueChanges = effect(() => {
319
337
  const incoming = this.value(); // dependency blijft
@@ -340,6 +358,23 @@ class SpxInputCycleComponent {
340
358
  this.spxSetFocus();
341
359
  this.applyValuesToDom();
342
360
  }
361
+ ngOnDestroy() {
362
+ this.clearPendingTimers();
363
+ }
364
+ // ===== Document-level pointer guards (NEW) =====
365
+ onDocPointerDown(ev) {
366
+ const inside = this.hostRef.nativeElement.contains(ev.target);
367
+ if (!inside) {
368
+ this.suppressRefocus = true; // block queued focus/selection during outside click
369
+ this.clearPendingTimers(); // cancel already queued focus/selection
370
+ }
371
+ }
372
+ onDocPointerUp() {
373
+ // re-enable refocus on the next frame after click completes
374
+ requestAnimationFrame(() => {
375
+ this.suppressRefocus = false;
376
+ });
377
+ }
343
378
  // =========================
344
379
  // Extern -> Intern & DOM
345
380
  // =========================
@@ -379,14 +414,12 @@ class SpxInputCycleComponent {
379
414
  // =========================
380
415
  // Intern -> Extern (emit)
381
416
  // =========================
382
- // -- emit helper
383
417
  handleChange(event, opts) {
384
- // laat je null->null guard staan als je die wilt
385
418
  if (event === null && this.lastEmitted === null && !opts?.internalIncomplete)
386
419
  return;
387
420
  untracked(() => {
388
421
  if (opts?.internalIncomplete) {
389
- this.internalIncompleteVer.update(v => v + 1); // markeer alleen interne-incomplete
422
+ this.internalIncompleteVer.update((v) => v + 1); // markeer alleen interne-incomplete
390
423
  }
391
424
  this.value.set(event);
392
425
  });
@@ -408,7 +441,6 @@ class SpxInputCycleComponent {
408
441
  this.handleChange(null, { internalIncomplete: true }); // markeer als intern-incompleet
409
442
  }
410
443
  }
411
- // Voor auto-advance stopconditie: ALLE posities gevuld?
412
444
  isAllFilled() {
413
445
  return this.values.every((v) => v !== '' && v !== null);
414
446
  }
@@ -417,13 +449,13 @@ class SpxInputCycleComponent {
417
449
  // =========================
418
450
  spxSetFocus() {
419
451
  const pos = this.spxCycleConfig().defaultFocusPosition;
420
- setTimeout(() => this.focusPos(pos));
452
+ this.setT(() => this.focusPos(pos));
421
453
  }
422
454
  onFocus(pos, _ev) {
423
455
  const input = this.inputBoxes.toArray()[pos]?.nativeElement;
424
456
  if (!input)
425
457
  return;
426
- setTimeout(() => input.select(), 0); // selecteer altijd alles bij focus
458
+ this.setT(() => input.select(), 0); // selecteer altijd alles bij focus (guarded)
427
459
  this.cancelNextMouseUp = true;
428
460
  }
429
461
  onMouseUp(ev) {
@@ -449,7 +481,6 @@ class SpxInputCycleComponent {
449
481
  return;
450
482
  const inputEl = event.target;
451
483
  let v = inputEl.value ?? '';
452
- // Eén teken per box
453
484
  if (v.length > 1) {
454
485
  v = v.charAt(0);
455
486
  if (inputEl.value !== v)
@@ -457,7 +488,6 @@ class SpxInputCycleComponent {
457
488
  }
458
489
  this.values[pos] = v;
459
490
  this.checkEmitCondition();
460
- // Auto-advance guards
461
491
  if (this.composing)
462
492
  return; // niet tijdens IME
463
493
  if (document.activeElement !== inputEl)
@@ -466,17 +496,14 @@ class SpxInputCycleComponent {
466
496
  return; // alleen als er precies 1 teken staat
467
497
  const order = this.spxCycleConfig().focusOrder;
468
498
  const isLastPos = pos === order.length - 1;
469
- // Op laatste positie én ALLES gevuld? -> niet wrappen; laat Tab verder gaan.
470
499
  if (isLastPos && this.isAllFilled()) {
471
500
  return;
472
501
  }
473
- // throttle & re-entrancy
474
502
  const now = Date.now();
475
503
  if (this.advancing || now - this.lastAdvanceAt < 40)
476
504
  return;
477
505
  this.advancing = true;
478
- setTimeout(() => {
479
- // dubbel-check voor zekerheid
506
+ this.setT(() => {
480
507
  if (inputEl.value.length === 1 && document.activeElement === inputEl) {
481
508
  const next = this.nextPos(pos); // mag wrappen zolang niet alles gevuld
482
509
  this.focusPos(next);
@@ -485,7 +512,6 @@ class SpxInputCycleComponent {
485
512
  this.advancing = false;
486
513
  }, 0);
487
514
  }
488
- // Alles leegmaken helper (incl. focus terugzetten naar startpositie)
489
515
  clearAllAndEmit() {
490
516
  const len = this.spxCycleConfig().length;
491
517
  if (!this.values || this.values.length !== len) {
@@ -495,12 +521,11 @@ class SpxInputCycleComponent {
495
521
  for (let i = 0; i < len; i++)
496
522
  this.values[i] = '';
497
523
  }
498
- this.applyValuesToDom(); // maak zichtbare inputs leeg
499
- this.handleChange(null); // emit meteen null
524
+ this.applyValuesToDom();
525
+ this.handleChange(null);
500
526
  const start = this.spxCycleConfig().defaultFocusPosition ?? 0;
501
- this.focusPos(start); // focus terug naar geconfigureerde startpositie
527
+ this.focusPos(start);
502
528
  }
503
- // Vind het volgende tabbable element in het document en geef het terug
504
529
  getNextTabStop(fromEl, backwards = false) {
505
530
  const selector = [
506
531
  'a[href]',
@@ -512,11 +537,7 @@ class SpxInputCycleComponent {
512
537
  '[tabindex]:not([tabindex="-1"])',
513
538
  '[contenteditable="true"]',
514
539
  ].join(',');
515
- const all = Array.from(document.querySelectorAll(selector))
516
- .filter((el) => !el.hasAttribute('disabled') &&
517
- el.tabIndex !== -1 &&
518
- el.offsetParent !== null && // zichtbaar/focusbaar
519
- !el.hasAttribute('inert'));
540
+ const all = Array.from(document.querySelectorAll(selector)).filter((el) => !el.hasAttribute('disabled') && el.tabIndex !== -1 && el.offsetParent !== null && !el.hasAttribute('inert'));
520
541
  const i = all.indexOf(fromEl);
521
542
  if (i === -1)
522
543
  return null;
@@ -527,27 +548,22 @@ class SpxInputCycleComponent {
527
548
  const el = this.inputBoxes.toArray()[pos]?.nativeElement;
528
549
  if (!el)
529
550
  return;
530
- // Replace-gedrag bij printable key (caret zonder selectie)
531
551
  const isPrintable = event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
532
552
  if (isPrintable && el.value.length >= 1 && el.selectionStart === el.selectionEnd) {
533
- el.select(); // laat browser de insert uitvoeren (vervangt)
553
+ el.select();
534
554
  }
535
- // Tab op laatste positie wanneer alles gevuld is:
536
- // zoek eerst volgende tabbable; alleen dan preventDefault + focus
537
555
  if (event.key === 'Tab') {
538
556
  const order = this.spxCycleConfig().focusOrder;
539
557
  const isLastPos = pos === order.length - 1;
540
558
  if (isLastPos && this.isAllFilled()) {
541
559
  const next = this.getNextTabStop(el, event.shiftKey);
542
560
  if (next) {
543
- event.preventDefault(); // alleen blokkeren als we zelf verplaatsen
561
+ event.preventDefault();
544
562
  next.focus();
545
563
  }
546
- return; // anders default laten doorgaan
564
+ return;
547
565
  }
548
- // anders: laat de browser normale Tab-afhandeling doen
549
566
  }
550
- // Backspace/Delete = ALLES leegmaken en focus naar start
551
567
  if (event.key === 'Backspace' || event.key === 'Delete') {
552
568
  event.preventDefault();
553
569
  this.clearAllAndEmit();
@@ -566,17 +582,19 @@ class SpxInputCycleComponent {
566
582
  return (pos - 1 + order.length) % order.length;
567
583
  }
568
584
  focusPos(pos) {
585
+ if (this.suppressRefocus)
586
+ return; // NEW: don't steal focus during outside clicks
569
587
  const el = this.inputBoxes.toArray()[pos]?.nativeElement;
570
588
  if (!el)
571
589
  return;
572
590
  if (document.activeElement === el)
573
591
  return; // al gefocust
574
- el.focus();
592
+ el.focus({ preventScroll: true });
575
593
  if (el.value)
576
- setTimeout(() => el.select(), 0);
594
+ this.setT(() => el.select()); // selection guarded
577
595
  }
578
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: SpxInputCycleComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
579
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.2", type: SpxInputCycleComponent, isStandalone: true, selector: "spx-input-cycle", inputs: { spxSpeedDial: { classPropertyName: "spxSpeedDial", publicName: "spxSpeedDial", isSignal: true, isRequired: false, transformFunction: null }, spxName: { classPropertyName: "spxName", publicName: "spxName", isSignal: true, isRequired: false, transformFunction: null }, spxAutofocus: { classPropertyName: "spxAutofocus", publicName: "spxAutofocus", isSignal: true, isRequired: false, transformFunction: null }, spxAutocomplete: { classPropertyName: "spxAutocomplete", publicName: "spxAutocomplete", isSignal: true, isRequired: false, transformFunction: null }, spxCycleConfig: { classPropertyName: "spxCycleConfig", publicName: "spxCycleConfig", isSignal: true, isRequired: true, transformFunction: null }, spxInputMode: { classPropertyName: "spxInputMode", publicName: "spxInputMode", isSignal: true, isRequired: false, transformFunction: null }, spxReadonly: { classPropertyName: "spxReadonly", publicName: "spxReadonly", isSignal: true, isRequired: false, transformFunction: null }, spxValidators: { classPropertyName: "spxValidators", publicName: "spxValidators", isSignal: true, isRequired: false, transformFunction: null }, spxCapitalize: { classPropertyName: "spxCapitalize", publicName: "spxCapitalize", isSignal: true, isRequired: false, transformFunction: null }, spxType: { classPropertyName: "spxType", publicName: "spxType", isSignal: true, isRequired: false, transformFunction: null }, spxWasInternalUpdate: { classPropertyName: "spxWasInternalUpdate", publicName: "spxWasInternalUpdate", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, spxElementId: { classPropertyName: "spxElementId", publicName: "spxElementId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", completeInput: "completeInput" }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }, { propertyName: "inputBoxes", predicate: ["inputBox"], descendants: true }], ngImport: i0, template: "<div class=\"relative text-black flex gap-2 w-full\">\n @for (val of values; track $index) {\n <input #inputBox type=\"text\" maxlength=\"1\" pattern=\"\\d*\" [value]=\"val\" (focus)=\"onFocus($index, $event)\"\n (compositionstart)=\"onCompositionStart()\" (compositionend)=\"onCompositionEnd()\" (input)=\"onInput($index, $event)\"\n (keydown)=\"onKeydown($index, $event)\" (mouseup)=\"onMouseUp($event)\"\n [attr.inputMode]=\"this.spxInputMode()\" [attr.pattern]=\"spxCycleConfig().fieldPattern\"\n [class.opacity-50]=\"spxCycleConfig().requiredPositions[$index] === false\"\n [class.bg-sky-100]=\"!this.spxReadonly() && !this.spxSeverity()\"\n [class.bg-red-100]=\"!this.spxReadonly() && this.spxSeverity() === severityError\"\n [class.bg-amber-100]=\"!this.spxReadonly() && this.spxSeverity() === severityWarning\"\n [class.bg-teal-100]=\"!this.spxReadonly() && this.spxSeverity() === severitySuccess\"\n [class.bg-gray-400]=\"this.spxReadonly()\" [class.cursor-not-allowed]=\"this.spxReadonly()\"\n [class.uppercase]=\"this.spxCapitalize()\" [disabled]=\"this.mappedReadonly()\"\n class=\"rounded text-center p-3 font-bold text-lg w-full\" />\n }\n</div>", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
596
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: SpxInputCycleComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
597
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.2", type: SpxInputCycleComponent, isStandalone: true, selector: "spx-input-cycle", inputs: { spxSpeedDial: { classPropertyName: "spxSpeedDial", publicName: "spxSpeedDial", isSignal: true, isRequired: false, transformFunction: null }, spxName: { classPropertyName: "spxName", publicName: "spxName", isSignal: true, isRequired: false, transformFunction: null }, spxAutofocus: { classPropertyName: "spxAutofocus", publicName: "spxAutofocus", isSignal: true, isRequired: false, transformFunction: null }, spxAutocomplete: { classPropertyName: "spxAutocomplete", publicName: "spxAutocomplete", isSignal: true, isRequired: false, transformFunction: null }, spxCycleConfig: { classPropertyName: "spxCycleConfig", publicName: "spxCycleConfig", isSignal: true, isRequired: true, transformFunction: null }, spxInputMode: { classPropertyName: "spxInputMode", publicName: "spxInputMode", isSignal: true, isRequired: false, transformFunction: null }, spxReadonly: { classPropertyName: "spxReadonly", publicName: "spxReadonly", isSignal: true, isRequired: false, transformFunction: null }, spxValidators: { classPropertyName: "spxValidators", publicName: "spxValidators", isSignal: true, isRequired: false, transformFunction: null }, spxCapitalize: { classPropertyName: "spxCapitalize", publicName: "spxCapitalize", isSignal: true, isRequired: false, transformFunction: null }, spxType: { classPropertyName: "spxType", publicName: "spxType", isSignal: true, isRequired: false, transformFunction: null }, spxWasInternalUpdate: { classPropertyName: "spxWasInternalUpdate", publicName: "spxWasInternalUpdate", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, spxElementId: { classPropertyName: "spxElementId", publicName: "spxElementId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", completeInput: "completeInput" }, host: { listeners: { "document:pointerdown": "onDocPointerDown($event)", "document:pointerup": "onDocPointerUp()" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["input"], descendants: true, isSignal: true }, { propertyName: "inputBoxes", predicate: ["inputBox"], descendants: true }], ngImport: i0, template: "<div class=\"relative text-black flex gap-2 w-full\">\n @for (val of values; track $index) {\n <input #inputBox type=\"text\" maxlength=\"1\" pattern=\"\\d*\" [value]=\"val\" (focus)=\"onFocus($index, $event)\"\n (compositionstart)=\"onCompositionStart()\" (compositionend)=\"onCompositionEnd()\" (input)=\"onInput($index, $event)\"\n (keydown)=\"onKeydown($index, $event)\" (mouseup)=\"onMouseUp($event)\"\n [attr.inputMode]=\"this.spxInputMode()\" [attr.pattern]=\"spxCycleConfig().fieldPattern\"\n [class.opacity-50]=\"spxCycleConfig().requiredPositions[$index] === false\"\n [class.bg-sky-100]=\"!this.spxReadonly() && !this.spxSeverity()\"\n [class.bg-red-100]=\"!this.spxReadonly() && this.spxSeverity() === severityError\"\n [class.bg-amber-100]=\"!this.spxReadonly() && this.spxSeverity() === severityWarning\"\n [class.bg-teal-100]=\"!this.spxReadonly() && this.spxSeverity() === severitySuccess\"\n [class.bg-gray-400]=\"this.spxReadonly()\" [class.cursor-not-allowed]=\"this.spxReadonly()\"\n [class.uppercase]=\"this.spxCapitalize()\" [disabled]=\"this.mappedReadonly()\"\n class=\"rounded text-center p-3 font-bold text-lg w-full\" />\n }\n</div>", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
580
598
  }
581
599
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: SpxInputCycleComponent, decorators: [{
582
600
  type: Component,
@@ -584,11 +602,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImpor
584
602
  ReactiveFormsModule,
585
603
  FormsModule,
586
604
  ], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative text-black flex gap-2 w-full\">\n @for (val of values; track $index) {\n <input #inputBox type=\"text\" maxlength=\"1\" pattern=\"\\d*\" [value]=\"val\" (focus)=\"onFocus($index, $event)\"\n (compositionstart)=\"onCompositionStart()\" (compositionend)=\"onCompositionEnd()\" (input)=\"onInput($index, $event)\"\n (keydown)=\"onKeydown($index, $event)\" (mouseup)=\"onMouseUp($event)\"\n [attr.inputMode]=\"this.spxInputMode()\" [attr.pattern]=\"spxCycleConfig().fieldPattern\"\n [class.opacity-50]=\"spxCycleConfig().requiredPositions[$index] === false\"\n [class.bg-sky-100]=\"!this.spxReadonly() && !this.spxSeverity()\"\n [class.bg-red-100]=\"!this.spxReadonly() && this.spxSeverity() === severityError\"\n [class.bg-amber-100]=\"!this.spxReadonly() && this.spxSeverity() === severityWarning\"\n [class.bg-teal-100]=\"!this.spxReadonly() && this.spxSeverity() === severitySuccess\"\n [class.bg-gray-400]=\"this.spxReadonly()\" [class.cursor-not-allowed]=\"this.spxReadonly()\"\n [class.uppercase]=\"this.spxCapitalize()\" [disabled]=\"this.mappedReadonly()\"\n class=\"rounded text-center p-3 font-bold text-lg w-full\" />\n }\n</div>" }]
587
- }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { completeInput: [{
605
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i0.ElementRef }], propDecorators: { completeInput: [{
588
606
  type: Output
589
607
  }], inputBoxes: [{
590
608
  type: ViewChildren,
591
609
  args: ['inputBox']
610
+ }], onDocPointerDown: [{
611
+ type: HostListener,
612
+ args: ['document:pointerdown', ['$event']]
613
+ }], onDocPointerUp: [{
614
+ type: HostListener,
615
+ args: ['document:pointerup']
592
616
  }] } });
593
617
 
594
618
  var stepType;