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:
Youness Fkhach 2020-04-08 21:00:27 +01:00 committed by GitHub
parent 3fd6f3023f
commit ff82d1cfa3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 357 additions and 9 deletions

View File

@ -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>
),
});

View File

@ -16,6 +16,7 @@ export {
actionChangeOpacity,
actionChangeFontSize,
actionChangeFontFamily,
actionChangeTextAlign,
} from "./actionProperties";
export {

View File

@ -49,6 +49,7 @@ export type ActionName =
| "zoomOut"
| "resetZoom"
| "changeFontFamily"
| "changeTextAlign"
| "toggleFullScreen"
| "toggleShortcuts";

View File

@ -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,
};

View File

@ -56,6 +56,8 @@ export function SelectedShapeActions({
{renderAction("changeFontSize")}
{renderAction("changeFontFamily")}
{renderAction("changeTextAlign")}
</>
)}

View File

@ -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 });

View File

@ -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) {

View File

@ -77,6 +77,7 @@ it("clones text element", () => {
opacity: 100,
text: "hello",
font: "Arial 20px",
textAlign: "left",
});
const copy = duplicateElement(element);

View File

@ -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,

View File

@ -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",

View File

@ -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";

View File

@ -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",

View File

@ -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

View File

@ -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;