/**
* Drag & Drop Module
* จัดการฟังก์ชันการลากและวางสำหรับคอมโพเนนต์และอิลิเมนต์
*/
(function(global) {
'use strict';
/**
* คลาส DragDrop
* ให้ความสามารถในการลากและวางคอมโพเนนต์และอิลิเมนต์
*/
class DragDrop {
constructor(editor) {
this.editor = editor;
this.draggedElement = null;
this.draggedComponent = null;
this.dropIndicator = null;
this.dropZones = [];
this.isDragging = false;
this.dragStartX = 0;
this.dragStartY = 0;
}
/**
* เริ่มต้นโมดูลลากและวาง
*/
init() {
this.createDropIndicator();
this.setupEventListeners();
this.identifyDropZones();
if (this.editor.config.debug) {
console.log('DragDrop เริ่มต้นแล้ว');
}
}
/**
* สร้างตัวบ่งชี้การวาง
*/
createDropIndicator() {
this.dropIndicator = this.editor.domUtils.createElement('div', {
'class': 'editor-drop-indicator',
'id': 'editor-drop-indicator'
});
document.body.appendChild(this.dropIndicator);
}
/**
* ตั้งค่า event listeners
*/
setupEventListeners() {
// ฟังเหตุการณ์การเริ่มลากคอมโพเนนต์
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('editor-component-item')) {
this.handleComponentDragStart(e);
} else if (e.target.classList.contains('editor-draggable')) {
this.handleElementDragStart(e);
}
});
// ฟังเหตุการณ์การลาก
document.addEventListener('dragover', (e) => {
if (this.isDragging) {
e.preventDefault();
this.handleDragOver(e);
}
});
// ฟังเหตุการณ์การวาง
document.addEventListener('drop', (e) => {
if (this.isDragging) {
e.preventDefault();
this.handleDrop(e);
}
});
// ฟังเหตุการณ์สิ้นสุดการลาก
document.addEventListener('dragend', (e) => {
if (this.isDragging) {
this.handleDragEnd(e);
}
});
// ฟังเหตุการณ์การเปลี่ยนแปลงโหมดตัวแก้ไข
this.editor.on('editor:mode-changed', (data) => {
if (data.mode === 'edit') {
this.enableDragDrop();
} else {
this.disableDragDrop();
}
});
}
/**
* ระบุโซนการวาง
*/
identifyDropZones() {
// ระบุโซนการวางทั้งหมดในคอนเทนเนอร์ตัวแก้ไข
this.dropZones = Array.from(this.editor.container.querySelectorAll('.editor-drop-zone, .editor-component, section, div'));
// เพิ่มคลาสและแอตทริบิวต์ให้กับโซนการวาง
this.dropZones.forEach(zone => {
if (!zone.classList.contains('editor-no-drop')) {
zone.classList.add('editor-drop-zone');
}
});
}
/**
* เปิดใช้งานการลากและวาง
*/
enableDragDrop() {
// ทำเครื่องหมายอิลิเมนต์ที่ลากได้
const draggableElements = this.editor.container.querySelectorAll('.editor-component, section, div');
draggableElements.forEach(element => {
if (!element.classList.contains('editor-no-drag')) {
element.classList.add('editor-draggable');
element.draggable = true;
}
});
// ทำเครื่องหมายรายการคอมโพเนนต์ที่ลากได้
const componentItems = document.querySelectorAll('.editor-component-item');
componentItems.forEach(item => {
item.draggable = true;
});
}
/**
* ปิดใช้งานการลากและวาง
*/
disableDragDrop() {
// ลบคลาสและแอตทริบิวต์จากอิลิเมนต์ที่ลากได้
const draggableElements = this.editor.container.querySelectorAll('.editor-draggable');
draggableElements.forEach(element => {
element.classList.remove('editor-draggable');
element.draggable = false;
});
// ลบแอตทริบิวต์จากรายการคอมโพเนนต์
const componentItems = document.querySelectorAll('.editor-component-item');
componentItems.forEach(item => {
item.draggable = false;
});
}
/**
* จัดการการเริ่มลากคอมโพเนนต์
*/
handleComponentDragStart(e) {
const componentType = e.target.getAttribute('data-component');
if (!componentType) return;
this.isDragging = true;
this.draggedComponent = componentType;
this.draggedElement = null;
// จัดเก็บตำแหน่งเริ่มต้น
this.dragStartX = e.clientX;
this.dragStartY = e.clientY;
// ตั้งค่าข้อมูลการลาก
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('text/html', e.target.innerHTML);
// เพิ่มคลาสการลาก
e.target.classList.add('editor-dragging');
// ส่งเหตุการณ์
this.editor.emit('dragdrop:component-drag-start', {
component: componentType,
element: e.target
});
}
/**
* จัดการการเริ่มลากอิลิเมนต์
*/
handleElementDragStart(e) {
this.isDragging = true;
this.draggedElement = e.target;
this.draggedComponent = null;
// จัดเก็บตำแหน่งเริ่มต้น
this.dragStartX = e.clientX;
this.dragStartY = e.clientY;
// ตั้งค่าข้อมูลการลาก
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.outerHTML);
// เพิ่มคลาสการลาก
e.target.classList.add('editor-dragging');
// ส่งเหตุการณ์
this.editor.emit('dragdrop:element-drag-start', {
element: e.target
});
}
/**
* จัดการการลาก
*/
handleDragOver(e) {
// หาโซนการวางที่ใกล้ที่สุด
const dropZone = this.findClosestDropZone(e.target);
if (dropZone) {
// คำนวณตำแหน่งการวาง
const position = this.calculateDropPosition(dropZone, e.clientX, e.clientY);
// แสดงตัวบ่งชี้การวาง
this.showDropIndicator(dropZone, position);
// อัปเดตคลาสโซนการวาง
this.dropZones.forEach(zone => {
zone.classList.remove('editor-drop-over');
});
dropZone.classList.add('editor-drop-over');
}
}
/**
* จัดการการวาง
*/
handleDrop(e) {
// หาโซนการวางที่ใกล้ที่สุด
const dropZone = this.findClosestDropZone(e.target);
if (!dropZone) return;
// คำนวณตำแหน่งการวาง
const position = this.calculateDropPosition(dropZone, e.clientX, e.clientY);
// วางอิลิเมนต์หรือคอมโพเนนต์
if (this.draggedComponent) {
this.dropComponent(dropZone, position);
} else if (this.draggedElement) {
this.dropElement(dropZone, position);
}
// ส่งเหตุการณ์
this.editor.emit('dragdrop:dropped', {
dropZone,
position,
component: this.draggedComponent,
element: this.draggedElement
});
}
/**
* จัดการสิ้นสุดการลาก
*/
handleDragEnd(e) {
// ลบคลาสการลาก
const draggingElements = document.querySelectorAll('.editor-dragging');
draggingElements.forEach(element => {
element.classList.remove('editor-dragging');
});
// ลบคลาสโซนการวาง
this.dropZones.forEach(zone => {
zone.classList.remove('editor-drop-over');
});
// ซ่อนตัวบ่งชี้การวาง
this.hideDropIndicator();
// รีเซ็ตสถานะ
this.isDragging = false;
this.draggedElement = null;
this.draggedComponent = null;
// ส่งเหตุการณ์
this.editor.emit('dragdrop:drag-end');
}
/**
* หาโซนการวางที่ใกล้ที่สุด
*/
findClosestDropZone(element) {
// ถ้าอิลิเมนต์เป็นโซนการวาง ให้คืนค่าอิลิเมนต์นั้น
if (element.classList.contains('editor-drop-zone')) {
return element;
}
// ค้นหาพาเรนต์ที่เป็นโซนการวาง
let parent = element.parentElement;
while (parent) {
if (parent.classList.contains('editor-drop-zone')) {
return parent;
}
parent = parent.parentElement;
}
// ถ้าไม่พบ ให้คืนค่าคอนเทนเนอร์ตัวแก้ไข
return this.editor.container;
}
/**
* คำนวณตำแหน่งการวาง
*/
calculateDropPosition(dropZone, x, y) {
const rect = dropZone.getBoundingClientRect();
// ตรวจสอบว่าเป็นการวางแนวนอนหรือแนวตั้ง
const isHorizontal = this.isHorizontalLayout(dropZone);
if (isHorizontal) {
// คำนวณตำแหน่งแนวนอน
const midPoint = rect.left + rect.width / 2;
return x < midPoint ? 'before' : 'after';
} else {
// คำนวณตำแหน่งแนวตั้ง
const midPoint = rect.top + rect.height / 2;
return y < midPoint ? 'before' : 'after';
}
}
/**
* ตรวจสอบว่าเป็นเลย์เอาต์แนวนอนหรือไม่
*/
isHorizontalLayout(element) {
const style = window.getComputedStyle(element);
// ตรวจสอบคุณสมบัติ flex
if (style.display === 'flex') {
return style.flexDirection === 'row' || style.flexDirection === 'row-reverse';
}
// ตรวจสอบคุณสมบัติ grid
if (style.display === 'grid') {
return parseInt(style.gridAutoFlow) === 1 || style.gridAutoFlow === 'column';
}
// ตรวจสอบจำนวนอิลิเมนต์ลูกและความกว้าง
const children = Array.from(element.children);
if (children.length > 0) {
const firstChildRect = children[0].getBoundingClientRect();
const secondChildRect = children[1] ? children[1].getBoundingClientRect() : null;
if (secondChildRect) {
// ถ้าอิลิเมนต์ลูกถัดไปอยู่ทางขวา ให้ถือว่าเป็นแนวนอน
return secondChildRect.left > firstChildRect.right;
}
}
// ค่าเริ่มต้น: แนวตั้ง
return false;
}
/**
* แสดงตัวบ่งชี้การวาง
*/
showDropIndicator(dropZone, position) {
const rect = dropZone.getBoundingClientRect();
// ตั้งค่าคลาสตามตำแหน่ง
this.dropIndicator.className = 'editor-drop-indicator';
if (position === 'before') {
if (this.isHorizontalLayout(dropZone)) {
this.dropIndicator.classList.add('horizontal');
this.dropIndicator.style.top = `${rect.top + window.scrollY}px`;
this.dropIndicator.style.left = `${rect.left + window.scrollX}px`;
this.dropIndicator.style.width = '3px';
this.dropIndicator.style.height = `${rect.height}px`;
} else {
this.dropIndicator.classList.add('horizontal');
this.dropIndicator.style.top = `${rect.top + window.scrollY}px`;
this.dropIndicator.style.left = `${rect.left + window.scrollX}px`;
this.dropIndicator.style.width = `${rect.width}px`;
this.dropIndicator.style.height = '3px';
}
} else {
if (this.isHorizontalLayout(dropZone)) {
this.dropIndicator.classList.add('horizontal');
this.dropIndicator.style.top = `${rect.top + window.scrollY}px`;
this.dropIndicator.style.left = `${rect.right + window.scrollX - 3}px`;
this.dropIndicator.style.width = '3px';
this.dropIndicator.style.height = `${rect.height}px`;
} else {
this.dropIndicator.classList.add('horizontal');
this.dropIndicator.style.top = `${rect.bottom + window.scrollY - 3}px`;
this.dropIndicator.style.left = `${rect.left + window.scrollX}px`;
this.dropIndicator.style.width = `${rect.width}px`;
this.dropIndicator.style.height = '3px';
}
}
// แสดงตัวบ่งชี้
this.dropIndicator.style.display = 'block';
}
/**
* ซ่อนตัวบ่งชี้การวาง
*/
hideDropIndicator() {
this.dropIndicator.style.display = 'none';
}
/**
* วางคอมโพเนนต์
*/
dropComponent(dropZone, position) {
// รับเทมเพลตคอมโพเนนต์จากปลั๊กอิน
let componentHTML = '';
if (this.editor.plugins[this.draggedComponent]) {
componentHTML = this.editor.plugins[this.draggedComponent].getTemplate();
} else {
// ใช้เทมเพลตเริ่มต้น
componentHTML = this.getDefaultComponentTemplate(this.draggedComponent);
}
// สร้างอิลิเมนต์จาก HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = componentHTML;
const componentElement = tempDiv.firstChild;
// เพิ่มคลาสและแอตทริบิวต์
componentElement.classList.add('editor-component');
componentElement.setAttribute('data-component', this.draggedComponent);
componentElement.classList.add('editor-draggable');
componentElement.draggable = true;
// ทำเครื่องหมายอิลิเมนต์ย่อยที่แก้ไขได้
this.markEditableElements(componentElement);
// แทรกอิลิเมนต์ในตำแหน่งที่เหมาะสม
if (position === 'before') {
dropZone.parentNode.insertBefore(componentElement, dropZone);
} else {
dropZone.parentNode.insertBefore(componentElement, dropZone.nextSibling);
}
// อัปเดตโซนการวาง
this.identifyDropZones();
// ส่งเหตุการณ์
this.editor.emit('dragdrop:component-dropped', {
component: this.draggedComponent,
element: componentElement,
dropZone,
position
});
}
/**
* วางอิลิเมนต์
*/
dropElement(dropZone, position) {
// ตรวจสอบว่าไม่ได้วางในตัวเอง
if (dropZone === this.draggedElement || dropZone.contains(this.draggedElement)) {
return;
}
// แทรกอิลิเมนต์ในตำแหน่งที่เหมาะสม
if (position === 'before') {
dropZone.parentNode.insertBefore(this.draggedElement, dropZone);
} else {
dropZone.parentNode.insertBefore(this.draggedElement, dropZone.nextSibling);
}
// อัปเดตโซนการวาง
this.identifyDropZones();
// ส่งเหตุการณ์
this.editor.emit('dragdrop:element-dropped', {
element: this.draggedElement,
dropZone,
position
});
}
/**
* รับเทมเพลตคอมโพเนนต์เริ่มต้น
*/
getDefaultComponentTemplate(componentType) {
switch (componentType) {
case 'header':
return `
<header class="editor-component" data-component="header">
<div class="container">
<h1>ส่วนหัวเว็บไซต์</h1>
<nav>
<ul>
<li><a href="#">หน้าแรก</a></li>
<li><a href="#">เกี่ยวกับ</a></li>
<li><a href="#">ติดต่อ</a></li>
</ul>
</nav>
</div>
</header>
`;
case 'footer':
return `
<footer class="editor-component" data-component="footer">
<div class="container">
<p>© 2023 เว็บไซต์ของฉัน. สงวนลิขสิทธิ์</p>
</div>
</footer>
`;
case 'hero':
return `
<section class="editor-component hero" data-component="hero">
<div class="container">
<h1>ยินดีต้อนรับสู่เว็บไซต์ของเรา</h1>
<p>นี่คือส่วนแสดงผลหลักของเว็บไซต์</p>
<button class="btn btn-primary">เรียนรู้เพิ่มเติม</button>
</div>
</section>
`;
case 'carousel':
return `
<section class="editor-component carousel" data-component="carousel">
<div class="container">
<div class="carousel-container">
<div class="carousel-slide active">
<img src="https://picsum.photos/seed/slide1/800/400.jpg" alt="สไลด์ 1">
<div class="carousel-caption">
<h3>สไลด์ที่ 1</h3>
<p>คำอธิบายสไลด์ที่ 1</p>
</div>
</div>
<div class="carousel-slide">
<img src="https://picsum.photos/seed/slide2/800/400.jpg" alt="สไลด์ 2">
<div class="carousel-caption">
<h3>สไลด์ที่ 2</h3>
<p>คำอธิบายสไลด์ที่ 2</p>
</div>
</div>
<div class="carousel-controls">
<button class="carousel-prev"><</button>
<button class="carousel-next">></button>
</div>
</div>
</div>
</section>
`;
case 'card':
return `
<div class="editor-component card" data-component="card">
<img src="https://picsum.photos/seed/card/300/200.jpg" alt="รูปภาพการ์ด">
<div class="card-content">
<h3>หัวข้อการ์ด</h3>
<p>นี่คือเนื้อหาของการ์ด สามารถแก้ไขได้ตามต้องการ</p>
<a href="#" class="btn">อ่านเพิ่มเติม</a>
</div>
</div>
`;
case 'grid':
return `
<div class="editor-component grid" data-component="grid">
<div class="grid-container">
<div class="grid-item">
<h3>รายการ 1</h3>
<p>เนื้อหารายการที่ 1</p>
</div>
<div class="grid-item">
<h3>รายการ 2</h3>
<p>เนื้อหารายการที่ 2</p>
</div>
<div class="grid-item">
<h3>รายการ 3</h3>
<p>เนื้อหารายการที่ 3</p>
</div>
</div>
</div>
`;
default:
return `
<div class="editor-component" data-component="${componentType}">
<p>คอมโพเนนต์: ${componentType}</p>
</div>
`;
}
}
/**
* ทำเครื่องหมายอิลิเมนต์ที่แก้ไขได้
*/
markEditableElements(element) {
const textElements = element.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div, li, td, th, a, button');
textElements.forEach(el => {
// ข้ามอิลิเมนต์ที่มีคลาสหรือแอตทริบิวต์เฉพาะ
if (el.classList.contains('editor-no-edit') ||
el.hasAttribute('data-no-edit') ||
el.closest('.editor-no-edit')) {
return;
}
// ทำเครื่องหมายว่าแก้ไขได้
el.setAttribute('data-editable', 'true');
});
}
}
// เปิดเผยคลาส DragDrop ทั่วโลก
global.DragDrop = DragDrop;
})(typeof window !== 'undefined' ? window : this);