소스 검색

Added font color picker

Zsolt Viczian 3 년 전
부모
커밋
09ae07ed7f

+ 105 - 23
src/actions/actionProperties.tsx

@@ -51,6 +51,7 @@ import {
   getContainerElement,
 } from "../element/textElement";
 import {
+  hasBoundTextElement,
   isBoundToContainer,
   isLinearElement,
   isLinearElementType,
@@ -106,6 +107,7 @@ const getFormValue = function <T>(
   appState: AppState,
   getAttribute: (element: ExcalidrawElement) => T,
   defaultValue?: T,
+  onlyBoundTextElements: boolean = false,
 ): T | null {
   const editingElement = appState.editingElement;
   const nonDeletedElements = getNonDeletedElements(elements);
@@ -116,6 +118,7 @@ const getFormValue = function <T>(
           nonDeletedElements,
           appState,
           getAttribute,
+          onlyBoundTextElements,
         )
       : defaultValue) ??
     null
@@ -196,8 +199,8 @@ const changeFontSize = (
 
 // -----------------------------------------------------------------------------
 
-export const actionChangeStrokeColor = register({
-  name: "changeStrokeColor",
+export const actionChangeFontColor = register({
+  name: "changeFontColor",
   perform: (elements, appState, value) => {
     return {
       ...(value.currentItemStrokeColor && {
@@ -205,7 +208,7 @@ export const actionChangeStrokeColor = register({
           elements,
           appState,
           (el) => {
-            return hasStrokeColor(el.type)
+            return isTextElement(el)
               ? newElementWith(el, {
                   strokeColor: value.currentItemStrokeColor,
                 })
@@ -221,28 +224,107 @@ export const actionChangeStrokeColor = register({
       commitToHistory: !!value.currentItemStrokeColor,
     };
   },
-  PanelComponent: ({ elements, appState, updateData }) => (
-    <>
-      <h3 aria-hidden="true">{t("labels.stroke")}</h3>
-      <ColorPicker
-        type="elementStroke"
-        label={t("labels.stroke")}
-        color={getFormValue(
+  PanelComponent: ({ elements, appState, updateData }) => {
+    return (
+      <>
+        <h3 aria-hidden="true">{t("labels.fontColor")}</h3>
+        <ColorPicker
+          type="elementFontColor"
+          label={t("labels.fontColor")}
+          color={getFormValue(
+            elements,
+            appState,
+            (element) => element.strokeColor,
+            appState.currentItemStrokeColor,
+            true,
+          )}
+          onChange={(color) => updateData({ currentItemStrokeColor: color })}
+          isActive={appState.openPopup === "fontColorPicker"}
+          setActive={(active) =>
+            updateData({ openPopup: active ? "fontColorPicker" : null })
+          }
+          elements={elements}
+          appState={appState}
+        />
+      </>
+    );
+  },
+});
+
+export const actionChangeStrokeColor = register({
+  name: "changeStrokeColor",
+  perform: (elements, appState, value) => {
+    const targetElements = getTargetElements(
+      getNonDeletedElements(elements),
+      appState,
+    );
+
+    const hasOnlyContainersWithBoundText =
+      targetElements.length > 1 &&
+      targetElements.every(
+        (element) =>
+          hasBoundTextElement(element) || isBoundToContainer(element),
+      );
+
+    return {
+      ...(value.currentItemStrokeColor && {
+        elements: changeProperty(
           elements,
           appState,
-          (element) => element.strokeColor,
-          appState.currentItemStrokeColor,
-        )}
-        onChange={(color) => updateData({ currentItemStrokeColor: color })}
-        isActive={appState.openPopup === "strokeColorPicker"}
-        setActive={(active) =>
-          updateData({ openPopup: active ? "strokeColorPicker" : null })
-        }
-        elements={elements}
-        appState={appState}
-      />
-    </>
-  ),
+          (el) => {
+            return (hasStrokeColor(el.type) &&
+              !hasOnlyContainersWithBoundText) ||
+              !isBoundToContainer(el)
+              ? newElementWith(el, {
+                  strokeColor: value.currentItemStrokeColor,
+                })
+              : el;
+          },
+          true,
+        ),
+      }),
+      appState: {
+        ...appState,
+        ...value,
+      },
+      commitToHistory: !!value.currentItemStrokeColor,
+    };
+  },
+  PanelComponent: ({ elements, appState, updateData }) => {
+    const targetElements = getTargetElements(
+      getNonDeletedElements(elements),
+      appState,
+    );
+
+    const hasOnlyContainersWithBoundText = targetElements.every(
+      (element) => hasBoundTextElement(element) || isBoundToContainer(element),
+    );
+
+    return (
+      <>
+        <h3 aria-hidden="true">{t("labels.stroke")}</h3>
+        <ColorPicker
+          type="elementStroke"
+          label={t("labels.stroke")}
+          color={getFormValue(
+            hasOnlyContainersWithBoundText
+              ? elements.filter((element) => !isTextElement(element))
+              : elements,
+            appState,
+            (element) => element.strokeColor,
+            appState.currentItemStrokeColor,
+          )}
+          onChange={(color) => updateData({ currentItemStrokeColor: color })}
+          isActive={appState.openPopup === "strokeColorPicker"}
+          setActive={(active) =>
+            updateData({ openPopup: active ? "strokeColorPicker" : null })
+          }
+          elements={elements}
+          appState={appState}
+        />
+      </>
+    );
+  },
 });
 
 export const actionChangeBackgroundColor = register({

+ 1 - 0
src/actions/index.ts

@@ -8,6 +8,7 @@ export {
 export { actionSelectAll } from "./actionSelectAll";
 export { actionDuplicateSelection } from "./actionDuplicateSelection";
 export {
+  actionChangeFontColor,
   actionChangeStrokeColor,
   actionChangeBackgroundColor,
   actionChangeStrokeWidth,

+ 1 - 0
src/actions/types.ts

@@ -49,6 +49,7 @@ export type ActionName =
   | "gridMode"
   | "zenMode"
   | "stats"
+  | "changeFontColor"
   | "changeStrokeColor"
   | "changeBackgroundColor"
   | "changeFillStyle"

+ 7 - 0
src/components/Actions.tsx

@@ -68,8 +68,15 @@ export const SelectedShapeActions = ({
     }
   }
 
+  const hasOnlyContainersWithBoundText =
+    targetElements.length > 1 &&
+    targetElements.every(
+      (element) => hasBoundTextElement(element) || isBoundToContainer(element),
+    );
+
   return (
     <div className="panelColumn">
+      {hasOnlyContainersWithBoundText && renderAction("changeFontColor")}
       {((hasStrokeColor(elementType) &&
         elementType !== "image" &&
         commonSelectedType !== "image") ||

+ 8 - 1
src/components/App.tsx

@@ -1840,7 +1840,11 @@ class App extends React.Component<AppProps, AppState> {
         event.preventDefault();
       }
 
-      if (event.key === KEYS.G || event.key === KEYS.S) {
+      if (
+        event.key === KEYS.G ||
+        event.key === KEYS.S ||
+        event.key === KEYS.C
+      ) {
         const selectedElements = getSelectedElements(
           this.scene.getElements(),
           this.state,
@@ -1862,6 +1866,9 @@ class App extends React.Component<AppProps, AppState> {
         if (event.key === KEYS.S) {
           this.setState({ openPopup: "strokeColorPicker" });
         }
+        if (event.key === KEYS.C) {
+          this.setState({ openPopup: "fontColorPicker" });
+        }
       }
     },
   );

+ 2 - 1
src/components/ColorPicker.scss

@@ -255,7 +255,8 @@
     color: #aaa;
   }
 
-  .color-picker-type-elementStroke .color-picker-keybinding {
+  .color-picker-type-elementStroke .color-picker-keybinding,
+  .color-picker-type-elementFontColor .color-picker-keybinding {
     color: #d4d4d4;
   }
 

+ 15 - 6
src/components/ColorPicker.tsx

@@ -101,19 +101,24 @@ const Picker = ({
   onClose: () => void;
   label: string;
   showInput: boolean;
-  type: "canvasBackground" | "elementBackground" | "elementStroke";
+  type:
+    | "canvasBackground"
+    | "elementBackground"
+    | "elementStroke"
+    | "elementFontColor";
   elements: readonly ExcalidrawElement[];
 }) => {
   const firstItem = React.useRef<HTMLButtonElement>();
   const activeItem = React.useRef<HTMLButtonElement>();
   const gallery = React.useRef<HTMLDivElement>();
   const colorInput = React.useRef<HTMLInputElement>();
+  const colorType = type === "elementFontColor" ? "elementStroke" : type;
 
   const [customColors] = React.useState(() => {
-    if (type === "canvasBackground") {
+    if (colorType === "canvasBackground") {
       return [];
     }
-    return getCustomColors(elements, type);
+    return getCustomColors(elements, colorType);
   });
 
   React.useEffect(() => {
@@ -356,7 +361,11 @@ export const ColorPicker = ({
   elements,
   appState,
 }: {
-  type: "canvasBackground" | "elementBackground" | "elementStroke";
+  type:
+    | "canvasBackground"
+    | "elementBackground"
+    | "elementStroke"
+    | "elementFontColor";
   color: string | null;
   onChange: (color: string) => void;
   label: string;
@@ -366,7 +375,7 @@ export const ColorPicker = ({
   appState: AppState;
 }) => {
   const pickerButton = React.useRef<HTMLButtonElement>(null);
-
+  const colorType = type === "elementFontColor" ? "elementStroke" : type;
   return (
     <div>
       <div className="color-picker-control-container">
@@ -393,7 +402,7 @@ export const ColorPicker = ({
             }
           >
             <Picker
-              colors={colors[type]}
+              colors={colors[colorType]}
               color={color || null}
               onChange={(changedColor) => {
                 onChange(changedColor);

+ 1 - 0
src/keys.ts

@@ -47,6 +47,7 @@ export const KEYS = {
   COMMA: ",",
 
   A: "a",
+  C: "c",
   D: "d",
   E: "e",
   G: "g",

+ 1 - 0
src/locales/en.json

@@ -16,6 +16,7 @@
     "delete": "Delete",
     "copyStyles": "Copy styles",
     "pasteStyles": "Paste styles",
+    "fontColor": "Font color",
     "stroke": "Stroke",
     "background": "Background",
     "fill": "Fill",

+ 7 - 4
src/scene/selection.ts

@@ -4,7 +4,7 @@ import {
 } from "../element/types";
 import { getElementAbsoluteCoords, getElementBounds } from "../element";
 import { AppState } from "../types";
-import { isBoundToContainer } from "../element/typeChecks";
+import { isBoundToContainer, isTextElement } from "../element/typeChecks";
 
 export const getElementsWithinSelection = (
   elements: readonly NonDeletedExcalidrawElement[],
@@ -41,12 +41,15 @@ export const getCommonAttributeOfSelectedElements = <T>(
   elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
   getAttribute: (element: ExcalidrawElement) => T,
+  onlyBoundTextElements: boolean = false,
 ): T | null => {
   const attributes = Array.from(
     new Set(
-      getSelectedElements(elements, appState).map((element) =>
-        getAttribute(element),
-      ),
+      getSelectedElements(elements, appState, onlyBoundTextElements)
+        .filter((element) =>
+          onlyBoundTextElements ? isTextElement(element) : true,
+        )
+        .map((element) => getAttribute(element)),
     ),
   );
   return attributes.length === 1 ? attributes[0] : null;

+ 1 - 0
src/types.ts

@@ -113,6 +113,7 @@ export type AppState = {
     | "canvasColorPicker"
     | "backgroundColorPicker"
     | "strokeColorPicker"
+    | "fontColorPicker"
     | null;
   lastPointerDownWith: PointerType;
   selectedElementIds: { [id: string]: boolean };