Drag and Drop – The Hack Edition

In this video, I add drag and drop sorting to a normal Business Central list page.

https://youtu.be/ZkH5ftnw_7Y

In this video, Erik takes on a hacking challenge: implementing drag-and-drop sorting on a list page in Business Central using AL and JavaScript. This isn’t a polished, production-ready solution — it’s a live experiment to see if it’s even possible to reorder rows in a BC repeater by dragging them. Spoiler: it works, and it’s a lot of fun to watch come together.

The Inspiration: SharePoint Connector Drop Zone

Erik starts by showing off a feature from the SharePoint connector app he’s built, where you can drag a file from your file explorer directly onto a Business Central page and have it upload to SharePoint. The entire SharePoint file list is rendered using native BC UI — no embedded HTML iframe — and the file list area acts as a drop zone.

The original plan for this video was to demonstrate how that drop zone works. But then a colleague named Jan texted Erik asking: “How can you do drag-and-drop sorting in BC? Like reordering rows in a list?” Erik initially said it couldn’t be done — but then started thinking it might actually be possible with some creative hacking.

Setting Up the Foundation

Erik prepared a simple table with three fields: a primary key, a description, and a sorting index. The page is a standard list page sorted by the sorting index. He also scaffolded out a control add-in with the minimal setup needed for a JavaScript-based hack.

The Table

table 50141 "Drop table"
{
    Caption = 'Drop table';
    DataClassification = ToBeClassified;

    fields
    {
        field(1; pkey; Code[10])
        {
            Caption = 'pkey';
            DataClassification = ToBeClassified;
        }
        field(2; Description; Text[50])
        {
            Caption = 'Description';
            DataClassification = ToBeClassified;
        }
        field(3; "Sorting Index"; Integer)
        {
            Caption = 'Sorting Index';
            DataClassification = ToBeClassified;
        }
    }
    keys
    {
        key(PK; pkey)
        {
            Clustered = true;
        }
        key(sort; "Sorting Index")
        {

        }
    }
}

The Control Add-in Definition

The control add-in is intentionally tiny — just 1 pixel — since it doesn’t need to render anything visible. It serves purely as a bridge between AL and JavaScript.

controladdin DragDrop
{
    Scripts = 'DropScript.js';
    StartupScript = 'DropStartup.js';
    MaximumHeight = 1;
    MinimumHeight = 1;
    MaximumWidth = 1;
    MinimumWidth = 1;
    RequestedHeight = 1;
    RequestedWidth = 1;

    event ControlReady();

    procedure DragDropEnable(IDField: Text);
    event DropEvent(DragID: Text; DropID: Text);
}

Exploring the DOM

Since the control add-in lives inside an iframe, Erik needs to break out to the parent window to access the actual page DOM. Using the browser’s developer tools, he inspects the rendered list page and discovers that Business Central renders repeater data as a standard HTML table with the CSS class ms-nav-grid-data-table.

Key observations from the DOM inspection:

  • The repeater rows are <tr> elements inside a <tbody>
  • Data rows have an aria-rowindex attribute (distinguishing them from header rows)
  • Each cell contains elements with ariaLabel attributes that include the field caption and value (e.g., pkey, 4)

This is the foundation of the entire hack — and also why it’s fragile. Microsoft could change these internal DOM structures at any time.

Making Rows Draggable

The first step is to locate the table and make each data row draggable. The JavaScript reaches up from the iframe to the parent document, finds the grid table, iterates over the rows, and sets the HTML5 draggable attribute on each one:

function DragDropEnable(IDField)
{
    _field = IDField;
    let DropTable = window.parent.document.querySelector(".ms-nav-grid-data-table");

    var rows = DropTable.querySelectorAll('table tr');
    [].forEach.call(rows, function(row) {
        if (row.hasAttribute('aria-rowindex'))
        {
            row.setAttribute('draggable', 'true');
            row.addEventListener('drop', handleDrop, false);
            row.addEventListener('dragstart', handleDragStart, false);
            row.addEventListener('dragover', handleDragOver, false);
        }
    });
}

After adding the draggable attribute, Erik immediately sees that rows can be picked up and dragged in the browser — a promising start. But without drop handlers, there’s nowhere to put them yet.

Implementing the Drag and Drop Event Handlers

HTML5 drag and drop requires several event handlers working together. Erik adds three:

  • dragstart — Captures which row is being dragged and extracts its primary key
  • dragover — Calls preventDefault() to allow the drop (without this, the browser shows a “forbidden” icon)
  • drop — Handles the actual drop, identifies the target row, and fires an event back to AL

Extracting the Primary Key

This is where the clever (and hacky) DOM gymnastics come in. When a drag starts, Erik uses a CSS attribute selector to find the element whose ariaLabel starts with the primary key field name, then extracts the value:

function handleDragStart(e)
{
    _CurrentDrag = e.srcElement.querySelector("[ariaLabel^='" + _field + "']").getAttribute("title");
}

On the drop side, there’s an extra challenge: the drop event’s source element is whatever specific element you dropped onto (often a <span> with text), not the table row. Erik uses the closest() function to climb back up the DOM tree to the parent <tr>:

function handleDrop(e)
{
    if (e.stopPropagation)
        e.stopPropagation(); 
    
    var _CurrentDrop = e.srcElement.closest("table tr")
        .querySelector("[ariaLabel^='" + _field + "']")
        .getAttribute("title");
    
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod("DropEvent", [_CurrentDrag, _CurrentDrop]);
}

The dragover Handler

function handleDragOver(e)
{
    if (e.preventDefault)
        e.preventDefault();

    return false;
}

The field name is passed in as a parameter from AL, making the JavaScript slightly more reusable rather than hard-coding it.

The Startup Script

The startup script is minimal — it just signals to AL that the control is ready:

// Drop Control / E Foqus Canada

HTMLContainer = document.getElementById("controlAddIn");
    
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod("ControlReady", []);

Handling the Drop in AL

Back in AL, the page wires everything together. When the control add-in is ready, it calls DragDropEnable with the primary key field caption. When a drop event fires, it receives both the dragged and target primary keys, looks up both records, and adjusts the sorting index:

page 50142 "Drop Table"
{
    ApplicationArea = All;
    Caption = 'Drop Table';
    PageType = List;
    SourceTable = "Drop table";
    SourceTableView = sorting("Sorting Index");
    UsageCategory = Lists;

    layout
    {
        area(content)
        {
            repeater(General)
            {
                field(pkey; pkey)
                {
                    ApplicationArea = All;
                }
                field(Description; Rec.Description)
                {
                    ApplicationArea = All;
                }
                field("Sorting Index"; "Sorting Index")
                {
                    ApplicationArea = All;
                }
            }
            usercontrol(HTML; DragDrop)
            {
                ApplicationArea = all;
                trigger ControlReady()
                begin
                    CurrPage.HTML.DragDropEnable('pkey');
                end;

                trigger DropEvent(DragID: Text; DropID: Text)
                var
                    drag: Record "Drop table";
                    drop: Record "Drop table";
                begin
                    drag.get(dragID);
                    drop.get(DropID);
                    if drag."Sorting Index" < drop."Sorting Index" then
                        drag."Sorting Index" := drop."Sorting Index" + 1
                    else
                        drag."Sorting Index" := drop."Sorting Index" - 1;
                    drag.Modify();
                    CurrPage.Update(false);
                end;
            }
        }
    }
}

The sorting logic is intentionally simple: if you drag a row downward (its sorting index is less than the target's), it gets placed just after the target. If you drag upward, it goes just before. After modifying the record, CurrPage.Update(false) refreshes the page to reflect the new order.

The Result

And it works! Erik demonstrates dragging "Boba Fett" onto "Vigo" and the rows reorder correctly. Dragging back onto "Bill Gates" works too. The sorting index updates, the page refreshes, and the rows appear in the new order. Erik can hardly contain his excitement — and rightfully so.

Important Caveats

Erik is very upfront that this is a hack, not production code. Here's why:

  • DOM dependency — The solution relies on internal Business Central DOM structures (CSS class names, aria attributes, element hierarchy) that Microsoft can change without notice in any cumulative update
  • Single table assumption — The querySelector grabs the first matching table on the page. If there are multiple repeaters or grids, this could target the wrong one
  • Simple primary keys only — The primary key extraction assumes a single-field key whose caption and value can be parsed from an ariaLabel attribute
  • Sorting logic is naive — The +1/-1 approach to sorting indexes works for demos but would need a proper renumbering algorithm for real use
  • Not AppSource-ready — This approach would likely not pass AppSource validation and could break with platform updates

Conclusion

What started as "that can't be done" turned into a working proof of concept for drag-and-drop row sorting in Business Central. By combining a tiny control add-in with some creative JavaScript DOM manipulation, Erik shows that BC's rendered HTML tables can be made draggable using standard HTML5 drag-and-drop APIs. The JavaScript identifies rows, extracts primary keys from aria attributes, and fires events back to AL code that handles the actual data reordering. It's fragile, it's hacky, and it's absolutely not production code — but it proves the concept works and demonstrates some powerful techniques for bridging JavaScript and AL in Business Central.