// docs/DESIGN.md "Background menu": a right-click on EMPTY content area opens the page's menu; over // text, a control, a link, media or an object with its own menu the browser's (or the object's) stays. // This decides "empty", from the element the pointer is over up to the page root that listens. /** An element that is not background: a control, a link, a field, media, or an object (a row or card * with its own menu, a list item the keyboard walks). Anything inside one is not background either. */ const NOT_BACKGROUND = [ "a", "button", "input", "textarea", "select", "label", "summary", "[contenteditable]", // A widget by its role (a div acting as a button, a menu, a listbox and its options, …). ...[ "button", "link", "menuitem", "tab", "option", "slider", "checkbox", "switch", "dialog", "menu", "listbox", ].map((role) => `[role="${role}"]`), "img", "picture", "video", "audio", "canvas", "svg", "iframe", "[data-context-menu-target]", "[data-nav-index]", ].join(","); /** True when `target` is text itself: an element holding words of its own (not only inside children). * A right-click on a paragraph, a heading or a label's padding is on text: its menu is the browser's. */ function holdsText(el: Element): boolean { for (const node of el.childNodes) if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) return true; return false; } /** True when a right-click on `target` falls on the empty area of `root` (the page). */ export function isBackgroundPoint(target: EventTarget | null, root: Element): boolean { if (!(target instanceof Element) || !root.contains(target)) return false; if (holdsText(target)) return false; const hit = target.closest(NOT_BACKGROUND); return !hit || !root.contains(hit); }