Replace svelte-contextmenu with a custom implementation

Keyboard navigation is to be handled.
This commit is contained in:
Abdo 2025-07-09 04:47:03 +03:00
parent b1771895c5
commit 01fd4df9cb
8 changed files with 138 additions and 16 deletions

View file

@ -77,8 +77,7 @@
"lodash-es": "^4.17.21",
"lru-cache": "^10.2.0",
"marked": "^5.1.0",
"mathjax": "^3.1.2",
"svelte-contextmenu": "^1.0.2"
"mathjax": "^3.1.2"
},
"resolutions": {
"canvas": "npm:empty-npm-package@1.0.0",

View file

@ -0,0 +1,82 @@
<!-- Copyright: Ankitects Pty Ltd and contributors -->
<!-- License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
<script lang="ts">
import { tick } from "svelte";
import type { ContextMenuMouseEvent } from "./types";
let visible = $state(false);
let x = $state(0);
let y = $state(0);
let contextMenuElement = $state<HTMLDivElement>();
const { children } = $props();
export async function show(event: ContextMenuMouseEvent) {
event.preventDefault();
x = event.clientX;
y = event.clientY;
visible = true;
await tick();
const rect = contextMenuElement!.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
if (x + rect.width > viewportWidth) {
x = viewportWidth - rect.width;
}
if (y + rect.height > viewportHeight) {
y = viewportHeight - rect.height;
}
x = Math.max(0, x);
y = Math.max(0, y);
}
function hide() {
visible = false;
}
function handleClickOutside(event: MouseEvent) {
if (
visible &&
contextMenuElement &&
!contextMenuElement.contains(event.target as Node)
) {
hide();
}
}
</script>
<svelte:document on:click={handleClickOutside} />
{#if visible}
<div
bind:this={contextMenuElement}
class="context-menu"
style="left: {x}px; top: {y}px;"
role="menu"
tabindex="0"
onclick={hide}
onkeydown={hide}
>
{@render children?.()}
</div>
{/if}
<style lang="scss">
.context-menu {
position: fixed;
background: var(--canvas);
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 4px 0;
min-width: 120px;
z-index: 1000;
font-size: 13px;
outline: none;
}
</style>

View file

@ -0,0 +1,33 @@
<!-- Copyright: Ankitects Pty Ltd and contributors -->
<!-- License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -->
<script lang="ts">
const { click, children } = $props();
</script>
<div
class="context-menu-item"
onclick={click}
onkeydown={click}
role="menuitem"
tabindex="-1"
>
{@render children?.()}
</div>
<style lang="scss">
.context-menu-item {
padding: 8px 16px;
cursor: pointer;
user-select: none;
white-space: nowrap;
&:hover {
background-color: var(--highlight-bg);
}
&:active {
background-color: var(--highlight-bg);
}
}
</style>

View file

@ -0,0 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export { default as ContextMenu } from "./ContextMenu.svelte";
export { default as Item } from "./Item.svelte";
export type { ContextMenuAPI, ContextMenuMouseEvent } from "./types";

View file

@ -0,0 +1,12 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export interface ContextMenuMouseEvent {
clientX: number;
clientY: number;
preventDefault(): void;
}
export interface ContextMenuAPI {
show(event: ContextMenuMouseEvent): void;
}

View file

@ -14,7 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import LabelContainer from "./LabelContainer.svelte";
import LabelName from "./LabelName.svelte";
import { EditorState, type EditorMode } from "./types";
import ContextMenu, { Item } from "svelte-contextmenu";
import { ContextMenu, Item } from "$lib/context-menu";
export interface NoteEditorAPI {
fields: EditorFieldAPI[];
@ -1226,7 +1226,7 @@ components and functionality for general note editing.
<ContextMenu bind:this={contextMenu}>
{#each contextMenuItems as item}
<Item
on:click={() => {
click={() => {
item.action();
contextMenuInput?.focus();
}}

View file

@ -1,12 +1,12 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { ContextMenu, ContextMenuMouseEvent } from "$lib/context-menu";
import { openMedia, showInMediaFolder } from "@generated/backend";
import * as tr from "@generated/ftl";
import { bridgeCommand } from "@tslib/bridgecommand";
import { getSelection } from "@tslib/cross-browser";
import type ContextMenu from "svelte-contextmenu";
import type { ContextMenuMouseEvent } from "svelte-contextmenu/ContextMenuMouseEvent";
import { get } from "svelte/store";
import type { EditingInputAPI } from "./EditingArea.svelte";
import type { NoteEditorAPI } from "./NoteEditor.svelte";
@ -114,8 +114,6 @@ export function setupContextMenu(): [
if (contextMenuItems.length > 0) {
contextMenu?.show(event);
}
event.preventDefault();
}
return [onContextMenu, contextMenuItems];

View file

@ -1761,7 +1761,6 @@ __metadata:
sass: "npm:<1.77"
svelte: "npm:^5.34.9"
svelte-check: "npm:^4.2.2"
svelte-contextmenu: "npm:^1.0.2"
svelte-preprocess: "npm:^6.0.3"
svelte-preprocess-esbuild: "npm:^3.0.1"
svgo: "npm:^3.2.0"
@ -6229,13 +6228,6 @@ __metadata:
languageName: node
linkType: hard
"svelte-contextmenu@npm:^1.0.2":
version: 1.0.2
resolution: "svelte-contextmenu@npm:1.0.2"
checksum: 10c0/33cf79540337862278927f4b732b5d97f2c029348666ebcd3105b5c08bed54498a1d66f61b0212c18c204942cf54c9f6ce5ed509981d9f19f99f9334d8961cdf
languageName: node
linkType: hard
"svelte-eslint-parser@npm:^0.43.0":
version: 0.43.0
resolution: "svelte-eslint-parser@npm:0.43.0"