Browse Source

Merge pull request #6489 from logseq/enhance/whiteboards-ui

Peng Xiao 3 years ago
parent
commit
028ea3bc83

+ 2 - 0
resources/css/common.css

@@ -931,6 +931,8 @@ button.menu:focus {
   @apply my-1;
 
   opacity: .5;
+  border-top-width: 1px;
+  border-color: var(--ls-border-color, #ccc);
 }
 
 a.login {

+ 19 - 0
tldraw/.editorconfig

@@ -0,0 +1,19 @@
+
+# https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+max_line_length = 80
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = 0
+trim_trailing_whitespace = false
+
+[COMMIT_EDITMSG]
+max_line_length = 0

+ 1 - 0
tldraw/apps/tldraw-logseq/package.json

@@ -10,6 +10,7 @@
     "dev:vite": "tsup --watch --sourcemap inline"
   },
   "devDependencies": {
+    "@radix-ui/react-context-menu": "^1.0.0",
     "@radix-ui/react-dropdown-menu": "^1.0.0",
     "@radix-ui/react-select": "^1.0.0",
     "@radix-ui/react-separator": "^1.0.0",

+ 9 - 5
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'
+import { ContextMenu } from './components/ContextMenu'
 import { useFileDrop } from './hooks/useFileDrop'
 import { usePaste } from './hooks/usePaste'
 import { useQuickAdd } from './hooks/useQuickAdd'
@@ -80,6 +81,7 @@ export const App = function App({
   const onFileDrop = useFileDrop(contextValue)
   const onPaste = usePaste(contextValue)
   const onQuickAdd = useQuickAdd()
+  const ref = React.useRef<HTMLDivElement>(null);
 
   const onPersistOnDiff: TLReactCallbacks<Shape>['onPersist'] = React.useCallback(
     (app, info) => {
@@ -102,11 +104,13 @@ export const App = function App({
         model={model}
         {...rest}
       >
-        <div className="logseq-tldraw logseq-tldraw-wrapper">
-          <AppCanvas components={components}>
-            <AppUI />
-          </AppCanvas>
-        </div>
+        <ContextMenu collisionRef={ref}>
+          <div ref={ref} className="logseq-tldraw logseq-tldraw-wrapper">
+            <AppCanvas components={components}>
+              <AppUI />
+            </AppCanvas>
+          </div>
+        </ContextMenu>
       </AppProvider>
     </LogseqContext.Provider>
   )

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

@@ -0,0 +1,129 @@
+import { useApp } from '@tldraw/react'
+import { MOD_KEY} from '@tldraw/core'
+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
+  collisionRef: React.RefObject<HTMLDivElement>
+}
+
+export const ContextMenu = observer(function ContextMenu({ children, collisionRef }: ContextMenuProps) {
+  const app = useApp()
+  const rContent = React.useRef<HTMLDivElement>(null)
+
+  return (
+    <ReactContextMenu.Root onOpenChange={state => {if (!state) app.transition('select')}}>
+      <ReactContextMenu.Trigger>{children}</ReactContextMenu.Trigger>
+      <ReactContextMenu.Content className="tl-context-menu"
+      ref={rContent}
+      onEscapeKeyDown={preventDefault}
+      collisionBoundary={collisionRef.current}
+      asChild
+      tabIndex={-1}
+      >
+        <div>
+          {app.selectedShapes?.size > 0 && (
+            <>
+              <ReactContextMenu.Item
+                className="tl-context-menu-button"
+                onClick={() => app.copy()}>
+                  Copy
+                  <div className="tl-context-menu-right-slot">
+                    <span className="keyboard-shortcut"><code>{MOD_KEY}</code> <code>C</code></span>
+                  </div>
+              </ReactContextMenu.Item>
+            </>
+          )}
+          <ReactContextMenu.Item
+            className="tl-context-menu-button"
+            onClick={() => app.paste()}>
+              Paste
+              <div className="tl-context-menu-right-slot">
+                <span className="keyboard-shortcut"><code>{MOD_KEY}</code> <code>V</code></span>
+              </div>
+          </ReactContextMenu.Item>
+          <ReactContextMenu.Separator className="menu-separator"/>
+          <ReactContextMenu.Item
+            className="tl-context-menu-button"
+            onClick={() => app.api.selectAll()}>
+              Select All
+            <div className="tl-context-menu-right-slot">
+              <span className="keyboard-shortcut"><code>{MOD_KEY}</code> <code>A</code></span>
+            </div>
+          </ReactContextMenu.Item>
+          {app.selectedShapes?.size > 1 && (
+            <ReactContextMenu.Item
+              className="tl-context-menu-button"
+              onClick={() => app.api.deselectAll()}>
+                Deselect All
+            </ReactContextMenu.Item>
+          )}
+          {app.selectedShapes?.size > 0 && (
+            <>
+              <ReactContextMenu.Item
+                className="tl-context-menu-button"
+                onClick={() => app.api.deleteShapes()}>
+                Delete
+                <div className="tl-context-menu-right-slot">
+                  <span className="keyboard-shortcut"><code>Del</code></span>
+                </div>
+              </ReactContextMenu.Item>
+              {app.selectedShapes?.size > 1 && (
+                <>
+                  <ReactContextMenu.Separator className="menu-separator"/>
+                  <ReactContextMenu.Item
+                    className="tl-context-menu-button"
+                    onClick={() => app.flipHorizontal()}>
+                      Flip Horizontally
+                  </ReactContextMenu.Item>
+                  <ReactContextMenu.Item
+                    className="tl-context-menu-button"
+                    onClick={() => app.flipVertical()}>
+                      Flip Vertically
+                  </ReactContextMenu.Item>
+                </>
+              )}
+              <ReactContextMenu.Separator className="menu-separator"/>
+              <ReactContextMenu.Item
+                className="tl-context-menu-button"
+                onClick={() => app.bringToFront()}>
+                  Move to Front
+                <div className="tl-context-menu-right-slot">
+                  <span className="keyboard-shortcut"><code>⇧</code> <code>]</code></span>
+                </div>
+              </ReactContextMenu.Item>
+              <ReactContextMenu.Item
+                className="tl-context-menu-button">
+                  Move forwards
+                <div className="tl-context-menu-right-slot">
+                  <span className="keyboard-shortcut"><code>]</code></span>
+                </div>
+              </ReactContextMenu.Item>
+              <ReactContextMenu.Item
+                className="tl-context-menu-button"
+                onClick={() => app.sendToBack()}>
+                  Move to back
+                <div className="tl-context-menu-right-slot">
+                  <span className="keyboard-shortcut"><code>⇧</code> <code>[</code></span>
+                </div>
+              </ReactContextMenu.Item>
+              <ReactContextMenu.Item
+                className="tl-context-menu-button"
+                onClick={() => app.sendBackward()}>
+                  Move backwards
+                <div className="tl-context-menu-right-slot">
+                  <span className="keyboard-shortcut"><code>[</code></span>
+                </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'

+ 10 - 9
tldraw/apps/tldraw-logseq/src/components/ZoomMenu/ZoomMenu.tsx

@@ -1,5 +1,6 @@
 import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
 import { useApp } from '@tldraw/react'
+import { MOD_KEY} from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
 
 export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
@@ -19,39 +20,39 @@ export const ZoomMenu = observer(function ZoomMenu(): JSX.Element {
       >
         <DropdownMenuPrimitive.Arrow style={{ fill: 'white' }}></DropdownMenuPrimitive.Arrow>
         <DropdownMenuPrimitive.Item
-          className="tl-zoom-menu-dropdown-item"
+          className="menu-link tl-zoom-menu-dropdown-item"
           onSelect={preventEvent}
           onClick={app.api.zoomToFit}
         >
           Zoom to Fit <div className="tl-zoom-menu-right-slot"></div>
         </DropdownMenuPrimitive.Item>
         <DropdownMenuPrimitive.Item
-          className="tl-zoom-menu-dropdown-item"
+          className="menu-link tl-zoom-menu-dropdown-item"
           onSelect={preventEvent}
           onClick={app.api.zoomToSelection}
         >
-          Zoom to Selection <div className="tl-zoom-menu-right-slot">⌘+Minus</div>
+          Zoom to Selection <div className="tl-zoom-menu-right-slot"><span className="keyboard-shortcut"><code>{MOD_KEY}</code> <code>-</code></span></div>
         </DropdownMenuPrimitive.Item>
         <DropdownMenuPrimitive.Item
-          className="tl-zoom-menu-dropdown-item"
+          className="menu-link tl-zoom-menu-dropdown-item"
           onSelect={preventEvent}
           onClick={app.api.zoomIn}
         >
-          Zoom In <div className="tl-zoom-menu-right-slot">⌘+Plus</div>
+          Zoom In <div className="tl-zoom-menu-right-slot"><span className="keyboard-shortcut"><code>{MOD_KEY}</code> <code>+</code></span></div>
         </DropdownMenuPrimitive.Item>
         <DropdownMenuPrimitive.Item
-          className="tl-zoom-menu-dropdown-item"
+          className="menu-link tl-zoom-menu-dropdown-item"
           onSelect={preventEvent}
           onClick={app.api.zoomOut}
         >
-          Zoom Out <div className="tl-zoom-menu-right-slot">⌘+Minus</div>
+          Zoom Out <div className="tl-zoom-menu-right-slot"><span className="keyboard-shortcut"><code>{MOD_KEY}</code> <code>-</code></span></div>
         </DropdownMenuPrimitive.Item>
         <DropdownMenuPrimitive.Item
-          className="tl-zoom-menu-dropdown-item"
+          className="menu-link tl-zoom-menu-dropdown-item"
           onSelect={preventEvent}
           onClick={app.api.resetZoom}
         >
-          Reset Zoom <div className="tl-zoom-menu-right-slot">⇧+0</div>
+          Reset Zoom <div className="tl-zoom-menu-right-slot"><span className="keyboard-shortcut"><code>⇧</code> <code>0</code></span></div>
         </DropdownMenuPrimitive.Item>
       </DropdownMenuPrimitive.Content>
     </DropdownMenuPrimitive.Root>

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

@@ -36,6 +36,45 @@
   font-size: inherit;
 }
 
+.tl-context-menu-button {
+  @apply flex items-center px-4 py-1 text-sm !important;
+
+  min-width: 220px;
+  all: unset;
+  line-height: 1;
+  height: 25px;
+  padding: 0 5px;
+  position: relative;
+  user-select: none;
+  color: var(--ls-primary-text-color);
+
+  &:hover,
+  &:focus {
+    background-color: var(--ls-secondary-background-color) !important;
+  }
+}
+
+.tl-context-menu {
+  @apply relative py-2 flex bottom-0 flex border-0 rounded shadow-lg;
+
+  opacity: 100%;
+  user-select: none;
+  flex-direction: column;
+  z-index: 180;
+  min-width: 220px;
+  pointer-events: 'all';
+  background: var(--ls-primary-background-color);
+}
+
+.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;
 
@@ -69,33 +108,26 @@
 }
 
 .tl-zoom-menu-dropdown-menu-button {
+  @apply py-2 rounded shadow-lg;
   opacity: 100%;
-  background-color: var(--color-panel);
-  border-radius: 6px;
-  padding: 5px;
-  box-shadow: 0 10px 38px -10px rgba(22, 23, 24, 0.35), 0 10px 20px -15px rgba(22, 23, 24, 0.2);
+  background-color: var(--ls-primary-background-color);
+
 
   > span svg {
-    fill: var(--color-panel) !important;
+    display: none !important;
   }
 }
 
 .tl-zoom-menu-dropdown-item {
+  @apply flex items-center px-4 py-1 text-sm !important;
   min-width: 220px;
   all: unset;
-  font-size: 13px;
-  line-height: 1;
-  color: black;
-  border-radius: 3px;
-  display: flex;
-  align-items: center;
   height: 25px;
-  padding: 0 5px;
-  position: relative;
   user-select: none;
   color: var(--color-text);
 
   &:hover {
+    cursor: pointer;
     background-color: var(--color-hover);
   }
 }
@@ -103,7 +135,6 @@
 .tl-zoom-menu-right-slot {
   margin-left: auto;
   padding-left: 20px;
-  color: var(--color-text);
 }
 
 .tl-contextbar {

+ 2 - 0
tldraw/packages/core/src/constants.ts

@@ -22,6 +22,8 @@ export const FIT_TO_SCREEN_PADDING = 100
 
 export const BINDING_DISTANCE = 4
 
+export const ZOOM_UPDATE_FACTOR = 0.8
+
 export const GRID_SIZE = 8
 
 export const EMPTY_OBJECT: any = {}

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

@@ -806,16 +806,42 @@ 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 })
   }
 
   readonly onWheel: TLEvents<S, K>['wheel'] = (info, e) => {
+    if (e.ctrlKey) {
+      return
+    }
+
     this.viewport.panCamera(info.delta)
     this.inputs.onWheel([...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5], e)
   }
 
   readonly onPointerDown: TLEvents<S, K>['pointer'] = (info, e) => {
+
+
+    // Pan canvas when holding middle click
+    if (!this.editingShape && e.button === 1 && !this.isIn('move')) {
+      this.temporaryTransitionToMove(e)
+      return
+    }
+
+    // Switch to select on right click to enable contextMenu state
+    if (e.button === 2) {
+      this.transition('select')
+      return
+    }
+
     if ('clientX' in e) {
       this.inputs.onPointerDown(
         [...this.viewport.getPagePoint([e.clientX, e.clientY]), 0.5],
@@ -825,6 +851,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],
@@ -841,11 +874,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 - 3
tldraw/packages/core/src/lib/TLViewport.ts

@@ -1,6 +1,6 @@
 import { Vec } from '@tldraw/vec'
 import { action, computed, makeObservable, observable } from 'mobx'
-import { FIT_TO_SCREEN_PADDING } from '../constants'
+import { FIT_TO_SCREEN_PADDING, ZOOM_UPDATE_FACTOR } from '../constants'
 import type { TLBounds } from '../types'
 
 export class TLViewport {
@@ -84,7 +84,7 @@ export class TLViewport {
 
   zoomIn = (): this => {
     const { camera, bounds } = this
-    const zoom: number = Math.min(TLViewport.maxZoom, Math.ceil((camera.zoom * 100 + 1) / 25) / 4)
+    const zoom: number = Math.min(TLViewport.maxZoom, camera.zoom / ZOOM_UPDATE_FACTOR)
     const center = [bounds.width / 2, bounds.height / 2]
     const p0 = Vec.sub(Vec.div(center, camera.zoom), center)
     const p1 = Vec.sub(Vec.div(center, zoom), center)
@@ -93,7 +93,7 @@ export class TLViewport {
 
   zoomOut = (): this => {
     const { camera, bounds } = this
-    const zoom: number = Math.max(TLViewport.minZoom, Math.floor((camera.zoom * 100 - 1) / 25) / 4)
+    const zoom: number = Math.max(TLViewport.minZoom, camera.zoom * ZOOM_UPDATE_FACTOR)
     const center = [bounds.width / 2, bounds.height / 2]
     const p0 = Vec.sub(Vec.div(center, camera.zoom), center)
     const p1 = Vec.sub(Vec.div(center, zoom), center)

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

@@ -5,6 +5,7 @@ import { TLTool } from '../../TLTool'
 import {
   IdleState,
   BrushingState,
+  ContextMenuState,
   PointingCanvasState,
   PointingShapeState,
   PointingShapeBehindBoundsState,
@@ -37,6 +38,7 @@ export class TLSelectTool<
   static states = [
     IdleState,
     BrushingState,
+    ContextMenuState,
     PointingCanvasState,
     PointingShapeState,
     PointingShapeBehindBoundsState,

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

@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import type { TLShape } from '../../../shapes'
+import type { TLApp } from '../../../TLApp'
+import { TLToolState } from '../../../TLToolState'
+import type { TLSelectTool } from '../TLSelectTool'
+import type { 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')
+  }
+}

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

@@ -46,6 +46,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'

+ 2 - 0
tldraw/packages/core/src/utils/index.ts

@@ -77,6 +77,8 @@ export function modKey(e: any): boolean {
   return isDarwin() ? e.metaKey : e.ctrlKey
 }
 
+export const MOD_KEY = isDarwin() ? '⌘' : 'Ctrl';
+
 export function isNonNullable<TValue>(value: TValue): value is NonNullable<TValue> {
   return Boolean(value)
 }

+ 1 - 12
tldraw/packages/react/src/components/ContextBarContainer/ContextBarContainer.tsx

@@ -56,22 +56,11 @@ export const ContextBarContainer = observer(function ContextBarContainer<S exten
     BoundsUtils.boundsContain(vpBounds, screenBounds) ||
     BoundsUtils.boundsCollide(vpBounds, screenBounds)
 
-  React.useLayoutEffect(() => {
-    const elm = rBounds.current
-    if (!elm) return
-    if (hidden || !inView) {
-      elm.classList.add('tl-fade-out')
-      elm.classList.remove('tl-fade-in')
-    } else {
-      elm.classList.add('tl-fade-in')
-      elm.classList.remove('tl-fade-out')
-    }
-  }, [hidden, inView])
 
   return (
     <div
       ref={rBounds}
-      className="tl-counter-scaled-positioned tl-fade-out"
+      className="tl-counter-scaled-positioned"
       aria-label="context-bar-container"
       onPointerMove={stopEventPropagation}
       onPointerUp={stopEventPropagation}

+ 1 - 1
tldraw/packages/react/src/constants.ts

@@ -6,7 +6,7 @@ export const TAU = PI / 2
 export const PI2 = PI * 2
 export const EPSILON = Math.PI / 180
 export const FIT_TO_SCREEN_PADDING = 100
-export const DOUBLE_CLICK_DURATION = 450
+export const DOUBLE_CLICK_DURATION = 300
 export const CARDINAL_DIRECTIONS = ['north', 'east', 'south', 'west']
 export const NOOP = () => void null
 const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)

+ 13 - 0
tldraw/yarn.lock

@@ -1512,6 +1512,19 @@
   dependencies:
     "@babel/runtime" "^7.13.10"
 
+"@radix-ui/react-context-menu@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-1.0.0.tgz#49f2ac5faafc8124add8069a39eab603b2437ba8"
+  integrity sha512-JkwOgdXwErwEEpsmgu0Ob8zD3gzWS1brPXnNGPyZEtR6/EYyDgruQYKiihXVsCrPCdrNUHawop9I1+6JTdXPTA==
+  dependencies:
+    "@babel/runtime" "^7.13.10"
+    "@radix-ui/primitive" "1.0.0"
+    "@radix-ui/react-context" "1.0.0"
+    "@radix-ui/react-menu" "1.0.0"
+    "@radix-ui/react-primitive" "1.0.0"
+    "@radix-ui/react-use-callback-ref" "1.0.0"
+    "@radix-ui/react-use-controllable-state" "1.0.0"
+
 "@radix-ui/[email protected]":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"