dragDrop.js

23.40 KB
12/10/2025 04:24
JS
dragDrop.js
/**
 * 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>&copy; 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">&lt;</button>
                                        <button class="carousel-next">&gt;</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);