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

251 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 模式:确定=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;
});
}
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();
}
})();