fix: Ensure size estimate is applied to text shapes while typing (#20809)
* ensure size estimate is applied to text shape while typing * add comment and comma
This commit is contained in:
parent
06d1a74cdc
commit
466e328477
@ -16,7 +16,7 @@ import {
|
||||
import Styled from './styles';
|
||||
import PanToolInjector from './pan-tool-injector/component';
|
||||
import {
|
||||
findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, usePrevious,
|
||||
findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, usePrevious, TextUtil,
|
||||
} from './utils';
|
||||
import { isEqual } from 'radash';
|
||||
|
||||
@ -28,6 +28,14 @@ const TOOLBAR_SMALL = 28;
|
||||
const TOOLBAR_LARGE = 32;
|
||||
const MOUNTED_RESIZE_DELAY = 1500;
|
||||
|
||||
const LETTER_SPACING = '-0.03em';
|
||||
const LINE_HEIGHT = 1.2;
|
||||
const fontSizeMap = {
|
||||
small: 12,
|
||||
medium: 16,
|
||||
large: 20
|
||||
};
|
||||
|
||||
export default function Whiteboard(props) {
|
||||
const {
|
||||
isPresenter,
|
||||
@ -747,6 +755,24 @@ export default function Whiteboard(props) {
|
||||
const onPatch = (e, t, reason) => {
|
||||
if (!e?.pageState || !reason) return;
|
||||
|
||||
function updateTextShapeSize(shape, textUtil, fsm) {
|
||||
// Create a new object for estimated shape
|
||||
const estimatedShape = {
|
||||
...shape,
|
||||
style: {
|
||||
...shape.style,
|
||||
fontFamily: shape.style.fontFamily || 'Comic Sans MS, cursive, sans-serif',
|
||||
fontSize: fsm[shape.style.size] || 16,
|
||||
letterSpacing: LETTER_SPACING,
|
||||
lineHeight: LINE_HEIGHT,
|
||||
},
|
||||
};
|
||||
// Calculate and set size
|
||||
const measuredBounds = textUtil.getBoundsEstimate(estimatedShape);
|
||||
// Assign size to a new object to avoid modifying the original shape parameter
|
||||
return { ...shape, size: [measuredBounds.width, measuredBounds.height] };
|
||||
}
|
||||
|
||||
if (((isPanning || panSelected) && (reason === 'selected' || reason === 'set_hovered_id'))) {
|
||||
e.patchState(
|
||||
{
|
||||
@ -889,15 +915,16 @@ export default function Whiteboard(props) {
|
||||
}
|
||||
|
||||
if (e?.session?.initialShape?.type === 'text' && !shapes[patchedShape.id]) {
|
||||
// check for maxShapes
|
||||
// Check for maxShapes
|
||||
const currentShapes = e?.document?.pages[e?.currentPageId]?.shapes;
|
||||
const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations;
|
||||
if (shapeNumberExceeded) {
|
||||
notifyShapeNumberExceeded(intl, maxNumberOfAnnotations);
|
||||
e?.cancelSession?.();
|
||||
} else {
|
||||
patchedShape.userId = currentUser?.userId;
|
||||
persistShape(patchedShape, whiteboardId, isModerator);
|
||||
const updatedShape = { ...patchedShape, userId: currentUser?.userId };
|
||||
const newShapeWithSize = updateTextShapeSize(updatedShape, TextUtil, fontSizeMap);
|
||||
persistShape(newShapeWithSize, whiteboardId, isModerator);
|
||||
}
|
||||
} else {
|
||||
const diff = {
|
||||
@ -905,6 +932,11 @@ export default function Whiteboard(props) {
|
||||
point: patchedShape.point,
|
||||
text: patchedShape.text,
|
||||
};
|
||||
|
||||
if (patchedShape.type === 'text') {
|
||||
const updatedShape = updateTextShapeSize(patchedShape, TextUtil, fontSizeMap);
|
||||
diff.size = updatedShape.size;
|
||||
}
|
||||
persistShape(diff, whiteboardId, isModerator);
|
||||
}
|
||||
}
|
||||
|
@ -275,11 +275,114 @@ const getTextSize = (text, style, padding) => {
|
||||
return [width + padding, height + padding];
|
||||
};
|
||||
|
||||
|
||||
// getBoundsEstimate here is a function inspired by implementations in tldraw-v1:
|
||||
// TextUtil: https://github.com/tldraw/tldraw-v1/blob/f786c38ac0fdce337c4405c11cbfa9223d2ee6dd/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx#L24
|
||||
// getTextSize: https://github.com/tldraw/tldraw-v1/blob/f786c38ac0fdce337c4405c11cbfa9223d2ee6dd/packages/tldraw/src/state/shapes/shared/getTextSize.ts#L6
|
||||
// It calculates the estimated bounds of a text shape based on its content and style.
|
||||
const TextUtil = {
|
||||
getBoundsEstimate(shape) {
|
||||
const { text, style } = shape;
|
||||
const div = document.createElement('div');
|
||||
div.style.position = 'absolute';
|
||||
div.style.visibility = 'hidden';
|
||||
div.style.whiteSpace = 'pre';
|
||||
div.style.fontSize = this.getFontSize(style.size);
|
||||
div.style.fontFamily = this.getFontFamily(style.font);
|
||||
div.style.letterSpacing = style.letterSpacing || 'normal';
|
||||
div.style.lineHeight = style.lineHeight || 'normal';
|
||||
div.textContent = text;
|
||||
document.body.appendChild(div);
|
||||
|
||||
// Calculate the number of lines
|
||||
const numberOfLines = text.split('\n').length;
|
||||
|
||||
// Get the bounding box dimensions
|
||||
const { width } = div.getBoundingClientRect();
|
||||
const lineHeight = parseFloat(getComputedStyle(div).lineHeight);
|
||||
const height = lineHeight * numberOfLines;
|
||||
document.body.removeChild(div);
|
||||
|
||||
// Dynamic padding adjustment based on font size
|
||||
const paddingAdjustment = parseInt(div.style.fontSize, 10) * 0.3;
|
||||
let finalWidth = width + paddingAdjustment;
|
||||
let finalHeight = height + paddingAdjustment;
|
||||
|
||||
// Apply width and height multipliers based on font size and style
|
||||
finalWidth *= this.getWidthMultiplier(style.size, style.font);
|
||||
finalHeight *= this.getHeightMultiplier(style.size, style.font, numberOfLines);
|
||||
|
||||
return {
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
};
|
||||
},
|
||||
|
||||
getFontSize(size) {
|
||||
switch (size) {
|
||||
case 'small': return '12px';
|
||||
case 'medium': return '16px';
|
||||
case 'large': return '20px';
|
||||
default: return '16px';
|
||||
}
|
||||
},
|
||||
|
||||
getFontFamily(font) {
|
||||
switch (font) {
|
||||
case 'mono': return 'monospace';
|
||||
case 'script': return 'Comic Sans MS, cursive, sans-serif';
|
||||
case 'sans': return 'Arial, sans-serif';
|
||||
case 'serif': return 'Times New Roman, serif';
|
||||
default: return 'sans-serif';
|
||||
}
|
||||
},
|
||||
|
||||
getWidthMultiplier(size, font) {
|
||||
const fontMultiplier = {
|
||||
small: { mono: 3, default: 2 },
|
||||
medium: { mono: 4, default: 3 },
|
||||
large: { mono: 7, default: 4 },
|
||||
};
|
||||
|
||||
if (font === 'mono') {
|
||||
return fontMultiplier[size].mono;
|
||||
} else if (['script', 'sans', 'serif'].includes(font)) {
|
||||
return fontMultiplier[size].default;
|
||||
}
|
||||
|
||||
return 1;
|
||||
},
|
||||
|
||||
getHeightMultiplier(size, font, numberOfLines) {
|
||||
const baseMultiplier = 1.5;
|
||||
|
||||
if (font === 'mono') {
|
||||
return baseMultiplier + (numberOfLines > 1 ? 0.5 : 0);
|
||||
} else if (['script', 'sans', 'serif'].includes(font)) {
|
||||
return baseMultiplier + (numberOfLines > 1 ? 0.3 : 0);
|
||||
}
|
||||
|
||||
return baseMultiplier;
|
||||
}
|
||||
};
|
||||
|
||||
const Utils = {
|
||||
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize,
|
||||
usePrevious,
|
||||
findRemoved,
|
||||
filterInvalidShapes,
|
||||
mapLanguage,
|
||||
sendShapeChanges,
|
||||
getTextSize,
|
||||
TextUtil
|
||||
};
|
||||
|
||||
export default Utils;
|
||||
export {
|
||||
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize,
|
||||
usePrevious,
|
||||
findRemoved,
|
||||
filterInvalidShapes,
|
||||
mapLanguage,
|
||||
sendShapeChanges,
|
||||
getTextSize,
|
||||
TextUtil
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user