Skip to content

Commit d412c22

Browse files
committed
feat: add drag-and-drop feature
1 parent 6ba1f02 commit d412c22

File tree

10 files changed

+376
-75
lines changed

10 files changed

+376
-75
lines changed

dist/simple-tree-component.js

Lines changed: 134 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
clearButton: false,
2626
scrollContainer: null,
2727
defaultDropdownHeight: 300,
28+
dragAndDrop: false,
2829
};
2930

3031
var constants = {
@@ -66,6 +67,7 @@
6667
events: {
6768
SelectionChanged: "selectionChanged",
6869
SelectionChanging: "selectionChanging",
70+
NodeOrderChanged: "nodeOrderChanged",
6971
_NodeSelected: "_nodeSelected",
7072
EscapePressed: "_escapePressed",
7173
HoverChanged: "_hoverChanged",
@@ -173,6 +175,91 @@
173175
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
174176
}
175177

178+
class DragAndDropHandler {
179+
constructor(setNodeIndex) {
180+
this.setNodeIndex = setNodeIndex;
181+
this.boundOnDragStart = this.onDragStart.bind(this);
182+
this.boundOnDragOver = this.onDragOver.bind(this);
183+
this.boundOnDrop = this.onDrop.bind(this);
184+
}
185+
initialize(liElement) {
186+
liElement.setAttribute("draggable", "true");
187+
liElement.addEventListener("dragstart", this.boundOnDragStart);
188+
liElement.addEventListener("dragover", this.boundOnDragOver);
189+
liElement.addEventListener("drop", this.boundOnDrop);
190+
}
191+
destroy(liElement) {
192+
liElement.removeAttribute("draggable");
193+
liElement.removeEventListener("dragstart", this.boundOnDragStart);
194+
liElement.removeEventListener("dragover", this.boundOnDragOver);
195+
liElement.removeEventListener("drop", this.boundOnDrop);
196+
}
197+
onDragStart(e) {
198+
var _a;
199+
const target = e.target;
200+
if (!target.hasAttribute("draggable")) {
201+
return;
202+
}
203+
if (e.dataTransfer) {
204+
e.dataTransfer.effectAllowed = "move";
205+
target.setAttribute("data-dragging", "true");
206+
(_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.setData("text/plain", target.id);
207+
}
208+
}
209+
onDragOver(e) {
210+
var _a, _b, _c, _d;
211+
e.preventDefault();
212+
const target = this.getTargetListItem(e.target);
213+
if (!target) {
214+
return;
215+
}
216+
const { elements: sameLevelNodes, ids: sameLevelNodeIds } = this.getSameLevelNodes(target);
217+
const draggedId = (_a = sameLevelNodes.find((x) => x.getAttribute("data-dragging") === "true")) === null || _a === void 0 ? void 0 : _a.id;
218+
if (!draggedId) {
219+
return;
220+
}
221+
if (!sameLevelNodeIds.includes(draggedId)) {
222+
return;
223+
}
224+
const toDrop = document.getElementById(draggedId);
225+
if (!toDrop) {
226+
return;
227+
}
228+
const bounding = target.getBoundingClientRect();
229+
const offset = bounding.y + bounding.height / 2;
230+
if (e.clientY - offset > 0) {
231+
(_b = target.parentElement) === null || _b === void 0 ? void 0 : _b.insertBefore(toDrop, target.nextSibling);
232+
}
233+
else {
234+
(_c = target.parentElement) === null || _c === void 0 ? void 0 : _c.insertBefore(toDrop, target);
235+
}
236+
(_d = e.dataTransfer) === null || _d === void 0 ? void 0 : _d.setData("text/plain", JSON.stringify({ id: toDrop.id, newIndex: sameLevelNodeIds.indexOf(toDrop.id) }));
237+
}
238+
onDrop(e) {
239+
var _a, _b;
240+
e.preventDefault();
241+
e.stopPropagation();
242+
const droppedId = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData("text/plain");
243+
if (droppedId) {
244+
(_b = document.getElementById(droppedId)) === null || _b === void 0 ? void 0 : _b.removeAttribute("data-dragging");
245+
const { ids } = this.getSameLevelNodes(document.getElementById(droppedId));
246+
this.setNodeIndex(droppedId, ids.indexOf(droppedId));
247+
}
248+
}
249+
getTargetListItem(t) {
250+
while (t.parentElement && t.tagName !== "LI") {
251+
t = t.parentElement;
252+
}
253+
return t;
254+
}
255+
getSameLevelNodes(target) {
256+
var _a;
257+
const sameLevelNodes = Array.from(((_a = target.parentElement) === null || _a === void 0 ? void 0 : _a.children) || []);
258+
const sameLevelNodeIds = sameLevelNodes.map((x) => x.id);
259+
return { elements: sameLevelNodes, ids: sameLevelNodeIds };
260+
}
261+
}
262+
176263
class BaseTree {
177264
constructor(element, config, dataService, eventManager, readOnly) {
178265
this.element = element;
@@ -185,10 +272,17 @@
185272
this.searchTextInput = null;
186273
this.searchTextInputEvent = null;
187274
this.keyEventHandler = new KeyEventHandler(this.eventManager, this.dataService, this.readOnly);
275+
this.dragAndDropHandler = new DragAndDropHandler((uid, newIndex) => {
276+
this.dataService.setNodeIndex(uid, newIndex);
277+
this.eventManager.publish(constants.events.NodeOrderChanged, this.dataService.getNodes());
278+
});
188279
this.subscription = this.eventManager.subscribe(constants.events.HoverChanged, (n) => this.hoverNode(n));
189280
}
190281
destroy() {
191282
this.deactivateKeyListener();
283+
if (this.config.dragAndDrop) {
284+
this.removeDragAndDropListeners();
285+
}
192286
if (this.subscription) {
193287
this.subscription.dispose();
194288
this.subscription = null;
@@ -208,6 +302,12 @@
208302
deactivateKeyListener() {
209303
this.keyEventHandler.destroy();
210304
}
305+
removeDragAndDropListeners() {
306+
const nodeContainer = this.getNodeContainer();
307+
if (nodeContainer) {
308+
Array.from(nodeContainer.querySelectorAll("li")).forEach((x) => this.dragAndDropHandler.destroy(x));
309+
}
310+
}
211311
setNodeUiState(node, current, cssClass) {
212312
var _a, _b, _c;
213313
if (!node || current !== node.value) {
@@ -266,6 +366,9 @@
266366
renderTree() {
267367
const nodeContainer = this.getNodeContainer();
268368
if (nodeContainer) {
369+
if (this.config.dragAndDrop) {
370+
this.removeDragAndDropListeners();
371+
}
269372
nodeContainer.innerHTML = "";
270373
nodeContainer.appendChild(this.renderUnorderedList(this.dataService.allNodes));
271374
}
@@ -279,13 +382,16 @@
279382
highlightRegex = new RegExp(`(${escapeRegex((_b = this.searchTextInput) === null || _b === void 0 ? void 0 : _b.value)})`, "ig");
280383
}
281384
nodes.forEach((node) => {
282-
var _a;
385+
var _a, _b;
283386
if (node.hidden) {
284387
return;
285388
}
286389
const hasChildren = ((_a = node.children) === null || _a === void 0 ? void 0 : _a.length) > 0;
287390
const liElement = document.createElement("li");
288391
liElement.id = node.uid;
392+
if (this.config.dragAndDrop && !((_b = this.searchTextInput) === null || _b === void 0 ? void 0 : _b.value)) {
393+
this.dragAndDropHandler.initialize(liElement);
394+
}
289395
const lineWrapperDiv = document.createElement("div");
290396
lineWrapperDiv.classList.add(constants.classNames.SimpleTreeNodeWrapper);
291397
lineWrapperDiv.addEventListener("mouseover", () => this.hoverNode(node));
@@ -381,7 +487,7 @@
381487
}
382488
}
383489
collapseAllNodes(flag) {
384-
this.dataService.getAllNodes().forEach((t) => this.collapseNode(t, flag, false));
490+
this.dataService.getNodesInternal().forEach((t) => this.collapseNode(t, flag, false));
385491
this.renderTree();
386492
}
387493
setReadOnly(readOnly) {
@@ -511,9 +617,12 @@
511617
clear() {
512618
this.allNodes = [];
513619
}
514-
getAllNodes() {
620+
getNodesInternal() {
515621
return this.allNodes;
516622
}
623+
getNodes() {
624+
return this.allNodes.map(this.copyNode);
625+
}
517626
getNode(value) {
518627
const nodeToReturn = this.getNodeInternal(this.allNodes, value);
519628
if (nodeToReturn) {
@@ -593,14 +702,17 @@
593702
node.label = newLabel;
594703
}
595704
}
596-
getParentForNode(nodes, value) {
705+
getParentForNode(nodes, comparisonValue, predicate = null) {
706+
if (!predicate) {
707+
predicate = (n) => n.value === comparisonValue;
708+
}
597709
for (const node of nodes) {
598-
if (node.children && node.children.some((n) => n.value === value)) {
710+
if (node.children && node.children.some(predicate)) {
599711
return node;
600712
}
601713
let parent = null;
602714
if (node.children) {
603-
parent = this.getParentForNode(node.children, value);
715+
parent = this.getParentForNode(node.children, comparisonValue, predicate);
604716
}
605717
if (parent) {
606718
return parent;
@@ -808,6 +920,22 @@
808920
}
809921
return `${this.treeInstanceId}-${Math.abs(hash)}`;
810922
}
923+
setNodeIndex(uid, newIndex) {
924+
const node = this.allNodes.find((node) => node.uid === uid);
925+
if (node) {
926+
this.allNodes.splice(this.allNodes.indexOf(node), 1);
927+
this.allNodes.splice(newIndex, 0, node);
928+
}
929+
else {
930+
const parent = this.getParentForNode(this.allNodes, uid, (n) => n.uid === uid);
931+
if (parent) {
932+
const childNode = parent.children.find((node) => node.uid === uid);
933+
parent.children.splice(parent.children.indexOf(childNode), 1);
934+
parent.children.splice(newIndex, 0, childNode);
935+
}
936+
}
937+
console.log(this.allNodes);
938+
}
811939
}
812940

813941
class CommonTreeLogic {

dist/simple-tree-component.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/types/options.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ export interface TreeConfiguration {
9393
* (Default: `300`)
9494
*/
9595
defaultDropdownHeight: number;
96+
/**
97+
* Indicates if drag-and-drop of nodes on the same hierarchy-level is enabled.
98+
* Only used in `tree` mode.
99+
* (Default: `false`)
100+
*/
101+
dragAndDrop: boolean;
96102
}
97103
/**
98104
* All instance-specific options and behaviors to initialize the tree.

src/__tests__/data-service.spec.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ describe("simpleTree", () => {
2222
dataService.addNode(treeNode);
2323

2424
expect(dataService.getNode("parent4")).not.toBeNull();
25-
expect(dataService.getAllNodes().length).toEqual(4);
26-
expect(dataService.getAllNodes()[3]).toEqual(expect.objectContaining(treeNode));
25+
expect(dataService.getNodesInternal().length).toEqual(4);
26+
expect(dataService.getNodesInternal()[3]).toEqual(expect.objectContaining(treeNode));
2727
});
2828

2929
it("addNode - should add node to specified parent by reference", () => {
@@ -48,7 +48,7 @@ describe("simpleTree", () => {
4848
it("addNode - should not allow adding duplicate node", () => {
4949
const treeNode = createTreeNode("Parent 3", "parent3");
5050
expect(() => dataService.addNode(treeNode)).toThrowError();
51-
expect(dataService.getAllNodes().length).toEqual(3);
51+
expect(dataService.getNodesInternal().length).toEqual(3);
5252
});
5353

5454
it("addNode - avoid non-selectable to be selected", () => {
@@ -87,9 +87,9 @@ describe("simpleTree", () => {
8787
});
8888

8989
it("deleteNode - should not remove anything if no node was found", () => {
90-
const nodeCountBefore = countTreeNodes(dataService.getAllNodes());
90+
const nodeCountBefore = countTreeNodes(dataService.getNodesInternal());
9191
dataService.deleteNode("parent4");
92-
expect(nodeCountBefore).toEqual(countTreeNodes(dataService.getAllNodes()));
92+
expect(nodeCountBefore).toEqual(countTreeNodes(dataService.getNodesInternal()));
9393
});
9494

9595
it("updateNodeLabel - should update the label of the specified node", () => {
@@ -189,18 +189,18 @@ describe("simpleTree", () => {
189189
});
190190

191191
it("moveNode - should not move anything if node is the only one in list", () => {
192-
let node = dataService.getAllNodes()[1].children[1].children[0];
193-
expect(dataService.getAllNodes()[1].children[1].children[0].value).toEqual("parent2Child2Sub1");
192+
let node = dataService.getNodesInternal()[1].children[1].children[0];
193+
expect(dataService.getNodesInternal()[1].children[1].children[0].value).toEqual("parent2Child2Sub1");
194194

195195
dataService.moveNode(node, "up");
196196

197-
node = dataService.getAllNodes()[1].children[1].children[0];
198-
expect(dataService.getAllNodes()[1].children[1].children[0].value).toEqual("parent2Child2Sub1");
197+
node = dataService.getNodesInternal()[1].children[1].children[0];
198+
expect(dataService.getNodesInternal()[1].children[1].children[0].value).toEqual("parent2Child2Sub1");
199199
});
200200

201201
it("moveNode - should switch nodes and also move child nodes", () => {
202-
let firstNode = dataService.getAllNodes()[0];
203-
let secondNode = dataService.getAllNodes()[1];
202+
let firstNode = dataService.getNodesInternal()[0];
203+
let secondNode = dataService.getNodesInternal()[1];
204204
expect(firstNode.value).toEqual("parent1");
205205
expect(firstNode.children.length).toEqual(2);
206206
expect(secondNode.value).toEqual("parent2");
@@ -209,8 +209,8 @@ describe("simpleTree", () => {
209209

210210
dataService.moveNode(firstNode, "down");
211211

212-
firstNode = dataService.getAllNodes()[0];
213-
secondNode = dataService.getAllNodes()[1];
212+
firstNode = dataService.getNodesInternal()[0];
213+
secondNode = dataService.getNodesInternal()[1];
214214
expect(firstNode.value).toEqual("parent2");
215215
expect(firstNode.children.length).toEqual(2);
216216
expect(firstNode.children[1].children.length).toEqual(2);
@@ -219,8 +219,8 @@ describe("simpleTree", () => {
219219

220220
dataService.moveNode(secondNode, "up");
221221

222-
firstNode = dataService.getAllNodes()[0];
223-
secondNode = dataService.getAllNodes()[1];
222+
firstNode = dataService.getNodesInternal()[0];
223+
secondNode = dataService.getNodesInternal()[1];
224224
expect(firstNode.value).toEqual("parent1");
225225
expect(firstNode.children.length).toEqual(2);
226226
expect(secondNode.value).toEqual("parent2");
@@ -229,30 +229,30 @@ describe("simpleTree", () => {
229229
});
230230

231231
it("moveNode - should not move nodes on end of list", () => {
232-
let firstNode = dataService.getAllNodes()[0].children[0];
233-
let secondNode = dataService.getAllNodes()[0].children[1];
232+
let firstNode = dataService.getNodesInternal()[0].children[0];
233+
let secondNode = dataService.getNodesInternal()[0].children[1];
234234
expect(firstNode.value).toEqual("parent1Child1");
235235
expect(secondNode.value).toEqual("parent1Child2");
236236

237237
dataService.moveNode(firstNode, "up");
238238

239-
firstNode = dataService.getAllNodes()[0].children[0];
240-
secondNode = dataService.getAllNodes()[0].children[1];
239+
firstNode = dataService.getNodesInternal()[0].children[0];
240+
secondNode = dataService.getNodesInternal()[0].children[1];
241241
expect(firstNode.value).toEqual("parent1Child1");
242242
expect(secondNode.value).toEqual("parent1Child2");
243243

244244
dataService.moveNode(secondNode, "down");
245245

246-
firstNode = dataService.getAllNodes()[0].children[0];
247-
secondNode = dataService.getAllNodes()[0].children[1];
246+
firstNode = dataService.getNodesInternal()[0].children[0];
247+
secondNode = dataService.getNodesInternal()[0].children[1];
248248
expect(firstNode.value).toEqual("parent1Child1");
249249
expect(secondNode.value).toEqual("parent1Child2");
250250
});
251251

252252
it("moveNode - should not move anything if node is unknown", () => {
253253
dataService.moveNode("parent4", "up");
254254

255-
const nodeList = dataService.getAllNodes();
255+
const nodeList = dataService.getNodesInternal();
256256
expect(nodeList[0].value).toEqual("parent1");
257257
expect(nodeList[1].value).toEqual("parent2");
258258
expect(nodeList[2].value).toEqual("parent3");

0 commit comments

Comments
 (0)