PageSourceSearch

https://trialog-8a95b.web.app/src/features/revenue-tracking/presen…/components/molecules/DateRangePicker.js

js trialog-8a95b.web.app collected 2026-10-03 12:41:17 UTC 7,371 bytes, 169 lines download raw bytes

1/** Kompakter Zeitraumwaehler mit Schnellwahl und gemeinsam bestaetigten Datumsfeldern. */
2import { createElement } from '../../../../../core/utils/index.js';
3import { Icon } from '../../../../hierarchy-tracking/presentation/components/atoms/Icon.js';
4import { datumsbereich, schnelleZeitraeume, zeitraumTitel, tabAusMenue } from './zeitraum.js';
5
6const FOKUSSIERBAR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
7
8let folge = 0;
9export class DateRangePicker {
10  #element;
11  #startDate;
12  #endDate;
13  #onChange;
14  #startInput;
15  #endInput;
16  #knopf;
17  #titel;
18  #menue;
19  #fehler;
20  #offen = false;
21  #schnell = [];
22
23  constructor(props = {}) {
24    this.#startDate = props.startDate || null;
25    this.#endDate = props.endDate || null;
26    this.#onChange = props.onChange || (() => {});
27    const id = `rv-zeitraum-${++folge}`;
28    this.#titel = createElement('span', { className: 'rv-zeitraum-titel' });
29    this.#knopf = createElement('button', {
30      type: 'button', className: 'rv-zeitraum-knopf', 'aria-expanded': 'false',
31      'aria-controls': id, 'aria-haspopup': 'dialog', onclick: () => this.#offen ? this.#schliesse() : this.#oeffne(),
32    }, [new Icon({ name: 'calendar', size: 16 }).element, this.#titel, new Icon({ name: 'chevronDown', size: 12 }).element]);
33    const feld = (name) => createElement('input', {
34      type: 'date', className: 'rv-zeitraum-datum', 'aria-label': name, 'aria-describedby': `${id}-fehler`,
35      oninput: () => { this.#fehler.hidden = true; },
36    });
37    this.#startInput = feld('Von');
38    this.#endInput = feld('Bis');
39    this.#fehler = createElement('p', { id: `${id}-fehler`, className: 'rv-zeitraum-fehler', role: 'alert', hidden: true });
40    const schnell = schnelleZeitraeume().map(opt => {
41      const knopf = createElement('button', {
42        type: 'button', className: 'rv-zeitraum-schnell',
43        onclick: () => this.#uebernehme(opt.start, opt.end),
44      }, [opt.label]);
45      this.#schnell.push({ knopf, ...opt });
46      return knopf;
47    });
48    this.#menue = createElement('form', {
49      id, className: 'rv-zeitraum-menue', role: 'dialog', 'aria-label': 'Zeitraum wählen',
50      onsubmit: e => { e.preventDefault(); this.#anwenden(); },
51    }, [
52      createElement('div', { className: 'rv-menue-titel' }, ['Zeitraum wählen']),
53      createElement('div', { className: 'rv-zeitraum-schnellwahlen' }, schnell),
54      createElement('div', { className: 'rv-zeitraum-felder' }, [
55        createElement('label', {}, ['Von', this.#startInput]),
56        createElement('label', {}, ['Bis', this.#endInput]),
57      ]),
58      this.#fehler,
59      createElement('button', { type: 'submit', className: 'rv-zeitraum-anwenden' }, ['Zeitraum übernehmen']),
60    ]);
61    this.#element = createElement('div', { className: 'rv-zeitraum' }, [this.#knopf]);
62    this.#aktualisiere();
63  }
64
65  #anwenden() {
66    try {
67      const { startDate, endDate } = datumsbereich(this.#startInput.value, this.#endInput.value);
68      this.#uebernehme(startDate, endDate);
69    } catch (fehler) {
70      this.#fehler.textContent = fehler.message;
71      this.#fehler.hidden = false;
72      this.#endInput.focus();
73    }
74  }
75
76  #uebernehme(start, end) {
77    this.setRange(start, end);
78    this.#schliesse(true);
79    this.#onChange({ startDate: start, endDate: end });
80  }
81
82  #format(date) {
83    return date ? `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` : '';
84  }
85
86  #aktualisiere() {
87    this.#startInput.value = this.#format(this.#startDate);
88    this.#endInput.value = this.#format(this.#endDate);
89    this.#titel.textContent = zeitraumTitel(this.#startDate, this.#endDate);
90    this.#knopf.setAttribute('aria-label', `Zeitraum: ${this.#titel.textContent}`);
91    this.#knopf.title = this.#titel.textContent;
92    for (const opt of this.#schnell) {
93      const aktiv = this.#format(opt.start) === this.#startInput.value && this.#format(opt.end) === this.#endInput.value;
94      opt.knopf.setAttribute('aria-pressed', String(aktiv));
95    }
96  }
97
98  #oeffne() {
99    this.#aktualisiere(); // Nicht bestaetigte Eingaben verwerfen.
100    this.#fehler.hidden = true;
101    this.#offen = true;
102    this.#knopf.setAttribute('aria-expanded', 'true');
103    document.body.append(this.#menue);
104    const r = this.#knopf.getBoundingClientRect();
105    const breite = Math.min(340, window.innerWidth - 24);
106    this.#menue.style.width = `${breite}px`;
107    this.#menue.style.maxHeight = `${window.innerHeight - 24}px`;
108    this.#menue.style.left = `${Math.max(12, Math.min(r.left, window.innerWidth - breite - 12))}px`;
109    const hoehe = this.#menue.getBoundingClientRect().height;
110    this.#menue.style.top = `${Math.max(12, Math.min(r.bottom + 8, window.innerHeight - hoehe - 12))}px`;
111    document.addEventListener('pointerdown', this.#aussen, true);
112    document.addEventListener('focusin', this.#aussen);
113    document.addEventListener('keydown', this.#taste, true);
114    window.addEventListener('resize', this.#umbruch);
115    window.addEventListener('scroll', this.#rollen, true);
116    this.#menue.querySelector('[aria-pressed="true"]')?.focus();
117    if (!this.#menue.contains(document.activeElement)) this.#startInput.focus();
118  }
119
120  #aussen = e => {
121    if (!this.#menue.contains(e.target) && !this.#element.contains(e.target)) this.#schliesse();
122  };
123  #taste = e => {
124    if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); this.#schliesse(true); return; }
125    if (e.key !== 'Tab') return;
126    // TAB AN DEN RAENDERN DES MENUES (D04-13, seit 5.314): zurueck auf den
127    // Knopf bzw. hinter ihn - nicht ans Seitenende, wo das Menue haengt.
128    const felder = [...this.#menue.querySelectorAll(FOKUSSIERBAR)];
129    const ziel = tabAusMenue({
130      erstes: felder[0], letztes: felder[felder.length - 1], aktiv: document.activeElement, shift: e.shiftKey,
131    });
132    if (!ziel) return;
133    e.preventDefault();
134    this.#schliesse(ziel === 'knopf');
135    if (ziel === 'danach') this.#fokusHinterKnopf();
136  };
137
138  /** Das naechste fokussierbare Element nach dem Knopf - wie ohne Menue. */
139  #fokusHinterKnopf() {
140    const alle = [...document.querySelectorAll(FOKUSSIERBAR)]
141      .filter((el) => el.offsetParent !== null || el === this.#knopf);
142    const naechstes = alle[alle.indexOf(this.#knopf) + 1];
143    (naechstes || this.#knopf).focus({ preventScroll: true });
144  }
145  #umbruch = () => this.#schliesse();
146  #rollen = e => { if (!this.#menue.contains(e.target)) this.#schliesse(); };
147
148  #schliesse(fokus = false) {
149    this.#offen = false;
150    this.#knopf.setAttribute('aria-expanded', 'false');
151    this.#menue.remove();
152    document.removeEventListener('pointerdown', this.#aussen, true);
153    document.removeEventListener('focusin', this.#aussen);
154    document.removeEventListener('keydown', this.#taste, true);
155    window.removeEventListener('resize', this.#umbruch);
156    window.removeEventListener('scroll', this.#rollen, true);
157    if (fokus) this.#knopf.focus({ preventScroll: true });
158  }
159
160  get element() { return this.#element; }
161  get startDate() { return this.#startDate; }
162  get endDate() { return this.#endDate; }
163  setRange(startDate, endDate) {
164    this.#startDate = startDate;
165    this.#endDate = endDate;
166    this.#aktualisiere();
167  }
168  destroy() { this.#schliesse(); }
169}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.