Packages

phoenix_kit

2.22.16
3.0.0 2.65.0 2.64.1 2.64.0 2.63.0 2.62.2 2.62.1 2.62.0 2.61.0 2.60.3 2.60.2 2.60.1 2.60.0 2.59.0 2.58.0 2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit priv static assets phoenix_kit_sortable.js
Raw

priv/static/assets/phoenix_kit_sortable.js

/**
 * PhoenixKit Sortable Grid
 *
 * Auto-loads SortableJS from CDN and provides drag-and-drop functionality
 * for the sortable_grid component.
 *
 * SETUP: Add the hook to your app.js:
 *
 *   // In assets/js/app.js, add to your Hooks object:
 *   import "../../../deps/phoenix_kit/priv/static/assets/phoenix_kit_sortable.js"
 *
 *   let Hooks = {
 *     // ... your other hooks ...
 *     SortableGrid: window.SortableGridHook
 *   }
 *
 *   let liveSocket = new LiveSocket("/live", Socket, {
 *     hooks: Hooks,
 *     // ... other options
 *   })
 *
 * That's it! The hook auto-loads SortableJS from CDN when needed.
 */
(function() {
  "use strict";

  // Prevent multiple initializations
  if (window.PhoenixKitSortable) return;
  window.PhoenixKitSortable = true;

  var SORTABLE_CDN = "https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js";
  var sortableLoading = false;
  var sortableCallbacks = [];
  var stylesInjected = false;

  /**
   * Inject CSS styles for sortable classes
   */
  function injectStyles() {
    if (stylesInjected) return;
    stylesInjected = true;

    var style = document.createElement("style");
    style.textContent = [
      ".sortable-ghost { opacity: 0.5; }",
      ".sortable-chosen { outline: 2px solid oklch(var(--p)); outline-offset: 2px; }",
      ".sortable-drag { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); }",
      "@keyframes pk-sortable-wiggle { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(-1.5deg); } 75% { transform: rotate(1.5deg); } }",
      ".pk-sortable-wiggle { animation: pk-sortable-wiggle 0.4s ease-in-out infinite; }",
      ".pk-sortable-wiggle:nth-child(even) { animation-delay: 0.1s; }",
      ".pk-sortable-wiggle:nth-child(3n) { animation-delay: 0.2s; }",
      "@media (prefers-reduced-motion: reduce) { .pk-sortable-wiggle { animation: none; } }"
    ].join("\n");
    document.head.appendChild(style);
  }

  /**
   * Load SortableJS from CDN if not already loaded
   */
  function loadSortableJS(callback) {
    if (window.Sortable) {
      callback();
      return;
    }

    sortableCallbacks.push(callback);

    if (sortableLoading) return;
    sortableLoading = true;

    var script = document.createElement("script");
    script.src = SORTABLE_CDN;
    script.onload = function() {
      sortableCallbacks.forEach(function(cb) { cb(); });
      sortableCallbacks = [];
    };
    script.onerror = function() {
      console.error("[PhoenixKitSortable] Failed to load SortableJS from CDN");
    };
    document.head.appendChild(script);
  }

  /**
   * LiveView Hook for sortable grids
   */
  var SortableGridHook = {
    mounted: function() {
      var self = this;
      loadSortableJS(function() {
        // Small delay to let LiveView finish any initial DOM morphing
        setTimeout(function() {
          self.initSortable();
        }, 100);
      });
    },

    updated: function() {
      // Re-init if items changed
      if (this.sortable) {
        var currentItems = this.el.querySelectorAll(".sortable-item[data-id]");
        if (currentItems.length !== this._itemCount) {
          this.sortable.destroy();
          this.initSortable();
        }
      }
    },

    destroyed: function() {
      if (this.sortable) {
        this.sortable.destroy();
        this.sortable = null;
      }
    },

    initSortable: function() {
      var self = this;
      var container = this.el;
      var eventName = container.dataset.sortableEvent || "reorder_items";
      var hideSource = container.dataset.sortableHideSource === "true";

      // Inject CSS styles for sortable classes
      injectStyles();

      this._itemCount = container.querySelectorAll(".sortable-item[data-id]").length;

      this.sortable = window.Sortable.create(container, {
        animation: 150,
        draggable: ".sortable-item",
        filter: ".sortable-ignore",
        forceFallback: true,  // Use JS-based drag instead of native HTML5 drag (avoids image drag conflicts)
        fallbackOnBody: true,  // Fixes offset in modals with CSS transforms
        ghostClass: "sortable-ghost",
        chosenClass: "sortable-chosen",
        dragClass: "sortable-drag",
        onStart: function() {
          if (hideSource) {
            // Hide the fallback clone that SortableJS places at the initial position on body
            setTimeout(function() {
              var fallback = document.querySelector("body > .sortable-fallback");
              if (fallback) fallback.style.display = "none";
            }, 0);
          }
        },
        onEnd: function(evt) {
          // Get new order of item IDs
          var items = container.querySelectorAll(".sortable-item[data-id]");
          var orderedIds = Array.from(items).map(function(el) {
            return el.dataset.id;
          });

          // Push event to LiveView
          self.pushEvent(eventName, { ordered_ids: orderedIds });
        }
      });
    }
  };

  // Export for window (hook registration)
  window.SortableGridHook = SortableGridHook;
})();