diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index d89b40932..4c064ad6e 100644 --- a/src/actions/actionProperties.tsx +++ b/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 ( 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 ( 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 }) => ( - <> - - { + return ( + <> + + 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 ( + <> + + !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({ diff --git a/src/actions/index.ts b/src/actions/index.ts index 5d417f069..9784c65bf 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -8,6 +8,7 @@ export { export { actionSelectAll } from "./actionSelectAll"; export { actionDuplicateSelection } from "./actionDuplicateSelection"; export { + actionChangeFontColor, actionChangeStrokeColor, actionChangeBackgroundColor, actionChangeStrokeWidth, diff --git a/src/actions/types.ts b/src/actions/types.ts index 4009822f5..01531c976 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -49,6 +49,7 @@ export type ActionName = | "gridMode" | "zenMode" | "stats" + | "changeFontColor" | "changeStrokeColor" | "changeBackgroundColor" | "changeFillStyle" diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index fd9156c61..b594fda60 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -68,8 +68,15 @@ export const SelectedShapeActions = ({ } } + const hasOnlyContainersWithBoundText = + targetElements.length > 1 && + targetElements.every( + (element) => hasBoundTextElement(element) || isBoundToContainer(element), + ); + return (
+ {hasOnlyContainersWithBoundText && renderAction("changeFontColor")} {((hasStrokeColor(elementType) && elementType !== "image" && commonSelectedType !== "image") || diff --git a/src/components/App.tsx b/src/components/App.tsx index fa3682f77..22f57c468 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1840,7 +1840,11 @@ class App extends React.Component { 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 { if (event.key === KEYS.S) { this.setState({ openPopup: "strokeColorPicker" }); } + if (event.key === KEYS.C) { + this.setState({ openPopup: "fontColorPicker" }); + } } }, ); diff --git a/src/components/ColorPicker.scss b/src/components/ColorPicker.scss index fdcb9baa9..6b43ba049 100644 --- a/src/components/ColorPicker.scss +++ b/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; } diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index e409557a3..a48b30607 100644 --- a/src/components/ColorPicker.tsx +++ b/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(); const activeItem = React.useRef(); const gallery = React.useRef(); const colorInput = React.useRef(); + 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(null); - + const colorType = type === "elementFontColor" ? "elementStroke" : type; return (
@@ -393,7 +402,7 @@ export const ColorPicker = ({ } > { onChange(changedColor); diff --git a/src/keys.ts b/src/keys.ts index fc495437d..cb1f2c23a 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -47,6 +47,7 @@ export const KEYS = { COMMA: ",", A: "a", + C: "c", D: "d", E: "e", G: "g", diff --git a/src/locales/en.json b/src/locales/en.json index 2ce8ea6b4..b40686c63 100644 --- a/src/locales/en.json +++ b/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", diff --git a/src/scene/selection.ts b/src/scene/selection.ts index a5abf7e43..4cbc8b95f 100644 --- a/src/scene/selection.ts +++ b/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 = ( 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; diff --git a/src/types.ts b/src/types.ts index 71c2b53ed..570410fb4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -113,6 +113,7 @@ export type AppState = { | "canvasColorPicker" | "backgroundColorPicker" | "strokeColorPicker" + | "fontColorPicker" | null; lastPointerDownWith: PointerType; selectedElementIds: { [id: string]: boolean };