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