mirror of https://github.com/logseq/logseq
wip: double click to create logseq page/block
parent
e8f7248b01
commit
55c0c5681e
|
@ -130,9 +130,6 @@ const _ContextBar: TLContextBarComponent<Shape> = ({ shapes, offsets }) => {
|
|||
) : null}
|
||||
</>
|
||||
)}
|
||||
<a className="shape-link" onClick={() => app.pubEvent('whiteboard-link', shapes)}>
|
||||
Link
|
||||
</a>
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
// TODO: provide "frontend.components.page/page" component?
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
|
||||
import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
|
||||
import { makeObservable, transaction } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import * as React from 'react'
|
||||
import { TextInput } from '~components/inputs/TextInput'
|
||||
|
@ -16,41 +15,20 @@ export interface LogseqPortalShapeProps extends TLBoxShapeProps, CustomStyleProp
|
|||
pageId: string // page name or UUID
|
||||
}
|
||||
|
||||
export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
|
||||
static id = 'logseq-portal'
|
||||
interface LogseqQuickSearchProps {
|
||||
onChange: (id: string) => void
|
||||
}
|
||||
|
||||
static defaultProps: LogseqPortalShapeProps = {
|
||||
id: 'logseq-portal',
|
||||
type: 'logseq-portal',
|
||||
parentId: 'page',
|
||||
point: [0, 0],
|
||||
size: [600, 320],
|
||||
stroke: '#000000',
|
||||
fill: '#ffffff',
|
||||
strokeWidth: 2,
|
||||
opacity: 1,
|
||||
pageId: '',
|
||||
}
|
||||
|
||||
hideRotateHandle = true
|
||||
canChangeAspectRatio = true
|
||||
canFlip = true
|
||||
canActivate = true
|
||||
canEdit = true
|
||||
|
||||
ReactContextBar = observer(() => {
|
||||
const { pageId } = this.props
|
||||
const [q, setQ] = React.useState(pageId)
|
||||
const LogseqQuickSearch = observer(({ onChange }: LogseqQuickSearchProps) => {
|
||||
const [q, setQ] = React.useState('')
|
||||
const rInput = React.useRef<HTMLInputElement>(null)
|
||||
const { search } = React.useContext(LogseqContext)
|
||||
const app = useApp()
|
||||
|
||||
const secretPrefix = 'œ::'
|
||||
|
||||
const commitChange = React.useCallback((id: string) => {
|
||||
setQ(id)
|
||||
this.update({ pageId: id, size: LogseqPortalShape.defaultProps.size })
|
||||
app.persist()
|
||||
onChange(id)
|
||||
rInput.current?.blur()
|
||||
}, [])
|
||||
|
||||
|
@ -71,6 +49,12 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
|
|||
return null
|
||||
}, [search, q])
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
rInput.current?.focus()
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
|
@ -95,45 +79,67 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
|
|||
</datalist>
|
||||
</>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
ReactComponent = observer(
|
||||
({ events, isEditing, isErasing, isBinding, isActivated }: TLComponentProps) => {
|
||||
export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
|
||||
static id = 'logseq-portal'
|
||||
static smart = true
|
||||
|
||||
static defaultProps: LogseqPortalShapeProps = {
|
||||
id: 'logseq-portal',
|
||||
type: 'logseq-portal',
|
||||
parentId: 'page',
|
||||
point: [0, 0],
|
||||
size: [180, 75],
|
||||
stroke: 'transparent',
|
||||
fill: 'var(--ls-secondary-background-color)',
|
||||
strokeWidth: 2,
|
||||
opacity: 1,
|
||||
pageId: '',
|
||||
}
|
||||
|
||||
hideRotateHandle = true
|
||||
canChangeAspectRatio = true
|
||||
canFlip = true
|
||||
canActivate = true
|
||||
canEdit = true
|
||||
|
||||
constructor(props = {} as Partial<LogseqPortalShapeProps>) {
|
||||
super(props)
|
||||
makeObservable(this)
|
||||
this.draft = true
|
||||
}
|
||||
|
||||
ReactComponent = observer(({ events, isErasing, isActivated }: TLComponentProps) => {
|
||||
const {
|
||||
props: { opacity, pageId, strokeWidth },
|
||||
props: { opacity, pageId, strokeWidth, stroke },
|
||||
} = this
|
||||
|
||||
const app = useApp<Shape>()
|
||||
const isMoving = useCameraMovingRef()
|
||||
const { Page } = React.useContext(LogseqContext)
|
||||
const isSelected = app.selectedIds.has(this.id)
|
||||
const enableTlEvents = () => {
|
||||
return isMoving || isEditing || isSelected || app.selectedTool.id !== 'select'
|
||||
}
|
||||
|
||||
const tlEventsEnabled = isMoving || isSelected || app.selectedTool.id !== 'select'
|
||||
const stop = React.useCallback(
|
||||
e => {
|
||||
if (!enableTlEvents()) {
|
||||
if (!tlEventsEnabled) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
[enableTlEvents]
|
||||
[tlEventsEnabled]
|
||||
)
|
||||
|
||||
if (!Page) {
|
||||
return null
|
||||
}
|
||||
|
||||
let linkButton = null
|
||||
const logseqLink = this.props.logseqLink
|
||||
if (logseqLink) {
|
||||
const f = () => app.pubEvent('whiteboard-go-to-link', logseqLink)
|
||||
linkButton = (
|
||||
<a className="ml-2" onMouseDown={f}>
|
||||
🔗 {logseqLink}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
const commitChange = React.useCallback((id: string) => {
|
||||
transaction(() => {
|
||||
this.update({
|
||||
pageId: id,
|
||||
size: [600, 320],
|
||||
})
|
||||
this.setDraft(false)
|
||||
app.setActivatedShapes([])
|
||||
app.persist()
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<HTMLContainer
|
||||
|
@ -141,71 +147,45 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
|
|||
overflow: 'hidden',
|
||||
pointerEvents: 'all',
|
||||
opacity: isErasing ? 0.2 : opacity,
|
||||
border: `${strokeWidth}px solid`,
|
||||
borderColor: isActivated ? 'var(--tl-selectStroke)' : 'rgb(52, 52, 52)',
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: isBinding ? '0px 0px 0 var(--tl-binding-distance) var(--tl-binding)' : '',
|
||||
backgroundColor: 'var(--ls-primary-background-color)',
|
||||
}}
|
||||
{...events}
|
||||
>
|
||||
{pageId && (
|
||||
<div
|
||||
className="ls-whiteboard-card-header"
|
||||
onWheelCapture={stop}
|
||||
onPointerDown={stop}
|
||||
onPointerUp={stop}
|
||||
style={{
|
||||
height: '32px',
|
||||
width: '100%',
|
||||
background: isActivated ? 'var(--tl-selectStroke)' : '#bbb',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: isActivated ? '#fff' : '#000',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: isActivated ? 'all' : 'none',
|
||||
}}
|
||||
>
|
||||
{pageId}
|
||||
{linkButton}
|
||||
</div>
|
||||
)}
|
||||
{this.draft ? (
|
||||
<LogseqQuickSearch onChange={commitChange} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
overflow: 'auto',
|
||||
overscrollBehavior: 'none',
|
||||
height: pageId ? 'calc(100% - 33px)' : '100%',
|
||||
pointerEvents: isActivated ? 'all' : 'none',
|
||||
userSelect: 'none',
|
||||
boxShadow: isActivated
|
||||
? '0px 0px 0 var(--tl-binding-distance) var(--tl-binding)'
|
||||
: '',
|
||||
opacity: isSelected ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
{pageId ? (
|
||||
<div
|
||||
onWheelCapture={stop}
|
||||
onPointerDown={stop}
|
||||
onPointerUp={stop}
|
||||
style={{ padding: '12px', height: '100%', cursor: 'default' }}
|
||||
>
|
||||
{pageId && Page ? (
|
||||
<div style={{ padding: '12px', height: '100%', cursor: 'default' }}>
|
||||
<Page pageId={pageId} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
fontSize: '24px'
|
||||
}}
|
||||
>
|
||||
LOGSEQ PORTAL PLACEHOLDER
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
ReactIndicator = observer(() => {
|
||||
const {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { TLBoxTool } from '@tldraw/core'
|
||||
import { TLBoxTool, TLDotTool } from '@tldraw/core'
|
||||
import type { TLReactEventMap } from '@tldraw/react'
|
||||
import { Shape, LogseqPortalShape } from '~lib/shapes'
|
||||
|
||||
export class LogseqPortalTool extends TLBoxTool<LogseqPortalShape, Shape, TLReactEventMap> {
|
||||
export class LogseqPortalTool extends TLDotTool<LogseqPortalShape, Shape, TLReactEventMap> {
|
||||
static id = 'logseq-portal'
|
||||
static shortcut = ['i']
|
||||
Shape = LogseqPortalShape
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import 'tldraw-logseq/styles.css'
|
||||
import '../../../public/static/css/common.css'
|
||||
|
||||
import App from './App'
|
||||
|
||||
|
|
|
@ -664,6 +664,15 @@ export class TLApp<
|
|||
|
||||
Shapes = new Map<string, TLShapeConstructor<S>>()
|
||||
|
||||
get SmartShape() {
|
||||
for (const S of this.Shapes.values()) {
|
||||
if (S.smart) {
|
||||
return S
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
registerShapes = (Shapes: TLShapeConstructor<S>[]) => {
|
||||
Shapes.forEach(Shape => this.Shapes.set(Shape.id, Shape))
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
|
|||
shapes: this.shapes.map(shape => toJS(shape.props)),
|
||||
bindings: toJS(this.bindings),
|
||||
nonce: this.nonce,
|
||||
activatedShape: toJS(this.app.activatedIds),
|
||||
}),
|
||||
(curr, prev) => {
|
||||
this.cleanup(curr, prev)
|
||||
|
@ -71,7 +72,8 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
|
|||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
shapes: this.shapes.map(shape => shape.serialized),
|
||||
// @ts-expect-error maybe later
|
||||
shapes: this.shapes.map(shape => shape.serialized).filter(s => !!s),
|
||||
bindings: deepCopy(this.bindings),
|
||||
nonce: this.nonce,
|
||||
}
|
||||
|
@ -178,6 +180,7 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
|
|||
direction === 'horizontal',
|
||||
direction === 'vertical'
|
||||
)
|
||||
if (shape.serialized) {
|
||||
shape.onResize(shape.serialized, {
|
||||
bounds: relativeBounds,
|
||||
center: BoundsUtils.getBoundsCenter(relativeBounds),
|
||||
|
@ -192,6 +195,7 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
|
|||
clip: false,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
})
|
||||
}
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
@ -271,12 +275,17 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
|
|||
}
|
||||
})
|
||||
|
||||
if (!deepEqual(updated, curr)) {
|
||||
// Cleanup inactive drafts
|
||||
const shapesToDelete = this.shapes.filter(s => s.draft && !this.app.activatedShapes.includes(s))
|
||||
|
||||
if (!deepEqual(updated, curr) || shapesToDelete.length) {
|
||||
transaction(() => {
|
||||
this.update({
|
||||
bindings: updated.bindings,
|
||||
})
|
||||
|
||||
this.removeShapes(...shapesToDelete)
|
||||
|
||||
updated.shapes.forEach(shape => {
|
||||
this.getShapeById(shape.id)?.update(shape)
|
||||
})
|
||||
|
|
|
@ -18,6 +18,7 @@ export type TLShapeModel<P extends TLShapeProps = TLShapeProps> = {
|
|||
export interface TLShapeConstructor<S extends TLShape = TLShape> {
|
||||
new (props: any): S
|
||||
id: string
|
||||
smart: boolean
|
||||
}
|
||||
|
||||
export type TLFlag = boolean
|
||||
|
@ -79,6 +80,8 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
|
|||
makeObservable(this)
|
||||
}
|
||||
|
||||
// there should be only one Shape that is smart (created by double click canvas)
|
||||
static smart: boolean
|
||||
static type: string
|
||||
|
||||
@observable props: P
|
||||
|
@ -104,6 +107,8 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
|
|||
|
||||
bindingDistance = BINDING_DISTANCE
|
||||
|
||||
// For smart shape
|
||||
@observable draft = false
|
||||
@observable private isDirty = false
|
||||
@observable private lastSerialized: TLShapeModel<P> | undefined
|
||||
|
||||
|
@ -113,6 +118,10 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
|
|||
return this.props.id
|
||||
}
|
||||
|
||||
@action setDraft(draft: boolean) {
|
||||
this.draft = draft
|
||||
}
|
||||
|
||||
@action setIsDirty(isDirty: boolean) {
|
||||
this.isDirty = isDirty
|
||||
}
|
||||
|
@ -278,8 +287,8 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
|
|||
}
|
||||
|
||||
@computed
|
||||
get serialized(): TLShapeModel<P> {
|
||||
return this.getCachedSerialized()
|
||||
get serialized(): TLShapeModel<P> | null {
|
||||
return this.draft ? null : this.getCachedSerialized()
|
||||
}
|
||||
|
||||
validateProps = (
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { IdleState, CreatingState } from './states'
|
||||
import { TLTool, TLApp, TLShape, TLDotShape, TLDotShapeProps } from '~lib'
|
||||
import { TLApp, TLBoxShape, TLShape, TLTool } from '~lib'
|
||||
import { TLCursor, TLEventMap } from '~types'
|
||||
import { CreatingState, IdleState } from './states'
|
||||
|
||||
export abstract class TLDotTool<
|
||||
T extends TLDotShape = TLDotShape,
|
||||
T extends TLBoxShape = TLBoxShape,
|
||||
S extends TLShape = TLShape,
|
||||
K extends TLEventMap = TLEventMap,
|
||||
R extends TLApp<S, K> = TLApp<S, K>
|
||||
|
@ -17,8 +17,9 @@ export abstract class TLDotTool<
|
|||
cursor = TLCursor.Cross
|
||||
|
||||
abstract Shape: {
|
||||
new (props: Partial<TLDotShapeProps>): T
|
||||
new (props: Partial<T['props']>): T
|
||||
id: string
|
||||
defaultProps: TLDotShapeProps
|
||||
smart: boolean
|
||||
defaultProps: T['props']
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import Vec from '@tldraw/vec'
|
||||
import { TLApp, TLShape, TLToolState, TLDotShape } from '~lib'
|
||||
import { TLApp, TLShape, TLToolState, TLBoxShape } from '~lib'
|
||||
import { uniqueId } from '~utils'
|
||||
import type { TLEventMap, TLStateEvents } from '~types'
|
||||
import type { TLDotTool } from '../TLDotTool'
|
||||
import { transaction } from 'mobx'
|
||||
|
||||
export class CreatingState<
|
||||
S extends TLShape,
|
||||
T extends S & TLDotShape,
|
||||
S extends TLBoxShape,
|
||||
T extends S & TLShape,
|
||||
K extends TLEventMap,
|
||||
R extends TLApp<S, K>,
|
||||
P extends TLDotTool<T, S, K, R>
|
||||
|
@ -19,15 +20,14 @@ export class CreatingState<
|
|||
|
||||
onEnter = () => {
|
||||
const { Shape } = this.tool
|
||||
this.offset = [Shape.defaultProps.radius, Shape.defaultProps.radius]
|
||||
this.offset = [Shape.defaultProps.size[0] / 2, Shape.defaultProps.size[1] / 2]
|
||||
const shape = new Shape({
|
||||
id: uniqueId(),
|
||||
parentId: this.app.currentPage.id,
|
||||
point: Vec.sub(this.app.inputs.originPoint, this.offset),
|
||||
})
|
||||
size: Shape.defaultProps.size,
|
||||
} as any)
|
||||
this.creatingShape = shape
|
||||
this.app.currentPage.addShapes(shape)
|
||||
this.app.setSelectedShapes([shape])
|
||||
}
|
||||
|
||||
onPointerMove: TLStateEvents<S, K>['onPointerMove'] = () => {
|
||||
|
@ -41,7 +41,15 @@ export class CreatingState<
|
|||
onPointerUp: TLStateEvents<S, K>['onPointerUp'] = () => {
|
||||
this.tool.transition('idle')
|
||||
if (this.creatingShape) {
|
||||
this.app.setSelectedShapes([this.creatingShape])
|
||||
const shape = this.creatingShape
|
||||
transaction(() => {
|
||||
this.app.currentPage.addShapes(shape)
|
||||
if (this.tool.Shape.smart && shape.draft) {
|
||||
this.app.setActivatedShapes([shape])
|
||||
} else {
|
||||
this.app.setSelectedShapes([shape])
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!this.app.settings.isToolLocked) {
|
||||
this.app.transition('select')
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Vec } from '@tldraw/vec'
|
||||
import { transaction } from 'mobx'
|
||||
import { TLApp, TLSelectTool, TLShape, TLToolState } from '~lib'
|
||||
import type { TLEventMap, TLEvents } from '~types'
|
||||
import { uniqueId } from '~utils'
|
||||
|
||||
export class PointingCanvasState<
|
||||
S extends TLShape,
|
||||
|
@ -40,7 +42,19 @@ export class PointingCanvasState<
|
|||
this.tool.transition('pinching', { info, event })
|
||||
}
|
||||
|
||||
onDoubleClick: TLEvents<S>['pointer'] = info => {
|
||||
console.log('TODO: bringing up Logseq autocomplete here', info)
|
||||
onDoubleClick: TLEvents<S>['pointer'] = () => {
|
||||
transaction(() => {
|
||||
const Shape = this.app.SmartShape
|
||||
if (Shape) {
|
||||
const shape = new Shape({
|
||||
id: uniqueId(),
|
||||
type: Shape.id,
|
||||
parentId: this.app.currentPage.id,
|
||||
point: [...this.app.inputs.originPoint],
|
||||
})
|
||||
this.app.setActivatedShapes([shape.id])
|
||||
this.app.currentPage.addShapes(shape)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,8 +64,9 @@ export class ResizingState<
|
|||
this.isSingle = selectedShapesArray.length === 1
|
||||
this.selectionRotation = this.isSingle ? selectedShapesArray[0].props.rotation ?? 0 : 0
|
||||
this.initialCommonBounds = { ...selectionBounds }
|
||||
// @ts-expect-error maybe later
|
||||
this.snapshots = Object.fromEntries(
|
||||
selectedShapesArray.map(shape => {
|
||||
selectedShapesArray.filter(s => !s.draft).map(shape => {
|
||||
const bounds = { ...shape.bounds }
|
||||
const [cx, cy] = BoundsUtils.getBoundsCenter(bounds)
|
||||
return [
|
||||
|
|
|
@ -154,7 +154,7 @@ export const Canvas = observer(function Renderer<S extends TLReactShape>({
|
|||
isSelected={true}
|
||||
/>
|
||||
))}
|
||||
{hoveredShape && (
|
||||
{hoveredShape && !hoveredShape.draft && (
|
||||
<Indicator key={'hovered_indicator_' + hoveredShape.id} shape={hoveredShape} />
|
||||
)}
|
||||
{brush && components.Brush && <components.Brush bounds={brush} />}
|
||||
|
|
|
@ -30,6 +30,7 @@ export interface TLComponentProps<M = unknown> extends TLCommonShapeProps<M> {
|
|||
export interface TLReactShapeConstructor<S extends TLReactShape = TLReactShape> {
|
||||
new (props: S['props'] & { type: any }): S
|
||||
id: string
|
||||
smart: boolean
|
||||
}
|
||||
|
||||
export abstract class TLReactShape<P extends TLShapeProps = TLShapeProps, M = any> extends TLShape<
|
||||
|
|
Loading…
Reference in New Issue