maquina-components 0.7.3 → 0.8.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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/calendar.css +48 -1
- data/app/assets/stylesheets/date_picker.css +13 -1
- data/app/assets/stylesheets/drawer.css +8 -8
- data/app/assets/stylesheets/form.css +58 -4
- data/app/assets/stylesheets/separator.css +6 -1
- data/app/assets/tailwind/maquina_components_engine/tokens.css +1 -0
- data/app/helpers/maquina_components/calendar_helper.rb +69 -0
- data/app/helpers/maquina_components/drawer_helper.rb +3 -5
- data/app/helpers/maquina_components/dropdown_menu_helper.rb +22 -5
- data/app/helpers/maquina_components/empty_helper.rb +3 -3
- data/app/helpers/maquina_components/icons_helper.rb +9 -5
- data/app/helpers/maquina_components/pagination_helper.rb +27 -5
- data/app/javascript/controllers/calendar_controller.js +114 -28
- data/app/javascript/controllers/combobox_controller.js +17 -2
- data/app/javascript/controllers/date_picker_controller.js +57 -21
- data/app/javascript/controllers/drawer_controller.js +66 -44
- data/app/javascript/controllers/drawer_trigger_controller.js +13 -0
- data/app/javascript/controllers/sidebar_controller.js +84 -42
- data/app/javascript/controllers/sidebar_trigger_controller.js +13 -0
- data/app/javascript/controllers/toast_controller.js +2 -3
- data/app/javascript/controllers/toaster_controller.js +23 -1
- data/app/javascript/controllers/toggle_group_controller.js +17 -6
- data/app/views/components/_badge.html.erb +1 -1
- data/app/views/components/_breadcrumbs.html.erb +7 -7
- data/app/views/components/_calendar.html.erb +39 -30
- data/app/views/components/_combobox.html.erb +2 -0
- data/app/views/components/_date_picker.html.erb +21 -33
- data/app/views/components/_drawer.html.erb +1 -1
- data/app/views/components/_menu_button.html.erb +1 -1
- data/app/views/components/_sidebar.html.erb +1 -1
- data/app/views/components/breadcrumbs/_ellipsis.html.erb +1 -1
- data/app/views/components/breadcrumbs/_item.html.erb +1 -1
- data/app/views/components/breadcrumbs/_link.html.erb +1 -1
- data/app/views/components/breadcrumbs/_list.html.erb +1 -1
- data/app/views/components/breadcrumbs/_page.html.erb +1 -1
- data/app/views/components/breadcrumbs/_separator.html.erb +1 -2
- data/app/views/components/calendar/_header.html.erb +28 -4
- data/app/views/components/calendar/_week.html.erb +7 -4
- data/app/views/components/card/_content.html.erb +1 -1
- data/app/views/components/card/_footer.html.erb +3 -10
- data/app/views/components/card/_header.html.erb +1 -1
- data/app/views/components/card/_title.html.erb +1 -1
- data/app/views/components/combobox/_option.html.erb +2 -2
- data/app/views/components/drawer/_close.html.erb +11 -5
- data/app/views/components/drawer/_content.html.erb +1 -1
- data/app/views/components/drawer/_footer.html.erb +1 -1
- data/app/views/components/drawer/_header.html.erb +1 -1
- data/app/views/components/drawer/_provider.html.erb +1 -1
- data/app/views/components/drawer/_trigger.html.erb +4 -2
- data/app/views/components/pagination/_ellipsis.html.erb +5 -8
- data/app/views/components/pagination/_link.html.erb +1 -1
- data/app/views/components/pagination/_next.html.erb +6 -7
- data/app/views/components/pagination/_previous.html.erb +6 -7
- data/app/views/components/sidebar/_content.html.erb +1 -1
- data/app/views/components/sidebar/_footer.html.erb +1 -1
- data/app/views/components/sidebar/_group.html.erb +1 -1
- data/app/views/components/sidebar/_header.html.erb +1 -1
- data/app/views/components/sidebar/_inset.html.erb +1 -1
- data/app/views/components/sidebar/_menu.html.erb +1 -1
- data/app/views/components/sidebar/_menu_button.html.erb +1 -1
- data/app/views/components/sidebar/_menu_item.html.erb +1 -1
- data/app/views/components/sidebar/_menu_link.html.erb +3 -3
- data/app/views/components/sidebar/_provider.html.erb +1 -1
- data/app/views/components/sidebar/_trigger.html.erb +1 -1
- data/app/views/components/stats/_stats_card.html.erb +7 -6
- data/app/views/components/toast/_action.html.erb +2 -1
- data/lib/generators/maquina_components/install/templates/theme.css.tt +11 -0
- data/lib/maquina_components/version.rb +1 -1
- metadata +1 -1
|
@@ -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,
|
|
@@ -11,12 +11,45 @@ export default class extends Controller {
|
|
|
11
11
|
minDate: String,
|
|
12
12
|
maxDate: String,
|
|
13
13
|
mode: { type: String, default: "single" },
|
|
14
|
-
weekStartsOn: { type: String, default: "sunday" }
|
|
14
|
+
weekStartsOn: { type: String, default: "sunday" },
|
|
15
|
+
disabledDates: Array,
|
|
16
|
+
showOutsideDays: { type: Boolean, default: true },
|
|
17
|
+
// Bounds of the caption's year select (caption_layout: :dropdown only)
|
|
18
|
+
startYear: Number,
|
|
19
|
+
endYear: Number
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
connect() {
|
|
18
23
|
this.updateNavigationState()
|
|
19
24
|
this.updateDayStates()
|
|
25
|
+
|
|
26
|
+
// Inside a popover (date picker or a caller's own), track which side of
|
|
27
|
+
// the trigger it opened on, so the header can sit next to the trigger
|
|
28
|
+
this.popover = this.element.closest("[popover]")
|
|
29
|
+
if (this.popover) {
|
|
30
|
+
this.boundUpdateSide = this.updateSide.bind(this)
|
|
31
|
+
this.popover.addEventListener("toggle", this.boundUpdateSide)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
disconnect() {
|
|
36
|
+
this.popover?.removeEventListener("toggle", this.boundUpdateSide)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sets data-side="top|bottom" when the popover is open. A popover flipped
|
|
41
|
+
* above its trigger is pinned at its bottom edge, so CSS moves the header
|
|
42
|
+
* there: month heights differ, and the arrows must not move under the cursor.
|
|
43
|
+
*/
|
|
44
|
+
updateSide() {
|
|
45
|
+
if (!this.popover?.matches(":popover-open")) return
|
|
46
|
+
|
|
47
|
+
const trigger = this.popover.id &&
|
|
48
|
+
document.querySelector(`[popovertarget="${CSS.escape(this.popover.id)}"]`)
|
|
49
|
+
if (!trigger) return
|
|
50
|
+
|
|
51
|
+
const above = this.popover.getBoundingClientRect().top < trigger.getBoundingClientRect().top
|
|
52
|
+
this.element.dataset.side = above ? "top" : "bottom"
|
|
20
53
|
}
|
|
21
54
|
|
|
22
55
|
previousMonth() {
|
|
@@ -43,12 +76,38 @@ export default class extends Controller {
|
|
|
43
76
|
this.navigateToMonth(newMonth, newYear)
|
|
44
77
|
}
|
|
45
78
|
|
|
79
|
+
// Caption dropdowns (caption_layout: :dropdown)
|
|
80
|
+
selectMonth() {
|
|
81
|
+
this.navigateToMonth(Number(this.monthSelectTarget.value), this.yearValue)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
selectYear() {
|
|
85
|
+
const year = Number(this.yearSelectTarget.value)
|
|
86
|
+
this.navigateToMonth(this.clampMonth(this.monthValue, year), year)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A year change keeps the month, unless min/max date rule it out that year
|
|
90
|
+
clampMonth(month, year) {
|
|
91
|
+
const [minYear, minMonth] = (this.minDateValue || "").split("-").map(Number)
|
|
92
|
+
const [maxYear, maxMonth] = (this.maxDateValue || "").split("-").map(Number)
|
|
93
|
+
|
|
94
|
+
if (year === minYear && month < minMonth) return minMonth
|
|
95
|
+
if (year === maxYear && month > maxMonth) return maxMonth
|
|
96
|
+
return month
|
|
97
|
+
}
|
|
98
|
+
|
|
46
99
|
navigateToMonth(month, year) {
|
|
100
|
+
// The year select lists only startYear..endYear; never show a month it can't
|
|
101
|
+
if ((this.hasStartYearValue && year < this.startYearValue) ||
|
|
102
|
+
(this.hasEndYearValue && year > this.endYearValue)) return
|
|
103
|
+
|
|
47
104
|
this.monthValue = month
|
|
48
105
|
this.yearValue = year
|
|
49
106
|
|
|
50
107
|
this.rebuildCalendar()
|
|
108
|
+
this.syncDropdowns()
|
|
51
109
|
this.updateNavigationState()
|
|
110
|
+
this.updateSide()
|
|
52
111
|
|
|
53
112
|
this.dispatch("navigate", {
|
|
54
113
|
detail: {
|
|
@@ -98,30 +157,35 @@ export default class extends Controller {
|
|
|
98
157
|
weeks.push(week)
|
|
99
158
|
}
|
|
100
159
|
|
|
101
|
-
// Parse dates for comparison
|
|
102
|
-
const selectedDate = this.selectedValue ? new Date(this.selectedValue) : null
|
|
103
|
-
const selectedEndDate = this.selectedEndValue ? new Date(this.selectedEndValue) : null
|
|
104
|
-
const minDate = this.minDateValue ? new Date(this.minDateValue) : null
|
|
105
|
-
const maxDate = this.maxDateValue ? new Date(this.maxDateValue) : null
|
|
106
|
-
|
|
107
160
|
// Rebuild grid HTML
|
|
108
|
-
grid.innerHTML = weeks.map(week => this.buildWeekHTML(week, month
|
|
161
|
+
grid.innerHTML = weeks.map(week => this.buildWeekHTML(week, month)).join('')
|
|
109
162
|
}
|
|
110
163
|
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
// Mirrors calendar/_week.html.erb. Dates compare as ISO strings, which sort
|
|
165
|
+
// lexicographically and avoid the UTC shift of new Date("YYYY-MM-DD").
|
|
166
|
+
buildWeekHTML(days, displayMonth) {
|
|
113
167
|
const selectedStr = this.selectedValue || ""
|
|
114
168
|
const selectedEndStr = this.selectedEndValue || ""
|
|
169
|
+
const minStr = this.minDateValue || ""
|
|
170
|
+
const maxStr = this.maxDateValue || ""
|
|
171
|
+
const disabledDates = new Set(this.disabledDatesValue)
|
|
115
172
|
|
|
116
173
|
const daysHTML = days.map(day => {
|
|
117
174
|
const dateStr = this.formatDate(day)
|
|
118
175
|
const isOutside = day.getMonth() + 1 !== displayMonth
|
|
176
|
+
|
|
177
|
+
// A hidden outside day keeps its grid column
|
|
178
|
+
if (isOutside && !this.showOutsideDaysValue) {
|
|
179
|
+
return '<span aria-hidden="true" data-calendar-part="outside-day"></span>'
|
|
180
|
+
}
|
|
119
181
|
const isToday = this.isSameDate(day, new Date())
|
|
120
182
|
const isSelected = selectedStr && dateStr === selectedStr
|
|
121
183
|
const isRangeEnd = selectedEndStr && dateStr === selectedEndStr
|
|
122
184
|
// ISO date strings are lexicographically sortable
|
|
123
185
|
const isRangeMiddle = selectedStr && selectedEndStr && dateStr > selectedStr && dateStr < selectedEndStr
|
|
124
|
-
const isDisabled = (
|
|
186
|
+
const isDisabled = (minStr && dateStr < minStr) ||
|
|
187
|
+
(maxStr && dateStr > maxStr) ||
|
|
188
|
+
disabledDates.has(dateStr)
|
|
125
189
|
|
|
126
190
|
let dayState = null
|
|
127
191
|
if (isSelected && this.modeValue === "range" && selectedEndStr) {
|
|
@@ -221,7 +285,8 @@ export default class extends Controller {
|
|
|
221
285
|
|
|
222
286
|
handleKeydown(event) {
|
|
223
287
|
const currentButton = event.currentTarget
|
|
224
|
-
|
|
288
|
+
// Local midnight: a bare "YYYY-MM-DD" parses as UTC and shifts a day west of it
|
|
289
|
+
const currentDate = new Date(currentButton.dataset.date + "T00:00:00")
|
|
225
290
|
let targetDate = null
|
|
226
291
|
|
|
227
292
|
switch (event.key) {
|
|
@@ -269,8 +334,9 @@ export default class extends Controller {
|
|
|
269
334
|
const dateStr = this.formatDate(date)
|
|
270
335
|
const targetButton = this.dayTargets.find(btn => btn.dataset.date === dateStr)
|
|
271
336
|
|
|
272
|
-
if (targetButton
|
|
273
|
-
|
|
337
|
+
if (targetButton) {
|
|
338
|
+
// A disabled day in the visible month is not focusable; stay put
|
|
339
|
+
if (!targetButton.disabled) targetButton.focus()
|
|
274
340
|
} else {
|
|
275
341
|
this.navigateToMonth(date.getMonth() + 1, date.getFullYear())
|
|
276
342
|
requestAnimationFrame(() => {
|
|
@@ -328,16 +394,40 @@ export default class extends Controller {
|
|
|
328
394
|
}
|
|
329
395
|
|
|
330
396
|
updateNavigationState() {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
397
|
+
const firstOfMonth = this.formatDate(new Date(this.yearValue, this.monthValue - 1, 1))
|
|
398
|
+
const lastOfMonth = this.formatDate(new Date(this.yearValue, this.monthValue, 0))
|
|
399
|
+
|
|
400
|
+
if (this.hasPrevButtonTarget) {
|
|
401
|
+
this.prevButtonTarget.disabled = Boolean(
|
|
402
|
+
(this.minDateValue && firstOfMonth <= this.minDateValue) ||
|
|
403
|
+
(this.hasStartYearValue && this.monthValue === 1 && this.yearValue <= this.startYearValue)
|
|
404
|
+
)
|
|
335
405
|
}
|
|
336
406
|
|
|
337
|
-
if (this.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
407
|
+
if (this.hasNextButtonTarget) {
|
|
408
|
+
this.nextButtonTarget.disabled = Boolean(
|
|
409
|
+
(this.maxDateValue && lastOfMonth >= this.maxDateValue) ||
|
|
410
|
+
(this.hasEndYearValue && this.monthValue === 12 && this.yearValue >= this.endYearValue)
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Mirrors calendar_month_options: a month wholly outside min/max is disabled
|
|
416
|
+
syncDropdowns() {
|
|
417
|
+
if (this.hasMonthSelectTarget) {
|
|
418
|
+
for (const option of this.monthSelectTarget.options) {
|
|
419
|
+
const month = Number(option.value)
|
|
420
|
+
const first = this.formatDate(new Date(this.yearValue, month - 1, 1))
|
|
421
|
+
const last = this.formatDate(new Date(this.yearValue, month, 0))
|
|
422
|
+
|
|
423
|
+
option.disabled = Boolean((this.minDateValue && last < this.minDateValue) ||
|
|
424
|
+
(this.maxDateValue && first > this.maxDateValue))
|
|
425
|
+
}
|
|
426
|
+
this.monthSelectTarget.value = String(this.monthValue)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (this.hasYearSelectTarget) {
|
|
430
|
+
this.yearSelectTarget.value = String(this.yearValue)
|
|
341
431
|
}
|
|
342
432
|
}
|
|
343
433
|
|
|
@@ -348,12 +438,8 @@ export default class extends Controller {
|
|
|
348
438
|
selectedEnd: this.selectedEndValue || null
|
|
349
439
|
}
|
|
350
440
|
|
|
441
|
+
// Stimulus dispatches a bubbling "calendar:change" — once
|
|
351
442
|
this.dispatch("change", { detail })
|
|
352
|
-
|
|
353
|
-
this.element.dispatchEvent(new CustomEvent("calendar:change", {
|
|
354
|
-
bubbles: true,
|
|
355
|
-
detail
|
|
356
|
-
}))
|
|
357
443
|
}
|
|
358
444
|
|
|
359
445
|
formatDate(date) {
|
|
@@ -9,9 +9,10 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
9
9
|
* - Single selection with toggle
|
|
10
10
|
* - Keyboard navigation
|
|
11
11
|
* - Focus management
|
|
12
|
+
* - Form submission through a hidden input (rendered when `name:` is given)
|
|
12
13
|
*/
|
|
13
14
|
export default class extends Controller {
|
|
14
|
-
static targets = ["trigger", "content", "input", "option", "empty", "label"]
|
|
15
|
+
static targets = ["trigger", "content", "input", "option", "empty", "label", "hiddenInput"]
|
|
15
16
|
|
|
16
17
|
static values = {
|
|
17
18
|
value: { type: String, default: "" },
|
|
@@ -171,6 +172,9 @@ export default class extends Controller {
|
|
|
171
172
|
|
|
172
173
|
this.updateSelection()
|
|
173
174
|
this.updateTriggerState()
|
|
175
|
+
// valueValueChanged runs later (attribute observer); write the input now so
|
|
176
|
+
// it is current when combobox:change fires
|
|
177
|
+
this.updateHiddenInput()
|
|
174
178
|
|
|
175
179
|
// Close popover
|
|
176
180
|
if (this.hasContentTarget) {
|
|
@@ -326,9 +330,20 @@ export default class extends Controller {
|
|
|
326
330
|
}
|
|
327
331
|
}
|
|
328
332
|
|
|
329
|
-
// Value change callback
|
|
333
|
+
// Value change callback: also covers a programmatic
|
|
334
|
+
// data-combobox-value-value write
|
|
330
335
|
valueValueChanged() {
|
|
331
336
|
this.updateSelection()
|
|
332
337
|
this.updateTriggerState()
|
|
338
|
+
this.updateHiddenInput()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
updateHiddenInput() {
|
|
342
|
+
if (!this.hasHiddenInputTarget) return
|
|
343
|
+
if (this.hiddenInputTarget.value === this.valueValue) return
|
|
344
|
+
|
|
345
|
+
this.hiddenInputTarget.value = this.valueValue
|
|
346
|
+
// Let form listeners (autosave, validation) see the new value
|
|
347
|
+
this.hiddenInputTarget.dispatchEvent(new Event("change", { bubbles: true }))
|
|
333
348
|
}
|
|
334
349
|
}
|
|
@@ -24,7 +24,10 @@ export default class extends Controller {
|
|
|
24
24
|
selectedEnd: String,
|
|
25
25
|
format: { type: String, default: "long" },
|
|
26
26
|
placeholder: String,
|
|
27
|
-
placeholderRange: String
|
|
27
|
+
placeholderRange: String,
|
|
28
|
+
// I18n date data from date_picker_locale_data: strftime patterns and
|
|
29
|
+
// localized names, so the display matches the server-rendered text
|
|
30
|
+
locale: Object
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
connect() {
|
|
@@ -188,22 +191,54 @@ export default class extends Controller {
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
/**
|
|
191
|
-
* Format date for display
|
|
194
|
+
* Format date for display, mirroring the server's date_picker_format:
|
|
195
|
+
* the locale's strftime pattern when there is one, Intl otherwise.
|
|
192
196
|
*/
|
|
193
197
|
formatDate(dateStr, format = "long") {
|
|
194
198
|
if (!dateStr) return ""
|
|
195
199
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const options = format === "short"
|
|
199
|
-
? { month: "short", day: "numeric", year: "numeric" }
|
|
200
|
-
: { weekday: "long", month: "long", day: "numeric", year: "numeric" }
|
|
200
|
+
const date = new Date(dateStr + "T00:00:00")
|
|
201
|
+
if (isNaN(date)) return dateStr
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
const pattern = this.localeValue.formats?.[format === "short" ? "short" : "long"]
|
|
204
|
+
if (pattern) return this.strftime(date, pattern)
|
|
205
|
+
|
|
206
|
+
const options = format === "short"
|
|
207
|
+
? { month: "short", day: "numeric", year: "numeric" }
|
|
208
|
+
: { month: "long", day: "numeric", year: "numeric" }
|
|
209
|
+
|
|
210
|
+
return date.toLocaleDateString(document.documentElement.lang || undefined, options)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The strftime subset Rails locale date formats use. "%-d" drops padding.
|
|
215
|
+
*/
|
|
216
|
+
strftime(date, pattern) {
|
|
217
|
+
const locale = this.localeValue
|
|
218
|
+
const pad = (number, width = 2) => String(number).padStart(width, "0")
|
|
219
|
+
|
|
220
|
+
return pattern.replace(/%(-?)([a-zA-Z%])/g, (match, noPad, directive) => {
|
|
221
|
+
const padded = (number) => noPad ? String(number) : pad(number)
|
|
222
|
+
|
|
223
|
+
switch (directive) {
|
|
224
|
+
case "Y": return String(date.getFullYear())
|
|
225
|
+
case "y": return pad(date.getFullYear() % 100)
|
|
226
|
+
case "m": return padded(date.getMonth() + 1)
|
|
227
|
+
case "d": return padded(date.getDate())
|
|
228
|
+
case "e": return noPad ? String(date.getDate()) : String(date.getDate()).padStart(2, " ")
|
|
229
|
+
case "j": {
|
|
230
|
+
const start = new Date(date.getFullYear(), 0, 1)
|
|
231
|
+
const day = Math.round((date - start) / 86400000) + 1
|
|
232
|
+
return noPad ? String(day) : pad(day, 3)
|
|
233
|
+
}
|
|
234
|
+
case "B": return locale.month_names?.[date.getMonth() + 1] ?? match
|
|
235
|
+
case "b": return locale.abbr_month_names?.[date.getMonth() + 1] ?? match
|
|
236
|
+
case "A": return locale.day_names?.[date.getDay()] ?? match
|
|
237
|
+
case "a": return locale.abbr_day_names?.[date.getDay()] ?? match
|
|
238
|
+
case "%": return "%"
|
|
239
|
+
default: return match
|
|
240
|
+
}
|
|
241
|
+
})
|
|
207
242
|
}
|
|
208
243
|
|
|
209
244
|
/**
|
|
@@ -237,20 +272,21 @@ export default class extends Controller {
|
|
|
237
272
|
* Clear the selection
|
|
238
273
|
*/
|
|
239
274
|
clear() {
|
|
275
|
+
const calendarController = this.hasCalendarTarget &&
|
|
276
|
+
this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
|
|
277
|
+
|
|
278
|
+
// The calendar's own calendar:change runs handleChange, which updates the
|
|
279
|
+
// inputs and display and dispatches date-picker:change exactly once
|
|
280
|
+
if (calendarController) {
|
|
281
|
+
calendarController.clear()
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
240
285
|
this.selectedValue = ""
|
|
241
286
|
this.selectedEndValue = ""
|
|
242
287
|
this.updateInputs()
|
|
243
288
|
this.updateDisplay()
|
|
244
289
|
|
|
245
|
-
// Also clear the calendar
|
|
246
|
-
if (this.hasCalendarTarget) {
|
|
247
|
-
const calendarController = this.application.getControllerForElementAndIdentifier(
|
|
248
|
-
this.calendarTarget,
|
|
249
|
-
"calendar"
|
|
250
|
-
)
|
|
251
|
-
calendarController?.clear()
|
|
252
|
-
}
|
|
253
|
-
|
|
254
290
|
this.dispatch("change", {
|
|
255
291
|
detail: { selected: null, selectedEnd: null }
|
|
256
292
|
})
|
|
@@ -20,7 +20,7 @@ export default class extends Controller {
|
|
|
20
20
|
keyboardShortcut: { type: String, default: "d" }
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
static targets = ["drawer", "
|
|
23
|
+
static targets = ["drawer", "backdrop", "panel"]
|
|
24
24
|
|
|
25
25
|
initialize() {
|
|
26
26
|
const cookieValue = this.getCookie(this.cookieNameValue)
|
|
@@ -28,35 +28,60 @@ export default class extends Controller {
|
|
|
28
28
|
? cookieValue === "true"
|
|
29
29
|
: this.defaultOpenValue
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// Set by a same-URL "replace" visit (a page refresh) until it renders
|
|
32
|
+
this._refreshing = false
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
connect() {
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
this.boundTrackVisit = this.trackVisit.bind(this)
|
|
37
|
+
this.boundClearVisit = this.clearVisit.bind(this)
|
|
38
38
|
this.boundTeardown = this.teardown.bind(this)
|
|
39
|
-
this.
|
|
39
|
+
this.boundPreserveAttribute = this.preserveAttribute.bind(this)
|
|
40
40
|
this.boundHandleMorph = this.handleMorph.bind(this)
|
|
41
|
+
document.addEventListener("turbo:visit", this.boundTrackVisit)
|
|
42
|
+
document.addEventListener("turbo:load", this.boundClearVisit)
|
|
41
43
|
document.addEventListener("turbo:before-cache", this.boundTeardown)
|
|
42
|
-
document.addEventListener("turbo:before-morph-
|
|
44
|
+
document.addEventListener("turbo:before-morph-attribute", this.boundPreserveAttribute)
|
|
43
45
|
document.addEventListener("turbo:morph", this.boundHandleMorph)
|
|
44
46
|
|
|
45
47
|
this.updateStateImmediate()
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
disconnect() {
|
|
49
|
-
|
|
51
|
+
document.removeEventListener("turbo:visit", this.boundTrackVisit)
|
|
52
|
+
document.removeEventListener("turbo:load", this.boundClearVisit)
|
|
50
53
|
document.removeEventListener("turbo:before-cache", this.boundTeardown)
|
|
51
|
-
document.removeEventListener("turbo:before-morph-
|
|
54
|
+
document.removeEventListener("turbo:before-morph-attribute", this.boundPreserveAttribute)
|
|
52
55
|
document.removeEventListener("turbo:morph", this.boundHandleMorph)
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
// ============================================================================
|
|
56
|
-
// Turbo
|
|
59
|
+
// Turbo Integration (cache teardown and page-refresh morphs)
|
|
57
60
|
// ============================================================================
|
|
58
61
|
|
|
62
|
+
// A page refresh is a same-path "replace" visit -- Turbo's own isPageRefresh
|
|
63
|
+
// test. It covers Turbo.visit(location.href, { action: "replace" }), refresh
|
|
64
|
+
// broadcasts and same-URL form redirects. An open drawer must survive it.
|
|
65
|
+
trackVisit(event) {
|
|
66
|
+
const { url, action } = event.detail
|
|
67
|
+
this._refreshing = action === "replace" &&
|
|
68
|
+
new URL(url, window.location.href).pathname === window.location.pathname
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clearVisit() {
|
|
72
|
+
this._refreshing = false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Runs before Turbo snapshots the page: real navigation closes the drawer so
|
|
76
|
+
// the cached snapshot starts closed; a page refresh leaves it open. Either
|
|
77
|
+
// way the scroll lock is released so no snapshot is cached locked --
|
|
78
|
+
// handleMorph() re-locks after a refresh.
|
|
59
79
|
teardown() {
|
|
80
|
+
if (this._refreshing) {
|
|
81
|
+
this.unlockScroll()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
this.openValue = false
|
|
61
86
|
|
|
62
87
|
if (this.hasBackdropTarget) {
|
|
@@ -67,25 +92,34 @@ export default class extends Controller {
|
|
|
67
92
|
this.unlockScroll()
|
|
68
93
|
}
|
|
69
94
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
// The morph must not overwrite the attributes this controller owns; the
|
|
96
|
+
// drawer's contents still morph normally.
|
|
97
|
+
preserveAttribute(event) {
|
|
98
|
+
const owned = this.ownedAttributes(event.target)
|
|
99
|
+
if (owned?.includes(event.detail.attributeName)) {
|
|
100
|
+
event.preventDefault()
|
|
73
101
|
}
|
|
74
102
|
}
|
|
75
103
|
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
ownedAttributes(element) {
|
|
105
|
+
if (element === this.element) return ["data-drawer-open-value"]
|
|
106
|
+
if (this.hasDrawerTarget && element === this.drawerTarget) return ["data-state"]
|
|
107
|
+
if (this.hasBackdropTarget && element === this.backdropTarget) return ["data-state", "class"]
|
|
108
|
+
if (this.hasPanelTarget && element === this.panelTarget) return ["aria-hidden", "inert"]
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
78
111
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
112
|
+
// After a morph, keep the live state and re-apply what the morph can still
|
|
113
|
+
// reset: the body scroll lock (<body> is morphed) and the trigger's aria.
|
|
114
|
+
handleMorph() {
|
|
115
|
+
this._refreshing = false
|
|
82
116
|
|
|
83
117
|
if (this.hasDrawerTarget) {
|
|
84
118
|
this.drawerTarget.classList.remove("drawer-loading")
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
this.updateState()
|
|
88
|
-
this.
|
|
122
|
+
this.dispatchStateChange()
|
|
89
123
|
}
|
|
90
124
|
|
|
91
125
|
// ============================================================================
|
|
@@ -108,7 +142,6 @@ export default class extends Controller {
|
|
|
108
142
|
|
|
109
143
|
openValueChanged(new_value, old_value) {
|
|
110
144
|
if (new_value === old_value) return
|
|
111
|
-
if (this._morphing) return
|
|
112
145
|
|
|
113
146
|
this.updateState()
|
|
114
147
|
this.persistState()
|
|
@@ -238,18 +271,26 @@ export default class extends Controller {
|
|
|
238
271
|
|
|
239
272
|
// Styling lives in drawer.css (body[data-maquina-scroll-locked]); the
|
|
240
273
|
// controller only records the scroll offset and flips the attribute
|
|
274
|
+
// Idempotent and owned: the offset is recorded once per lock (re-locking
|
|
275
|
+
// after a morph reset <body> must not record the locked page's offset of 0),
|
|
276
|
+
// and a closed drawer never releases a lock it didn't take -- the sidebar
|
|
277
|
+
// shares the same body attribute.
|
|
241
278
|
lockScroll() {
|
|
242
|
-
this.
|
|
279
|
+
if (!this._scrollLocked) {
|
|
280
|
+
this.scrollPosition = window.scrollY
|
|
281
|
+
this._scrollLocked = true
|
|
282
|
+
}
|
|
243
283
|
document.body.style.setProperty("--maquina-scroll-lock-top", `-${this.scrollPosition}px`)
|
|
244
284
|
document.body.setAttribute("data-maquina-scroll-locked", "")
|
|
245
285
|
}
|
|
246
286
|
|
|
247
287
|
unlockScroll() {
|
|
288
|
+
if (!this._scrollLocked) return
|
|
289
|
+
|
|
290
|
+
this._scrollLocked = false
|
|
248
291
|
document.body.removeAttribute("data-maquina-scroll-locked")
|
|
249
292
|
document.body.style.removeProperty("--maquina-scroll-lock-top")
|
|
250
|
-
|
|
251
|
-
window.scrollTo(0, this.scrollPosition)
|
|
252
|
-
}
|
|
293
|
+
window.scrollTo(0, this.scrollPosition)
|
|
253
294
|
}
|
|
254
295
|
|
|
255
296
|
// ============================================================================
|
|
@@ -265,23 +306,4 @@ export default class extends Controller {
|
|
|
265
306
|
bubbles: true
|
|
266
307
|
})
|
|
267
308
|
}
|
|
268
|
-
|
|
269
|
-
// ============================================================================
|
|
270
|
-
// Utilities
|
|
271
|
-
// ============================================================================
|
|
272
|
-
|
|
273
|
-
checkScreenSize() {
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
debounce(func, wait) {
|
|
277
|
-
let timeout
|
|
278
|
-
return function executedFunction(...args) {
|
|
279
|
-
const later = () => {
|
|
280
|
-
clearTimeout(timeout)
|
|
281
|
-
func(...args)
|
|
282
|
-
}
|
|
283
|
-
clearTimeout(timeout)
|
|
284
|
-
timeout = setTimeout(later, wait)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
309
|
+
}
|
|
@@ -26,11 +26,24 @@ export default class extends Controller {
|
|
|
26
26
|
|
|
27
27
|
connect() {
|
|
28
28
|
this.handleStateChanged = this.handleStateChanged.bind(this)
|
|
29
|
+
this.preserveAttribute = this.preserveAttribute.bind(this)
|
|
29
30
|
window.addEventListener("drawer:stateChanged", this.handleStateChanged)
|
|
31
|
+
this.element.addEventListener("turbo:before-morph-attribute", this.preserveAttribute)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
disconnect() {
|
|
33
35
|
window.removeEventListener("drawer:stateChanged", this.handleStateChanged)
|
|
36
|
+
this.element.removeEventListener("turbo:before-morph-attribute", this.preserveAttribute)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The server never renders these (only the client knows the live state), so
|
|
40
|
+
// without this every page morph strips them from the trigger
|
|
41
|
+
preserveAttribute(event) {
|
|
42
|
+
if (event.target !== this.element) return
|
|
43
|
+
|
|
44
|
+
if (["aria-expanded", "aria-controls", "aria-haspopup"].includes(event.detail.attributeName)) {
|
|
45
|
+
event.preventDefault()
|
|
46
|
+
}
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
drawerOutletConnected(outlet) {
|