UNPKG

@selenite/commons

Version:

This typescript package provides a set of frequently used utilities, types and svelte actions for building projects with Typescript and Svelte.

175 lines (174 loc) 5.19 kB
// import type { ShowContextMenu } from '$graph-editor/plugins/context-menu'; /** * This class is a singleton that represents the state of the context menu. * * It autohides based on the hovered state and filters items based on a query. */ export class ContextMenuState { triggerFirstItem() { this.triggerItem(0); } triggerItem(i) { const item = this.filteredItems.at(i); if (!item) { console.warn(`Tried to trigger a non existing item at ${i}.`); return; } item.action(); this.visible = false; } /** Singleton instance */ static #instance; /** Returns the singleton instance. */ static get instance() { if (!this.#instance) { this.#instance = new ContextMenuState(); } return this.#instance; } target = $state(); /** Position of the menu, in client coordinates. */ pos = $state({ x: 0, y: 0 }); minHeight = $state(); minWidth = $state(); /** Visibility of the menu. */ #visible = $state(false); get visible() { return this.#visible; } set visible(v) { this.#visible = v; if (!v) { if (this.onHide) { this.onHide(); this.onHide = undefined; } this.minHeight = undefined; this.minWidth = undefined; this.query = ''; this.target = undefined; this.expanded = false; } } /** Delay before hiding menu in miliseconds. */ hidingDelay = $state(100); /** Visibility of the searchbar. */ searchbar = $state(false); onHide = $state(); /** Items of the menu. */ items = $state([]); /** Is menu fully expanded */ expanded = $state(false); /** Query string to filter items. */ #query = $state(''); get query() { return this.#query; } set query(q) { this.#query = q.trim(); this.expanded = q.trim() !== ''; } sort = $state(false); /** Filtered items. */ filteredItems = $derived.by(() => { const query = this.query.toLowerCase().trim(); if (query === '') { return this.items; } // Filter and sort items based on the query return (this.items .map((item) => { // Compute a score based on the position of the query in the item const repr = [item.label, ...item.tags, ...item.path].map((s) => s.toLowerCase()); const score = repr.reduce((acc, s) => { const pos = s.indexOf(query); return acc + (pos === -1 ? 0 : 1 / (pos + 1)); }, 0); return { item, score }; }) // Keep only items with a positive score .filter(({ score }) => score > 0) .sort((a, b) => b.score - a.score) .map(({ item }) => item)); }); #focused = $state(false); get focused() { return this.#focused; } set focused(f) { this.#focused = f; // this.updateAutohide(); } /** Is the context menu hovered. */ #hovered = $state(false); /** Timeout to hide the menu. */ #hideTimeout = null; #autohide = $state(true); get autohide() { return this.#autohide; } set autohide(v) { this.#autohide = v; this.updateAutohide(); } updateAutohide() { if (this.#hovered || !this.#autohide) { if (this.#hideTimeout) { clearTimeout(this.#hideTimeout); this.#hideTimeout = null; } } else { if (!this.#autohide) { return; } this.#hideTimeout = setTimeout(() => { if (!this.#hovered) { this.visible = false; } }, this.hidingDelay); } } /** Sets hovered state and manages autohide. */ set hovered(v) { this.#hovered = v; this.updateAutohide(); } /** Returns hovered state. */ get hovered() { return this.#hovered; } constructor() { } } export const contextMenu = ContextMenuState.instance; /** * Shows the context menu with the given items at the given position. * * Helper function to use the context menu singleton. */ export const showContextMenu = ({ items, pos, sort = false, searchbar = false, onHide, expand = false, autoHide: autohide = true, target }) => { const menu = ContextMenuState.instance; menu.autohide = autohide; menu.minHeight = undefined; menu.minWidth = undefined; menu.target = target; menu.expanded = expand; menu.visible = true; menu.pos = pos; menu.sort = sort; menu.searchbar = searchbar; menu.items = items.map((item, i) => { return { label: String(i), id: String(i), description: '', action: () => { console.warn('Missing action for menu item', item.label ?? String(i)); }, path: [], tags: [], ...item }; }); menu.onHide = onHide; };