feat(whiteboard): show shape references

feat/wb-linkings
Peng Xiao 2022-11-19 16:21:55 +08:00
parent 962998a7b2
commit f322160ca6
20 changed files with 196 additions and 58 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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