From 1753190f5c8086d068e915c02325d6bba6db5567 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 6 Jun 2023 14:27:32 +0100 Subject: [PATCH] [feature] add vertical align to note shape (#1539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/editor/api-report.md | 2 + .../ArrowShapeUtil/ArrowShapeUtil.tsx | 6 +++ .../shapeutils/GeoShapeUtil/GeoShapeUtil.tsx | 45 ++++++------------- .../NoteShapeUtil/NoteShapeUtil.tsx | 43 +++++------------- .../shared/getTextLabelSvgElement.ts | 44 ++++++++++++++++++ .../app/tools/GeoShapeTool/GeoShapeTool.ts | 12 ++++- .../app/tools/NoteShapeTool/NoteShapeTool.ts | 2 +- .../__snapshots__/getSvg.test.ts.snap | 3 ++ packages/tlschema/src/migrations.test.ts | 15 +++++++ packages/tlschema/src/shapes/TLNoteShape.ts | 25 ++++++++++- 10 files changed, 130 insertions(+), 67 deletions(-) create mode 100644 packages/editor/src/lib/app/shapeutils/shared/getTextLabelSvgElement.ts diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 28253e231..1e9f93bdb 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -1701,6 +1701,7 @@ export class NoteShapeUtil extends ShapeUtil { 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 { 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; diff --git a/packages/editor/src/lib/app/shapeutils/ArrowShapeUtil/ArrowShapeUtil.tsx b/packages/editor/src/lib/app/shapeutils/ArrowShapeUtil/ArrowShapeUtil.tsx index 381518e9e..80684675c 100644 --- a/packages/editor/src/lib/app/shapeutils/ArrowShapeUtil/ArrowShapeUtil.tsx +++ b/packages/editor/src/lib/app/shapeutils/ArrowShapeUtil/ArrowShapeUtil.tsx @@ -1075,6 +1075,12 @@ export class ArrowShapeUtil extends ShapeUtil { 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) } diff --git a/packages/editor/src/lib/app/shapeutils/GeoShapeUtil/GeoShapeUtil.tsx b/packages/editor/src/lib/app/shapeutils/GeoShapeUtil/GeoShapeUtil.tsx index 938057b37..fdf819220 100644 --- a/packages/editor/src/lib/app/shapeutils/GeoShapeUtil/GeoShapeUtil.tsx +++ b/packages/editor/src/lib/app/shapeutils/GeoShapeUtil/GeoShapeUtil.tsx @@ -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 { 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) diff --git a/packages/editor/src/lib/app/shapeutils/NoteShapeUtil/NoteShapeUtil.tsx b/packages/editor/src/lib/app/shapeutils/NoteShapeUtil/NoteShapeUtil.tsx index d8c6b427f..6fb4e06df 100644 --- a/packages/editor/src/lib/app/shapeutils/NoteShapeUtil/NoteShapeUtil.tsx +++ b/packages/editor/src/lib/app/shapeutils/NoteShapeUtil/NoteShapeUtil.tsx @@ -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 { text: '', font: 'draw', align: 'middle', + verticalAlign: 'middle', growY: 0, url: '', } @@ -54,7 +54,7 @@ export class NoteShapeUtil extends ShapeUtil { 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 { font={font} size={size} align={align} - verticalAlign="middle" + verticalAlign={verticalAlign} text={text} labelColor="inherit" wrap @@ -130,36 +130,15 @@ export class NoteShapeUtil extends ShapeUtil { 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 diff --git a/packages/editor/src/lib/app/shapeutils/shared/getTextLabelSvgElement.ts b/packages/editor/src/lib/app/shapeutils/shared/getTextLabelSvgElement.ts new file mode 100644 index 000000000..48851fd2f --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/shared/getTextLabelSvgElement.ts @@ -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 +} diff --git a/packages/editor/src/lib/app/tools/GeoShapeTool/GeoShapeTool.ts b/packages/editor/src/lib/app/tools/GeoShapeTool/GeoShapeTool.ts index 0d3f9fdde..c8b0b69d3 100644 --- a/packages/editor/src/lib/app/tools/GeoShapeTool/GeoShapeTool.ts +++ b/packages/editor/src/lib/app/tools/GeoShapeTool/GeoShapeTool.ts @@ -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[] } diff --git a/packages/editor/src/lib/app/tools/NoteShapeTool/NoteShapeTool.ts b/packages/editor/src/lib/app/tools/NoteShapeTool/NoteShapeTool.ts index 350608b58..1d7b71b57 100644 --- a/packages/editor/src/lib/app/tools/NoteShapeTool/NoteShapeTool.ts +++ b/packages/editor/src/lib/app/tools/NoteShapeTool/NoteShapeTool.ts @@ -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[] } diff --git a/packages/editor/src/lib/test/commands/__snapshots__/getSvg.test.ts.snap b/packages/editor/src/lib/test/commands/__snapshots__/getSvg.test.ts.snap index d4cfe4618..564c84aca 100644 --- a/packages/editor/src/lib/test/commands/__snapshots__/getSvg.test.ts.snap +++ b/packages/editor/src/lib/test/commands/__snapshots__/getSvg.test.ts.snap @@ -96,11 +96,14 @@ exports[`Matches a snapshot: Basic SVG 1`] = ` { }) }) +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) { diff --git a/packages/tlschema/src/shapes/TLNoteShape.ts b/packages/tlschema/src/shapes/TLNoteShape.ts index 5ac881af0..70caf8250 100644 --- a/packages/tlschema/src/shapes/TLNoteShape.ts +++ b/packages/tlschema/src/shapes/TLNoteShape.ts @@ -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 = 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, + } + }, + }, }, })