Add context menu and pan with middle click

This commit is contained in:
Konstantinos Kaloutas
2022-08-26 17:20:32 +03:00
parent c447a9abea
commit f7fd54203f
13 changed files with 247 additions and 6 deletions

View File

@@ -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>
)

View File

@@ -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>
)
})

View File

@@ -0,0 +1 @@
export * from './ContextMenu'

View File

@@ -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);

View File

@@ -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)

View File

@@ -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,

View File

@@ -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')
}
}

View File

@@ -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')

View File

@@ -1,4 +1,5 @@
export * from './BrushingState'
export * from './ContextMenuState'
export * from './IdleState'
export * from './PointingShapeState'
export * from './PointingBoundsBackgroundState'

View File

@@ -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}

View File

@@ -22,6 +22,7 @@ describe('Canvas', () => {
showRotateHandles={app.showRotateHandles}
showSelectionDetail={app.showSelectionDetail}
showContextBar={app.showContextBar}
showContextMenu={app.showContextMenu}
/>
)
}

View File

@@ -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,

View File

@@ -25,6 +25,7 @@ describe('HTMLLayer', () => {
showRotateHandles={app.showRotateHandles}
showSelectionDetail={app.showSelectionDetail}
showContextBar={app.showContextBar}
showContextMenu={app.showContextBar}
/>
)
}