Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Page Builder - Use object HTML Tag When Rendering SVGs #4041

Merged
merged 2 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
115 changes: 78 additions & 37 deletions packages/app-page-builder-elements/src/renderers/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import { useRenderer } from "~/hooks/useRenderer";
import { LinkComponent as LinkComponentType } from "~/types";
import { DefaultLinkComponent } from "~/renderers/components";

declare global {
// eslint-disable-next-line
namespace JSX {
interface IntrinsicElements {
// "object" HTML tags cannot be clicked, hence the need for "pb-image-object-wrapper" wrapper.
"pb-image-object-wrapper": any;
}
}
}

export interface ImageElementData {
image?: {
title: string;
Expand All @@ -13,6 +23,7 @@ export interface ImageElementData {
file?: {
src: string;
};
htmlTag?: string;
};
link?: {
newTab: boolean;
Expand All @@ -30,6 +41,17 @@ const PbImg = styled.img`
height: ${props => props.height};
`;

const PbImgObject = styled.object`
max-width: 100%;
width: ${props => props.width};
height: ${props => props.height};

// "object" HTML tags cannot be clicked, hence the need for the wrapper
// "pb-image-object-wrapper" tag, which handles click events. For this
// to work, we need to set "pointer-events: none" on the object tag.
pointer-events: none;
`;

export const ImageRendererComponent = ({
onClick,
renderEmpty,
Expand All @@ -44,48 +66,67 @@ export const ImageRendererComponent = ({
let content;
if (element.data?.image?.file?.src) {
const { title, width, height, file } = element.data.image;
let { htmlTag } = element.data.image;
const { src } = value || file;

// If a fixed image width in pixels was set, let's filter out unneeded
// image resize widths. For example, if 155px was set as the fixed image
// width, then we want the `srcset` attribute to only contain 100w and 300w.
let srcSetWidths: number[] = [];

if (width && width.endsWith("px")) {
const imageWidthInt = parseInt(width);
for (let i = 0; i < SUPPORTED_IMAGE_RESIZE_WIDTHS.length; i++) {
const supportedResizeWidth = SUPPORTED_IMAGE_RESIZE_WIDTHS[i];
if (imageWidthInt > supportedResizeWidth) {
srcSetWidths.push(supportedResizeWidth);
} else {
srcSetWidths.push(supportedResizeWidth);
break;
if (htmlTag === "auto") {
htmlTag = src.endsWith(".svg") ? "object" : "img";
}

if (htmlTag === "object") {
content = (
<pb-image-object-wrapper onClick={onClick}>
<PbImgObject
// Image has its width / height set from its own settings.
width={width}
height={height}
title={title}
data={src}
/>
</pb-image-object-wrapper>
);
} else {
// If a fixed image width in pixels was set, let's filter out unneeded
// image resize widths. For example, if 155px was set as the fixed image
// width, then we want the `srcset` attribute to only contain 100w and 300w.
let srcSetWidths: number[] = [];

if (width && width.endsWith("px")) {
const imageWidthInt = parseInt(width);
for (let i = 0; i < SUPPORTED_IMAGE_RESIZE_WIDTHS.length; i++) {
const supportedResizeWidth = SUPPORTED_IMAGE_RESIZE_WIDTHS[i];
if (imageWidthInt > supportedResizeWidth) {
srcSetWidths.push(supportedResizeWidth);
} else {
srcSetWidths.push(supportedResizeWidth);
break;
}
}
} else {
// If a fixed image width was not provided, we
// rely on all the supported image resize widths.
srcSetWidths = SUPPORTED_IMAGE_RESIZE_WIDTHS;
}
} else {
// If a fixed image width was not provided, we
// rely on all the supported image resize widths.
srcSetWidths = SUPPORTED_IMAGE_RESIZE_WIDTHS;
}

const srcSet = srcSetWidths
.map(item => {
return `${src}?width=${item} ${item}w`;
})
.join(", ");

content = (
<PbImg
// Image has its width / height set from its own settings.
width={width}
height={height}
alt={title}
title={title}
src={src}
srcSet={srcSet}
onClick={onClick}
/>
);
const srcSet = srcSetWidths
.map(item => {
return `${src}?width=${item} ${item}w`;
})
.join(", ");

content = (
<PbImg
// Image has its width / height set from its own settings.
width={width}
height={height}
alt={title}
title={title}
src={src}
srcSet={srcSet}
onClick={onClick}
/>
);
}
} else {
content = renderEmpty || null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
WIDTH_UNIT_OPTIONS,
HEIGHT_UNIT_OPTIONS
} from "~/editor/plugins/elementSettings/elementSettingsUtils";
import SelectField from "~/editor/plugins/elementSettings/components/SelectField";

const classes = {
grid: css({
Expand Down Expand Up @@ -45,6 +46,7 @@ const ImageSettings = ({
const updateTitle = useCallback(value => getUpdateValue("title")(value), []);
const updateWidth = useCallback(value => getUpdateValue("width")(value), []);
const updateHeight = useCallback(value => getUpdateValue("height")(value), []);
const updateHtmlTag = useCallback(value => getUpdateValue("htmlTag")(value), []);

return (
<Accordion title={"Image"} defaultValue={defaultAccordionValue}>
Expand Down Expand Up @@ -78,6 +80,18 @@ const ImageSettings = ({
className={spacingPickerStyle}
/>
</Wrapper>
<Wrapper
label={"HTML Tag"}
containerClassName={classes.grid}
leftCellSpan={4}
rightCellSpan={8}
>
<SelectField value={image?.htmlTag || "img"} onChange={updateHtmlTag}>
<option value={"auto"}>{"Auto-detect"}</option>
<option value={"img"}>{"<img>"}</option>
<option value={"object"}>{"<object>"} (for SVGs)</option>
</SelectField>
</Wrapper>
</>
</Accordion>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useCallback } from "react";
import { FileManager, SingleImageUploadProps } from "@webiny/app-admin";
import { UpdateElementActionEvent } from "~/editor/recoil/actions";
import pick from "lodash/pick";
import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler";
import { ImageRendererComponent } from "@webiny/app-page-builder-elements/renderers/image";
import { AddImageIconWrapper, AddImageWrapper } from "@webiny/ui/ImageUpload/styled";
Expand Down Expand Up @@ -33,18 +32,19 @@ const PeImage = createRenderer(() => {

const onChange = useCallback<NonNullable<SingleImageUploadProps["onChange"]>>(
file => {
const elementClone = structuredClone(element);
if (file) {
const { id, src } = file;
elementClone.data.image = {
...elementClone.data.image,
file: { id, src },
htmlTag: "auto"
};
}

handler.trigger(
new UpdateElementActionEvent({
element: {
...element,
data: {
...element.data,
image: {
...(element.data.image || {}),
file: file ? pick(file, ["id", "src"]) : undefined
}
}
},
element: elementClone,
history: true
})
);
Expand Down
1 change: 1 addition & 0 deletions packages/app-page-builder/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export interface PbElementDataImageType {
src?: string;
};
title?: string;
htmlTag?: string;
}

export interface PbElementDataIconType {
Expand Down