maquina-components 0.7.4 → 0.8.1
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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/calendar.css +48 -1
- data/app/assets/stylesheets/date_picker.css +13 -1
- data/app/helpers/maquina_components/calendar_helper.rb +37 -0
- data/app/javascript/controllers/calendar_controller.js +108 -12
- data/app/javascript/controllers/date_picker_controller.js +13 -0
- data/app/views/components/_calendar.html.erb +28 -3
- data/app/views/components/_date_picker.html.erb +6 -2
- data/app/views/components/calendar/_header.html.erb +28 -4
- data/app/views/components/calendar/_week.html.erb +3 -1
- data/lib/maquina_components/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cc64fa3b021d92f3dd1e54f71c762404e3c7778a137f2eee3ca4a33d28c247b
|
|
4
|
+
data.tar.gz: aa3a45337cc8d43e1c47e884750418b198c2afde379cc74858a9c866b7aa0bec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9568653a565de12c6a0485663fc48b7f31b75855da80a5cbbb118df5025ecae1a8e3bcf88eeaf43d86e42bd2c1f458b2dc4aebe9deff0e3e8276689a98a75185
|
|
7
|
+
data.tar.gz: cce7f348df9e0594b464366b492efbc76270698e44d76316bcd6c11e2aacf9d2c99c6c2dfda2b3cc40da07705c4480bc4cb0c43d2701bbb661a2645b0dfde283
|
|
@@ -7,15 +7,24 @@
|
|
|
7
7
|
* Structure:
|
|
8
8
|
* - calendar (root container)
|
|
9
9
|
* - header (navigation)
|
|
10
|
+
* - caption, or with caption_layout: :dropdown
|
|
11
|
+
* dropdowns > month-select, year-select (native [data-component="select"])
|
|
10
12
|
* - weekdays (day name headers)
|
|
11
13
|
* - grid (day buttons grid)
|
|
12
14
|
* - week (row)
|
|
13
15
|
* - day (button)
|
|
16
|
+
* - footer (optional, the partial's block)
|
|
17
|
+
*
|
|
18
|
+
* Inside a popover the controller sets data-side="top|bottom" on the root.
|
|
19
|
+
* Flipped above its trigger, a popover is pinned at its bottom edge while
|
|
20
|
+
* months change height (4-6 weeks), so [data-side="top"] moves the header
|
|
21
|
+
* last — next to the trigger, where the arrows stay put.
|
|
14
22
|
*
|
|
15
23
|
* Cascade order in this file (full contract documented in form.css):
|
|
16
24
|
* 1. base [data-component="calendar"], [data-calendar-part="…"]
|
|
17
25
|
* 2. sizes — none (cell metrics come from --cell-size)
|
|
18
|
-
* 3. variants [data-calendar-part="day"]:where([data-today], [data-state="…"], [data-outside])
|
|
26
|
+
* 3. variants [data-calendar-part="day"]:where([data-today], [data-state="…"], [data-outside]),
|
|
27
|
+
* [data-component="calendar"]:where([data-side="top"])
|
|
19
28
|
* 4. states […]:where(:hover, :focus-visible, :disabled)
|
|
20
29
|
* 5. dark — none; dark-mode differences are tokens (see tokens.css)
|
|
21
30
|
* Every rule is flattened to 0,1,0 with :where() and lives in
|
|
@@ -94,6 +103,26 @@
|
|
|
94
103
|
color: var(--foreground);
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
/* ===== Caption dropdowns =====
|
|
107
|
+
Month and year are native selects styled by form.css (colors, radius,
|
|
108
|
+
focus, chevron, dark mode). Here they only shrink to fit beside the
|
|
109
|
+
arrows in a 7-cell header: calendar.css loads after form.css, so these
|
|
110
|
+
0,1,0 rules win on source order. */
|
|
111
|
+
[data-calendar-part="dropdowns"] {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
@apply gap-1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:where([data-calendar-part="dropdowns"]) [data-component="select"] {
|
|
118
|
+
width: auto;
|
|
119
|
+
height: var(--cell-size);
|
|
120
|
+
@apply pl-2 pr-6 py-0 text-sm;
|
|
121
|
+
font-weight: var(--label-weight, 500);
|
|
122
|
+
background-position: right 0.25rem center;
|
|
123
|
+
background-size: 1rem 1rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
/* ===== Weekday Headers ===== */
|
|
98
127
|
[data-calendar-part="weekdays"] {
|
|
99
128
|
display: grid;
|
|
@@ -123,6 +152,11 @@
|
|
|
123
152
|
grid-template-columns: repeat(7, var(--cell-size));
|
|
124
153
|
}
|
|
125
154
|
|
|
155
|
+
/* ===== Footer ===== */
|
|
156
|
+
[data-calendar-part="footer"] {
|
|
157
|
+
@apply mt-4;
|
|
158
|
+
}
|
|
159
|
+
|
|
126
160
|
/* ===== Day Button Base ===== */
|
|
127
161
|
[data-calendar-part="day"] {
|
|
128
162
|
display: inline-flex;
|
|
@@ -205,6 +239,19 @@
|
|
|
205
239
|
@apply opacity-30;
|
|
206
240
|
}
|
|
207
241
|
|
|
242
|
+
/* ===== Popover side ===== */
|
|
243
|
+
|
|
244
|
+
/* Opened above the trigger: weekdays, grid, footer, then the header */
|
|
245
|
+
[data-component="calendar"]:where([data-side="top"]) {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
:where([data-component="calendar"][data-side="top"]) [data-calendar-part="header"] {
|
|
251
|
+
order: 1;
|
|
252
|
+
@apply mt-4 mb-0;
|
|
253
|
+
}
|
|
254
|
+
|
|
208
255
|
/* ===== Day States ===== */
|
|
209
256
|
|
|
210
257
|
/* Hover — only for cells with no selection state of their own */
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* 2. sizes :where([data-size="sm"|"lg"]) [data-date-picker-part="trigger"]
|
|
15
15
|
* 3. variants :where([data-full-width]) …
|
|
16
16
|
* 4. states […]:where(:hover, :focus-visible, :disabled, [aria-expanded],
|
|
17
|
-
* :has(input:invalid))
|
|
17
|
+
* :has(input:invalid)); popover open/closed, and
|
|
18
|
+
* :has(calendar[data-side="top"]) for the animation
|
|
18
19
|
* 5. dark — none; dark-mode differences are tokens (see tokens.css)
|
|
19
20
|
* Every rule is flattened to 0,1,0 with :where() and lives in
|
|
20
21
|
* @layer components, so a caller's utility class wins and source order is
|
|
@@ -160,6 +161,17 @@
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
/* Opened above the trigger (the calendar's data-side): grow upward out of it */
|
|
165
|
+
[data-date-picker-part="popover"]:where(:not(:popover-open):has([data-component="calendar"][data-side="top"])) {
|
|
166
|
+
transform: translateY(0.5rem) scale(0.95);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@starting-style {
|
|
170
|
+
[data-date-picker-part="popover"]:where(:popover-open:has([data-component="calendar"][data-side="top"])) {
|
|
171
|
+
transform: translateY(0.5rem) scale(0.95);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
163
175
|
/* Backdrop */
|
|
164
176
|
[data-date-picker-part="popover"]::backdrop {
|
|
165
177
|
background-color: transparent;
|
|
@@ -72,6 +72,43 @@ module MaquinaComponents
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Years offered by the caption's year select (caption_layout: :dropdown)
|
|
76
|
+
#
|
|
77
|
+
# min_date/max_date set the bounds when present; otherwise years applies,
|
|
78
|
+
# 100 years back to 10 ahead by default. Given both, the narrower bound
|
|
79
|
+
# wins. The displayed year is always included.
|
|
80
|
+
#
|
|
81
|
+
# @param display_year [Integer] Year the calendar shows
|
|
82
|
+
# @param min_date [Date, nil] Minimum selectable date
|
|
83
|
+
# @param max_date [Date, nil] Maximum selectable date
|
|
84
|
+
# @param years [Range, nil] Explicit span of years
|
|
85
|
+
# @return [Range]
|
|
86
|
+
def calendar_year_range(display_year, min_date: nil, max_date: nil, years: nil)
|
|
87
|
+
first, last = (years || ((Date.current.year - 100)..(Date.current.year + 10))).minmax
|
|
88
|
+
first = min_date.year if min_date && (years.nil? || min_date.year > first)
|
|
89
|
+
last = max_date.year if max_date && (years.nil? || max_date.year < last)
|
|
90
|
+
|
|
91
|
+
[first, display_year].min..[last, display_year].max
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Options for the caption's month select: abbreviated localized names
|
|
95
|
+
# (the full ones do not fit beside the year in a 7-cell header), with the
|
|
96
|
+
# months wholly outside min_date/max_date disabled for that year
|
|
97
|
+
#
|
|
98
|
+
# @param year [Integer] Year the months belong to
|
|
99
|
+
# @param min_date [Date, nil] Minimum selectable date
|
|
100
|
+
# @param max_date [Date, nil] Maximum selectable date
|
|
101
|
+
# @return [Array<Array(String, Integer, Boolean)>] [name, month, disabled]
|
|
102
|
+
def calendar_month_options(year, min_date: nil, max_date: nil)
|
|
103
|
+
I18n.t("date.abbr_month_names").drop(1).each_with_index.map do |name, index|
|
|
104
|
+
first_of_month = Date.new(year, index + 1, 1)
|
|
105
|
+
disabled = (min_date && first_of_month.end_of_month < min_date) ||
|
|
106
|
+
(max_date && first_of_month > max_date)
|
|
107
|
+
|
|
108
|
+
[name, index + 1, !!disabled]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
75
112
|
# Check if a date falls within a range
|
|
76
113
|
#
|
|
77
114
|
# @param date [Date] The date to check
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = ["day", "input", "inputEnd", "prevButton", "nextButton", "grid", "caption"]
|
|
4
|
+
static targets = ["day", "input", "inputEnd", "prevButton", "nextButton", "grid", "caption", "monthSelect", "yearSelect"]
|
|
5
5
|
|
|
6
6
|
static values = {
|
|
7
7
|
month: Number,
|
|
@@ -13,12 +13,47 @@ export default class extends Controller {
|
|
|
13
13
|
mode: { type: String, default: "single" },
|
|
14
14
|
weekStartsOn: { type: String, default: "sunday" },
|
|
15
15
|
disabledDates: Array,
|
|
16
|
-
showOutsideDays: { type: Boolean, default: true }
|
|
16
|
+
showOutsideDays: { type: Boolean, default: true },
|
|
17
|
+
// false: a click on the selected day keeps it (single mode), and the start
|
|
18
|
+
// clicked again makes a one-day range (range mode), so a required date
|
|
19
|
+
// can't be emptied by a click
|
|
20
|
+
deselectable: { type: Boolean, default: true },
|
|
21
|
+
// Bounds of the caption's year select (caption_layout: :dropdown only)
|
|
22
|
+
startYear: Number,
|
|
23
|
+
endYear: Number
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
connect() {
|
|
20
27
|
this.updateNavigationState()
|
|
21
28
|
this.updateDayStates()
|
|
29
|
+
|
|
30
|
+
// Inside a popover (date picker or a caller's own), track which side of
|
|
31
|
+
// the trigger it opened on, so the header can sit next to the trigger
|
|
32
|
+
this.popover = this.element.closest("[popover]")
|
|
33
|
+
if (this.popover) {
|
|
34
|
+
this.boundUpdateSide = this.updateSide.bind(this)
|
|
35
|
+
this.popover.addEventListener("toggle", this.boundUpdateSide)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
disconnect() {
|
|
40
|
+
this.popover?.removeEventListener("toggle", this.boundUpdateSide)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sets data-side="top|bottom" when the popover is open. A popover flipped
|
|
45
|
+
* above its trigger is pinned at its bottom edge, so CSS moves the header
|
|
46
|
+
* there: month heights differ, and the arrows must not move under the cursor.
|
|
47
|
+
*/
|
|
48
|
+
updateSide() {
|
|
49
|
+
if (!this.popover?.matches(":popover-open")) return
|
|
50
|
+
|
|
51
|
+
const trigger = this.popover.id &&
|
|
52
|
+
document.querySelector(`[popovertarget="${CSS.escape(this.popover.id)}"]`)
|
|
53
|
+
if (!trigger) return
|
|
54
|
+
|
|
55
|
+
const above = this.popover.getBoundingClientRect().top < trigger.getBoundingClientRect().top
|
|
56
|
+
this.element.dataset.side = above ? "top" : "bottom"
|
|
22
57
|
}
|
|
23
58
|
|
|
24
59
|
previousMonth() {
|
|
@@ -45,12 +80,38 @@ export default class extends Controller {
|
|
|
45
80
|
this.navigateToMonth(newMonth, newYear)
|
|
46
81
|
}
|
|
47
82
|
|
|
83
|
+
// Caption dropdowns (caption_layout: :dropdown)
|
|
84
|
+
selectMonth() {
|
|
85
|
+
this.navigateToMonth(Number(this.monthSelectTarget.value), this.yearValue)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
selectYear() {
|
|
89
|
+
const year = Number(this.yearSelectTarget.value)
|
|
90
|
+
this.navigateToMonth(this.clampMonth(this.monthValue, year), year)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A year change keeps the month, unless min/max date rule it out that year
|
|
94
|
+
clampMonth(month, year) {
|
|
95
|
+
const [minYear, minMonth] = (this.minDateValue || "").split("-").map(Number)
|
|
96
|
+
const [maxYear, maxMonth] = (this.maxDateValue || "").split("-").map(Number)
|
|
97
|
+
|
|
98
|
+
if (year === minYear && month < minMonth) return minMonth
|
|
99
|
+
if (year === maxYear && month > maxMonth) return maxMonth
|
|
100
|
+
return month
|
|
101
|
+
}
|
|
102
|
+
|
|
48
103
|
navigateToMonth(month, year) {
|
|
104
|
+
// The year select lists only startYear..endYear; never show a month it can't
|
|
105
|
+
if ((this.hasStartYearValue && year < this.startYearValue) ||
|
|
106
|
+
(this.hasEndYearValue && year > this.endYearValue)) return
|
|
107
|
+
|
|
49
108
|
this.monthValue = month
|
|
50
109
|
this.yearValue = year
|
|
51
110
|
|
|
52
111
|
this.rebuildCalendar()
|
|
112
|
+
this.syncDropdowns()
|
|
53
113
|
this.updateNavigationState()
|
|
114
|
+
this.updateSide()
|
|
54
115
|
|
|
55
116
|
this.dispatch("navigate", {
|
|
56
117
|
detail: {
|
|
@@ -131,7 +192,10 @@ export default class extends Controller {
|
|
|
131
192
|
disabledDates.has(dateStr)
|
|
132
193
|
|
|
133
194
|
let dayState = null
|
|
134
|
-
if (isSelected &&
|
|
195
|
+
if (isSelected && isRangeEnd) {
|
|
196
|
+
// A one-day range
|
|
197
|
+
dayState = "selected"
|
|
198
|
+
} else if (isSelected && this.modeValue === "range" && selectedEndStr) {
|
|
135
199
|
dayState = "range-start"
|
|
136
200
|
} else if (isRangeEnd) {
|
|
137
201
|
dayState = "range-end"
|
|
@@ -185,6 +249,8 @@ export default class extends Controller {
|
|
|
185
249
|
|
|
186
250
|
handleSingleSelection(dateStr) {
|
|
187
251
|
if (this.selectedValue === dateStr) {
|
|
252
|
+
// Nothing changes, so nothing is dispatched
|
|
253
|
+
if (!this.deselectableValue) return
|
|
188
254
|
this.selectedValue = ""
|
|
189
255
|
} else {
|
|
190
256
|
this.selectedValue = dateStr
|
|
@@ -211,12 +277,13 @@ export default class extends Controller {
|
|
|
211
277
|
// Clicked before start - swap
|
|
212
278
|
this.selectedEndValue = this.selectedValue
|
|
213
279
|
this.selectedValue = dateStr
|
|
214
|
-
} else if (dateStr === selectedStr) {
|
|
280
|
+
} else if (dateStr === selectedStr && this.deselectableValue) {
|
|
215
281
|
// Clicked same date - clear
|
|
216
282
|
this.selectedValue = ""
|
|
217
283
|
this.selectedEndValue = ""
|
|
218
284
|
} else {
|
|
219
|
-
// Clicked after start
|
|
285
|
+
// Clicked after start, or the start again when not deselectable
|
|
286
|
+
// (a one-day range) - set end
|
|
220
287
|
this.selectedEndValue = dateStr
|
|
221
288
|
}
|
|
222
289
|
}
|
|
@@ -301,7 +368,10 @@ export default class extends Controller {
|
|
|
301
368
|
let state = null
|
|
302
369
|
|
|
303
370
|
if (this.modeValue === "range" && selectedStr && selectedEndStr) {
|
|
304
|
-
if (dateStr === selectedStr) {
|
|
371
|
+
if (dateStr === selectedStr && dateStr === selectedEndStr) {
|
|
372
|
+
// A one-day range
|
|
373
|
+
state = "selected"
|
|
374
|
+
} else if (dateStr === selectedStr) {
|
|
305
375
|
state = "range-start"
|
|
306
376
|
} else if (dateStr === selectedEndStr) {
|
|
307
377
|
state = "range-end"
|
|
@@ -337,14 +407,40 @@ export default class extends Controller {
|
|
|
337
407
|
}
|
|
338
408
|
|
|
339
409
|
updateNavigationState() {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
410
|
+
const firstOfMonth = this.formatDate(new Date(this.yearValue, this.monthValue - 1, 1))
|
|
411
|
+
const lastOfMonth = this.formatDate(new Date(this.yearValue, this.monthValue, 0))
|
|
412
|
+
|
|
413
|
+
if (this.hasPrevButtonTarget) {
|
|
414
|
+
this.prevButtonTarget.disabled = Boolean(
|
|
415
|
+
(this.minDateValue && firstOfMonth <= this.minDateValue) ||
|
|
416
|
+
(this.hasStartYearValue && this.monthValue === 1 && this.yearValue <= this.startYearValue)
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (this.hasNextButtonTarget) {
|
|
421
|
+
this.nextButtonTarget.disabled = Boolean(
|
|
422
|
+
(this.maxDateValue && lastOfMonth >= this.maxDateValue) ||
|
|
423
|
+
(this.hasEndYearValue && this.monthValue === 12 && this.yearValue >= this.endYearValue)
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Mirrors calendar_month_options: a month wholly outside min/max is disabled
|
|
429
|
+
syncDropdowns() {
|
|
430
|
+
if (this.hasMonthSelectTarget) {
|
|
431
|
+
for (const option of this.monthSelectTarget.options) {
|
|
432
|
+
const month = Number(option.value)
|
|
433
|
+
const first = this.formatDate(new Date(this.yearValue, month - 1, 1))
|
|
434
|
+
const last = this.formatDate(new Date(this.yearValue, month, 0))
|
|
435
|
+
|
|
436
|
+
option.disabled = Boolean((this.minDateValue && last < this.minDateValue) ||
|
|
437
|
+
(this.maxDateValue && first > this.maxDateValue))
|
|
438
|
+
}
|
|
439
|
+
this.monthSelectTarget.value = String(this.monthValue)
|
|
343
440
|
}
|
|
344
441
|
|
|
345
|
-
if (this.
|
|
346
|
-
|
|
347
|
-
this.nextButtonTarget.disabled = lastOfMonth >= this.maxDateValue
|
|
442
|
+
if (this.hasYearSelectTarget) {
|
|
443
|
+
this.yearSelectTarget.value = String(this.yearValue)
|
|
348
444
|
}
|
|
349
445
|
}
|
|
350
446
|
|
|
@@ -128,6 +128,19 @@ export default class extends Controller {
|
|
|
128
128
|
})
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Close the popover when the selected day is clicked again and stays
|
|
133
|
+
* selected (a non-deselectable calendar): the click confirms the date.
|
|
134
|
+
* Bound on the calendar, so it runs after the day's own selectDay; a click
|
|
135
|
+
* that picks a new day has already closed the popover in handleChange.
|
|
136
|
+
*/
|
|
137
|
+
confirmSelection(event) {
|
|
138
|
+
const day = event.target.closest("[data-calendar-part='day']")
|
|
139
|
+
if (day?.dataset.state === "selected" && this.popoverTarget.matches(":popover-open")) {
|
|
140
|
+
this.closePopover()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
131
144
|
/**
|
|
132
145
|
* Handle calendar navigate event
|
|
133
146
|
* Can be used to fetch events for the new month
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# locals: (selected: nil, selected_end: nil, month: nil, year: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, cell_size: nil, input_name: nil, input_name_end: nil, css_classes: "", **html_options) %>
|
|
1
|
+
<%# locals: (selected: nil, selected_end: nil, month: nil, year: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, caption_layout: :label, years: nil, deselectable: true, cell_size: nil, input_name: nil, input_name_end: nil, css_classes: "", **html_options) %>
|
|
2
2
|
<%
|
|
3
3
|
# Dates may arrive as Date, Time or ISO strings
|
|
4
4
|
selected_date = calendar_to_date(selected)
|
|
@@ -36,6 +36,18 @@
|
|
|
36
36
|
abbr_day_names.rotate(1) :
|
|
37
37
|
abbr_day_names
|
|
38
38
|
|
|
39
|
+
# caption_layout: :dropdown swaps the caption for month and year selects.
|
|
40
|
+
# Their locals reach the header only then, so an app's own calendar/header
|
|
41
|
+
# override written for the caption keeps working.
|
|
42
|
+
dropdown = caption_layout.to_sym == :dropdown
|
|
43
|
+
year_range = calendar_year_range(display_year, min_date: min_date_parsed, max_date: max_date_parsed, years: years) if dropdown
|
|
44
|
+
header_locals = if dropdown
|
|
45
|
+
{caption_layout: :dropdown, years: year_range,
|
|
46
|
+
month_options: calendar_month_options(display_year, min_date: min_date_parsed, max_date: max_date_parsed)}
|
|
47
|
+
else
|
|
48
|
+
{}
|
|
49
|
+
end
|
|
50
|
+
|
|
39
51
|
merged_data = merge_component_data(html_options,
|
|
40
52
|
controller: "calendar",
|
|
41
53
|
component: "calendar",
|
|
@@ -48,7 +60,10 @@
|
|
|
48
60
|
"calendar-max-date-value": max_date_parsed&.iso8601,
|
|
49
61
|
"calendar-week-starts-on-value": week_starts_on,
|
|
50
62
|
"calendar-disabled-dates-value": disabled_dates_parsed.map(&:iso8601).presence,
|
|
51
|
-
"calendar-show-outside-days-value": show_outside_days
|
|
63
|
+
"calendar-show-outside-days-value": show_outside_days,
|
|
64
|
+
"calendar-deselectable-value": (false unless deselectable),
|
|
65
|
+
"calendar-start-year-value": year_range&.first,
|
|
66
|
+
"calendar-end-year-value": year_range&.last
|
|
52
67
|
)
|
|
53
68
|
|
|
54
69
|
# cell_size and a caller's style: both write the style attribute, so join them
|
|
@@ -80,7 +95,8 @@
|
|
|
80
95
|
<%= render "components/calendar/header",
|
|
81
96
|
month: display_month,
|
|
82
97
|
year: display_year,
|
|
83
|
-
month_name: I18n.l(first_of_month, format: "%B %Y")
|
|
98
|
+
month_name: I18n.l(first_of_month, format: "%B %Y"),
|
|
99
|
+
**header_locals %>
|
|
84
100
|
|
|
85
101
|
<%# Weekday headers %>
|
|
86
102
|
<div data-calendar-part="weekdays">
|
|
@@ -104,4 +120,13 @@
|
|
|
104
120
|
show_outside_days: show_outside_days %>
|
|
105
121
|
<% end %>
|
|
106
122
|
</div>
|
|
123
|
+
|
|
124
|
+
<%# Optional footer (actions such as Clear/Done); stays between the grid and
|
|
125
|
+
a header moved to the bottom when the calendar's popover opens above.
|
|
126
|
+
Without a caller block a partial's yield returns content_for(:layout),
|
|
127
|
+
which is the whole page when rendered from a layout, so that is no footer. %>
|
|
128
|
+
<% footer = capture { yield }.presence %>
|
|
129
|
+
<% if footer && footer != content_for(:layout) %>
|
|
130
|
+
<div data-calendar-part="footer"><%= footer %></div>
|
|
131
|
+
<% end %>
|
|
107
132
|
<% end %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# locals: (selected: nil, selected_end: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, placeholder: nil, input_name: nil, input_name_end: nil, id: nil, disabled: false, required: false, size: :default, full_width: false, css_classes: "", **html_options) %>
|
|
1
|
+
<%# locals: (selected: nil, selected_end: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, caption_layout: :label, years: nil, placeholder: nil, input_name: nil, input_name_end: nil, id: nil, disabled: false, required: false, deselectable: nil, size: :default, full_width: false, css_classes: "", **html_options) %>
|
|
2
2
|
<%
|
|
3
3
|
# Deterministic ID for popover targeting: random ids break idiomorph
|
|
4
4
|
# matching across Turbo morphs
|
|
@@ -81,9 +81,13 @@
|
|
|
81
81
|
disabled_dates: disabled_dates,
|
|
82
82
|
show_outside_days: show_outside_days,
|
|
83
83
|
week_starts_on: week_starts_on,
|
|
84
|
+
caption_layout: caption_layout,
|
|
85
|
+
years: years,
|
|
86
|
+
# A required date can't be emptied by clicking it again
|
|
87
|
+
deselectable: deselectable.nil? ? !required : deselectable,
|
|
84
88
|
data: {
|
|
85
89
|
"date-picker-target": "calendar",
|
|
86
|
-
action: "calendar:change->date-picker#handleChange calendar:navigate->date-picker#handleNavigate"
|
|
90
|
+
action: "calendar:change->date-picker#handleChange calendar:navigate->date-picker#handleNavigate click->date-picker#confirmSelection"
|
|
87
91
|
} %>
|
|
88
92
|
</div>
|
|
89
93
|
<% end %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# locals: (month:, year:, month_name:, css_classes: "", **html_options) %>
|
|
1
|
+
<%# locals: (month:, year:, month_name:, caption_layout: :label, years: nil, month_options: nil, css_classes: "", **html_options) %>
|
|
2
2
|
<% merged_data = merge_component_data(html_options, "calendar-part": "header") %>
|
|
3
3
|
|
|
4
4
|
<%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
|
|
@@ -9,9 +9,33 @@
|
|
|
9
9
|
<%= builtin_icon_for :chevron_left, class: "size-4" %>
|
|
10
10
|
</button>
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
<% if caption_layout.to_sym == :dropdown %>
|
|
13
|
+
<div data-calendar-part="dropdowns">
|
|
14
|
+
<select data-component="select"
|
|
15
|
+
data-calendar-part="month-select"
|
|
16
|
+
data-calendar-target="monthSelect"
|
|
17
|
+
data-action="change->calendar#selectMonth"
|
|
18
|
+
aria-label="<%= t("maquina_components.calendar.month", default: "Month") %>">
|
|
19
|
+
<% month_options.each do |name, value, disabled| %>
|
|
20
|
+
<%= tag.option name, value: value, selected: value == month, disabled: disabled %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</select>
|
|
23
|
+
|
|
24
|
+
<select data-component="select"
|
|
25
|
+
data-calendar-part="year-select"
|
|
26
|
+
data-calendar-target="yearSelect"
|
|
27
|
+
data-action="change->calendar#selectYear"
|
|
28
|
+
aria-label="<%= t("maquina_components.calendar.year", default: "Year") %>">
|
|
29
|
+
<% years.each do |value| %>
|
|
30
|
+
<%= tag.option value, value: value, selected: value == year %>
|
|
31
|
+
<% end %>
|
|
32
|
+
</select>
|
|
33
|
+
</div>
|
|
34
|
+
<% else %>
|
|
35
|
+
<div data-calendar-part="caption">
|
|
36
|
+
<%= month_name %>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
15
39
|
|
|
16
40
|
<button type="button"
|
|
17
41
|
data-action="click->calendar#nextMonth"
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
(max_date && day > max_date) ||
|
|
21
21
|
disabled_dates.include?(day)
|
|
22
22
|
|
|
23
|
-
day_state = if is_selected &&
|
|
23
|
+
day_state = if is_selected && is_range_end
|
|
24
|
+
"selected" # a one-day range
|
|
25
|
+
elsif is_selected && mode == :range && selected_end_date
|
|
24
26
|
"range-start"
|
|
25
27
|
elsif is_range_end
|
|
26
28
|
"range-end"
|