/** * Utilities - 工具函式 * 包含 XSS 防護、表單欄位工具、UI 回饋工具 */ import { TOAST_DURATION } from './config.js'; // ==================== XSS 防護工具 ==================== /** * 消毒 HTML 字串,防止 XSS 攻擊 * @param {string} str - 需要消毒的字串 * @returns {string} - 安全的字串 */ export function sanitizeHTML(str) { if (str === null || str === undefined) return ''; const temp = document.createElement('div'); temp.textContent = str; return temp.innerHTML; } /** * 安全設定元素文字內容 * @param {HTMLElement} element - 目標元素 * @param {string} text - 文字內容 */ export function safeSetText(element, text) { if (element) { element.textContent = text; } } // ==================== 表單欄位工具 ==================== /** * 只在欄位為空時填入值 * @param {string} elementId - DOM 元素 ID * @param {string} value - 要填入的值 * @returns {boolean} - 是否有填入值 */ export function fillIfEmpty(elementId, value) { const el = document.getElementById(elementId); if (el && !el.value.trim() && value) { el.value = value; return true; } return false; } /** * 獲取欄位當前值 * @param {string} elementId - DOM 元素 ID * @returns {string} - 欄位值(已 trim) */ export function getFieldValue(elementId) { const el = document.getElementById(elementId); return el ? el.value.trim() : ''; } /** * 獲取空白欄位列表 * @param {string[]} fieldIds - 欄位 ID 陣列 * @returns {string[]} - 空白欄位 ID 陣列 */ export function getEmptyFields(fieldIds) { return fieldIds.filter(id => !getFieldValue(id)); } // ==================== UI 回饋工具 ==================== /** * 顯示 Toast 提示訊息 * @param {string} message - 訊息內容 * @param {number} duration - 顯示時長(毫秒),預設 3000 */ export function showToast(message, duration = TOAST_DURATION) { const existingToast = document.querySelector('.toast'); if (existingToast) { existingToast.remove(); } const toast = document.createElement('div'); toast.className = 'toast'; toast.innerHTML = ` ${sanitizeHTML(message)} `; document.body.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } /** * 設定按鈕載入狀態 * @param {HTMLElement} btn - 按鈕元素 * @param {boolean} loading - 是否載入中 */ export function setButtonLoading(btn, loading) { if (loading) { btn.disabled = true; btn.innerHTML = '
AI 生成中...'; } else { btn.disabled = false; btn.innerHTML = '✨ I\'m feeling lucky'; } } // ==================== 錯誤處理工具 ==================== /** * 顯示可複製的錯誤對話框 * @param {Object} options - 錯誤選項 * @param {string} options.title - 錯誤標題 * @param {string} options.message - 錯誤訊息 * @param {string} options.details - 錯誤詳情 * @param {string[]} options.suggestions - 建議列表 */ export function showCopyableError(options) { const { title, message, details, suggestions } = options; // 移除舊的錯誤對話框 const existingModal = document.getElementById('errorModal'); if (existingModal) { existingModal.remove(); } const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); } /** * 關閉錯誤對話框 * @param {HTMLElement} button - 按鈕元素 */ export function closeErrorModal(button) { const modal = button.closest('.modal-overlay'); if (modal) { modal.remove(); } } /** * 複製錯誤詳情到剪貼板 */ export function copyErrorDetails() { const modal = document.getElementById('errorModal'); if (!modal) return; const errorText = modal.querySelector('.modal-body').textContent; navigator.clipboard.writeText(errorText).then(() => { showToast('錯誤詳情已複製到剪貼板'); }).catch(err => { console.error('複製失敗:', err); }); } // 將函式掛載到 window 上以便內聯事件處理器使用 if (typeof window !== 'undefined') { window.closeErrorModal = closeErrorModal; window.copyErrorDetails = copyErrorDetails; }