Added font color picker

This commit is contained in:
Zsolt Viczian 2022-03-14 20:56:12 +01:00
parent 6d45430344
commit 09ae07ed7f
11 changed files with 149 additions and 35 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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