/** * 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(); } }