feat/ability to change the alignment of the text (#1213)
* feat: add the ability to change the alignement of the text * test: update the snapshots to included the newely textAlign state * style: use explicit key assignment to object * test: add missing new key textAlign to newElement.test.ts * style: make the text on the buttons start with uppercase * Update src/locales/en.json * add types * add migration * remove incorrect update Co-authored-by: Youness Fkhach <younessfkhach@porotonmail.com> Co-authored-by: Lipis <lipiridis@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
3fd6f3023f
commit
ff82d1cfa3
@ -1,5 +1,9 @@
|
||||
import React from "react";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
TextAlign,
|
||||
} from "../element/types";
|
||||
import {
|
||||
getCommonAttributeOfSelectedElements,
|
||||
isSomeElementSelected,
|
||||
@ -361,3 +365,47 @@ export const actionChangeFontFamily = register({
|
||||
</fieldset>
|
||||
),
|
||||
});
|
||||
|
||||
export const actionChangeTextAlign = register({
|
||||
name: "changeTextAlign",
|
||||
perform: (elements, appState, value) => {
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) => {
|
||||
if (isTextElement(el)) {
|
||||
const element: ExcalidrawTextElement = newElementWith(el, {
|
||||
textAlign: value,
|
||||
});
|
||||
redrawTextBoundingBox(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
return el;
|
||||
}),
|
||||
appState: {
|
||||
...appState,
|
||||
currentItemTextAlign: value,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.textAlign")}</legend>
|
||||
<ButtonSelect<TextAlign | false>
|
||||
group="text-align"
|
||||
options={[
|
||||
{ value: "left", text: t("labels.left") },
|
||||
{ value: "center", text: t("labels.center") },
|
||||
{ value: "right", text: t("labels.right") },
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => isTextElement(element) && element.textAlign,
|
||||
appState.currentItemTextAlign,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
</fieldset>
|
||||
),
|
||||
});
|
||||
|
@ -16,6 +16,7 @@ export {
|
||||
actionChangeOpacity,
|
||||
actionChangeFontSize,
|
||||
actionChangeFontFamily,
|
||||
actionChangeTextAlign,
|
||||
} from "./actionProperties";
|
||||
|
||||
export {
|
||||
|
@ -49,6 +49,7 @@ export type ActionName =
|
||||
| "zoomOut"
|
||||
| "resetZoom"
|
||||
| "changeFontFamily"
|
||||
| "changeTextAlign"
|
||||
| "toggleFullScreen"
|
||||
| "toggleShortcuts";
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { getDateTime } from "./utils";
|
||||
import { t } from "./i18n";
|
||||
|
||||
export const DEFAULT_FONT = "20px Virgil";
|
||||
export const DEFAULT_TEXT_ALIGN = "left";
|
||||
|
||||
export function getDefaultAppState(): AppState {
|
||||
return {
|
||||
@ -22,6 +23,7 @@ export function getDefaultAppState(): AppState {
|
||||
currentItemRoughness: 1,
|
||||
currentItemOpacity: 100,
|
||||
currentItemFont: DEFAULT_FONT,
|
||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||
viewBackgroundColor: "#ffffff",
|
||||
scrollX: 0 as FlooredNumber,
|
||||
scrollY: 0 as FlooredNumber,
|
||||
@ -77,6 +79,7 @@ export function clearAppStatePropertiesForHistory(
|
||||
currentItemRoughness: appState.currentItemRoughness,
|
||||
currentItemOpacity: appState.currentItemOpacity,
|
||||
currentItemFont: appState.currentItemFont,
|
||||
currentItemTextAlign: appState.currentItemTextAlign,
|
||||
viewBackgroundColor: appState.viewBackgroundColor,
|
||||
name: appState.name,
|
||||
};
|
||||
|
@ -56,6 +56,8 @@ export function SelectedShapeActions({
|
||||
{renderAction("changeFontSize")}
|
||||
|
||||
{renderAction("changeFontFamily")}
|
||||
|
||||
{renderAction("changeTextAlign")}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -714,6 +714,7 @@ export class App extends React.Component<any, AppState> {
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: text,
|
||||
font: this.state.currentItemFont,
|
||||
textAlign: this.state.currentItemTextAlign,
|
||||
});
|
||||
|
||||
globalSceneState.replaceAllElements([
|
||||
@ -1217,6 +1218,7 @@ export class App extends React.Component<any, AppState> {
|
||||
opacity: element.opacity,
|
||||
font: element.font,
|
||||
angle: element.angle,
|
||||
textAlign: element.textAlign,
|
||||
zoom: this.state.zoom,
|
||||
onChange: withBatchedUpdates((text) => {
|
||||
if (text) {
|
||||
@ -1288,6 +1290,7 @@ export class App extends React.Component<any, AppState> {
|
||||
opacity: this.state.currentItemOpacity,
|
||||
text: "",
|
||||
font: this.state.currentItemFont,
|
||||
textAlign: this.state.currentItemTextAlign,
|
||||
});
|
||||
|
||||
this.setState({ editingElement: element });
|
||||
|
@ -3,9 +3,14 @@ import { Point } from "../types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { DataState } from "./types";
|
||||
import { isInvisiblySmallElement, normalizeDimensions } from "../element";
|
||||
import {
|
||||
isInvisiblySmallElement,
|
||||
normalizeDimensions,
|
||||
isTextElement,
|
||||
} from "../element";
|
||||
import { calculateScrollCenter } from "../scene";
|
||||
import { randomId } from "../random";
|
||||
import { DEFAULT_TEXT_ALIGN } from "../appState";
|
||||
|
||||
export function restore(
|
||||
// we're making the elements mutable for this API because we want to
|
||||
@ -51,6 +56,10 @@ export function restore(
|
||||
}
|
||||
element.points = points;
|
||||
} else {
|
||||
if (isTextElement(element)) {
|
||||
element.textAlign = DEFAULT_TEXT_ALIGN;
|
||||
}
|
||||
|
||||
normalizeDimensions(element);
|
||||
// old spec, where non-linear elements used to have empty points arrays
|
||||
if ("points" in element) {
|
||||
|
@ -77,6 +77,7 @@ it("clones text element", () => {
|
||||
opacity: 100,
|
||||
text: "hello",
|
||||
font: "Arial 20px",
|
||||
textAlign: "left",
|
||||
});
|
||||
|
||||
const copy = duplicateElement(element);
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawGenericElement,
|
||||
NonDeleted,
|
||||
TextAlign,
|
||||
} from "../element/types";
|
||||
import { measureText } from "../utils";
|
||||
import { randomInteger, randomId } from "../random";
|
||||
@ -73,15 +74,16 @@ export function newTextElement(
|
||||
opts: {
|
||||
text: string;
|
||||
font: string;
|
||||
textAlign: TextAlign;
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawTextElement> {
|
||||
const { text, font } = opts;
|
||||
const metrics = measureText(text, font);
|
||||
const metrics = measureText(opts.text, opts.font);
|
||||
const textElement = newElementWith(
|
||||
{
|
||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||
text: text,
|
||||
font: font,
|
||||
text: opts.text,
|
||||
font: opts.font,
|
||||
textAlign: opts.textAlign,
|
||||
// Center the text
|
||||
x: opts.x - metrics.width / 2,
|
||||
y: opts.y - metrics.height / 2,
|
||||
|
@ -21,6 +21,7 @@ type TextWysiwygParams = {
|
||||
opacity: number;
|
||||
zoom: number;
|
||||
angle: number;
|
||||
textAlign: string;
|
||||
onChange?: (text: string) => void;
|
||||
onSubmit: (text: string) => void;
|
||||
onCancel: () => void;
|
||||
@ -36,6 +37,7 @@ export function textWysiwyg({
|
||||
zoom,
|
||||
angle,
|
||||
onChange,
|
||||
textAlign,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: TextWysiwygParams) {
|
||||
@ -59,7 +61,7 @@ export function textWysiwyg({
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
transform: `translate(-50%, -50%) scale(${zoom}) rotate(${degree}deg)`,
|
||||
textAlign: "left",
|
||||
textAlign: textAlign,
|
||||
display: "inline-block",
|
||||
font: font,
|
||||
padding: "4px",
|
||||
|
@ -45,6 +45,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
||||
font: string;
|
||||
text: string;
|
||||
baseline: number;
|
||||
textAlign: TextAlign;
|
||||
}>;
|
||||
|
||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||
@ -55,3 +56,5 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||
}>;
|
||||
|
||||
export type PointerType = "mouse" | "pen" | "touch";
|
||||
|
||||
export type TextAlign = "left" | "center" | "right";
|
||||
|
@ -18,6 +18,7 @@
|
||||
"strokeWidth": "Stroke width",
|
||||
"sloppiness": "Sloppiness",
|
||||
"opacity": "Opacity",
|
||||
"textAlign": "Text align",
|
||||
"fontSize": "Font size",
|
||||
"fontFamily": "Font family",
|
||||
"onlySelected": "Only selected",
|
||||
@ -34,6 +35,9 @@
|
||||
"crossHatch": "Cross-hatch",
|
||||
"thin": "Thin",
|
||||
"bold": "Bold",
|
||||
"left": "Left",
|
||||
"center": "Center",
|
||||
"right": "Right",
|
||||
"extraBold": "Extra bold",
|
||||
"architect": "Architect",
|
||||
"artist": "Artist",
|
||||
|
@ -101,15 +101,28 @@ function drawElementOnCanvas(
|
||||
context.font = element.font;
|
||||
const fillStyle = context.fillStyle;
|
||||
context.fillStyle = element.strokeColor;
|
||||
const textAlign = context.textAlign;
|
||||
context.textAlign = element.textAlign as CanvasTextAlign;
|
||||
// Canvas does not support multiline text by default
|
||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||
const lineHeight = element.height / lines.length;
|
||||
const offset = element.height - element.baseline;
|
||||
const verticalOffset = element.height - element.baseline;
|
||||
const horizontalOffset =
|
||||
element.textAlign === "center"
|
||||
? element.width / 2
|
||||
: element.textAlign === "right"
|
||||
? element.width
|
||||
: 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
context.fillText(lines[i], 0, (i + 1) * lineHeight - offset);
|
||||
context.fillText(
|
||||
lines[i],
|
||||
0 + horizontalOffset,
|
||||
(i + 1) * lineHeight - verticalOffset,
|
||||
);
|
||||
}
|
||||
context.fillStyle = fillStyle;
|
||||
context.font = font;
|
||||
context.textAlign = textAlign;
|
||||
} else {
|
||||
throw new Error(`Unimplemented type ${element.type}`);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ import {
|
||||
ExcalidrawLinearElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
TextAlign,
|
||||
} from "./element/types";
|
||||
import { SHAPES } from "./shapes";
|
||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
@ -30,6 +31,7 @@ export type AppState = {
|
||||
currentItemRoughness: number;
|
||||
currentItemOpacity: number;
|
||||
currentItemFont: string;
|
||||
currentItemTextAlign: TextAlign;
|
||||
viewBackgroundColor: string;
|
||||
scrollX: FlooredNumber;
|
||||
scrollY: FlooredNumber;
|
||||
|
Loading…
x
Reference in New Issue
Block a user