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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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