@acecss/core 0.0.0-stage → 1.0.4
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.
- package/LICENSE +21 -0
- package/README.md +78 -2
- package/dist/ace-datepicker.js +518 -0
- package/dist/ace-datepicker.min.js +2 -0
- package/dist/ace-select.js +329 -0
- package/dist/ace-select.min.js +2 -0
- package/dist/ace.css +1203 -0
- package/dist/ace.min.css +2 -0
- package/package.json +46 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kosowwwa LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AceCSS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Buttons, inputs, dropdowns and a date range picker in one CSS file.** No framework, no build step, ~12 KB gzipped. Dark mode built in. MIT licensed.
|
|
4
|
+
|
|
5
|
+
**Guide and live demo: https://acecss.dev**
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @acecss/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// Vite, Webpack, Next.js, Nuxt… (load it after any CSS reset / Tailwind)
|
|
17
|
+
import '@acecss/core' // the stylesheet (dist/ace.css)
|
|
18
|
+
|
|
19
|
+
// Optional, browser-side behaviour:
|
|
20
|
+
import '@acecss/core/datepicker' // date picker + pick-up → return range
|
|
21
|
+
import '@acecss/core/select' // dropdown for <select data-ace-select>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Nuxt: `css: ['@acecss/core']` in `nuxt.config.ts`.
|
|
25
|
+
Plain CSS: `@import '@acecss/core/ace.css';`
|
|
26
|
+
|
|
27
|
+
### Or straight from a CDN
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@acecss/core@1/dist/ace.min.css">
|
|
31
|
+
<script src="https://cdn.jsdelivr.net/npm/@acecss/core@1/dist/ace-datepicker.min.js" defer></script>
|
|
32
|
+
<script src="https://cdn.jsdelivr.net/npm/@acecss/core@1/dist/ace-select.min.js" defer></script>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Pinned links with SRI hashes: https://acecss.dev/#install
|
|
36
|
+
|
|
37
|
+
## Use it
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<button class="ace-btn ace-btn--primary">Save</button>
|
|
41
|
+
<button class="ace-btn ace-btn--secondary">Cancel</button>
|
|
42
|
+
|
|
43
|
+
<div class="ace-field">
|
|
44
|
+
<label class="ace-label" for="email">Email</label>
|
|
45
|
+
<input id="email" type="email" class="ace-input">
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<select name="plan" class="ace-select" data-ace-select>
|
|
49
|
+
<option value="basic">Basic</option>
|
|
50
|
+
<option value="full">Full cover</option>
|
|
51
|
+
</select>
|
|
52
|
+
|
|
53
|
+
<div class="ace-datepicker" data-ace-daterange
|
|
54
|
+
data-start-name="pickup_date" data-end-name="return_date" data-min="today"></div>
|
|
55
|
+
|
|
56
|
+
<span class="ace-badge ace-badge--success">Paid</span>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every component, with copy-paste code and usage rules: **https://acecss.dev**
|
|
60
|
+
|
|
61
|
+
- **Dark mode:** add `class="dark"` (or `data-theme="dark"`) to `<html>`, or `class="ace-auto-dark"` to follow the OS.
|
|
62
|
+
- **Re-brand:** override the tokens, e.g. `:root { --ace-primary: #2563eb; --ace-radius: 12px; }`
|
|
63
|
+
- **Button colours:** `ace-btn--blue`, `--green`, `--purple`, `--pink`, `--black`, or any colour with
|
|
64
|
+
`style="--ace-tone:#FF0022"` on a primary button (or a whole section). Light colour? Add `--ace-tone-fg:#111`.
|
|
65
|
+
Tailwind: `class="ace-btn ace-btn--primary [--ace-tone:#FF0022]"`
|
|
66
|
+
- **Markup added later** (Vue, React, HTMX): call `AceDatePicker.init(el)` / `AceSelect.init(el)`.
|
|
67
|
+
|
|
68
|
+
## Files
|
|
69
|
+
|
|
70
|
+
| File | |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `dist/ace.css`, `dist/ace.min.css` | All components (required) |
|
|
73
|
+
| `dist/ace-datepicker.js`, `.min.js` | Date picker and range picker (optional) |
|
|
74
|
+
| `dist/ace-select.js`, `.min.js` | Dropdown (optional) |
|
|
75
|
+
|
|
76
|
+
## Credits & license
|
|
77
|
+
|
|
78
|
+
AceCSS started as the component set of [PRC.rent](https://prc.rent), built by Kosowwwa LLC.
|
|
79
|
+
MIT License © 2026 Kosowwwa LLC. Source: https://gitlab.com/erblin.kosowwwa/acecss
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/* GENERATED by build.py from the PRC kit — do not edit by hand. */
|
|
2
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
3
|
+
ACECSS DATE PICKER — v1.0.4 · MIT License · (c) 2026 Kosowwwa LLC
|
|
4
|
+
Dependency-free single-date and range picker. Styles live in ace.css (section 8).
|
|
5
|
+
|
|
6
|
+
SINGLE DATE
|
|
7
|
+
<label class="ace-label" for="pickup">Pick-up date</label>
|
|
8
|
+
<div class="ace-datepicker" data-ace-datepicker
|
|
9
|
+
data-id="pickup" data-name="pickup_date" data-min="today"></div>
|
|
10
|
+
|
|
11
|
+
RANGE (pick-up → return, with times)
|
|
12
|
+
<div class="ace-datepicker" data-ace-daterange
|
|
13
|
+
data-start-name="pickup_date" data-end-name="return_date"
|
|
14
|
+
data-start-time-name="pickup_time" data-end-time-name="return_time"
|
|
15
|
+
data-min="today"></div>
|
|
16
|
+
|
|
17
|
+
Options (data-*): value | start / end | start-time / end-time
|
|
18
|
+
min / max (YYYY-MM-DD or "today") · placeholder · locale (BCP 47,
|
|
19
|
+
default = page language) · disabled · invalid · time="false"
|
|
20
|
+
start-label / end-label (time labels) · clear-label / close-label
|
|
21
|
+
|
|
22
|
+
Values are submitted with the form as hidden inputs in ISO format
|
|
23
|
+
(YYYY-MM-DD, times HH:MM). Every change fires a `ace:change` event on
|
|
24
|
+
the .ace-datepicker element; event.detail holds the current values.
|
|
25
|
+
|
|
26
|
+
Auto-initialises on DOMContentLoaded. For markup added later (Vue,
|
|
27
|
+
React, HTMX…) call AceDatePicker.init(containerElement).
|
|
28
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
29
|
+
(function () {
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
// Server-side rendering (Nuxt, Next.js, …) imports this too: there is
|
|
33
|
+
// no DOM there, so do nothing and let the browser bundle initialise.
|
|
34
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
35
|
+
|
|
36
|
+
var SVG = {
|
|
37
|
+
cal: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
|
|
38
|
+
prev: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>',
|
|
39
|
+
next: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>',
|
|
40
|
+
down: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>',
|
|
41
|
+
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>',
|
|
42
|
+
clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/* ── date helpers (local dates, never UTC) ── */
|
|
46
|
+
function pad(n) { return String(n).padStart(2, '0'); }
|
|
47
|
+
function toISO(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
|
|
48
|
+
function parseISO(s) {
|
|
49
|
+
var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s || '');
|
|
50
|
+
return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
|
|
51
|
+
}
|
|
52
|
+
function isoToDisplay(iso) {
|
|
53
|
+
var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso || '');
|
|
54
|
+
return m ? m[3] + '/' + m[2] + '/' + m[1] : '';
|
|
55
|
+
}
|
|
56
|
+
function resolveBound(v) { return v === 'today' ? toISO(new Date()) : (v || ''); }
|
|
57
|
+
function esc(s) {
|
|
58
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
59
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* 24h HH:MM masking */
|
|
64
|
+
function maskTime(raw) {
|
|
65
|
+
var v = raw.replace(/[^\d]/g, '');
|
|
66
|
+
// A first digit of 3-9 can only be a single-digit hour: "930" → 09:30
|
|
67
|
+
if (/^[3-9]/.test(v)) v = '0' + v;
|
|
68
|
+
v = v.slice(0, 4);
|
|
69
|
+
if (v.length >= 2) { var h = Math.min(parseInt(v.slice(0, 2), 10), 23); v = pad(h) + v.slice(2); }
|
|
70
|
+
if (v.length >= 4) { var mi = Math.min(parseInt(v.slice(2, 4), 10), 59); v = v.slice(0, 2) + pad(mi); }
|
|
71
|
+
if (v.length >= 3) v = v.slice(0, 2) + ':' + v.slice(2);
|
|
72
|
+
return v;
|
|
73
|
+
}
|
|
74
|
+
function padTime(raw) {
|
|
75
|
+
if (!raw) return '';
|
|
76
|
+
var d = raw.replace(/[^\d]/g, ''), v = raw;
|
|
77
|
+
if (d.length === 1) v = '0' + d + ':00';
|
|
78
|
+
else if (d.length === 2) v = d + ':00';
|
|
79
|
+
else if (d.length === 3) v = '0' + d.slice(0, 1) + ':' + d.slice(1);
|
|
80
|
+
else if (d.length === 4) v = d.slice(0, 2) + ':' + d.slice(2);
|
|
81
|
+
return maskTime(v);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function weekdays(locale) {
|
|
85
|
+
var out = [];
|
|
86
|
+
for (var i = 0; i < 7; i++) {
|
|
87
|
+
out.push(new Date(2024, 0, 7 + i).toLocaleDateString(locale, { weekday: 'short' }).slice(0, 2));
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Month grid: Sunday-first, trailing rows trimmed */
|
|
93
|
+
function monthCells(year, month) {
|
|
94
|
+
var start = new Date(year, month, 1).getDay();
|
|
95
|
+
var dim = new Date(year, month + 1, 0).getDate();
|
|
96
|
+
var cells = [];
|
|
97
|
+
for (var i = 0; i < 42; i++) {
|
|
98
|
+
var dt = new Date(year, month, 1 - start + i);
|
|
99
|
+
cells.push({ date: dt.getDate(), month: dt.getMonth(), year: dt.getFullYear(), iso: toISO(dt), current: dt.getMonth() === month });
|
|
100
|
+
if (i >= start + dim - 1 && (i + 1) % 7 === 0) break;
|
|
101
|
+
}
|
|
102
|
+
return cells;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hidden(el, name, value) {
|
|
106
|
+
var h = document.createElement('input');
|
|
107
|
+
h.type = 'hidden';
|
|
108
|
+
if (name) h.name = name;
|
|
109
|
+
h.value = value || '';
|
|
110
|
+
el.appendChild(h);
|
|
111
|
+
return h;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function emit(el, detail) {
|
|
115
|
+
el.dispatchEvent(new CustomEvent('ace:change', { bubbles: true, detail: detail }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Shared calendar header + grid markup */
|
|
119
|
+
function calendarHTML(st, opts) {
|
|
120
|
+
var label = new Date(st.viewYear, st.viewMonth, 1).toLocaleDateString(st.locale, { month: 'long', year: 'numeric' });
|
|
121
|
+
var html = '';
|
|
122
|
+
if (opts.yearPicker) {
|
|
123
|
+
var monthName = new Date(st.viewYear, st.viewMonth, 1).toLocaleDateString(st.locale, { month: 'long' });
|
|
124
|
+
html += '<div class="ace-dp-head"><span class="ace-dp-title">' + esc(monthName) + '</span>' +
|
|
125
|
+
'<div class="ace-dp-year"><button type="button" class="ace-btn ace-btn--secondary ace-dp-yearbtn" data-act="years" aria-haspopup="listbox" aria-expanded="' + !!st.yearsOpen + '">' +
|
|
126
|
+
st.viewYear + SVG.down + '</button>';
|
|
127
|
+
if (st.yearsOpen) {
|
|
128
|
+
html += '<div class="ace-dp-years" role="listbox">';
|
|
129
|
+
for (var y = st.minYear; y <= st.maxYear; y++) {
|
|
130
|
+
var on = y === st.viewYear;
|
|
131
|
+
html += '<button type="button" role="option" data-year="' + y + '" aria-selected="' + on + '">' +
|
|
132
|
+
(on ? SVG.check : '<span class="ace-dp-check"></span>') + y + '</button>';
|
|
133
|
+
}
|
|
134
|
+
html += '</div>';
|
|
135
|
+
}
|
|
136
|
+
html += '</div></div>';
|
|
137
|
+
}
|
|
138
|
+
html += '<div class="ace-dp-nav">' +
|
|
139
|
+
'<button type="button" class="ace-btn ace-btn--secondary ace-btn--icon ace-dp-navbtn" data-act="prev" aria-label="Previous month">' + SVG.prev + '</button>' +
|
|
140
|
+
'<span class="ace-dp-month" aria-live="polite">' + esc(label) + '</span>' +
|
|
141
|
+
'<button type="button" class="ace-btn ace-btn--secondary ace-btn--icon ace-dp-navbtn" data-act="next" aria-label="Next month">' + SVG.next + '</button></div>';
|
|
142
|
+
html += '<div class="ace-dp-weekdays">' + weekdays(st.locale).map(function (w) { return '<div>' + esc(w) + '</div>'; }).join('') + '</div>';
|
|
143
|
+
html += '<div class="ace-dp-grid" role="grid">' + opts.cells.map(opts.cellHTML).join('') + '</div>';
|
|
144
|
+
return html;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function dayHTML(c, attrs, cellAttrs) {
|
|
148
|
+
var full = parseISO(c.iso).toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' });
|
|
149
|
+
return '<div class="ace-dp-cell"' + (cellAttrs || '') + '><button type="button" class="ace-dp-day" data-iso="' + c.iso + '"' +
|
|
150
|
+
' aria-label="' + esc(full) + '"' + attrs + '>' + c.date + '</button></div>';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stepMonth(st, delta) {
|
|
154
|
+
var d = new Date(st.viewYear, st.viewMonth + delta, 1);
|
|
155
|
+
st.viewYear = d.getFullYear();
|
|
156
|
+
st.viewMonth = d.getMonth();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ════════════════════════ single date ════════════════════════ */
|
|
160
|
+
function initSingle(el) {
|
|
161
|
+
var ds = el.dataset;
|
|
162
|
+
var today = toISO(new Date());
|
|
163
|
+
var st = {
|
|
164
|
+
value: ds.value || '',
|
|
165
|
+
min: resolveBound(ds.min), max: resolveBound(ds.max),
|
|
166
|
+
locale: ds.locale || document.documentElement.lang || undefined,
|
|
167
|
+
open: false, yearsOpen: false
|
|
168
|
+
};
|
|
169
|
+
var cur = new Date().getFullYear();
|
|
170
|
+
st.minYear = st.min ? +st.min.slice(0, 4) : cur - 20;
|
|
171
|
+
st.maxYear = st.max ? +st.max.slice(0, 4) : cur + 10;
|
|
172
|
+
|
|
173
|
+
el.innerHTML = '';
|
|
174
|
+
var field = document.createElement('div');
|
|
175
|
+
field.className = 'ace-input-group ace-date-field' + (ds.invalid != null ? ' is-invalid' : '') + (ds.disabled != null ? ' is-disabled' : '');
|
|
176
|
+
field.innerHTML = '<input type="text" inputmode="numeric" autocomplete="off"' +
|
|
177
|
+
(ds.id ? ' id="' + esc(ds.id) + '"' : '') +
|
|
178
|
+
' placeholder="' + esc(ds.placeholder || 'DD/MM/YYYY') + '"' +
|
|
179
|
+
(ds.invalid != null ? ' aria-invalid="true"' : '') +
|
|
180
|
+
(ds.disabled != null ? ' disabled' : '') + '>' +
|
|
181
|
+
'<button type="button" class="ace-date-toggle" aria-label="Choose date" aria-haspopup="dialog" aria-expanded="false"' +
|
|
182
|
+
(ds.disabled != null ? ' disabled' : '') + '>' + SVG.cal + '</button>';
|
|
183
|
+
el.appendChild(field);
|
|
184
|
+
var input = field.querySelector('input');
|
|
185
|
+
var toggleBtn = field.querySelector('.ace-date-toggle');
|
|
186
|
+
var out = hidden(el, ds.name, st.value);
|
|
187
|
+
input.value = isoToDisplay(st.value);
|
|
188
|
+
var pop = null;
|
|
189
|
+
|
|
190
|
+
function setValue(iso, silent) {
|
|
191
|
+
st.value = iso || '';
|
|
192
|
+
out.value = st.value;
|
|
193
|
+
input.value = isoToDisplay(st.value);
|
|
194
|
+
if (!silent) {
|
|
195
|
+
emit(el, { value: st.value });
|
|
196
|
+
out.dispatchEvent(new Event('change', { bubbles: true }));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function commitTyped() {
|
|
201
|
+
var s = input.value.trim();
|
|
202
|
+
if (!s) { if (st.value) setValue(''); return; }
|
|
203
|
+
var m = /^(\d{1,2})[./-](\d{1,2})[./-](\d{4})$/.exec(s) || /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(s);
|
|
204
|
+
if (!m) { input.value = isoToDisplay(st.value); return; }
|
|
205
|
+
var y, mo, d;
|
|
206
|
+
if (m[1].length === 4) { y = +m[1]; mo = +m[2]; d = +m[3]; } else { d = +m[1]; mo = +m[2]; y = +m[3]; }
|
|
207
|
+
var dt = new Date(y, mo - 1, d);
|
|
208
|
+
if (mo < 1 || mo > 12 || dt.getDate() !== d) { input.value = isoToDisplay(st.value); return; }
|
|
209
|
+
var iso = toISO(dt);
|
|
210
|
+
if ((st.min && iso < st.min) || (st.max && iso > st.max)) { input.value = isoToDisplay(st.value); return; }
|
|
211
|
+
if (iso !== st.value) setValue(iso); else input.value = isoToDisplay(iso);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function render() {
|
|
215
|
+
if (!pop) return;
|
|
216
|
+
var cells = monthCells(st.viewYear, st.viewMonth);
|
|
217
|
+
pop.innerHTML = calendarHTML(st, {
|
|
218
|
+
yearPicker: true,
|
|
219
|
+
cells: cells,
|
|
220
|
+
cellHTML: function (c) {
|
|
221
|
+
var dis = (st.min && c.iso < st.min) || (st.max && c.iso > st.max);
|
|
222
|
+
var sel = c.iso === st.value;
|
|
223
|
+
return dayHTML(c,
|
|
224
|
+
(sel ? ' data-selected aria-pressed="true"' : ' aria-pressed="false"') +
|
|
225
|
+
(c.iso === today ? ' data-today aria-current="date"' : '') +
|
|
226
|
+
(!c.current ? ' data-outside' : '') +
|
|
227
|
+
(dis ? ' disabled' : ''));
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function position() {
|
|
233
|
+
if (!pop) return;
|
|
234
|
+
var r = field.getBoundingClientRect();
|
|
235
|
+
var w = pop.offsetWidth, h = pop.offsetHeight;
|
|
236
|
+
var top = r.bottom + 8;
|
|
237
|
+
if (top + h > window.innerHeight - 8) top = Math.max(8, r.top - h - 8);
|
|
238
|
+
var left = r.left;
|
|
239
|
+
if (left + w > window.innerWidth - 8) left = Math.max(8, window.innerWidth - w - 8);
|
|
240
|
+
pop.style.top = top + 'px';
|
|
241
|
+
pop.style.left = left + 'px';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function open() {
|
|
245
|
+
if (st.open || ds.disabled != null) return;
|
|
246
|
+
var d = parseISO(st.value) || new Date();
|
|
247
|
+
st.viewYear = d.getFullYear(); st.viewMonth = d.getMonth();
|
|
248
|
+
st.open = true; st.yearsOpen = false;
|
|
249
|
+
pop = document.createElement('div');
|
|
250
|
+
pop.className = 'ace-dp';
|
|
251
|
+
pop.setAttribute('role', 'dialog');
|
|
252
|
+
pop.setAttribute('aria-label', 'Choose date');
|
|
253
|
+
document.body.appendChild(pop);
|
|
254
|
+
render();
|
|
255
|
+
position();
|
|
256
|
+
toggleBtn.setAttribute('aria-expanded', 'true');
|
|
257
|
+
var focusEl = pop.querySelector('.ace-dp-day[data-selected]') || pop.querySelector('.ace-dp-day[data-today]');
|
|
258
|
+
if (focusEl && !focusEl.disabled) focusEl.focus({ preventScroll: true });
|
|
259
|
+
pop.addEventListener('click', onPopClick);
|
|
260
|
+
pop.addEventListener('keydown', onKey);
|
|
261
|
+
document.addEventListener('mousedown', onDocDown);
|
|
262
|
+
window.addEventListener('scroll', position, true);
|
|
263
|
+
window.addEventListener('resize', position);
|
|
264
|
+
}
|
|
265
|
+
function close(refocus) {
|
|
266
|
+
if (!st.open) return;
|
|
267
|
+
st.open = false;
|
|
268
|
+
pop.remove(); pop = null;
|
|
269
|
+
toggleBtn.setAttribute('aria-expanded', 'false');
|
|
270
|
+
document.removeEventListener('mousedown', onDocDown);
|
|
271
|
+
window.removeEventListener('scroll', position, true);
|
|
272
|
+
window.removeEventListener('resize', position);
|
|
273
|
+
if (refocus) toggleBtn.focus();
|
|
274
|
+
}
|
|
275
|
+
function onPopClick(e) {
|
|
276
|
+
var t = e.target.closest('button');
|
|
277
|
+
if (!t) return;
|
|
278
|
+
if (t.dataset.act === 'prev' || t.dataset.act === 'next') {
|
|
279
|
+
stepMonth(st, t.dataset.act === 'prev' ? -1 : 1); render();
|
|
280
|
+
pop.querySelector('[data-act="' + t.dataset.act + '"]').focus();
|
|
281
|
+
}
|
|
282
|
+
else if (t.dataset.act === 'years') {
|
|
283
|
+
st.yearsOpen = !st.yearsOpen; render();
|
|
284
|
+
var sel = pop.querySelector('.ace-dp-years [aria-selected="true"]');
|
|
285
|
+
if (sel) sel.scrollIntoView({ block: 'center' });
|
|
286
|
+
}
|
|
287
|
+
else if (t.dataset.year) { st.viewYear = +t.dataset.year; st.yearsOpen = false; render(); }
|
|
288
|
+
else if (t.dataset.iso && !t.disabled) { setValue(t.dataset.iso); close(true); }
|
|
289
|
+
}
|
|
290
|
+
function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); close(true); } }
|
|
291
|
+
function onDocDown(e) { if (!el.contains(e.target) && pop && !pop.contains(e.target)) close(false); }
|
|
292
|
+
|
|
293
|
+
toggleBtn.addEventListener('click', function () { st.open ? close(false) : open(); });
|
|
294
|
+
input.addEventListener('blur', commitTyped);
|
|
295
|
+
input.addEventListener('keydown', function (e) {
|
|
296
|
+
if (e.key === 'Enter') { e.preventDefault(); commitTyped(); }
|
|
297
|
+
if (e.key === 'ArrowDown' && e.altKey) { e.preventDefault(); open(); }
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
el.aceDatepicker = {
|
|
301
|
+
getValue: function () { return st.value; },
|
|
302
|
+
setValue: function (iso) { setValue(iso, true); },
|
|
303
|
+
open: open, close: close
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ════════════════════════ date range ════════════════════════ */
|
|
308
|
+
function initRange(el) {
|
|
309
|
+
var ds = el.dataset;
|
|
310
|
+
var today = toISO(new Date());
|
|
311
|
+
var withTime = ds.time !== 'false';
|
|
312
|
+
var st = {
|
|
313
|
+
start: ds.start || '', end: ds.end || '',
|
|
314
|
+
startTime: ds.startTime || '', endTime: ds.endTime || '',
|
|
315
|
+
min: resolveBound(ds.min), max: resolveBound(ds.max),
|
|
316
|
+
locale: ds.locale || document.documentElement.lang || undefined,
|
|
317
|
+
hover: '', open: false
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
el.innerHTML = '';
|
|
321
|
+
var trigger = document.createElement('button');
|
|
322
|
+
trigger.type = 'button';
|
|
323
|
+
trigger.className = 'ace-input-group ace-date-trigger' + (ds.invalid != null ? ' is-invalid' : '');
|
|
324
|
+
if (ds.id) trigger.id = ds.id;
|
|
325
|
+
if (ds.invalid != null) trigger.setAttribute('aria-invalid', 'true');
|
|
326
|
+
if (ds.disabled != null) trigger.disabled = true;
|
|
327
|
+
trigger.setAttribute('aria-haspopup', 'dialog');
|
|
328
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
329
|
+
el.appendChild(trigger);
|
|
330
|
+
var outStart = hidden(el, ds.startName, st.start);
|
|
331
|
+
var outEnd = hidden(el, ds.endName, st.end);
|
|
332
|
+
var outST = withTime ? hidden(el, ds.startTimeName, st.startTime) : null;
|
|
333
|
+
var outET = withTime ? hidden(el, ds.endTimeName, st.endTime) : null;
|
|
334
|
+
var pop = null, backdrop = null;
|
|
335
|
+
|
|
336
|
+
function triggerLabel() {
|
|
337
|
+
var s = parseISO(st.start), e = parseISO(st.end);
|
|
338
|
+
var short = { day: 'numeric', month: 'short' }, full = { day: 'numeric', month: 'short', year: 'numeric' };
|
|
339
|
+
var txt = '';
|
|
340
|
+
if (s && e) txt = s.toLocaleDateString(st.locale, short) + (st.startTime ? ' ' + st.startTime : '') +
|
|
341
|
+
' → ' + e.toLocaleDateString(st.locale, full) + (st.endTime ? ' ' + st.endTime : '');
|
|
342
|
+
else if (s) txt = s.toLocaleDateString(st.locale, full) + (st.startTime ? ' ' + st.startTime : '');
|
|
343
|
+
return txt;
|
|
344
|
+
}
|
|
345
|
+
function paintTrigger() {
|
|
346
|
+
var txt = triggerLabel();
|
|
347
|
+
trigger.innerHTML = SVG.cal + '<span class="ace-date-label' + (txt ? '' : ' is-empty') + '">' +
|
|
348
|
+
esc(txt || ds.placeholder || 'Pick-up → return') + '</span>';
|
|
349
|
+
}
|
|
350
|
+
function sync() {
|
|
351
|
+
outStart.value = st.start; outEnd.value = st.end;
|
|
352
|
+
if (outST) outST.value = st.startTime;
|
|
353
|
+
if (outET) outET.value = st.endTime;
|
|
354
|
+
paintTrigger();
|
|
355
|
+
emit(el, { start: st.start, end: st.end, startTime: st.startTime, endTime: st.endTime });
|
|
356
|
+
outEnd.dispatchEvent(new Event('change', { bubbles: true }));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function previewEnd() {
|
|
360
|
+
var h = st.hover;
|
|
361
|
+
if (!st.start || st.end || !h || h <= st.start) return '';
|
|
362
|
+
if ((st.min && h < st.min) || (st.max && h > st.max)) return '';
|
|
363
|
+
return h;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderGrid() {
|
|
367
|
+
var pe = previewEnd();
|
|
368
|
+
var cells = monthCells(st.viewYear, st.viewMonth);
|
|
369
|
+
var html = calendarHTML(st, {
|
|
370
|
+
yearPicker: false,
|
|
371
|
+
cells: cells,
|
|
372
|
+
cellHTML: function (c) {
|
|
373
|
+
var iso = c.iso;
|
|
374
|
+
var isStart = !!st.start && iso === st.start;
|
|
375
|
+
var isEnd = !!st.end && iso === st.end;
|
|
376
|
+
var inRange = !!st.start && !!st.end && iso >= st.start && iso <= st.end;
|
|
377
|
+
var inPreview = !!pe && iso >= st.start && iso <= pe;
|
|
378
|
+
var band = inRange ? 'range' : inPreview ? 'preview' : '';
|
|
379
|
+
var bStart = iso === st.start;
|
|
380
|
+
var bEnd = inRange ? isEnd : iso === pe;
|
|
381
|
+
var edge = !band ? '' : bStart && bEnd ? 'both' : bStart ? 'start' : bEnd ? 'end' : '';
|
|
382
|
+
var dis = (st.min && iso < st.min) || (st.max && iso > st.max);
|
|
383
|
+
return dayHTML(c,
|
|
384
|
+
(isStart || isEnd ? ' data-selected' : '') +
|
|
385
|
+
(band === 'range' && !isStart && !isEnd ? ' data-in-range' : '') +
|
|
386
|
+
(pe && iso === pe && pe !== st.start ? ' data-preview-end' : '') +
|
|
387
|
+
(iso === today ? ' data-today aria-current="date"' : '') +
|
|
388
|
+
(!c.current ? ' data-outside' : '') +
|
|
389
|
+
' aria-pressed="' + (isStart || isEnd || band === 'range') + '"' +
|
|
390
|
+
(dis ? ' disabled' : ''),
|
|
391
|
+
(band ? ' data-band="' + band + '"' : '') + (edge ? ' data-band-edge="' + edge + '"' : ''));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
pop.querySelector('.ace-dp-cal').innerHTML = html;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function timeField(which, label) {
|
|
398
|
+
var id = 'ace-dp-' + which + '-' + Math.random().toString(36).slice(2, 8);
|
|
399
|
+
return '<div><label for="' + id + '">' + esc(label) + '</label>' +
|
|
400
|
+
'<div class="ace-input-group">' + SVG.clock +
|
|
401
|
+
'<input id="' + id + '" type="text" inputmode="numeric" placeholder="00:00" maxlength="5" data-time="' + which + '" value="' +
|
|
402
|
+
esc(which === 'start' ? st.startTime : st.endTime) + '"></div></div>';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function open() {
|
|
406
|
+
if (st.open || trigger.disabled) return;
|
|
407
|
+
var d = parseISO(st.start) || new Date();
|
|
408
|
+
st.viewYear = d.getFullYear(); st.viewMonth = d.getMonth();
|
|
409
|
+
st.open = true; st.hover = '';
|
|
410
|
+
backdrop = document.createElement('div');
|
|
411
|
+
backdrop.className = 'ace-dp-backdrop';
|
|
412
|
+
pop = document.createElement('div');
|
|
413
|
+
pop.className = 'ace-dp ace-dp--range';
|
|
414
|
+
pop.setAttribute('role', 'dialog');
|
|
415
|
+
pop.setAttribute('aria-modal', 'true');
|
|
416
|
+
pop.setAttribute('aria-label', 'Choose pick-up and return dates');
|
|
417
|
+
pop.innerHTML = '<div class="ace-dp-cal"></div><div class="ace-dp-foot">' +
|
|
418
|
+
(withTime ? '<div class="ace-dp-times">' + timeField('start', ds.startLabel || 'Pick-up time') + timeField('end', ds.endLabel || 'Return time') + '</div>' : '') +
|
|
419
|
+
'<div class="ace-dp-actions"><button type="button" class="ace-btn ace-btn--secondary ace-btn--sm" data-act="clear">' + esc(ds.clearLabel || 'Clear') + '</button>' +
|
|
420
|
+
'<button type="button" class="ace-btn ace-btn--primary ace-btn--sm" data-act="close">' + esc(ds.closeLabel || 'Close') + '</button></div></div>';
|
|
421
|
+
document.body.appendChild(backdrop);
|
|
422
|
+
document.body.appendChild(pop);
|
|
423
|
+
renderGrid();
|
|
424
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
425
|
+
var f = pop.querySelector('.ace-dp-day[data-selected]') || pop.querySelector('.ace-dp-day[data-today]') || pop.querySelector('.ace-dp-navbtn');
|
|
426
|
+
if (f && !f.disabled) f.focus({ preventScroll: true });
|
|
427
|
+
backdrop.addEventListener('click', function () { close(true); });
|
|
428
|
+
pop.addEventListener('click', onPopClick);
|
|
429
|
+
pop.addEventListener('keydown', onKey);
|
|
430
|
+
pop.addEventListener('mouseover', onHover);
|
|
431
|
+
pop.addEventListener('mouseleave', function () { if (st.hover) { st.hover = ''; renderGrid(); } });
|
|
432
|
+
pop.addEventListener('input', onTimeInput);
|
|
433
|
+
pop.addEventListener('focusout', onTimeBlur);
|
|
434
|
+
}
|
|
435
|
+
function close(refocus) {
|
|
436
|
+
if (!st.open) return;
|
|
437
|
+
st.open = false;
|
|
438
|
+
pop.remove(); backdrop.remove(); pop = backdrop = null;
|
|
439
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
440
|
+
if (refocus) trigger.focus();
|
|
441
|
+
}
|
|
442
|
+
function onPopClick(e) {
|
|
443
|
+
var t = e.target.closest('button');
|
|
444
|
+
if (!t) return;
|
|
445
|
+
if (t.dataset.act === 'prev' || t.dataset.act === 'next') {
|
|
446
|
+
stepMonth(st, t.dataset.act === 'prev' ? -1 : 1); renderGrid();
|
|
447
|
+
pop.querySelector('[data-act="' + t.dataset.act + '"]').focus();
|
|
448
|
+
}
|
|
449
|
+
else if (t.dataset.act === 'close') close(true);
|
|
450
|
+
else if (t.dataset.act === 'clear') {
|
|
451
|
+
st.start = st.end = ''; st.hover = '';
|
|
452
|
+
renderGrid(); sync();
|
|
453
|
+
}
|
|
454
|
+
else if (t.dataset.iso && !t.disabled) {
|
|
455
|
+
var iso = t.dataset.iso;
|
|
456
|
+
// No start yet, or a full range already → start a new range.
|
|
457
|
+
// Clicking before the start moves the start instead.
|
|
458
|
+
if (!st.start || st.end || iso < st.start) { st.start = iso; st.end = ''; }
|
|
459
|
+
else st.end = iso;
|
|
460
|
+
renderGrid(); sync();
|
|
461
|
+
var again = pop.querySelector('.ace-dp-day[data-iso="' + iso + '"]');
|
|
462
|
+
if (again) again.focus({ preventScroll: true });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function onHover(e) {
|
|
466
|
+
var t = e.target.closest('.ace-dp-day');
|
|
467
|
+
var iso = t && !t.disabled ? t.dataset.iso : '';
|
|
468
|
+
if (iso && iso !== st.hover && st.start && !st.end) { st.hover = iso; renderGrid(); }
|
|
469
|
+
}
|
|
470
|
+
function onKey(e) {
|
|
471
|
+
if (e.key === 'Escape') { e.preventDefault(); close(true); return; }
|
|
472
|
+
if (e.key === 'Tab') { // keep focus inside the modal
|
|
473
|
+
var f = pop.querySelectorAll('button:not(:disabled), input');
|
|
474
|
+
var first = f[0], last = f[f.length - 1];
|
|
475
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
476
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function onTimeInput(e) {
|
|
480
|
+
var w = e.target.dataset && e.target.dataset.time;
|
|
481
|
+
if (!w) return;
|
|
482
|
+
e.target.value = maskTime(e.target.value);
|
|
483
|
+
}
|
|
484
|
+
function onTimeBlur(e) {
|
|
485
|
+
var w = e.target.dataset && e.target.dataset.time;
|
|
486
|
+
if (!w) return;
|
|
487
|
+
var v = padTime(e.target.value);
|
|
488
|
+
e.target.value = v;
|
|
489
|
+
if (w === 'start') st.startTime = v; else st.endTime = v;
|
|
490
|
+
sync();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
trigger.addEventListener('click', open);
|
|
494
|
+
paintTrigger();
|
|
495
|
+
|
|
496
|
+
el.aceDatepicker = {
|
|
497
|
+
getValue: function () { return { start: st.start, end: st.end, startTime: st.startTime, endTime: st.endTime }; },
|
|
498
|
+
setValue: function (v) {
|
|
499
|
+
['start', 'end', 'startTime', 'endTime'].forEach(function (k) { if (v && k in v) st[k] = v[k] || ''; });
|
|
500
|
+
outStart.value = st.start; outEnd.value = st.end;
|
|
501
|
+
if (outST) outST.value = st.startTime;
|
|
502
|
+
if (outET) outET.value = st.endTime;
|
|
503
|
+
paintTrigger();
|
|
504
|
+
},
|
|
505
|
+
open: open, close: close
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function init(root) {
|
|
510
|
+
root = root || document;
|
|
511
|
+
root.querySelectorAll('[data-ace-datepicker]').forEach(function (el) { if (!el.aceDatepicker) initSingle(el); });
|
|
512
|
+
root.querySelectorAll('[data-ace-daterange]').forEach(function (el) { if (!el.aceDatepicker) initRange(el); });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
window.AceDatePicker = { init: init };
|
|
516
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { init(); });
|
|
517
|
+
else init();
|
|
518
|
+
})();
|