Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 114 additions & 92 deletions app/assets/javascripts/activeadmin_reorderable.js
Original file line number Diff line number Diff line change
@@ -1,116 +1,138 @@
const setupReorderable = ({ table, onDragover, onDragEnd }) => {
const rows = table.getElementsByTagName('tbody')[0].rows
// ActiveAdmin Reorderable - JavaScript for drag-and-drop reordering
// Compatible with ActiveAdmin 4.0 and Importmap
document.addEventListener("DOMContentLoaded", function () {
initReorderable();
});

function initReorderable() {
// Get all tables that might be reorderable
const tables = document.querySelectorAll("table.data-table");

// Find all reorder handles in the document
const allHandles = document.querySelectorAll(".reorder-handle");

tables.forEach(function (table) {
// Check if this table has reorder handles
const handles = table.querySelectorAll(".reorder-handle");
if (handles.length > 0) {
initTableDragDrop(table);
}
});
}

let dragSrc = null
let srcIndex = null
function initTableDragDrop(table) {
const tbody = table.querySelector("tbody");
if (!tbody) return;

for (var i = 0; i < rows.length; i++) {
const row = rows[i]
const handle = row.querySelector(".reorder-handle")
const rows = tbody.querySelectorAll("tr");

// Add draggable only when the handle is clicked, to prevent dragging from the rest of the row
handle.addEventListener("mousedown", () => row.setAttribute("draggable", "true"))
handle.addEventListener("mouseup", () => row.setAttribute("draggable", "false"))
let dragSrcRow = null;

row.addEventListener("dragstart", (e) => {
e.dataTransfer.effectAllowed = "move"
// Set up drag events for each row that has a handle
rows.forEach(function (row) {
const handle = row.querySelector(".reorder-handle");
if (!handle) return;

dragSrc = row
srcIndex = row.rowIndex
// Get the reorder URL from the handle's data attribute
const reorderUrl = handle.dataset.reorderUrl;

// Apply styling a millisecond later, so the dragging image shows up correctly
setTimeout(() => { row.classList.add("dragged-row") }, 1)
})
if (!reorderUrl) {
console.warn(`Row ${row.id} is missing a reorder URL!`);
// Check all data attributes for debugging
return;
}

// Make the row draggable only when the handle is clicked
handle.addEventListener("mousedown", function () {
row.draggable = true;
});

handle.addEventListener("mouseup", function () {
row.draggable = false;
});

// Set up drag start
row.addEventListener("dragstart", function (e) {
dragSrcRow = row;
e.dataTransfer.effectAllowed = "move";

// Add a class to style the dragged row
setTimeout(function () {
row.classList.add("dragged-row");
}, 0);
});

row.addEventListener("dragover", (e) => {
e.preventDefault()
e.dataTransfer.dropEffect = "move"
// Handle drag over
row.addEventListener("dragover", function (e) {
if (!dragSrcRow) return;

// If dragged to a new location, move the dragged row
if (dragSrc != row) {
const sourceIndex = dragSrc.rowIndex
const targetIndex = row.rowIndex
e.preventDefault();
e.dataTransfer.dropEffect = "move";

if (sourceIndex < targetIndex) {
table.tBodies[0].insertBefore(dragSrc, row.nextSibling)
if (dragSrcRow !== row) {
// Determine whether to insert before or after
const rect = row.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;

if (e.clientY < midpoint) {
tbody.insertBefore(dragSrcRow, row);
} else {
table.tBodies[0].insertBefore(dragSrc, row)
tbody.insertBefore(dragSrcRow, row.nextSibling);
}
onDragover(dragSrc)
}
})

row.addEventListener("dragend", () => {
// Disable dragging, so only the handle can start the dragging again
row.setAttribute("draggable", "false")
row.classList.remove("dragged-row")
});

if (srcIndex != row.rowIndex) {
onDragEnd(dragSrc)
}
// Handle drag end
row.addEventListener("dragend", function () {
row.draggable = false;
row.classList.remove("dragged-row");

dragSrc = null
srcIndex = null
})
}
}
if (dragSrcRow) {
// Get the new position (1-indexed)
const allRows = tbody.querySelectorAll("tr");
const newPosition = Array.from(allRows).indexOf(dragSrcRow) + 1;

const updateEvenOddClasses = (row, index) => {
row.classList.remove("odd")
row.classList.remove("even")
// Update the backend
updateRowPosition(reorderUrl, newPosition);

if ((index + 1) % 2 == 0) {
row.classList.add("even")
} else {
row.classList.add("odd")
}
}
// Update row classes (odd/even)
Array.from(allRows).forEach(function (r, i) {
r.classList.remove("odd", "even");
r.classList.add(i % 2 === 0 ? "odd" : "even");
});

const updatePositionText = (row, index) => {
const position = row.querySelector(".position")
if (position) {
position.textContent = index
}
dragSrcRow = null;
}
});
});
}

const updateBackend = (url, rowIndex) => {
let headers = { }

const csrfElement = document.querySelector("meta[name=csrf-token]")
if (csrfElement) {
headers["X-CSRF-Token"] = csrfElement.getAttribute("content")
} else {
console.warn("Rails CSRF element not present. AJAX requests may fail due to CORS issues.")
function updateRowPosition(url, position) {
// Get the CSRF token
const csrfToken = document
.querySelector("meta[name='csrf-token']")
?.getAttribute("content");
const headers = {};
if (csrfToken) {
headers["X-CSRF-Token"] = csrfToken;
}

const formData = new FormData()
formData.append("position", rowIndex)
// Create form data
const formData = new FormData();
formData.append("position", position);

fetch(url, { method: "POST", headers, body: formData })
}

document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("table.aa-reorderable").forEach((table) => {
setupReorderable({
table,
onDragover: (_row) => {
const allRows = table.getElementsByTagName('tbody')[0].rows

for (var i = 0; i < allRows.length; i++) {
const loopRow = allRows[i]
const index = i + 1
updateEvenOddClasses(loopRow, index)
updatePositionText(loopRow, index)
}
},
onDragEnd: (row) => {
const handle = row.querySelector(".reorder-handle")
const url = handle.dataset["reorderUrl"]
const allRows = table.getElementsByTagName('tbody')[0].rows
const rowIndex = Array.prototype.indexOf.call(allRows, row)

updateBackend(url, rowIndex + 1)
// Send the request
fetch(url, {
method: "POST",
headers: headers,
body: formData,
})
.then(function (response) {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
})
})
})
.catch(function (error) {
console.error("Error updating position:", error);
});
}
57 changes: 52 additions & 5 deletions app/assets/stylesheets/activeadmin_reorderable.scss
Original file line number Diff line number Diff line change
@@ -1,13 +1,60 @@
@use "@activeadmin/activeadmin/src/scss/mixins/all" as *;

.aa-reorderable {
/* Styles for both classic ActiveAdmin and ActiveAdmin 4.0 with Tailwind */
.aa-reorderable,
[data-reorderable="true"],
.index-as-table {
.reorder-handle {
cursor: move;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 4px;
background-color: #f9fafb;
border: 1px solid #e5e7eb;

@include light-button;
&:hover {
background-color: #f3f4f6;
}
}

.dragged-row {
opacity: 0;
opacity: 0.5;
background-color: #f0f0f0 !important;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
}

/* Additional styles specific to ActiveAdmin 4.0 */
.data-table {
.reorder-handle-col {
width: 30px;
min-width: 30px;
cursor: move;
vertical-align: middle;
}

tr {
&[draggable="true"] {
cursor: move;
}
}

.reorder-handle {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 4px;

&:hover {
background-color: #f0f0f0;
}
}
}

/* Style the SVG icon inside the handle */
.reorder-handle svg {
width: 16px;
height: 16px;
color: currentColor;
}
40 changes: 35 additions & 5 deletions lib/active_admin/reorderable/dsl.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
module ActiveAdmin
module Reorderable
module DSL
private

def reorderable(&block)
body = proc do
# In ActiveAdmin 4.0, we need a different approach
# This method is called on the resource DSL (ActiveAdmin.register)
def reorderable
# Log that we're registering the reorderable functionality
# Add the member action for handling reordering via AJAX
member_action :reorder, method: :post do
resource.insert_at(params[:position].to_i)
head :ok
end

member_action(:reorder, :method => :post, &block || body)
# Only available in AA4
if defined?(::ActiveAdmin::Views::IndexTableFor)
# Add the reorder_column method to IndexTableFor
::ActiveAdmin::Views::IndexTableFor.class_eval do
def reorder_column
column "", data: { column: 'reorder' }, class: "reorder-handle-col", sortable: false do |resource|

# Get the URL for the reorder action
aa_resource = active_admin_namespace.resource_for(resource.class)
url = aa_resource.route_member_action_path(:reorder, resource)


# Test both attribute formats
# Render the handle with an SVG icon - ensure we're using valid HTML attributes
content_tag :span,
class: 'reorder-handle',
data: { reorder_url: url, reorder_id: resource.id },
style: 'cursor: move; display: inline-flex; align-items: center; justify-content: center; padding: 4px;' do
raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>')
end
end
end
end
end
end
end
end
end

# Include in the ResourceDSL
::ActiveAdmin::ResourceDSL.send(:include, ActiveAdmin::Reorderable::DSL)
24 changes: 20 additions & 4 deletions lib/active_admin/reorderable/table_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module Reorderable
module TableMethods

def reorder_column
column '', :class => 'reorder-handle-col' do |resource|
# Add data attribute to help with debugging
column '', class: 'reorder-handle-col', data: { reorderable_handle: true } do |resource|
reorder_handle_for(resource)
end
end
Expand All @@ -13,16 +14,31 @@ def reorder_column
def reorder_handle_for(resource)
aa_resource = active_admin_namespace.resource_for(resource.class)
url = aa_resource.route_member_action_path(:reorder, resource)

span(reorder_handle_content, :class => 'reorder-handle', 'data-reorder-url' => url)

# Create a handle that works with both classic AA and AA4 with Tailwind
span(reorder_handle_content,
class: 'reorder-handle',
'data-reorder-url' => url,
'data-reorder-id' => resource.id,
style: 'cursor: move; display: inline-block;'
)
end

def reorder_handle_content
'&equiv;&equiv;'.html_safe
# SVG icon more compatible with Tailwind styling
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>'.html_safe
end

end

# Include the TableMethods in both the TableFor and new AA4 "standard" table
::ActiveAdmin::Views::TableFor.send(:include, TableMethods)

# Attempt to detect and include in AA4's data table class if it exists
if defined?(::ActiveAdmin::Views::IndexAsTable::IndexTableFor)
::ActiveAdmin::Views::IndexAsTable::IndexTableFor.send(:include, TableMethods)
end
end
end
Loading