Packages
moon
2.71.1
2.90.5
retired
2.90.4
2.90.3
2.90.2
2.90.1
2.90.0
2.89.4
2.89.1
2.89.0
2.88.0
2.87.11
2.87.10
2.87.9
2.87.8
2.87.7
2.87.6
2.87.5
2.87.4
2.87.3
2.87.2
2.87.1
2.87.0
2.86.1
2.86.0
2.85.1
2.85.0
2.84.0
2.83.0
2.82.0
2.81.4
2.81.3
2.81.2
2.81.1
2.81.0
2.80.2
2.80.1
2.80.0
2.79.13
2.79.12
2.79.11
2.79.10
2.79.9
2.79.8
2.79.7
2.79.6
2.79.5
2.79.4
2.79.3
2.79.2
2.79.1
2.79.0
2.78.4
2.78.3
2.78.2
2.78.1
2.78.0
2.77.0
2.76.5
2.76.4
2.76.3
2.76.2
2.76.1
2.76.0
2.75.0
2.74.1
2.74.0
2.73.8
2.73.7
2.73.6
2.73.5
2.73.4
2.73.3
2.73.2
2.73.1
2.73.0
2.72.5
2.72.4
2.72.3
2.72.2
2.72.1
2.72.0
2.71.1
2.71.0
2.70.0
2.69.2
2.69.1
2.69.0
2.68.11
2.68.10
Components-based design system written in elixir
Current section
Files
Jump to
Current section
Files
lib/moon/components/datepicker.ex
defmodule Moon.Components.Datepicker do
@moduledoc false
use Moon.StatefulComponent
alias Moon.Components.Chip
alias Moon.Components.Datepicker.Month.FirstMonth
alias Moon.Components.Datepicker.Month.SecondMonth
alias Moon.Components.Datepicker.Helpers
alias Moon.Components.Datepicker.DateInputs
alias Moon.Components.Datepicker.Buttons
alias Moon.Components.Popover
@default_label "Select Dates"
@ranges %{
lastMonth: ~c"Last month",
lastWeek: ~c"Last week",
last24hours: ~c"Last 24 hours",
yesterday: ~c"Yesterday",
today: ~c"Today",
tomorrow: ~c"Tomorrow",
thisWeek: ~c"This week",
nextWeek: ~c"Next week",
thisMonth: ~c"This month",
nextMonth: ~c"Next month"
}
prop(with_time, :boolean, default: false)
prop(week_starts_on, :integer, default: 1, values: Enum.to_list(1..7))
prop(start_date_field, :atom, default: :start_date)
prop(end_date_field, :atom, default: :end_date)
prop(button_class, :css_class, default: "")
prop(show_date_inputs, :boolean, default: false)
prop(ranges, :list,
default: ~w(lastMonth lastWeek yesterday thisWeek thisMonth last24hours today)
)
prop(start_date, :datetime)
prop(end_date, :datetime)
prop(submit, :event)
prop(testid, :string)
# Internal values
data(internal_start_date, :datetime, default: Timex.today())
data(internal_end_date, :datetime, default: Timex.today())
data(left_panel_date, :datetime, default: Timex.today())
data(selected_range, :string, default: "thisMonth")
data(temp_range, :string, default: nil)
data(show, :boolean, default: false)
def render(assigns) do
~F"""
<Popover show={@show} on_close="toggle_picker" testid={@testid}>
<Chip
class={@button_class}
on_click="toggle_picker"
right_icon="controls_chevron_down"
active={@start_date && @end_date}
testid={"#{@testid}-chip"}
button_type="button"
>
{button_label(@start_date, @end_date, @with_time, @selected_range)}
</Chip>
<:content>
<div class={
"p-2 pr-3 origin-top-left absolute left-0 bg-goku flex shadow-lg rounded text-moon-14 z-10 mt-2",
hidden: !@show
}>
<!-- Ranges -->
<div :if={length(@ranges) > 0} class="space-y-0.5 w-48 mr-4">
<div
:for={range <- @ranges}
class={
"py-2 px-3 hover:bg-gohan rounded cursor-pointer",
"bg-gohan": range == @selected_range
}
:on-click="select_range"
phx-value-range={range}
data-testid={"#{@testid}-select-range-#{range}"}
>
{range_label(range)}
</div>
</div>
<!-- Content -->
<div class="flex flex-col pt-2 pl-3">
<!-- Months -->
<div class="flex grow space-x-6">
<FirstMonth
date={@left_panel_date}
start_date={@internal_start_date}
end_date={@internal_end_date}
week_starts_on={@week_starts_on}
on_click="select_date"
/>
<SecondMonth
date={Timex.shift(@left_panel_date, months: 1)}
start_date={@internal_start_date}
end_date={@internal_end_date}
week_starts_on={@week_starts_on}
on_click="select_date"
/>
</div>
<div class={
"flex items-center mt-6 gap-x-2",
"justify-between": @show_date_inputs,
"justify-end": !@show_date_inputs
}>
<DateInputs
:if={@show_date_inputs}
{=@with_time}
{=@start_date_field}
{=@end_date_field}
{=@internal_start_date}
{=@internal_end_date}
/>
<Buttons
on_click={@submit || "update_dates"}
values={
start_date: Helpers.format_date(@internal_start_date, @with_time),
end_date: Helpers.format_date(@internal_end_date, @with_time)
}
testid={"#{@testid}-apply"}
/>
</div>
</div>
</div>
</:content>
</Popover>
"""
end
# Handle date-input fields updates
def update(assigns, socket) do
socket = assign(socket, assigns)
socket =
case assigns do
%{start_date: start_date, end_date: end_date} ->
assign(
socket,
start_date: Helpers.parse_date(start_date),
end_date: Helpers.parse_date(end_date),
internal_start_date: Helpers.parse_date(start_date),
internal_end_date: Helpers.parse_date(end_date)
)
_ ->
# this is required, because if someone uses LiveView.send_update
# then start_date and end_date might be missing
socket
end
{:ok, socket}
end
def validate(start_date, end_date) do
parsed_start_date = Helpers.parse_date(start_date)
parsed_end_date = Helpers.parse_date(end_date)
if parsed_start_date && parsed_end_date && Timex.after?(parsed_start_date, parsed_end_date) do
{start_date, start_date}
else
{start_date, end_date}
end
end
def handle_event("toggle_picker", _, socket) do
%{
show: show,
start_date: start_date,
end_date: end_date
} = socket.assigns
left_panel_date = if start_date, do: Timex.to_date(start_date), else: Timex.today()
selected_range =
if show do
# Restore to an initial value on close
socket.assigns.temp_range
else
socket.assigns.selected_range
end
socket =
assign(socket,
internal_start_date: start_date,
internal_end_date: end_date,
left_panel_date: left_panel_date,
temp_range: selected_range,
selected_range: selected_range,
show: !show
)
{:noreply, socket}
end
def handle_event("select_range", %{"range" => range}, socket) do
{start_date, end_date} = Helpers.dates_from_range(range, socket.assigns.week_starts_on)
socket =
assign(socket,
selected_range: range,
internal_start_date: start_date,
internal_end_date: end_date,
left_panel_date: Timex.to_date(start_date)
)
{:noreply, socket}
end
def handle_event("shift_months", %{"months" => months}, socket) do
months = String.to_integer(months)
left_panel_date = Timex.shift(socket.assigns.left_panel_date, months: months)
{:noreply, assign(socket, left_panel_date: left_panel_date)}
end
def handle_event("select_date", %{"date" => date}, socket) do
%{
with_time: with_time,
internal_start_date: start_date,
internal_end_date: end_date
} = socket.assigns
date =
date
|> Helpers.parse_date()
|> Helpers.format_to_date(with_time)
{start_date, end_date} =
if start_date && is_nil(end_date) && Timex.after?(date, start_date) do
# Keep start, set end
end_date = if with_time, do: Timex.end_of_day(date), else: date
{start_date, end_date}
else
# Set start, reset end
start_date = if with_time, do: Timex.beginning_of_day(date), else: date
{start_date, nil}
end
{:noreply,
assign(socket,
selected_range: nil,
internal_start_date: start_date,
internal_end_date: end_date
)}
end
def handle_event("update_dates", _, socket) do
%{
id: filter_id,
start_date_field: start_date_field,
end_date_field: end_date_field,
internal_start_date: start_date,
internal_end_date: end_date
} = socket.assigns
# TODO this is too filters specific and needs to be removed.
# this should use normal Surface Event with handle_callback
send(
self(),
{:filter,
{
filter_id,
:apply,
%{
start_date_field => start_date,
end_date_field => end_date
}
}}
)
{:noreply, assign(socket, show: false)}
end
def close(id: id) do
send_update(__MODULE__, id: id, show: false)
end
defp button_label(nil, _, _, _), do: @default_label
defp button_label(_, nil, _, _), do: @default_label
defp button_label(start_date, end_date, with_time, nil) do
date_format = "%0d/%0m/%Y"
date_format = if with_time, do: date_format <> ", %R", else: date_format
start_date_formatted = Timex.format!(start_date, date_format, :strftime)
end_date_formatted = Timex.format!(end_date, date_format, :strftime)
"#{start_date_formatted}-#{end_date_formatted}"
end
defp button_label(_start_date, _end_date, _, range), do: range_label(range)
defp range_label(range_name) when is_binary(range_name) do
@ranges[String.to_existing_atom(range_name)]
end
end