204 lines
4.4 KiB
TypeScript
204 lines
4.4 KiB
TypeScript
import {
|
|
type Accessor,
|
|
type JSX,
|
|
createEffect,
|
|
createSignal,
|
|
on,
|
|
onCleanup,
|
|
} from "solid-js";
|
|
|
|
type Props = JSX.Directives["floating"] & object;
|
|
|
|
export type FloatingElement = {
|
|
config: () => Props;
|
|
element: HTMLElement;
|
|
hide: () => void;
|
|
show: Accessor<Props | undefined>;
|
|
};
|
|
|
|
const [floatingElements, setFloatingElements] = createSignal<FloatingElement[]>(
|
|
[],
|
|
);
|
|
|
|
export { floatingElements };
|
|
|
|
/**
|
|
* Register a new floating element
|
|
* @param element element
|
|
*/
|
|
export function registerFloatingElement(element: FloatingElement) {
|
|
setFloatingElements((elements) => [...elements, element]);
|
|
}
|
|
|
|
/**
|
|
* Un register floating element
|
|
* @param element DOM Element
|
|
*/
|
|
export function unregisterFloatingElement(element: HTMLElement) {
|
|
setFloatingElements((elements) =>
|
|
elements.filter((entry) => entry.element !== element),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add floating elements
|
|
* @param element Element
|
|
* @param accessor Parameters
|
|
*/
|
|
export function floating(element: HTMLElement, accessor: Accessor<Props>) {
|
|
const config = accessor();
|
|
if (!config) return;
|
|
|
|
const [show, setShow] = createSignal<Props | undefined>();
|
|
// DEBUG: createEffect(() => console.info("show:", show()));
|
|
|
|
registerFloatingElement({
|
|
config: accessor,
|
|
element,
|
|
show,
|
|
/**
|
|
* Hide the element
|
|
*/
|
|
hide() {
|
|
setShow(undefined);
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Trigger a floating element
|
|
*/
|
|
function trigger(target: keyof Props, desiredState?: boolean) {
|
|
const current = show();
|
|
const config = accessor();
|
|
|
|
if (target === "userCard" && config.userCard) {
|
|
if (current?.userCard) {
|
|
setShow(undefined);
|
|
} else if (!current) {
|
|
setShow({ userCard: config.userCard });
|
|
} else {
|
|
setShow(undefined);
|
|
setShow({ userCard: config.userCard });
|
|
}
|
|
}
|
|
|
|
if (target === "tooltip" && config.tooltip) {
|
|
if (current?.tooltip) {
|
|
if (desiredState !== true) {
|
|
setShow(undefined);
|
|
}
|
|
} else if (!current) {
|
|
if (desiredState !== false) {
|
|
setShow({ tooltip: config.tooltip });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (target === "contextMenu" && config.contextMenu) {
|
|
if (current?.contextMenu) {
|
|
setShow(undefined);
|
|
} else if (!current) {
|
|
setShow({ contextMenu: config.contextMenu });
|
|
} else {
|
|
setShow(undefined);
|
|
setShow({ contextMenu: config.contextMenu });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle click events
|
|
*/
|
|
function onClick() {
|
|
// TODO: handle shift+click for mention
|
|
trigger("userCard");
|
|
}
|
|
|
|
/**
|
|
* Handle context menu click
|
|
*/
|
|
function onContextMenu(event: MouseEvent) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
|
|
trigger("contextMenu");
|
|
}
|
|
|
|
/**
|
|
* Handle mouse entering
|
|
*/
|
|
function onMouseEnter() {
|
|
trigger("tooltip", true);
|
|
}
|
|
|
|
/**
|
|
* Handle mouse leaving
|
|
*/
|
|
function onMouseLeave() {
|
|
trigger("tooltip", false);
|
|
}
|
|
|
|
createEffect(
|
|
on(
|
|
() => accessor().userCard,
|
|
(userCard) => {
|
|
if (userCard) {
|
|
element.style.cursor = "pointer";
|
|
element.style.userSelect = "none";
|
|
element.addEventListener("click", onClick);
|
|
|
|
onCleanup(() => element.removeEventListener("click", onClick));
|
|
}
|
|
},
|
|
),
|
|
);
|
|
|
|
createEffect(
|
|
on(
|
|
() => accessor().tooltip,
|
|
(tooltip) => {
|
|
if (tooltip) {
|
|
element.ariaLabel =
|
|
typeof tooltip.content === "string"
|
|
? tooltip.content
|
|
: tooltip!.aria!;
|
|
|
|
element.addEventListener("mouseenter", onMouseEnter);
|
|
element.addEventListener("mouseleave", onMouseLeave);
|
|
|
|
onCleanup(() => {
|
|
element.removeEventListener("mouseenter", onMouseEnter);
|
|
element.removeEventListener("mouseleave", onMouseLeave);
|
|
});
|
|
}
|
|
},
|
|
),
|
|
);
|
|
|
|
createEffect(
|
|
on(
|
|
() => accessor().contextMenu,
|
|
(contextMenu) => {
|
|
if (contextMenu) {
|
|
element.addEventListener(
|
|
accessor().contextMenuHandler ?? "contextmenu",
|
|
onContextMenu,
|
|
);
|
|
|
|
// TODO: iOS events for touch
|
|
|
|
onCleanup(() => {
|
|
element.removeEventListener(
|
|
config.contextMenuHandler ?? "contextmenu",
|
|
onContextMenu,
|
|
);
|
|
});
|
|
}
|
|
},
|
|
),
|
|
);
|
|
|
|
onCleanup(() => unregisterFloatingElement(element));
|
|
}
|