Files
mnote/rust/crates/mnote-web/browser/mnote-ui-runtime.js
T

251 lines
9.0 KiB
JavaScript
Raw Normal View History

// MNote UI 基础运行时:全局 portal、toast、居中 confirm/alert。
2026-06-25 21:08:17 +08:00
(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;
2026-06-25 21:08:17 +08:00
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 模式:确定=truealert 模式始终 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;
});
}
2026-06-25 21:08:17 +08:00
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);
};
2026-06-25 21:08:17 +08:00
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);
});
});
2026-06-25 21:08:17 +08:00
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ensurePortalRoot, { once: true });
} else {
ensurePortalRoot();
}
})();