ngwr 9.0.0 → 9.1.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.
@@ -635,9 +635,517 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
635
635
  args: [{ selector: 'wr-date-picker', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()' }, imports: [WrInput, WrInputGroup, WrInputSuffix], template: "<wr-input-group>\n <input\n #input\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [value]=\"text()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n />\n <button\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel()\"\n [disabled]=\"disabled()\"\n (click)=\"toggleOverlay()\"\n >\n @if (isTime()) {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <circle cx=\"7\" cy=\"7\" r=\"5.75\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"7\" y1=\"7\" x2=\"7\" y2=\"3.75\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"7\" y1=\"7\" x2=\"9.5\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n </svg>\n } @else {\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n }\n </button>\n</wr-input-group>\n" }]
636
636
  }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], inputEl: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
637
637
 
638
+ /**
639
+ * @license
640
+ *
641
+ * Use of this source code is governed by an MIT-style license that can be
642
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
643
+ */
644
+ /**
645
+ * Internal panel behind `<wr-date-range-picker>`: a range-mode
646
+ * `<wr-calendar>`, plus one `<wr-time-picker>` per end when `withTime` is on.
647
+ * Not part of the public API.
648
+ *
649
+ * The calendar owns range picking (including the start/end swap and the hover
650
+ * preview); this panel only re-attaches the times so a datetime range keeps the
651
+ * hours the user set while the dates move around.
652
+ *
653
+ * @internal
654
+ */
655
+ class WrDateRangePanel {
656
+ value = input([null, null], /* @ts-ignore */
657
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
658
+ min = input(null, /* @ts-ignore */
659
+ ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
660
+ max = input(null, /* @ts-ignore */
661
+ ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
662
+ dateFilter = input(null, /* @ts-ignore */
663
+ ...(ngDevMode ? [{ debugName: "dateFilter" }] : /* istanbul ignore next */ []));
664
+ withTime = input(false, /* @ts-ignore */
665
+ ...(ngDevMode ? [{ debugName: "withTime" }] : /* istanbul ignore next */ []));
666
+ timeFormat = input('auto', /* @ts-ignore */
667
+ ...(ngDevMode ? [{ debugName: "timeFormat" }] : /* istanbul ignore next */ []));
668
+ showSeconds = input(false, /* @ts-ignore */
669
+ ...(ngDevMode ? [{ debugName: "showSeconds" }] : /* istanbul ignore next */ []));
670
+ step = input(1, /* @ts-ignore */
671
+ ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
672
+ changed = output();
673
+ adapter = inject(WrDateAdapter);
674
+ startTimeLabel = readI18nText('datePicker.startTime', 'Start time');
675
+ endTimeLabel = readI18nText('datePicker.endTime', 'End time');
676
+ /**
677
+ * A time set before its end has a date. The stepper is always live, so the
678
+ * hours are parked here until a date arrives to carry them — otherwise the
679
+ * edit would be silently dropped and the pick would land on midnight.
680
+ */
681
+ pendingStartTime = signal(null, /* @ts-ignore */
682
+ ...(ngDevMode ? [{ debugName: "pendingStartTime" }] : /* istanbul ignore next */ []));
683
+ pendingEndTime = signal(null, /* @ts-ignore */
684
+ ...(ngDevMode ? [{ debugName: "pendingEndTime" }] : /* istanbul ignore next */ []));
685
+ /** The calendar's own range model shape — same tuple, narrower name. */
686
+ calendarRange = computed(() => this.value(), /* @ts-ignore */
687
+ ...(ngDevMode ? [{ debugName: "calendarRange" }] : /* istanbul ignore next */ []));
688
+ /**
689
+ * Midnight today. The steppers need a concrete date to render, but the
690
+ * fallback must NOT be the current wall clock: picking a date carries no time
691
+ * across until a stepper is touched, so a `14:37` default would display a time
692
+ * the commit never actually stores.
693
+ */
694
+ dayStart = computed(() => {
695
+ const t = this.adapter.today();
696
+ return this.adapter.createDate(this.adapter.getYear(t), this.adapter.getMonth(t), this.adapter.getDate(t));
697
+ }, /* @ts-ignore */
698
+ ...(ngDevMode ? [{ debugName: "dayStart" }] : /* istanbul ignore next */ []));
699
+ /** Time steppers need a concrete date; fall back to a parked time, then midnight. */
700
+ startTime = computed(() => this.value()[0] ?? this.pendingStartTime() ?? this.dayStart(), /* @ts-ignore */
701
+ ...(ngDevMode ? [{ debugName: "startTime" }] : /* istanbul ignore next */ []));
702
+ endTime = computed(() => this.value()[1] ?? this.pendingEndTime() ?? this.dayStart(), /* @ts-ignore */
703
+ ...(ngDevMode ? [{ debugName: "endTime" }] : /* istanbul ignore next */ []));
704
+ onRangeChange(range) {
705
+ const [start, end] = range;
706
+ const [prevStart, prevEnd] = this.value();
707
+ // `WrCalendar.pickRange` restarts the range whenever it has no start, so a
708
+ // range that was end-only (typed into the end input alone) would lose that
709
+ // end on the next click. Read the click as filling in the missing start
710
+ // instead; the picker orders the pair afterwards.
711
+ const keptEnd = !prevStart && prevEnd && start && !end ? prevEnd : end;
712
+ // The calendar only moves dates, so carry each end's existing time across —
713
+ // or the time parked by a stepper that ran before this date existed.
714
+ this.changed.emit([
715
+ this.withTimeOf(start, prevStart ?? this.pendingStartTime()),
716
+ this.withTimeOf(keptEnd, prevEnd ?? this.pendingEndTime()),
717
+ ]);
718
+ }
719
+ onStartTimeChange(time) {
720
+ if (!time)
721
+ return;
722
+ const [start, end] = this.value();
723
+ if (!start) {
724
+ this.pendingStartTime.set(time);
725
+ return;
726
+ }
727
+ this.changed.emit([this.withTimeOf(start, time), end]);
728
+ }
729
+ onEndTimeChange(time) {
730
+ if (!time)
731
+ return;
732
+ const [start, end] = this.value();
733
+ if (!end) {
734
+ this.pendingEndTime.set(time);
735
+ return;
736
+ }
737
+ this.changed.emit([start, this.withTimeOf(end, time)]);
738
+ }
739
+ /** `date`'s calendar day carrying `time`'s clock. Null date stays null. */
740
+ withTimeOf(date, time) {
741
+ if (!date)
742
+ return null;
743
+ if (!this.withTime() || !time)
744
+ return date;
745
+ return this.adapter.setTime(this.adapter.createDate(this.adapter.getYear(date), this.adapter.getMonth(date), this.adapter.getDate(date)), this.adapter.getHours(time), this.adapter.getMinutes(time), this.adapter.getSeconds(time));
746
+ }
747
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrDateRangePanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
748
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: WrDateRangePanel, isStandalone: true, selector: "wr-date-range-panel", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, withTime: { classPropertyName: "withTime", publicName: "withTime", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changed: "changed" }, host: { classAttribute: "wr-date-range-panel" }, ngImport: i0, template: "<wr-calendar\n mode=\"range\"\n [range]=\"calendarRange()\"\n [min]=\"min()\"\n [max]=\"max()\"\n [dateFilter]=\"dateFilter()\"\n (rangeChange)=\"onRangeChange($event)\"\n/>\n\n@if (withTime()) {\n <div class=\"wr-date-range-panel__times\">\n <!-- A time picker is several inputs, not one control, so each end is a\n labelled group rather than a <label>. -->\n <div class=\"wr-date-range-panel__time\" role=\"group\" [attr.aria-label]=\"startTimeLabel\">\n <span class=\"wr-date-range-panel__time-label\" aria-hidden=\"true\">{{ startTimeLabel }}</span>\n <wr-time-picker\n [ngModel]=\"startTime()\"\n [format]=\"timeFormat()\"\n [showSeconds]=\"showSeconds()\"\n [step]=\"step()\"\n (ngModelChange)=\"onStartTimeChange($event)\"\n />\n </div>\n <div class=\"wr-date-range-panel__time\" role=\"group\" [attr.aria-label]=\"endTimeLabel\">\n <span class=\"wr-date-range-panel__time-label\" aria-hidden=\"true\">{{ endTimeLabel }}</span>\n <wr-time-picker\n [ngModel]=\"endTime()\"\n [format]=\"timeFormat()\"\n [showSeconds]=\"showSeconds()\"\n [step]=\"step()\"\n (ngModelChange)=\"onEndTimeChange($event)\"\n />\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: WrCalendar, selector: "wr-calendar", inputs: ["mode", "date", "range", "min", "max", "dateFilter", "disabled"], outputs: ["dateChange", "rangeChange"] }, { kind: "component", type: WrTimePanel, selector: "wr-time-picker", inputs: ["format", "showSeconds", "step", "disabled", "readonly", "value"], outputs: ["valueChange", "touch"] }], encapsulation: i0.ViewEncapsulation.None });
749
+ }
750
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrDateRangePanel, decorators: [{
751
+ type: Component,
752
+ args: [{ selector: 'wr-date-range-panel', encapsulation: ViewEncapsulation.None, host: { class: 'wr-date-range-panel' }, imports: [FormsModule, WrCalendar, WrTimePanel], template: "<wr-calendar\n mode=\"range\"\n [range]=\"calendarRange()\"\n [min]=\"min()\"\n [max]=\"max()\"\n [dateFilter]=\"dateFilter()\"\n (rangeChange)=\"onRangeChange($event)\"\n/>\n\n@if (withTime()) {\n <div class=\"wr-date-range-panel__times\">\n <!-- A time picker is several inputs, not one control, so each end is a\n labelled group rather than a <label>. -->\n <div class=\"wr-date-range-panel__time\" role=\"group\" [attr.aria-label]=\"startTimeLabel\">\n <span class=\"wr-date-range-panel__time-label\" aria-hidden=\"true\">{{ startTimeLabel }}</span>\n <wr-time-picker\n [ngModel]=\"startTime()\"\n [format]=\"timeFormat()\"\n [showSeconds]=\"showSeconds()\"\n [step]=\"step()\"\n (ngModelChange)=\"onStartTimeChange($event)\"\n />\n </div>\n <div class=\"wr-date-range-panel__time\" role=\"group\" [attr.aria-label]=\"endTimeLabel\">\n <span class=\"wr-date-range-panel__time-label\" aria-hidden=\"true\">{{ endTimeLabel }}</span>\n <wr-time-picker\n [ngModel]=\"endTime()\"\n [format]=\"timeFormat()\"\n [showSeconds]=\"showSeconds()\"\n [step]=\"step()\"\n (ngModelChange)=\"onEndTimeChange($event)\"\n />\n </div>\n </div>\n}\n" }]
753
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], withTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "withTime", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], changed: [{ type: i0.Output, args: ["changed"] }] } });
754
+
755
+ /**
756
+ * @license
757
+ *
758
+ * Use of this source code is governed by an MIT-style license that can be
759
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
760
+ */
761
+ /**
762
+ * Date-range picker — two text inputs sharing one range calendar.
763
+ *
764
+ * Separate from `<wr-date-picker>` because the value is a different type: a
765
+ * range is `[start, end]`, and folding that into the single picker's
766
+ * `Date | null` would break `[formField]` inference for every existing usage.
767
+ * Everything else matches — same input skeleton, same overlay, same
768
+ * {@link WrDateAdapter} formats.
769
+ *
770
+ * - `'date'` (default) — popover renders a range calendar. Picking the second
771
+ * date closes the overlay.
772
+ * - `'datetime'` — popover adds a time stepper per end. Picking dates does NOT
773
+ * close, since the user typically sets the times next.
774
+ *
775
+ * A signal-forms native control: it implements
776
+ * `FormValueControl<WrDateRange | null>`, so `[formField]` binds straight to
777
+ * its `value` model. `[(value)]` works standalone, and classic `[(ngModel)]` /
778
+ * reactive forms keep working through Angular's bridge.
779
+ *
780
+ * Either end may be `null` while the range is half-picked. Out-of-order ends
781
+ * are swapped on commit, matching the calendar's own behaviour.
782
+ *
783
+ * @example
784
+ * ```html
785
+ * <!-- signal forms -->
786
+ * <wr-date-range-picker [formField]="form.period" format="dd.MM.yyyy" />
787
+ *
788
+ * <!-- standalone two-way binding -->
789
+ * <wr-date-range-picker [(value)]="period" [minDate]="minDate" />
790
+ *
791
+ * <!-- date + time on both ends -->
792
+ * <wr-date-range-picker mode="datetime" [(value)]="window" timeFormat="24h" />
793
+ * ```
794
+ *
795
+ * @see https://ngwr.dev/reference/components/date-picker
796
+ */
797
+ class WrDateRangePicker {
798
+ /** Picker behavior — see class doc. @default 'date' */
799
+ mode = input('date', /* @ts-ignore */
800
+ ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
801
+ /**
802
+ * Format used for both display and parsing, on both ends. When `null`
803
+ * (default), it is derived from `mode` (`shortDate` / `shortDateTime`). Pass
804
+ * a named key or raw token string to override.
805
+ */
806
+ format = input(null, /* @ts-ignore */
807
+ ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
808
+ /** Placeholder for the start input. */
809
+ startPlaceholder = input('', /* @ts-ignore */
810
+ ...(ngDevMode ? [{ debugName: "startPlaceholder" }] : /* istanbul ignore next */ []));
811
+ /** Placeholder for the end input. */
812
+ endPlaceholder = input('', /* @ts-ignore */
813
+ ...(ngDevMode ? [{ debugName: "endPlaceholder" }] : /* istanbul ignore next */ []));
814
+ /** Glyph rendered between the two inputs. @default '–' */
815
+ separator = input('–', /* @ts-ignore */
816
+ ...(ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []));
817
+ // Named `minDate` / `maxDate`, not `min` / `max`: `FormUiControl` reserves
818
+ // those slots for the control's own value type, which here is a range — a
819
+ // `min` typed as a range makes no sense. Matches `WrValidators.minDate`.
820
+ /** Min selectable date, applied to both ends. */
821
+ minDate = input(null, /* @ts-ignore */
822
+ ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
823
+ /** Max selectable date, applied to both ends. */
824
+ maxDate = input(null, /* @ts-ignore */
825
+ ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
826
+ /** Predicate to disable specific dates (forwarded to the calendar). */
827
+ dateFilter = input(null, /* @ts-ignore */
828
+ ...(ngDevMode ? [{ debugName: "dateFilter" }] : /* istanbul ignore next */ []));
829
+ /** Time-panel 12 / 24-hour format. Applies in `datetime` mode. @default 'auto' */
830
+ timeFormat = input('auto', /* @ts-ignore */
831
+ ...(ngDevMode ? [{ debugName: "timeFormat" }] : /* istanbul ignore next */ []));
832
+ /** Render the seconds column. Applies in `datetime` mode. @default false */
833
+ showSeconds = input(false, { ...(ngDevMode ? { debugName: "showSeconds" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
834
+ /** Minute / second step for the time panels. @default 1 */
835
+ step = input(1, { ...(ngDevMode ? { debugName: "step" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(1, coerceNumberProperty(v, 1)) });
836
+ /**
837
+ * Disable interaction. Bound automatically from the field's disabled state
838
+ * when used with `[formField]`.
839
+ *
840
+ * @default false
841
+ */
842
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
843
+ /** Read-only — inputs not typeable, but the trigger icon still opens the overlay. @default false */
844
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
845
+ adapter = inject(WrDateAdapter);
846
+ overlay = inject(WR_OVERLAY);
847
+ scrollStrategies = inject(ScrollStrategyOptions);
848
+ host = inject(ElementRef);
849
+ destroyRef = inject(DestroyRef);
850
+ startEl = viewChild.required('startInput', /* @ts-ignore */
851
+ ...(ngDevMode ? [{ debugName: "startEl" }] : /* istanbul ignore next */ []));
852
+ /** The picked range. Bound by `[formField]`, or two-way via `[(value)]`. */
853
+ value = model(null, /* @ts-ignore */
854
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
855
+ /** Emitted on blur so a bound field can mark itself touched. */
856
+ touch = output();
857
+ /** Text currently in each input (may be partial / invalid mid-type). */
858
+ startText = signal('', /* @ts-ignore */
859
+ ...(ngDevMode ? [{ debugName: "startText" }] : /* istanbul ignore next */ []));
860
+ endText = signal('', /* @ts-ignore */
861
+ ...(ngDevMode ? [{ debugName: "endText" }] : /* istanbul ignore next */ []));
862
+ /** Whether the popover is currently open. */
863
+ overlayOpen = signal(false, /* @ts-ignore */
864
+ ...(ngDevMode ? [{ debugName: "overlayOpen" }] : /* istanbul ignore next */ []));
865
+ /** Resolved format — falls back to a mode-appropriate default. */
866
+ resolvedFormat = computed(() => this.format() ? String(this.format()) : this.mode() === 'datetime' ? 'shortDateTime' : 'shortDate', /* @ts-ignore */
867
+ ...(ngDevMode ? [{ debugName: "resolvedFormat" }] : /* istanbul ignore next */ []));
868
+ isDateTime = computed(() => this.mode() === 'datetime', /* @ts-ignore */
869
+ ...(ngDevMode ? [{ debugName: "isDateTime" }] : /* istanbul ignore next */ []));
870
+ triggerLabel = readI18nText('datePicker.openRange', 'Open range calendar');
871
+ startLabel = readI18nText('datePicker.rangeStart', 'Range start');
872
+ endLabel = readI18nText('datePicker.rangeEnd', 'Range end');
873
+ classes = computed(() => {
874
+ const parts = ['wr-date-range-picker', `wr-date-range-picker--${this.mode()}`];
875
+ if (this.disabled())
876
+ parts.push('wr-date-range-picker--disabled');
877
+ return parts.join(' ');
878
+ }, /* @ts-ignore */
879
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
880
+ overlayRef = null;
881
+ /** Live panel ref while the popover is open — lets typed edits move the
882
+ * displayed month in real time (not just on reopen). */
883
+ panelRef = signal(null, /* @ts-ignore */
884
+ ...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
885
+ /** Last range we pushed into the model ourselves, so the sync effect can skip
886
+ * the echo of our own edits and never reformat text mid-type. */
887
+ lastValue = null;
888
+ constructor() {
889
+ this.destroyRef.onDestroy(() => this.dispose());
890
+ // Mirror external writes to `value` into the two display texts.
891
+ effect(() => {
892
+ const v = this.value();
893
+ untracked(() => {
894
+ if (this.sameRange(v, this.lastValue))
895
+ return;
896
+ this.lastValue = v;
897
+ this.startText.set(this.display(v?.[0] ?? null));
898
+ this.endText.set(this.display(v?.[1] ?? null));
899
+ });
900
+ });
901
+ // Keep the open panel in step with the model.
902
+ effect(() => {
903
+ const ref = this.panelRef();
904
+ if (ref)
905
+ ref.setInput('value', this.value() ?? [null, null]);
906
+ });
907
+ // …and with every other input. Pushing these only at attach time meant a
908
+ // `[minDate]` (or filter, or time format) that changed while the popover was
909
+ // open silently kept the stale constraint until the next reopen.
910
+ effect(() => {
911
+ const ref = this.panelRef();
912
+ if (!ref)
913
+ return;
914
+ ref.setInput('min', this.minDate());
915
+ ref.setInput('max', this.maxDate());
916
+ ref.setInput('dateFilter', this.dateFilter());
917
+ ref.setInput('withTime', this.isDateTime());
918
+ ref.setInput('timeFormat', this.timeFormat());
919
+ ref.setInput('showSeconds', this.showSeconds());
920
+ ref.setInput('step', this.step());
921
+ });
922
+ }
923
+ // Template handlers
924
+ onStartInput(event) {
925
+ this.onRangeInput(event, 0);
926
+ }
927
+ onEndInput(event) {
928
+ this.onRangeInput(event, 1);
929
+ }
930
+ onBlur(event) {
931
+ // Moving between the two ends is still one interaction — only report the
932
+ // control as touched once focus actually leaves it, or tabbing from start to
933
+ // end would mark a bound field touched mid-entry.
934
+ const next = event.relatedTarget;
935
+ if (!next || !this.host.nativeElement.contains(next))
936
+ this.touch.emit();
937
+ // Typing leaves the ends in whatever order they were entered; settle it
938
+ // here, once the user has stopped.
939
+ const [start, end] = this.commitRange(this.current(), { normalise: true });
940
+ // Reformat to canonical on blur (cleans up `1/5/25` → `1/5/2025`).
941
+ this.startText.set(this.display(start));
942
+ this.endText.set(this.display(end));
943
+ }
944
+ /** Called by an input's click — opens the overlay if it isn't open already. */
945
+ openOnInput() {
946
+ if (this.disabled() || this.readonly() || this.overlayRef)
947
+ return;
948
+ this.openOverlay();
949
+ }
950
+ toggleOverlay() {
951
+ // `readonly` blocks the calendar too — otherwise the inputs refuse typing
952
+ // while the popover happily edits the same value.
953
+ if (this.disabled() || this.readonly())
954
+ return;
955
+ if (this.overlayRef)
956
+ this.closeOverlay();
957
+ else
958
+ this.openOverlay();
959
+ }
960
+ // Input parsing
961
+ onRangeInput(event, end) {
962
+ const raw = event.target.value;
963
+ (end === 0 ? this.startText : this.endText).set(raw);
964
+ const [start, finish] = this.current();
965
+ if (!raw) {
966
+ this.commitRange(end === 0 ? [null, finish] : [start, null], { normalise: false });
967
+ return;
968
+ }
969
+ const parsed = this.adapter.parse(raw, this.resolvedFormat());
970
+ if (!parsed || !this.adapter.isValid(parsed) || this.isOutOfBounds(parsed))
971
+ return;
972
+ // Never reorder mid-keystroke: a half-typed date can parse to an
973
+ // out-of-order value, and swapping there would yank the text the user is
974
+ // still typing over to the other input. Ordering is settled on blur.
975
+ this.commitRange(end === 0 ? [parsed, finish] : [start, parsed], { normalise: false });
976
+ }
977
+ // Overlay
978
+ openOverlay() {
979
+ if (this.overlayRef)
980
+ return;
981
+ const positionStrategy = this.overlay
982
+ .position()
983
+ .flexibleConnectedTo(this.host)
984
+ .withPositions([
985
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
986
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
987
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
988
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
989
+ ])
990
+ .withPush(true);
991
+ this.overlayRef = this.overlay.create({
992
+ positionStrategy,
993
+ scrollStrategy: this.scrollStrategies.reposition(),
994
+ panelClass: 'wr-date-picker-overlay',
995
+ });
996
+ this.overlayOpen.set(true);
997
+ const ref = this.overlayRef.attach(new ComponentPortal(WrDateRangePanel));
998
+ ref.setInput('value', this.value() ?? [null, null]);
999
+ this.panelRef.set(ref);
1000
+ ref.instance.changed.subscribe(next => {
1001
+ const committed = this.commitRange(next, { normalise: true });
1002
+ // A complete date-only range is the end of the interaction; a datetime
1003
+ // range still needs its hours, so the panel stays put.
1004
+ if (!this.isDateTime() && committed[0] && committed[1])
1005
+ this.closeOverlay();
1006
+ });
1007
+ this.overlayRef
1008
+ .outsidePointerEvents()
1009
+ .pipe(takeUntilDestroyed(this.destroyRef))
1010
+ .subscribe(event => {
1011
+ if (this.host.nativeElement.contains(event.target))
1012
+ return;
1013
+ this.closeOverlay();
1014
+ });
1015
+ this.overlayRef
1016
+ .keydownEvents()
1017
+ .pipe(takeUntilDestroyed(this.destroyRef))
1018
+ .subscribe(event => {
1019
+ if (event.key === 'Escape') {
1020
+ event.preventDefault();
1021
+ this.closeOverlay();
1022
+ this.startEl().nativeElement.focus();
1023
+ }
1024
+ });
1025
+ }
1026
+ /**
1027
+ * Close the popover. `restoreFocus` returns focus to the start input — needed
1028
+ * when the overlay is dismissed while focus lives inside it (picking the
1029
+ * second date), because disposing then leaves focus on a removed element and
1030
+ * the browser falls back to `<body>`.
1031
+ */
1032
+ closeOverlay(restoreFocus = false) {
1033
+ const inside = this.overlayRef?.overlayElement.contains(this.host.nativeElement.ownerDocument.activeElement);
1034
+ this.dispose();
1035
+ if (restoreFocus || inside)
1036
+ this.startEl().nativeElement.focus();
1037
+ }
1038
+ dispose() {
1039
+ if (this.overlayRef) {
1040
+ this.overlayRef.dispose();
1041
+ this.overlayRef = null;
1042
+ }
1043
+ this.panelRef.set(null);
1044
+ this.overlayOpen.set(false);
1045
+ }
1046
+ // Commit
1047
+ /**
1048
+ * Push to the model and refresh the text of whichever ends changed. Returns
1049
+ * what was actually committed.
1050
+ *
1051
+ * `normalise` swaps out-of-order ends. The calendar and blur ask for it; a
1052
+ * keystroke does not, because reordering while the user is still typing would
1053
+ * move their in-progress text into the other input.
1054
+ */
1055
+ commitRange(next, options) {
1056
+ const normalised = options.normalise ? this.normalise(next) : next;
1057
+ // Only write when a date actually moved. `normalise()` allocates a fresh
1058
+ // tuple on every call and `model()` compares by reference, so an
1059
+ // unconditional write would emit on every blur — enough to mark a bound
1060
+ // `[formField]` dirty just by tabbing through the two inputs.
1061
+ if (!this.sameRange(normalised, this.value())) {
1062
+ this.lastValue = normalised;
1063
+ this.value.set(normalised);
1064
+ }
1065
+ // Only rewrite the text of an end whose date moved out from under it —
1066
+ // otherwise a half-typed date would be reformatted on every keystroke.
1067
+ if (!this.sameDate(normalised[0], this.parseText(this.startText()))) {
1068
+ this.startText.set(this.display(normalised[0]));
1069
+ }
1070
+ if (!this.sameDate(normalised[1], this.parseText(this.endText()))) {
1071
+ this.endText.set(this.display(normalised[1]));
1072
+ }
1073
+ return normalised;
1074
+ }
1075
+ // Helpers
1076
+ current() {
1077
+ return this.value() ?? [null, null];
1078
+ }
1079
+ display(date) {
1080
+ return date && this.adapter.isValid(date) ? this.adapter.format(date, this.resolvedFormat()) : '';
1081
+ }
1082
+ parseText(raw) {
1083
+ if (!raw)
1084
+ return null;
1085
+ const parsed = this.adapter.parse(raw, this.resolvedFormat());
1086
+ return parsed && this.adapter.isValid(parsed) ? parsed : null;
1087
+ }
1088
+ /** Keep `[start, end]` in chronological order once both ends exist. */
1089
+ normalise(range) {
1090
+ const [start, end] = range;
1091
+ if (start && end && this.compareEnds(start, end) > 0)
1092
+ return [end, start];
1093
+ return [start, end];
1094
+ }
1095
+ /**
1096
+ * Chronological comparison at the precision the mode needs. The adapter's
1097
+ * `compareDate` is day-precision by contract, which would leave an inverted
1098
+ * same-day `datetime` range (18:00 → 09:00) unswapped.
1099
+ */
1100
+ compareEnds(a, b) {
1101
+ const byDay = this.adapter.compareDate(a, b);
1102
+ if (byDay !== 0 || !this.isDateTime())
1103
+ return byDay;
1104
+ const clock = (d) => this.adapter.getHours(d) * 3600 + this.adapter.getMinutes(d) * 60 + this.adapter.getSeconds(d);
1105
+ return clock(a) - clock(b);
1106
+ }
1107
+ /**
1108
+ * Whether a typed date must be rejected. Covers `dateFilter` as well as the
1109
+ * bounds — the calendar disables filtered days, so accepting them from the
1110
+ * keyboard would make the two entry paths disagree.
1111
+ */
1112
+ isOutOfBounds(date) {
1113
+ const min = this.minDate();
1114
+ if (min && this.adapter.compareDate(date, min) < 0)
1115
+ return true;
1116
+ const max = this.maxDate();
1117
+ if (max && this.adapter.compareDate(date, max) > 0)
1118
+ return true;
1119
+ const filter = this.dateFilter();
1120
+ if (filter && !filter(date))
1121
+ return true;
1122
+ return false;
1123
+ }
1124
+ sameDate(a, b) {
1125
+ if (a === b)
1126
+ return true;
1127
+ if (!a || !b)
1128
+ return false;
1129
+ return a.getTime() === b.getTime();
1130
+ }
1131
+ sameRange(a, b) {
1132
+ if (a === b)
1133
+ return true;
1134
+ if (!a || !b)
1135
+ return false;
1136
+ return this.sameDate(a[0], b[0]) && this.sameDate(a[1], b[1]);
1137
+ }
1138
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
1139
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: WrDateRangePicker, isStandalone: true, selector: "wr-date-range-picker", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { properties: { "class": "classes()" } }, viewQueries: [{ propertyName: "startEl", first: true, predicate: ["startInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<wr-input-group>\n <input\n #startInput\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"startLabel\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [value]=\"startText()\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (input)=\"onStartInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <span class=\"wr-date-range-picker__separator\" aria-hidden=\"true\">{{ separator() }}</span>\n <input\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"endLabel\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [value]=\"endText()\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (input)=\"onEndInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <button\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel\"\n [disabled]=\"disabled()\"\n (click)=\"toggleOverlay()\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n </button>\n</wr-input-group>\n", dependencies: [{ kind: "directive", type: WrInput, selector: "input[wrInput], textarea[wrInput]", inputs: ["wrSize", "rounded"] }, { kind: "component", type: WrInputGroup, selector: "wr-input-group", inputs: ["rounded"] }, { kind: "directive", type: WrInputSuffix, selector: "[wrInputSuffix]" }], encapsulation: i0.ViewEncapsulation.None });
1140
+ }
1141
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WrDateRangePicker, decorators: [{
1142
+ type: Component,
1143
+ args: [{ selector: 'wr-date-range-picker', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()' }, imports: [WrInput, WrInputGroup, WrInputSuffix], template: "<wr-input-group>\n <input\n #startInput\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"startLabel\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [value]=\"startText()\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (input)=\"onStartInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <span class=\"wr-date-range-picker__separator\" aria-hidden=\"true\">{{ separator() }}</span>\n <input\n wrInput\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n class=\"wr-date-range-picker__input\"\n [attr.aria-label]=\"endLabel\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"overlayOpen()\"\n [value]=\"endText()\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n (click)=\"openOnInput()\"\n (input)=\"onEndInput($event)\"\n (blur)=\"onBlur($event)\"\n />\n <button\n type=\"button\"\n wrInputSuffix\n class=\"wr-date-picker__trigger\"\n [attr.aria-label]=\"triggerLabel\"\n [disabled]=\"disabled()\"\n (click)=\"toggleOverlay()\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" aria-hidden=\"true\">\n <rect x=\"1.5\" y=\"2.5\" width=\"11\" height=\"10\" rx=\"1.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"1.5\" y1=\"5.5\" x2=\"12.5\" y2=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.2\" />\n <line x1=\"4\" y1=\"1\" x2=\"4\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n <line x1=\"10\" y1=\"1\" x2=\"10\" y2=\"4\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" />\n @if (isDateTime()) {\n <circle cx=\"7\" cy=\"9\" r=\"1.5\" fill=\"currentColor\" />\n }\n </svg>\n </button>\n</wr-input-group>\n" }]
1144
+ }], ctorParameters: () => [], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], startEl: [{ type: i0.ViewChild, args: ['startInput', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
1145
+
638
1146
  /**
639
1147
  * Generated bundle index. Do not edit.
640
1148
  */
641
1149
 
642
- export { WrDatePicker };
1150
+ export { WrDatePicker, WrDateRangePicker };
643
1151
  //# sourceMappingURL=ngwr-date-picker.mjs.map