[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";
|
size: "l" | "m" | "s" | "xl";
|
||||||
font: "draw" | "mono" | "sans" | "serif";
|
font: "draw" | "mono" | "sans" | "serif";
|
||||||
align: "end" | "middle" | "start";
|
align: "end" | "middle" | "start";
|
||||||
|
verticalAlign: "end" | "middle" | "start";
|
||||||
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
|
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
|
||||||
url: string;
|
url: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -1723,6 +1724,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
size: "l" | "m" | "s" | "xl";
|
size: "l" | "m" | "s" | "xl";
|
||||||
font: "draw" | "mono" | "sans" | "serif";
|
font: "draw" | "mono" | "sans" | "serif";
|
||||||
align: "end" | "middle" | "start";
|
align: "end" | "middle" | "start";
|
||||||
|
verticalAlign: "end" | "middle" | "start";
|
||||||
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
|
opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1";
|
||||||
url: string;
|
url: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
|
|
@ -1075,6 +1075,12 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
child.setAttribute('y', y + labelSize!.y + 'px')
|
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)
|
g.appendChild(textElm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,10 @@ import {
|
||||||
import { TLDashType, TLGeoShape } from '@tldraw/tlschema'
|
import { TLDashType, TLGeoShape } from '@tldraw/tlschema'
|
||||||
import { SVGContainer } from '../../../components/SVGContainer'
|
import { SVGContainer } from '../../../components/SVGContainer'
|
||||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||||
import { getLegacyOffsetX } from '../../../utils/legacy'
|
|
||||||
import { Editor } from '../../Editor'
|
import { Editor } from '../../Editor'
|
||||||
import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
|
import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
|
||||||
import { TLOnEditEndHandler, TLOnResizeHandler } from '../ShapeUtil'
|
import { TLOnEditEndHandler, TLOnResizeHandler } from '../ShapeUtil'
|
||||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
|
||||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||||
import { TextLabel } from '../shared/TextLabel'
|
import { TextLabel } from '../shared/TextLabel'
|
||||||
import { TLExportColors } from '../shared/TLExportColors'
|
import { TLExportColors } from '../shared/TLExportColors'
|
||||||
|
@ -633,42 +632,24 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
|
|
||||||
if (props.text) {
|
if (props.text) {
|
||||||
const bounds = this.bounds(shape)
|
const bounds = this.bounds(shape)
|
||||||
const padding = 16
|
|
||||||
|
|
||||||
const opts = {
|
const rootTextElm = getTextLabelSvgElement({
|
||||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
editor: this.editor,
|
||||||
fontFamily: font,
|
shape,
|
||||||
textAlign: shape.props.align,
|
font,
|
||||||
padding,
|
bounds,
|
||||||
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 textElm = textBgEl.cloneNode(true) as SVGTextElement
|
const textElm = rootTextElm.cloneNode(true) as SVGTextElement
|
||||||
textElm.setAttribute('fill', colors.fill[shape.props.labelColor])
|
textElm.setAttribute('fill', colors.fill[shape.props.labelColor])
|
||||||
textElm.setAttribute('stroke', 'none')
|
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(textBgEl)
|
||||||
groupEl.append(textElm)
|
groupEl.append(textElm)
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
|
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
|
||||||
import { TLNoteShape } from '@tldraw/tlschema'
|
import { TLNoteShape } from '@tldraw/tlschema'
|
||||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
|
||||||
import { getLegacyOffsetX } from '../../../utils/legacy'
|
|
||||||
import { Editor } from '../../Editor'
|
import { Editor } from '../../Editor'
|
||||||
import { ShapeUtil, TLOnEditEndHandler } from '../ShapeUtil'
|
import { ShapeUtil, TLOnEditEndHandler } from '../ShapeUtil'
|
||||||
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
|
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
|
||||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||||
import { TextLabel } from '../shared/TextLabel'
|
import { TextLabel } from '../shared/TextLabel'
|
||||||
import { TLExportColors } from '../shared/TLExportColors'
|
import { TLExportColors } from '../shared/TLExportColors'
|
||||||
|
@ -28,6 +27,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
text: '',
|
text: '',
|
||||||
font: 'draw',
|
font: 'draw',
|
||||||
align: 'middle',
|
align: 'middle',
|
||||||
|
verticalAlign: 'middle',
|
||||||
growY: 0,
|
growY: 0,
|
||||||
url: '',
|
url: '',
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
props: { color, font, size, align, text },
|
props: { color, font, size, align, text, verticalAlign },
|
||||||
} = shape
|
} = shape
|
||||||
|
|
||||||
const adjustedColor = color === 'black' ? 'yellow' : color
|
const adjustedColor = color === 'black' ? 'yellow' : color
|
||||||
|
@ -82,7 +82,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
font={font}
|
font={font}
|
||||||
size={size}
|
size={size}
|
||||||
align={align}
|
align={align}
|
||||||
verticalAlign="middle"
|
verticalAlign={verticalAlign}
|
||||||
text={text}
|
text={text}
|
||||||
labelColor="inherit"
|
labelColor="inherit"
|
||||||
wrap
|
wrap
|
||||||
|
@ -130,36 +130,15 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
rect2.setAttribute('opacity', '.28')
|
rect2.setAttribute('opacity', '.28')
|
||||||
g.appendChild(rect2)
|
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('fill', colors.text)
|
||||||
textElm.setAttribute('transform', `translate(0 ${PADDING})`)
|
textElm.setAttribute('stroke', 'none')
|
||||||
g.appendChild(textElm)
|
g.appendChild(textElm)
|
||||||
|
|
||||||
return g
|
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 initial = 'idle'
|
||||||
static children = () => [Idle, Pointing]
|
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 initial = 'idle'
|
||||||
static children = () => [Idle, Pointing]
|
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
|
<text
|
||||||
alignment-baseline="mathematical"
|
alignment-baseline="mathematical"
|
||||||
dominant-baseline="mathematical"
|
dominant-baseline="mathematical"
|
||||||
|
fill=""
|
||||||
font-family=""
|
font-family=""
|
||||||
font-size="22px"
|
font-size="22px"
|
||||||
font-style="normal"
|
font-style="normal"
|
||||||
font-weight="normal"
|
font-weight="normal"
|
||||||
line-height="29.700000000000003px"
|
line-height="29.700000000000003px"
|
||||||
|
stroke=""
|
||||||
|
stroke-width="2"
|
||||||
>
|
>
|
||||||
<tspan
|
<tspan
|
||||||
alignment-baseline="mathematical"
|
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 --- */
|
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||||
|
|
||||||
for (const migrator of allMigrators) {
|
for (const migrator of allMigrators) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle'
|
||||||
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
|
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
|
||||||
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
|
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
|
||||||
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
|
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
|
||||||
|
import { TLVerticalAlignType, verticalAlignValidator } from '../styles/TLVerticalAlignStyle'
|
||||||
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
|
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -13,6 +14,7 @@ export type TLNoteShapeProps = {
|
||||||
size: TLSizeType
|
size: TLSizeType
|
||||||
font: TLFontType
|
font: TLFontType
|
||||||
align: TLAlignType
|
align: TLAlignType
|
||||||
|
verticalAlign: TLVerticalAlignType
|
||||||
opacity: TLOpacityType
|
opacity: TLOpacityType
|
||||||
growY: number
|
growY: number
|
||||||
url: string
|
url: string
|
||||||
|
@ -30,6 +32,7 @@ export const noteShapeValidator: T.Validator<TLNoteShape> = createShapeValidator
|
||||||
size: sizeValidator,
|
size: sizeValidator,
|
||||||
font: fontValidator,
|
font: fontValidator,
|
||||||
align: alignValidator,
|
align: alignValidator,
|
||||||
|
verticalAlign: verticalAlignValidator,
|
||||||
opacity: opacityValidator,
|
opacity: opacityValidator,
|
||||||
growY: T.positiveNumber,
|
growY: T.positiveNumber,
|
||||||
url: T.string,
|
url: T.string,
|
||||||
|
@ -41,11 +44,12 @@ const Versions = {
|
||||||
AddUrlProp: 1,
|
AddUrlProp: 1,
|
||||||
RemoveJustify: 2,
|
RemoveJustify: 2,
|
||||||
MigrateLegacyAlign: 3,
|
MigrateLegacyAlign: 3,
|
||||||
|
AddVerticalAlign: 4,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const noteShapeMigrations = defineMigrations({
|
export const noteShapeMigrations = defineMigrations({
|
||||||
currentVersion: Versions.MigrateLegacyAlign,
|
currentVersion: Versions.AddVerticalAlign,
|
||||||
migrators: {
|
migrators: {
|
||||||
[Versions.AddUrlProp]: {
|
[Versions.AddUrlProp]: {
|
||||||
up: (shape) => {
|
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…
Add table
Add a link
Reference in a new issue