From db91f0a2b4d6d4ad753a476dc6c863a161a6e015 Mon Sep 17 00:00:00 2001
From: abaghdasaryan <arthur.baghdasar@gmail.com>
Date: Thu, 16 Jan 2025 10:26:11 +0400
Subject: [PATCH] Replace Dragula with core Sortable JS

---
 composer.libraries.json         |  19 ----
 js/builder.js                   | 152 ++++++++++++++++++++------------
 layout_paragraphs.libraries.yml |  18 +---
 3 files changed, 96 insertions(+), 93 deletions(-)

diff --git a/composer.libraries.json b/composer.libraries.json
index 122943a..7a666c2 100644
--- a/composer.libraries.json
+++ b/composer.libraries.json
@@ -11,31 +11,12 @@
   "license": "GPL-2.0+",
   "minimum-stability": "dev",
   "require": {
-    "bevacqua/dragula": "*",
     "drupal/paragraphs": "^1.6"
   },
   "repositories": {
     "drupal": {
       "type": "composer",
       "url": "https://packages.drupal.org/8"
-    },
-    "dragula": {
-        "type": "package",
-        "package": {
-            "name": "bevacqua/dragula",
-            "version": "3.7.3",
-            "type": "drupal-library",
-            "extra": {
-                "installer-name": "dragula"
-            },
-            "dist": {
-                "url": "https://github.com/bevacqua/dragula/archive/v3.7.3.zip",
-                "type": "zip"
-            },
-            "require": {
-                "composer/installers": "*"
-            }
-        }
     }
   }
 }
diff --git a/js/builder.js b/js/builder.js
index 6af8e5c..00aa38e 100644
--- a/js/builder.js
+++ b/js/builder.js
@@ -1,4 +1,4 @@
-(($, Drupal, debounce, dragula, once) => {
+(($, Drupal, debounce, Sortable, once) => {
   const idAttr = 'data-lpb-id';
 
   /**
@@ -95,7 +95,7 @@
   const reorderComponents = debounce(doReorderComponents);
 
   /**
-   * Returns a list of errors for the "accepts" dragula callback, or an empty array if there are no errors.
+   * Returns a list of errors for the "accepts" Sortable callback, or an empty array if there are no errors.
    * @param {Element} settings The builder settings.
    * @param {Element} el The element being moved.
    * @param {Element} target The destination
@@ -112,7 +112,7 @@
   }
 
   /**
-   * Returns a list of errors for the "moves" dragula callback, or an empty array if there are no errors.
+   * Returns a list of errors for the "moves" Sortable callback, or an empty array if there are no errors.
    * @param {Element} settings The builder settings.
    * @param {Element} el The element being moved.
    * @param {Element} source The source
@@ -413,49 +413,44 @@
     });
   }
 
-  function initDragAndDrop($element, settings) {
-    const containers = once('is-dragula-enabled', '.js-lpb-component-list, .js-lpb-region', $element[0]);
-    const drake = dragula(
-      containers,
-      {
-        accepts: (el, target, source, sibling) =>
-          acceptsErrors(settings, el, target, source, sibling).length === 0,
-        moves: (el, source, handle) =>
-          movesErrors(settings, el, source, handle).length === 0,
-      },
-    );
-    drake.on('drop', (el) => {
-      const $el = $(el);
-      if ($el.prev().is('a')) {
-        $el.insertBefore($el.prev());
-      }
-      $element.trigger('lpb-component:drop', [$el.attr('data-uuid')]);
-    });
-    drake.on('drag', (el) => {
-      $element.addClass('is-dragging');
-      if (el.className.indexOf('lpb-layout') > -1) {
-        $element.addClass('is-dragging-layout');
-      } else {
-        $element.addClass('is-dragging-item');
-      }
-      $element.trigger('lpb-component:drag', [$(el).attr('data-uuid')]);
-    });
-    drake.on('dragend', () => {
-      $element
-        .removeClass('is-dragging')
-        .removeClass('is-dragging-layout')
-        .removeClass('is-dragging-item');
-    });
-    drake.on('over', (el, container) => {
-      $(container).addClass('drag-target');
-    });
-    drake.on('out', (el, container) => {
-      $(container).removeClass('drag-target');
+  /**
+   * Initializes Sortable.js for drag-and-drop.
+   * @param {jQuery} $element The builder element.
+   * @param {Object} settings The builder settings.
+   */
+  function initSortable($element, settings) {
+    // Find all containers that should support drag-and-drop.
+    const containers = once('is-sortable-enabled', '.js-lpb-component-list, .js-lpb-region', $element[0]);
+
+    containers.forEach((container) => {
+      Sortable.create(container, {
+        group: 'shared',
+        draggable: '.js-lpb-component',
+        handle: '.lpb-drag',
+        animation: 150,
+        onStart: (evt) => {
+          const $el = $(evt.item);
+          $el.addClass('is-dragging');
+          $element.addClass('is-dragging');
+          $element.trigger('lpb-component:drag', [$el.attr('data-uuid')]);
+        },
+        onEnd: (evt) => {
+          const $el = $(evt.item);
+          $el.removeClass('is-dragging');
+          $element.removeClass('is-dragging');
+          reorderComponents($element); // Trigger reordering logic.
+          $element.trigger('lpb-component:drop', [$el.attr('data-uuid')]);
+        },
+        onMove: (evt) => {
+          // Validate move using acceptsErrors (if applicable).
+          const errors = acceptsErrors(settings, evt.dragged, evt.to, evt.from, evt.related);
+          return errors.length === 0; // Allow move only if no errors.
+        },
+      });
     });
-    return drake;
   }
 
-  // An object with arrays for "accepts" and "moves" dragula callback functions.
+  // An object with arrays for "accepts" and "moves" Sortable callback functions.
   Drupal._lpbMoveErrors = {
     'accepts': [],
     'moves': [],
@@ -463,7 +458,7 @@
   /**
    * Registers a move validation function.
    * @param {Function} f The validator function.
-   * @param {String} t The dragula callback to register the validator for.
+   * @param {String} t The Sortable callback to register the validator for.
    */
   Drupal.registerLpbMoveError = (f, c = 'accepts') => {
     Drupal._lpbMoveErrors[c].push(f);
@@ -561,28 +556,71 @@
 
       // Listen to relevant events and update UI.
       once('lpb-events', '[data-lpb-id]').forEach((el) => {
-        $(el).on('lpb-builder:init.lpb lpb-component:insert.lpb lpb-component:update.lpb lpb-component:move.lpb lpb-component:drop.lpb lpb-component:delete.lpb', (e) => {
-          const $element = $(e.currentTarget);
-          updateUi($element);
-        });
+        $(el).on(
+          'lpb-builder:init.lpb lpb-component:insert.lpb lpb-component:update.lpb lpb-component:move.lpb lpb-component:drop.lpb lpb-component:delete.lpb',
+          (e) => {
+            const $element = $(e.currentTarget);
+            updateUi($element);
+            // Remove focus from all `+` buttons after a new component is inserted.
+            const $addButton = $element.find('.lpb-btn--add:focus');
+            if ($addButton.length) {
+              $addButton.blur();
+            }
+          }
+        );
       });
 
-      // Initialize the editor drag and drop ui.
+      // Initialize the editor drag-and-drop UI with Sortable.js.
       once('lpb-enabled', '[data-lpb-id].has-components').forEach((el) => {
         const $element = $(el);
         const id = $element.attr(idAttr);
         const lpbSettings = settings.lpBuilder[id];
-        // Attach event listeners and init dragula just once.
-        $element.data('drake', initDragAndDrop($element, lpbSettings));
+        // Attach event listeners and initialize Sortable.js.
+        initSortable($element, lpbSettings);
         attachEventListeners($element, lpbSettings);
         $element.trigger('lpb-builder:init');
       });
 
-      // Add new containers to the dragula instance.
-      once('is-dragula-enabled', '.js-lpb-region').forEach((c) => {
-        const builderElement = c.closest('[data-lpb-id]');
-        const drake = $(builderElement).data('drake');
-        drake.containers.push(c);
+      // Add new containers dynamically to Sortable instances.
+      once('is-sortable-enabled', '.js-lpb-region').forEach((container) => {
+        const $builderElement = $(container).closest('[data-lpb-id]');
+        const sortableInstance = $builderElement.data('sortable');
+
+        if (sortableInstance) {
+          // Dynamically add the container to the existing Sortable instance.
+          Sortable.create(container, {
+            group: sortableInstance.options.group,
+            draggable: '.js-lpb-component',
+            handle: '.lpb-drag',
+            animation: 150,
+            onStart: (evt) => {
+              const $el = $(evt.item);
+              $el.addClass('is-dragging');
+              $builderElement.addClass('is-dragging');
+              $builderElement.trigger('lpb-component:drag', [$el.attr('data-uuid')]);
+            },
+            onEnd: (evt) => {
+              const $el = $(evt.item);
+              $el.removeClass('is-dragging');
+              $builderElement.removeClass('is-dragging');
+              reorderComponents($builderElement); // Trigger reordering logic.
+              $builderElement.trigger('lpb-component:drop', [$el.attr('data-uuid')]);
+            },
+            onMove: (evt) => {
+              // Validate move using acceptsErrors (if applicable).
+              const errors = acceptsErrors(
+                sortableInstance.options.settings,
+                evt.dragged,
+                evt.to,
+                evt.from,
+                evt.related
+              );
+              return errors.length === 0; // Allow move only if no errors.
+            },
+          });
+        } else {
+          console.warn('No Sortable instance found for:', $builderElement);
+        }
       });
 
       // If UI elements have been attached to the DOM, we need to attach behaviors.
@@ -626,4 +664,4 @@
     window.addEventListener('dialog:aftercreate', handleAfterDialogCreate);
   }
 
-})(jQuery, Drupal, Drupal.debounce, dragula, once);
+})(jQuery, Drupal, Drupal.debounce, Sortable, once);
diff --git a/layout_paragraphs.libraries.yml b/layout_paragraphs.libraries.yml
index f62170d..2809da3 100644
--- a/layout_paragraphs.libraries.yml
+++ b/layout_paragraphs.libraries.yml
@@ -1,19 +1,3 @@
-dragula:
-  remote: https://github.com/bevacqua/dragula
-  version: 3.7.3
-  license:
-    name: MIT
-    url: https://github.com/bevacqua/dragula/blob/master/license
-    gpl-compatible: true
-  directory: dragula
-  cdn:
-    /libraries/dragula/dist/: https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.3/
-  css:
-    theme:
-      /libraries/dragula/dist/dragula.min.css: { minified: true }
-  js:
-    /libraries/dragula/dist/dragula.min.js: { minified: true }
-
 component_form:
   js:
     js/component-form.js: {}
@@ -36,7 +20,7 @@ builder:
   js:
     js/builder.js: {}
   dependencies:
-    - layout_paragraphs/dragula
+    - core/sortable
     - core/jquery
     - core/once
     - core/drupal.dialog
-- 
GitLab