[feature] add vertical align to note shape (#1539)
This PR adds vertical align to the note shape. ### Change Type - [x] `minor` — New Feature ### Test Plan 1. Try the vertical align prop on note shapes ### Release Notes - Adds vertical align prop to note shapes
This commit is contained in:
parent
8d1817a3e3
commit
1753190f5c
10 changed files with 130 additions and 67 deletions
|
@ -1701,6 +1701,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
size: "l" | "m" | "s" | "xl";
|
||||
font: "draw" | "mono" | "sans" | "serif";
|
||||
align: "end" | "middle" | "start";
|
||||
verticalAlign: "end" | "middle" | "start";
|
||||
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
|
||||
url: string;
|
||||
text: string;
|
||||
|
@ -1723,6 +1724,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
size: "l" | "m" | "s" | "xl";
|
||||
font: "draw" | "mono" | "sans" | "serif";
|
||||
align: "end" | "middle" | "start";
|
||||
verticalAlign: "end" | "middle" | "start";
|
||||
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
|
||||
url: string;
|
||||
text: string;
|
||||
|
|
|
@ -1075,6 +1075,12 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
child.setAttribute('y', y + labelSize!.y + 'px')
|
||||
})
|
||||
|
||||
const textBgEl = textElm.cloneNode(true) as SVGTextElement
|
||||
textBgEl.setAttribute('stroke-width', '2')
|
||||
textBgEl.setAttribute('fill', colors.background)
|
||||
textBgEl.setAttribute('stroke', colors.background)
|
||||
|
||||
g.appendChild(textBgEl)
|
||||
g.appendChild(textElm)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,11 +15,10 @@ import {
|
|||
import { TLDashType, TLGeoShape } from '@tldraw/tlschema'
|
||||
import { SVGContainer } from '../../../components/SVGContainer'
|
||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||
import { getLegacyOffsetX } from '../../../utils/legacy'
|
||||
import { Editor } from '../../Editor'
|
||||
import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
|
||||
import { TLOnEditEndHandler, TLOnResizeHandler } from '../ShapeUtil'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
|
@ -633,42 +632,24 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
|
||||
if (props.text) {
|
||||
const bounds = this.bounds(shape)
|
||||
const padding = 16
|
||||
|
||||
const opts = {
|
||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
||||
fontFamily: font,
|
||||
textAlign: shape.props.align,
|
||||
padding,
|
||||
verticalTextAlign: shape.props.verticalAlign,
|
||||
lineHeight: TEXT_PROPS.lineHeight,
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 'normal',
|
||||
width: Math.ceil(bounds.width),
|
||||
height: Math.ceil(bounds.height),
|
||||
overflow: 'wrap' as const,
|
||||
offsetX: 0,
|
||||
}
|
||||
|
||||
const spans = this.editor.textMeasure.measureTextSpans(props.text, opts)
|
||||
const offsetX = getLegacyOffsetX(shape.props.align, padding, spans, bounds.width)
|
||||
if (offsetX) {
|
||||
opts.offsetX = offsetX
|
||||
}
|
||||
|
||||
const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
|
||||
const textBgEl = createTextSvgElementFromSpans(this.editor, spans, {
|
||||
...opts,
|
||||
strokeWidth: 2,
|
||||
stroke: colors.background,
|
||||
fill: colors.background,
|
||||
const rootTextElm = getTextLabelSvgElement({
|
||||
editor: this.editor,
|
||||
shape,
|
||||
font,
|
||||
bounds,
|
||||
})
|
||||
|
||||
const textElm = textBgEl.cloneNode(true) as SVGTextElement
|
||||
const textElm = rootTextElm.cloneNode(true) as SVGTextElement
|
||||
textElm.setAttribute('fill', colors.fill[shape.props.labelColor])
|
||||
textElm.setAttribute('stroke', 'none')
|
||||
|
||||
const textBgEl = rootTextElm.cloneNode(true) as SVGTextElement
|
||||
textBgEl.setAttribute('stroke-width', '2')
|
||||
textBgEl.setAttribute('fill', colors.background)
|
||||
textBgEl.setAttribute('stroke', colors.background)
|
||||
|
||||
const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||
groupEl.append(textBgEl)
|
||||
groupEl.append(textElm)
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
|
||||
import { TLNoteShape } from '@tldraw/tlschema'
|
||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||
import { getLegacyOffsetX } from '../../../utils/legacy'
|
||||
import { Editor } from '../../Editor'
|
||||
import { ShapeUtil, TLOnEditEndHandler } from '../ShapeUtil'
|
||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
||||
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { TLExportColors } from '../shared/TLExportColors'
|
||||
|
@ -28,6 +27,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
text: '',
|
||||
font: 'draw',
|
||||
align: 'middle',
|
||||
verticalAlign: 'middle',
|
||||
growY: 0,
|
||||
url: '',
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
const {
|
||||
id,
|
||||
type,
|
||||
props: { color, font, size, align, text },
|
||||
props: { color, font, size, align, text, verticalAlign },
|
||||
} = shape
|
||||
|
||||
const adjustedColor = color === 'black' ? 'yellow' : color
|
||||
|
@ -82,7 +82,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
font={font}
|
||||
size={size}
|
||||
align={align}
|
||||
verticalAlign="middle"
|
||||
verticalAlign={verticalAlign}
|
||||
text={text}
|
||||
labelColor="inherit"
|
||||
wrap
|
||||
|
@ -130,36 +130,15 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
rect2.setAttribute('opacity', '.28')
|
||||
g.appendChild(rect2)
|
||||
|
||||
const PADDING = 17
|
||||
const textElm = getTextLabelSvgElement({
|
||||
editor: this.editor,
|
||||
shape,
|
||||
font,
|
||||
bounds,
|
||||
})
|
||||
|
||||
const opts = {
|
||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
||||
fontFamily: font,
|
||||
textAlign: shape.props.align,
|
||||
verticalTextAlign: 'middle' as const,
|
||||
width: bounds.width - PADDING * 2,
|
||||
height: bounds.height - PADDING * 2,
|
||||
padding: 0,
|
||||
lineHeight: TEXT_PROPS.lineHeight,
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 'normal',
|
||||
overflow: 'wrap' as const,
|
||||
offsetX: 0,
|
||||
}
|
||||
|
||||
const spans = this.editor.textMeasure.measureTextSpans(shape.props.text, opts)
|
||||
|
||||
opts.width = bounds.width
|
||||
const offsetX = getLegacyOffsetX(shape.props.align, PADDING, spans, bounds.width)
|
||||
if (offsetX) {
|
||||
opts.offsetX = offsetX
|
||||
}
|
||||
|
||||
opts.padding = PADDING
|
||||
|
||||
const textElm = createTextSvgElementFromSpans(this.editor, spans, opts)
|
||||
textElm.setAttribute('fill', colors.text)
|
||||
textElm.setAttribute('transform', `translate(0 ${PADDING})`)
|
||||
textElm.setAttribute('stroke', 'none')
|
||||
g.appendChild(textElm)
|
||||
|
||||
return g
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { Box2d } from '@tldraw/primitives'
|
||||
import { TLGeoShape, TLNoteShape } from '@tldraw/tlschema'
|
||||
import { LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||
import { getLegacyOffsetX } from '../../../utils/legacy'
|
||||
import { Editor } from '../../Editor'
|
||||
import { createTextSvgElementFromSpans } from './createTextSvgElementFromSpans'
|
||||
|
||||
export function getTextLabelSvgElement({
|
||||
bounds,
|
||||
editor,
|
||||
font,
|
||||
shape,
|
||||
}: {
|
||||
bounds: Box2d
|
||||
editor: Editor
|
||||
font: string
|
||||
shape: TLGeoShape | TLNoteShape
|
||||
}) {
|
||||
const padding = 16
|
||||
|
||||
const opts = {
|
||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
||||
fontFamily: font,
|
||||
textAlign: shape.props.align,
|
||||
verticalTextAlign: shape.props.verticalAlign,
|
||||
width: Math.ceil(bounds.width),
|
||||
height: Math.ceil(bounds.height),
|
||||
padding: 16,
|
||||
lineHeight: TEXT_PROPS.lineHeight,
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 'normal',
|
||||
overflow: 'wrap' as const,
|
||||
offsetX: 0,
|
||||
}
|
||||
|
||||
const spans = editor.textMeasure.measureTextSpans(shape.props.text, opts)
|
||||
const offsetX = getLegacyOffsetX(shape.props.align, padding, spans, bounds.width)
|
||||
if (offsetX) {
|
||||
opts.offsetX = offsetX
|
||||
}
|
||||
|
||||
const textElm = createTextSvgElementFromSpans(editor, spans, opts)
|
||||
return textElm
|
||||
}
|
|
@ -9,5 +9,15 @@ export class GeoShapeTool extends StateNode {
|
|||
static initial = 'idle'
|
||||
static children = () => [Idle, Pointing]
|
||||
|
||||
styles = ['color', 'opacity', 'dash', 'fill', 'size', 'geo', 'font', 'align'] as TLStyleType[]
|
||||
styles = [
|
||||
'color',
|
||||
'opacity',
|
||||
'dash',
|
||||
'fill',
|
||||
'size',
|
||||
'geo',
|
||||
'font',
|
||||
'align',
|
||||
'verticalAlign',
|
||||
] as TLStyleType[]
|
||||
}
|
||||
|
|
|
@ -8,5 +8,5 @@ export class NoteShapeTool extends StateNode {
|
|||
static initial = 'idle'
|
||||
static children = () => [Idle, Pointing]
|
||||
|
||||
styles = ['color', 'opacity', 'size', 'align', 'font'] as TLStyleType[]
|
||||
styles = ['color', 'opacity', 'size', 'align', 'verticalAlign', 'font'] as TLStyleType[]
|
||||
}
|
||||
|
|
|
@ -96,11 +96,14 @@ exports[`Matches a snapshot: Basic SVG 1`] = `
|
|||
<text
|
||||
alignment-baseline="mathematical"
|
||||
dominant-baseline="mathematical"
|
||||
fill=""
|
||||
font-family=""
|
||||
font-size="22px"
|
||||
font-style="normal"
|
||||
font-weight="normal"
|
||||
line-height="29.700000000000003px"
|
||||
stroke=""
|
||||
stroke-width="2"
|
||||
>
|
||||
<tspan
|
||||
alignment-baseline="mathematical"
|
||||
|
|
|
@ -1029,6 +1029,21 @@ describe('making instance state independent', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Adds NoteShape vertical alignment', () => {
|
||||
const { up, down } = noteShapeMigrations.migrators[4]
|
||||
|
||||
test('up works as expected', () => {
|
||||
expect(up({ props: { color: 'red' } })).toEqual({
|
||||
props: { verticalAlign: 'middle', color: 'red' },
|
||||
})
|
||||
})
|
||||
test('down works as expected', () => {
|
||||
expect(down({ props: { verticalAlign: 'top', color: 'red' } })).toEqual({
|
||||
props: { color: 'red' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||
|
||||
for (const migrator of allMigrators) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle'
|
|||
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
|
||||
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
|
||||
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
|
||||
import { TLVerticalAlignType, verticalAlignValidator } from '../styles/TLVerticalAlignStyle'
|
||||
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
|
||||
|
||||
/** @public */
|
||||
|
@ -13,6 +14,7 @@ export type TLNoteShapeProps = {
|
|||
size: TLSizeType
|
||||
font: TLFontType
|
||||
align: TLAlignType
|
||||
verticalAlign: TLVerticalAlignType
|
||||
opacity: TLOpacityType
|
||||
growY: number
|
||||
url: string
|
||||
|
@ -30,6 +32,7 @@ export const noteShapeValidator: T.Validator<TLNoteShape> = createShapeValidator
|
|||
size: sizeValidator,
|
||||
font: fontValidator,
|
||||
align: alignValidator,
|
||||
verticalAlign: verticalAlignValidator,
|
||||
opacity: opacityValidator,
|
||||
growY: T.positiveNumber,
|
||||
url: T.string,
|
||||
|
@ -41,11 +44,12 @@ const Versions = {
|
|||
AddUrlProp: 1,
|
||||
RemoveJustify: 2,
|
||||
MigrateLegacyAlign: 3,
|
||||
AddVerticalAlign: 4,
|
||||
} as const
|
||||
|
||||
/** @internal */
|
||||
export const noteShapeMigrations = defineMigrations({
|
||||
currentVersion: Versions.MigrateLegacyAlign,
|
||||
currentVersion: Versions.AddVerticalAlign,
|
||||
migrators: {
|
||||
[Versions.AddUrlProp]: {
|
||||
up: (shape) => {
|
||||
|
@ -122,5 +126,24 @@ export const noteShapeMigrations = defineMigrations({
|
|||
}
|
||||
},
|
||||
},
|
||||
[Versions.AddVerticalAlign]: {
|
||||
up: (shape) => {
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (shape) => {
|
||||
const { verticalAlign: _, ...props } = shape.props
|
||||
|
||||
return {
|
||||
...shape,
|
||||
props,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue