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