kopia lustrzana https://github.com/Tldraw/Tldraw
333 wiersze
8.5 KiB
TypeScript
333 wiersze
8.5 KiB
TypeScript
import {
|
|
DefaultFontFamilies,
|
|
Editor,
|
|
Rectangle2d,
|
|
ShapeUtil,
|
|
SvgExportContext,
|
|
TLNoteShape,
|
|
TLOnEditEndHandler,
|
|
featureFlags,
|
|
getDefaultColorTheme,
|
|
noteShapeMigrations,
|
|
noteShapeProps,
|
|
toDomPrecision,
|
|
} from '@tldraw/editor'
|
|
import { useState } from 'react'
|
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
|
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
|
import { TextLabel } from '../shared/TextLabel'
|
|
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
|
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
|
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
|
|
import { getRotatedBoxShadow } from '../shared/rotated-box-shadow'
|
|
|
|
const NOTE_SIZE = 200
|
|
|
|
/** @public */
|
|
export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|
static override type = 'note' as const
|
|
static override props = noteShapeProps
|
|
static override migrations = noteShapeMigrations
|
|
|
|
override canEdit = () => true
|
|
override hideResizeHandles = () => true
|
|
override hideSelectionBoundsFg = () => true
|
|
override hideRotateHandle = () => true
|
|
|
|
getDefaultProps(): TLNoteShape['props'] {
|
|
return {
|
|
color: 'black',
|
|
size: 'm',
|
|
text: '',
|
|
font: 'draw',
|
|
align: 'middle',
|
|
verticalAlign: 'middle',
|
|
growY: 0,
|
|
url: '',
|
|
}
|
|
}
|
|
|
|
getHeight(shape: TLNoteShape) {
|
|
return NOTE_SIZE + shape.props.growY
|
|
}
|
|
|
|
getGeometry(shape: TLNoteShape) {
|
|
const height = this.getHeight(shape)
|
|
return new Rectangle2d({ width: NOTE_SIZE, height, isFilled: true })
|
|
}
|
|
|
|
component(shape: TLNoteShape) {
|
|
const {
|
|
id,
|
|
type,
|
|
props: { color, font, size, align, text, verticalAlign },
|
|
} = shape
|
|
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
const theme = useDefaultColorTheme()
|
|
const adjustedColor = color === 'black' ? 'yellow' : color
|
|
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null)
|
|
|
|
// held together with sticky tape for prototype
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
const [isDraggingForRealThough, setIsDraggingForRealThough] = useState(false)
|
|
|
|
const isDragging =
|
|
(this.editor.isIn('select.translating') ||
|
|
(this.editor.isInAny('select.pointing_shape') && isDraggingForRealThough)) &&
|
|
this.editor.getSelectedShapeIds().includes(shape.id)
|
|
|
|
const pageRotation = this.editor.getShapePageTransform(shape)!.rotation()
|
|
|
|
const handlePointerDown = () => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId)
|
|
}
|
|
setIsDraggingForRealThough(false)
|
|
|
|
if (featureFlags.delayedFloatingStickies.get()) {
|
|
setTimeoutId(
|
|
setTimeout(() => {
|
|
setIsDraggingForRealThough(true)
|
|
if (this.editor.isInAny('select.translating', 'select.pointing_shape')) {
|
|
if (featureFlags.bringStickiesToFront.get()) {
|
|
this.editor.bringToFront([shape.id])
|
|
}
|
|
this.editor.updateShape({
|
|
id: shape.id,
|
|
type: shape.type,
|
|
meta: {
|
|
isDragging: true,
|
|
},
|
|
})
|
|
}
|
|
}, 200)
|
|
)
|
|
} else {
|
|
setIsDraggingForRealThough(true)
|
|
if (featureFlags.bringStickiesToFront.get()) {
|
|
this.editor.bringToFront([shape.id])
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
width: NOTE_SIZE,
|
|
height: this.getHeight(shape),
|
|
transform: featureFlags.floatingStickies.get()
|
|
? `scale(${isDragging ? 1 : 1}) translateY(${isDragging ? -5 : 0}px)`
|
|
: '',
|
|
pointerEvents: 'all',
|
|
}}
|
|
onPointerDown={handlePointerDown}
|
|
>
|
|
<div
|
|
className="tl-note__container"
|
|
style={{
|
|
color: theme[adjustedColor].solid,
|
|
backgroundColor: theme[adjustedColor].solid,
|
|
// boxShadow: isDragging
|
|
// ? '0px 6px 6px hsl(0, 0%, 0%, 10%), 0px 6px 9px hsl(0, 0%, 0%, 3%)'
|
|
// : '0px 1px 2px hsl(0, 0%, 0%, 25%), 0px 1px 3px hsl(0, 0%, 0%, 9%)',
|
|
boxShadow: featureFlags.floatingStickies.get()
|
|
? getRotatedBoxShadow(pageRotation, {
|
|
offsetModifier: isDragging ? 3 : 1,
|
|
// spreadModifier: isDragging ? 3 : 1,
|
|
blurModifier: isDragging ? 2 : 1,
|
|
// spreadModifier: 1.5,
|
|
// spreadModifier: isDragging ? 0.5 : 0.5,
|
|
// blurModifier: isDragging ? 1.5 : 1,
|
|
})
|
|
: 'var(--shadow-1)',
|
|
}}
|
|
>
|
|
<div className="tl-note__scrim" />
|
|
<TextLabel
|
|
id={id}
|
|
type={type}
|
|
font={font}
|
|
size={size}
|
|
align={align}
|
|
verticalAlign={verticalAlign}
|
|
text={text}
|
|
labelColor="black"
|
|
wrap
|
|
/>
|
|
</div>
|
|
</div>
|
|
{'url' in shape.props && shape.props.url && (
|
|
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
override onClick = (shape: TLNoteShape) => {
|
|
this.editor.updateShape({
|
|
id: shape.id,
|
|
type: shape.type,
|
|
meta: {
|
|
isDragging: false,
|
|
},
|
|
})
|
|
}
|
|
|
|
override onTranslateStart = (shape: TLNoteShape) => {
|
|
this.editor.bringToFront([shape.id])
|
|
this.editor.updateShape({
|
|
id: shape.id,
|
|
type: shape.type,
|
|
meta: {
|
|
isDragging: true,
|
|
},
|
|
})
|
|
}
|
|
|
|
override onTranslateEnd = (shape: TLNoteShape) => {
|
|
this.editor.updateShape({
|
|
id: shape.id,
|
|
type: shape.type,
|
|
meta: {
|
|
isDragging: false,
|
|
},
|
|
})
|
|
}
|
|
|
|
indicator(shape: TLNoteShape) {
|
|
if (featureFlags.hideStickyIndicator.get()) {
|
|
return null
|
|
}
|
|
|
|
const isDraggingForRealThough = shape.meta?.isDragging
|
|
|
|
const isDragging =
|
|
this.editor.isInAny('select.translating', 'select.pointing_shape') &&
|
|
this.editor.getSelectedShapeIds().includes(shape.id) &&
|
|
(featureFlags.delayedFloatingStickies.get() ? isDraggingForRealThough : true)
|
|
|
|
return (
|
|
<rect
|
|
rx="6"
|
|
width={toDomPrecision(NOTE_SIZE)}
|
|
height={toDomPrecision(this.getHeight(shape))}
|
|
transform={isDragging && featureFlags.floatingStickies.get() ? 'translate(0, -5)' : ''}
|
|
/>
|
|
)
|
|
}
|
|
|
|
override toSvg(shape: TLNoteShape, ctx: SvgExportContext) {
|
|
ctx.addExportDef(getFontDefForExport(shape.props.font))
|
|
const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
|
|
const bounds = this.editor.getShapeGeometry(shape).bounds
|
|
|
|
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
|
|
|
const adjustedColor = shape.props.color === 'black' ? 'yellow' : shape.props.color
|
|
|
|
const rect1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
|
rect1.setAttribute('rx', '10')
|
|
rect1.setAttribute('width', NOTE_SIZE.toString())
|
|
rect1.setAttribute('height', bounds.height.toString())
|
|
rect1.setAttribute('fill', theme[adjustedColor].solid)
|
|
rect1.setAttribute('stroke', theme[adjustedColor].solid)
|
|
rect1.setAttribute('stroke-width', '1')
|
|
g.appendChild(rect1)
|
|
|
|
const rect2 = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
|
rect2.setAttribute('rx', '10')
|
|
rect2.setAttribute('width', NOTE_SIZE.toString())
|
|
rect2.setAttribute('height', bounds.height.toString())
|
|
rect2.setAttribute('fill', theme.background)
|
|
rect2.setAttribute('opacity', '.28')
|
|
g.appendChild(rect2)
|
|
|
|
const textElm = getTextLabelSvgElement({
|
|
editor: this.editor,
|
|
shape,
|
|
font: DefaultFontFamilies[shape.props.font],
|
|
bounds,
|
|
})
|
|
|
|
textElm.setAttribute('fill', theme.text)
|
|
textElm.setAttribute('stroke', 'none')
|
|
g.appendChild(textElm)
|
|
|
|
return g
|
|
}
|
|
|
|
override onBeforeCreate = (next: TLNoteShape) => {
|
|
return getGrowY(this.editor, next, next.props.growY)
|
|
}
|
|
|
|
override onBeforeUpdate = (prev: TLNoteShape, next: TLNoteShape) => {
|
|
if (
|
|
prev.props.text === next.props.text &&
|
|
prev.props.font === next.props.font &&
|
|
prev.props.size === next.props.size
|
|
) {
|
|
return
|
|
}
|
|
|
|
return getGrowY(this.editor, next, prev.props.growY)
|
|
}
|
|
|
|
override onEditEnd: TLOnEditEndHandler<TLNoteShape> = (shape) => {
|
|
const {
|
|
id,
|
|
type,
|
|
props: { text },
|
|
} = shape
|
|
|
|
if (text.trimEnd() !== shape.props.text) {
|
|
this.editor.updateShapes([
|
|
{
|
|
id,
|
|
type,
|
|
props: {
|
|
text: text.trimEnd(),
|
|
},
|
|
},
|
|
])
|
|
}
|
|
}
|
|
}
|
|
|
|
function getGrowY(editor: Editor, shape: TLNoteShape, prevGrowY = 0) {
|
|
const PADDING = 17
|
|
|
|
const nextTextSize = editor.textMeasure.measureText(shape.props.text, {
|
|
...TEXT_PROPS,
|
|
fontFamily: FONT_FAMILIES[shape.props.font],
|
|
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
|
maxWidth: NOTE_SIZE - PADDING * 2,
|
|
})
|
|
|
|
const nextHeight = nextTextSize.h + PADDING * 2
|
|
|
|
let growY: number | null = null
|
|
|
|
if (nextHeight > NOTE_SIZE) {
|
|
growY = nextHeight - NOTE_SIZE
|
|
} else {
|
|
if (prevGrowY) {
|
|
growY = 0
|
|
}
|
|
}
|
|
|
|
if (growY !== null) {
|
|
return {
|
|
...shape,
|
|
props: {
|
|
...shape.props,
|
|
growY,
|
|
},
|
|
}
|
|
}
|
|
}
|