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