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 Styled from './styles';
|
||||||
import PanToolInjector from './pan-tool-injector/component';
|
import PanToolInjector from './pan-tool-injector/component';
|
||||||
import {
|
import {
|
||||||
findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, usePrevious,
|
findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, usePrevious, TextUtil,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { isEqual } from 'radash';
|
import { isEqual } from 'radash';
|
||||||
|
|
||||||
@ -28,6 +28,14 @@ const TOOLBAR_SMALL = 28;
|
|||||||
const TOOLBAR_LARGE = 32;
|
const TOOLBAR_LARGE = 32;
|
||||||
const MOUNTED_RESIZE_DELAY = 1500;
|
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) {
|
export default function Whiteboard(props) {
|
||||||
const {
|
const {
|
||||||
isPresenter,
|
isPresenter,
|
||||||
@ -747,6 +755,24 @@ export default function Whiteboard(props) {
|
|||||||
const onPatch = (e, t, reason) => {
|
const onPatch = (e, t, reason) => {
|
||||||
if (!e?.pageState || !reason) return;
|
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'))) {
|
if (((isPanning || panSelected) && (reason === 'selected' || reason === 'set_hovered_id'))) {
|
||||||
e.patchState(
|
e.patchState(
|
||||||
{
|
{
|
||||||
@ -889,15 +915,16 @@ export default function Whiteboard(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e?.session?.initialShape?.type === 'text' && !shapes[patchedShape.id]) {
|
if (e?.session?.initialShape?.type === 'text' && !shapes[patchedShape.id]) {
|
||||||
// check for maxShapes
|
// Check for maxShapes
|
||||||
const currentShapes = e?.document?.pages[e?.currentPageId]?.shapes;
|
const currentShapes = e?.document?.pages[e?.currentPageId]?.shapes;
|
||||||
const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations;
|
const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations;
|
||||||
if (shapeNumberExceeded) {
|
if (shapeNumberExceeded) {
|
||||||
notifyShapeNumberExceeded(intl, maxNumberOfAnnotations);
|
notifyShapeNumberExceeded(intl, maxNumberOfAnnotations);
|
||||||
e?.cancelSession?.();
|
e?.cancelSession?.();
|
||||||
} else {
|
} else {
|
||||||
patchedShape.userId = currentUser?.userId;
|
const updatedShape = { ...patchedShape, userId: currentUser?.userId };
|
||||||
persistShape(patchedShape, whiteboardId, isModerator);
|
const newShapeWithSize = updateTextShapeSize(updatedShape, TextUtil, fontSizeMap);
|
||||||
|
persistShape(newShapeWithSize, whiteboardId, isModerator);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const diff = {
|
const diff = {
|
||||||
@ -905,6 +932,11 @@ export default function Whiteboard(props) {
|
|||||||
point: patchedShape.point,
|
point: patchedShape.point,
|
||||||
text: patchedShape.text,
|
text: patchedShape.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (patchedShape.type === 'text') {
|
||||||
|
const updatedShape = updateTextShapeSize(patchedShape, TextUtil, fontSizeMap);
|
||||||
|
diff.size = updatedShape.size;
|
||||||
|
}
|
||||||
persistShape(diff, whiteboardId, isModerator);
|
persistShape(diff, whiteboardId, isModerator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -275,11 +275,114 @@ const getTextSize = (text, style, padding) => {
|
|||||||
return [width + padding, height + 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 = {
|
const Utils = {
|
||||||
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize,
|
usePrevious,
|
||||||
|
findRemoved,
|
||||||
|
filterInvalidShapes,
|
||||||
|
mapLanguage,
|
||||||
|
sendShapeChanges,
|
||||||
|
getTextSize,
|
||||||
|
TextUtil
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Utils;
|
export default Utils;
|
||||||
export {
|
export {
|
||||||
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize,
|
usePrevious,
|
||||||
|
findRemoved,
|
||||||
|
filterInvalidShapes,
|
||||||
|
mapLanguage,
|
||||||
|
sendShapeChanges,
|
||||||
|
getTextSize,
|
||||||
|
TextUtil
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user