Translates user gestures into task and link attributes.
GanttEx.Live.Gantt receives raw client values (pixels, strings) from the
hook and from inline editors. This module turns them into validated
attributes for GanttEx.Store, so the conversion rules can be tested on
their own. Every function returns nil (or :error) for values that do
not make sense, which the component simply ignores.
Summary
Functions
Returns the attributes for an inline edit of column, or nil when the
value is invalid. Fields that are not task fields are stored in :meta,
reusing the existing key type (atom or string).
Returns the snapped datetime at a horizontal position of the timeline.
Returns the attributes that result from dragging a bar.
Returns {parent, index} for dropping task "before", "after" or
"inside" the target row, or nil when the drop would put the task
inside its own subtree.
Converts the params of custom lightbox fields into meta attributes, reusing
the key type already present in meta.
Parses a number sent by the client.
Returns the resource assignments chosen in the lightbox ("resources", a
list of resource keys), keeping the values of existing assignments.
Returns %{} when the form has no resources field.
Casts a link edge sent by the client.
Returns {start, end} for a task drawn on the timeline from x to
x + width, lasting at least one unit.
Types
@type context() :: %{ store: GanttEx.Store.t(), timeline: GanttEx.Timeline.t(), unit: GanttEx.Dates.unit(), week_start: 1..7 }
What the conversions need to know about the chart.
Functions
@spec cell_attrs(map(), term(), GanttEx.Task.t(), GanttEx.Dates.unit()) :: map() | nil
Returns the attributes for an inline edit of column, or nil when the
value is invalid. Fields that are not task fields are stored in :meta,
reusing the existing key type (atom or string).
iex> task = GanttEx.Task.new!(id: 1, start_date: ~D[2025-01-06], owner: "Ana")
iex> GanttEx.Live.Interaction.cell_attrs(%{field: :end_date}, "2025-01-08", task, :day)
%{end_date: ~N[2025-01-09 00:00:00]}
iex> GanttEx.Live.Interaction.cell_attrs(%{field: :progress}, "120", task, :day)
nil
iex> GanttEx.Live.Interaction.cell_attrs(%{field: :owner}, "Luis", task, :day)
%{meta: %{owner: "Luis"}}
@spec date_at(context(), number()) :: NaiveDateTime.t()
Returns the snapped datetime at a horizontal position of the timeline.
@spec drag_attrs(GanttEx.Task.t(), String.t(), map(), context()) :: map() | nil
Returns the attributes that result from dragging a bar.
mode is "move", "start", "end" (resizing) or "progress".
params holds the final "x" and "width" of the bar in pixels, or the
"progress" between 0 and 1. Dates snap to the duration unit and a task
keeps at least one unit. Project bars with children cannot be moved or
resized: their dates come from their children.
iex> store = GanttEx.Store.new!([%{id: 1, start_date: ~D[2025-01-06], duration: 3}])
iex> timeline = GanttEx.Timeline.new(scales: [[unit: :day]], start_date: ~N[2025-01-01 00:00:00], end_date: ~N[2025-02-01 00:00:00], column_width: 40)
iex> ctx = %{store: store, timeline: timeline, unit: :day, week_start: 1}
iex> task = GanttEx.Store.get_task(store, 1)
iex> GanttEx.Live.Interaction.drag_attrs(task, "move", %{"x" => 318, "width" => 120}, ctx)
%{start_date: ~N[2025-01-09 00:00:00]}
iex> GanttEx.Live.Interaction.drag_attrs(task, "end", %{"x" => 200, "width" => 2}, ctx)
%{end_date: ~N[2025-01-07 00:00:00]}
iex> GanttEx.Live.Interaction.drag_attrs(task, "progress", %{"progress" => "0.456"}, ctx)
%{progress: 0.46}
iex> GanttEx.Live.Interaction.drag_attrs(task, "fly", %{}, ctx)
nil
@spec drop_position(GanttEx.Store.t(), GanttEx.Task.t(), GanttEx.Task.t(), String.t()) :: {GanttEx.Task.id() | nil, non_neg_integer() | nil} | nil
Returns {parent, index} for dropping task "before", "after" or
"inside" the target row, or nil when the drop would put the task
inside its own subtree.
iex> store = GanttEx.Store.new!([%{id: 1, start_date: ~D[2025-01-01]}, %{id: 2, parent: 1, start_date: ~D[2025-01-01]}, %{id: 3, start_date: ~D[2025-01-01]}])
iex> [one, two, three] = GanttEx.Store.tasks(store)
iex> GanttEx.Live.Interaction.drop_position(store, three, two, "after")
{1, 1}
iex> GanttEx.Live.Interaction.drop_position(store, three, one, "before")
{nil, 0}
iex> GanttEx.Live.Interaction.drop_position(store, one, two, "inside")
nil
Converts the params of custom lightbox fields into meta attributes, reusing
the key type already present in meta.
iex> GanttEx.Live.Interaction.extra_params(%{"text" => "A", "owner" => "Ana", "_target" => "x"}, %{owner: nil})
%{owner: "Ana"}
Parses a number sent by the client.
iex> GanttEx.Live.Interaction.number("12.5")
12.5
iex> GanttEx.Live.Interaction.number("12px")
nil
@spec resource_attrs( map(), %{required(String.t()) => GanttEx.Resource.t()}, map() | nil ) :: map()
Returns the resource assignments chosen in the lightbox ("resources", a
list of resource keys), keeping the values of existing assignments.
Returns %{} when the form has no resources field.
iex> resources = %{"ana" => GanttEx.Resource.new!(id: "ana"), "7" => GanttEx.Resource.new!(id: 7)}
iex> task = GanttEx.Task.new!(id: 1, resources: [%{id: "ana", value: 4}])
iex> GanttEx.Live.Interaction.resource_attrs(%{"resources" => ["", "ana", "7", "ghost"]}, resources, task)
%{resources: [%{id: "ana", value: 4}, %{id: 7, value: nil}]}
iex> GanttEx.Live.Interaction.resource_attrs(%{"text" => "A"}, resources, task)
%{}
@spec side(term()) :: {:ok, :start | :end} | :error
Casts a link edge sent by the client.
iex> GanttEx.Live.Interaction.side("end")
{:ok, :end}
iex> GanttEx.Live.Interaction.side("middle")
:error
@spec span_at(context(), number(), number()) :: {NaiveDateTime.t(), NaiveDateTime.t()}
Returns {start, end} for a task drawn on the timeline from x to
x + width, lasting at least one unit.