Ver Fonte

Add context menu and pan with middle click

Konstantinos Kaloutas há 3 anos atrás
pai
commit
f7fd54203f

+ 4 - 0
tldraw/apps/tldraw-logseq/src/app.tsx

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

+ 111 - 0
tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx

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

+ 1 - 0
tldraw/apps/tldraw-logseq/src/components/ContextMenu/index.ts

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

+ 45 - 1
tldraw/apps/tldraw-logseq/src/styles.css

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

+ 34 - 5
tldraw/packages/core/src/lib/TLApp/TLApp.ts

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

+ 2 - 0
tldraw/packages/core/src/lib/tools/TLSelectTool/TLSelectTool.tsx

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

+ 39 - 0
tldraw/packages/core/src/lib/tools/TLSelectTool/states/ContextMenuState.ts

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

+ 5 - 0
tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts

@@ -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 - 0
tldraw/packages/core/src/lib/tools/TLSelectTool/states/index.ts

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

+ 1 - 0
tldraw/packages/react/src/components/AppCanvas.tsx

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

+ 1 - 0
tldraw/packages/react/src/components/Canvas/Canvas.test.tsx

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

+ 2 - 0
tldraw/packages/react/src/components/Canvas/Canvas.tsx

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

+ 1 - 0
tldraw/packages/react/src/components/Renderer/Renderer.test.tsx

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