generated from Klectr/KTemplate
Merge pull request 'feat/context-menu' (#10) from feat/context-menu into main
Reviewed-on: https://git.tristonarmstrong.com/Klectr/KSlab/pulls/10
This commit is contained in:
commit
98fc00bac1
@ -11,6 +11,7 @@
|
|||||||
"commit": "cz"
|
"commit": "cz"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@kaioken-core/hooks": "^0.0.8",
|
||||||
"@tauri-apps/api": "^1",
|
"@tauri-apps/api": "^1",
|
||||||
"caniuse-lite": "^1.0.30001667",
|
"caniuse-lite": "^1.0.30001667",
|
||||||
"kaioken": "^0.32.1",
|
"kaioken": "^0.32.1",
|
||||||
|
@ -9,6 +9,7 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<ToastContextProvider>
|
<ToastContextProvider>
|
||||||
<MemoInfyCanvas />
|
<MemoInfyCanvas />
|
||||||
|
<div id="context-menu-portal" />
|
||||||
</ToastContextProvider>
|
</ToastContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
52
src/components/ContextMenuPortal.tsx
Normal file
52
src/components/ContextMenuPortal.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useClickOutside, useKeyStroke, useMouse } from "@kaioken-core/hooks";
|
||||||
|
import { Portal, signal, useEffect, useRef } from "kaioken";
|
||||||
|
|
||||||
|
namespace ContextMenuPortal {
|
||||||
|
export interface Props {
|
||||||
|
children: JSX.Children
|
||||||
|
open: boolean
|
||||||
|
closeAction: (() => void) | null | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function ContextMenuPortal({ children, open, closeAction }: ContextMenuPortal.Props) {
|
||||||
|
const { mouse } = useMouse()
|
||||||
|
const pos = signal({ x: 0, y: 0 })
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useClickOutside(ref, () => {
|
||||||
|
closeAction?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
pos.value = { x: mouse.x, y: mouse.y }
|
||||||
|
function _handleEscapeKey(e: KeyboardEvent) {
|
||||||
|
if (e.key !== "Escape") return
|
||||||
|
closeAction?.()
|
||||||
|
}
|
||||||
|
document.addEventListener("keydown", _handleEscapeKey)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", _handleEscapeKey)
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal
|
||||||
|
container={() => document.querySelector("#context-menu-portal")!}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="fixed z-10 select-none shadow-md rounded min-w-[150px]"
|
||||||
|
style={{
|
||||||
|
left: `${pos.value.x}px`,
|
||||||
|
top: `${pos.value.y}px`,
|
||||||
|
zIndex: `1000`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
)
|
||||||
|
}
|
@ -5,6 +5,7 @@ import { LayerEnum } from "../utils/enums"
|
|||||||
import images, { ImageCardType } from "../signals/images"
|
import images, { ImageCardType } from "../signals/images"
|
||||||
import { updateLocalStorage } from "../utils/localStorage"
|
import { updateLocalStorage } from "../utils/localStorage"
|
||||||
import { useThemeDetector } from "../utils/useThemeDetector"
|
import { useThemeDetector } from "../utils/useThemeDetector"
|
||||||
|
import { ContextMenuPortal } from "./ContextMenuPortal"
|
||||||
|
|
||||||
namespace ImageCard {
|
namespace ImageCard {
|
||||||
export interface ImageCardProps {
|
export interface ImageCardProps {
|
||||||
@ -22,6 +23,7 @@ export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps
|
|||||||
const offsetY = useRef(0)
|
const offsetY = useRef(0)
|
||||||
const initialResizeX = useRef(0)
|
const initialResizeX = useRef(0)
|
||||||
const initialResizeY = useRef(0)
|
const initialResizeY = useRef(0)
|
||||||
|
const openContextMenu = signal(false)
|
||||||
|
|
||||||
function debounceLSUpdate(time?: number) {
|
function debounceLSUpdate(time?: number) {
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
@ -93,29 +95,65 @@ export function ImageCard({ key: itemKey, data: item }: ImageCard.ImageCardProps
|
|||||||
ImagesSignal.default.images.notify()
|
ImagesSignal.default.images.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
function _handleMouseClick(e: MouseEvent) {
|
||||||
<div
|
e.preventDefault()
|
||||||
onmousedown={_handleMouseDown}
|
openContextMenu.value = !openContextMenu.value
|
||||||
className="select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#1c1c1c] absolute"
|
}
|
||||||
style={{
|
|
||||||
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
|
||||||
top: `${item.position.y}px`,
|
|
||||||
left: `${item.position.x}px`,
|
|
||||||
width: `${item.dimensions.w}px`,
|
|
||||||
height: `${item.dimensions.h}px`,
|
|
||||||
backgroundColor: '#181818',
|
|
||||||
backgroundImage: `url(${item.contents})`,
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="flex justify-center items-center hover:bg-blue-500 rounded w-5 h-5 dark:text-[#777] dark:hover:text-white text-white text-md absolute right-0 top-0"
|
|
||||||
onclick={_handleClose}>x</button>
|
|
||||||
|
|
||||||
<ExpandIcon cb={_handleResizeMouseDown} />
|
function _handleContextClose() {
|
||||||
</div >
|
openContextMenu.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
oncontextmenu={_handleMouseClick}
|
||||||
|
onmousedown={_handleMouseDown}
|
||||||
|
className="select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#1c1c1c] absolute"
|
||||||
|
style={{
|
||||||
|
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
||||||
|
top: `${item.position.y}px`,
|
||||||
|
left: `${item.position.x}px`,
|
||||||
|
width: `${item.dimensions.w}px`,
|
||||||
|
height: `${item.dimensions.h}px`,
|
||||||
|
backgroundColor: '#181818',
|
||||||
|
backgroundImage: `url(${item.contents})`,
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="flex justify-center items-center hover:bg-blue-500 rounded w-5 h-5 dark:text-[#777] dark:hover:text-white text-white text-md absolute right-0 top-0"
|
||||||
|
onclick={_handleClose}>x</button>
|
||||||
|
|
||||||
|
<ExpandIcon cb={_handleResizeMouseDown} />
|
||||||
|
</div >
|
||||||
|
|
||||||
|
<ContextMenuPortal open={openContextMenu.value} closeAction={_handleContextClose}>
|
||||||
|
<div className="bg-[#3c3c3c] flex flex-col rounded">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-md dark:text-[#999] text-black px-2 py-1">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border dark:border-[#2c2c2c] border-[#ddd] m-0 p-0" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li className="flex items-center gap-2 hover:bg-[#fff] dark:hover:bg-[#1a1a1a] cursor-pointer px-2 py-1">
|
||||||
|
<button onclick={_handleClose} className="text-md dark:text-[#999] text-black">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2 hover:bg-[#fff] dark:hover:bg-[#1a1a1a] cursor-pointer px-2 py-1">
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ContextMenuPortal>
|
||||||
|
</>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { ChangeEvent } from "tiny-markdown-editor"
|
|||||||
import { Divider } from "./Divider"
|
import { Divider } from "./Divider"
|
||||||
import { ExportIcon } from "./icons/ExportIcon"
|
import { ExportIcon } from "./icons/ExportIcon"
|
||||||
import { createFileAndExport } from "../utils/createFileAndExport"
|
import { createFileAndExport } from "../utils/createFileAndExport"
|
||||||
|
import { ContextMenuPortal } from "./ContextMenuPortal"
|
||||||
|
|
||||||
namespace NoteCard {
|
namespace NoteCard {
|
||||||
export interface NoteCardProps {
|
export interface NoteCardProps {
|
||||||
@ -26,6 +27,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
|
|||||||
const offsetY = useRef(0)
|
const offsetY = useRef(0)
|
||||||
const initialResizeX = useRef(0)
|
const initialResizeX = useRef(0)
|
||||||
const initialResizeY = useRef(0)
|
const initialResizeY = useRef(0)
|
||||||
|
const openContextMenu = signal(false)
|
||||||
|
|
||||||
const { debounce } = useDebounce()
|
const { debounce } = useDebounce()
|
||||||
|
|
||||||
@ -113,6 +115,15 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
|
|||||||
createFileAndExport("Note", item.contents, "text/markdown")
|
createFileAndExport("Note", item.contents, "text/markdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _handleMouseClick(e: MouseEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
openContextMenu.value = !openContextMenu.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function _handleContextClose() {
|
||||||
|
openContextMenu.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const cardPositionStyle = {
|
const cardPositionStyle = {
|
||||||
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
zIndex: `${focusedItem.value == itemKey ? LayerEnum.CARD_ELEVATED : LayerEnum.CARD}`,
|
||||||
width: `${item.dimensions.w}px`,
|
width: `${item.dimensions.w}px`,
|
||||||
@ -127,6 +138,7 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
oncontextmenu={_handleMouseClick}
|
||||||
onmousedown={_handleFocusCard}
|
onmousedown={_handleFocusCard}
|
||||||
style={cardPositionStyle}
|
style={cardPositionStyle}
|
||||||
className="overflow-hidden text-[#333] dark:bg-[#1a1a1a] dark:border-[#1c1c1c] bg-[#efeff0] select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#ddd] absolute"
|
className="overflow-hidden text-[#333] dark:bg-[#1a1a1a] dark:border-[#1c1c1c] bg-[#efeff0] select-none transition flex flex-col justify-stretch shadow-md rounded border border-[#ddd] absolute"
|
||||||
@ -158,6 +170,33 @@ export function NoteCard({ key: itemKey, data: item }: NoteCard.NoteCardProps) {
|
|||||||
<MarkDownEditor initial={item.contents} onChange={_handleMdChange} />
|
<MarkDownEditor initial={item.contents} onChange={_handleMdChange} />
|
||||||
<ExpandIcon cb={_handleResizeMouseDown} />
|
<ExpandIcon cb={_handleResizeMouseDown} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ContextMenuPortal open={openContextMenu.value} closeAction={_handleContextClose}>
|
||||||
|
<div className="bg-[#3c3c3c] flex flex-col rounded">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="text-md dark:text-[#999] text-black px-2 py-1">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border dark:border-[#2c2c2c] border-[#ddd] m-0 p-0" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li className="flex items-center gap-2 hover:bg-[#fff] dark:hover:bg-[#1a1a1a] cursor-pointer px-2 py-1">
|
||||||
|
<button onclick={_handleClose} className="text-md dark:text-[#999] text-black">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2 hover:bg-[#fff] dark:hover:bg-[#1a1a1a] cursor-pointer px-2 py-1">
|
||||||
|
<button onclick={_handleExportClick} className="text-md dark:text-[#999] text-black">
|
||||||
|
export
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ContextMenuPortal>
|
||||||
</div >
|
</div >
|
||||||
|
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user