Skip to content

Repository files navigation

Gregory

Picker data, rozsahu dat, času a období pro obyčejné weby. Vznikl z otravné situace, kterou zná asi každý, kdo spravuje víc než jeden projekt: tady jQuery daterangepicker, tam nativní <input type="date">, jinde něco třetího. Každý vypadá jinak, jinak se ovládá a jinak vrací hodnotu. Gregory je jedna komponenta, která to všechno zvládne — bez jediné runtime závislosti a bez toho, aby si diktovala, jakým frameworkem je stránka postavená.

const picker = new Gregory('#termin', { mode: 'range', locale: 'cs' })
picker.on('apply', ({ value }) => console.log(value.from, value.to))

Do pole se napíše 10.–16. 8. 2026, ven vypadnou dva obyčejné Date. Žádné momenty, žádné řetězce, které se pak musí luštit.

Proč zrovna tenhle

  • Osm režimů, jedno API. Jedno datum, rozsah, datum s časem, rozsah s časem, seznam samostatných dnů, měsíc, čtvrtletí, rok. Přepíná se jedinou volbou mode, zbytek zůstává stejný.
  • ~17 kB gzip JavaScriptu a 2,6 kB stylů. Žádné závislosti — ani jQuery, ani knihovna na práci s daty.
  • Funguje všude. Ve staré jQuery aplikaci stejně jako v Reactu; vedle třídy je i custom element <gregory-picker> pro deklarativní použití.
  • Omezení, která dávají smysl v praxi. min/max, zakázané dny, nejkratší i nejdelší rozsah, zákaz přeskočit obsazený termín, časové okno zvlášť pro každý den. Co picker nepustí do výběru, to nepustí ani do hodnoty.
  • Lokalizace přes Intl. Názvy měsíců, formáty a skloňování počtu dnů fungují pro jakýkoli jazyk; popisky tlačítek jsou hotové pro osm z nich.
  • Vzhled přes CSS proměnné. Motivy i hustota jsou jen sady --gr-*, takže se nikdy nemusíš prát o specificitu. Tmavý režim automaticky.
  • Ovládání klávesnicí. Šipky, PageUp/PageDown, Enter, Escape — a datum jde do pole i prostě napsat rukou.
  • Místní čas, žádná magie. Datum, na které uživatel klikne, je to datum, které dostaneš. Časové zóny knihovna vědomě neřeší.
  • 300+ testů ve Vitestu nad jádrem i nad DOM, pokrytí přes 90 % příkazů.

Dokumentace: svatekr70.github.io/gregorydemo s konfigurátorem, uživatelská příručka na nasazení krok za krokem a API reference s úplným výčtem voleb, metod, událostí a CSS proměnných. Web se dá spustit i lokálně přes npm run site:dev.

Instalace

Knihovna se instaluje rovnou z GitHubu — do npm registru se vydávat nebude. npm si ji po naklonování sestaví sám (skript prepare), nic se nemusí buildit ručně:

npm install github:svatekr70/gregory        # poslední main
npm install github:svatekr70/gregory#v0.3.0 # konkrétní verze

Jméno balíčku je @svatekr70/gregory, takže importy vypadají obvykle:

import { Gregory } from '@svatekr70/gregory'
import '@svatekr70/gregory/style.css'
import '@svatekr70/gregory/themes.css'      // nepovinné, jen hotové motivy

Bez npm, rovnou ze stránky

Sestavená knihovna se publikuje s dokumentací, takže jde načíst z URL. Soubory odpovídají poslední verzi na main:

<link rel="stylesheet" href="https://svatekr70.github.io/gregory/dist/gregory.css">
<script src="https://svatekr70.github.io/gregory/dist/gregory.umd.js"></script>
<script>
  new Gregory.Gregory('#termin', { mode: 'date', locale: 'cs' })
</script>

Nebo jako ES modul:

<link rel="stylesheet" href="https://svatekr70.github.io/gregory/dist/gregory.css">
<script type="module">
  import { Gregory } from 'https://svatekr70.github.io/gregory/dist/gregory.js'
  new Gregory('#termin', { mode: 'date', locale: 'cs' })
</script>

Pro produkci, kde nechceš viset na cizí adrese ani na posledním commitu, si stáhni přílohu vydání a soubory si nahraj k sobě.

Použití

Imperativně

const picker = new Gregory('#input', {
  mode: 'range',
  locale: 'cs',
  maxSpan: 31,
})

picker.on('apply', ({ value }) => {
  console.log(value.from, value.to)
})

Na jiném prvku než inputu

<span id="termin" data-value="2026-08-13" data-placeholder="Nezadáno">
  📅 <b data-gr-value>13. 8. 2026</b>
</span>
new Gregory('#termin', { mode: 'date' })

Klik nebo Enter otevře panel, potvrzená hodnota se vypíše do [data-gr-value] (nebo do prvku, když takový potomek není) a strojová podoba do data-value.

Značky pod čísly dnů

dayBadge vrací krátký text (nebo prázdný řetězec pro tečku), který se vykreslí pod číslo dne — kolik je ten den rezervací, hovorů, směn:

const CALLS = new Map([['2026-08-27', 3], ['2026-08-28', 12]])

new Gregory('#termin', {
  mode: 'date',
  locale: 'cs',
  dayBadge: (date) => {
    const count = CALLS.get(formatISODate(date))
    return count ? String(count) : null
  },
  // Vyšší buňka dá značce vzduch. Šířku sloupců drží --gr-day-size, kalendář
  // tedy zůstane stejně široký.
  className: 'kalendar-s-pocty',
})
.kalendar-s-pocty {
  --gr-day-height: 38px;
  --gr-day-badge-size: 11px;
}

Barvu značky si obarvíš podle vytížení přes dayClass:

dayClass: (date) => ((CALLS.get(formatISODate(date)) ?? 0) > 8 ? 'je-plno' : null)
.gr-day.je-plno .gr-day-badge { color: #b91c1c; }

dayBadge se volá i pro dny přesahující ze sousedních měsíců — stejně jako dayClass — aby konec měsíce nebyl obarvený, ale bez čísla. Značka v nich zdědí ztlumení .is-outside. Když značky mimo zobrazený měsíc nechceš, vrať pro ně null.

Srovnávací období

Analytické reporty skoro vždycky potřebují ke zvolenému období ještě to předchozí. compare ho dopočítá z hlavního rozsahu — nevybírá se, jen se v mřížce vyznačí pruhem pod dny a je k dispozici v hodnotě:

const picker = new Gregory('#obdobi', {
  mode: 'range',
  locale: 'cs',
  summary: true,
  compare: 'previous',
})

picker.on('apply', ({ value, compare }) => {
  nacti(value.from, value.to)
  if (compare) nacti(compare.from, compare.to)   // { from: Date, to: Date }
})
hodnota co spočítá
'previous' (nebo true) stejně dlouhé období těsně před začátkem
'year' stejná data o rok zpět
'year-weekday' posun o 364 dní, takže sedí dny v týdnu
(range) => [from, to] vlastní výpočet; null znamená „neporovnávat"

Dvě věci, které dělá jinak, než by čekal prostý odečet dnů:

  • Celý kalendářní celek se porovnává s celým předchozím celkem. Únor je kratší než leden, takže „předchozí období" k 1.–28. 2. by po dnech vyšlo na 4.–31. 1. Vybraný celý měsíc, čtvrtletí i rok proto vrací celý předchozí měsíc, čtvrtletí, rok.
  • 'year-weekday' posouvá o 52 týdnů, ne o rok. Data nesedí, ale pondělí padne na pondělí — což je to, co potřebují týdenní a prodejní reporty. 'year' naopak drží data a konce měsíců: 29. 2. 2024 vyjde na 28. 2. 2023.

Matematika je čistá funkce, takže se dá použít i mimo picker — třeba když stejný výpočet potřebuje i dotaz na serveru:

import { comparePeriod } from '@svatekr70/gregory'

comparePeriod({ from: new Date(2026, 1, 1), to: new Date(2026, 1, 28) }, 'previous')
// → { from: 1. 1. 2026, to: 31. 1. 2026 }

Barvu pruhu drží --gr-compare, jeho tloušťku --gr-compare-bar. Porovnávat jde jen v režimech rozsahu; jinde je compare bez efektu.

Deklarativně

import { defineElement } from '@svatekr70/gregory'
defineElement()
<gregory-picker mode="range" locale="cs" months="2" value="2026-08-01/2026-08-13">
</gregory-picker>

Element vypisuje gregory:change, gregory:apply, gregory:open a gregory:close jako bublající CustomEvent, hodnota je v event.detail.value. Atribut compare zapne srovnávací období — compare bez hodnoty znamená previous.

Volby

volba výchozí popis
mode 'date' date, range, datetime, datetime-range, multiple, month, quarter, year
className vlastní třídy pro kořen panelu (takhle se aplikují motivy)
value null Date, ISO string, {from,to} nebo [from, to]
locale jazyk prohlížeče BCP 47 tag nebo částečný objekt Locale
min / max null hranice výběru
firstDayOfWeek podle locale 0 = neděle … 6 = sobota
months 2 v range módu, jinak 1 počet panelů vedle sebe
linkedCalendars false listovat všemi panely najednou místo každým zvlášť
weekNumbers false sloupec s ISO čísly týdnů
showOutsideDays true zobrazovat dny přesahující ze sousedních měsíců
weekSelection 'off' výběr celého týdne: 'number' klikem na číslo týdne, 'day' klikem na kterýkoli den, 'both' obojí
dropdowns false výběr měsíce a roku: true nativní <select>, 'menu' seznam po kliknutí na caption
endInput druhé pole pro konec rozsahu (from do prvního, to do druhého)
allowTyping true číst datum napsané rukou do pole
submitName skrytá pole s ISO hodnotou pro odeslání formuláře
disabled false zamkne picker; totéž udělá disabled na poli
lockOnReadonly false zamknout i nad polem s readonly (jinak se nad ním picker normálně otevře)
inline false vykreslit na místo místo popoveru
autoApply true jen v módu date potvrdit hned, bez tlačítek Apply/Cancel
presets vestavěné v range módu postranní zkratky, false je skryje
compare false srovnávací období: 'previous', 'year', 'year-weekday' nebo vlastní funkce (viz Srovnávací období)
maxSpan null nejdelší povolený rozsah ve dnech
minSpan null nejkratší povolený rozsah ve dnech
stopAtDisabled false rozsah nesmí přeskočit den zakázaný přes isDisabled
allowOpenRange false povolí rozsah otevřený na jednom konci ({ from, to: null })
maxSelected null nejvíc dnů v režimu multiple
timeStep 5 krok minut v časových režimech
timeUi 'select' ovládání času: 'select' selecty, 'slider' posuvníky, 'input' nativní pole
minTime / maxTime null okno dne, 'HH:MM', včetně obou hranic
timeWindow (date) => { min, max } — okno dne pro konkrétní den
fullscreenBelow 480 pod touto šířkou okna se panel otevře přes celou obrazovku
opens / drops 'right' / 'auto' umístění popoveru
isDisabled (date) => boolean
dayClass (date) => string | null, např. svátky
dayBadge (date) => string | null — značka pod číslem dne (viz Vzhled)
format (value, locale) => string pro text v inputu
summary false řádek v panelu s právě vybranými daty (true nebo vlastní funkce)

API

picker.getValue()      // Date | DateRange | Date[] | null — potvrzená hodnota
picker.getSelection()  // rozpracovaný výběr (v range módu i poloviční)
picker.getCompare()    // { from, to } | null — srovnávací období k hodnotě
picker.setValue(value, { silent })
picker.clear()
picker.setOptions(patch)
picker.goTo('2026-12-01')
picker.openPanel() / picker.close() / picker.toggle()
picker.apply() / picker.cancel()
picker.on(event, listener)   // vrací odhlašovací funkci
picker.destroy()

Události: change (s příznakem complete), apply, cancel, open, close, invalid (hodnota neprošla omezeními), month-change. change a apply nesou i compare — srovnávací období, nebo null.

Lokalizace

Názvy měsíců, zkratky dnů, první den v týdnu i formát data řeší Intl, takže fungují pro jakýkoli jazyk. Popisky tlačítek a presetů knihovna nese sama — pro cs, sk, de, pl, en, es, fr, it. Rozhoduje jazyk, ne region, takže de-AT dostane němčinu. Ostatní jazyky mají popisky anglicky.

Chybějící jazyk se dodá zvenčí:

import { registerTranslation } from '@svatekr70/gregory'

registerTranslation('ja', {
  labels: { apply: '適用', cancel: 'キャンセル', today: '今日', now: '現在', /* … */ },
  presets: { today: '今日', yesterday: '昨日', /* … */ },
  days: { other: '日' },
})

Vzhled

Všechno jsou CSS proměnné na .gr, není potřeba přebíjet selektory:

.gr {
  --gr-accent: #0f766e;
  --gr-range-bg: #ccfbf1;
  --gr-radius: 14px;
}

Panel je light DOM — schválně, protože přes shadow DOM by se barvení proměnnými spíš komplikovalo. Kalendář uvnitř používá běžné značky (<header>, <section>, <footer>, <aside>, <button>…), takže by ho chytly i holé elementové selektory hostitelské stránky. Knihovna je proto hned na začátku nuluje pravidlem přes :where() — typografii i rozvržení, včetně flex-direction a rozměrů:

/* Tohle panel nerozhodí. */
header { display: flex; flex-direction: column; gap: 1rem; }
section { padding: 52px 0; }

:where() má nulovou specificitu, takže vlastní úpravy přes .gr-* mají dál přednost bez souboje o specificitu. Nenulují se schválně dvě věci: display (přepsat ho pro všechny značky najednou by rozbilo tlačítka i inputy) a text-align (čísla dnů jsou vycentrovaná od prohlížeče).

Hustotu drží čtyři proměnné — velikost dne, mezera, odsazení a písmo. Ostatní odsazení se z --gr-pad dopočítává:

.gr {
  --gr-day-size: 21px;
  --gr-gap: 0;
  --gr-pad: 7px;
  --gr-font-size: 13px;
  /* Čísla dnů se odvozují z velikosti políčka (výchozí 50 %), ne ze základního
     písma — prázdno okolo číslice je poměr, ne odsazení. */
  --gr-day-font-size: calc(var(--gr-day-size) * 0.6);
}

Hotové stupně jsou v themes.css jako gr-density-compact a gr-density-comfortable; barvy neřeší, takže se s motivy kombinují.

Den je čtverec o hraně --gr-day-size — ta zároveň určuje šířku sloupců, a tím i celého panelu. Když je potřeba vyšší buňka (typicky kvůli dayBadge), zvedni jen výšku; šířka kalendáře zůstane stejná:

.gr {
  --gr-day-height: 38px;    /* výchozí je var(--gr-day-size) */
  --gr-day-badge-gap: 3px;  /* mezera mezi číslem a značkou */
  --gr-day-badge-size: 11px; /* písmo značky a výška jejího řádku */
}

Řádek pro značku si v takové mřížce drží i dny, které žádnou nedostaly, takže čísla v řádku sedí na jedné lince.

Srovnávací období (compare) se kreslí pruhem při spodní hraně dne, aby se nepralo s výplní výběru — barva je --gr-compare, tloušťka --gr-compare-bar.

Tmavý režim se aktivuje sám podle prefers-color-scheme, nebo natvrdo přes data-theme="dark" / data-theme="light" na kořenovém prvku pickeru.

Hotové motivy

import '@svatekr70/gregory/style.css'
import '@svatekr70/gregory/themes.css'   // vždy až po style.css

new Gregory('#vstup', { className: 'gr-theme-riso' })
Třída Charakter
gr-theme-blueprint technický výkres — tmavě modrá, monospace, hustá mřížka
gr-theme-riso dvoubarevný tisk — papír, fluorescentní růžová, posunutý stín
gr-theme-clinic objednávkový systém — vzdušná bílá, modrozelená, dny jako pilulky
gr-theme-nocturne noční provoz — skoro černá s teplým jantarem

Žádný z nich nepřepisuje selektor komponenty, mění jen proměnné --gr-*.

Vývoj

npm run dev            # vývojový playground
npm test               # vitest
npm run test:coverage
npm run build          # typecheck + ESM/UMD/d.ts do dist/

npm run site:dev       # projektový web (úvod, demo, příručka, API dokumentace)
npm run site:build     # statický web do dist-site/

Web v site/ importuje knihovnu přímo ze src/, takže demo vždy ukazuje aktuální kód. dist-site/ je čistě statický — nahraje se kamkoli.

Podpora prohlížečů

Moderní evergreen prohlížeče — Chrome a Edge 90+, Firefox 88+, Safari 15+. Knihovna se sestavuje na ES2022 a nepoužívá polyfilly. Intl.Locale#getWeekInfo() (první den v týdnu) zatím neumí každý prohlížeč, takže na něj existuje záložní tabulka.

Licence

MIT © Rudolf Svátek

About

Rychlý picker data, rozsahu dat, času a období. Bez závislostí, ~17 kB gzip, osm režimů, lokalizace přes Intl, motivy přes CSS proměnné.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages