Drag and Drop Nested Tree for Bootstrap - BsNestedSortable
| File Size: | 33.8 KB |
|---|---|
| Views Total: | 1 |
| Last Update: | |
| Publish Date: | |
| Official Website: | Go to website |
| License: | MIT |
BsNestedSortable is a jQuery and Bootstrap plugin that turns flat parent-child data into a draggable, collapsible, hierarchical tree structure. It's designed for category managers, menu builders, taxonomy editors, and other admin screens where your users reorder nested records and save the revised hierarchy as JSON.
The plugin rebuilds parent IDs, levels, sibling positions, ancestor lists, and descendant lists after a drag. Its serialization layer can write several tree representations to form fields, page elements, the console, alerts, or global variables.
Features:
- Converts flat parent ID data into draggable nested branches.
- Recalculates levels, parents, children, and sibling order after each drag.
- Serializes tree state as flat records, hierarchy objects, or relationship maps.
- Supports touch dragging through jQuery UI Touch Punch.
- Branch creation, editing, deletion, collapse, and expansion controls.
- Maps custom record field names to the expected tree data model.
- Limits nesting depth through a configurable maximum level.
- Async save callbacks for reorder, add, edit, and delete actions.
- Optional branch images and descriptions.
How to use it:
1. Load the required CSS and JavaScript in your HTML document. The plugin uses jQuery, jQuery UI Sortable, Bootstrap styles, and its own CSS and JavaScript files. The default icons use Font Awesome.
<!-- Bootstrap tree styles --> <link rel="stylesheet" href="/path/to/cdn/bootstrap.min.css"> <!-- Font Awesome Icons --> <link rel="stylesheet" href="/path/to/cdn/all.min.css"> <!-- Plugin styles --> <link rel="stylesheet" href="/path/to/css/BsNestedSortable.css"> <!-- jQuery must load before jQuery UI and Bootstrap JS --> <script src="/path/to/cdn/jquerymin.js"></script> <script src="/path/to/cdn/jquery-ui.min.js"></script> <!-- Optional touch adapter --> <script src="/path/to/cdn/jquery.ui.touch-punch.min.js"></script> <!-- Bootstrap bundle supports the plugin's jQuery modal calls --> <script src="/path/to/cdn/bootstrap.bundle.min.js"></script> <!-- BsNestedSortable --> <script src="/path/to/js/BsNestedSortable.js"></script>
2. Create an empty list for the tree:
<div class="container py-4"> <ul id="category-tree" class="list-group"></ul> </div>
3. Initialize the plugin and pass a flat array with unique IDs, parent IDs, titles, and order values:
var categoryRows = [
{
id: 101,
parent_id: 0,
title: 'Electronics',
description: 'Devices and accessories',
img: '',
order: 1
},
{
id: 102,
parent_id: 101,
title: 'Laptops',
description: 'Portable computers',
img: '',
order: 1
},
{
id: 103,
parent_id: 101,
title: 'Monitors',
description: 'Desktop displays',
img: '',
order: 2
},
{
id: 104,
parent_id: 0,
title: 'Office',
description: 'Workspace products',
img: '',
order: 2
}
];
$(function () {
$('#category-tree').BsNestedSortable({
data: categoryRows,
options: {
rootID: 0,
maxLevel: 4
}
});
});
4. All plugin options:
Top-Level Configuration
data(Array): Supplies flat records for the tree. Each record needs an ID, a parent reference, and a title. The order, image, and description fields are optional.options(Object): Groups tree selectors, data mappings, icons, modal selectors, depth limits, and display settings.language(Object): Replaces labels for empty-tree creation, new records, edit actions, child creation, and deletion.serializeOption(Object): Activates serialization and routes selected output objects to DOM targets, console output, alerts, or global variables.eventsOptions(Object): Registers async callbacks and field exclusions for change comparison.
Tree Options
treeSelector(String): Selects the tree root. The jQuery wrapper replaces the default with the ID and classes from the initialized element. Default:#tree.branchSelector(String): Selects every branch<li>. Default:.tree-branch.branchPathSelector(String): Selects the visual connector beside a branch. Default:.branch-path.dragHandlerSelector(String): Selects the element that starts a drag action. Default:.moveAble.placeholderName(String): Sets the jQuery UI Sortable placeholder class name. Enter the class name with no leading dot. Default:sortable-placeholder.childrenBusSelector(String): Selects the temporary container that carries descendants during a drag. Default:.children-bus.collapseChildren(String): Selects the expand and collapse button. Default:.collapseChildren.levelPrefix(String): Sets the class prefix for branch levels. A second-level branch receives a class such asbranch-level-2. Default:branch-level.depth(Number): Sets the horizontal indentation in pixels for each level. Update the related CSS level rules after changing this value. Default:30.maxHeight(Number): Sets a connector height value used during branch-path calculations. Default:40.minHeight(Number): Sets the minimum extra height used during branch-path calculations. Default:10.insertNewButton(String): Defines the button markup shown for an empty data set. The{insertText}token receives the configured language label.imagesUrlPrefix(String): Adds a shared path before each branch image value. Default: an empty string.maxLevel(Number): Caps the permitted nesting level. Default:3.rootID(String or Number): Identifies top-level records. Default:0.icons(Object): Groups markup for remove, edit, add, expand, and collapse controls.modal(Object): Groups selectors for create, edit, and delete dialogs and their fields.dataAttributes(Object): Maps generateddata-*attribute suffixes.dataKeys(Object): Maps incoming record fields to the tree model.
Icon Options
icons.remove(String): Defines the delete button markup. Default: a Font Awesome trash icon.icons.edit(String): Defines the edit button markup. Default: a Font Awesome edit icon.icons.add(String): Defines the add-child button markup. Default: a Font Awesome plus-square icon.icons.expand(String): Defines the collapsed-state icon. Default: a Font Awesome plus icon.icons.collapse(String): Defines the expanded-state icon. Default: a Font Awesome minus icon.
Modal Options
modal.id(String): Selects the create and edit modal. Default:#myModal.modal.ModalDelete(String): Selects the delete confirmation modal. Default:#myModalDelete.modal.name(String): Selects the branch title field. Default:#CatName.modal.description(String): Selects the branch description field. Default:#CatDescId.modal.image(String): Selects the branch image field. Default:#image.
Data Attribute Options
dataAttributes.id(String): Sets the suffix for the generated branch ID attribute. Default:id.dataAttributes.parent(String): Sets the suffix for the generated parent attribute. Default:parent.dataAttributes.title(String): Defines a title attribute name in the configuration object. The renderer writes titles through.branch-titleinstead of this setting. Default:title.
Data Key Options
dataKeys.id(String): Maps the record ID field. Default:id.dataKeys.parent(String): Maps the parent ID field. Default:parent_id.dataKeys.title(String): Maps the branch title field. Default:title.dataKeys.description(String): Maps the description field. Default:description.dataKeys.image(String): Maps the image path field. Default:img.dataKeys.order(String): Maps the sibling order field. Default:order.
Language Options
language.createTheFirstBranch(String): Sets the empty-tree button text. Default:Insert New Branch.language.newItem(String): Sets the fallback title for a new record. Default:New Item.language.editBranch(String): Sets the edit button title. Default:Edit Branch.language.addBranch(String): Sets the add-child button title. Default:Add a new child.language.removeBranch(String): Sets the delete button title. Default:Remove Branch.
Serialization Options
serializeOption.serializeON(Boolean): Activates initial-state tracking and serialized output. SupplyingserializeOptionoreventsOptionsturns it on automatically.serializeOption.method(String): SelectsJSON,console,alert, orasVar. Default:JSON.serializeOption.call(String): Selectshtml,val, ortextfor JSON output targets. Default:html.serializeOption.outPuts(Object): Maps each serialized representation to a target selector.serializeOption.outPuts.catObj(String): Receives the flat record collection with parent, level, order, ancestor, and child data.serializeOption.outPuts.parentArr(String): Receives the direct parent map for every branch.serializeOption.outPuts.childrenArr(String): Receives direct child arrays grouped by parent ID.serializeOption.outPuts.Hierarchy(String): Receives the expanded recursive hierarchy.serializeOption.outPuts.minHierarchy(String): Receives the compact recursive hierarchy with IDs and child nodes.serializeOption.outPuts.allParentsArr(String): Receives each branch's complete ancestor list.serializeOption.outPuts.allChildrenArr(String): Receives each branch's complete descendant list.
Callback Options
eventsOptions.onComplete(Function): Runs after a completed drag changes the level or position.eventsOptions.onDelete(Function): Runs before a leaf branch is removed.eventsOptions.onEdit(Function): Runs before an edited branch updates its rendered content.eventsOptions.onAdd(Function): Runs before a new branch enters the tree.eventsOptions.excludedObjElms(Array): Lists record properties that the change comparator should ignore.
$('#category-tree').BsNestedSortable({
options: {
depth: 30,
treeSelector: '#tree',
branchSelector: '.tree-branch',
branchPathSelector: '.branch-path',
dragHandlerSelector: '.moveAble',
placeholderName: 'sortable-placeholder',
childrenBusSelector: '.children-bus',
collapseChildren: '.collapseChildren',
levelPrefix: 'branch-level',
maxHeight: 40, //px
minHeight: 10, // px
insertNewButton: '<button type="button" class="btn btn-primary btn-lg btn-block w-100">{insertText}</button>',
imagesUrlPrefix: "",
icons: {
remove: '<i class="far fa-trash-alt"></i>',
edit: '<i class="far fa-edit"></i>',
add: '<i class="far fa-plus-square"></i>',
expand: '<i class="fas fa-plus"></i>',
collapse: '<i class="fas fa-minus"></i>',
},
modal: {
id: "#myModal",
ModalDelete: "#myModalDelete",
name: "#CatName",
description: "#CatDescId",
image: "#image"
},
maxLevel: 3,
rootID: 0,
dataAttributes: {
id: 'id',
parent: 'parent',
title: 'title',
},
dataKeys: {
id: 'id',
parent: 'parent_id',
title: 'title',
description: 'description',
image: 'img',
order: 'order'
},
},
language: {
createTheFirstBranch: "Insert New Branch",
newItem: "New Item",
editBranch: "Edit Branch",
addBranch: "Add a new child",
removeBranch: "Remove Branch",
exportSQL: "Export to SQL",
exportTXT: "Export to TXT",
exportSimple: "Simple Format",
exportDetailed: "Detailed Format",
exportHierarchical: "Hierarchical Format"
},
exportOptions: {
enableExport: true,
sqlTableName: 'nested_sortable',
txtFormat: 'simple' // 'simple', 'detailed', or 'hierarchical'
},
serializeOption: {
serializeON: false,
method: "JSON",
call: "html",
outPuts: {
catObj: "#catObj", // the places which will have all categories
parentArr: "#parentArr", // parents array
childrenArr: "#childrenArr", // children array
Hierarchy: "#hierarchy", // expanded hierarchical array
minHierarchy: "#minHierarchy", // minimized hierarchical array
allParentsArr: "#allParentsArr", // all of parents
allChildrenArr: "#allChildrenArr" // all of children
}
},
eventsOptions: {
onComplete: async function () { return true },
onDelete: async function () { return true },
onEdit: async function () { return true },
onAdd: async function () { return true },
excludedObjElms: [],
},
});
Advanced Examples:
Save the Sorted Tree in Hidden Form Fields
The plugin writes both hidden values during initialization and after drag operations. The built-in create, edit, and delete handlers update their callback state. They do not run the DOM output writer. Persist those actions through onAdd, onEdit, and onDelete, or patch the accepted-action paths to call the internal serializer. Parse and validate every submitted value on the server before writing parent IDs or order values.
<form id="catalog-form" method="post" action="/admin/catalog/save">
<ul id="catalog-tree" class="list-group"></ul>
<!-- Receives the flat record collection -->
<input type="hidden" id="catalog-records" name="catalog_records">
<!-- Receives the compact recursive hierarchy -->
<input type="hidden" id="catalog-hierarchy" name="catalog_hierarchy">
<button type="submit" class="btn btn-primary mt-3">Save Catalog</button>
</form>
<script>
var catalogRows = [
{ id: 1, parent_id: 0, title: 'Hardware', order: 1 },
{ id: 2, parent_id: 1, title: 'Keyboards', order: 1 },
{ id: 3, parent_id: 1, title: 'Pointing Devices', order: 2 },
{ id: 4, parent_id: 0, title: 'Software', order: 2 }
];
$('#catalog-tree').BsNestedSortable({
data: catalogRows,
serializeOption: {
method: 'JSON',
call: 'val',
outPuts: {
catObj: '#catalog-records',
minHierarchy: '#catalog-hierarchy'
}
}
});
</script>
Map an Existing Database Schema
Every parent value must match another mapped ID or the configured root ID. The preparation step moves orphaned records to the root. Duplicate IDs produce ambiguous branch lookups and invalid serialization.
var departmentRows = [
{
node_key: 20,
parent_key: 0,
label: 'Operations',
details: 'Internal operations',
avatar_path: 'departments/operations.svg',
sort_index: 1
},
{
node_key: 21,
parent_key: 20,
label: 'Facilities',
details: 'Office and building services',
avatar_path: 'departments/facilities.svg',
sort_index: 1
}
];
$('#department-tree').BsNestedSortable({
data: departmentRows,
options: {
rootID: 0,
maxLevel: 5,
imagesUrlPrefix: '/media/',
dataKeys: {
id: 'node_key',
parent: 'parent_key',
title: 'label',
description: 'details',
image: 'avatar_path',
order: 'sort_index'
}
}
});
Autosave Reordered Records Through AJAX
A failed reorder request does not restore the old DOM order. Reorder completion does not replace the internal comparison baseline either. Later diff objects may include earlier drag changes. Send theLastSerializ as the authoritative snapshot, reload the records after a failed request, or patch the stop handler to store a new baseline after a successful save.
$('#navigation-tree').BsNestedSortable({
data: navigationRows,
eventsOptions: {
excludedObjElms: ['description', 'img'],
onComplete: async function (ui, diff) {
if (!diff || !diff.modified.length) {
return true;
}
$('#tree-save-status').text('Saving changes...');
try {
await $.ajax({
url: '/admin/navigation/reorder',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({
modified: diff.modified,
tree: diff.theLastSerializ
})
});
$('#tree-save-status').text('Saved');
} catch (requestError) {
$('#tree-save-status').text('Save failed. Reload the tree before editing again.');
}
return true;
}
}
});
Connect Custom Bootstrap Modals
Keep the fixed hidden fields and action classes expected by the plugin. The create and edit dialog needs a .submit button and a .close control. The delete dialog needs #IdOfDelete and a .Delete button. Bootstrap 5 projects need native bootstrap.Modal calls in the plugin code.
$('#menu-tree').BsNestedSortable({
data: menuRows,
options: {
modal: {
id: '#menu-item-modal',
ModalDelete: '#menu-delete-modal',
name: '#menu-item-label',
description: '#menu-item-notes',
image: '#menu-item-icon'
}
}
});
Alternatives And Related Resources:
- Sortable List View With jQuery - treeSortable: Reorder tree nodes across multiple nesting levels through jQuery UI drag and drop.
- jQuery Plugin To Sort Nested Lists Using Drag and Drop - nestedSortable: Add sortable behavior to nested HTML lists with drag handles, depth limits, and serialized hierarchy data.
- Sortable and Collapsible Tree View Plugin For jQuery: Combine nested list sorting with expandable and collapsible branches in a touch-enabled tree interface.
- Dynamic Interactive Tree Table Plugin For Bootstrap - GTreeTable 2: Render editable Bootstrap tree tables with drag-and-drop reordering, sorting, caching, and hierarchical records.
- Hierarchical Data Display with Shadcn/ui Tree View: Build a modern React tree view with expandable nodes, responsive layouts, and drag-and-drop item movement.
FAQs:
Q: Does BsNestedSortable require jQuery UI?
A: Yes. The drag layer calls jQuery UI Sortable, and the plugin extends jQuery objects with its own traversal helpers. Load jQuery first, then jQuery UI, then BsNestedSortable.
Q: Why do the add, edit, and delete modals fail in Bootstrap 5?
A: The plugin calls the removed jQuery .modal() interface. Use Bootstrap 4 for the original flow or replace those calls with Bootstrap 5 bootstrap.Modal instances.
Q: Can the plugin initialize from AJAX data?
A: Yes. Fetch the array first, then call BsNestedSortable({ data: rows }) after the request completes. Reinitialization can duplicate handlers. Create the tree once per page load.
Q: Why does a branch move to the root after initialization?
A: Its parent value does not match another record ID. Fix the parent reference or change rootID to match the root value used by the data source.
Q: Does the generated tree support keyboard drag controls and ARIA tree semantics?
A: The renderer does not add ARIA tree roles or keyboard reordering controls. Add a separate accessible move-up, move-down, indent, and outdent interface when keyboard operation forms part of the project requirement.
This awesome jQuery plugin is developed by karamelikli. For more Advanced Usages, please check the demo page or visit the official website.
- Prev: Able Player: Accessible HTML5 Media Player with Captions
- Next: None











