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