mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-28 04:48:12 +08:00
Improve floating menu hover transfer behavior (#829)
This commit is contained in:
@@ -359,22 +359,25 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
pointerMoveHandler(e: PointerEvent) {
|
||||
// This element and the element being hovered over
|
||||
const self = this.$el as HTMLDivElement | undefined;
|
||||
const target = e.target as HTMLElement | undefined;
|
||||
const pointerOverFloatingMenuKeepOpen = target?.closest("[data-hover-menu-keep-open]") as HTMLElement | undefined;
|
||||
const pointerOverFloatingMenuSpawner = target?.closest("[data-hover-menu-spawner]") as HTMLElement | undefined;
|
||||
const floatingMenu: HTMLDivElement | undefined = this.$el;
|
||||
const pointerOverOwnFloatingMenuSpawner = floatingMenu && pointerOverFloatingMenuSpawner?.parentElement?.contains(floatingMenu);
|
||||
|
||||
// Swap this open floating menu with the one created by the floating menu spawner being hovered over
|
||||
if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) {
|
||||
this.$emit("update:open", false);
|
||||
pointerOverFloatingMenuSpawner.click();
|
||||
}
|
||||
// Get the spawner element (that which is clicked to spawn this floating menu)
|
||||
// Assumes the spawner is a sibling of this FloatingMenu component
|
||||
const ownSpawner: HTMLElement | undefined = self?.parentElement?.querySelector(":scope > [data-floating-menu-spawner]") || undefined;
|
||||
// Get the spawner element containing whatever element the user is hovering over now, if there is one
|
||||
const targetSpawner: HTMLElement | undefined = target?.closest("[data-floating-menu-spawner]") || undefined;
|
||||
|
||||
// Close the floating menu if the pointer has strayed far enough from its bounds
|
||||
if (this.isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE) && !pointerOverOwnFloatingMenuSpawner && !pointerOverFloatingMenuKeepOpen) {
|
||||
// TODO: Extend this rectangle bounds check to all `data-hover-menu-keep-open` element bounds up the DOM tree since currently
|
||||
// submenus disappear with zero stray distance if the cursor is further than the stray distance from only the top-level menu
|
||||
// Hover transfer
|
||||
// Transfer from this open floating menu to a sibling floating menu if the pointer hovers to a valid neighboring floating menu spawner
|
||||
this.hoverTransfer(self, ownSpawner, targetSpawner);
|
||||
|
||||
// Pointer stray
|
||||
// Close the floating menu if the pointer has strayed far enough from its bounds (and it's not hovering over its own spawner)
|
||||
if (ownSpawner !== targetSpawner && this.isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE)) {
|
||||
// TODO: Extend this rectangle bounds check to all submenu bounds up the DOM tree since currently submenus disappear
|
||||
// TODO: with zero stray distance if the cursor is further than the stray distance from only the top-level menu
|
||||
this.$emit("update:open", false);
|
||||
}
|
||||
|
||||
@@ -385,6 +388,72 @@ export default defineComponent({
|
||||
window.removeEventListener("pointerup", this.pointerUpHandler);
|
||||
}
|
||||
},
|
||||
hoverTransfer(self: HTMLDivElement | undefined, ownSpawner: HTMLElement | undefined, targetSpawner: HTMLElement | undefined): void {
|
||||
// Algorithm pseudo-code to detect and transfer to hover-transferrable floating menu spawners
|
||||
// Accompanying diagram: <https://files.keavon.com/-/SpringgreenKnownXantus/capture.png>
|
||||
//
|
||||
// Check our own parent for descendant spawners
|
||||
// Filter out ourself and our children
|
||||
// Filter out all with a different distance than our own distance from the currently-being-checked parent
|
||||
// How many left?
|
||||
// None -> go up a level and repeat
|
||||
// Some -> is one of them the target?
|
||||
// Yes -> click it and terminate
|
||||
// No -> do nothing and terminate
|
||||
|
||||
// Helper function that gets used below
|
||||
const getDepthFromAncestor = (item: Element, ancestor: Element): number | undefined => {
|
||||
let depth = 1;
|
||||
|
||||
let parent = item.parentElement || undefined;
|
||||
while (parent) {
|
||||
if (parent === ancestor) return depth;
|
||||
|
||||
parent = parent.parentElement || undefined;
|
||||
depth += 1;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// A list of all the descendant spawners: the spawner for this floating menu plus any spawners belonging to widgets inside this floating menu
|
||||
const ownDescendantMenuSpawners = Array.from(self?.parentElement?.querySelectorAll("[data-floating-menu-spawner]") || []);
|
||||
|
||||
// Start with the parent of the spawner for this floating menu and keep widening the search for any other valid spawners that are hover-transferrable
|
||||
let currentAncestor = (targetSpawner && ownSpawner?.parentElement) || undefined;
|
||||
while (currentAncestor) {
|
||||
const ownSpawnerDepthFromCurrentAncestor = ownSpawner && getDepthFromAncestor(ownSpawner, currentAncestor);
|
||||
const currentAncestor2 = currentAncestor; // This duplicate variable avoids an ESLint warning
|
||||
|
||||
// Get the list of descendant spawners and filter out invalid possibilities for spawners that are hover-transferrable
|
||||
const listOfDescendantSpawners = Array.from(currentAncestor?.querySelectorAll("[data-floating-menu-spawner]") || []);
|
||||
const filteredListOfDescendantSpawners = listOfDescendantSpawners.filter((item: Element): boolean => {
|
||||
// Filter away ourself and our descendants
|
||||
const notOurself = !ownDescendantMenuSpawners.includes(item);
|
||||
// And filter away unequal depths from the current ancestor
|
||||
const notUnequalDepths = notOurself && getDepthFromAncestor(item, currentAncestor2) === ownSpawnerDepthFromCurrentAncestor;
|
||||
// And filter away elements that explicitly disable hover transfer
|
||||
return notUnequalDepths && !(item as HTMLElement).getAttribute?.("data-floating-menu-spawner")?.includes("no-hover-transfer");
|
||||
});
|
||||
|
||||
// If none were found, widen the search by a level and keep trying (or stop looping if the root was reached)
|
||||
if (filteredListOfDescendantSpawners.length === 0) {
|
||||
currentAncestor = currentAncestor?.parentElement || undefined;
|
||||
}
|
||||
// Stop after the first non-empty set was found
|
||||
else {
|
||||
const foundTarget = filteredListOfDescendantSpawners.find((item: Element): boolean => item === targetSpawner);
|
||||
// If the currently hovered spawner is one of the found valid hover-transferrable spawners, swap to it by clicking on it
|
||||
if (foundTarget) {
|
||||
this.$emit("update:open", false);
|
||||
(foundTarget as HTMLElement).click();
|
||||
}
|
||||
|
||||
// In either case, we are done searching
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
keyDownHandler(e: KeyboardEvent) {
|
||||
if (this.escapeCloses && e.key.toLowerCase() === "escape") {
|
||||
this.$emit("update:open", false);
|
||||
|
||||
Reference in New Issue
Block a user