/** * TemplateManager รับผิดชอบการจัดการเทมเพลต HTML และ modal * จัดการการโหลดเทมเพลตจาก DOM, การสร้างอินสแตนซ์พร้อมการผูกข้อมูล * และการจัดการสถานะและอีเวนต์ของ modal */ const TemplateManager = { /** * แมพเก็บเทมเพลตโดยใช้ ID เป็นคีย์ * @type {Map} */ templates: new Map(), /** * โหมดดีบัก * @type {boolean} */ debugMode: false, /** * ค่า z-index พื้นฐานสำหรับ modal เพื่อการจัดเรียงที่ถูกต้อง * @type {number} */ modalBaseZIndex: 1000, /** * เริ่มต้นการทำงานของ TemplateManager */ init() { try { // โหลดเทมเพลตจาก DOM document.querySelectorAll('template[id]').forEach(template => { if (this.isValidTemplate(template)) { this.templates.set(template.id, template); } }); this.debugMode = CONFIG.DEBUG_MODE || false; if (this.debugMode) { console.log('[TemplateManager] จำนวนเทมเพลตที่โหลด:', this.templates.size); } State.activeModals = []; this.setupGlobalEvents(); } catch (error) { console.error('[TemplateManager] ข้อผิดพลาดในการเริ่มต้น:', error); } }, /** * ตรวจสอบความถูกต้องของเทมเพลต */ isValidTemplate(template) { return template && template.id && template.content; }, /** * ตั้งค่าอีเวนต์ระดับ global สำหรับจัดการ modal */ setupGlobalEvents() { // จัดการปุ่ม ESC สำหรับปิด modal บนสุด document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.hasActiveModals()) { const topModal = this.getTopModal(); if (topModal && !topModal.classList.contains('persistent')) { this.closeModal(topModal); } } }); // จัดการคลิกนอก modal เพื่อปิด document.addEventListener('click', (e) => { if (this.hasActiveModals()) { const topModal = this.getTopModal(); if (topModal && !topModal.classList.contains('persistent') && e.target === topModal) { this.closeModal(topModal); } } }); }, /** * สร้างอินสแตนซ์ของเทมเพลตพร้อมข้อมูล */ create(templateId, data = {}) { try { if (!templateId || typeof templateId !== 'string') { throw new Error('ID เทมเพลตไม่ถูกต้อง'); } const template = this.templates.get(templateId); if (!template) { throw new Error(`ไม่พบเทมเพลต: ${templateId}`); } const element = template.content.cloneNode(true); element.firstElementChild?.setAttribute('data-template', templateId); return this.fillData(element, this.sanitizeData(data)); } catch (error) { this.debug('เกิดข้อผิดพลาดในการสร้างเทมเพลต:', error); return null; } }, /** * ทำความสะอาดข้อมูลเพื่อป้องกัน XSS */ sanitizeData(data) { if (data instanceof DocumentFragment || data instanceof HTMLElement) { return data; } if (data == null) { return ''; } if (data instanceof Date) { return data.toISOString(); } if (typeof data === 'string') { const div = document.createElement('div'); div.textContent = data; return div.innerHTML; } if (typeof data === 'number' || typeof data === 'boolean') { return data.toString(); } if (Array.isArray(data)) { return data.map(item => this.sanitizeData(item)); } if (typeof data === 'object') { const sanitized = {}; Object.entries(data).forEach(([key, value]) => { sanitized[key] = this.sanitizeData(value); }); return sanitized; } return String(data); }, /** * แทนที่ข้อมูลในเทมเพลต */ fillData(element, data) { element.querySelectorAll('[data-text]').forEach(el => { const key = el.dataset.text; if (data[key] !== undefined) { el.textContent = data[key]; } }); element.querySelectorAll('[data-attr]').forEach(el => { const attrs = el.dataset.attr.split(';'); attrs.forEach(attr => { const [attrName, key] = attr.split(':'); if (['disabled', 'checked'].includes(attrName)) { el[attrName] = data[key] == true; } else { el.setAttribute(attrName, data[key]); } }); }); element.querySelectorAll('[data-class]').forEach(el => { const key = el.dataset.class; if (data[key] !== undefined) { if (typeof data[key] === 'string') { el.className = data[key]; } else if (typeof data[key] === 'object') { Object.entries(data[key]).forEach(([className, condition]) => { el.classList.toggle(className, condition); }); } } }); element.querySelectorAll('[data-if]').forEach(el => { const condition = el.dataset.if; if (!data[condition]) { el.remove(); } }); element.querySelectorAll('[data-container]').forEach(el => { const key = el.dataset.container; const content = data[key]; if (!content) return; el.innerHTML = ''; if (Array.isArray(content)) { content.forEach(item => { this.appendContent(el, item); }); } else { this.appendContent(el, content); } }); return element; }, /** * เพิ่มเนื้อหาลงใน container */ appendContent(container, content) { try { if (content instanceof DocumentFragment) { container.appendChild(content.cloneNode(true)); } else if (content instanceof HTMLElement) { container.appendChild(content.cloneNode(true)); } else if (typeof content === 'string') { if (content.trim().startsWith('<') && content.trim().endsWith('>')) { container.innerHTML += content; } else { const textNode = document.createTextNode(content); container.appendChild(textNode); } } else if (content !== null && content !== undefined) { const textNode = document.createTextNode(String(content)); container.appendChild(textNode); } } catch (error) { console.error('เกิดข้อผิดพลาดในการเพิ่มเนื้อหา:', error); container.appendChild(document.createTextNode(String(content))); } }, /** * ประมวลผลการผูกข้อมูลตามประเภทที่ระบุ */ processDataBinding(element, type, data) { const selector = `[data-${type}]`; element.querySelectorAll(selector).forEach(el => { try { switch (type) { case 'text': this.bindText(el, data); break; case 'attr': this.bindAttributes(el, data); break; case 'class': this.bindClasses(el, data); break; case 'if': this.bindCondition(el, data); break; case 'container': this.bindContainer(el, data); break; case 'repeat': this.bindRepeat(el, data); break; } } catch (error) { this.debug(`เกิดข้อผิดพลาดในการประมวลผล ${type}:`, error); } }); }, /** * แสดง modal */ showModal(templateId, data = {}) { try { const fragment = this.create(templateId, data); if (!fragment) return null; const wrapper = document.createElement('div'); wrapper.appendChild(fragment); const modal = wrapper.firstElementChild; modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.style.zIndex = this.modalBaseZIndex + State.activeModals.length; document.body.appendChild(modal); this.setupModalEvents(modal); State.activeModals.push(modal); requestAnimationFrame(() => { modal.classList.add('active'); this.focusFirstElement(modal); }); return modal; } catch (error) { this.debug('เกิดข้อผิดพลาดในการแสดง modal:', error); return null; } }, /** * ตั้งค่าอีเวนต์สำหรับ modal */ setupModalEvents(modal) { if (!modal) return; const handlers = { close: () => this.closeModal(modal), backdropClick: (e) => { if (e.target === modal && !modal.classList.contains('persistent')) { this.closeModal(modal); } }, trapFocus: (e) => this.handleTabKey(e, modal) }; modal.querySelectorAll('.modal-close') .forEach(btn => btn.addEventListener('click', handlers.close)); modal.addEventListener('click', handlers.backdropClick); modal.addEventListener('keydown', handlers.trapFocus); State.activeModals.forEach(m => { if (m !== modal) this.closeModal(m); }); modal.addEventListener('modalClosed', () => { modal.removeEventListener('click', handlers.backdropClick); modal.removeEventListener('keydown', handlers.trapFocus); }, {once: true}); }, /** * จัดการการกดปุ่ม Tab เพื่อควบคุมโฟกัสภายใน modal */ handleTabKey(e, modal) { if (e.key !== 'Tab') return; const focusableElements = modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (e.shiftKey) { if (document.activeElement === firstElement) { lastElement.focus(); e.preventDefault(); } } else { if (document.activeElement === lastElement) { firstElement.focus(); e.preventDefault(); } } }, /** * ตั้งค่าโฟกัสให้กับอิลิเมนต์แรกที่โฟกัสได้ภายใน modal */ focusFirstElement(modal) { const focusable = modal.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusable) { focusable.focus(); } }, /** * ปิด modal */ closeModal(modal) { if (!modal) return; const close = () => { modal.dispatchEvent(new CustomEvent('modalClosed')); modal.remove(); State.activeModals = State.activeModals.filter(m => m !== modal); const topModal = this.getTopModal(); if (topModal) { this.focusFirstElement(topModal); } }; modal.classList.remove('active'); modal.classList.add('closing'); modal.addEventListener('animationend', () => close(), {once: true}); setTimeout(close, CONFIG.ANIMATION_DURATION); }, /** * ปิด modal ทั้งหมด */ closeAllModals() { [...State.activeModals].forEach(modal => this.closeModal(modal)); }, /** * ตรวจสอบว่ามี modal ที่เปิดอยู่หรือไม่ */ hasActiveModals() { return State.activeModals.length > 0; }, /** * ดึง modal ที่อยู่บนสุด */ getTopModal() { return State.activeModals[State.activeModals.length - 1] || null; }, /** * ลงทะเบียนเทมเพลตใหม่ */ registerTemplate(id, templateString) { const template = document.createElement('template'); template.id = id; template.innerHTML = templateString; this.templates.set(id, template); }, /** * ลบเทมเพลตที่ลงทะเบียนไว้ */ removeTemplate(id) { this.templates.delete(id); }, /** * ตรวจสอบว่ามีเทมเพลตที่ระบุหรือไม่ */ hasTemplate(id) { return this.templates.has(id); }, /** * ดึง HTML string ของเทมเพลต */ getTemplateString(id) { const template = this.templates.get(id); return template ? template.innerHTML : null; }, /** * แสดงข้อความดีบัก */ debug(...args) { if (this.debugMode) { console.log('[TemplateManager]', ...args); } }, /** * ผูกข้อความกับอิลิเมนต์ */ bindText(element, data) { const key = element.dataset.text; if (data[key] !== undefined) { element.textContent = this.sanitizeData(data[key]); } }, /** * ผูกแอตทริบิวต์กับอิลิเมนต์ */ bindAttributes(element, data) { const attrs = element.dataset.attr.split(';'); attrs.forEach(attr => { const [attrName, key] = attr.split(':'); const value = this.sanitizeData(data[key]); if (['disabled', 'checked', 'selected', 'readonly'].includes(attrName)) { element[attrName] = Boolean(value); } else { element.setAttribute(attrName, value); } }); }, /** * ผูกคลาสกับอิลิเมนต์ */ bindClasses(element, data) { const key = element.dataset.class; const value = data[key]; if (value !== undefined) { if (typeof value === 'string') { element.className = this.sanitizeData(value); } else if (typeof value === 'object') { Object.entries(value).forEach(([className, condition]) => { element.classList.toggle(className, Boolean(condition)); }); } } }, /** * ผูกเงื่อนไขกับอิลิเมนต์ */ bindCondition(element, data) { const condition = element.dataset.if; if (!data[condition]) { element.remove(); } }, /** * ผูกข้อมูลกับคอนเทนเนอร์ */ bindContainer(element, data) { const key = element.dataset.container; const content = data[key]; if (!content) { element.innerHTML = ''; return; } if (Array.isArray(content)) { element.innerHTML = ''; content.forEach(item => { this.appendContent(element, this.sanitizeData(item)); }); } else { element.innerHTML = ''; this.appendContent(element, this.sanitizeData(content)); } }, /** * ผูกการทำซ้ำกับอิลิเมนต์ */ bindRepeat(element, data) { const config = element.dataset.repeat.split(':'); if (config.length !== 2) return; const [itemName, arrayKey] = config; const array = data[arrayKey]; if (!Array.isArray(array)) return; const template = element.cloneNode(true); element.innerHTML = ''; array.forEach((item, index) => { const itemData = { [itemName]: item, index, isFirst: index === 0, isLast: index === array.length - 1 }; const instance = template.cloneNode(true); this.fillData(instance, {...data, ...itemData}); element.appendChild(instance); }); }, /** * หน่วงเวลาการทำงาน */ async delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, /** * เพิ่มคลาสชั่วคราว */ async addTemporaryClass(element, className, duration) { element.classList.add(className); await this.delay(duration); element.classList.remove(className); }, /** * อัพเดทข้อมูลในเทมเพลต */ update(element, data) { if (!element) return; const templateId = element.getAttribute('data-template'); if (!templateId || !this.hasTemplate(templateId)) return; const updatedElement = this.create(templateId, data); if (updatedElement) { element.replaceWith(updatedElement); } }, /** * ล้างข้อมูลในเทมเพลต */ clear(element) { if (!element) return; this.update(element, {}); } };