mirror of
https://github.com/logseq/logseq.git
synced 2026-05-26 13:44:13 +00:00
Add context menu and pan with middle click
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
import * as React from 'react'
|
||||
import { AppUI } from '~components/AppUI'
|
||||
import { ContextBar } from '~components/ContextBar/ContextBar'
|
||||
import { ContextMenu } from '~components/ContextMenu/ContextMenu'
|
||||
import { useFileDrop } from '~hooks/useFileDrop'
|
||||
import { usePaste } from '~hooks/usePaste'
|
||||
import { useQuickAdd } from '~hooks/useQuickAdd'
|
||||
@@ -102,11 +103,14 @@ export const App = function App({
|
||||
model={model}
|
||||
{...rest}
|
||||
>
|
||||
<ContextMenu>
|
||||
<div className="logseq-tldraw logseq-tldraw-wrapper">
|
||||
<AppCanvas components={components}>
|
||||
<AppUI />
|
||||
</AppCanvas>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
|
||||
</AppProvider>
|
||||
</LogseqContext.Provider>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useApp } from '@tldraw/react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
|
||||
import * as ReactContextMenu from '@radix-ui/react-context-menu'
|
||||
|
||||
const preventDefault = (e: Event) => e.stopPropagation()
|
||||
|
||||
interface ContextMenuProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const ContextMenu = observer(function ContextMenu({ children }: ContextMenuProps) {
|
||||
const app = useApp()
|
||||
const rContent = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<ReactContextMenu.Root>
|
||||
<ReactContextMenu.Trigger data-state={app.showContextMenu ? "open" : "closed"}>{children}</ReactContextMenu.Trigger>
|
||||
<ReactContextMenu.Content className="tl-context-menu" data-state={app.showContextMenu ? "open" : "closed"}
|
||||
ref={rContent}
|
||||
onEscapeKeyDown={preventDefault}
|
||||
asChild
|
||||
tabIndex={-1}>
|
||||
<div>
|
||||
<ReactContextMenu.Item className="tl-context-menu-button" onClick={app.copy}>
|
||||
Copy
|
||||
<div className="tl-context-menu-right-slot">⌘+C</div>
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item className="tl-context-menu-button" onClick={app.paste}>
|
||||
Paste
|
||||
<div className="tl-context-menu-right-slot">⌘+V</div>
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item className="tl-context-menu-button" onClick={app.api.selectAll}>
|
||||
Select All
|
||||
<div className="tl-context-menu-right-slot">⌘+A</div>
|
||||
</ReactContextMenu.Item>
|
||||
{/*TODO: Add paste to this menu*/}
|
||||
{app.selectedShapes && app.selectedShapes.size > 0 && (
|
||||
<>
|
||||
<ReactContextMenu.Item
|
||||
className="tl-context-menu-button"
|
||||
onClick={() => {
|
||||
app.api.deleteShapes()
|
||||
}}>
|
||||
Delete
|
||||
<div className="tl-context-menu-right-slot">Delete</div>
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item className="tl-context-menu-button">
|
||||
Duplicate
|
||||
<div className="tl-context-menu-right-slot">⌘+D</div>
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item
|
||||
className="tl-context-menu-button"
|
||||
onClick={() => {
|
||||
app.flipHorizontal(app.selectedShapesArray)
|
||||
}}>
|
||||
Flip Horizontally
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item
|
||||
className="tl-context-menu-button"
|
||||
onClick={() => {
|
||||
app.flipVertical(app.selectedShapesArray)
|
||||
}}>
|
||||
Flip Vertically
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item
|
||||
className="tl-context-menu-button"
|
||||
onClick={() => {
|
||||
app.bringToFront(app.selectedShapesArray)
|
||||
}}
|
||||
>
|
||||
Move to Front
|
||||
<div className="tl-context-menu-right-slot">⇧+]</div>
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item
|
||||
className="tl-context-menu-button"
|
||||
onClick={() => {
|
||||
app.bringForward(app.selectedShapesArray)
|
||||
}}
|
||||
>
|
||||
Move forwards
|
||||
<div className="tl-context-menu-right-slot">]</div>
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item
|
||||
className="tl-context-menu-button"
|
||||
onClick={() => {
|
||||
app.sendToBack(app.selectedShapesArray)
|
||||
}}
|
||||
>
|
||||
Move to back
|
||||
<div className="tl-context-menu-right-slot">⇧+[</div>
|
||||
</ReactContextMenu.Item>
|
||||
<ReactContextMenu.Item
|
||||
className="tl-context-menu-button"
|
||||
onClick={() => {
|
||||
app.sendBackward(app.selectedShapesArray)
|
||||
}}
|
||||
>
|
||||
Move backwards
|
||||
<div className="tl-context-menu-right-slot">[</div>
|
||||
</ReactContextMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ReactContextMenu.Content>
|
||||
</ReactContextMenu.Root>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ContextMenu'
|
||||
@@ -32,6 +32,51 @@
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.tl-context-menu-button {
|
||||
min-width: 220px;
|
||||
all: unset;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 25px;
|
||||
padding: 0 5px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
color: var(--color-text);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--ls-menu-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tl-context-menu {
|
||||
@apply relative flex bottom-0 flex border-0;
|
||||
|
||||
opacity: 100%;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
flex-direction: column;
|
||||
z-index: 180;
|
||||
min-width: 180;
|
||||
pointer-events: 'all';
|
||||
background: var(--ls-primary-background-color);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.tl-context-menu-right-slot {
|
||||
margin-left: auto;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.tl-context-menu-right-slot:focus {
|
||||
color: whites;
|
||||
}
|
||||
|
||||
.tl-action-bar {
|
||||
@apply absolute bottom-0 flex border-0;
|
||||
|
||||
@@ -79,7 +124,6 @@
|
||||
@apply flex items-center px-4 py-1 text-sm !important;
|
||||
min-width: 220px;
|
||||
all: unset;
|
||||
color: black;
|
||||
height: 25px;
|
||||
user-select: none;
|
||||
color: var(--color-text);
|
||||
|
||||
@@ -719,6 +719,12 @@ export class TLApp<
|
||||
)
|
||||
}
|
||||
|
||||
@computed get showContextMenu() {
|
||||
return (
|
||||
this.isIn('select.contextMenu')
|
||||
)
|
||||
}
|
||||
|
||||
@computed get showRotateHandles() {
|
||||
const { selectedShapesArray } = this
|
||||
return (
|
||||
@@ -812,6 +818,14 @@ export class TLApp<
|
||||
|
||||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
temporaryTransitionToMove(event: any) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const prevTool = this.selectedTool
|
||||
this.transition('move', { prevTool })
|
||||
this.selectedTool.transition('idleHold')
|
||||
}
|
||||
|
||||
readonly onTransition: TLStateEvents<S, K>['onTransition'] = () => {
|
||||
this.settings.update({ isToolLocked: false })
|
||||
}
|
||||
@@ -822,6 +836,18 @@ export class TLApp<
|
||||
}
|
||||
|
||||
readonly onPointerDown: TLEvents<S, K>['pointer'] = (info, e) => {
|
||||
// Switch to select on right click to enable contextMenu state
|
||||
if (e.button === 2) {
|
||||
this.transition('select', info)
|
||||
return false
|
||||
}
|
||||
|
||||
// Pan canvas when holding middle click
|
||||
if (!this.editingShape && e.button === 1 && !this.isIn('move')) {
|
||||
this.temporaryTransitionToMove(e)
|
||||
return
|
||||
}
|
||||
|
||||
if ('clientX' in e) {
|
||||
this.inputs.onPointerDown(
|
||||
[...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
|
||||
@@ -831,6 +857,13 @@ export class TLApp<
|
||||
}
|
||||
|
||||
readonly onPointerUp: TLEvents<S, K>['pointer'] = (info, e) => {
|
||||
if (!this.editingShape && e.button === 1 && this.isIn('move')) {
|
||||
this.selectedTool.transition('idle', { exit: true })
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if ('clientX' in e) {
|
||||
this.inputs.onPointerUp(
|
||||
[...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
|
||||
@@ -847,11 +880,7 @@ export class TLApp<
|
||||
|
||||
readonly onKeyDown: TLEvents<S, K>['keyboard'] = (info, e) => {
|
||||
if (!this.editingShape && e['key'] === ' ' && !this.isIn('move')) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
const prevTool = this.selectedTool
|
||||
this.transition('move', { prevTool })
|
||||
this.selectedTool.transition('idleHold')
|
||||
this.temporaryTransitionToMove(e)
|
||||
return
|
||||
}
|
||||
this.inputs.onKeyDown(e)
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { TLEvents, TLEventMap } from '~types'
|
||||
import {
|
||||
IdleState,
|
||||
BrushingState,
|
||||
ContextMenuState,
|
||||
PointingCanvasState,
|
||||
PointingShapeState,
|
||||
PointingShapeBehindBoundsState,
|
||||
@@ -35,6 +36,7 @@ export class TLSelectTool<
|
||||
static states = [
|
||||
IdleState,
|
||||
BrushingState,
|
||||
ContextMenuState,
|
||||
PointingCanvasState,
|
||||
PointingShapeState,
|
||||
PointingShapeBehindBoundsState,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLApp, TLSelectTool, TLShape, TLToolState } from '~lib'
|
||||
import {
|
||||
TLEvents,
|
||||
TLSelectionHandle,
|
||||
TLEventMap,
|
||||
TLEventSelectionInfo,
|
||||
} from '~types'
|
||||
|
||||
export class ContextMenuState<
|
||||
S extends TLShape,
|
||||
K extends TLEventMap,
|
||||
R extends TLApp<S, K>,
|
||||
P extends TLSelectTool<S, K, R>
|
||||
> extends TLToolState<S, K, R, P> {
|
||||
static id = 'contextMenu'
|
||||
|
||||
handle?: TLSelectionHandle
|
||||
|
||||
onEnter = (info: TLEventSelectionInfo) => {
|
||||
this.handle = info.handle
|
||||
}
|
||||
|
||||
onPointerDown: TLEvents<S>['pointer'] = () => {
|
||||
this.tool.transition('idle')
|
||||
}
|
||||
|
||||
onPinch: TLEvents<S>['pinch'] = info => {
|
||||
this.tool.transition('idle')
|
||||
}
|
||||
|
||||
onPinchEnd: TLEvents<S>['pinch'] = () => {
|
||||
this.tool.transition('idle')
|
||||
}
|
||||
|
||||
onWheel: TLEvents<S>['wheel'] = (info, e) => {
|
||||
this.tool.transition('idle')
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,11 @@ export class IdleState<
|
||||
inputs: { ctrlKey },
|
||||
} = this.app
|
||||
|
||||
if (event.button === 2) {
|
||||
this.tool.transition('contextMenu')
|
||||
return
|
||||
}
|
||||
|
||||
// Holding ctrlKey should ignore shapes
|
||||
if (ctrlKey) {
|
||||
this.tool.transition('pointingCanvas')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './BrushingState'
|
||||
export * from './ContextMenuState'
|
||||
export * from './IdleState'
|
||||
export * from './PointingShapeState'
|
||||
export * from './PointingBoundsBackgroundState'
|
||||
|
||||
@@ -33,6 +33,7 @@ export const AppCanvas = observer(function InnerApp<S extends TLReactShape>(
|
||||
showRotateHandles={app.showRotateHandles}
|
||||
showSelectionDetail={app.showSelectionDetail}
|
||||
showContextBar={app.showContextBar}
|
||||
showContextMenu={app.showContextMenu}
|
||||
cursor={app.cursors.cursor}
|
||||
cursorRotation={app.cursors.rotation}
|
||||
selectionRotation={app.selectionRotation}
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('Canvas', () => {
|
||||
showRotateHandles={app.showRotateHandles}
|
||||
showSelectionDetail={app.showSelectionDetail}
|
||||
showContextBar={app.showContextBar}
|
||||
showContextMenu={app.showContextMenu}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface TLCanvasProps<S extends TLReactShape> {
|
||||
showResizeHandles: boolean
|
||||
showRotateHandles: boolean
|
||||
showContextBar: boolean
|
||||
showContextMenu: boolean
|
||||
showSelectionDetail: boolean
|
||||
showSelectionRotation: boolean
|
||||
children: React.ReactNode
|
||||
@@ -82,6 +83,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
|
||||
showRotateHandles = true,
|
||||
showSelectionDetail = true,
|
||||
showContextBar = true,
|
||||
showContextMenu = true,
|
||||
showGrid = true,
|
||||
gridSize = 8,
|
||||
onEditingEnd = NOOP,
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('HTMLLayer', () => {
|
||||
showRotateHandles={app.showRotateHandles}
|
||||
showSelectionDetail={app.showSelectionDetail}
|
||||
showContextBar={app.showContextBar}
|
||||
showContextMenu={app.showContextBar}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user