phlex_kit 0.4.0 → 0.5.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/phlex_kit/_tokens.css +16 -0
- data/app/assets/stylesheets/phlex_kit/phlex_kit.css +1 -0
- data/app/components/phlex_kit/alert/alert.css +60 -22
- data/app/components/phlex_kit/alert/alert.rb +11 -7
- data/app/components/phlex_kit/alert/alert_action.rb +14 -0
- data/app/components/phlex_kit/alert/alert_title.rb +3 -2
- data/app/components/phlex_kit/alert_dialog/alert_dialog.css +5 -2
- data/app/components/phlex_kit/avatar/avatar.css +80 -9
- data/app/components/phlex_kit/avatar/avatar.rb +6 -4
- data/app/components/phlex_kit/avatar/avatar_badge.rb +15 -0
- data/app/components/phlex_kit/avatar/avatar_group_count.rb +14 -0
- data/app/components/phlex_kit/badge/badge.css +61 -39
- data/app/components/phlex_kit/badge/badge.rb +24 -7
- data/app/components/phlex_kit/breadcrumb/breadcrumb.css +2 -2
- data/app/components/phlex_kit/bubble/bubble.css +49 -8
- data/app/components/phlex_kit/button/button.css +71 -26
- data/app/components/phlex_kit/button/button.rb +20 -13
- data/app/components/phlex_kit/button_group/button_group.css +59 -8
- data/app/components/phlex_kit/button_group/button_group.rb +11 -5
- data/app/components/phlex_kit/button_group/button_group_separator.rb +14 -0
- data/app/components/phlex_kit/button_group/button_group_text.rb +14 -0
- data/app/components/phlex_kit/calendar/calendar.css +66 -15
- data/app/components/phlex_kit/calendar/calendar.rb +87 -19
- data/app/components/phlex_kit/calendar/calendar_controller.js +226 -75
- data/app/components/phlex_kit/calendar/calendar_days.rb +7 -6
- data/app/components/phlex_kit/calendar/calendar_weekdays.rb +3 -1
- data/app/components/phlex_kit/checkbox/checkbox.css +52 -7
- data/app/components/phlex_kit/collapsible/collapsible.css +10 -1
- data/app/components/phlex_kit/collapsible/collapsible_controller.js +9 -2
- data/app/components/phlex_kit/combobox/combobox.css +37 -3
- data/app/components/phlex_kit/combobox/combobox.rb +3 -1
- data/app/components/phlex_kit/combobox/combobox_controller.js +9 -1
- data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +7 -2
- data/app/components/phlex_kit/command/command.css +58 -22
- data/app/components/phlex_kit/command/command_controller.js +3 -1
- data/app/components/phlex_kit/command/command_input.rb +29 -25
- data/app/components/phlex_kit/command/command_separator.rb +18 -0
- data/app/components/phlex_kit/command/command_shortcut.rb +13 -0
- data/app/components/phlex_kit/context_menu/context_menu.css +78 -6
- data/app/components/phlex_kit/context_menu/context_menu_checkbox_item.rb +27 -0
- data/app/components/phlex_kit/context_menu/context_menu_group.rb +10 -0
- data/app/components/phlex_kit/context_menu/context_menu_item.rb +13 -3
- data/app/components/phlex_kit/context_menu/context_menu_radio_group.rb +10 -0
- data/app/components/phlex_kit/context_menu/context_menu_radio_item.rb +30 -0
- data/app/components/phlex_kit/context_menu/context_menu_shortcut.rb +11 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub.rb +11 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub_content.rb +12 -0
- data/app/components/phlex_kit/context_menu/context_menu_sub_trigger.rb +17 -0
- data/app/components/phlex_kit/dialog/dialog.css +34 -16
- data/app/components/phlex_kit/dialog/dialog_close.rb +13 -0
- data/app/components/phlex_kit/dialog/dialog_content.rb +13 -5
- data/app/components/phlex_kit/drawer/drawer.css +56 -17
- data/app/components/phlex_kit/drawer/drawer_content.rb +8 -4
- data/app/components/phlex_kit/empty/empty.css +13 -8
- data/app/components/phlex_kit/field/field.css +152 -0
- data/app/components/phlex_kit/field/field.rb +35 -0
- data/app/components/phlex_kit/field/field_content.rb +11 -0
- data/app/components/phlex_kit/field/field_description.rb +10 -0
- data/app/components/phlex_kit/field/field_error.rb +29 -0
- data/app/components/phlex_kit/field/field_group.rb +12 -0
- data/app/components/phlex_kit/field/field_label.rb +12 -0
- data/app/components/phlex_kit/field/field_legend.rb +17 -0
- data/app/components/phlex_kit/field/field_separator.rb +15 -0
- data/app/components/phlex_kit/field/field_set.rb +10 -0
- data/app/components/phlex_kit/field/field_title.rb +11 -0
- data/app/components/phlex_kit/hover_card/hover_card.css +12 -4
- data/app/components/phlex_kit/hover_card/hover_card_content.rb +11 -2
- data/app/components/phlex_kit/input/input.css +45 -11
- data/app/components/phlex_kit/input_group/input_group.css +88 -12
- data/app/components/phlex_kit/input_group/input_group_addon.rb +11 -4
- data/app/components/phlex_kit/input_group/input_group_button.rb +18 -0
- data/app/components/phlex_kit/input_otp/input_otp.css +29 -5
- data/app/components/phlex_kit/item/item.css +56 -8
- data/app/components/phlex_kit/item/item.rb +13 -3
- data/app/components/phlex_kit/item/item_footer.rb +8 -0
- data/app/components/phlex_kit/item/item_header.rb +8 -0
- data/app/components/phlex_kit/item/item_media.rb +13 -3
- data/app/components/phlex_kit/item/item_separator.rb +10 -0
- data/app/components/phlex_kit/kbd/kbd.css +10 -0
- data/app/components/phlex_kit/marker/marker.css +29 -9
- data/app/components/phlex_kit/marker/marker.rb +13 -3
- data/app/components/phlex_kit/menubar/menubar.css +88 -17
- data/app/components/phlex_kit/menubar/menubar_checkbox_item.rb +26 -0
- data/app/components/phlex_kit/menubar/menubar_group.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_item.rb +9 -4
- data/app/components/phlex_kit/menubar/menubar_label.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_radio_group.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_radio_item.rb +29 -0
- data/app/components/phlex_kit/menubar/menubar_shortcut.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_sub.rb +10 -0
- data/app/components/phlex_kit/menubar/menubar_sub_content.rb +11 -0
- data/app/components/phlex_kit/menubar/menubar_sub_trigger.rb +17 -0
- data/app/components/phlex_kit/message/message.css +8 -1
- data/app/components/phlex_kit/message_scroller/message_scroller.css +18 -0
- data/app/components/phlex_kit/native_select/native_select.css +25 -5
- data/app/components/phlex_kit/navigation_menu/navigation_menu.css +9 -6
- data/app/components/phlex_kit/pagination/pagination.css +10 -3
- data/app/components/phlex_kit/pagination/pagination_link.rb +21 -0
- data/app/components/phlex_kit/pagination/pagination_next.rb +20 -0
- data/app/components/phlex_kit/pagination/pagination_previous.rb +21 -0
- data/app/components/phlex_kit/popover/popover.css +15 -4
- data/app/components/phlex_kit/popover/popover_content.rb +11 -2
- data/app/components/phlex_kit/popover/popover_description.rb +10 -0
- data/app/components/phlex_kit/popover/popover_header.rb +10 -0
- data/app/components/phlex_kit/popover/popover_title.rb +10 -0
- data/app/components/phlex_kit/progress/progress.css +5 -5
- data/app/components/phlex_kit/radio_button/radio_button.css +46 -2
- data/app/components/phlex_kit/radio_group/radio_group.css +2 -2
- data/app/components/phlex_kit/resizable/resizable.css +17 -0
- data/app/components/phlex_kit/resizable/resizable_handle.rb +7 -3
- data/app/components/phlex_kit/select/select.css +46 -10
- data/app/components/phlex_kit/select/select_separator.rb +10 -0
- data/app/components/phlex_kit/select/select_trigger.rb +5 -2
- data/app/components/phlex_kit/sheet/sheet.css +18 -9
- data/app/components/phlex_kit/sheet/sheet_close.rb +10 -0
- data/app/components/phlex_kit/sheet/sheet_content.rb +8 -4
- data/app/components/phlex_kit/sidebar/sidebar.css +76 -9
- data/app/components/phlex_kit/sidebar/sidebar_controller.js +22 -7
- data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +8 -3
- data/app/components/phlex_kit/sidebar/sidebar_rail.rb +21 -0
- data/app/components/phlex_kit/sidebar/sidebar_wrapper.rb +25 -8
- data/app/components/phlex_kit/skeleton/skeleton.css +1 -1
- data/app/components/phlex_kit/slider/slider.css +33 -12
- data/app/components/phlex_kit/switch/switch.css +50 -13
- data/app/components/phlex_kit/switch/switch.rb +5 -2
- data/app/components/phlex_kit/table/table.css +8 -4
- data/app/components/phlex_kit/tabs/tabs.css +72 -8
- data/app/components/phlex_kit/tabs/tabs.rb +7 -2
- data/app/components/phlex_kit/tabs/tabs_list.rb +11 -2
- data/app/components/phlex_kit/textarea/textarea.css +33 -13
- data/app/components/phlex_kit/toast/toast.css +1 -1
- data/app/components/phlex_kit/toggle/toggle.css +25 -10
- data/app/components/phlex_kit/toggle_group/toggle_group.css +7 -6
- data/app/components/phlex_kit/tooltip/tooltip.css +37 -10
- data/app/components/phlex_kit/tooltip/tooltip_content.rb +9 -4
- data/lib/phlex_kit/version.rb +1 -1
- metadata +50 -1
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# Calendar (month grid with day selection), ported from ruby_ui's
|
|
3
|
-
# RubyUI::Calendar
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
3
|
+
# RubyUI::Calendar and extended to shadcn/ui's current calendar surface.
|
|
4
|
+
# Renders the header (title or month/year dropdown caption + prev/next),
|
|
5
|
+
# the <table> the phlex-kit--calendar controller fills, and the <template>s
|
|
6
|
+
# it clones per day state.
|
|
7
|
+
#
|
|
8
|
+
# `mode:` — :single (default), :range (pick start then end;
|
|
9
|
+
# range-start/in-range/range-end day states), or :multiple (toggle days).
|
|
10
|
+
# `selected_date:` / `range_start:`+`range_end:` / `selected_dates:` seed
|
|
11
|
+
# the selection per mode. `min_date:`/`max_date:` clamp; `disabled_dates:`
|
|
12
|
+
# (array of ISO strings or Dates) are unpickable and struck through
|
|
13
|
+
# ("booked"). `week_numbers: true` prepends an ISO week column.
|
|
14
|
+
# `caption_layout: :dropdown` swaps the title for native month/year
|
|
15
|
+
# <select>s (their captionLayout="dropdown"; `from_year:`/`to_year:` bound
|
|
16
|
+
# the year list). Pass `input_id:` a CSS selector (e.g. "#due-date") to
|
|
17
|
+
# push the picked date (or "start – end" range) into a
|
|
18
|
+
# phlex-kit--calendar-input outlet — that's how PhlexKit::DatePicker binds
|
|
19
|
+
# an Input. Sizing rides --pk-cell-size/--pk-cell-radius (calendar.css).
|
|
10
20
|
class Calendar < BaseComponent
|
|
11
|
-
|
|
21
|
+
MODES = { single: "single", range: "range", multiple: "multiple" }.freeze
|
|
22
|
+
CAPTION_LAYOUTS = %i[label dropdown].freeze
|
|
23
|
+
MONTHS = %w[January February March April May June July August September October November December].freeze
|
|
24
|
+
|
|
25
|
+
def initialize(mode: :single, selected_date: nil, selected_dates: [], range_start: nil, range_end: nil,
|
|
26
|
+
min_date: nil, max_date: nil, disabled_dates: [], week_numbers: false,
|
|
27
|
+
caption_layout: :label, from_year: nil, to_year: nil,
|
|
28
|
+
input_id: nil, date_format: "yyyy-MM-dd", **attrs)
|
|
29
|
+
@mode = MODES.fetch(mode.to_sym)
|
|
12
30
|
@selected_date = selected_date
|
|
31
|
+
@selected_dates = Array(selected_dates).map(&:to_s)
|
|
32
|
+
@range_start = range_start
|
|
33
|
+
@range_end = range_end
|
|
13
34
|
@min_date = min_date
|
|
35
|
+
@max_date = max_date
|
|
36
|
+
@disabled_dates = Array(disabled_dates).map(&:to_s)
|
|
37
|
+
@week_numbers = week_numbers
|
|
38
|
+
@caption_layout = caption_layout.to_sym
|
|
39
|
+
raise ArgumentError, "caption_layout must be :label or :dropdown" unless CAPTION_LAYOUTS.include?(@caption_layout)
|
|
40
|
+
@from_year = from_year
|
|
41
|
+
@to_year = to_year
|
|
14
42
|
@input_id = input_id
|
|
15
43
|
@date_format = date_format
|
|
16
44
|
@attrs = attrs
|
|
@@ -19,29 +47,69 @@ module PhlexKit
|
|
|
19
47
|
def view_template
|
|
20
48
|
div(**mix(calendar_attrs, @attrs)) do
|
|
21
49
|
render CalendarHeader.new do
|
|
22
|
-
|
|
50
|
+
if @caption_layout == :dropdown
|
|
51
|
+
render_dropdown_caption
|
|
52
|
+
else
|
|
53
|
+
render CalendarTitle.new
|
|
54
|
+
end
|
|
23
55
|
render CalendarPrev.new
|
|
24
56
|
render CalendarNext.new
|
|
25
57
|
end
|
|
26
58
|
render CalendarBody.new # Where the calendar is rendered (Weekdays and Days)
|
|
27
|
-
render CalendarWeekdays.new # Template for the weekdays
|
|
59
|
+
render CalendarWeekdays.new(week_numbers: @week_numbers) # Template for the weekdays
|
|
28
60
|
render CalendarDays.new # Templates for the day states
|
|
29
61
|
end
|
|
30
62
|
end
|
|
31
63
|
|
|
32
64
|
private
|
|
33
65
|
|
|
66
|
+
def render_dropdown_caption
|
|
67
|
+
view_year = parse_year(@selected_date) || parse_year(@range_start) || Time.now.year
|
|
68
|
+
first_year = @from_year || parse_year(@min_date) || (view_year - 10)
|
|
69
|
+
last_year = @to_year || parse_year(@max_date) || (view_year + 10)
|
|
70
|
+
|
|
71
|
+
div(class: "pk-calendar-dropdowns") do
|
|
72
|
+
select(
|
|
73
|
+
class: "pk-native-select-field pk-calendar-dropdown",
|
|
74
|
+
aria: { label: "Month" },
|
|
75
|
+
data: { phlex_kit__calendar_target: "monthSelect", action: "change->phlex-kit--calendar#setMonth" }
|
|
76
|
+
) do
|
|
77
|
+
MONTHS.each_with_index { |name, index| option(value: index.to_s) { name } }
|
|
78
|
+
end
|
|
79
|
+
select(
|
|
80
|
+
class: "pk-native-select-field pk-calendar-dropdown",
|
|
81
|
+
aria: { label: "Year" },
|
|
82
|
+
data: { phlex_kit__calendar_target: "yearSelect", action: "change->phlex-kit--calendar#setYear" }
|
|
83
|
+
) do
|
|
84
|
+
(first_year..last_year).each { |year| option(value: year.to_s) { year.to_s } }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_year(value)
|
|
90
|
+
value.to_s[/\A(\d{4})/, 1]&.to_i
|
|
91
|
+
end
|
|
92
|
+
|
|
34
93
|
def calendar_attrs
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
94
|
+
data = {
|
|
95
|
+
controller: "phlex-kit--calendar",
|
|
96
|
+
phlex_kit__calendar_mode_value: @mode,
|
|
97
|
+
phlex_kit__calendar_selected_date_value: @selected_date&.to_s,
|
|
98
|
+
phlex_kit__calendar_min_date_value: @min_date&.to_s,
|
|
99
|
+
phlex_kit__calendar_max_date_value: @max_date&.to_s,
|
|
100
|
+
phlex_kit__calendar_format_value: @date_format,
|
|
101
|
+
phlex_kit__calendar_phlex_kit__calendar_input_outlet: @input_id
|
|
44
102
|
}
|
|
103
|
+
data[:phlex_kit__calendar_range_start_value] = @range_start.to_s if @range_start
|
|
104
|
+
data[:phlex_kit__calendar_range_end_value] = @range_end.to_s if @range_end
|
|
105
|
+
data[:phlex_kit__calendar_selected_dates_value] = JSON.generate(@selected_dates) if @selected_dates.any?
|
|
106
|
+
data[:phlex_kit__calendar_disabled_dates_value] = JSON.generate(@disabled_dates) if @disabled_dates.any?
|
|
107
|
+
data[:phlex_kit__calendar_week_numbers_value] = "true" if @week_numbers
|
|
108
|
+
# Seed the view on the selection so the grid opens on the right month.
|
|
109
|
+
view_seed = @selected_date || @range_start || @selected_dates.first
|
|
110
|
+
data[:phlex_kit__calendar_view_date_value] = view_seed.to_s if view_seed
|
|
111
|
+
|
|
112
|
+
{ class: "pk-calendar", data: data }
|
|
45
113
|
end
|
|
46
114
|
end
|
|
47
115
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
|
-
// Ported from ruby_ui's calendar controller
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// (
|
|
3
|
+
// Ported from ruby_ui's calendar controller (mustache replaced by a tiny
|
|
4
|
+
// {{key}} interpolator) and extended to shadcn's current calendar surface:
|
|
5
|
+
// mode single (default) / range / multiple, min/max + explicit disabled
|
|
6
|
+
// dates (rendered struck-through as "booked"), ISO week numbers, and a
|
|
7
|
+
// month/year dropdown caption (native <select>s the controller keeps in
|
|
8
|
+
// sync). Day templates gain a {{state}} class hook so range-start /
|
|
9
|
+
// range-end / in-range / booked ride the same five templates.
|
|
8
10
|
function renderTemplate(template, data) {
|
|
9
11
|
return template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => String(data[key] ?? ""));
|
|
10
12
|
}
|
|
@@ -13,6 +15,8 @@ export default class extends Controller {
|
|
|
13
15
|
static targets = [
|
|
14
16
|
"calendar",
|
|
15
17
|
"title",
|
|
18
|
+
"monthSelect",
|
|
19
|
+
"yearSelect",
|
|
16
20
|
"weekdaysTemplate",
|
|
17
21
|
"disabledDateTemplate",
|
|
18
22
|
"selectedDateTemplate",
|
|
@@ -21,22 +25,20 @@ export default class extends Controller {
|
|
|
21
25
|
"otherMonthDateTemplate",
|
|
22
26
|
];
|
|
23
27
|
static values = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
},
|
|
28
|
+
mode: { type: String, default: "single" },
|
|
29
|
+
selectedDate: { type: String, default: null },
|
|
30
|
+
selectedDates: { type: Array, default: [] },
|
|
31
|
+
rangeStart: { type: String, default: null },
|
|
32
|
+
rangeEnd: { type: String, default: null },
|
|
33
|
+
minDate: { type: String, default: null },
|
|
34
|
+
maxDate: { type: String, default: null },
|
|
35
|
+
disabledDates: { type: Array, default: [] },
|
|
36
|
+
weekNumbers: { type: Boolean, default: false },
|
|
32
37
|
viewDate: {
|
|
33
38
|
type: String,
|
|
34
39
|
default: new Date().toISOString().slice(0, 10),
|
|
35
40
|
},
|
|
36
|
-
format: {
|
|
37
|
-
type: String,
|
|
38
|
-
default: "yyyy-MM-dd", // Default format
|
|
39
|
-
},
|
|
41
|
+
format: { type: String, default: "yyyy-MM-dd" },
|
|
40
42
|
};
|
|
41
43
|
static outlets = ["phlex-kit--calendar-input"];
|
|
42
44
|
|
|
@@ -54,12 +56,70 @@ export default class extends Controller {
|
|
|
54
56
|
this.viewDateValue = this.adjustMonth(-1);
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// month/year dropdown caption (native selects)
|
|
60
|
+
setMonth(e) {
|
|
61
|
+
const date = this.viewDate();
|
|
62
|
+
date.setDate(2);
|
|
63
|
+
date.setMonth(Number(e.target.value));
|
|
64
|
+
this.viewDateValue = date.toISOString().slice(0, 10);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setYear(e) {
|
|
68
|
+
const date = this.viewDate();
|
|
69
|
+
date.setDate(2);
|
|
70
|
+
date.setFullYear(Number(e.target.value));
|
|
71
|
+
this.viewDateValue = date.toISOString().slice(0, 10);
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
selectDay(e) {
|
|
58
75
|
e.preventDefault();
|
|
59
|
-
|
|
76
|
+
const iso = e.currentTarget.dataset.day;
|
|
77
|
+
if (this.isDateDisabled(iso)) return;
|
|
78
|
+
|
|
79
|
+
switch (this.modeValue) {
|
|
80
|
+
case "range":
|
|
81
|
+
this.selectRangeDay(iso);
|
|
82
|
+
break;
|
|
83
|
+
case "multiple":
|
|
84
|
+
this.toggleMultipleDay(iso);
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
this.selectedDateValue = iso;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
selectRangeDay(iso) {
|
|
92
|
+
const candidate = this.parseDate(iso);
|
|
93
|
+
const start = this.parseDate(this.rangeStartValue);
|
|
94
|
+
const end = this.parseDate(this.rangeEndValue);
|
|
95
|
+
|
|
96
|
+
if (!start || (start && end)) {
|
|
97
|
+
// begin a fresh range
|
|
98
|
+
this.rangeStartValue = this.isoDate(candidate);
|
|
99
|
+
this.rangeEndValue = null;
|
|
100
|
+
} else if (candidate < start) {
|
|
101
|
+
this.rangeStartValue = this.isoDate(candidate);
|
|
102
|
+
} else {
|
|
103
|
+
this.rangeEndValue = this.isoDate(candidate);
|
|
104
|
+
}
|
|
105
|
+
this.updateCalendar();
|
|
106
|
+
this.pushToOutlets();
|
|
107
|
+
this.dispatchChange();
|
|
108
|
+
}
|
|
60
109
|
|
|
61
|
-
|
|
62
|
-
|
|
110
|
+
toggleMultipleDay(iso) {
|
|
111
|
+
const day = this.isoDate(this.parseDate(iso));
|
|
112
|
+
const current = [...this.selectedDatesValue];
|
|
113
|
+
const index = current.indexOf(day);
|
|
114
|
+
if (index >= 0) {
|
|
115
|
+
current.splice(index, 1);
|
|
116
|
+
} else {
|
|
117
|
+
current.push(day);
|
|
118
|
+
current.sort();
|
|
119
|
+
}
|
|
120
|
+
this.selectedDatesValue = current;
|
|
121
|
+
this.updateCalendar();
|
|
122
|
+
this.dispatchChange();
|
|
63
123
|
}
|
|
64
124
|
|
|
65
125
|
selectedDateValueChanged(value, prevValue) {
|
|
@@ -69,38 +129,77 @@ export default class extends Controller {
|
|
|
69
129
|
return;
|
|
70
130
|
}
|
|
71
131
|
|
|
72
|
-
// update the viewDateValue to the
|
|
132
|
+
// update the viewDateValue to the selected date's month (this re-renders)
|
|
73
133
|
const newViewDate = new Date(selectedDate);
|
|
74
|
-
newViewDate.setDate(2); //
|
|
134
|
+
newViewDate.setDate(2); // avoid month-length/timezone edges
|
|
75
135
|
this.viewDateValue = newViewDate.toISOString().slice(0, 10);
|
|
76
136
|
|
|
77
|
-
// Re-render the calendar
|
|
78
137
|
this.updateCalendar();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.phlexKitCalendarInputOutlets.forEach((outlet) => {
|
|
82
|
-
const formattedDate = this.formatDate(selectedDate);
|
|
83
|
-
outlet.setValue(formattedDate);
|
|
84
|
-
});
|
|
138
|
+
this.pushToOutlets();
|
|
139
|
+
this.dispatchChange();
|
|
85
140
|
}
|
|
86
141
|
|
|
87
142
|
viewDateValueChanged(value, prevValue) {
|
|
88
143
|
this.updateCalendar();
|
|
89
144
|
}
|
|
90
145
|
|
|
146
|
+
pushToOutlets() {
|
|
147
|
+
const value = this.formattedSelection();
|
|
148
|
+
if (value === null) return;
|
|
149
|
+
this.phlexKitCalendarInputOutlets.forEach((outlet) => outlet.setValue(value));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
formattedSelection() {
|
|
153
|
+
if (this.modeValue === "range") {
|
|
154
|
+
const start = this.parseDate(this.rangeStartValue);
|
|
155
|
+
const end = this.parseDate(this.rangeEndValue);
|
|
156
|
+
if (!start) return null;
|
|
157
|
+
return end ? `${this.formatDate(start)} – ${this.formatDate(end)}` : this.formatDate(start);
|
|
158
|
+
}
|
|
159
|
+
if (this.modeValue === "multiple") {
|
|
160
|
+
return this.selectedDatesValue.map((d) => this.formatDate(this.parseDate(d))).join(", ");
|
|
161
|
+
}
|
|
162
|
+
const selected = this.selectedDate();
|
|
163
|
+
return selected ? this.formatDate(selected) : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
dispatchChange() {
|
|
167
|
+
this.dispatch("change", {
|
|
168
|
+
detail: {
|
|
169
|
+
mode: this.modeValue,
|
|
170
|
+
selected: this.selectedDateValue,
|
|
171
|
+
selectedDates: this.selectedDatesValue,
|
|
172
|
+
rangeStart: this.rangeStartValue,
|
|
173
|
+
rangeEnd: this.rangeEndValue,
|
|
174
|
+
formatted: this.formattedSelection(),
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
91
179
|
adjustMonth(adjustment) {
|
|
92
180
|
const date = this.viewDate();
|
|
93
|
-
date.setDate(2); //
|
|
181
|
+
date.setDate(2); // avoid month-length/timezone edges
|
|
94
182
|
date.setMonth(date.getMonth() + adjustment);
|
|
95
183
|
return date.toISOString().slice(0, 10);
|
|
96
184
|
}
|
|
97
185
|
|
|
98
186
|
updateCalendar() {
|
|
99
|
-
|
|
100
|
-
|
|
187
|
+
if (this.hasTitleTarget) {
|
|
188
|
+
this.titleTarget.textContent = this.monthAndYear();
|
|
189
|
+
}
|
|
190
|
+
this.syncDropdowns();
|
|
101
191
|
this.calendarTarget.innerHTML = this.calendarHTML();
|
|
102
192
|
}
|
|
103
193
|
|
|
194
|
+
syncDropdowns() {
|
|
195
|
+
if (this.hasMonthSelectTarget) {
|
|
196
|
+
this.monthSelectTarget.value = String(this.viewDate().getMonth());
|
|
197
|
+
}
|
|
198
|
+
if (this.hasYearSelectTarget) {
|
|
199
|
+
this.yearSelectTarget.value = String(this.viewDate().getFullYear());
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
104
203
|
calendarHTML() {
|
|
105
204
|
return this.weekdaysTemplateTarget.innerHTML + this.calendarDays();
|
|
106
205
|
}
|
|
@@ -112,53 +211,83 @@ export default class extends Controller {
|
|
|
112
211
|
}
|
|
113
212
|
|
|
114
213
|
renderWeek(week) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return `<tr class="pk-calendar-week">${
|
|
214
|
+
let cells = week.map((day) => this.renderDay(day)).join("");
|
|
215
|
+
if (this.weekNumbersValue) {
|
|
216
|
+
const weekNumber = this.isoWeekNumber(week[0]);
|
|
217
|
+
cells = `<td class="pk-calendar-weeknumber" role="presentation">${weekNumber}</td>` + cells;
|
|
218
|
+
}
|
|
219
|
+
return `<tr class="pk-calendar-week">${cells}</tr>`;
|
|
121
220
|
}
|
|
122
221
|
|
|
123
222
|
renderDay(day) {
|
|
124
223
|
const today = new Date();
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
const data = { day: day, dayDate: day.getDate() };
|
|
224
|
+
const iso = this.isoDate(day);
|
|
225
|
+
const data = { day: iso, dayDate: day.getDate(), state: this.dayState(day) };
|
|
128
226
|
|
|
129
227
|
if (this.isDateDisabled(day)) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
228
|
+
return renderTemplate(this.disabledDateTemplateTarget.innerHTML, data);
|
|
229
|
+
}
|
|
230
|
+
if (this.isDaySelected(day)) {
|
|
231
|
+
return renderTemplate(this.selectedDateTemplateTarget.innerHTML, data);
|
|
232
|
+
}
|
|
233
|
+
if (this.isDayInRange(day)) {
|
|
234
|
+
// middle of a range: current-month look + the in-range state class
|
|
235
|
+
return renderTemplate(this.currentMonthDateTemplateTarget.innerHTML, data);
|
|
236
|
+
}
|
|
237
|
+
if (day.toDateString() === today.toDateString()) {
|
|
238
|
+
return renderTemplate(this.todayDateTemplateTarget.innerHTML, data);
|
|
239
|
+
}
|
|
240
|
+
if (day.getMonth() === this.viewDate().getMonth()) {
|
|
241
|
+
return renderTemplate(this.currentMonthDateTemplateTarget.innerHTML, data);
|
|
242
|
+
}
|
|
243
|
+
return renderTemplate(this.otherMonthDateTemplateTarget.innerHTML, data);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Extra state classes interpolated into the day templates ({{state}}).
|
|
247
|
+
dayState(day) {
|
|
248
|
+
const states = [];
|
|
249
|
+
if (this.isBooked(day)) states.push("booked");
|
|
250
|
+
if (this.modeValue === "range") {
|
|
251
|
+
const start = this.parseDate(this.rangeStartValue);
|
|
252
|
+
const end = this.parseDate(this.rangeEndValue);
|
|
253
|
+
const isStart = start && day.toDateString() === start.toDateString();
|
|
254
|
+
const isEnd = end && day.toDateString() === end.toDateString();
|
|
255
|
+
if (isStart) states.push("range-start");
|
|
256
|
+
if (isEnd) states.push("range-end");
|
|
257
|
+
// caps only band with the accent once the range is complete
|
|
258
|
+
if (start && end && (isStart || isEnd)) states.push("range-cap");
|
|
259
|
+
if (this.isDayInRange(day)) states.push("in-range");
|
|
260
|
+
}
|
|
261
|
+
return states.join(" ");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
isDaySelected(day) {
|
|
265
|
+
if (this.modeValue === "range") {
|
|
266
|
+
const start = this.parseDate(this.rangeStartValue);
|
|
267
|
+
const end = this.parseDate(this.rangeEndValue);
|
|
268
|
+
return (
|
|
269
|
+
(start && day.toDateString() === start.toDateString()) ||
|
|
270
|
+
(end && day.toDateString() === end.toDateString())
|
|
159
271
|
);
|
|
160
272
|
}
|
|
161
|
-
|
|
273
|
+
if (this.modeValue === "multiple") {
|
|
274
|
+
return this.selectedDatesValue.includes(this.isoDate(day));
|
|
275
|
+
}
|
|
276
|
+
const selectedDate = this.selectedDate();
|
|
277
|
+
return selectedDate && day.toDateString() === selectedDate.toDateString();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
isDayInRange(day) {
|
|
281
|
+
if (this.modeValue !== "range") return false;
|
|
282
|
+
const start = this.parseDate(this.rangeStartValue);
|
|
283
|
+
const end = this.parseDate(this.rangeEndValue);
|
|
284
|
+
if (!start || !end) return false;
|
|
285
|
+
const candidate = this.startOfDay(day);
|
|
286
|
+
return candidate > this.startOfDay(start) && candidate < this.startOfDay(end);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
isBooked(day) {
|
|
290
|
+
return this.disabledDatesValue.includes(this.isoDate(day));
|
|
162
291
|
}
|
|
163
292
|
|
|
164
293
|
monthAndYear() {
|
|
@@ -222,7 +351,6 @@ export default class extends Controller {
|
|
|
222
351
|
break;
|
|
223
352
|
}
|
|
224
353
|
}
|
|
225
|
-
// *** the magic starts here
|
|
226
354
|
return weeks.map(({ start, end }, index) => {
|
|
227
355
|
const sub = +(start > end && index === 0);
|
|
228
356
|
return Array.from({ length: 7 }, (_, index) => {
|
|
@@ -232,6 +360,21 @@ export default class extends Controller {
|
|
|
232
360
|
});
|
|
233
361
|
}
|
|
234
362
|
|
|
363
|
+
isoWeekNumber(date) {
|
|
364
|
+
const target = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
365
|
+
const dayNumber = target.getUTCDay() || 7;
|
|
366
|
+
target.setUTCDate(target.getUTCDate() + 4 - dayNumber);
|
|
367
|
+
const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 1));
|
|
368
|
+
return Math.ceil(((target - yearStart) / 86400000 + 1) / 7);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
isoDate(date) {
|
|
372
|
+
const year = date.getFullYear();
|
|
373
|
+
const month = ("0" + (date.getMonth() + 1)).slice(-2);
|
|
374
|
+
const day = ("0" + date.getDate()).slice(-2);
|
|
375
|
+
return `${year}-${month}-${day}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
235
378
|
formatDate(date) {
|
|
236
379
|
const format = this.formatValue;
|
|
237
380
|
const day = date.getDate();
|
|
@@ -282,13 +425,21 @@ export default class extends Controller {
|
|
|
282
425
|
return this.parseDate(this.minDateValue);
|
|
283
426
|
}
|
|
284
427
|
|
|
428
|
+
maxDate() {
|
|
429
|
+
return this.parseDate(this.maxDateValue);
|
|
430
|
+
}
|
|
431
|
+
|
|
285
432
|
isDateDisabled(date) {
|
|
286
|
-
const minDate = this.minDate();
|
|
287
433
|
const candidate = this.parseDate(date);
|
|
434
|
+
if (!candidate) return false;
|
|
435
|
+
|
|
436
|
+
const minDate = this.minDate();
|
|
437
|
+
if (minDate && this.startOfDay(candidate) < this.startOfDay(minDate)) return true;
|
|
288
438
|
|
|
289
|
-
|
|
439
|
+
const maxDate = this.maxDate();
|
|
440
|
+
if (maxDate && this.startOfDay(candidate) > this.startOfDay(maxDate)) return true;
|
|
290
441
|
|
|
291
|
-
return this.
|
|
442
|
+
return this.disabledDatesValue.includes(this.isoDate(candidate));
|
|
292
443
|
}
|
|
293
444
|
|
|
294
445
|
parseDate(value) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module PhlexKit
|
|
2
2
|
# <template>s for the five day states (disabled / selected / today / current
|
|
3
3
|
# month / other month). The controller picks one per day and interpolates
|
|
4
|
-
# {{day}} / {{dayDate}}
|
|
4
|
+
# {{day}} / {{dayDate}} / {{state}} — state carries the extra range/booked
|
|
5
|
+
# classes (range-start / range-end / in-range / booked). See calendar.rb.
|
|
5
6
|
class CalendarDays < BaseComponent
|
|
6
7
|
def initialize(**attrs)
|
|
7
8
|
@attrs = attrs
|
|
@@ -22,7 +23,7 @@ module PhlexKit
|
|
|
22
23
|
button(
|
|
23
24
|
data_day: "{{day}}",
|
|
24
25
|
name: "day",
|
|
25
|
-
class: "pk-calendar-day disabled",
|
|
26
|
+
class: "pk-calendar-day disabled {{state}}",
|
|
26
27
|
disabled: true,
|
|
27
28
|
role: "gridcell",
|
|
28
29
|
tabindex: "-1",
|
|
@@ -38,7 +39,7 @@ module PhlexKit
|
|
|
38
39
|
data_day: "{{day}}",
|
|
39
40
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
40
41
|
name: "day",
|
|
41
|
-
class: "pk-calendar-day selected",
|
|
42
|
+
class: "pk-calendar-day selected {{state}}",
|
|
42
43
|
role: "gridcell",
|
|
43
44
|
tabindex: "0",
|
|
44
45
|
type: "button",
|
|
@@ -53,7 +54,7 @@ module PhlexKit
|
|
|
53
54
|
data_day: "{{day}}",
|
|
54
55
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
55
56
|
name: "day",
|
|
56
|
-
class: "pk-calendar-day today",
|
|
57
|
+
class: "pk-calendar-day today {{state}}",
|
|
57
58
|
role: "gridcell",
|
|
58
59
|
tabindex: "-1",
|
|
59
60
|
type: "button"
|
|
@@ -67,7 +68,7 @@ module PhlexKit
|
|
|
67
68
|
data_day: "{{day}}",
|
|
68
69
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
69
70
|
name: "day",
|
|
70
|
-
class: "pk-calendar-day",
|
|
71
|
+
class: "pk-calendar-day {{state}}",
|
|
71
72
|
role: "gridcell",
|
|
72
73
|
tabindex: "-1",
|
|
73
74
|
type: "button"
|
|
@@ -81,7 +82,7 @@ module PhlexKit
|
|
|
81
82
|
data_day: "{{day}}",
|
|
82
83
|
data_action: "click->phlex-kit--calendar#selectDay",
|
|
83
84
|
name: "day",
|
|
84
|
-
class: "pk-calendar-day other",
|
|
85
|
+
class: "pk-calendar-day other {{state}}",
|
|
85
86
|
role: "gridcell",
|
|
86
87
|
tabindex: "-1",
|
|
87
88
|
type: "button"
|
|
@@ -4,7 +4,8 @@ module PhlexKit
|
|
|
4
4
|
class CalendarWeekdays < BaseComponent
|
|
5
5
|
DAYS = %w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday].freeze
|
|
6
6
|
|
|
7
|
-
def initialize(**attrs)
|
|
7
|
+
def initialize(week_numbers: false, **attrs)
|
|
8
|
+
@week_numbers = week_numbers
|
|
8
9
|
@attrs = attrs
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -12,6 +13,7 @@ module PhlexKit
|
|
|
12
13
|
template(data: { phlex_kit__calendar_target: "weekdaysTemplate" }) do
|
|
13
14
|
thead(**@attrs) do
|
|
14
15
|
tr(class: "pk-calendar-weekdays-row") do
|
|
16
|
+
th(scope: "col", class: "pk-calendar-weekday pk-calendar-weeknumber-head", aria: { label: "Week number" }) { "#" } if @week_numbers
|
|
15
17
|
DAYS.each { |day| render_day(day) }
|
|
16
18
|
end
|
|
17
19
|
end
|