contentEditor.js

21.69 KB
12/10/2025 04:25
JS
contentEditor.js
/**
 * Content Editor Module
 * จัดการการแก้ไขเนื้อหาแบบอินไลน์ด้วยแถบเครื่องมือลอย
 */
(function(global) {
  'use strict';

  /**
   * คลาส ContentEditor
   * ให้ความสามารถในการแก้ไขเนื้อหาแบบอินไลน์สำหรับอิลิเมนต์ข้อความ
   */
  class ContentEditor {
    constructor(editor) {
      this.editor = editor;
      this.isEditing = false;
      this.currentElement = null;
      this.textToolbar = null;
      this.history = [];
      this.historyIndex = -1;
      this.maxHistorySize = 50;
      this.autoSaveTimer = null;
    }

    /**
     * เริ่มต้นโมดูลตัวแก้ไขเนื้อหา
     */
    init() {
      this.createTextToolbar();
      this.setupEventListeners();

      // ทำเครื่องหมายอิลิเมนต์ที่แก้ไขได้
      this.markEditableElements();

      if (this.editor.config.debug) {
        console.log('ContentEditor เริ่มต้นแล้ว');
      }
    }

    /**
     * ทำเครื่องหมายอิลิเมนต์ที่แก้ไขได้ในคอนเทนเนอร์
     */
    markEditableElements() {
      const textElements = this.editor.container.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div, li, td, th, a, button');

      textElements.forEach(element => {
        // ข้ามอิลิเมนต์ที่มีคลาสหรือแอตทริบิวต์เฉพาะ
        if (element.classList.contains('editor-no-edit') ||
          element.hasAttribute('data-no-edit') ||
          element.closest('.editor-no-edit')) {
          return;
        }

        // ทำเครื่องหมายว่าแก้ไขได้
        element.setAttribute('data-editable', 'true');
      });
    }

    /**
     * ตั้งค่า event listeners
     */
    setupEventListeners() {
      // ฟังเหตุการณ์การคลิกอิลิเมนต์
      this.editor.container.addEventListener('click', (e) => {
        if (this.editor.state !== 'edit') return;

        const editableElement = e.target.closest('[data-editable="true"]');
        if (editableElement) {
          e.preventDefault();
          this.enableEditing(editableElement);
        }
      });

      // ฟังเหตุการณ์การเปลี่ยนแปลงโหมดตัวแก้ไข
      this.editor.on('editor:mode-changed', (data) => {
        if (data.mode === 'preview' && this.isEditing) {
          this.disableEditing();
        }
      });

      // ฟังเหตุการณ์คำสั่งจากแถบเครื่องมือ
      this.editor.on('content:command', (data) => {
        if (this.isEditing) {
          this.executeCommand(data.command, data.value);
        }
      });

      // ฟังเหตุการณ์การเลิกทำ/ทำซ้ำ
      this.editor.on('toolbar:undo', () => {
        this.undo();
      });

      this.editor.on('toolbar:redo', () => {
        this.redo();
      });

      // ฟังเหตุการณ์การเปลี่ยนแปลงเนื้อหา
      this.editor.on('content:changed', () => {
        this.startAutoSave();
      });
    }

    /**
     * สร้างแถบเครื่องมือข้อความลอย
     */
    createTextToolbar() {
      this.textToolbar = this.editor.domUtils.createElement('div', {
        'class': 'editor-text-toolbar',
        'id': 'editor-text-toolbar'
      });

      // สร้างกลุ่มแถบเครื่องมือ
      const formattingGroup = this.createToolbarGroup('formatting', [
        {command: 'bold', icon: 'format_bold', title: 'ตัวหนา'},
        {command: 'italic', icon: 'format_italic', title: 'ตัวเอียง'},
        {command: 'underline', icon: 'format_underlined', title: 'ขีดเส้นใต้'},
        {command: 'strikeThrough', icon: 'format_strikethrough', title: 'ขีดฆ่า'}
      ]);

      const alignmentGroup = this.createToolbarGroup('alignment', [
        {command: 'justifyLeft', icon: 'format_align_left', title: 'จัดชิดซ้าย'},
        {command: 'justifyCenter', icon: 'format_align_center', title: 'จัดกึ่งกลาง'},
        {command: 'justifyRight', icon: 'format_align_right', title: 'จัดชิดขวา'},
        {command: 'justifyFull', icon: 'format_align_justify', title: 'เต็มแถว'}
      ]);

      const listGroup = this.createToolbarGroup('list', [
        {command: 'insertUnorderedList', icon: 'format_list_bulleted', title: 'รายการแบบจุด'},
        {command: 'insertOrderedList', icon: 'format_list_numbered', title: 'รายการแบบลำดับ'},
        {command: 'outdent', icon: 'format_indent_decrease', title: 'ลดระยะย่อหน้า'},
        {command: 'indent', icon: 'format_indent_increase', title: 'เพิ่มระยะย่อหน้า'}
      ]);

      const insertGroup = this.createToolbarGroup('insert', [
        {command: 'createLink', icon: 'link', title: 'แทรกลิงก์'},
        {command: 'insertImage', icon: 'image', title: 'แทรกรูปภาพ'},
        {command: 'insertTable', icon: 'grid_on', title: 'แทรกตาราง'}
      ]);

      const styleGroup = this.createToolbarGroup('style', [
        {command: 'removeFormat', icon: 'format_clear', title: 'ล้างรูปแบบ'},
        {command: 'formatBlock', icon: 'text_format', title: 'รูปแบบย่อหน้า', value: 'p'}
      ]);

      // เพิ่มกลุ่มในแถบเครื่องมือ
      this.textToolbar.appendChild(formattingGroup);
      this.textToolbar.appendChild(alignmentGroup);
      this.textToolbar.appendChild(listGroup);
      this.textToolbar.appendChild(insertGroup);
      this.textToolbar.appendChild(styleGroup);

      // เพิ่มปุ่มปิด
      const closeButton = this.editor.domUtils.createElement('button', {
        'class': 'editor-toolbar-close',
        'title': 'ปิด'
      }, '×');

      closeButton.addEventListener('click', () => {
        this.disableEditing();
      });

      this.textToolbar.appendChild(closeButton);

      // เพิ่มในเอกสาร
      document.body.appendChild(this.textToolbar);

      // ซ่อนเริ่มต้น
      this.textToolbar.style.display = 'none';
    }

    /**
     * สร้างกลุ่มแถบเครื่องมือ
     */
    createToolbarGroup(name, buttons) {
      const group = this.editor.domUtils.createElement('div', {
        'class': `editor-toolbar-group editor-toolbar-group-${name}`
      });

      buttons.forEach(button => {
        const btn = this.editor.domUtils.createElement('button', {
          'class': 'editor-toolbar-btn',
          'data-command': button.command,
          'title': button.title
        });

        // เพิ่มไอคอน
        const icon = this.editor.domUtils.createElement('i', {
          'class': 'material-icons'
        }, button.icon);

        btn.appendChild(icon);

        // เพิ่มค่าถ้ามี
        if (button.value) {
          btn.setAttribute('data-value', button.value);
        }

        // เพิ่มเหตุการณ์คลิก
        btn.addEventListener('click', (e) => {
          e.preventDefault();
          const value = btn.getAttribute('data-value');
          this.editor.emit('content:command', {command: button.command, value});
        });

        group.appendChild(btn);
      });

      return group;
    }

    /**
     * เปิดใช้งานการแก้ไขแบบอินไลน์สำหรับอิลิเมนต์
     */
    enableEditing(element) {
      if (this.isEditing && this.currentElement === element) {
        return; // กำลังแก้ไขอิลิเมนต์นี้อยู่แล้ว
      }

      // ปิดการแก้ไขปัจจุบัน
      if (this.isEditing) {
        this.disableEditing();
      }

      // บันทึกสถานะปัจจุบันในประวัติ
      this.saveToHistory();

      // ตั้งค่าอิลิเมนต์ปัจจุบัน
      this.currentElement = element;
      this.isEditing = true;

      // เพิ่มคลาสการแก้ไข
      element.classList.add('editor-editing');

      // ทำให้เนื้อหาแก้ไขได้
      element.contentEditable = true;

      // โฟกัสที่อิลิเมนต์
      element.focus();

      // เลือกข้อความทั้งหมด
      const range = document.createRange();
      range.selectNodeContents(element);
      const selection = window.getSelection();
      selection.removeAllRanges();
      selection.addRange(range);

      // แสดงแถบเครื่องมือข้อความ
      this.showTextToolbar(element);

      // เพิ่ม event listeners สำหรับอิลิเมนต์
      this.addElementEventListeners(element);

      // ส่งเหตุการณ์
      this.editor.emit('content:editing-started', {element});
    }

    /**
     * ปิดใช้งานการแก้ไขแบบอินไลน์
     */
    disableEditing() {
      if (!this.isEditing) return;

      // ลบคลาสการแก้ไข
      this.currentElement.classList.remove('editor-editing');

      // ทำให้เนื้อหาไม่สามารถแก้ไขได้
      this.currentElement.contentEditable = false;

      // ลบ event listeners
      this.removeElementEventListeners(this.currentElement);

      // ซ่อนแถบเครื่องมือข้อความ
      this.hideTextToolbar();

      // รีเซ็ตสถานะ
      this.isEditing = false;
      this.currentElement = null;

      // ส่งเหตุการณ์
      this.editor.emit('content:editing-ended');
    }

    /**
     * เพิ่ม event listeners ให้กับอิลิเมนต์ที่แก้ไขได้
     */
    addElementEventListeners(element) {
      // เก็บการอ้างอิงถึง this สำหรับตัวจัดการเหตุการณ์
      const self = this;

      // เหตุการณ์อินพุต
      element.addEventListener('input', this.elementInputHandler = function() {
        self.editor.emit('content:changed', {element: this});
      });

      // เหตุการณ์คีย์ดาวน์
      element.addEventListener('keydown', this.elementKeydownHandler = function(e) {
        // จัดการการผสมคีย์เฉพาะ
        if (e.ctrlKey || e.metaKey) {
          switch (e.key) {
            case 'b':
              e.preventDefault();
              self.executeCommand('bold');
              break;
            case 'i':
              e.preventDefault();
              self.executeCommand('italic');
              break;
            case 'u':
              e.preventDefault();
              self.executeCommand('underline');
              break;
            case 'z':
              e.preventDefault();
              if (e.shiftKey) {
                self.redo();
              } else {
                self.undo();
              }
              break;
            case 'y':
              e.preventDefault();
              self.redo();
              break;
            case 's':
              e.preventDefault();
              self.editor.saveTemplate();
              break;
          }
        }

        // จัดการปุ่ม Escape
        if (e.key === 'Escape') {
          e.preventDefault();
          self.disableEditing();
        }

        // จัดการปุ่ม Enter
        if (e.key === 'Enter') {
          // ถ้าเป็นอิลิเมนต์ที่ไม่ใช่บล็อก ให้ป้องกันการสร้างบรรทัดใหม่
          if (element.tagName !== 'DIV' && element.tagName !== 'P' && element.tagName !== 'LI') {
            e.preventDefault();
            self.disableEditing();
          }
        }
      });

      // เหตุการณ์คีย์อัพเพื่ออัปเดตตำแหน่งแถบเครื่องมือ
      element.addEventListener('keyup', this.elementKeyupHandler = function() {
        self.updateTextToolbarPosition();
      });

      // เหตุการณ์เมาส์อัพเพื่ออัปเดตตำแหน่งแถบเครื่องมือ
      element.addEventListener('mouseup', this.elementMouseupHandler = function() {
        self.updateTextToolbarPosition();
      });

      // เหตุการณ์คลิกเพื่อป้องกันการแพร่กระจาย
      element.addEventListener('click', this.elementClickHandler = function(e) {
        e.stopPropagation();
      });
    }

    /**
     * ลบ event listeners จากอิลิเมนต์ที่แก้ไขได้
     */
    removeElementEventListeners(element) {
      if (this.elementInputHandler) {
        element.removeEventListener('input', this.elementInputHandler);
        this.elementInputHandler = null;
      }

      if (this.elementKeydownHandler) {
        element.removeEventListener('keydown', this.elementKeydownHandler);
        this.elementKeydownHandler = null;
      }

      if (this.elementKeyupHandler) {
        element.removeEventListener('keyup', this.elementKeyupHandler);
        this.elementKeyupHandler = null;
      }

      if (this.elementMouseupHandler) {
        element.removeEventListener('mouseup', this.elementMouseupHandler);
        this.elementMouseupHandler = null;
      }

      if (this.elementClickHandler) {
        element.removeEventListener('click', this.elementClickHandler);
        this.elementClickHandler = null;
      }
    }

    /**
     * แสดงแถบเครื่องมือข้อความ
     */
    showTextToolbar(element) {
      // อัปเดตตำแหน่งแถบเครื่องมือ
      this.updateTextToolbarPosition();

      // แสดงแถบเครื่องมือ
      this.textToolbar.style.display = 'flex';

      // เพิ่มคลาสที่ใช้งานสำหรับแอนิเมชัน
      setTimeout(() => {
        this.textToolbar.classList.add('editor-toolbar-active');
      }, 10);
    }

    /**
     * ซ่อนแถบเครื่องมือข้อความ
     */
    hideTextToolbar() {
      // ลบคลาสที่ใช้งาน
      this.textToolbar.classList.remove('editor-toolbar-active');

      // ซ่อนหลังแอนิเมชัน
      setTimeout(() => {
        this.textToolbar.style.display = 'none';
      }, 200);
    }

    /**
     * อัปเดตตำแหน่งของแถบเครื่องมือข้อความ
     */
    updateTextToolbarPosition() {
      if (!this.isEditing || !this.currentElement) return;

      const selection = window.getSelection();
      if (selection.rangeCount === 0) return;

      const range = selection.getRangeAt(0);
      const rect = range.getBoundingClientRect();

      // วางแถบเครื่องมือเหนือการเลือก
      let top = rect.top + window.scrollY - this.textToolbar.offsetHeight - 10;
      let left = rect.left + window.scrollX + (rect.width / 2) - (this.textToolbar.offsetWidth / 2);

      // ตรวจสอบให้แน่ใจว่าแถบเครื่องมืออยู่ในวิวพอร์ต
      if (top < window.scrollY + 10) {
        top = rect.bottom + window.scrollY + 10;
      }

      if (left < window.scrollX + 10) {
        left = window.scrollX + 10;
      }

      if (left + this.textToolbar.offsetWidth > window.scrollX + window.innerWidth - 10) {
        left = window.scrollX + window.innerWidth - this.textToolbar.offsetWidth - 10;
      }

      // ใช้ตำแหน่ง
      this.textToolbar.style.top = `${top}px`;
      this.textToolbar.style.left = `${left}px`;
    }

    /**
     * ดำเนินการคำสั่งเนื้อหา
     */
    executeCommand(command, value = null) {
      if (!this.isEditing) return;

      // จัดการคำสั่งพิเศษ
      switch (command) {
        case 'createLink':
          const url = prompt('ป้อน URL:');
          if (url) {
            document.execCommand(command, false, url);
          }
          break;

        case 'insertImage':
          const imageUrl = prompt('ป้อน URL รูปภาพ:');
          if (imageUrl) {
            document.execCommand(command, false, imageUrl);
          }
          break;

        case 'insertTable':
          const rows = prompt('จำนวนแถว:', '2');
          const cols = prompt('จำนวนคอลัมน์:', '2');
          if (rows && cols) {
            this.insertTable(parseInt(rows), parseInt(cols));
          }
          break;

        case 'formatBlock':
          if (value) {
            document.execCommand(command, false, value);
          }
          break;

        default:
          document.execCommand(command, false, value);
      }

      // โฟกัสกลับไปที่อิลิเมนต์
      this.currentElement.focus();

      // ส่งเหตุการณ์คำสั่งที่ดำเนินการ
      this.editor.emit('content:command-executed', {command, value});
    }

    /**
     * แทรกตาราง
     */
    insertTable(rows, cols) {
      if (!this.isEditing) return;

      let tableHTML = '<table border="1" style="border-collapse: collapse; width: 100%;">';

      for (let i = 0; i < rows; i++) {
        tableHTML += '<tr>';
        for (let j = 0; j < cols; j++) {
          tableHTML += '<td contenteditable="true">&nbsp;</td>';
        }
        tableHTML += '</tr>';
      }

      tableHTML += '</table>';

      document.execCommand('insertHTML', false, tableHTML);
    }

    /**
     * บันทึกสถานะปัจจุบันในประวัติ
     */
    saveToHistory() {
      if (!this.currentElement) return;

      // สร้างสแนปชอตของอิลิเมนต์ปัจจุบัน
      const snapshot = {
        element: this.currentElement,
        html: this.currentElement.innerHTML,
        timestamp: Date.now()
      };

      // ลบสถานะใดๆ หลังดัชนีปัจจุบัน
      this.history = this.history.slice(0, this.historyIndex + 1);

      // เพิ่มสแนปชอตใหม่
      this.history.push(snapshot);
      this.historyIndex++;

      // จำกัดขนาดประวัติ
      if (this.history.length > this.maxHistorySize) {
        this.history.shift();
        this.historyIndex--;
      }
    }

    /**
     * เลิกทำการเปลี่ยนแปลงล่าสุด
     */
    undo() {
      if (this.historyIndex <= 0) return;

      this.historyIndex--;
      this.restoreFromHistory(this.history[this.historyIndex]);

      // ส่งเหตุการณ์เลิกทำ
      this.editor.emit('content:undo');
    }

    /**
     * ทำซ้ำการเปลี่ยนแปลงที่เลิกทำ
     */
    redo() {
      if (this.historyIndex >= this.history.length - 1) return;

      this.historyIndex++;
      this.restoreFromHistory(this.history[this.historyIndex]);

      // ส่งเหตุการณ์ทำซ้ำ
      this.editor.emit('content:redo');
    }

    /**
     * กู้คืนอิลิเมนต์จากสแนปชอตประวัติ
     */
    restoreFromHistory(snapshot) {
      if (!snapshot || !snapshot.element) return;

      // กู้คืน HTML
      snapshot.element.innerHTML = snapshot.html;

      // ถ้าเป็นอิลิเมนต์ปัจจุบัน ให้โฟกัส
      if (snapshot.element === this.currentElement) {
        snapshot.element.focus();
      }

      // ส่งเหตุการณ์กู้คืน
      this.editor.emit('content:restored', {element: snapshot.element});
    }

    /**
     * เริ่มต้นการบันทึกอัตโนมัติ
     */
    startAutoSave() {
      // ล้างตัวจับเวลาที่มีอยู่
      if (this.autoSaveTimer) {
        clearTimeout(this.autoSaveTimer);
      }

      // ตั้งตัวจับเวลาใหม่
      this.autoSaveTimer = setTimeout(() => {
        if (this.editor.config.autoSave) {
          this.editor.saveTemplate();
        }
      }, 2000); // บันทึกอัตโนมัติหลังจาก 2 วินาทีของการเปลี่ยนแปลง
    }
  }

  // เปิดเผยคลาส ContentEditor ทั่วโลก
  global.ContentEditor = ContentEditor;

})(typeof window !== 'undefined' ? window : this);