domUtils.js

21.82 KB
12/10/2025 04:51
JS
domUtils.js
/**
 * DOM Utils - เครื่องมือสำหรับจัดการ DOM และอิลิเมนต์ต่างๆ
 */
(function(global) {
  'use strict';

  /**
   * คลาส DOMUtils
   * เครื่องมือสำหรับจัดการ DOM และอิลิเมนต์ต่างๆ
   */
  class DOMUtils {
    constructor(editor) {
      this.editor = editor;
    }

    /**
     * สร้างอิลิเมนต์
     * @param {string} tag - ชื่อแท็ก
     * @param {Object} attributes - แอตทริบิวต์ (optional)
     * @param {string} content - เนื้อหา (optional)
     * @returns {HTMLElement} อิลิเมนต์ที่สร้าง
     */
    createElement(tag, attributes = {}, content = '') {
      const element = document.createElement(tag);

      // เพิ่มแอตทริบิวต์
      Object.keys(attributes).forEach(key => {
        if (key === 'className') {
          element.className = attributes[key];
        } else if (key === 'innerHTML') {
          element.innerHTML = attributes[key];
        } else if (key === 'textContent') {
          element.textContent = attributes[key];
        } else {
          element.setAttribute(key, attributes[key]);
        }
      });

      // เพิ่มเนื้อหา
      if (content) {
        element.innerHTML = content;
      }

      return element;
    }

    /**
     * ค้นหาอิลิเมนต์
     * @param {string} selector - ตัวเลือก CSS
     * @param {HTMLElement} parent - อิลิเมนต์พาเรนต์ (optional)
     * @returns {HTMLElement} อิลิเมนต์แรกที่พบ
     */
    find(selector, parent = document) {
      return parent.querySelector(selector);
    }

    /**
     * ค้นหาอิลิเมนต์ทั้งหมด
     * @param {string} selector - ตัวเลือก CSS
     * @param {HTMLElement} parent - อิลิเมนต์พาเรนต์ (optional)
     * @returns {NodeList} อิลิเมนต์ทั้งหมดที่พบ
     */
    findAll(selector, parent = document) {
      return parent.querySelectorAll(selector);
    }

    /**
     * หาพาเรนต์ที่ตรงกับตัวเลือก
     * @param {HTMLElement} element - อิลิเมนต์ที่ต้องการหาพาเรนต์
     * @param {string} selector - ตัวเลือก CSS
     * @returns {HTMLElement} พาเรนต์ที่ตรงกับตัวเลือก
     */
    findParent(element, selector) {
      let parent = element.parentElement;

      while (parent) {
        if (parent.matches(selector)) {
          return parent;
        }
        parent = parent.parentElement;
      }

      return null;
    }

    /**
     * หาพาเรนต์ที่มีคลาสที่กำหนด
     * @param {HTMLElement} element - อิลิเมนต์ที่ต้องการหาพาเรนต์
     * @param {string} className - ชื่อคลาส
     * @returns {HTMLElement} พาเรนต์ที่มีคลาสที่กำหนด
     */
    findParentByClass(element, className) {
      let parent = element.parentElement;

      while (parent) {
        if (parent.classList.contains(className)) {
          return parent;
        }
        parent = parent.parentElement;
      }

      return null;
    }

    /**
     * หาพาเรนต์ที่มีแอตทริบิวต์ที่กำหนด
     * @param {HTMLElement} element - อิลิเมนต์ที่ต้องการหาพาเรนต์
     * @param {string} attribute - ชื่อแอตทริบิวต์
     * @param {string} value - ค่าของแอตทริบิวต์ (optional)
     * @returns {HTMLElement} พาเรนต์ที่มีแอตทริบิวต์ที่กำหนด
     */
    findParentByAttribute(element, attribute, value = null) {
      let parent = element.parentElement;

      while (parent) {
        if (parent.hasAttribute(attribute)) {
          if (value === null || parent.getAttribute(attribute) === value) {
            return parent;
          }
        }
        parent = parent.parentElement;
      }

      return null;
    }

    /**
     * เพิ่มคลาสให้อิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {string} className - ชื่อคลาส
     */
    addClass(element, className) {
      if (element) {
        element.classList.add(className);
      }
    }

    /**
     * ลบคลาสจากอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {string} className - ชื่อคลาส
     */
    removeClass(element, className) {
      if (element) {
        element.classList.remove(className);
      }
    }

    /**
     * สลับคลาสของอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {string} className - ชื่อคลาส
     */
    toggleClass(element, className) {
      if (element) {
        element.classList.toggle(className);
      }
    }

    /**
     * ตรวจสอบว่าอิลิเมนต์มีคลาสที่กำหนดหรือไม่
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {string} className - ชื่อคลาส
     * @returns {boolean} มีคลาสที่กำหนดหรือไม่
     */
    hasClass(element, className) {
      return element ? element.classList.contains(className) : false;
    }

    /**
     * ตั้งค่าสไตล์ของอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {Object} styles - ออบเจกต์สไตล์
     */
    setStyles(element, styles) {
      if (!element) return;

      Object.keys(styles).forEach(property => {
        element.style[property] = styles[property];
      });
    }

    /**
     * รับค่าสไตล์ของอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {string} property - ชื่อคุณสมบัติสไตล์
     * @returns {string} ค่าของคุณสมบัติสไตล์
     */
    getStyle(element, property) {
      if (!element) return '';

      return window.getComputedStyle(element).getPropertyValue(property);
    }

    /**
     * ตั้งค่าแอตทริบิวต์ของอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {string} attribute - ชื่อแอตทริบิวต์
     * @param {string} value - ค่าของแอตทริบิวต์
     */
    setAttribute(element, attribute, value) {
      if (element) {
        element.setAttribute(attribute, value);
      }
    }

    /**
     * รับค่าแอตทริบิวต์ของอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {string} attribute - ชื่อแอตทริบิวต์
     * @returns {string} ค่าของแอตทริบิวต์
     */
    getAttribute(element, attribute) {
      return element ? element.getAttribute(attribute) : null;
    }

    /**
     * ลบแอตทริบิวต์ของอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์
     * @param {string} attribute - ชื่อแอตทริบิวต์
     */
    removeAttribute(element, attribute) {
      if (element) {
        element.removeAttribute(attribute);
      }
    }

    /**
     * แทรกอิลิเมนต์หลังจากอิลิเมนต์ที่กำหนด
     * @param {HTMLElement} element - อิลิเมนต์ที่จะแทรก
     * @param {HTMLElement} referenceElement - อิลิเมนต์อ้างอิง
     */
    insertAfter(element, referenceElement) {
      if (element && referenceElement && referenceElement.parentNode) {
        referenceElement.parentNode.insertBefore(element, referenceElement.nextSibling);
      }
    }

    /**
     * แทรกอิลิเมนต์ก่อนอิลิเมนต์ที่กำหนด
     * @param {HTMLElement} element - อิลิเมนต์ที่จะแทรก
     * @param {HTMLElement} referenceElement - อิลิเมนต์อ้างอิง
     */
    insertBefore(element, referenceElement) {
      if (element && referenceElement && referenceElement.parentNode) {
        referenceElement.parentNode.insertBefore(element, referenceElement);
      }
    }

    /**
     * ลบอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์ที่จะลบ
     */
    removeElement(element) {
      if (element && element.parentNode) {
        element.parentNode.removeChild(element);
      }
    }

    /**
     * ลบอิลิเมนต์ทั้งหมดที่ตรงกับตัวเลือก
     * @param {string} selector - ตัวเลือก CSS
     * @param {HTMLElement} parent - อิลิเมนต์พาเรนต์ (optional)
     */
    removeAll(selector, parent = document) {
      const elements = this.findAll(selector, parent);

      elements.forEach(element => {
        this.removeElement(element);
      });
    }

    /**
     * ล้างเนื้อหาของอิลิเมนต์
     * @param {HTMLElement} element - อิลิเมนต์ที่จะล้าง
     */
    empty(element) {
      if (element) {
        element.innerHTML = '';
      }
    }

    /**
     * ทำให้อิลิเมนต์ลอย
     * @param {HTMLElement} element - อิลิเมนต์ที่จะทำให้ลอย
     * @param {Object} options - ตัวเลือก (optional)
     */
    makeDraggable(element, options = {}) {
      if (!element) return;

      const defaults = {
        handle: null,
        containment: null,
        zIndex: 1000
      };

      const settings = Object.assign({}, defaults, options);

      let isDragging = false;
      let startX, startY, initialX, initialY;

      // หาอิลิเมนต์ที่ใช้จับ
      const handle = settings.handle ? this.find(settings.handle, element) : element;

      if (!handle) return;

      // เพิ่มคลาส
      element.classList.add('editor-draggable');

      // เพิ่ม event listeners
      handle.addEventListener('mousedown', startDrag);

      function startDrag(e) {
        isDragging = true;

        // ตำแหน่งเริ่มต้น
        startX = e.clientX;
        startY = e.clientY;

        // ตำแหน่งเริ่มต้นของอิลิเมนต์
        const rect = element.getBoundingClientRect();
        initialX = rect.left;
        initialY = rect.top;

        // ตั้งค่า z-index
        element.style.zIndex = settings.zIndex;

        // เพิ่ม event listeners
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', stopDrag);

        // ป้องกันการเลือกข้อความ
        e.preventDefault();
      }

      function drag(e) {
        if (!isDragging) return;

        // คำนวณตำแหน่งใหม่
        const deltaX = e.clientX - startX;
        const deltaY = e.clientY - startY;

        let newX = initialX + deltaX;
        let newY = initialY + deltaY;

        // ตรวจสอบขอบเขต
        if (settings.containment) {
          const container = typeof settings.containment === 'string'
            ? document.querySelector(settings.containment)
            : settings.containment;

          if (container) {
            const containerRect = container.getBoundingClientRect();
            const elementRect = element.getBoundingClientRect();

            // ขอบซ้าย
            if (newX < containerRect.left) {
              newX = containerRect.left;
            }

            // ขอบขวา
            if (newX + elementRect.width > containerRect.right) {
              newX = containerRect.right - elementRect.width;
            }

            // ขอบบน
            if (newY < containerRect.top) {
              newY = containerRect.top;
            }

            // ขอบล่าง
            if (newY + elementRect.height > containerRect.bottom) {
              newY = containerRect.bottom - elementRect.height;
            }
          }
        }

        // ตั้งค่าตำแหน่ง
        element.style.position = 'absolute';
        element.style.left = `${newX}px`;
        element.style.top = `${newY}px`;
      }

      function stopDrag() {
        isDragging = false;

        // ลบ event listeners
        document.removeEventListener('mousemove', drag);
        document.removeEventListener('mouseup', stopDrag);
      }
    }

    /**
     * ทำให้อิลิเมนต์สามารถปรับขนาดได้
     * @param {HTMLElement} element - อิลิเมนต์ที่จะทำให้ปรับขนาดได้
     * @param {Object} options - ตัวเลือก (optional)
     */
    makeResizable(element, options = {}) {
      if (!element) return;

      const defaults = {
        handles: 'e, s, se',
        minWidth: 50,
        minHeight: 50,
        maxWidth: null,
        maxHeight: null
      };

      const settings = Object.assign({}, defaults, options);

      // สร้าง handles
      const handles = settings.handles.split(',').map(handle => handle.trim());

      handles.forEach(handle => {
        const handleElement = this.createElement('div', {
          className: `editor-resize-handle editor-resize-${handle}`
        });

        element.appendChild(handleElement);

        // เพิ่ม event listeners
        handleElement.addEventListener('mousedown', startResize);
      });

      // เพิ่มคลาส
      element.classList.add('editor-resizable');

      function startResize(e) {
        const handle = e.target.className.match(/editor-resize-(\w+)/)[1];

        let startX = e.clientX;
        let startY = e.clientY;
        let startWidth = element.offsetWidth;
        let startHeight = element.offsetHeight;

        function resize(e) {
          const deltaX = e.clientX - startX;
          const deltaY = e.clientY - startY;

          let newWidth = startWidth;
          let newHeight = startHeight;

          // ปรับขนาดตาม handle
          if (handle.includes('e')) {
            newWidth = startWidth + deltaX;
          }

          if (handle.includes('w')) {
            newWidth = startWidth - deltaX;
          }

          if (handle.includes('s')) {
            newHeight = startHeight + deltaY;
          }

          if (handle.includes('n')) {
            newHeight = startHeight - deltaY;
          }

          // จำกัดขนาด
          newWidth = Math.max(settings.minWidth, newWidth);
          newHeight = Math.max(settings.minHeight, newHeight);

          if (settings.maxWidth) {
            newWidth = Math.min(settings.maxWidth, newWidth);
          }

          if (settings.maxHeight) {
            newHeight = Math.min(settings.maxHeight, newHeight);
          }

          // ตั้งค่าขนาด
          element.style.width = `${newWidth}px`;
          element.style.height = `${newHeight}px`;

          // ปรับตำแหน่งถ้าจากด้านบนหรือด้านซ้าย
          if (handle.includes('w')) {
            const deltaX = newWidth - startWidth;
            element.style.left = `${element.offsetLeft - deltaX}px`;
          }

          if (handle.includes('n')) {
            const deltaY = newHeight - startHeight;
            element.style.top = `${element.offsetTop - deltaY}px`;
          }
        }

        function stopResize() {
          document.removeEventListener('mousemove', resize);
          document.removeEventListener('mouseup', stopResize);
        }

        document.addEventListener('mousemove', resize);
        document.addEventListener('mouseup', stopResize);

        e.preventDefault();
      }
    }

    /**
     * ทำให้อิลิเมนต์สามารถเลือกได้
     * @param {HTMLElement} element - อิลิเมนต์ที่จะทำให้เลือกได้
     * @param {Object} options - ตัวเลือก (optional)
     */
    makeSelectable(element, options = {}) {
      if (!element) return;

      const defaults = {
        multiple: false,
        className: 'editor-selected'
      };

      const settings = Object.assign({}, defaults, options);

      // เพิ่มคลาส
      element.classList.add('editor-selectable');

      // เพิ่ม event listener
      element.addEventListener('click', function(e) {
        // ถ้าไม่ใช่การเลือกหลายรายการ ให้ลบการเลือกทั้งหมดก่อน
        if (!settings.multiple) {
          document.querySelectorAll(`.${settings.className}`).forEach(el => {
            el.classList.remove(settings.className);
          });
        }

        // สลับการเลือก
        this.classList.toggle(settings.className);

        // ส่งเหตุการณ์
        const event = new CustomEvent('element:selected', {
          detail: {element: this, selected: this.classList.contains(settings.className)}
        });
        document.dispatchEvent(event);
      });
    }

    /**
     * ทำให้อิลิเมนต์สามารถแก้ไขได้
     * @param {HTMLElement} element - อิลิเมนต์ที่จะทำให้แก้ไขได้
     * @param {Object} options - ตัวเลือก (optional)
     */
    makeEditable(element, options = {}) {
      if (!element) return;

      const defaults = {
        singleLine: false,
        maxLength: null,
        placeholder: ''
      };

      const settings = Object.assign({}, defaults, options);

      // เพิ่มคลาส
      element.classList.add('editor-editable');

      // ตั้งค่า contenteditable
      element.contentEditable = true;

      // ตั้งค่า placeholder
      if (settings.placeholder) {
        element.setAttribute('data-placeholder', settings.placeholder);
      }

      // เพิ่ม event listeners
      element.addEventListener('keydown', function(e) {
        // จำกัดบรรทัดเดียว
        if (settings.singleLine && e.key === 'Enter') {
          e.preventDefault();
        }

        // จำกัดความยาว
        if (settings.maxLength && this.textContent.length >= settings.maxLength) {
          if (e.key.length === 1) {
            e.preventDefault();
          }
        }
      });

      element.addEventListener('paste', function(e) {
        // จัดการการวางข้อความ
        e.preventDefault();

        const text = e.clipboardData.getData('text/plain');

        // จำกัดความยาว
        let newText = text;
        if (settings.maxLength) {
          const currentLength = this.textContent.length;
          if (currentLength >= settings.maxLength) {
            return;
          }

          const remainingLength = settings.maxLength - currentLength;
          newText = text.substring(0, remainingLength);
        }

        // จัดการบรรทัดเดียว
        if (settings.singleLine) {
          newText = newText.replace(/\n/g, '');
        }

        // แทรกข้อความ
        document.execCommand('insertText', false, newText);
      });
    }

    /**
     * ทำความสะอาด HTML
     * @param {string} html - HTML ที่จะทำความสะอาด
     * @returns {string} HTML ที่ทำความสะอาดแล้ว
     */
    sanitizeHTML(html) {
      const temp = document.createElement('div');
      temp.textContent = html;
      return temp.innerHTML;
    }

    /**
     * แปลงค่า RGB เป็น Hex
     * @param {string} rgb - ค่า RGB
     * @returns {string} ค่า Hex
     */
    rgbToHex(rgb) {
      if (!rgb || rgb === 'transparent' || rgb === 'rgba(0, 0, 0, 0)') {
        return '#ffffff';
      }

      const result = rgb.match(/\d+/g);
      if (!result || result.length < 3) {
        return '#ffffff';
      }

      const r = parseInt(result[0]);
      const g = parseInt(result[1]);
      const b = parseInt(result[2]);

      return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
    }

    /**
     * แปลงค่า Hex เป็น RGB
     * @param {string} hex - ค่า Hex
     * @returns {string} ค่า RGB
     */
    hexToRgb(hex) {
      if (!hex) {
        return 'rgb(0, 0, 0)';
      }

      const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);

      return result ?
        `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})` :
        'rgb(0, 0, 0)';
    }
  }

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

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