diff --git a/app/assets/javascripts/activeadmin_reorderable.js b/app/assets/javascripts/activeadmin_reorderable.js
index 0f540af..83901c6 100644
--- a/app/assets/javascripts/activeadmin_reorderable.js
+++ b/app/assets/javascripts/activeadmin_reorderable.js
@@ -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);
+ });
+}
diff --git a/app/assets/stylesheets/activeadmin_reorderable.scss b/app/assets/stylesheets/activeadmin_reorderable.scss
index f19ceda..ab6f0a4 100644
--- a/app/assets/stylesheets/activeadmin_reorderable.scss
+++ b/app/assets/stylesheets/activeadmin_reorderable.scss
@@ -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;
}
diff --git a/lib/active_admin/reorderable/dsl.rb b/lib/active_admin/reorderable/dsl.rb
index 5066929..9697d52 100644
--- a/lib/active_admin/reorderable/dsl.rb
+++ b/lib/active_admin/reorderable/dsl.rb
@@ -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('')
+ end
+ end
+ end
+ end
+ end
end
end
end
end
+
+# Include in the ResourceDSL
+::ActiveAdmin::ResourceDSL.send(:include, ActiveAdmin::Reorderable::DSL)
diff --git a/lib/active_admin/reorderable/table_methods.rb b/lib/active_admin/reorderable/table_methods.rb
index 040c54e..c1378b1 100644
--- a/lib/active_admin/reorderable/table_methods.rb
+++ b/lib/active_admin/reorderable/table_methods.rb
@@ -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
@@ -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
- '≡≡'.html_safe
+ # SVG icon more compatible with Tailwind styling
+ ''.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
diff --git a/lib/active_admin/views/index_as_reorderable_table.rb b/lib/active_admin/views/index_as_reorderable_table.rb
index eb8a29e..73a5d79 100644
--- a/lib/active_admin/views/index_as_reorderable_table.rb
+++ b/lib/active_admin/views/index_as_reorderable_table.rb
@@ -1,20 +1,51 @@
module ActiveAdmin
module Views
+ # This class is just a placeholder/compatibility layer
+ # In ActiveAdmin 4.0, we're adding the reorder_column method directly to IndexTableFor
class IndexAsReorderableTable < IndexAsTable
-
def self.index_name
'reorderable_table'
end
def build(page_presenter, collection)
add_class 'aa-reorderable'
+ # Add data-reorderable attribute for easier JS selection with ActiveAdmin 4
+ @table_options = { data: { reorderable: true } }
super(page_presenter, collection)
end
def table_for(*args, &block)
- insert_tag ReorderableTableFor, *args, &block
+ # Ensure the first column is a reorder handle
+ new_block = proc do
+ # Try both methods of adding the reorder column
+ if respond_to?(:reorder_column)
+ reorder_column
+ else
+ column '', class: 'reorder-handle-col' do |resource|
+ span(''.html_safe,
+ class: 'reorder-handle',
+ 'data-reorder-url' => active_admin_namespace.resource_for(resource.class).route_member_action_path(:reorder, resource),
+ 'data-reorder-id' => resource.id,
+ style: 'cursor: move; display: inline-block;'
+ )
+ end
+ end
+
+ # Call the original block
+ instance_eval(&block) if block
+ end
+
+ # First try with our custom table component
+ begin
+ tag = insert_tag ReorderableTableFor, *args, &new_block
+ tag
+ rescue => e
+ # If that fails, try with the standard AA4 table
+ super(*args, &new_block)
+ end
end
-
end
end
end
diff --git a/lib/active_admin/views/reorderable_table_for.rb b/lib/active_admin/views/reorderable_table_for.rb
index dc8aa05..32023e5 100644
--- a/lib/active_admin/views/reorderable_table_for.rb
+++ b/lib/active_admin/views/reorderable_table_for.rb
@@ -4,14 +4,42 @@ class ReorderableTableFor < IndexAsTable::IndexTableFor
builder_method :reorderable_table_for
def build(collection, options = {}, &block)
- options[:class] = [options[:class], 'aa-reorderable'].compact.join(' ')
-
+ # Add both classic and Tailwind-compatible classes
+ options[:class] = [options[:class], 'aa-reorderable', 'data-table'].compact.join(' ')
+ # Add data attribute for easier selection with modern JS
+ options[:data] ||= {}
+ options[:data][:reorderable] = true
+
super(collection, options) do
reorder_column
block.call if block.present?
end
end
-
+ end
+
+ # Try to monkey-patch ActiveAdmin 4.0's table implementation if it exists
+ begin
+ aa4_table_classes = ActiveAdmin::Views.constants.select { |c| c.to_s.include?('Table') }
+
+ if defined?(::ActiveAdmin::Views::Table)
+ class ::ActiveAdmin::Views::Table
+ def reorderable_column
+ column '', class: 'reorder-handle-col', data: { reorderable_handle: true } do |resource|
+ aa_resource = active_admin_namespace.resource_for(resource.class)
+ url = aa_resource.route_member_action_path(:reorder, resource)
+
+ span(''.html_safe,
+ class: 'reorder-handle',
+ 'data-reorder-url' => url,
+ 'data-reorder-id' => resource.id,
+ style: 'cursor: move; display: inline-block;'
+ )
+ end
+ end
+ end
+ end
end
end
end
diff --git a/lib/activeadmin_reorderable/engine.rb b/lib/activeadmin_reorderable/engine.rb
index 797890b..2113a44 100644
--- a/lib/activeadmin_reorderable/engine.rb
+++ b/lib/activeadmin_reorderable/engine.rb
@@ -2,12 +2,48 @@
module ActiveadminReorderable
class Engine < Rails::Engine
+ initializer "activeadmin_reorderable.precompile", group: :all do |app|
+ # Add assets to precompile for ActiveAdmin 3 and below
+ app.config.assets.precompile += %w(activeadmin_reorderable.js activeadmin_reorderable.css)
+ end
+
initializer "activeadmin_reorderable.importmap", before: "importmap" do |app|
# Skip if importmap-rails is not installed
next unless app.config.respond_to?(:importmap)
+ # Add to importmap for ActiveAdmin 4
app.config.importmap.paths << Engine.root.join("config/importmap.rb")
app.config.importmap.cache_sweepers << Engine.root.join("app/assets/javascripts")
end
+
+ initializer "activeadmin_reorderable.load_views", after: "active_admin.load_views" do
+ puts "ActiveAdmin Reorderable: Loading views"
+ ActiveAdmin.application.load_paths.unshift(Engine.root.join("lib/active_admin/views").to_s)
+ end
+
+ initializer "activeadmin_reorderable.register_plugin", after: "active_admin.register_plugin" do
+ puts "ActiveAdmin Reorderable: Registering plugin"
+ require File.expand_path("../../active_admin/reorderable/dsl", __FILE__)
+ require File.expand_path("../../active_admin/reorderable/table_methods", __FILE__)
+ require File.expand_path("../../active_admin/views/index_as_reorderable_table", __FILE__)
+ require File.expand_path("../../active_admin/views/reorderable_table_for", __FILE__)
+ end
+
+ # Setup ActiveAdmin importmap integration
+ initializer "activeadmin_reorderable.setup_importmap", after: "activeadmin.importmap" do
+
+ # Make sure ActiveAdmin knows about our JavaScript
+ if defined?(ActiveAdmin.importmap)
+ js_path = Engine.root.join("app/assets/javascripts/activeadmin_reorderable.js")
+
+ if File.exist?(js_path)
+ ActiveAdmin.importmap.pin "activeadmin_reorderable", to: "activeadmin_reorderable.js", preload: true
+ else
+ puts "WARNING: Could not find #{js_path} ###############"
+ end
+ else
+ puts "WARNING: ActiveAdmin.importmap not defined ###############"
+ end
+ end
end
end