diff --git a/DistFiles/localization/en/BloomMediumPriority.xlf b/DistFiles/localization/en/BloomMediumPriority.xlf index 281cad055862..e197f14e0026 100644 --- a/DistFiles/localization/en/BloomMediumPriority.xlf +++ b/DistFiles/localization/en/BloomMediumPriority.xlf @@ -463,6 +463,25 @@ Drag any of these onto a canvas: ID: EditTab.Toolbox.CanvasTool.DragInstructions2 + + Image Fit + ID: EditTab.Toolbox.CanvasTool.ImageFit + + + Fit with Margin + ID: EditTab.Toolbox.CanvasTool.ImageFit.Margin + Leave a margin around the image within the button. + + + Fit to Edge + ID: EditTab.Toolbox.CanvasTool.ImageFit.FitToEdge + Fill up the button with the image, leaving a margin on the sides as needed. + + + Fill + Fill up the button with the image, leaving no margin, cropping if necessary + ID: EditTab.Toolbox.CanvasTool.ImageFit.Fill + Choose books... ID: EditTab.Toolbox.CanvasTool.LinkGrid.ChooseBooks diff --git a/src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts b/src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts index f17932a41bbe..56bcfa5d516e 100644 --- a/src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts +++ b/src/BloomBrowserUI/bookEdit/js/CanvasElementManager.ts @@ -59,6 +59,9 @@ import { kBloomCanvasClass, kBloomCanvasSelector, kBloomButtonClass, + kImageFitModeAttribute, + kImageFitModeContainValue, + kImageFitModeCoverValue, } from "../toolbox/canvas/canvasElementUtils"; import OverflowChecker from "../OverflowChecker/OverflowChecker"; import theOneLocalizationManager from "../../lib/localizationManager/localizationManager"; @@ -5835,6 +5838,16 @@ export class CanvasElementManager { patriarchDuplicateElement.classList.add("bloom-gif"); if (sourceElement.classList.contains(kBloomButtonClass)) patriarchDuplicateElement.classList.add(kBloomButtonClass); + const imageFitMode = sourceElement.getAttribute(kImageFitModeAttribute); + if ( + imageFitMode === kImageFitModeCoverValue || + imageFitMode === kImageFitModeContainValue + ) { + patriarchDuplicateElement.setAttribute( + kImageFitModeAttribute, + imageFitMode, + ); + } // copy any data-sound const sourceDataSound = sourceElement.getAttribute("data-sound"); diff --git a/src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts b/src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts index 79c36f80575f..5b28003d2709 100644 --- a/src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts +++ b/src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasElementUtils.ts @@ -11,6 +11,9 @@ export const kHasCanvasElementClass = "bloom-has-canvas-element"; export const kBloomCanvasClass = "bloom-canvas"; export const kBloomCanvasSelector = `.${kBloomCanvasClass}`; export const kBloomButtonClass = "bloom-canvas-button"; +export const kImageFitModeAttribute = "data-image-fit"; +export const kImageFitModeContainValue = "contain"; +export const kImageFitModeCoverValue = "cover"; // Enhance: we could reduce cross-bundle dependencies by separately defining the CanvasElementManager interface // and just importing that here. diff --git a/src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasTool.tsx b/src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasTool.tsx index c9075643dc81..ef0fde0f9dc5 100644 --- a/src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasTool.tsx +++ b/src/BloomBrowserUI/bookEdit/toolbox/canvas/canvasTool.tsx @@ -48,6 +48,9 @@ import { import { getCanvasElementManager, kBloomButtonClass, + kImageFitModeAttribute, + kImageFitModeContainValue, + kImageFitModeCoverValue, } from "./canvasElementUtils"; import { deselectVideoContainers } from "../../js/videoUtils"; import { CanvasElementKeyHints } from "./CanvasElementKeyHints"; @@ -63,6 +66,23 @@ import { TriangleCollapse } from "../../../react_components/TriangleCollapse"; import { BloomTooltip } from "../../../react_components/BloomToolTip"; import { text } from "stream/consumers"; +const kImageFillModePaddedValue = "padded"; +type ImageFillMode = + | typeof kImageFillModePaddedValue + | typeof kImageFitModeContainValue + | typeof kImageFitModeCoverValue; + +const getImageFillModeForElement = (element: HTMLElement): ImageFillMode => { + const currentFillMode = element.getAttribute(kImageFitModeAttribute); + if ( + currentFillMode === kImageFitModeContainValue || + currentFillMode === kImageFitModeCoverValue + ) { + return currentFillMode; + } + return kImageFillModePaddedValue; +}; + const CanvasToolControls: React.FunctionComponent = () => { const l10nPrefix = "ColorPicker."; type CanvasElementType = "text" | "image" | "video" | undefined; @@ -77,6 +97,9 @@ const CanvasToolControls: React.FunctionComponent = () => { const [showTailChecked, setShowTailChecked] = useState(false); const [isRoundedCornersChecked, setIsRoundedCornersChecked] = useState(false); + const [imageFillMode, setImageFillMode] = useState( + kImageFillModePaddedValue, + ); const [isXmatter, setIsXmatter] = useState(true); // This 'counter' increments on new page ready so we can re-check if the book is locked. const [pageRefreshIndicator, setPageRefreshIndicator] = useState(0); @@ -85,16 +108,22 @@ const CanvasToolControls: React.FunctionComponent = () => { const [isStyleSelectOpen, setIsStyleSelectOpen] = useState(false); const [isOutlineColorSelectOpen, setIsOutlineColorSelectOpen] = useState(false); - function openStyleSelect() { + const [isImageFillSelectOpen, setIsImageFillSelectOpen] = useState(false); + const openStyleSelect = () => { setIsStyleSelectOpen(true); // Make sure we don't leave the select open when the tool closes. callWhenFocusLost(() => setIsStyleSelectOpen(false)); - } - function openOutlineColorSelect() { + }; + const openOutlineColorSelect = () => { setIsOutlineColorSelectOpen(true); // Make sure we don't leave the select open when the tool closes. callWhenFocusLost(() => setIsOutlineColorSelectOpen(false)); - } + }; + const openImageFillSelect = () => { + setIsImageFillSelectOpen(true); + // Make sure we don't leave the select open when the tool closes. + callWhenFocusLost(() => setIsImageFillSelectOpen(false)); + }; // Calls to useL10n const deleteTooltip = useL10n("Delete", "Common.Delete"); @@ -205,6 +234,7 @@ const CanvasToolControls: React.FunctionComponent = () => { const canvasElementManager = getCanvasElementManager(); setCanvasElementType(getBubbleType(canvasElementManager)); + setImageFillMode(getImageFillModeForElement(currentBubble.content)); if (canvasElementManager) { // Get the current canvas element's textColor and set it const canvasElementTextColorInformation: ITextColorInfo = @@ -219,6 +249,7 @@ const CanvasToolControls: React.FunctionComponent = () => { } } else { setCanvasElementType(undefined); + setImageFillMode(kImageFillModePaddedValue); } }, [currentBubble]); @@ -436,6 +467,27 @@ const CanvasToolControls: React.FunctionComponent = () => { const bubble = canvasElementManager.getPatriarchBubbleOfActiveElement(); setCurrentBubble(bubble); }; + const handleImageFillChanged = (event) => { + const newMode = event.target.value as ImageFillMode; + setImageFillMode(newMode); + const activeElement = getCanvasElementManager()?.getActiveElement(); + if (!activeElement) { + return; + } + if (newMode === kImageFitModeCoverValue) { + activeElement.setAttribute( + kImageFitModeAttribute, + kImageFitModeCoverValue, + ); + } else if (newMode === kImageFitModeContainValue) { + activeElement.setAttribute( + kImageFitModeAttribute, + kImageFitModeContainValue, + ); + } else { + activeElement.removeAttribute(kImageFitModeAttribute); + } + }; // Callback when outline color of the bubble is changed const handleOutlineColorChanged = (event) => { @@ -576,6 +628,52 @@ const CanvasToolControls: React.FunctionComponent = () => { /> ); + const imageFillControl = ( + + + + Image Fit + + + + + + + ); const textColorControl = ( @@ -595,6 +693,9 @@ const CanvasToolControls: React.FunctionComponent = () => { const activeElement = canvasElementManager?.getActiveElement(); const isButton = activeElement?.classList.contains(kBloomButtonClass) ?? false; + const hasImage = + (activeElement?.getElementsByClassName("bloom-imageContainer") + ?.length ?? 0) > 0; const hasText = (activeElement?.getElementsByClassName("bloom-translationGroup") ?.length ?? 0) > 0; @@ -625,6 +726,7 @@ const CanvasToolControls: React.FunctionComponent = () => { <> {hasText && textColorControl} {backgroundColorControl} + {hasImage && imageFillControl} ); switch (canvasElementType) { diff --git a/src/content/bookLayout/canvasElement.less b/src/content/bookLayout/canvasElement.less index bc22089479cd..d69d0c8036ba 100644 --- a/src/content/bookLayout/canvasElement.less +++ b/src/content/bookLayout/canvasElement.less @@ -357,6 +357,54 @@ } } +.bloom-page .bloom-canvas-button[data-image-fit="contain"], +.bloom-page .bloom-canvas-button[data-image-fit="cover"] { + padding: 0; + overflow: hidden; +} + +.bloom-page + .bloom-canvas-button[data-image-fit="contain"] + .bloom-imageContainer, +.bloom-page .bloom-canvas-button[data-image-fit="cover"] .bloom-imageContainer { + img, + video { + width: 100%; + height: 100%; + max-width: none; + max-height: none; + } +} + +.bloom-page + .bloom-canvas-button[data-image-fit="contain"] + .bloom-imageContainer { + img, + video { + object-fit: contain; + } +} + +.bloom-page .bloom-canvas-button[data-image-fit="cover"] .bloom-imageContainer { + img, + video { + object-fit: cover; + } +} + +// In contain/cover modes, move padding from the button to the text group so +// the image can reach the edge while labels keep their spacing. +.bloom-page + .bloom-canvas-button:is( + [data-image-fit="contain"], + [data-image-fit="cover"] + ):has(> .bloom-translationGroup) + .bloom-translationGroup { + padding: 0.6em; + width: 100%; + box-sizing: border-box; +} + .bloom-page .bloom-canvas-button { display: inline-flex; // for when it has both image and text