mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 00:47:09 +00:00
refactor(frontent): move components and ts to lib folder
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
<script lang="ts">
|
||||
import { get_shifted_color } from '../ts/stores/ui_behavior';
|
||||
import type { MenuOption } from '../ts/types';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let {
|
||||
className = '',
|
||||
bg = 'bg-stone-700',
|
||||
hover_bg = get_shifted_color(bg, 100),
|
||||
active_bg = get_shifted_color(bg, 200),
|
||||
disabled = false,
|
||||
title = '',
|
||||
click_function = () => {},
|
||||
menu_options = null,
|
||||
menu_class = 'right-0',
|
||||
div_class = '',
|
||||
children
|
||||
}: {
|
||||
className?: string;
|
||||
bg?: string;
|
||||
hover_bg?: string;
|
||||
active_bg?: string;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
click_function?: (e: MouseEvent) => void;
|
||||
menu_options?: MenuOption[] | null;
|
||||
menu_class?: string;
|
||||
div_class?: string;
|
||||
children?: any;
|
||||
} = $props();
|
||||
|
||||
let menu_shown = $state(false);
|
||||
let button_element: HTMLButtonElement;
|
||||
let menu_element: HTMLDivElement | null = $state(null);
|
||||
let position_bottom = $state(true);
|
||||
|
||||
function onclick(e: MouseEvent) {
|
||||
if (menu_options !== null) {
|
||||
if (menu_shown) {
|
||||
close_menu();
|
||||
} else {
|
||||
open_menu();
|
||||
}
|
||||
}
|
||||
click_function(e);
|
||||
}
|
||||
|
||||
function getPolygon(): [number, number][] | null {
|
||||
if (!button_element || !menu_element) return null;
|
||||
const b = button_element.getBoundingClientRect();
|
||||
const m = menu_element.getBoundingClientRect();
|
||||
|
||||
// get polygon coords
|
||||
if (position_bottom) {
|
||||
return [
|
||||
[b.left, b.top],
|
||||
[b.right, b.top],
|
||||
[m.right, m.top],
|
||||
[m.right, m.bottom],
|
||||
[m.left, m.bottom],
|
||||
[m.left, m.top],
|
||||
[b.left, b.top]
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
[b.left, b.bottom],
|
||||
[b.right, b.bottom],
|
||||
[m.right, m.bottom],
|
||||
[m.right, m.top],
|
||||
[m.left, m.top],
|
||||
[m.left, m.bottom],
|
||||
[b.left, b.bottom]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!menu_shown) return;
|
||||
const polygon = getPolygon();
|
||||
if (!polygon) return;
|
||||
|
||||
const inside = pointInPolygon([e.clientX, e.clientY], polygon);
|
||||
if (!inside) {
|
||||
close_menu();
|
||||
}
|
||||
}
|
||||
|
||||
function pointInPolygon(point: [number, number], polygon: [number, number][]) {
|
||||
let [x, y] = point;
|
||||
let inside = false;
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
let [xi, yi] = polygon[i];
|
||||
let [xj, yj] = polygon[j];
|
||||
let intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
function is_enough_space_below(): boolean {
|
||||
if (!button_element || !menu_element) return true;
|
||||
const button_rectangle = button_element.getBoundingClientRect();
|
||||
const menu_height = menu_element.offsetHeight;
|
||||
const space_below_available = window.innerHeight - button_rectangle.bottom;
|
||||
|
||||
return space_below_available > menu_height;
|
||||
}
|
||||
|
||||
function open_menu() {
|
||||
menu_shown = true;
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
setTimeout(() => {
|
||||
position_bottom = is_enough_space_below();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function close_menu() {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
menu_shown = false;
|
||||
}
|
||||
|
||||
function no_option_has_icon(): boolean {
|
||||
for (const option of menu_options) {
|
||||
if (option.icon) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative min-w-0 flex {div_class}" {title}>
|
||||
<button
|
||||
bind:this={button_element}
|
||||
class="{className} {menu_shown ? hover_bg : bg} {disabled
|
||||
? 'text-stone-500 cursor-not-allowed'
|
||||
: 'hover:' +
|
||||
hover_bg +
|
||||
' active:' +
|
||||
active_bg +
|
||||
' cursor-pointer'} p-2 rounded-xl flex justify-center items-center transition-colors duration-200"
|
||||
{disabled}
|
||||
{onclick}
|
||||
>
|
||||
{@render children()}
|
||||
</button>
|
||||
|
||||
{#if menu_shown}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
bind:this={menu_element}
|
||||
transition:fade={{ duration: 50 }}
|
||||
class="absolute {position_bottom
|
||||
? 'top-full'
|
||||
: 'bottom-full'} {menu_class} z-100 my-1.5 min-w-64 rounded-xl backdrop-blur bg-stone-800/45 border border-stone-400/10 shadow-xl/20 p-2 flex flex-col gap-2 text-stone-200 cursor-auto"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{#each menu_options as option}
|
||||
<button
|
||||
disabled={option.disabled ?? false}
|
||||
class="bg-white/15 {option.disabled
|
||||
? 'text-stone-500 cursor-not-allowed'
|
||||
: 'hover:bg-white/35 active:bg-white/60 cursor-pointer ' +
|
||||
option.class} rounded-lg p-2 transition-colors duration-200 select-none flex flex-row gap-2 items-center"
|
||||
onclick={async (e) => {
|
||||
if (option.on_select) await option.on_select();
|
||||
close_menu();
|
||||
}}
|
||||
>
|
||||
{#if !no_option_has_icon()}
|
||||
<div class="aspect-square h-[1.2rem]">
|
||||
{#if option.icon}
|
||||
{@const Icon = option.icon}
|
||||
<Icon class="size-full" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="truncate min-w-0" title={option.name}>
|
||||
{option.name}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user