Module: Plutonium::Positioning

Extended by:
ActiveSupport::Concern
Defined in:
lib/plutonium/positioning.rb

Overview

Standalone decimal/fractional ordering. Kanban-independent.

Including this concern and calling positioned_on gives a model:

  • automatic position assignment on create (appends to the end of its scope group)
  • reposition!(prev_record:, next_record:) for drag-and-drop reordering
  • backfill_positions! class method to number existing rows

Pure math helpers are exposed as module-level methods so they can be called without an AR instance:

Plutonium::Positioning.position_between(1.0, 3.0)  # => 2.0
Plutonium::Positioning.gap_exhausted?(1.0, 1.0)    # => true

Defined Under Namespace

Modules: MigrationHelpers

Constant Summary collapse

EPSILON =
1e-6

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.gap_exhausted?(prev_val, next_val) ⇒ Boolean

Returns true when prev_val and next_val are so close together that inserting a new midpoint would produce a duplicate.

Returns:

  • (Boolean)


36
37
38
39
# File 'lib/plutonium/positioning.rb', line 36

def self.gap_exhausted?(prev_val, next_val)
  return false if prev_val.nil? || next_val.nil?
  (next_val - prev_val).abs < EPSILON
end

.position_between(prev_val, next_val) ⇒ Object

Returns the position that sits between prev_val and next_val.

Rules:

both nil  → 0.0            (first item in an empty list)
prev nil  → next_val - 1   (prepend)
next nil  → prev_val + 1   (append)
else      → midpoint


27
28
29
30
31
32
# File 'lib/plutonium/positioning.rb', line 27

def self.position_between(prev_val, next_val)
  return 0.0 if prev_val.nil? && next_val.nil?
  return next_val - 1 if prev_val.nil?
  return prev_val + 1 if next_val.nil?
  (prev_val + next_val) / 2.0
end

Instance Method Details

#reposition!(prev_record:, next_record:) ⇒ Object

Move this record so it sits between prev_record and next_record within its scope group. Pass nil for either neighbor to move to an end.

If the gap between the two neighbors is exhausted (too small to split) the scope group is rebalanced first so that fresh integer positions are available, then the record is positioned between the reloaded neighbors.

Parameters:

  • prev_record (ActiveRecord::Base, nil)
  • next_record (ActiveRecord::Base, nil)


84
85
86
87
88
89
90
91
92
93
94
# File 'lib/plutonium/positioning.rb', line 84

def reposition!(prev_record:, next_record:)
  col = self.class.positioning_column
  prev_val = prev_record&.public_send(col)
  next_val = next_record&.public_send(col)
  if Plutonium::Positioning.gap_exhausted?(prev_val, next_val)
    rebalance_scope_group!
    prev_val = prev_record&.reload&.public_send(col)
    next_val = next_record&.reload&.public_send(col)
  end
  update!(col => Plutonium::Positioning.position_between(prev_val, next_val))
end