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 React from "react";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
TextAlign,
|
||||||
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
getCommonAttributeOfSelectedElements,
|
getCommonAttributeOfSelectedElements,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
@ -361,3 +365,47 @@ export const actionChangeFontFamily = register({
|
|||||||
</fieldset>
|
</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,
|
actionChangeOpacity,
|
||||||
actionChangeFontSize,
|
actionChangeFontSize,
|
||||||
actionChangeFontFamily,
|
actionChangeFontFamily,
|
||||||
|
actionChangeTextAlign,
|
||||||
} from "./actionProperties";
|
} from "./actionProperties";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -49,6 +49,7 @@ export type ActionName =
|
|||||||
| "zoomOut"
|
| "zoomOut"
|
||||||
| "resetZoom"
|
| "resetZoom"
|
||||||
| "changeFontFamily"
|
| "changeFontFamily"
|
||||||
|
| "changeTextAlign"
|
||||||
| "toggleFullScreen"
|
| "toggleFullScreen"
|
||||||
| "toggleShortcuts";
|
| "toggleShortcuts";
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { getDateTime } from "./utils";
|
|||||||
import { t } from "./i18n";
|
import { t } from "./i18n";
|
||||||
|
|
||||||
export const DEFAULT_FONT = "20px Virgil";
|
export const DEFAULT_FONT = "20px Virgil";
|
||||||
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
|
|
||||||
export function getDefaultAppState(): AppState {
|
export function getDefaultAppState(): AppState {
|
||||||
return {
|
return {
|
||||||
@ -22,6 +23,7 @@ export function getDefaultAppState(): AppState {
|
|||||||
currentItemRoughness: 1,
|
currentItemRoughness: 1,
|
||||||
currentItemOpacity: 100,
|
currentItemOpacity: 100,
|
||||||
currentItemFont: DEFAULT_FONT,
|
currentItemFont: DEFAULT_FONT,
|
||||||
|
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||||
viewBackgroundColor: "#ffffff",
|
viewBackgroundColor: "#ffffff",
|
||||||
scrollX: 0 as FlooredNumber,
|
scrollX: 0 as FlooredNumber,
|
||||||
scrollY: 0 as FlooredNumber,
|
scrollY: 0 as FlooredNumber,
|
||||||
@ -77,6 +79,7 @@ export function clearAppStatePropertiesForHistory(
|
|||||||
currentItemRoughness: appState.currentItemRoughness,
|
currentItemRoughness: appState.currentItemRoughness,
|
||||||
currentItemOpacity: appState.currentItemOpacity,
|
currentItemOpacity: appState.currentItemOpacity,
|
||||||
currentItemFont: appState.currentItemFont,
|
currentItemFont: appState.currentItemFont,
|
||||||
|
currentItemTextAlign: appState.currentItemTextAlign,
|
||||||
viewBackgroundColor: appState.viewBackgroundColor,
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
name: appState.name,
|
name: appState.name,
|
||||||
};
|
};
|
||||||
|
@ -56,6 +56,8 @@ export function SelectedShapeActions({
|
|||||||
{renderAction("changeFontSize")}
|
{renderAction("changeFontSize")}
|
||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
|
{renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -714,6 +714,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
text: text,
|
text: text,
|
||||||
font: this.state.currentItemFont,
|
font: this.state.currentItemFont,
|
||||||
|
textAlign: this.state.currentItemTextAlign,
|
||||||
});
|
});
|
||||||
|
|
||||||
globalSceneState.replaceAllElements([
|
globalSceneState.replaceAllElements([
|
||||||
@ -1217,6 +1218,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
opacity: element.opacity,
|
opacity: element.opacity,
|
||||||
font: element.font,
|
font: element.font,
|
||||||
angle: element.angle,
|
angle: element.angle,
|
||||||
|
textAlign: element.textAlign,
|
||||||
zoom: this.state.zoom,
|
zoom: this.state.zoom,
|
||||||
onChange: withBatchedUpdates((text) => {
|
onChange: withBatchedUpdates((text) => {
|
||||||
if (text) {
|
if (text) {
|
||||||
@ -1288,6 +1290,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
opacity: this.state.currentItemOpacity,
|
opacity: this.state.currentItemOpacity,
|
||||||
text: "",
|
text: "",
|
||||||
font: this.state.currentItemFont,
|
font: this.state.currentItemFont,
|
||||||
|
textAlign: this.state.currentItemTextAlign,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({ editingElement: element });
|
this.setState({ editingElement: element });
|
||||||
|
@ -3,9 +3,14 @@ import { Point } from "../types";
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { DataState } from "./types";
|
import { DataState } from "./types";
|
||||||
import { isInvisiblySmallElement, normalizeDimensions } from "../element";
|
import {
|
||||||
|
isInvisiblySmallElement,
|
||||||
|
normalizeDimensions,
|
||||||
|
isTextElement,
|
||||||
|
} from "../element";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
|
import { DEFAULT_TEXT_ALIGN } from "../appState";
|
||||||
|
|
||||||
export function restore(
|
export function restore(
|
||||||
// we're making the elements mutable for this API because we want to
|
// we're making the elements mutable for this API because we want to
|
||||||
@ -51,6 +56,10 @@ export function restore(
|
|||||||
}
|
}
|
||||||
element.points = points;
|
element.points = points;
|
||||||
} else {
|
} else {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
element.textAlign = DEFAULT_TEXT_ALIGN;
|
||||||
|
}
|
||||||
|
|
||||||
normalizeDimensions(element);
|
normalizeDimensions(element);
|
||||||
// old spec, where non-linear elements used to have empty points arrays
|
// old spec, where non-linear elements used to have empty points arrays
|
||||||
if ("points" in element) {
|
if ("points" in element) {
|
||||||
|
@ -77,6 +77,7 @@ it("clones text element", () => {
|
|||||||
opacity: 100,
|
opacity: 100,
|
||||||
text: "hello",
|
text: "hello",
|
||||||
font: "Arial 20px",
|
font: "Arial 20px",
|
||||||
|
textAlign: "left",
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(element);
|
const copy = duplicateElement(element);
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
TextAlign,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { measureText } from "../utils";
|
import { measureText } from "../utils";
|
||||||
import { randomInteger, randomId } from "../random";
|
import { randomInteger, randomId } from "../random";
|
||||||
@ -73,15 +74,16 @@ export function newTextElement(
|
|||||||
opts: {
|
opts: {
|
||||||
text: string;
|
text: string;
|
||||||
font: string;
|
font: string;
|
||||||
|
textAlign: TextAlign;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawTextElement> {
|
): NonDeleted<ExcalidrawTextElement> {
|
||||||
const { text, font } = opts;
|
const metrics = measureText(opts.text, opts.font);
|
||||||
const metrics = measureText(text, font);
|
|
||||||
const textElement = newElementWith(
|
const textElement = newElementWith(
|
||||||
{
|
{
|
||||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||||
text: text,
|
text: opts.text,
|
||||||
font: font,
|
font: opts.font,
|
||||||
|
textAlign: opts.textAlign,
|
||||||
// Center the text
|
// Center the text
|
||||||
x: opts.x - metrics.width / 2,
|
x: opts.x - metrics.width / 2,
|
||||||
y: opts.y - metrics.height / 2,
|
y: opts.y - metrics.height / 2,
|
||||||
|
@ -21,6 +21,7 @@ type TextWysiwygParams = {
|
|||||||
opacity: number;
|
opacity: number;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
angle: number;
|
angle: number;
|
||||||
|
textAlign: string;
|
||||||
onChange?: (text: string) => void;
|
onChange?: (text: string) => void;
|
||||||
onSubmit: (text: string) => void;
|
onSubmit: (text: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@ -36,6 +37,7 @@ export function textWysiwyg({
|
|||||||
zoom,
|
zoom,
|
||||||
angle,
|
angle,
|
||||||
onChange,
|
onChange,
|
||||||
|
textAlign,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: TextWysiwygParams) {
|
}: TextWysiwygParams) {
|
||||||
@ -59,7 +61,7 @@ export function textWysiwyg({
|
|||||||
top: `${y}px`,
|
top: `${y}px`,
|
||||||
left: `${x}px`,
|
left: `${x}px`,
|
||||||
transform: `translate(-50%, -50%) scale(${zoom}) rotate(${degree}deg)`,
|
transform: `translate(-50%, -50%) scale(${zoom}) rotate(${degree}deg)`,
|
||||||
textAlign: "left",
|
textAlign: textAlign,
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
font: font,
|
font: font,
|
||||||
padding: "4px",
|
padding: "4px",
|
||||||
|
@ -45,6 +45,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
|||||||
font: string;
|
font: string;
|
||||||
text: string;
|
text: string;
|
||||||
baseline: number;
|
baseline: number;
|
||||||
|
textAlign: TextAlign;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||||
@ -55,3 +56,5 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type PointerType = "mouse" | "pen" | "touch";
|
export type PointerType = "mouse" | "pen" | "touch";
|
||||||
|
|
||||||
|
export type TextAlign = "left" | "center" | "right";
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"strokeWidth": "Stroke width",
|
"strokeWidth": "Stroke width",
|
||||||
"sloppiness": "Sloppiness",
|
"sloppiness": "Sloppiness",
|
||||||
"opacity": "Opacity",
|
"opacity": "Opacity",
|
||||||
|
"textAlign": "Text align",
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
"fontFamily": "Font family",
|
"fontFamily": "Font family",
|
||||||
"onlySelected": "Only selected",
|
"onlySelected": "Only selected",
|
||||||
@ -34,6 +35,9 @@
|
|||||||
"crossHatch": "Cross-hatch",
|
"crossHatch": "Cross-hatch",
|
||||||
"thin": "Thin",
|
"thin": "Thin",
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
|
"left": "Left",
|
||||||
|
"center": "Center",
|
||||||
|
"right": "Right",
|
||||||
"extraBold": "Extra bold",
|
"extraBold": "Extra bold",
|
||||||
"architect": "Architect",
|
"architect": "Architect",
|
||||||
"artist": "Artist",
|
"artist": "Artist",
|
||||||
|
@ -101,15 +101,28 @@ function drawElementOnCanvas(
|
|||||||
context.font = element.font;
|
context.font = element.font;
|
||||||
const fillStyle = context.fillStyle;
|
const fillStyle = context.fillStyle;
|
||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle = element.strokeColor;
|
||||||
|
const textAlign = context.textAlign;
|
||||||
|
context.textAlign = element.textAlign as CanvasTextAlign;
|
||||||
// Canvas does not support multiline text by default
|
// Canvas does not support multiline text by default
|
||||||
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
const lineHeight = element.height / lines.length;
|
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++) {
|
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.fillStyle = fillStyle;
|
||||||
context.font = font;
|
context.font = font;
|
||||||
|
context.textAlign = textAlign;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unimplemented type ${element.type}`);
|
throw new Error(`Unimplemented type ${element.type}`);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ import {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
TextAlign,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
@ -30,6 +31,7 @@ export type AppState = {
|
|||||||
currentItemRoughness: number;
|
currentItemRoughness: number;
|
||||||
currentItemOpacity: number;
|
currentItemOpacity: number;
|
||||||
currentItemFont: string;
|
currentItemFont: string;
|
||||||
|
currentItemTextAlign: TextAlign;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
scrollX: FlooredNumber;
|
scrollX: FlooredNumber;
|
||||||
scrollY: FlooredNumber;
|
scrollY: FlooredNumber;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user