(written by Grok and reviewed by Max Milbers)
Audience: Extension developers, template builders
Library: SortableJS (MIT) under media/com_virtuemart/js/sortable/
This is not a full SortableJS API manual. It is why VirtueMart uses it, how core loads it, and what you actually have to write — which is usually markup, not a second init loop.
1. Why drag-and-drop still matters in a shop
Merchants reorder media, custom fields, related items, price rows, country lists — every day. That used to mean jQuery UI Sortable, another dependency chain, and fragile “init after AJAX” scripts.
VirtueMart 5 standardises on SortableJS: small, maintained, no jQuery, works with plain DOM lists and tables. Core admin already uses it. You get the same library and the same wait pattern as the core.
Product pitch in one line: reorderable UI without inventing your own drag stack — and without racing module load order.
2. Where it lives
| File | Role |
|---|---|
media/com_virtuemart/js/sortable/sortable.core.esm.min.js |
SortableJS library (ES module) |
media/com_virtuemart/js/sortable/sortable-global.js |
Globalizer + VM init: import + window.Sortable + initSortable() on [data-vmjs-sortable] |
media/com_virtuemart/js/sortable/LICENSE |
MIT — SortableJS contributors |
Modern admin loads this in vmJsApi::loadModernAdminJs() / the admin UI bootstrap. You do not copy those two addvScriptModule lines onto every view that already runs that bootstrap.
3. How VirtueMart loads it (you usually do not repeat this)
Sortable is the textbook header + globalizer example. Core does:
vmJsApi::addvScriptModule('sortable/sortable.core.esm.min');
vmJsApi::addvScriptModule('sortable/sortable-global');
The globalizer (shipped by core) conceptually:
import Sortable from './sortable.core.esm.min.js';
window.Sortable = Sortable;
VMInit.add('Sortable Ready', initSortable, 520, { reInit: true });
initSortable() lives in sortable-global.js. It attaches drag-and-drop to every [data-vmjs-sortable] container (and .vm-js-sortable as an extra class). That wait-and-attach loop is core code. Do not paste it into a plugin “to be safe”. If your markup uses the core hooks on a page that loaded sortable-global, it is already running.
Full story of the init queue: VMInit registry Technics article.
4. What you write: markup
This is the normal 3rd-party job. Core then:
- restricts the drag to a handle so links and buttons stay clickable,
- on
TBODY, treats rows as the draggable unit, - on drag end, renumbers
input.ordering/.ordering(and.orderin list tables) so a normal form save persists the order.
<ul class="vm-js-sortable" data-vmjs-sortable>
<li>
<span class="vm-sortable-handle">☰</span>
Item A
<input type="hidden" class="ordering" name="ordering[]" value="0">
</li>
<li>
<span class="vm-sortable-handle">☰</span>
Item B
<input type="hidden" class="ordering" name="ordering[]" value="1">
</li>
</ul>
| Hook | Who consumes it |
|---|---|
data-vmjs-sortable on the list/table body |
Core initSortable() in sortable-global.js — use this |
.vm-js-sortable |
Product-edit module as well; still fine as an extra class |
.vm-sortable-handle |
Drag grip (legacy .uk-sortable-handle still accepted) |
input.ordering / .ordering / .order |
Renumbered in onEnd; PHP still saves the form |
Core examples: media gallery thumbs, product custom-field / related blocks, admin list bodies, price rows.
5. When the library is not loaded yet
If the page does not run the modern admin JS bootstrap, load the same two modules. Calling them again on a page that already loaded them is cheap (same asset name). After that, stop at markup with data-vmjs-sortable.
vmJsApi::addvScriptModule('sortable/sortable.core.esm.min');
vmJsApi::addvScriptModule('sortable/sortable-global');
6. Options we actually use
| Option | Typical VM use |
|---|---|
handle |
Grip only, so buttons stay clickable |
animation |
Short ms value (core often ~80) |
draggable |
On TBODY: tr |
onEnd |
Renumber ordering fields for the next save |
Group, multi-drag, swap plugins: official SortableJS manual (below). Core stays on a small subset.
7. Do’s and don’ts
Do
- Prefer
data-vmjs-sortable+ handle + ordering fields and let core init. - Load the dual pair only when your page does not already have modern admin JS.
- Persist order through form fields — Sortable only moves DOM.
Don’t
- Do not paste core’s
querySelectorAll('[data-vmjs-sortable]')loop into a plugin. - Do not ship jQuery UI Sortable next to SortableJS on the same page.
- Do not assume
window.Sortableexists at parse time of a head inline script.
8. What this is not
- Not a CMS-wide drag framework — list/container reordering for shop UIs.
- Not automatic server sort — you still save ordering fields.
- Not a full SortableJS course — advanced plugins live upstream.
9. Official SortableJS documentation
SortableJS — official site / docs
Repository: github.com/SortableJS/Sortable
SortableJS is MIT-licensed. VirtueMart ships the core ESM build and a thin globalizer so the shop stack can treat window.Sortable as a shared dependency.
Related Technics:
- VMInit registry — priorities,
waitForGlobal,reInit - Public assets in media/com_virtuemart — where JS lives and how templates override a file
- Why Alpine.js — HTML-first UI (not a replacement for drag ordering)