/**
* TreeManager - คลาสจัดการโครงสร้างต้นไม้
*/
class TreeManager {
/**
* สร้าง TreeManager instance ใหม่
* @param {Object} config - การตั้งค่าต้นไม้
* @param {string} config.containerId - ID ของ element ที่จะแสดงต้นไม้
* @param {boolean} config.limitedDepth - บ่งชี้ว่าจำกัดความลึกของระดับชั้นหรือไม่
* @param {number} config.maxDepth - จำนวนระดับชั้นสูงสุด (เมื่อ limitedDepth เป็น true)
* @param {Object} config.levelNames - ชื่อของแต่ละระดับชั้น {1: 'Level 1', 2: 'Level 2', ...}
* @param {string} config.outputId - ID ของ element ที่จะแสดง JSON output
*/
constructor(config) {
this.containerId = config.containerId || 'tree';
this.container = document.getElementById(this.containerId);
this.limitedDepth = config.limitedDepth !== undefined ? config.limitedDepth : true;
this.maxDepth = config.maxDepth || 3;
this.levelNames = config.levelNames || {
1: 'Stage',
2: 'Area',
3: 'Zone'
};
this.outputId = config.outputId || 'json-output';
this.outputElement = document.getElementById(this.outputId);
this.data = [];
this.nextId = 1;
this.draggedItem = null;
this.dropTarget = null;
this.dropPosition = null; // 'before', 'after', 'inside'
this.dragGhost = null;
this.dropBeforeIndicator = null;
this.dropAfterIndicator = null;
this.dropChildIndicator = null;
this._createDropIndicators();
this._setupDragAndDrop();
}
/**
* สร้างตัวชี้บอกตำแหน่งการวาง
* @private
*/
_createDropIndicators() {
this.dropBeforeIndicator = document.createElement('div');
this.dropBeforeIndicator.className = 'drop-indicator drop-before';
this.dropAfterIndicator = document.createElement('div');
this.dropAfterIndicator.className = 'drop-indicator drop-after';
this.dropChildIndicator = document.createElement('div');
this.dropChildIndicator.className = 'drop-indicator-child';
this.dropChildIndicator.textContent = 'Place as child';
this.indentIndicator = document.createElement('div');
this.indentIndicator.className = 'drop-indicator-level indent';
this.indentIndicator.innerHTML = 'Increase level';
this.outdentIndicator = document.createElement('div');
this.outdentIndicator.className = 'drop-indicator-level outdent';
this.outdentIndicator.innerHTML = 'Decrease level';
}
/**
* กำหนด event listeners สำหรับการลากวาง
* @private
*/
_setupDragAndDrop() {
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('tree-content')) {
const itemId = parseInt(e.target.closest('.tree-item').dataset.id);
this.draggedItem = this._findNodeById(itemId, this.data);
if (!this.draggedItem) return;
e.dataTransfer.setData('text/plain', itemId);
const ghost = this._createDragGhost(e.target);
document.body.appendChild(ghost);
const offsetX = e.clientX - e.target.getBoundingClientRect().left;
const offsetY = e.clientY - e.target.getBoundingClientRect().top;
e.dataTransfer.setDragImage(ghost, offsetX, offsetY);
this.dragGhost = ghost;
e.target.classList.add('dragging');
this._createDropZones();
this._highlightValidDropTargets();
}
});
document.addEventListener('dragover', (e) => {
if (!this.draggedItem) return;
e.preventDefault();
const targetItem = e.target.closest('.tree-item');
if (!targetItem) {
this._hideDropHighlights();
return;
}
const targetId = parseInt(targetItem.dataset.id);
const targetNode = this._findNodeById(targetId, this.data);
if (!targetNode) {
this._hideDropHighlights();
return;
}
if (this.draggedItem.id === targetNode.id || this._isDescendantOf(targetNode.id, this.draggedItem)) {
this._hideDropHighlights();
return;
}
const targetRect = targetItem.getBoundingClientRect();
const mouseY = e.clientY;
const topZone = targetRect.top + (targetRect.height * 0.3);
const bottomZone = targetRect.bottom - (targetRect.height * 0.3);
let position;
if (mouseY < topZone) {
position = 'before';
} else if (mouseY > bottomZone) {
position = 'after';
} else {
position = 'inside';
}
let canDrop = false;
if (position === 'inside') {
canDrop = this._canDropAsChild(this.draggedItem, targetNode);
} else {
canDrop = this._canDropInSameLevel(this.draggedItem, targetNode);
}
if (!canDrop) {
this._hideDropHighlights();
return;
}
this.dropTarget = targetNode;
this.dropPosition = position;
this._showDropHighlight(targetItem, position);
});
document.addEventListener('dragleave', (e) => {
if (!e.target.closest('.tree-item') && !e.relatedTarget?.closest('.tree-item')) {
this._hideDropHighlights();
}
});
document.addEventListener('drop', (e) => {
e.preventDefault();
if (!this.draggedItem || !this.dropTarget || !this.dropPosition) {
this._cleanupDrag();
return;
}
this.moveNode(this.draggedItem.id, this.dropTarget.id, this.dropPosition);
this._cleanupDrag();
});
document.addEventListener('dragend', () => {
this._cleanupDrag();
});
}
/**
* สร้าง dropzones เมื่อเริ่มต้นลาก
* @private
*/
_createDropZones() {
this.dropZonesContainer = document.createElement('div');
this.dropZonesContainer.className = 'drop-zones-container';
this.dropZonesContainer.style.position = 'absolute';
this.dropZonesContainer.style.pointerEvents = 'none';
this.dropZonesContainer.style.zIndex = '1000';
document.body.appendChild(this.dropZonesContainer);
this.beforeDropZone = document.createElement('div');
this.beforeDropZone.className = 'drop-zone drop-before';
this.beforeDropZone.style.height = '4px';
this.beforeDropZone.style.background = 'transparent';
this.beforeDropZone.style.position = 'absolute';
this.beforeDropZone.style.display = 'none';
this.dropZonesContainer.appendChild(this.beforeDropZone);
this.afterDropZone = document.createElement('div');
this.afterDropZone.className = 'drop-zone drop-after';
this.afterDropZone.style.height = '4px';
this.afterDropZone.style.background = 'transparent';
this.afterDropZone.style.position = 'absolute';
this.afterDropZone.style.display = 'none';
this.dropZonesContainer.appendChild(this.afterDropZone);
this.insideDropZone = document.createElement('div');
this.insideDropZone.className = 'drop-zone drop-inside';
this.insideDropZone.style.background = 'transparent';
this.insideDropZone.style.position = 'absolute';
this.insideDropZone.style.display = 'none';
this.dropZonesContainer.appendChild(this.insideDropZone);
}
/**
* เพิ่ม highlight ให้กับโหนดทั้งหมดที่สามารถรับการวางได้
* @private
*/
_highlightValidDropTargets() {
const allItems = document.querySelectorAll('.tree-item');
allItems.forEach(item => {
item.classList.remove('valid-drop-target');
});
allItems.forEach(item => {
const itemId = parseInt(item.dataset.id);
if (itemId !== this.draggedItem.id) {
const node = this._findNodeById(itemId, this.data);
if (node && !this._isDescendantOf(node.id, this.draggedItem)) {
item.classList.add('valid-drop-target');
}
}
});
}
/**
* แสดงตำแหน่งวางโดยไม่มีการสร้าง/ลบ DOM elements
* @param {HTMLElement} targetElement - Element เป้าหมายที่จะวาง
* @param {string} position - ตำแหน่งที่จะวาง ('before', 'after', 'inside')
* @private
*/
_showDropHighlight(targetElement, position) {
this._hideDropHighlights();
const rect = targetElement.getBoundingClientRect();
if (position === 'before') {
this.beforeDropZone.style.display = 'block';
this.beforeDropZone.style.top = (rect.top - 2) + 'px';
this.beforeDropZone.style.left = rect.left + 'px';
this.beforeDropZone.style.width = rect.width + 'px';
this.beforeDropZone.style.background = 'var(--primary-color)';
this.beforeDropZone.style.animation = 'pulse 1.5s infinite';
} else if (position === 'after') {
this.afterDropZone.style.display = 'block';
this.afterDropZone.style.top = (rect.bottom - 2) + 'px';
this.afterDropZone.style.left = rect.left + 'px';
this.afterDropZone.style.width = rect.width + 'px';
this.afterDropZone.style.background = 'var(--primary-color)';
this.afterDropZone.style.animation = 'pulse 1.5s infinite';
} else if (position === 'inside') {
this.insideDropZone.style.display = 'block';
this.insideDropZone.style.top = rect.top + 'px';
this.insideDropZone.style.left = rect.left + 'px';
this.insideDropZone.style.width = rect.width + 'px';
this.insideDropZone.style.height = rect.height + 'px';
this.insideDropZone.style.border = '2px dashed var(--primary-color)';
this.insideDropZone.style.backgroundColor = 'rgba(52, 152, 219, 0.1)';
this.insideDropZone.style.borderRadius = '4px';
this.insideDropZone.style.animation = 'glow 1.5s infinite alternate';
}
targetElement.querySelector('.tree-content').classList.add('over-' + position);
}
/**
* ซ่อน highlight ทั้งหมด
* @private
*/
_hideDropHighlights() {
if (this.beforeDropZone) this.beforeDropZone.style.display = 'none';
if (this.afterDropZone) this.afterDropZone.style.display = 'none';
if (this.insideDropZone) this.insideDropZone.style.display = 'none';
const highlightedElements = document.querySelectorAll('.over-before, .over-after, .over-inside');
highlightedElements.forEach(el => {
el.classList.remove('over-before', 'over-after', 'over-inside');
});
}
/**
* ล้างข้อมูลการลากและซ่อนตัวชี้ทั้งหมด
* @private
*/
_cleanupDrag() {
this._hideDropHighlights();
if (this.dropZonesContainer && this.dropZonesContainer.parentNode) {
this.dropZonesContainer.parentNode.removeChild(this.dropZonesContainer);
this.dropZonesContainer = null;
this.beforeDropZone = null;
this.afterDropZone = null;
this.insideDropZone = null;
}
const validTargets = document.querySelectorAll('.valid-drop-target');
validTargets.forEach(item => {
item.classList.remove('valid-drop-target');
});
if (this.dragGhost && this.dragGhost.parentNode) {
this.dragGhost.parentNode.removeChild(this.dragGhost);
}
const draggingItems = document.querySelectorAll('.dragging');
draggingItems.forEach(item => item.classList.remove('dragging'));
this.draggedItem = null;
this.dropTarget = null;
this.dropPosition = null;
}
/**
* สร้างธีมของ element ที่ถูกลาก
* @param {HTMLElement} original - Element ต้นฉบับ
* @returns {HTMLElement} - Element สำหรับแสดงขณะลาก
* @private
*/
_createDragGhost(original) {
const ghost = original.cloneNode(true);
ghost.classList.add('drag-ghost');
ghost.style.position = 'absolute';
ghost.style.top = '-1000px';
ghost.style.opacity = '0.7';
return ghost;
}
/**
* ซ่อนตัวชี้ตำแหน่งการวางทั้งหมด
* @private
*/
_hideDropIndicator() {
[
this.dropBeforeIndicator,
this.dropAfterIndicator,
this.dropChildIndicator,
this.indentIndicator,
this.outdentIndicator
].forEach(indicator => {
if (indicator && indicator.parentNode) {
indicator.parentNode.removeChild(indicator);
}
if (indicator) {
indicator.style.display = 'none';
}
});
}
/**
* ตรวจสอบว่าสามารถวางเป็นลูกของโหนดเป้าหมายได้หรือไม่
* @param {Object} dragNode - โหนดที่ถูกลาก
* @param {Object} targetNode - โหนดเป้าหมาย
* @returns {boolean} - สามารถวางได้หรือไม่
* @private
*/
_canDropAsChild(dragNode, targetNode) {
if (this.limitedDepth) {
const newLevel = targetNode.level + 1;
if (newLevel > this.maxDepth) {
return false;
}
}
return true;
}
/**
* ตรวจสอบว่าสามารถวางในระดับเดียวกับโหนดเป้าหมายได้หรือไม่
* @param {Object} dragNode - โหนดที่ถูกลาก
* @param {Object} targetNode - โหนดเป้าหมาย
* @returns {boolean} - สามารถวางได้หรือไม่
* @private
*/
_canDropInSameLevel(dragNode, targetNode) {
if (targetNode.level === 1 && !targetNode.parentId) {
return true;
}
const targetParent = this._findParentNode(targetNode.id);
if (!targetParent) {
return true;
}
return true;
}
/**
* ตรวจสอบว่าโหนด A เป็นลูกหลานของโหนด B หรือไม่
* @param {number} nodeAId - ID ของโหนด A
* @param {Object} nodeB - โหนด B
* @returns {boolean} - เป็นลูกหลานหรือไม่
* @private
*/
_isDescendantOf(nodeAId, nodeB) {
if (!nodeB.children || nodeB.children.length === 0) {
return false;
}
for (const child of nodeB.children) {
if (child.id === nodeAId || this._isDescendantOf(nodeAId, child)) {
return true;
}
}
return false;
}
/**
* หาโหนดตาม ID
* @param {number} id - ID ของโหนดที่ต้องการหา
* @param {Array} nodes - อาร์เรย์ของโหนดที่จะค้นหา (default: this.data)
* @returns {Object|null} - โหนดที่พบ หรือ null ถ้าไม่พบ
* @private
*/
_findNodeById(id, nodes = this.data) {
for (const node of nodes) {
if (node.id === id) {
return node;
}
if (node.children && node.children.length > 0) {
const found = this._findNodeById(id, node.children);
if (found) {
return found;
}
}
}
return null;
}
/**
* หาโหนดพ่อของโหนดที่กำหนด
* @param {number} nodeId - ID ของโหนดที่ต้องการหาพ่อ
* @param {Array} nodes - อาร์เรย์ของโหนดที่จะค้นหา (default: this.data)
* @returns {Object|null} - โหนดพ่อที่พบ หรือ null ถ้าไม่พบ
* @private
*/
_findParentNode(nodeId, nodes = this.data) {
for (const node of nodes) {
if (node.children && node.children.length > 0) {
for (const child of node.children) {
if (child.id === nodeId) {
return node;
}
}
const found = this._findParentNode(nodeId, node.children);
if (found) {
return found;
}
}
}
return null;
}
/**
* คำนวณความลึกสูงสุดของโหนด
* @param {Object} node - โหนดที่ต้องการคำนวณความลึก
* @returns {number} - ความลึกสูงสุด
* @private
*/
_getMaxDepth(node) {
if (!node.children || node.children.length === 0) {
return node.level;
}
let maxChildDepth = 0;
for (const child of node.children) {
const childDepth = this._getMaxDepth(child);
if (childDepth > maxChildDepth) {
maxChildDepth = childDepth;
}
}
return maxChildDepth;
}
/**
* อัพเดตระดับของโหนดและลูกหลานทั้งหมด
* @param {Object} node - โหนดที่ต้องการอัพเดตระดับ
* @param {number} newLevel - ระดับใหม่
* @private
*/
_updateNodeLevel(node, newLevel) {
node.level = newLevel;
if (node.children && node.children.length > 0) {
for (const child of node.children) {
this._updateNodeLevel(child, newLevel + 1);
}
}
}
/**
* เพิ่มโหนดระดับบนสุด
* @param {string} name - ชื่อของโหนด
* @returns {Object} - โหนดที่ถูกสร้าง
*/
addRootNode(name = 'New item') {
const newNode = {
id: this.nextId++,
name: name,
level: 1,
children: []
};
this.data.push(newNode);
this._renderTree();
this._updateResult();
return newNode;
}
/**
* เพิ่มโหนดลูก
* @param {number} parentId - ID ของโหนดพ่อ
* @param {string} name - ชื่อของโหนด
* @returns {Object|null} - โหนดที่ถูกสร้าง หรือ null ถ้าไม่สามารถสร้างได้
*/
addChildNode(parentId, name = 'Child item') {
const parent = this._findNodeById(parentId);
if (!parent) {
return null;
}
if (this.limitedDepth && parent.level >= this.maxDepth) {
console.warn('Cannot add: Exceeds maximum depth limit');
return null;
}
const newNode = {
id: this.nextId++,
name: name,
level: parent.level + 1,
parentId: parent.id,
children: []
};
if (!parent.children) {
parent.children = [];
}
parent.children.push(newNode);
this._renderTree();
this._updateResult();
return newNode;
}
/**
* ลบโหนด
* @param {number} nodeId - ID ของโหนดที่ต้องการลบ
* @returns {boolean} - สำเร็จหรือไม่
*/
deleteNode(nodeId) {
const node = this._findNodeById(nodeId);
if (!node) {
return false;
}
if (!node.parentId) {
const index = this.data.findIndex(item => item.id === nodeId);
if (index !== -1) {
this.data.splice(index, 1);
}
} else {
const parent = this._findParentNode(nodeId);
if (parent && parent.children) {
const index = parent.children.findIndex(item => item.id === nodeId);
if (index !== -1) {
parent.children.splice(index, 1);
}
}
}
this._renderTree();
this._updateResult();
return true;
}
/**
* อัพเดตชื่อของโหนด
* @param {number} nodeId - ID ของโหนดที่ต้องการอัพเดต
* @param {string} newName - ชื่อใหม่
* @returns {boolean} - สำเร็จหรือไม่
* @private
*/
_updateNodeName(nodeId, newName) {
const node = this._findNodeById(nodeId);
if (!node) {
return false;
}
node.name = newName;
this._updateResult();
return true;
}
/**
* ย้ายโหนด
* @param {number} nodeId - ID ของโหนดที่ต้องการย้าย
* @param {number} targetId - ID ของโหนดเป้าหมาย
* @param {string} position - ตำแหน่งที่จะวาง ('before', 'after', 'inside', 'indent', 'outdent')
* @returns {boolean} - สำเร็จหรือไม่
*/
moveNode(nodeId, targetId, position) {
const node = this._findNodeById(nodeId);
const target = this._findNodeById(targetId);
if (!node || !target) {
return false;
}
if (!node.parentId) {
const index = this.data.findIndex(item => item.id === nodeId);
if (index !== -1) {
this.data.splice(index, 1);
}
} else {
const parent = this._findParentNode(nodeId);
if (parent && parent.children) {
const index = parent.children.findIndex(item => item.id === nodeId);
if (index !== -1) {
parent.children.splice(index, 1);
}
}
}
if (position === 'indent') {
const previous = this._findPreviousSibling(targetId);
if (previous) {
node.parentId = previous.id;
node.level = previous.level + 1;
if (!previous.children) {
previous.children = [];
}
previous.children.push(node);
} else {
node.parentId = target.id;
node.level = target.level + 1;
if (!target.children) {
target.children = [];
}
target.children.push(node);
}
} else if (position === 'outdent') {
const targetParent = this._findParentNode(targetId);
if (targetParent) {
node.parentId = targetParent.parentId;
node.level = targetParent.level;
if (!targetParent.parentId) {
const index = this.data.findIndex(item => item.id === targetParent.id);
this.data.splice(index + 1, 0, node);
} else {
const grandParent = this._findParentNode(targetParent.id);
const index = grandParent.children.findIndex(item => item.id === targetParent.id);
grandParent.children.splice(index + 1, 0, node);
}
} else {
node.parentId = null;
node.level = 1;
const index = this.data.findIndex(item => item.id === targetId);
this.data.splice(index + 1, 0, node);
}
} else if (position === 'inside') {
node.parentId = target.id;
node.level = target.level + 1;
if (!target.children) {
target.children = [];
}
target.children.push(node);
} else {
if (!target.parentId) {
node.parentId = null;
node.level = 1;
const index = this.data.findIndex(item => item.id === targetId);
if (position === 'before') {
this.data.splice(index, 0, node);
} else {
this.data.splice(index + 1, 0, node);
}
} else {
const parent = this._findParentNode(targetId);
if (parent && parent.children) {
node.parentId = parent.id;
node.level = target.level;
const index = parent.children.findIndex(item => item.id === targetId);
if (position === 'before') {
parent.children.splice(index, 0, node);
} else {
parent.children.splice(index + 1, 0, node);
}
}
}
}
if (node.children && node.children.length > 0) {
for (const child of node.children) {
this._updateNodeLevel(child, node.level + 1);
}
}
this._renderTree();
this._updateResult();
return true;
}
/**
* หาโหนดพี่น้องก่อนหน้าของโหนดที่กำหนด
* @param {number} nodeId - ID ของโหนดที่ต้องการหาพี่น้องก่อนหน้า
* @returns {Object|null} - โหนดพี่น้องก่อนหน้าที่พบ หรือ null ถ้าไม่พบ
* @private
*/
_findPreviousSibling(nodeId) {
const node = this._findNodeById(nodeId);
if (!node) return null;
if (!node.parentId) {
const index = this.data.findIndex(item => item.id === nodeId);
if (index > 0) {
return this.data[index - 1];
}
} else {
const parent = this._findParentNode(nodeId);
if (parent && parent.children) {
const index = parent.children.findIndex(item => item.id === nodeId);
if (index > 0) {
return parent.children[index - 1];
}
}
}
return null;
}
/**
* สลับสถานะการยุบ/ขยายของโหนด
* @param {number} nodeId - ID ของโหนดที่ต้องการสลับสถานะ
* @private
*/
_toggleCollapse(nodeId) {
const itemElement = document.querySelector(`.tree-item[data-id="${nodeId}"]`);
if (itemElement) {
itemElement.classList.toggle('collapsed');
}
}
/**
* ยุบโหนดทั้งหมด
*/
collapseAll() {
const items = document.querySelectorAll('.tree-item');
items.forEach(item => {
if (item.querySelector('.tree-children')) {
item.classList.add('collapsed');
}
});
}
/**
* ขยายโหนดทั้งหมด
*/
expandAll() {
const items = document.querySelectorAll('.tree-item');
items.forEach(item => {
item.classList.remove('collapsed');
});
}
/**
* สร้าง DOM element สำหรับรายการ
* @param {Object} node - ข้อมูลโหนด
* @returns {HTMLElement} - Element ที่สร้าง
* @private
*/
_createTreeItemElement(node) {
const item = document.createElement('div');
item.className = 'tree-item';
item.dataset.id = node.id;
const content = document.createElement('div');
content.className = 'tree-content';
content.draggable = true;
content.classList.add(`level-${node.level}`);
const hasChildren = node.children && node.children.length > 0;
const collapseBtn = document.createElement('span');
collapseBtn.className = 'collapse-btn';
collapseBtn.innerHTML = hasChildren ? '▼' : ' ';
collapseBtn.addEventListener('click', () => {
if (hasChildren) {
this._toggleCollapse(node.id);
collapseBtn.innerHTML = item.classList.contains('collapsed') ? '►' : '▼';
}
});
content.appendChild(collapseBtn);
const label = document.createElement('div');
label.className = 'tree-label';
const levelTag = document.createElement('span');
levelTag.className = 'tree-level-tag';
const levelName = this.levelNames[node.level] || `Level ${node.level}`;
levelTag.textContent = levelName;
label.appendChild(levelTag);
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'tree-name';
nameInput.value = node.name;
nameInput.addEventListener('change', () => {
this._updateNodeName(node.id, nameInput.value);
});
nameInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
nameInput.blur();
}
});
label.appendChild(nameInput);
content.appendChild(label);
const actionsMenu = document.createElement('div');
actionsMenu.className = 'tree-actions-menu';
const moveUpBtn = document.createElement('button');
moveUpBtn.className = 'tree-action-btn';
moveUpBtn.innerHTML = '';
moveUpBtn.title = 'Move up';
moveUpBtn.addEventListener('click', () => {
this.moveNodeUp(node.id);
});
actionsMenu.appendChild(moveUpBtn);
const moveDownBtn = document.createElement('button');
moveDownBtn.className = 'tree-action-btn';
moveDownBtn.innerHTML = '';
moveDownBtn.title = 'Move down';
moveDownBtn.addEventListener('click', () => {
this.moveNodeDown(node.id);
});
actionsMenu.appendChild(moveDownBtn);
const moveLeftBtn = document.createElement('button');
moveLeftBtn.className = 'tree-action-btn';
moveLeftBtn.innerHTML = '';
moveLeftBtn.title = 'Move out (up one level)';
moveLeftBtn.addEventListener('click', () => {
this.moveNodeLeft(node.id);
});
actionsMenu.appendChild(moveLeftBtn);
const moveRightBtn = document.createElement('button');
moveRightBtn.className = 'tree-action-btn';
moveRightBtn.innerHTML = '';
moveRightBtn.title = 'Move in (down one level)';
moveRightBtn.addEventListener('click', () => {
this.moveNodeRight(node.id);
});
actionsMenu.appendChild(moveRightBtn);
const addBtn = document.createElement('button');
addBtn.className = 'tree-action-btn';
addBtn.innerHTML = '';
addBtn.title = 'Add child item';
addBtn.addEventListener('click', () => {
if (this.limitedDepth && node.level >= this.maxDepth) {
alert(`Cannot add child item: Exceeds maximum depth limit (${this.maxDepth} levels)`);
return;
}
this.addChildNode(node.id);
});
actionsMenu.appendChild(addBtn);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'tree-action-btn';
deleteBtn.innerHTML = '';
deleteBtn.title = 'Delete this item';
deleteBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to delete this item and all its children?')) {
this.deleteNode(node.id);
}
});
actionsMenu.appendChild(deleteBtn);
content.appendChild(actionsMenu);
item.appendChild(content);
if (hasChildren) {
const children = document.createElement('div');
children.className = 'tree-children';
node.children.forEach(child => {
children.appendChild(this._createTreeItemElement(child));
});
item.appendChild(children);
}
return item;
}
/**
* เลื่อนโหนดขึ้นหนึ่งตำแหน่งในระดับเดียวกัน
* @param {number} nodeId - ID ของโหนดที่ต้องการเลื่อน
* @returns {boolean} - สำเร็จหรือไม่
*/
moveNodeUp(nodeId) {
const node = this._findNodeById(nodeId);
if (!node) return false;
let siblings;
let nodeIndex;
if (!node.parentId) {
siblings = this.data;
nodeIndex = siblings.findIndex(item => item.id === nodeId);
} else {
const parent = this._findParentNode(nodeId);
if (!parent || !parent.children) return false;
siblings = parent.children;
nodeIndex = siblings.findIndex(item => item.id === nodeId);
}
if (nodeIndex <= 0) return false;
const temp = siblings[nodeIndex];
siblings[nodeIndex] = siblings[nodeIndex - 1];
siblings[nodeIndex - 1] = temp;
this._renderTree();
this._updateResult();
return true;
}
/**
* เลื่อนโหนดลงหนึ่งตำแหน่งในระดับเดียวกัน
* @param {number} nodeId - ID ของโหนดที่ต้องการเลื่อน
* @returns {boolean} - สำเร็จหรือไม่
*/
moveNodeDown(nodeId) {
const node = this._findNodeById(nodeId);
if (!node) return false;
let siblings;
let nodeIndex;
if (!node.parentId) {
siblings = this.data;
nodeIndex = siblings.findIndex(item => item.id === nodeId);
} else {
const parent = this._findParentNode(nodeId);
if (!parent || !parent.children) return false;
siblings = parent.children;
nodeIndex = siblings.findIndex(item => item.id === nodeId);
}
if (nodeIndex >= siblings.length - 1) return false;
const temp = siblings[nodeIndex];
siblings[nodeIndex] = siblings[nodeIndex + 1];
siblings[nodeIndex + 1] = temp;
this._renderTree();
this._updateResult();
return true;
}
/**
* เลื่อนโหนดไปทางซ้าย (เลื่อนระดับขึ้น)
* @param {number} nodeId - ID ของโหนดที่ต้องการเลื่อน
* @returns {boolean} - สำเร็จหรือไม่
*/
moveNodeLeft(nodeId) {
const node = this._findNodeById(nodeId);
if (!node) return false;
if (!node.parentId) return false;
const parent = this._findParentNode(nodeId);
if (!parent) return false;
const grandparent = this._findParentNode(parent.id);
const parentIndex = !grandparent
? this.data.findIndex(item => item.id === parent.id)
: grandparent.children.findIndex(item => item.id === parent.id);
const nodeIndex = parent.children.findIndex(item => item.id === nodeId);
if (nodeIndex === -1) return false;
const movedNode = parent.children.splice(nodeIndex, 1)[0];
movedNode.level = parent.level;
movedNode.parentId = parent.parentId;
if (movedNode.children && movedNode.children.length > 0) {
for (const child of movedNode.children) {
this._updateNodeLevel(child, movedNode.level + 1);
}
}
if (!grandparent) {
this.data.splice(parentIndex + 1, 0, movedNode);
} else {
grandparent.children.splice(parentIndex + 1, 0, movedNode);
}
this._renderTree();
this._updateResult();
return true;
}
/**
* เลื่อนโหนดไปทางขวา (เลื่อนระดับลง) - จะเป็นลูกของโหนดที่อยู่ข้างบน
* @param {number} nodeId - ID ของโหนดที่ต้องการเลื่อน
* @returns {boolean} - สำเร็จหรือไม่
*/
moveNodeRight(nodeId) {
const node = this._findNodeById(nodeId);
if (!node) return false;
let siblings;
let nodeIndex;
if (!node.parentId) {
siblings = this.data;
nodeIndex = siblings.findIndex(item => item.id === nodeId);
} else {
const parent = this._findParentNode(nodeId);
if (!parent || !parent.children) return false;
siblings = parent.children;
nodeIndex = siblings.findIndex(item => item.id === nodeId);
}
if (nodeIndex <= 0) return false;
if (this.limitedDepth) {
const potentialNewLevel = node.level + 1;
if (potentialNewLevel > this.maxDepth) {
alert(`Cannot indent: Exceeds maximum depth limit (${this.maxDepth} levels)`);
return false;
}
}
const newParent = siblings[nodeIndex - 1];
if (this._isDescendantOf(newParent.id, node)) {
return false;
}
siblings.splice(nodeIndex, 1);
node.parentId = newParent.id;
node.level = newParent.level + 1;
if (node.children && node.children.length > 0) {
for (const child of node.children) {
this._updateNodeLevel(child, node.level + 1);
}
}
if (!newParent.children) {
newParent.children = [];
}
newParent.children.push(node);
this._renderTree();
this._updateResult();
return true;
}
/**
* แสดงผลต้นไม้
* @private
*/
_renderTree() {
this.container.innerHTML = '';
this.data.forEach(node => {
this.container.appendChild(this._createTreeItemElement(node));
});
}
/**
* อัพเดตผลลัพธ์ JSON
* @private
*/
_updateResult() {
if (this.outputElement) {
this.outputElement.textContent = JSON.stringify(this.data, null, 2);
}
}
/**
* สร้างโครงสร้างเริ่มต้น
*/
generateInitialStructure() {
this.data = [
{
id: this.nextId++,
name: 'Item 1',
level: 1,
children: [
{
id: this.nextId++,
name: 'Child item 1.1',
level: 2,
parentId: 1,
children: [
{
id: this.nextId++,
name: 'Child item 1.1.1',
level: 3,
parentId: 2,
children: []
}
]
},
{
id: this.nextId++,
name: 'Child item 1.2',
level: 2,
parentId: 1,
children: []
}
]
},
{
id: this.nextId++,
name: 'Item 2',
level: 1,
children: []
}
];
this._renderTree();
this._updateResult();
}
/**
* รับข้อมูลต้นไม้ในรูปแบบ JSON
* @returns {Array} - ข้อมูลต้นไม้
*/
getTreeData() {
return JSON.parse(JSON.stringify(this.data));
}
/**
* กำหนดข้อมูลต้นไม้จาก JSON
* @param {Array} data - ข้อมูลต้นไม้
*/
setTreeData(data) {
this.data = JSON.parse(JSON.stringify(data));
const findMaxId = (nodes) => {
let maxId = 0;
for (const node of nodes) {
if (node.id > maxId) {
maxId = node.id;
}
if (node.children && node.children.length > 0) {
const childMaxId = findMaxId(node.children);
if (childMaxId > maxId) {
maxId = childMaxId;
}
}
}
return maxId;
};
this.nextId = findMaxId(this.data) + 1;
this._renderTree();
this._updateResult();
}
/**
* อัพเดตการตั้งค่า
* @param {Object} config - การตั้งค่าใหม่
*/
updateConfig(config) {
if (config.limitedDepth !== undefined) {
this.limitedDepth = config.limitedDepth;
}
if (config.maxDepth !== undefined) {
this.maxDepth = config.maxDepth;
}
if (config.levelNames !== undefined) {
this.levelNames = config.levelNames;
}
this._renderTree();
}
}