251 lines
9.0 KiB
JavaScript
251 lines
9.0 KiB
JavaScript
// 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<boolean>} 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();
|
||
}
|
||
})();
|