Просмотр исходного кода

Enhance (Whiteboards): UX (#8797)

- Improve zooming performance (try zooming using the scrollbar or the trackpad on a portal heavy board)
- Auto remove arrows when we delete connected shapes (see video)
- Create new portals when we Alt+Click on refs in portal elements
- Enable development toolbar within Logseq
- Clear brush (select) area on exit to avoid the leftover when we double click and drag at the same time (see image below)
---------

Co-authored-by: Tienson Qin <[email protected]>
Konstantinos 2 лет назад
Родитель
Сommit
2c987d0c6a

+ 52 - 14
e2e-tests/whiteboards.spec.ts

@@ -77,15 +77,11 @@ test('draw a rectangle', async ({ page }) => {
   await page.mouse.move(bounds.x + 5, bounds.y + 5)
   await page.mouse.down()
 
-  await page.mouse.move(
-    bounds.x + bounds.width / 2,
-    bounds.y + bounds.height / 2
-  )
+  await page.mouse.move(bounds.x + 50, bounds.y + 50 )
   await page.mouse.up()
+  await page.keyboard.press('Escape')
 
-  await expect(
-    page.locator('.logseq-tldraw .tl-positioned-svg rect')
-  ).not.toHaveCount(0)
+  await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
 })
 
 test('undo the rectangle action', async ({ page }) => {
@@ -94,6 +90,54 @@ test('undo the rectangle action', async ({ page }) => {
   await expect(page.locator('.logseq-tldraw .tl-positioned-svg rect')).toHaveCount(0)
 })
 
+test('redo the rectangle action', async ({ page }) => {
+  await page.keyboard.press(modKey + '+Shift+z')
+
+  await page.keyboard.press('Escape')
+  await page.waitForTimeout(100)
+
+  await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
+})
+
+test('clone the rectangle', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  const bounds = (await canvas.boundingBox())!
+
+  await page.mouse.move(bounds.x + 20, bounds.y + 20, {steps: 5})
+
+  await page.keyboard.down('Alt')
+  await page.mouse.down()
+
+  await page.mouse.move(bounds.x + 100, bounds.y + 100, {steps: 5})
+  await page.mouse.up()
+  await page.keyboard.up('Alt')
+
+  await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
+})
+
+test('connect rectangles with an arrow', async ({ page }) => {
+  const canvas = await page.waitForSelector('.logseq-tldraw')
+  const bounds = (await canvas.boundingBox())!
+
+  await page.keyboard.press('c')
+
+  await page.mouse.move(bounds.x + 20, bounds.y + 20)
+  await page.mouse.down()
+
+  await page.mouse.move(bounds.x + 100, bounds.y + 100, {steps: 5}) // will fail without steps
+  await page.mouse.up()
+  await page.keyboard.press('Escape')
+
+
+  await expect(page.locator('.logseq-tldraw .tl-line-container')).toHaveCount(1)
+})
+
+test('cleanup the shapes', async ({ page }) => {
+  await page.keyboard.press(`${modKey}+a`)
+  await page.keyboard.press('Delete')
+  await expect(page.locator('[data-type=Shape]')).toHaveCount(0)
+})
+
 test('create a block', async ({ page }) => {
   const canvas = await page.waitForSelector('.logseq-tldraw')
   const bounds = (await canvas.boundingBox())!
@@ -183,13 +227,7 @@ test('copy/paste youtube video url to create a Youtube shape', async ({ page })
 
   await page.keyboard.press(modKey + '+v')
 
-  await expect( page.locator('.logseq-tldraw .tl-youtube-container')).toHaveCount(1)
-})
-
-test('cleanup the shapes', async ({ page }) => {
-  await page.keyboard.press(`${modKey}+a`)
-  await page.keyboard.press('Delete')
-  await expect(page.locator('[data-type=Shape]')).toHaveCount(0)
+  await expect(page.locator('.logseq-tldraw .tl-youtube-container')).toHaveCount(1)
 })
 
 test('zoom in', async ({ page }) => {

+ 10 - 2
src/main/frontend/components/block.cljs

@@ -511,6 +511,11 @@
          (:db/id page-entity)
          :page))
 
+      (and (util/meta-key? e) (whiteboard-handler/inside-portal? (.-target e)))
+      (whiteboard-handler/add-new-block-portal-shape!
+       page-name
+       (whiteboard-handler/closest-shape (.-target e)))
+
       whiteboard-page?
       (route-handler/redirect-to-whiteboard! page-name)
 
@@ -884,7 +889,7 @@
                      (:db/id block)
                      :block-ref)
 
-                    (whiteboard-handler/inside-portal? (.-target e))
+                    (and (util/meta-key? e) (whiteboard-handler/inside-portal? (.-target e)))
                     (whiteboard-handler/add-new-block-portal-shape!
                      (:block/uuid block)
                      (whiteboard-handler/closest-shape (.-target e)))
@@ -2151,7 +2156,10 @@
           button (gobj/get e "buttons")
           shift? (gobj/get e "shiftKey")
           meta? (util/meta-key? e)]
-      (if (and meta? (not (state/get-edit-input-id)))
+      (if (and meta?
+               (not (state/get-edit-input-id))
+               (not (dom/has-class? target "page-ref"))
+               (not= "A" (gobj/get target "tagName")))
         (do
           (util/stop e)
           (state/conj-selection-block! (gdom/getElement block-id) :down)

+ 3 - 4
tldraw/apps/tldraw-logseq/src/components/AppUI.tsx

@@ -3,14 +3,13 @@ import { ActionBar } from './ActionBar'
 import { DevTools } from './Devtools'
 import { PrimaryTools } from './PrimaryTools'
 import { StatusBar } from './StatusBar'
-
-const isDev = process.env.NODE_ENV === 'development'
+import { isDev } from '@tldraw/core'
 
 export const AppUI = observer(function AppUI() {
   return (
     <>
-      {isDev && <StatusBar />}
-      {isDev && <DevTools />}
+      {isDev() && <StatusBar />}
+      {isDev() && <DevTools />}
       <PrimaryTools />
       <ActionBar />
     </>

+ 2 - 5
tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx

@@ -1,5 +1,5 @@
 import { useApp } from '@tldraw/react'
-import { MOD_KEY, AlignType, DistributeType } from '@tldraw/core'
+import { MOD_KEY, AlignType, DistributeType, isDev } from '@tldraw/core'
 import { observer } from 'mobx-react-lite'
 import { TablerIcon } from '../icons'
 import { Button } from '../Button'
@@ -27,10 +27,7 @@ export const ContextMenu = observer(function ContextMenu({
   }
 
   const developerMode = React.useMemo(() => {
-    return (
-      window?.logseq?.api?.get_state_from_store?.('ui/developer-mode?') ||
-      process.env.NODE_ENV === 'development'
-    )
+    return isDev()
   }, [])
 
   return (

+ 0 - 1
tldraw/apps/tldraw-logseq/src/components/Devtools/Devtools.tsx

@@ -112,7 +112,6 @@ export const DevTools = observer(() => {
     <>
       {originPoint}
       {rendererStatus}
-      <HistoryStack />
     </>
   )
 })

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/BoxShape.tsx

@@ -98,7 +98,7 @@ export class BoxShape extends TLBoxShape<BoxShapeProps> {
       )
 
       return (
-        <div {...events} style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
+        <div {...events} style={{ width: '100%', height: '100%', overflow: 'hidden' }}  className="tl-box-container">
           <TextLabel
             font={font}
             text={label}

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/shapes/LineShape.tsx

@@ -99,7 +99,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
       [label]
     )
     return (
-      <div {...events} style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
+      <div {...events} style={{ width: '100%', height: '100%', overflow: 'hidden' }} className="tl-line-container">
         <TextLabel
           font={font}
           text={label}

+ 2 - 4
tldraw/apps/tldraw-logseq/src/styles.css

@@ -243,17 +243,15 @@ html[data-theme='light'] {
 }
 
 .tl-statusbar {
-  @apply fixed flex items-center w-full bottom-0;
+  @apply absolute flex items-center w-full bottom-0;
 
+  background: var(--ls-secondary-background-color);
   font-family: monospace;
   font-size: 10px;
   grid-row: 3;
   padding: 8px;
-  color: black;
   z-index: 100000;
   user-select: none;
-  background: white;
-  border-top: 1px solid var(--ls-secondary-border-color);
   flex-shrink: 0;
   height: 32px;
 }

+ 17 - 0
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -368,6 +368,23 @@ export class TLApp<
       }
     })
 
+    const deleteBinding = (shapeA: string, shapeB: string) => {
+      if ([...ids].includes(shapeA) && this.getShapeById(shapeB)?.type === "line")
+        ids.add(shapeB)
+    }
+
+     this.currentPage.shapes
+      .flatMap(s => Object.values(s.props.handles ?? {}))
+      .flatMap(h => h.bindingId)
+      .filter(isNonNullable)
+      .map(binding => {
+        const toId = this.currentPage.bindings[binding]?.toId
+        const fromId = this.currentPage.bindings[binding]?.fromId
+        if (toId && fromId) {
+          deleteBinding(toId, fromId)
+          deleteBinding(fromId, toId)
+        }})
+
     const allShapesToDelete = [...ids].map(id => this.getShapeById(id)!)
 
     this.setSelectedShapes(this.selectedShapesArray.filter(shape => !ids.has(shape.id)))

+ 1 - 0
tldraw/packages/core/src/lib/tools/TLSelectTool/states/BrushingState.ts

@@ -30,6 +30,7 @@ export class BrushingState<
   onExit = () => {
     this.initialSelectedIds = []
     this.tree.clear()
+    this.app.setBrush(undefined)
   }
 
   onPointerMove: TLEvents<S>['pointer'] = () => {

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

@@ -81,6 +81,10 @@ export function isDarwin(): boolean {
   return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
 }
 
+export function isDev():boolean {
+  return window?.logseq?.api?.get_state_from_store?.('ui/developer-mode?') || process.env.NODE_ENV === 'development'
+}
+
 /**
  * Migrated from frontend.util/safari?
  */

+ 9 - 3
tldraw/packages/react/src/hooks/useZoom.ts

@@ -2,16 +2,22 @@ import { autorun } from 'mobx'
 import * as React from 'react'
 import { useApp } from './useApp'
 import { useRendererContext } from './useRendererContext'
+import { debounce } from '@tldraw/core'
 
 export function useZoom(ref: React.RefObject<HTMLDivElement>) {
   const { viewport } = useRendererContext()
   const app = useApp()
+
   React.useLayoutEffect(() => {
     return autorun(() => {
-      const zoom = viewport.camera.zoom
-      if (app.inputs.state !== 'pinching' && zoom != null) {
-        ref.current?.style.setProperty('--tl-zoom', zoom.toString())
+      const debouncedZoom = debounce(() => {
+        ref.current?.style.setProperty('--tl-zoom', viewport.camera.zoom.toString())
+      }, 200);
+
+      if (app.inputs.state !== 'pinching' && viewport.camera.zoom != null) {
+        debouncedZoom()
       }
     })
   }, [])
+
 }