mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
Add support for datetime, time and date inputs (#2002)
Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
parent
16723b98f0
commit
649a556025
|
@ -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,
|
||||
};
|
||||
|
|
68
assets/js/hooks/utc_datetime_input.js
Normal file
68
assets/js/hooks/utc_datetime_input.js
Normal 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;
|
62
assets/js/hooks/utc_time_input.js
Normal file
62
assets/js/hooks/utc_time_input.js
Normal 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;
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue