Pārlūkot izejas kodu

feat(whiteboard): show shape references

Peng Xiao 2 gadi atpakaļ
vecāks
revīzija
f322160ca6

+ 58 - 35
src/main/frontend/components/whiteboard.cljs

@@ -46,39 +46,53 @@
     (when generate-preview
       (generate-preview tldr))))
 
+;; TODO: use frontend.ui instead of making a new one
 (rum/defc dropdown
-  [label children show? outside-click-hander]
+  [label children show? outside-click-hander portal?]
   (let [[anchor-ref anchor-rect] (use-bounding-client-rect show?)
         [content-ref content-rect] (use-bounding-client-rect show?)
         offset-x (when (and anchor-rect content-rect)
-                   (let [offset-x (+ (* 0.5 (- (.-width anchor-rect) (.-width content-rect)))
-                                     (.-x anchor-rect))
-                         vp-w (.-innerWidth js/window)
-                         right (+ offset-x (.-width content-rect) 16)
-                         offset-x (if (> right vp-w) (- offset-x (- right vp-w)) offset-x)]
-                     offset-x))
+                   (if portal?
+                     (let [offset-x (+ (* 0.5 (- (.-width anchor-rect) (.-width content-rect)))
+                                       (.-x anchor-rect))
+                           vp-w (.-innerWidth js/window)
+                           right (+ offset-x (.-width content-rect) 16)
+                           offset-x (if (> right vp-w) (- offset-x (- right vp-w)) offset-x)]
+                       offset-x)
+                     (* 0.5 (- (.-width anchor-rect) (.-width content-rect)))))
         offset-y (when (and anchor-rect content-rect)
                    (+ (.-y anchor-rect) (.-height anchor-rect) 8))
         click-outside-ref (use-click-outside outside-click-hander)
         [d-open set-d-open] (rum/use-state false)
         _ (rum/use-effect! (fn [] (js/setTimeout #(set-d-open show?) 100))
                            [show?])]
-    [:div.dropdown-anchor {:ref anchor-ref}
+    [:div.inline-block.dropdown-anchor {:ref anchor-ref}
      label
-     (ui/portal
-      [:div.fixed.shadow-lg.color-level.px-2.rounded-lg.transition.md:w-64.lg:w-128.overflow-auto
-       {:ref (juxt content-ref click-outside-ref)
-        :style {:opacity (if d-open 1 0)
-                :pointer-events (if d-open "auto" "none")
-                :transform (str "translateY(" (if d-open 0 10) "px)")
-                :min-height "40px"
-                :max-height "420px"
-                :left offset-x
-                :top offset-y}}
-       (when d-open children)])]))
+     (if portal?
+       ;; FIXME: refactor the following code 
+       (ui/portal
+        [:div.fixed.shadow-lg.color-level.px-2.rounded-lg.transition.md:w-64.lg:w-128.overflow-auto
+         {:ref (juxt content-ref click-outside-ref)
+          :style {:opacity (if d-open 1 0)
+                  :pointer-events (if d-open "auto" "none")
+                  :transform (str "translateY(" (if d-open 0 10) "px)")
+                  :min-height "40px"
+                  :max-height "420px"
+                  :left offset-x
+                  :top offset-y}}
+         (when d-open children)])
+       [:div.absolute.shadow-lg.color-level.px-2.rounded-lg.transition.md:w-64.lg:w-128.overflow-auto
+        {:ref (juxt content-ref click-outside-ref)
+         :style {:opacity (if d-open 1 0)
+                 :pointer-events (if d-open "auto" "none")
+                 :transform (str "translateY(" (if d-open 0 10) "px)")
+                 :min-height "40px"
+                 :max-height "420px"
+                 :left offset-x}}
+        (when d-open children)])]))
 
 (rum/defc dropdown-menu
-  [{:keys [label children classname hover?]}]
+  [{:keys [label children classname hover? portal?]}]
   (let [[open-flag set-open-flag] (rum/use-state 0)
         open? (> open-flag (if hover? 0 1))
         d-open-flag (rum/use-memo #(util/debounce 200 set-open-flag) [])]
@@ -90,22 +104,30 @@
                         (util/stop e)
                         (d-open-flag (fn [o] (if (not= o 2) 2 0))))}
       (if (fn? label) (label open?) label)]
-     children open? #(set-open-flag 0))))
+     children open? #(set-open-flag 0) portal?)))
 
-(rum/defc page-refs-count < rum/static
-  ([page-name classname]
-   (page-refs-count page-name classname nil))
-  ([page-name classname render-fn]
-   (let [page-entity (model/get-page page-name)
+;; TODO: move to frontend.components.reference
+;; TODO: reactivity when ref count change
+(rum/defc references-count < rum/static
+  "Shows a references count for any block or page.
+   When clicked, a dropdown menu will show the reference details"
+  ([page-name-or-uuid classname]
+   (references-count page-name-or-uuid classname nil))
+  ([page-name-or-uuid classname {:keys [render-fn
+                                        hover?
+                                        portal?]
+                                 :or {portal? true}}]
+   (let [page-entity (model/get-page page-name-or-uuid)
          block-uuid (:block/uuid page-entity)
          refs-count (count (:block/_refs page-entity))]
      (when (> refs-count 0)
        (dropdown-menu {:classname classname
                        :label (fn [open?]
-                                [:div.flex.items-center.gap-2
+                                [:div.inline-flex.items-center.gap-2
                                  [:div.open-page-ref-link refs-count]
                                  (when render-fn (render-fn open? refs-count))])
-                       :hover? true
+                       :hover? hover?
+                       :portal? portal?
                        :children (reference/block-linked-references block-uuid)})))))
 
 (defn- get-page-display-name
@@ -151,7 +173,7 @@
     [:div.flex.w-full.opacity-50
      [:div (get-page-human-update-time page-name)]
      [:div.flex-1]
-     (page-refs-count page-name nil)]]
+     (references-count page-name nil {:hover? true})]]
    [:div.p-4.h-64.flex.justify-center
     (tldraw-preview page-name)]])
 
@@ -255,12 +277,13 @@
                         false)]
 
       [:div.whiteboard-page-refs
-       (page-refs-count page-name
-                        "text-md px-3 py-2 cursor-default whiteboard-page-refs-count"
-                        (fn [open? refs-count] [:span.whiteboard-page-refs-count-label
-                                                (if (> refs-count 1) "References" "Reference")
-                                                (ui/icon (if open? "references-hide" "references-show")
-                                                         {:extension? true})]))]]
+       (references-count page-name
+                         "text-md px-3 py-2 cursor-default whiteboard-page-refs-count"
+                         {:hover? true
+                          :render-fn (fn [open? refs-count] [:span.whiteboard-page-refs-count-label
+                                                             (if (> refs-count 1) "References" "Reference")
+                                                             (ui/icon (if open? "references-hide" "references-show")
+                                                                      {:extension? true})])})]]
      (tldraw-app page-name block-id)]))
 
 (rum/defc whiteboard-route

+ 11 - 4
src/main/frontend/extensions/tldraw.cljs

@@ -14,7 +14,8 @@
             [goog.object :as gobj]
             [promesa.core :as p]
             [rum.core :as rum]
-            [frontend.ui :as ui]))
+            [frontend.ui :as ui]
+            [frontend.components.whiteboard :as whiteboard]))
 
 (def tldraw (r/adapt-class (gobj/get TldrawLogseq "App")))
 
@@ -54,12 +55,18 @@
          (when-let [[asset-file-name _ full-file-path] (and (seq res) (first res))]
            (editor-handler/resolve-relative-path (or full-file-path asset-file-name)))))))
 
+(defn references-count
+  [props]
+  (apply whiteboard/references-count
+         (map (fn [k] (js->clj (gobj/get props k) {:keywordize-keys true})) ["id" "className" "options"])))
+
 (def tldraw-renderers {:Page page-cp
                        :Block block-cp
                        :Breadcrumb breadcrumb
-                       :PageNameLink page-name-link})
+                       :PageNameLink page-name-link
+                       :ReferencesCount references-count})
 
-(defn get-tldraw-handlers [name]
+(defn get-tldraw-handlers [current-whiteboard-name]
   {:search search-handler
    :queryBlockByUUID #(clj->js (model/query-block-by-uuid (parse-uuid %)))
    :isWhiteboardPage model/whiteboard-page?
@@ -68,7 +75,7 @@
    :addNewWhiteboard (fn [page-name]
                        (whiteboard-handler/create-new-whiteboard-page! page-name))
    :addNewBlock (fn [content]
-                  (str (whiteboard-handler/add-new-block! name content)))
+                  (str (whiteboard-handler/add-new-block! current-whiteboard-name content)))
    :sidebarAddBlock (fn [uuid type]
                       (state/sidebar-add-block! (state/get-current-repo)
                                                 (:db/id (model/get-page uuid))

+ 3 - 2
src/main/frontend/handler/paste.cljs

@@ -68,8 +68,9 @@
 
 (defn- get-whiteboard-tldr-from-text
   [text]
-  (when-let [matched-text (util/safe-re-find #"<whiteboard-tldr>(.*)</whiteboard-tldr>" text)]
-    (try-parse-as-json (gp-util/safe-decode-uri-component (second matched-text)))))
+  (when-let [matched-text (util/safe-re-find #"<whiteboard-tldr>(.*)</whiteboard-tldr>"
+                                             (gp-util/safe-decode-uri-component text))]
+    (try-parse-as-json (second matched-text))))
 
 (defn- get-whiteboard-shape-refs-text
   [text]

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

@@ -34,10 +34,6 @@ import {
 } from './lib'
 import { LogseqContext, type LogseqContextValue } from './lib/logseq-context'
 
-const components: TLReactComponents<Shape> = {
-  ContextBar: ContextBar,
-}
-
 const tools: TLReactToolConstructor<Shape>[] = [
   BoxTool,
   // DotTool,
@@ -62,9 +58,24 @@ interface LogseqTldrawProps {
   onPersist?: TLReactCallbacks<Shape>['onPersist']
 }
 
+const ReferencesCount: LogseqContextValue['renderers']['ReferencesCount'] = props => {
+  const { renderers } = React.useContext(LogseqContext)
+
+  const options = { 'portal?': false }
+
+  return <renderers.ReferencesCount {...props} options={options} />
+}
+
 const AppImpl = () => {
   const ref = React.useRef<HTMLDivElement>(null)
   const app = useApp()
+  const components = React.useMemo(
+    () => ({
+      ContextBar,
+      ReferencesCount,
+    }),
+    []
+  )
   return (
     <ContextMenu collisionRef={ref}>
       <div ref={ref} className="logseq-tldraw logseq-tldraw-wrapper" data-tlapp={app.uuid}>

+ 3 - 3
tldraw/apps/tldraw-logseq/src/components/ContextBar/ContextBar.tsx

@@ -1,3 +1,4 @@
+import * as Separator from '@radix-ui/react-separator'
 import {
   getContextBarTranslation,
   HTMLContainer,
@@ -5,11 +6,10 @@ import {
   useApp,
 } from '@tldraw/react'
 import { observer } from 'mobx-react-lite'
-import * as Separator from '@radix-ui/react-separator'
 
 import * as React from 'react'
-import type { Shape } from '~lib/shapes'
-import { getContextBarActionsForTypes as getContextBarActionsForShapes } from './contextBarActionFactory'
+import type { Shape } from '../../lib'
+import { getContextBarActionsForShapes } from './contextBarActionFactory'
 
 const _ContextBar: TLContextBarComponent<Shape> = ({ shapes, offsets, hidden }) => {
   const app = useApp()

+ 1 - 1
tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx

@@ -512,7 +512,7 @@ const getContextBarActionTypes = (type: ShapeType) => {
   return (shapeMapping[type] ?? []).filter(isNonNullable)
 }
 
-export const getContextBarActionsForTypes = (shapes: Shape[]) => {
+export const getContextBarActionsForShapes = (shapes: Shape[]) => {
   const types = shapes.map(s => s.props.type)
   const actionTypes = new Set(shapes.length > 0 ? getContextBarActionTypes(types[0]) : [])
   for (let i = 1; i < types.length && actionTypes.size > 0; i++) {

+ 9 - 0
tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts

@@ -20,6 +20,15 @@ export interface LogseqContextValue {
     PageNameLink: React.FC<{
       pageName: string
     }>
+    ReferencesCount: React.FC<{
+      id: string
+      className?: string
+      options?: {
+        'portal?'?: boolean
+        'hover?'?: boolean
+        renderFn?: (open?: boolean, count?: number) => React.ReactNode
+      }
+    }>
   }
   handlers: {
     search: (

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

@@ -42,7 +42,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
     label: '',
   }
 
-  hideSelection = true
+  hideSelection = false
   canEdit = true
 
   ReactComponent = observer(({ events, isErasing, isEditing, onEditingEnd }: TLComponentProps) => {

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

@@ -82,7 +82,7 @@ const LogseqPortalShapeHeader = observer(
     children,
   }: {
     type: 'P' | 'B'
-    fill: string
+    fill?: string
     opacity: number
     children: React.ReactNode
   }) => {

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

@@ -8,7 +8,7 @@ interface ShapeStyles {
   stroke: string
   strokeWidth: number
   strokeType: 'line' | 'dashed'
-  fill: string
+  fill?: string
 }
 
 interface ArrowSvgProps {

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

@@ -319,7 +319,7 @@ button.tl-select-input-trigger {
   }
 
   &[data-state='checked'] {
-    background-color: #4285f4;
+    background-color: var(--color-selectedFill);
     color: #fff;
   }
 }
@@ -987,3 +987,10 @@ html[data-theme='dark'] {
     background-color: var(--ls-tertiary-background-color);
   }
 }
+
+.tl-reference-count-container {
+  @apply inline-flex items-center justify-center p-1;
+  background-color: var(--color-selectedFill);
+  border-top-right-radius: 6px;
+  border-bottom-right-radius: 6px;
+}

+ 5 - 0
tldraw/demo/src/App.jsx

@@ -83,6 +83,10 @@ const PageNameLink = props => {
   )
 }
 
+const ReferencesCount = props => {
+  return <div className="open-page-ref-link rounded bg-gray-400 p-0.5">3</div>
+}
+
 const StatusBarSwitcher = ({ label, onClick }) => {
   const [anchor, setAnchor] = React.useState(null)
   React.useEffect(() => {
@@ -210,6 +214,7 @@ export default function App() {
           Block,
           Breadcrumb,
           PageNameLink,
+          ReferencesCount,
         }}
         handlers={{
           search: searchHandler,

+ 1 - 1
tldraw/package.json

@@ -21,7 +21,7 @@
     "postinstall": "yarn build",
     "dev": "cd demo && yarn dev",
     "fix:style": "yarn run pretty-quick",
-    "pretty-quick": "pretty-quick --pattern 'tldraw/**/*.{js,jsx,ts,tsx,css,html}'"
+    "pretty-quick": "pretty-quick --pattern 'tldraw/**/*.{js,jsx,ts,tsx,html}'"
   },
   "devDependencies": {
     "@types/node": "^17.0.42",

+ 1 - 1
tldraw/packages/core/src/utils/ColorUtils.ts

@@ -1,6 +1,6 @@
 import { Color } from '../types'
 
-export function getComputedColor(color: string, type: string): string {
+export function getComputedColor(color: string | undefined, type: string): string {
   if (Object.values(Color).includes(color as Color) || color == null) {
     return `var(--ls-wb-${type}-color-${color ? color : 'default'})`
   }

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

@@ -22,6 +22,7 @@ import { Container } from '../Container'
 import { ContextBarContainer } from '../ContextBarContainer'
 import { HTMLLayer } from '../HTMLLayer'
 import { Indicator } from '../Indicator'
+import { ReferencesCountContainer } from '../ReferencesCountContainer'
 import { SelectionDetailContainer } from '../SelectionDetailContainer'
 import { Shape } from '../Shape'
 import { SVGContainer } from '../SVGContainer'
@@ -107,6 +108,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
     onlySelectedShape && 'handles' in onlySelectedShape.props ? selectedShapes?.[0] : undefined
   const selectedShapesSet = React.useMemo(() => new Set(selectedShapes || []), [selectedShapes])
   const erasingShapesSet = React.useMemo(() => new Set(erasingShapes || []), [erasingShapes])
+  const singleSelectedShape = selectedShapes?.length === 1 ? selectedShapes[0] : undefined
 
   return (
     <div ref={rContainer} className={`tl-container ${className ?? ''}`}>
@@ -170,6 +172,13 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
                   />
                 </Container>
               )}
+              {showSelection && singleSelectedShape && components.ReferencesCount && (
+                <ReferencesCountContainer
+                  hidden={false}
+                  bounds={singleSelectedShape.bounds}
+                  shape={singleSelectedShape}
+                />
+              )}
               {showHandles && onlySelectedShapeWithHandles && components.Handle && (
                 <Container
                   data-type="onlySelectedShapeWithHandles"
@@ -204,8 +213,8 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
                   key={'context' + selectedShapes.map(shape => shape.id).join('')}
                   shapes={selectedShapes}
                   hidden={!showContextBar}
-                  bounds={selectedShapes.length === 1 ? selectedShapes[0].bounds : selectionBounds}
-                  rotation={selectedShapes.length === 1 ? selectedShapes[0].props.rotation : 0}
+                  bounds={singleSelectedShape ? singleSelectedShape.bounds : selectionBounds}
+                  rotation={singleSelectedShape ? singleSelectedShape.props.rotation : 0}
                 />
               )}
             </>

+ 50 - 0
tldraw/packages/react/src/components/ReferencesCountContainer/ReferencesCountContainer.tsx

@@ -0,0 +1,50 @@
+import type { TLBounds } from '@tldraw/core'
+import { observer } from 'mobx-react-lite'
+import { useRendererContext } from '../../hooks'
+import type { TLReactShape } from '../../lib'
+import { Container } from '../Container'
+import { HTMLContainer } from '../HTMLContainer'
+
+export interface TLReferencesCountContainerProps<S extends TLReactShape> {
+  hidden: boolean
+  bounds: TLBounds
+  shape: S
+}
+
+export const ReferencesCountContainer = observer(function ReferencesCountContainer<
+  S extends TLReactShape
+>({ bounds, hidden, shape }: TLReferencesCountContainerProps<S>) {
+  const {
+    components: { ReferencesCount },
+  } = useRendererContext()
+
+  if (!ReferencesCount) throw Error('Expected a ReferencesCount component.')
+
+  const stop: React.EventHandler<any> = e => e.stopPropagation()
+
+  return (
+    <Container
+      style={{
+        zIndex: 20000,
+      }}
+      bounds={bounds}
+      aria-label="references-count-container"
+    >
+      <HTMLContainer>
+        <span
+          style={{
+            position: 'absolute',
+            left: '100%',
+            pointerEvents: 'all',
+            transformOrigin: 'left top',
+            transform: 'scale(var(--tl-scale)) translateY(8px)',
+          }}
+          onPointerDown={stop}
+          onWheelCapture={stop}
+        >
+          <ReferencesCount className="tl-reference-count-container" id={shape.id} shape={shape} />
+        </span>
+      </HTMLContainer>
+    </Container>
+  )
+})

+ 1 - 0
tldraw/packages/react/src/components/ReferencesCountContainer/index.ts

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

+ 4 - 0
tldraw/packages/react/src/components/Renderer/RendererContext.tsx

@@ -59,6 +59,7 @@ export const RendererContext = observer(function App<S extends TLReactShape>({
       SelectionBackground,
       SelectionDetail,
       SelectionForeground,
+      ...rest
     } = components
 
     return {
@@ -68,6 +69,7 @@ export const RendererContext = observer(function App<S extends TLReactShape>({
       callbacks,
       meta,
       components: {
+        ...rest,
         Brush: Brush === null ? undefined : _Brush,
         ContextBar,
         DirectionIndicator: DirectionIndicator === null ? undefined : _DirectionIndicator,
@@ -90,6 +92,7 @@ export const RendererContext = observer(function App<S extends TLReactShape>({
       SelectionBackground,
       SelectionDetail,
       SelectionForeground,
+      ...rest
     } = components
 
     return autorun(() => {
@@ -100,6 +103,7 @@ export const RendererContext = observer(function App<S extends TLReactShape>({
         callbacks,
         meta,
         components: {
+          ...rest,
           Brush: Brush === null ? undefined : _Brush,
           ContextBar,
           DirectionIndicator: DirectionIndicator === null ? undefined : _DirectionIndicator,

+ 1 - 1
tldraw/packages/react/src/hooks/useStylesheet.ts

@@ -69,7 +69,7 @@ const defaultTheme: TLTheme = {
   accent: 'rgb(255, 0, 0)',
   brushFill: 'var(--ls-scrollbar-background-color, rgba(0, 0, 0, .05))',
   brushStroke: 'var(--ls-scrollbar-thumb-hover-color, rgba(0, 0, 0, .05))',
-  selectStroke: 'rgb(66, 133, 244)',
+  selectStroke: 'var(--color-selectedFill)',
   selectFill: 'rgba(65, 132, 244, 0.05)',
   binding: 'rgba(65, 132, 244, 0.5)',
   background: 'var(--ls-primary-background-color)',

+ 11 - 0
tldraw/packages/react/src/types/component-props.ts

@@ -72,6 +72,16 @@ export type TLHandleComponent<
   H extends TLHandle = TLHandle
 > = (props: TLHandleComponentProps<S, H>) => JSX.Element | null
 
+export interface TLReferencesCountComponentProps<S extends TLReactShape = TLReactShape> {
+  shape: S
+  id: string
+  className?: string
+}
+
+export type TLReferencesCountComponent<S extends TLReactShape = TLReactShape> = (
+  props: TLReferencesCountComponentProps<S>
+) => JSX.Element | null
+
 export interface TLGridProps {
   size: number
 }
@@ -82,6 +92,7 @@ export type TLReactComponents<S extends TLReactShape = TLReactShape> = {
   SelectionBackground?: TLBoundsComponent<S> | null
   SelectionForeground?: TLBoundsComponent<S> | null
   SelectionDetail?: TLSelectionDetailComponent<S> | null
+  ReferencesCount?: TLReferencesCountComponent<S> | null
   DirectionIndicator?: TLDirectionIndicatorComponent<S> | null
   Handle?: TLHandleComponent<S> | null
   ContextBar?: TLContextBarComponent<S> | null