Compare commits

...

1 Commits

Author SHA1 Message Date
Zsolt Viczian
09ae07ed7f Added font color picker 2022-03-14 20:56:12 +01:00
11 changed files with 149 additions and 35 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -255,7 +255,8 @@
color: #aaa; 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; color: #d4d4d4;
} }

View File

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

View File

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

View File

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

View File

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

View File

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