// MNote UI 基础运行时:全局 portal、toast、居中 confirm/alert。 (function initMnoteUiRuntime() { if (window.__mnoteUiRuntimeStarted) return; window.__mnoteUiRuntimeStarted = true; var nativeConfirm = typeof window.confirm === 'function' ? window.confirm.bind(window) : null; var nativeAlert = typeof window.alert === 'function' ? window.alert.bind(window) : null; var activeDialog = null; function ensurePortalRoot() { var root = document.getElementById('mnote-portal-root'); if (root instanceof HTMLElement) return root; root = document.createElement('div'); root.id = 'mnote-portal-root'; root.setAttribute('data-mnote-portal-root', 'true'); document.body.appendChild(root); return root; } function ensureToastRegion() { var root = ensurePortalRoot(); var region = root.querySelector('[data-mnote-toast-region="true"]'); if (region instanceof HTMLElement) return region; region = document.createElement('div'); region.className = 'mnote-toast-region'; region.setAttribute('data-mnote-toast-region', 'true'); region.setAttribute('role', 'status'); region.setAttribute('aria-live', 'polite'); root.appendChild(region); return region; } function toast(message, options) { var text = String(message || '').trim(); if (!text) return null; var config = options || {}; var kind = String(config.kind || config.type || 'info').trim() || 'info'; var timeoutMs = Number(config.timeoutMs || config.duration || 3200); if (!Number.isFinite(timeoutMs) || timeoutMs < 800) timeoutMs = 3200; var region = ensureToastRegion(); var item = document.createElement('div'); item.className = 'mnote-toast mnote-toast--' + kind; item.setAttribute('data-mnote-toast', 'true'); item.setAttribute('data-kind', kind); item.textContent = text; region.appendChild(item); requestAnimationFrame(function showToast() { item.classList.add('mnote-toast--visible'); }); window.setTimeout(function hideToast() { item.classList.remove('mnote-toast--visible'); window.setTimeout(function removeToast() { if (item.parentNode) item.parentNode.removeChild(item); }, 180); }, timeoutMs); return item; } function closeActiveDialog(result) { if (!activeDialog) return; var state = activeDialog; activeDialog = null; document.removeEventListener('keydown', state.onKeyDown, true); if (state.backdrop && state.backdrop.parentNode) { state.backdrop.parentNode.removeChild(state.backdrop); } if (typeof state.resolve === 'function') state.resolve(result); } /** * 居中对话框。 * @param {object} options * @param {string} [options.title] * @param {string} options.message * @param {'confirm'|'alert'} [options.mode] * @param {string} [options.confirmLabel] * @param {string} [options.cancelLabel] * @param {'primary'|'danger'} [options.tone] * @returns {Promise} confirm 模式:确定=true;alert 模式始终 true */ function showDialog(options) { var config = options || {}; var message = String(config.message || '').trim(); if (!message) return Promise.resolve(config.mode === 'alert'); return new Promise(function (resolve) { if (activeDialog) closeActiveDialog(false); var mode = config.mode === 'alert' ? 'alert' : 'confirm'; var tone = String(config.tone || 'primary'); var title = String(config.title || (mode === 'alert' ? '提示' : '确认')).trim() || (mode === 'alert' ? '提示' : '确认'); var confirmLabel = String(config.confirmLabel || (mode === 'alert' ? '知道了' : '确定')).trim(); var cancelLabel = String(config.cancelLabel || '取消').trim(); var backdrop = document.createElement('div'); backdrop.className = 'mnote-dialog-backdrop'; backdrop.setAttribute('data-mnote-dialog-backdrop', 'true'); backdrop.setAttribute('data-mode', mode); var dialog = document.createElement('div'); dialog.className = 'mnote-dialog' + (tone === 'danger' ? ' mnote-dialog--danger' : ''); dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true'); dialog.setAttribute('data-mnote-dialog', 'true'); var body = document.createElement('div'); body.className = 'mnote-dialog__body'; var titleEl = document.createElement('h2'); titleEl.className = 'mnote-dialog__title'; titleEl.textContent = title; body.appendChild(titleEl); var messageEl = document.createElement('p'); messageEl.className = 'mnote-dialog__message'; messageEl.textContent = message; body.appendChild(messageEl); dialog.appendChild(body); var actions = document.createElement('div'); actions.className = 'mnote-dialog__actions'; var confirmBtn = document.createElement('button'); confirmBtn.type = 'button'; confirmBtn.className = 'mnote-dialog__btn ' + (tone === 'danger' ? 'mnote-dialog__btn--danger' : 'mnote-dialog__btn--primary'); confirmBtn.setAttribute('data-role', 'confirm'); confirmBtn.textContent = confirmLabel || '确定'; if (mode === 'confirm') { var cancelBtn = document.createElement('button'); cancelBtn.type = 'button'; cancelBtn.className = 'mnote-dialog__btn'; cancelBtn.setAttribute('data-role', 'cancel'); cancelBtn.textContent = cancelLabel || '取消'; cancelBtn.addEventListener('click', function () { closeActiveDialog(false); }); actions.appendChild(cancelBtn); } confirmBtn.addEventListener('click', function () { closeActiveDialog(true); }); actions.appendChild(confirmBtn); dialog.appendChild(actions); backdrop.appendChild(dialog); backdrop.addEventListener('click', function (event) { if (event.target === backdrop) { closeActiveDialog(mode === 'alert' ? true : false); } }); var onKeyDown = function (event) { if (event.key === 'Escape') { event.preventDefault(); closeActiveDialog(mode === 'alert' ? true : false); } else if (event.key === 'Enter') { var tag = (event.target && event.target.tagName) || ''; if (tag === 'TEXTAREA' || tag === 'BUTTON') return; event.preventDefault(); closeActiveDialog(true); } }; activeDialog = { backdrop: backdrop, resolve: resolve, onKeyDown: onKeyDown, }; document.addEventListener('keydown', onKeyDown, true); ensurePortalRoot().appendChild(backdrop); (mode === 'confirm' ? actions.querySelector('[data-role="cancel"]') || confirmBtn : confirmBtn).focus({ preventScroll: true, }); }); } function confirmDialog(message, options) { var config = options && typeof options === 'object' ? options : {}; return showDialog({ mode: 'confirm', message: message, title: config.title, confirmLabel: config.confirmLabel || config.okText, cancelLabel: config.cancelLabel || config.cancelText, tone: config.tone || (config.danger ? 'danger' : 'primary'), }); } function alertDialog(message, options) { var config = options && typeof options === 'object' ? options : {}; return showDialog({ mode: 'alert', message: message, title: config.title || '提示', confirmLabel: config.confirmLabel || config.okText || '知道了', tone: config.tone || (config.kind === 'error' || config.danger ? 'danger' : 'primary'), }).then(function () { return undefined; }); } window.mnote = window.mnote || {}; window.mnote.ensurePortalRoot = ensurePortalRoot; window.mnote.toast = toast; window.mnote.confirm = confirmDialog; window.mnote.alert = alertDialog; window.mnote.dialog = showDialog; // 全局替换:统一返回 Promise。调用点需 await(已在各 runtime 迁移)。 window.alert = function mnoteWindowAlert(message) { if (!document.body) { if (nativeAlert) nativeAlert(message); return Promise.resolve(); } return alertDialog(message); }; window.confirm = function mnoteWindowConfirm(message) { if (!document.body) { return Promise.resolve(nativeConfirm ? !!nativeConfirm(message) : true); } return confirmDialog(message); }; window.addEventListener('mnote:toast', function onMnoteToast(event) { var detail = event && event.detail ? event.detail : {}; toast(detail.message, detail); }); window.addEventListener('mnote:confirm', function onMnoteConfirm(event) { var detail = event && event.detail ? event.detail : {}; var message = detail.message || ''; confirmDialog(message, detail).then(function (ok) { if (typeof detail.onResult === 'function') detail.onResult(ok); }); }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', ensurePortalRoot, { once: true }); } else { ensurePortalRoot(); } })();