diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index 0d02e2a4..fdb31f72 100644 --- a/src/actions/actionProperties.tsx +++ b/src/actions/actionProperties.tsx @@ -41,9 +41,16 @@ import { isTextElement, redrawTextBoundingBox, } from "../element"; -import { newElementWith } from "../element/mutateElement"; -import { getBoundTextElement } from "../element/textElement"; -import { isLinearElement, isLinearElementType } from "../element/typeChecks"; +import { mutateElement, newElementWith } from "../element/mutateElement"; +import { + getBoundTextElement, + getContainerElement, +} from "../element/textElement"; +import { + isBoundToContainer, + isLinearElement, + isLinearElementType, +} from "../element/typeChecks"; import { Arrowhead, ExcalidrawElement, @@ -53,6 +60,7 @@ import { TextAlign, } from "../element/types"; import { getLanguage, t } from "../i18n"; +import { KEYS } from "../keys"; import { randomInteger } from "../random"; import { canChangeSharpness, @@ -63,10 +71,11 @@ import { isSomeElementSelected, } from "../scene"; import { hasStrokeColor } from "../scene/comparisons"; -import Scene from "../scene/Scene"; import { arrayToMap } from "../utils"; import { register } from "./register"; +const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; + const changeProperty = ( elements: readonly ExcalidrawElement[], appState: AppState, @@ -108,6 +117,79 @@ const getFormValue = function ( ); }; +const offsetElementAfterFontResize = ( + prevElement: ExcalidrawTextElement, + nextElement: ExcalidrawTextElement, +) => { + if (isBoundToContainer(nextElement)) { + return nextElement; + } + return mutateElement( + nextElement, + { + x: + prevElement.textAlign === "left" + ? prevElement.x + : prevElement.x + + (prevElement.width - nextElement.width) / + (prevElement.textAlign === "center" ? 2 : 1), + // centering vertically is non-standard, but for Excalidraw I think + // it makes sense + y: prevElement.y + (prevElement.height - nextElement.height) / 2, + }, + false, + ); +}; + +const changeFontSize = ( + elements: readonly ExcalidrawElement[], + appState: AppState, + getNewFontSize: (element: ExcalidrawTextElement) => number, +) => { + const newFontSizes = new Set(); + + return { + elements: changeProperty( + elements, + appState, + (oldElement) => { + if (isTextElement(oldElement)) { + const newFontSize = getNewFontSize(oldElement); + newFontSizes.add(newFontSize); + + let newElement: ExcalidrawTextElement = newElementWith(oldElement, { + fontSize: newFontSize, + }); + redrawTextBoundingBox( + newElement, + getContainerElement(oldElement), + appState, + ); + + newElement = offsetElementAfterFontResize(oldElement, newElement); + + return newElement; + } + + return oldElement; + }, + true, + ), + appState: { + ...appState, + // update state only if we've set all select text elements to + // the same font size + currentItemFontSize: + newFontSizes.size === 1 + ? [...newFontSizes][0] + : appState.currentItemFontSize, + }, + commitToHistory: true, + }; +}; + +// ----------------------------------------------------------------------------- + export const actionChangeStrokeColor = register({ name: "changeStrokeColor", perform: (elements, appState, value) => { @@ -438,33 +520,7 @@ export const actionChangeOpacity = register({ export const actionChangeFontSize = register({ name: "changeFontSize", perform: (elements, appState, value) => { - return { - elements: changeProperty( - elements, - appState, - (el) => { - if (isTextElement(el)) { - const element: ExcalidrawTextElement = newElementWith(el, { - fontSize: value, - }); - let container = null; - if (el.containerId) { - container = Scene.getScene(el)!.getElement(el.containerId); - } - redrawTextBoundingBox(element, container, appState); - return element; - } - - return el; - }, - true, - ), - appState: { - ...appState, - currentItemFontSize: value, - }, - commitToHistory: true, - }; + return changeFontSize(elements, appState, () => value); }, PanelComponent: ({ elements, appState, updateData }) => (
@@ -514,6 +570,44 @@ export const actionChangeFontSize = register({ ), }); +export const actionDecreaseFontSize = register({ + name: "decreaseFontSize", + perform: (elements, appState, value) => { + return changeFontSize(elements, appState, (element) => + Math.round( + // get previous value before relative increase (doesn't work fully + // due to rounding and float precision issues) + (1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)) * element.fontSize, + ), + ); + }, + keyTest: (event) => { + return ( + event[KEYS.CTRL_OR_CMD] && + event.shiftKey && + // KEYS.COMMA needed for MacOS + (event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA) + ); + }, +}); + +export const actionIncreaseFontSize = register({ + name: "increaseFontSize", + perform: (elements, appState, value) => { + return changeFontSize(elements, appState, (element) => + Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)), + ); + }, + keyTest: (event) => { + return ( + event[KEYS.CTRL_OR_CMD] && + event.shiftKey && + // KEYS.PERIOD needed for MacOS + (event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD) + ); + }, +}); + export const actionChangeFontFamily = register({ name: "changeFontFamily", perform: (elements, appState, value) => { @@ -521,20 +615,23 @@ export const actionChangeFontFamily = register({ elements: changeProperty( elements, appState, - (el) => { - if (isTextElement(el)) { - const element: ExcalidrawTextElement = newElementWith(el, { - fontFamily: value, - }); - let container = null; - if (el.containerId) { - container = Scene.getScene(el)!.getElement(el.containerId); - } - redrawTextBoundingBox(element, container, appState); - return element; + (oldElement) => { + if (isTextElement(oldElement)) { + const newElement: ExcalidrawTextElement = newElementWith( + oldElement, + { + fontFamily: value, + }, + ); + redrawTextBoundingBox( + newElement, + getContainerElement(oldElement), + appState, + ); + return newElement; } - return el; + return oldElement; }, true, ), @@ -603,20 +700,23 @@ export const actionChangeTextAlign = register({ elements: changeProperty( elements, appState, - (el) => { - if (isTextElement(el)) { - const element: ExcalidrawTextElement = newElementWith(el, { - textAlign: value, - }); - let container = null; - if (el.containerId) { - container = Scene.getScene(el)!.getElement(el.containerId); - } - redrawTextBoundingBox(element, container, appState); - return element; + (oldElement) => { + if (isTextElement(oldElement)) { + const newElement: ExcalidrawTextElement = newElementWith( + oldElement, + { + textAlign: value, + }, + ); + redrawTextBoundingBox( + newElement, + getContainerElement(oldElement), + appState, + ); + return newElement; } - return el; + return oldElement; }, true, ), diff --git a/src/actions/actionStyles.ts b/src/actions/actionStyles.ts index 40496cdf..57c6b3d3 100644 --- a/src/actions/actionStyles.ts +++ b/src/actions/actionStyles.ts @@ -12,9 +12,7 @@ import { DEFAULT_FONT_FAMILY, DEFAULT_TEXT_ALIGN, } from "../constants"; -import Scene from "../scene/Scene"; -import { isBoundToContainer } from "../element/typeChecks"; -import { ExcalidrawTextElement } from "../element/types"; +import { getContainerElement } from "../element/textElement"; // `copiedStyles` is exported only for tests. export let copiedStyles: string = "{}"; @@ -58,22 +56,16 @@ export const actionPasteStyles = register({ opacity: pastedElement?.opacity, roughness: pastedElement?.roughness, }); - if (isTextElement(newElement)) { + if (isTextElement(newElement) && isTextElement(element)) { mutateElement(newElement, { fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE, fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY, textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN, }); - let container = null; - if (isBoundToContainer(element)) { - container = Scene.getScene(element)!.getElement( - element.containerId, - ); - } redrawTextBoundingBox( - element as ExcalidrawTextElement, - container, + element, + getContainerElement(element), appState, ); } diff --git a/src/actions/types.ts b/src/actions/types.ts index 672fb91b..5c04a2bf 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -101,7 +101,9 @@ export type ActionName = | "flipVertical" | "viewMode" | "exportWithDarkMode" - | "toggleTheme"; + | "toggleTheme" + | "increaseFontSize" + | "decreaseFontSize"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/src/components/App.tsx b/src/components/App.tsx index deb1316e..a582e26d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1649,7 +1649,10 @@ class App extends React.Component { } if ( - (isWritableElement(event.target) && event.key !== KEYS.ESCAPE) || + (isWritableElement(event.target) && + event.key !== KEYS.ESCAPE && + // handle cmd/ctrl-modifier shortcuts even inside inputs + !event[KEYS.CTRL_OR_CMD]) || // case: using arrows to move between buttons (isArrowKey(event.key) && isInputLike(event.target)) ) { diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 5803fbe0..d4e209f6 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -394,6 +394,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { label={t("labels.showBackground")} shortcuts={[getShortcutKey("G")]} /> + + ")]} + /> diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 06d4437e..cb21b85e 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -21,9 +21,8 @@ import { AppState } from "../types"; import { getElementAbsoluteCoords } from "."; import { adjustXYWithRotation } from "../math"; import { getResizedElementAbsoluteCoords } from "./bounds"; -import { measureText } from "./textElement"; +import { getContainerElement, measureText } from "./textElement"; import { isBoundToContainer } from "./typeChecks"; -import Scene from "../scene/Scene"; import { BOUND_TEXT_PADDING } from "../constants"; type ElementConstructorOpts = MarkOptional< @@ -159,8 +158,8 @@ const getAdjustedDimensions = ( baseline: number; } => { let maxWidth = null; - if (element.containerId) { - const container = Scene.getScene(element)!.getElement(element.containerId)!; + const container = getContainerElement(element); + if (container) { maxWidth = container.width - BOUND_TEXT_PADDING * 2; } const { @@ -220,7 +219,7 @@ const getAdjustedDimensions = ( // make sure container dimensions are set properly when // text editor overflows beyond viewport dimensions if (isBoundToContainer(element)) { - const container = Scene.getScene(element)!.getElement(element.containerId)!; + const container = getContainerElement(element)!; let height = container.height; let width = container.width; if (nextHeight > height - BOUND_TEXT_PADDING * 2) { diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 3e25d1a1..2e3bbb4d 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -416,9 +416,25 @@ export const getBoundTextElement = (element: ExcalidrawElement | null) => { } const boundTextElementId = getBoundTextElementId(element); if (boundTextElementId) { - return Scene.getScene(element)!.getElement( - boundTextElementId, - ) as ExcalidrawTextElementWithContainer; + return ( + (Scene.getScene(element)?.getElement( + boundTextElementId, + ) as ExcalidrawTextElementWithContainer) || null + ); + } + return null; +}; + +export const getContainerElement = ( + element: + | (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null }) + | null, +) => { + if (!element) { + return null; + } + if (element.containerId) { + return Scene.getScene(element)?.getElement(element.containerId) || null; } return null; }; diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 7c7e484e..c07302bc 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -8,16 +8,13 @@ import { import Scene from "../scene/Scene"; import { isBoundToContainer, isTextElement } from "./typeChecks"; import { CLASSES, BOUND_TEXT_PADDING } from "../constants"; -import { - ExcalidrawBindableElement, - ExcalidrawElement, - ExcalidrawTextElement, -} from "./types"; +import { ExcalidrawElement, ExcalidrawTextElement } from "./types"; import { AppState } from "../types"; import { mutateElement } from "./mutateElement"; import { getApproxLineHeight, getBoundTextElementId, + getContainerElement, wrapText, } from "./textElement"; @@ -102,9 +99,7 @@ export const textWysiwyg = ({ if (updatedElement && isTextElement(updatedElement)) { let coordX = updatedElement.x; let coordY = updatedElement.y; - const container = updatedElement?.containerId - ? Scene.getScene(updatedElement)!.getElement(updatedElement.containerId) - : null; + const container = getContainerElement(updatedElement); let maxWidth = updatedElement.width; let maxHeight = updatedElement.height; @@ -274,9 +269,7 @@ export const textWysiwyg = ({ let height = "auto"; if (lines === 2) { - const container = Scene.getScene(element)!.getElement( - element.containerId, - ); + const container = getContainerElement(element); const actualLineCount = wrapText( editable.value, getFontString(element), @@ -300,13 +293,16 @@ export const textWysiwyg = ({ } editable.onkeydown = (event) => { - event.stopPropagation(); + if (!event[KEYS.CTRL_OR_CMD]) { + event.stopPropagation(); + } if (event.key === KEYS.ESCAPE) { event.preventDefault(); submittedViaKeyboard = true; handleSubmit(); } else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) { event.preventDefault(); + event.stopPropagation(); if (event.isComposing || event.keyCode === 229) { return; } @@ -319,6 +315,7 @@ export const textWysiwyg = ({ event.code === CODES.BRACKET_RIGHT)) ) { event.preventDefault(); + event.stopPropagation(); if (event.shiftKey || event.code === CODES.BRACKET_LEFT) { outdent(); } else { @@ -443,9 +440,7 @@ export const textWysiwyg = ({ } let wrappedText = ""; if (isTextElement(updateElement) && updateElement?.containerId) { - const container = Scene.getScene(updateElement)!.getElement( - updateElement.containerId, - ) as ExcalidrawBindableElement; + const container = getContainerElement(updateElement); if (container) { wrappedText = wrapText( diff --git a/src/keys.ts b/src/keys.ts index 93af2d08..6219197f 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -40,6 +40,10 @@ export const KEYS = { QUESTION_MARK: "?", SPACE: " ", TAB: "Tab", + CHEVRON_LEFT: "<", + CHEVRON_RIGHT: ">", + PERIOD: ".", + COMMA: ",", A: "a", D: "d", diff --git a/src/locales/en.json b/src/locales/en.json index b01680b2..8a240907 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -102,7 +102,9 @@ "showBackground": "Show background color picker", "toggleTheme": "Toggle theme", "personalLib": "Personal Library", - "excalidrawLib": "Excalidraw Library" + "excalidrawLib": "Excalidraw Library", + "decreaseFontSize": "Decrease font size", + "increaseFontSize": "Increase font size" }, "buttons": { "clearReset": "Reset the canvas",