mirror of
https://github.com/mizarzh/mathlive-in-editor-mode.git
synced 2026-07-22 10:10:33 +00:00
fix: Fix menu dismissal blocking MathLive item clicks
This commit is contained in:
parent
bd50e254ce
commit
4467c726e3
1 changed files with 61 additions and 19 deletions
|
|
@ -4,7 +4,10 @@ type MenuCleanup = () => void;
|
|||
|
||||
interface DocumentMenuState {
|
||||
fields: Set<MathfieldElement>;
|
||||
lastMathfield: MathfieldElement | null;
|
||||
pendingClickMathfield: MathfieldElement | null;
|
||||
onPointerDown: EventListener;
|
||||
onClick: EventListener;
|
||||
}
|
||||
|
||||
const documentMenuStates = new WeakMap<Document, DocumentMenuState>();
|
||||
|
|
@ -33,6 +36,27 @@ function getActiveMathfield(
|
|||
return mathfield;
|
||||
}
|
||||
|
||||
function getPathMathfield(
|
||||
path: EventTarget[],
|
||||
fields: Set<MathfieldElement>
|
||||
): MathfieldElement | null {
|
||||
return (path.find((target) =>
|
||||
fields.has(target as MathfieldElement)
|
||||
) as MathfieldElement | undefined) ?? null;
|
||||
}
|
||||
|
||||
function isInsideMenu(event: Event, menus: HTMLElement[]): boolean {
|
||||
const path = event.composedPath();
|
||||
if (menus.some((menu) => path.includes(menu))) return true;
|
||||
if (!("clientX" in event) || !("clientY" in event)) return false;
|
||||
const { clientX, clientY } = event as MouseEvent;
|
||||
return menus.some((menu) => {
|
||||
const bounds = menu.getBoundingClientRect();
|
||||
return clientX >= bounds.left && clientX <= bounds.right &&
|
||||
clientY >= bounds.top && clientY <= bounds.bottom;
|
||||
});
|
||||
}
|
||||
|
||||
export function dismissOpenMathLiveMenu(mfe: MathfieldElement): boolean {
|
||||
const menu = getOpenMenus(mfe)[0];
|
||||
if (!menu) return false;
|
||||
|
|
@ -41,35 +65,42 @@ export function dismissOpenMathLiveMenu(mfe: MathfieldElement): boolean {
|
|||
const KeyboardEventConstructor =
|
||||
scrim.ownerDocument.defaultView?.KeyboardEvent ?? KeyboardEvent;
|
||||
// A top-layer menu does not reliably bubble its synthetic Escape to the scrim.
|
||||
// Keep it local so the math-field Escape handler does not undo current edits.
|
||||
scrim.dispatchEvent(new KeyboardEventConstructor("keydown", {
|
||||
key: "Escape",
|
||||
bubbles: true,
|
||||
bubbles: false,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
composed: false,
|
||||
}));
|
||||
if (menu.parentElement && mfe.isConnected) {
|
||||
mfe.executeCommand("toggleContextMenu");
|
||||
}
|
||||
// toggleContextMenu() can reopen a menu whose state is not exactly "open".
|
||||
return menu.parentElement === null;
|
||||
}
|
||||
|
||||
function handleDocumentPointerDown(
|
||||
event: Event,
|
||||
ownerDocument: Document,
|
||||
fields: Set<MathfieldElement>
|
||||
state: DocumentMenuState
|
||||
): void {
|
||||
const path = event.composedPath();
|
||||
const pathMathfield = path.find((target) =>
|
||||
fields.has(target as MathfieldElement)
|
||||
) as MathfieldElement | undefined;
|
||||
const mfe = pathMathfield ?? getActiveMathfield(ownerDocument, fields);
|
||||
if (!mfe) return;
|
||||
const pathMathfield = getPathMathfield(path, state.fields);
|
||||
const activeMathfield = getActiveMathfield(ownerDocument, state.fields);
|
||||
state.pendingClickMathfield = [
|
||||
pathMathfield,
|
||||
activeMathfield,
|
||||
state.lastMathfield,
|
||||
].find((mfe) => mfe && getOpenMenus(mfe).length > 0) ?? null;
|
||||
if (pathMathfield) state.lastMathfield = pathMathfield;
|
||||
}
|
||||
|
||||
function handleDocumentClick(event: Event, state: DocumentMenuState): void {
|
||||
const mfe = state.pendingClickMathfield;
|
||||
state.pendingClickMathfield = null;
|
||||
if (!mfe || !state.fields.has(mfe)) return;
|
||||
const menus = getOpenMenus(mfe);
|
||||
if (menus.length === 0) return;
|
||||
if (menus.some((menu) => path.includes(menu))) return;
|
||||
// Let MathLive finish the pointer dispatch before changing its popover state.
|
||||
if (menus.length === 0 || isInsideMenu(event, menus)) return;
|
||||
// Menu item click handlers finish before this document-level fallback.
|
||||
queueMicrotask(() => {
|
||||
if (fields.has(mfe)) dismissOpenMathLiveMenu(mfe);
|
||||
if (state.fields.has(mfe)) dismissOpenMathLiveMenu(mfe);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +109,19 @@ function getDocumentMenuState(ownerDocument: Document): DocumentMenuState {
|
|||
if (existing) return existing;
|
||||
|
||||
const fields = new Set<MathfieldElement>();
|
||||
const onPointerDown: EventListener = (event) => {
|
||||
handleDocumentPointerDown(event, ownerDocument, fields);
|
||||
const state: DocumentMenuState = {
|
||||
fields,
|
||||
lastMathfield: null,
|
||||
pendingClickMathfield: null,
|
||||
onPointerDown: (event) => {
|
||||
handleDocumentPointerDown(event, ownerDocument, state);
|
||||
},
|
||||
onClick: (event) => {
|
||||
handleDocumentClick(event, state);
|
||||
},
|
||||
};
|
||||
const state = { fields, onPointerDown };
|
||||
ownerDocument.addEventListener("pointerdown", onPointerDown, true);
|
||||
ownerDocument.addEventListener("pointerdown", state.onPointerDown, true);
|
||||
ownerDocument.addEventListener("click", state.onClick);
|
||||
documentMenuStates.set(ownerDocument, state);
|
||||
return state;
|
||||
}
|
||||
|
|
@ -101,8 +140,11 @@ export function setupMathLiveMenuDismissal(mfe: MathfieldElement): void {
|
|||
if (menuCleanups.get(mfe) !== cleanup) return;
|
||||
menuCleanups.delete(mfe);
|
||||
state.fields.delete(mfe);
|
||||
if (state.lastMathfield === mfe) state.lastMathfield = null;
|
||||
if (state.pendingClickMathfield === mfe) state.pendingClickMathfield = null;
|
||||
if (state.fields.size > 0) return;
|
||||
ownerDocument.removeEventListener("pointerdown", state.onPointerDown, true);
|
||||
ownerDocument.removeEventListener("click", state.onClick);
|
||||
documentMenuStates.delete(ownerDocument);
|
||||
};
|
||||
menuCleanups.set(mfe, cleanup);
|
||||
|
|
|
|||
Loading…
Reference in a new issue