Преглед изворни кода

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>
Youness Fkhach пре 5 година
родитељ
комит
ff82d1cfa3

+ 49 - 1
src/actions/actionProperties.tsx

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

+ 1 - 0
src/actions/index.ts

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

+ 1 - 0
src/actions/types.ts

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

+ 3 - 0
src/appState.ts

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

+ 2 - 0
src/components/Actions.tsx

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

+ 3 - 0
src/components/App.tsx

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

+ 10 - 1
src/data/restore.ts

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

+ 1 - 0
src/element/newElement.test.ts

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

+ 6 - 4
src/element/newElement.ts

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

+ 3 - 1
src/element/textWysiwyg.tsx

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

+ 3 - 0
src/element/types.ts

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

+ 4 - 0
src/locales/en.json

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

+ 15 - 2
src/renderer/renderElement.ts

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

Разлика између датотеке није приказан због своје велике величине
+ 126 - 0
src/tests/__snapshots__/regressionTests.test.tsx.snap


+ 2 - 0
src/types.ts

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

Неке датотеке нису приказане због велике количине промена