mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
do not center text when not applicable (#1783)
This commit is contained in:
parent
9c89504b6f
commit
cd87bd6901
16 changed files with 418 additions and 321 deletions
|
@ -27,6 +27,7 @@ import {
|
|||
getResizeArrowDirection,
|
||||
getResizeHandlerFromCoords,
|
||||
isNonDeletedElement,
|
||||
updateTextElement,
|
||||
dragSelectedElements,
|
||||
getDragOffsetXY,
|
||||
dragNewElement,
|
||||
|
@ -55,7 +56,11 @@ import Portal from "./Portal";
|
|||
|
||||
import { renderScene } from "../renderer";
|
||||
import { AppState, GestureEvent, Gesture } from "../types";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeleted,
|
||||
} from "../element/types";
|
||||
|
||||
import { distance2d, isPathALoop, getGridPoint } from "../math";
|
||||
|
||||
|
@ -113,6 +118,7 @@ import {
|
|||
EVENT,
|
||||
ENV,
|
||||
CANVAS_ONLY_ACTIONS,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
GRID_SIZE,
|
||||
} from "../constants";
|
||||
import {
|
||||
|
@ -583,7 +589,11 @@ class App extends React.Component<any, AppState> {
|
|||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
}
|
||||
const scrolledOutside = !atLeastOneVisibleElement && elements.length > 0;
|
||||
const scrolledOutside =
|
||||
// hide when editing text
|
||||
this.state.editingElement?.type === "text"
|
||||
? false
|
||||
: !atLeastOneVisibleElement && elements.length > 0;
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside: scrolledOutside });
|
||||
}
|
||||
|
@ -790,6 +800,7 @@ class App extends React.Component<any, AppState> {
|
|||
fontSize: this.state.currentItemFontSize,
|
||||
fontFamily: this.state.currentItemFontFamily,
|
||||
textAlign: this.state.currentItemTextAlign,
|
||||
verticalAlign: DEFAULT_VERTICAL_ALIGN,
|
||||
});
|
||||
|
||||
globalSceneState.replaceAllElements([
|
||||
|
@ -1250,12 +1261,9 @@ class App extends React.Component<any, AppState> {
|
|||
!isLinearElement(selectedElements[0])
|
||||
) {
|
||||
const selectedElement = selectedElements[0];
|
||||
const x = selectedElement.x + selectedElement.width / 2;
|
||||
const y = selectedElement.y + selectedElement.height / 2;
|
||||
|
||||
this.startTextEditing({
|
||||
x: x,
|
||||
y: y,
|
||||
sceneX: selectedElement.x + selectedElement.width / 2,
|
||||
sceneY: selectedElement.y + selectedElement.height / 2,
|
||||
});
|
||||
event.preventDefault();
|
||||
return;
|
||||
|
@ -1346,10 +1354,10 @@ class App extends React.Component<any, AppState> {
|
|||
private handleTextWysiwyg(
|
||||
element: ExcalidrawTextElement,
|
||||
{
|
||||
x,
|
||||
y,
|
||||
isExistingElement = false,
|
||||
}: { x: number; y: number; isExistingElement?: boolean },
|
||||
}: {
|
||||
isExistingElement?: boolean;
|
||||
},
|
||||
) {
|
||||
const resetSelection = () => {
|
||||
this.setState({
|
||||
|
@ -1358,26 +1366,13 @@ class App extends React.Component<any, AppState> {
|
|||
});
|
||||
};
|
||||
|
||||
const deleteElement = () => {
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getElementsIncludingDeleted().map((_element) => {
|
||||
if (_element.id === element.id) {
|
||||
return newElementWith(_element, { isDeleted: true });
|
||||
}
|
||||
return _element;
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
const updateElement = (text: string) => {
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getElementsIncludingDeleted().map((_element) => {
|
||||
if (_element.id === element.id) {
|
||||
return newTextElement({
|
||||
...(_element as ExcalidrawTextElement),
|
||||
x: element.x,
|
||||
y: element.y,
|
||||
if (_element.id === element.id && isTextElement(_element)) {
|
||||
return updateTextElement(_element, {
|
||||
text,
|
||||
isDeleted: !text.trim(),
|
||||
});
|
||||
}
|
||||
return _element;
|
||||
|
@ -1387,22 +1382,18 @@ class App extends React.Component<any, AppState> {
|
|||
|
||||
textWysiwyg({
|
||||
id: element.id,
|
||||
x,
|
||||
y,
|
||||
initText: element.text,
|
||||
strokeColor: element.strokeColor,
|
||||
opacity: element.opacity,
|
||||
fontSize: element.fontSize,
|
||||
fontFamily: element.fontFamily,
|
||||
angle: element.angle,
|
||||
textAlign: element.textAlign,
|
||||
zoom: this.state.zoom,
|
||||
getViewportCoords: (x, y) => {
|
||||
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x, sceneY: y },
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
return [viewportX, viewportY];
|
||||
},
|
||||
onChange: withBatchedUpdates((text) => {
|
||||
if (text) {
|
||||
updateElement(text);
|
||||
} else {
|
||||
deleteElement();
|
||||
}
|
||||
updateElement(text);
|
||||
}),
|
||||
onSubmit: withBatchedUpdates((text) => {
|
||||
updateElement(text);
|
||||
|
@ -1419,7 +1410,7 @@ class App extends React.Component<any, AppState> {
|
|||
resetSelection();
|
||||
}),
|
||||
onCancel: withBatchedUpdates(() => {
|
||||
deleteElement();
|
||||
updateElement("");
|
||||
if (isExistingElement) {
|
||||
history.resumeRecording();
|
||||
}
|
||||
|
@ -1438,20 +1429,11 @@ class App extends React.Component<any, AppState> {
|
|||
updateElement(element.text);
|
||||
}
|
||||
|
||||
private startTextEditing = ({
|
||||
x,
|
||||
y,
|
||||
clientX,
|
||||
clientY,
|
||||
centerIfPossible = true,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
clientX?: number;
|
||||
clientY?: number;
|
||||
centerIfPossible?: boolean;
|
||||
}) => {
|
||||
const elementAtPosition = getElementAtPosition(
|
||||
private getTextElementAtPosition(
|
||||
x: number,
|
||||
y: number,
|
||||
): NonDeleted<ExcalidrawTextElement> | null {
|
||||
const element = getElementAtPosition(
|
||||
globalSceneState.getElements(),
|
||||
this.state,
|
||||
x,
|
||||
|
@ -1459,78 +1441,83 @@ class App extends React.Component<any, AppState> {
|
|||
this.state.zoom,
|
||||
);
|
||||
|
||||
const element =
|
||||
elementAtPosition && isTextElement(elementAtPosition)
|
||||
? elementAtPosition
|
||||
: newTextElement({
|
||||
x: x,
|
||||
y: y,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: "",
|
||||
fontSize: this.state.currentItemFontSize,
|
||||
fontFamily: this.state.currentItemFontFamily,
|
||||
textAlign: this.state.currentItemTextAlign,
|
||||
});
|
||||
if (element && isTextElement(element) && !element.isDeleted) {
|
||||
return element;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setState({ editingElement: element });
|
||||
private startTextEditing = ({
|
||||
sceneX,
|
||||
sceneY,
|
||||
insertAtParentCenter = true,
|
||||
}: {
|
||||
/** X position to insert text at */
|
||||
sceneX: number;
|
||||
/** Y position to insert text at */
|
||||
sceneY: number;
|
||||
/** whether to attempt to insert at element center if applicable */
|
||||
insertAtParentCenter?: boolean;
|
||||
}) => {
|
||||
const existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||
|
||||
let textX = clientX || x;
|
||||
let textY = clientY || y;
|
||||
|
||||
let isExistingTextElement = false;
|
||||
|
||||
if (elementAtPosition && isTextElement(elementAtPosition)) {
|
||||
isExistingTextElement = true;
|
||||
const centerElementX = elementAtPosition.x + elementAtPosition.width / 2;
|
||||
const centerElementY = elementAtPosition.y + elementAtPosition.height / 2;
|
||||
|
||||
const {
|
||||
x: centerElementXInViewport,
|
||||
y: centerElementYInViewport,
|
||||
} = sceneCoordsToViewportCoords(
|
||||
{ sceneX: centerElementX, sceneY: centerElementY },
|
||||
const parentCenterPosition =
|
||||
insertAtParentCenter &&
|
||||
this.getTextWysiwygSnappedToCenterPosition(
|
||||
sceneX,
|
||||
sceneY,
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
textX = centerElementXInViewport;
|
||||
textY = centerElementYInViewport;
|
||||
const element = existingTextElement
|
||||
? existingTextElement
|
||||
: newTextElement({
|
||||
x: parentCenterPosition
|
||||
? parentCenterPosition.elementCenterX
|
||||
: sceneX,
|
||||
y: parentCenterPosition
|
||||
? parentCenterPosition.elementCenterY
|
||||
: sceneY,
|
||||
strokeColor: this.state.currentItemStrokeColor,
|
||||
backgroundColor: this.state.currentItemBackgroundColor,
|
||||
fillStyle: this.state.currentItemFillStyle,
|
||||
strokeWidth: this.state.currentItemStrokeWidth,
|
||||
strokeStyle: this.state.currentItemStrokeStyle,
|
||||
roughness: this.state.currentItemRoughness,
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: "",
|
||||
fontSize: this.state.currentItemFontSize,
|
||||
fontFamily: this.state.currentItemFontFamily,
|
||||
textAlign: parentCenterPosition
|
||||
? "center"
|
||||
: this.state.currentItemTextAlign,
|
||||
verticalAlign: parentCenterPosition
|
||||
? "middle"
|
||||
: DEFAULT_VERTICAL_ALIGN,
|
||||
});
|
||||
|
||||
// x and y will change after calling newTextElement function
|
||||
mutateElement(element, {
|
||||
x: centerElementX,
|
||||
y: centerElementY,
|
||||
});
|
||||
this.setState({ editingElement: element });
|
||||
|
||||
if (existingTextElement) {
|
||||
// if text element is no longer centered to a container, reset
|
||||
// verticalAlign to default because it's currently internal-only
|
||||
if (!parentCenterPosition || element.textAlign !== "center") {
|
||||
mutateElement(element, { verticalAlign: DEFAULT_VERTICAL_ALIGN });
|
||||
}
|
||||
} else {
|
||||
globalSceneState.replaceAllElements([
|
||||
...globalSceneState.getElementsIncludingDeleted(),
|
||||
element,
|
||||
]);
|
||||
|
||||
if (centerIfPossible) {
|
||||
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
|
||||
x,
|
||||
y,
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
if (snappedToCenterPosition) {
|
||||
mutateElement(element, {
|
||||
x: snappedToCenterPosition.elementCenterX,
|
||||
y: snappedToCenterPosition.elementCenterY,
|
||||
});
|
||||
textX = snappedToCenterPosition.wysiwygX;
|
||||
textY = snappedToCenterPosition.wysiwygY;
|
||||
}
|
||||
// case: creating new text not centered to parent elemenent → offset Y
|
||||
// so that the text is centered to cursor position
|
||||
if (!parentCenterPosition) {
|
||||
mutateElement(element, {
|
||||
y: element.y - element.baseline / 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1539,9 +1526,7 @@ class App extends React.Component<any, AppState> {
|
|||
});
|
||||
|
||||
this.handleTextWysiwyg(element, {
|
||||
x: textX,
|
||||
y: textY,
|
||||
isExistingElement: isExistingTextElement,
|
||||
isExistingElement: !!existingTextElement,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1574,7 +1559,7 @@ class App extends React.Component<any, AppState> {
|
|||
|
||||
resetCursor();
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
|
@ -1588,8 +1573,8 @@ class App extends React.Component<any, AppState> {
|
|||
const hitElement = getElementAtPosition(
|
||||
elements,
|
||||
this.state,
|
||||
x,
|
||||
y,
|
||||
sceneX,
|
||||
sceneY,
|
||||
this.state.zoom,
|
||||
);
|
||||
|
||||
|
@ -1616,11 +1601,9 @@ class App extends React.Component<any, AppState> {
|
|||
resetCursor();
|
||||
|
||||
this.startTextEditing({
|
||||
x: x,
|
||||
y: y,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
centerIfPossible: !event.altKey,
|
||||
sceneX,
|
||||
sceneY,
|
||||
insertAtParentCenter: !event.altKey,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -2213,19 +2196,10 @@ class App extends React.Component<any, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
event,
|
||||
this.state,
|
||||
this.canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
|
||||
this.startTextEditing({
|
||||
x: x,
|
||||
y: y,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
centerIfPossible: !event.altKey,
|
||||
sceneX: x,
|
||||
sceneY: y,
|
||||
insertAtParentCenter: !event.altKey,
|
||||
});
|
||||
|
||||
resetCursor();
|
||||
|
@ -2640,7 +2614,12 @@ class App extends React.Component<any, AppState> {
|
|||
resizingElement: null,
|
||||
selectionElement: null,
|
||||
cursorButton: "up",
|
||||
editingElement: multiElement ? this.state.editingElement : null,
|
||||
// text elements are reset on finalize, and resetting on pointerup
|
||||
// may cause issues with double taps
|
||||
editingElement:
|
||||
multiElement || isTextElement(this.state.editingElement)
|
||||
? this.state.editingElement
|
||||
: null,
|
||||
});
|
||||
|
||||
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
|
||||
|
@ -3006,7 +2985,9 @@ class App extends React.Component<any, AppState> {
|
|||
scale: number,
|
||||
) {
|
||||
const elementClickedInside = getElementContainingPosition(
|
||||
globalSceneState.getElementsIncludingDeleted(),
|
||||
globalSceneState
|
||||
.getElementsIncludingDeleted()
|
||||
.filter((element) => !isTextElement(element)),
|
||||
x,
|
||||
y,
|
||||
);
|
||||
|
@ -3022,13 +3003,13 @@ class App extends React.Component<any, AppState> {
|
|||
const isSnappedToCenter =
|
||||
distanceToCenter < TEXT_TO_CENTER_SNAP_THRESHOLD;
|
||||
if (isSnappedToCenter) {
|
||||
const { x: wysiwygX, y: wysiwygY } = sceneCoordsToViewportCoords(
|
||||
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: elementCenterX, sceneY: elementCenterY },
|
||||
state,
|
||||
canvas,
|
||||
scale,
|
||||
);
|
||||
return { wysiwygX, wysiwygY, elementCenterX, elementCenterY };
|
||||
return { viewportX, viewportY, elementCenterX, elementCenterY };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue