145 lines
4.2 KiB
TypeScript
145 lines
4.2 KiB
TypeScript
/**
|
|
* implements a popover using floating-ui for positioning and auto-update
|
|
* see https://www.skeleton.dev/docs/svelte/guides/cookbook/floating-ui-attachments
|
|
* for more details, examples, and original source.
|
|
*/
|
|
|
|
import { computePosition, autoUpdate, flip, offset, type Placement } from '@floating-ui/dom';
|
|
import { createAttachmentKey } from 'svelte/attachments';
|
|
|
|
/**
|
|
* Options for configuring the Popover behavior and appearance.
|
|
*/
|
|
export interface PopoverOptions {
|
|
/** Interaction type for the popover */
|
|
interaction?: 'click' | 'hover' | 'manual';
|
|
/** Placement of the popover */
|
|
placement?: Placement;
|
|
/** Offset distance between the reference and floating elements (default: 8) */
|
|
offset?: number;
|
|
/** Callback when the popover is opened or closed */
|
|
ontoggle?: (open: boolean) => void;
|
|
}
|
|
|
|
/**
|
|
* Popover class that manages the state and behavior of a popover element.
|
|
* It uses floating-ui for positioning and auto-update functionality.
|
|
*/
|
|
export class Popover {
|
|
private options: PopoverOptions = {
|
|
interaction: 'click',
|
|
placement: 'bottom-start',
|
|
offset: 8
|
|
};
|
|
private referenceElement: HTMLElement | undefined = $state();
|
|
private floatingElement: HTMLElement | undefined = $state();
|
|
private open = $state(false);
|
|
|
|
/**
|
|
* Creates a new Popover instance with optional configuration options.
|
|
* @param options - Optional configuration for the popover behavior and appearance.
|
|
*/
|
|
constructor(options?: PopoverOptions) {
|
|
if (options) this.options = { ...this.options, ...options };
|
|
$effect(() => {
|
|
if (!this.referenceElement || !this.floatingElement) return;
|
|
return autoUpdate(this.referenceElement, this.floatingElement, this.#updatePosition);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generates attributes for the reference element that triggers the popover.
|
|
* Includes event handlers based on the specified interaction type (click or hover).
|
|
* @returns An object containing necessary attributes and event handlers for
|
|
* the reference element.
|
|
*/
|
|
reference() {
|
|
const attrs = {
|
|
[createAttachmentKey()]: (node: HTMLElement) => {
|
|
this.referenceElement = node;
|
|
return () => {
|
|
this.referenceElement = undefined;
|
|
};
|
|
},
|
|
onclick: () => {},
|
|
onmouseover: () => {},
|
|
onmouseout: () => {}
|
|
};
|
|
// If click interaction
|
|
if (this.options.interaction === 'click') {
|
|
attrs['onclick'] = () => {
|
|
console.log('reference clicked, toggling popover');
|
|
this.setOpen(!this.open);
|
|
};
|
|
}
|
|
// If hover interaction
|
|
if (this.options.interaction === 'hover') {
|
|
attrs['onclick'] = () => {
|
|
this.setOpen(!this.open);
|
|
};
|
|
attrs['onmouseover'] = () => {
|
|
this.setOpen(true);
|
|
};
|
|
attrs['onmouseout'] = () => {
|
|
this.setOpen(false);
|
|
};
|
|
}
|
|
return attrs;
|
|
}
|
|
|
|
/** Returns whether the popover is open */
|
|
isOpen() {
|
|
return this.open;
|
|
}
|
|
/** Sets whether the popover is open, and triggers callbacks */
|
|
setOpen(open: boolean) {
|
|
if (this.open !== open) {
|
|
this.open = open;
|
|
this.options.ontoggle?.(open);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates attributes for the floating element (popover content) that is positioned
|
|
* relative to the reference element. It includes an attachment key to link the
|
|
* floating element to the popover instance.
|
|
* @returns An object containing necessary attributes for the floating element.
|
|
*/
|
|
floating() {
|
|
return {
|
|
[createAttachmentKey()]: (node: HTMLElement) => {
|
|
this.floatingElement = node;
|
|
node.style.position = 'absolute';
|
|
node.style.top = '0';
|
|
node.style.left = '0';
|
|
return () => {
|
|
this.floatingElement = undefined;
|
|
node.style.position = '';
|
|
node.style.top = '';
|
|
node.style.left = '';
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Updates the position of the floating element based on the reference element using
|
|
* the computePosition function from floating-ui. It applies the calculated
|
|
* position to the floating element's style.
|
|
*/
|
|
#updatePosition = async () => {
|
|
if (!this.referenceElement || !this.floatingElement) {
|
|
return;
|
|
}
|
|
const position = await computePosition(this.referenceElement, this.floatingElement, {
|
|
placement: this.options.placement,
|
|
middleware: [flip(), offset(this.options.offset)]
|
|
});
|
|
const { x, y } = position;
|
|
Object.assign(this.floatingElement.style, {
|
|
left: `${x}px`,
|
|
top: `${y}px`
|
|
});
|
|
};
|
|
}
|