Add support for datetime, time and date inputs (#2002)

Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
Jannik Becher 2023-07-03 21:39:16 +02:00 committed by GitHub
parent 16723b98f0
commit 649a556025
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 259 additions and 1 deletions

View file

@ -18,6 +18,8 @@ import Session from "./session";
import TextareaAutosize from "./textarea_autosize";
import Timer from "./timer";
import UserForm from "./user_form";
import UtcDateTimeInput from "./utc_datetime_input";
import UtcTimeInput from "./utc_time_input";
import VirtualizedLines from "./virtualized_lines";
export default {
@ -41,5 +43,7 @@ export default {
TextareaAutosize,
Timer,
UserForm,
UtcDateTimeInput,
UtcTimeInput,
VirtualizedLines,
};

View file

@ -0,0 +1,68 @@
import { getAttributeOrDefault, getAttributeOrThrow } from "../lib/attribute";
/**
* A hook for client-preprocessed datetime input.
*/
const UtcDateTimeInput = {
mounted() {
this.props = this.getProps();
this.updateAttrs();
this.el.addEventListener("blur", (event) => {
const value = this.datetimeLocalToUtc(this.el.value);
this.pushEventTo(this.props.phxTarget, "change", { html_value: value });
});
},
updated() {
this.props = this.getProps();
this.updateAttrs();
},
getProps() {
return {
utcValue: getAttributeOrDefault(this.el, "data-utc-value", null),
utcMin: getAttributeOrDefault(this.el, "data-utc-min", null),
utcMax: getAttributeOrDefault(this.el, "data-utc-max", null),
phxTarget: getAttributeOrThrow(this.el, "data-phx-target"),
};
},
updateAttrs() {
this.el.value = this.datetimeUtcToLocal(this.props.utcValue);
this.el.min = this.datetimeUtcToLocal(this.props.utcMin);
this.el.max = this.datetimeUtcToLocal(this.props.utcMax);
},
datetimeUtcToLocal(datetime) {
if (!datetime) return null;
const date = new Date(datetime + "Z");
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
},
datetimeLocalToUtc(datetime) {
if (!datetime) return null;
const date = new Date(datetime);
const year = date.getUTCFullYear().toString();
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
const day = date.getUTCDate().toString().padStart(2, "0");
const hours = date.getUTCHours().toString().padStart(2, "0");
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
const seconds = date.getUTCSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
},
};
export default UtcDateTimeInput;

View file

@ -0,0 +1,62 @@
import { getAttributeOrDefault, getAttributeOrThrow } from "../lib/attribute";
/**
* A hook for client-preprocessed time input.
*/
const UtcTimeInput = {
mounted() {
this.props = this.getProps();
this.updateAttrs();
this.el.addEventListener("blur", (event) => {
const value = this.timeLocalToUtc(this.el.value);
this.pushEventTo(this.props.phxTarget, "change", { html_value: value });
});
},
updated() {
this.props = this.getProps();
this.updateAttrs();
},
getProps() {
return {
utcValue: getAttributeOrDefault(this.el, "data-utc-value", null),
utcMin: getAttributeOrDefault(this.el, "data-utc-min", null),
utcMax: getAttributeOrDefault(this.el, "data-utc-max", null),
phxTarget: getAttributeOrThrow(this.el, "data-phx-target"),
};
},
updateAttrs() {
this.el.value = this.timeUtcToLocal(this.props.utcValue);
this.el.min = this.timeUtcToLocal(this.props.utcMin);
this.el.max = this.timeUtcToLocal(this.props.utcMax);
},
timeUtcToLocal(time) {
if (!time) return null;
const date = new Date();
date.setUTCHours(...time.split(":"));
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
},
timeLocalToUtc(time) {
if (!time) return null;
const date = new Date();
date.setHours(...time.split(":"));
const hours = date.getUTCHours().toString().padStart(2, "0");
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
const seconds = date.getUTCSeconds().toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
},
};
export default UtcTimeInput;

View file

@ -82,6 +82,54 @@ defmodule LivebookWeb.Output.InputComponent do
"""
end
def render(%{attrs: %{type: :utc_datetime}} = assigns) do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.label help="Choose the time in your local time zone">
<%= @attrs.label %>
</.label>
<input
id={@id}
type="datetime-local"
data-el-input
class="input w-auto invalid:input--error"
name="html_value"
step="60"
autocomplete="off"
phx-hook="UtcDateTimeInput"
data-utc-value={@value && NaiveDateTime.to_iso8601(@value)}
data-utc-min={@attrs.min && NaiveDateTime.to_iso8601(@attrs.min)}
data-utc-max={@attrs.max && NaiveDateTime.to_iso8601(@attrs.max)}
data-phx-target={@myself}
/>
</div>
"""
end
def render(%{attrs: %{type: :utc_time}} = assigns) do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.label help="Choose the time in your local time zone">
<%= @attrs.label %>
</.label>
<input
id={@id}
type="time"
data-el-input
class="input w-auto invalid:input--error"
name="html_value"
step="60"
autocomplete="off"
phx-hook="UtcTimeInput"
data-utc-value={@value && Time.to_iso8601(@value)}
data-utc-min={@attrs.min && Time.to_iso8601(@attrs.min)}
data-utc-max={@attrs.max && Time.to_iso8601(@attrs.max)}
data-phx-target={@myself}
/>
</div>
"""
end
def render(assigns) do
~H"""
<form id={"#{@id}-form-#{@counter}"} phx-change="change" phx-submit="submit" phx-target={@myself}>
@ -171,6 +219,23 @@ defmodule LivebookWeb.Output.InputComponent do
"""
end
defp input_output(%{attrs: %{type: :date}} = assigns) do
~H"""
<input
type="date"
data-el-input
class="input w-auto invalid:input--error"
name="html_value"
value={@value}
phx-debounce="blur"
phx-target={@myself}
min={@attrs.min}
max={@attrs.max}
autocomplete="off"
/>
"""
end
defp input_output(%{attrs: %{type: type}} = assigns)
when type in [:number, :color, :url, :text] do
~H"""
@ -278,7 +343,7 @@ defmodule LivebookWeb.Output.InputComponent do
cond do
html_value == "" -> {:ok, nil}
Livebook.Utils.valid_url?(html_value) -> {:ok, html_value}
true -> {:error, "not a valid URL"}
true -> :error
end
end
@ -305,6 +370,65 @@ defmodule LivebookWeb.Output.InputComponent do
{:ok, html_value}
end
defp parse(html_value, %{type: :utc_datetime} = attrs) do
if html_value do
with {:ok, datetime} <- NaiveDateTime.from_iso8601(html_value),
datetime <- truncate_datetime(datetime),
true <- in_range?(datetime, attrs.min, attrs.max) do
{:ok, datetime}
else
_ -> :error
end
else
{:ok, nil}
end
end
defp parse(html_value, %{type: :utc_time} = attrs) do
if html_value do
with {:ok, time} <- Time.from_iso8601(html_value),
time <- truncate_time(time),
true <- in_range?(time, attrs.min, attrs.max) do
{:ok, time}
else
_ -> :error
end
else
{:ok, nil}
end
end
defp parse(html_value, %{type: :date} = attrs) do
if html_value == "" do
{:ok, nil}
else
with {:ok, date} <- Date.from_iso8601(html_value),
true <- in_range?(date, attrs.min, attrs.max) do
{:ok, date}
else
_ -> :error
end
end
end
defp truncate_datetime(datetime) do
datetime
|> NaiveDateTime.truncate(:second)
|> Map.replace!(:second, 0)
end
defp truncate_time(time) do
time
|> Time.truncate(:second)
|> Map.replace!(:second, 0)
end
defp in_range?(%struct{} = datetime, min, max)
when struct in [NaiveDateTime, Time, Date] do
(min == nil or struct.compare(datetime, min) != :lt) and
(max == nil or struct.compare(datetime, max) != :gt)
end
defp report_event(socket, value) do
topic = socket.assigns.attrs.ref
event = %{value: value, origin: socket.assigns.client_id, type: :change}