GanttEx.Live.Interaction (GanttEx v0.1.0)

Copy Markdown View Source

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

Types

What the conversions need to know about the chart.

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

context()

@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

cell_attrs(map, value, task, unit)

@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"}}

date_at(ctx, x)

@spec date_at(context(), number()) :: NaiveDateTime.t()

Returns the snapped datetime at a horizontal position of the timeline.

drag_attrs(task, mode, params, ctx)

@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

drop_position(store, task, target, position)

@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

extra_params(params, meta)

@spec extra_params(map(), map()) :: map()

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"}

number(value)

@spec number(term()) :: number() | nil

Parses a number sent by the client.

iex> GanttEx.Live.Interaction.number("12.5")
12.5

iex> GanttEx.Live.Interaction.number("12px")
nil

resource_attrs(arg1, resources, task)

@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)
%{}

side(arg1)

@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

span_at(ctx, x, width)

@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.