Skip to content

Instantly share code, notes, and snippets.

@annelyse
Created April 25, 2023 16:46
Show Gist options
  • Save annelyse/79bd38f196e4f14dc2784d058961a5b8 to your computer and use it in GitHub Desktop.
Save annelyse/79bd38f196e4f14dc2784d058961a5b8 to your computer and use it in GitHub Desktop.
/**
* External dependencies
*/
import { isEmpty } from "lodash";
/**
* WordPress dependencies
*/
const { isBlobURL } = wp.blob;
const {
ExternalLink,
PanelBody,
ResizableBox,
Spinner,
TextareaControl,
TextControl,
ToolbarButton,
} = wp.components;
const { useViewportMatch, usePrevious } = wp.compose;
const { useSelect, useDispatch } = wp.data;
const {
BlockControls,
InspectorControls,
RichText,
__experimentalImageSizeControl,
__experimentalImageURLInputUI,
MediaReplaceFlow,
store,
BlockAlignmentControl,
__experimentalImageEditor,
__experimentalGetElementClassName,
__experimentalUseBorderProps,
} = wp.blockEditor;
const { useEffect, useMemo, useState, useRef, useCallback } = wp.element;
const { __, sprintf, isRTL } = wp.i18n;
const { getFilename } = wp.url;
const { registerBlockType, createBlock, getDefaultBlockName, switchToBlockType } =
wp.blocks;
import {
crop,
overlayText,
upload,
caption as captionIcon,
} from "@wordpress/icons";
import { store as noticesStore } from "@wordpress/notices";
import { store as coreStore } from "@wordpress/core-data";
/**
* Internal dependencies
*/
import { createUpgradedEmbedBlock } from "../embed/util";
import useClientWidth from "./use-client-width";
import { isExternalImage } from "./edit";
/**
* Module constants
*/
// import { MIN_SIZE, ALLOWED_MEDIA_TYPES } from "./constants";
export const MIN_SIZE = 20;
export const LINK_DESTINATION_NONE = 'none';
export const LINK_DESTINATION_MEDIA = 'media';
export const LINK_DESTINATION_ATTACHMENT = 'attachment';
export const LINK_DESTINATION_CUSTOM = 'custom';
export const NEW_TAB_REL = [ 'noreferrer', 'noopener' ];
export const ALLOWED_MEDIA_TYPES = [ 'image' ];
export const MEDIA_ID_NO_FEATURED_IMAGE_SET = 0;
export default function blocTest() {
const BLOCKNAME = "block-edit";
const BLOCKPATH = `wp-gb/${BLOCKNAME}`;
registerBlockType(BLOCKPATH, {
apiVersion: 2,
title: __(BLOCKNAME.replace("-", " ").toUpperCase(), "wp-gb"),
description: __("The description"),
category: "wp-gb",
icon: "smiley",
attributes: {
heading: {
type: "string",
},
content: {
type: "string",
},
image: {
type: "string",
},
isRounded: {
type: "boolean",
},
pictureID: {
// L'identifiant de l'image
type: "number",
default: null,
},
pictureURL: {
// Son URL
type: "string",
source: "attribute",
attribute: "src",
selector: "img",
},
pictureAlt: {
// Son texte alternatif
type: "string",
source: "attribute",
attribute: "alt",
selector: "img",
},
},
edit: ({ attributes, setAttributes, clientId, isSelected }) => {
console.log(attributes);
const {
url = "",
alt,
caption,
align,
id,
href,
rel,
linkClass,
linkDestination,
title,
width,
height,
linkTarget,
sizeSlug,
} = attributes;
const imageRef = useRef();
const prevCaption = usePrevious(caption);
const [showCaption, setShowCaption] = useState(!!caption);
const { allowResize = true } = context;
const { getBlock } = useSelect(store);
const { image, multiImageSelection } = useSelect(
(select) => {
const { getMedia } = select(coreStore);
const { getMultiSelectedBlockClientIds, getBlockName } =
select(store);
const multiSelectedClientIds = getMultiSelectedBlockClientIds();
return {
image:
id && isSelected
? getMedia(id, { context: "view" })
: null,
multiImageSelection:
multiSelectedClientIds.length &&
multiSelectedClientIds.every(
(_clientId) =>
getBlockName(_clientId) === "core/image"
),
};
},
[id, isSelected, clientId]
);
const {
canInsertCover,
imageEditing,
imageSizes,
maxWidth,
mediaUpload,
} = useSelect(
(select) => {
const {
getBlockRootClientId,
getSettings,
canInsertBlockType,
} = select(store);
const rootClientId = getBlockRootClientId(clientId);
const settings = getSettings();
return {
imageEditing: settings.imageEditing,
imageSizes: settings.imageSizes,
maxWidth: settings.maxWidth,
mediaUpload: settings.mediaUpload,
canInsertCover: canInsertBlockType(
"core/cover",
rootClientId
),
};
},
[clientId]
);
const { replaceBlocks, toggleSelection } = useDispatch(store);
const { createErrorNotice, createSuccessNotice } =
useDispatch(noticesStore);
const isLargeViewport = useViewportMatch("medium");
const isWideAligned = ["wide", "full"].includes(align);
const [
{ loadedNaturalWidth, loadedNaturalHeight },
setLoadedNaturalSize,
] = useState({});
const [isEditingImage, setIsEditingImage] = useState(false);
const [externalBlob, setExternalBlob] = useState();
const clientWidth = useClientWidth(containerRef, [align]);
const isResizable =
allowResize &&
!isContentLocked &&
!(isWideAligned && isLargeViewport);
const imageSizeOptions = imageSizes
.filter(
({ slug }) => image?.media_details?.sizes?.[slug]?.source_url
)
.map(({ name, slug }) => ({ value: slug, label: name }));
const canUploadMedia = !!mediaUpload;
// If an image is externally hosted, try to fetch the image data. This may
// fail if the image host doesn't allow CORS with the domain. If it works,
// we can enable a button in the toolbar to upload the image.
useEffect(() => {
if (
!isExternalImage(id, url) ||
!isSelected ||
!canUploadMedia ||
externalBlob
) {
return;
}
window
.fetch(url)
.then((response) => response.blob())
.then((blob) => setExternalBlob(blob))
// Do nothing, cannot upload.
.catch(() => {});
}, [id, url, isSelected, externalBlob, canUploadMedia]);
// We need to show the caption when changes come from
// history navigation(undo/redo).
useEffect(() => {
if (caption && !prevCaption) {
setShowCaption(true);
}
}, [caption, prevCaption]);
// Focus the caption when we click to add one.
const captionRef = useCallback(
(node) => {
if (node && !caption) {
node.focus();
}
},
[caption]
);
// Get naturalWidth and naturalHeight from image ref, and fall back to loaded natural
// width and height. This resolves an issue in Safari where the loaded natural
// width and height is otherwise lost when switching between alignments.
// See: https://github.com/WordPress/gutenberg/pull/37210.
const { naturalWidth, naturalHeight } = useMemo(() => {
return {
naturalWidth:
imageRef.current?.naturalWidth ||
loadedNaturalWidth ||
undefined,
naturalHeight:
imageRef.current?.naturalHeight ||
loadedNaturalHeight ||
undefined,
};
}, [
loadedNaturalWidth,
loadedNaturalHeight,
imageRef.current?.complete,
]);
function onResizeStart() {
toggleSelection(false);
}
function onResizeStop() {
toggleSelection(true);
}
function onImageError() {
// Check if there's an embed block that handles this URL, e.g., instagram URL.
// See: https://github.com/WordPress/gutenberg/pull/11472
const embedBlock = createUpgradedEmbedBlock({
attributes: { url },
});
if (undefined !== embedBlock) {
onReplace(embedBlock);
}
}
function onSetHref(props) {
setAttributes(props);
}
function onSetTitle(value) {
// This is the HTML title attribute, separate from the media object
// title.
setAttributes({ title: value });
}
function updateAlt(newAlt) {
setAttributes({ alt: newAlt });
}
function updateImage(newSizeSlug) {
const newUrl =
image?.media_details?.sizes?.[newSizeSlug]?.source_url;
if (!newUrl) {
return null;
}
setAttributes({
url: newUrl,
width: undefined,
height: undefined,
sizeSlug: newSizeSlug,
});
}
function uploadExternal() {
mediaUpload({
filesList: [externalBlob],
onFileChange([img]) {
onSelectImage(img);
if (isBlobURL(img.url)) {
return;
}
setExternalBlob();
createSuccessNotice(__("Image uploaded."), {
type: "snackbar",
});
},
allowedTypes: ALLOWED_MEDIA_TYPES,
onError(message) {
createErrorNotice(message, { type: "snackbar" });
},
});
}
function updateAlignment(nextAlign) {
const extraUpdatedAttributes = ["wide", "full"].includes(nextAlign)
? { width: undefined, height: undefined }
: {};
setAttributes({
...extraUpdatedAttributes,
align: nextAlign,
});
}
useEffect(() => {
if (!isSelected) {
setIsEditingImage(false);
if (!caption) {
setShowCaption(false);
}
}
}, [isSelected, caption]);
const canEditImage =
id && naturalWidth && naturalHeight && imageEditing;
const allowCrop =
!multiImageSelection && canEditImage && !isEditingImage;
function switchToCover() {
replaceBlocks(
clientId,
switchToBlockType(getBlock(clientId), "core/cover")
);
}
const controls = (
<>
<BlockControls group="block">
{!isContentLocked && (
<BlockAlignmentControl
value={align}
onChange={updateAlignment}
/>
)}
{!isContentLocked && (
<ToolbarButton
onClick={() => {
setShowCaption(!showCaption);
if (showCaption && caption) {
setAttributes({ caption: undefined });
}
}}
icon={captionIcon}
isPressed={showCaption}
label={
showCaption
? __("Remove caption")
: __("Add caption")
}
/>
)}
{!multiImageSelection && !isEditingImage && (
<__experimentalImageURLInputUI
url={href || ""}
onChangeUrl={onSetHref}
linkDestination={linkDestination}
mediaUrl={(image && image.source_url) || url}
mediaLink={image && image.link}
linkTarget={linkTarget}
linkClass={linkClass}
rel={rel}
/>
)}
{allowCrop && (
<ToolbarButton
onClick={() => setIsEditingImage(true)}
icon={crop}
label={__("Crop")}
/>
)}
{externalBlob && (
<ToolbarButton
onClick={uploadExternal}
icon={upload}
label={__("Upload external image")}
/>
)}
{!multiImageSelection && canInsertCover && (
<ToolbarButton
icon={overlayText}
label={__("Add text over image")}
onClick={switchToCover}
/>
)}
</BlockControls>
{!multiImageSelection && !isEditingImage && (
<BlockControls group="other">
<MediaReplaceFlow
mediaId={id}
mediaURL={url}
allowedTypes={ALLOWED_MEDIA_TYPES}
accept="image/*"
onSelect={onSelectImage}
onSelectURL={onSelectURL}
onError={onUploadError}
/>
</BlockControls>
)}
<InspectorControls>
<PanelBody title={__("Settings")}>
{!multiImageSelection && (
<TextareaControl
__nextHasNoMarginBottom
label={__("Alternative text")}
value={alt}
onChange={updateAlt}
help={
<>
<ExternalLink href="https://www.w3.org/WAI/tutorials/images/decision-tree">
{__(
"Describe the purpose of the image."
)}
</ExternalLink>
<br />
{__("Leave empty if decorative.")}
</>
}
/>
)}
<__experimentalImageSizeControl
onChangeImage={updateImage}
onChange={(value) => setAttributes(value)}
slug={sizeSlug}
width={width}
height={height}
imageSizeOptions={imageSizeOptions}
isResizable={isResizable}
imageWidth={naturalWidth}
imageHeight={naturalHeight}
imageSizeHelp={__(
"Select the size of the source image."
)}
/>
</PanelBody>
</InspectorControls>
<InspectorControls group="advanced">
<TextControl
__nextHasNoMarginBottom
label={__("Title attribute")}
value={title || ""}
onChange={onSetTitle}
help={
<>
{__(
"Describe the role of this image on the page."
)}
<ExternalLink href="https://www.w3.org/TR/html52/dom.html#the-title-attribute">
{__(
"(Note: many devices and browsers do not display this text.)"
)}
</ExternalLink>
</>
}
/>
</InspectorControls>
</>
);
const filename = getFilename(url);
let defaultedAlt;
if (alt) {
defaultedAlt = alt;
} else if (filename) {
defaultedAlt = sprintf(
/* translators: %s: file name */
__(
"This image has an empty alt attribute; its file name is %s"
),
filename
);
} else {
defaultedAlt = __("This image has an empty alt attribute");
}
const borderProps = __experimentalUseBorderProps(attributes);
const isRounded = attributes.className?.includes("is-style-rounded");
const hasCustomBorder =
!!borderProps.className || !isEmpty(borderProps.style);
let img = (
// Disable reason: Image itself is not meant to be interactive, but
// should direct focus to block.
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
<>
<img
src={temporaryURL || url}
alt={defaultedAlt}
onError={() => onImageError()}
onLoad={(event) => {
setLoadedNaturalSize({
loadedNaturalWidth: event.target?.naturalWidth,
loadedNaturalHeight: event.target?.naturalHeight,
});
}}
ref={imageRef}
className={borderProps.className}
style={borderProps.style}
/>
{temporaryURL && <Spinner />}
</>
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
);
let imageWidthWithinContainer;
let imageHeightWithinContainer;
if (clientWidth && naturalWidth && naturalHeight) {
const exceedMaxWidth = naturalWidth > clientWidth;
const ratio = naturalHeight / naturalWidth;
imageWidthWithinContainer = exceedMaxWidth
? clientWidth
: naturalWidth;
imageHeightWithinContainer = exceedMaxWidth
? clientWidth * ratio
: naturalHeight;
}
if (canEditImage && isEditingImage) {
img = (
<__experimentalImageEditor
id={id}
url={url}
width={width}
height={height}
clientWidth={clientWidth}
naturalHeight={naturalHeight}
naturalWidth={naturalWidth}
onSaveImage={(imageAttributes) =>
setAttributes(imageAttributes)
}
onFinishEditing={() => {
setIsEditingImage(false);
}}
borderProps={isRounded ? undefined : borderProps}
/>
);
} else if (!isResizable || !imageWidthWithinContainer) {
img = <div style={{ width, height }}>{img}</div>;
} else {
const currentWidth = width || imageWidthWithinContainer;
const currentHeight = height || imageHeightWithinContainer;
const ratio = naturalWidth / naturalHeight;
const minWidth =
naturalWidth < naturalHeight ? MIN_SIZE : MIN_SIZE * ratio;
const minHeight =
naturalHeight < naturalWidth ? MIN_SIZE : MIN_SIZE / ratio;
// With the current implementation of ResizableBox, an image needs an
// explicit pixel value for the max-width. In absence of being able to
// set the content-width, this max-width is currently dictated by the
// vanilla editor style. The following variable adds a buffer to this
// vanilla style, so 3rd party themes have some wiggleroom. This does,
// in most cases, allow you to scale the image beyond the width of the
// main column, though not infinitely.
// @todo It would be good to revisit this once a content-width variable
// becomes available.
const maxWidthBuffer = maxWidth * 2.5;
let showRightHandle = false;
let showLeftHandle = false;
/* eslint-disable no-lonely-if */
// See https://github.com/WordPress/gutenberg/issues/7584.
if (align === "center") {
// When the image is centered, show both handles.
showRightHandle = true;
showLeftHandle = true;
} else if (isRTL()) {
// In RTL mode the image is on the right by default.
// Show the right handle and hide the left handle only when it is
// aligned left. Otherwise always show the left handle.
if (align === "left") {
showRightHandle = true;
} else {
showLeftHandle = true;
}
} else {
// Show the left handle and hide the right handle only when the
// image is aligned right. Otherwise always show the right handle.
if (align === "right") {
showLeftHandle = true;
} else {
showRightHandle = true;
}
}
/* eslint-enable no-lonely-if */
img = (
<ResizableBox
size={{
width: width ?? "auto",
height: height && !hasCustomBorder ? height : "auto",
}}
showHandle={isSelected}
minWidth={minWidth}
maxWidth={maxWidthBuffer}
minHeight={minHeight}
maxHeight={maxWidthBuffer / ratio}
lockAspectRatio
enable={{
top: false,
right: showRightHandle,
bottom: true,
left: showLeftHandle,
}}
onResizeStart={onResizeStart}
onResizeStop={(event, direction, elt, delta) => {
onResizeStop();
setAttributes({
width: parseInt(currentWidth + delta.width, 10),
height: parseInt(currentHeight + delta.height, 10),
});
}}
resizeRatio={align === "center" ? 2 : 1}
>
{img}
</ResizableBox>
);
}
return (
<>
{/* Hide controls during upload to avoid component remount,
which causes duplicated image upload. */}
{!temporaryURL && controls}
{img}
{showCaption && (!RichText.isEmpty(caption) || isSelected) && (
<RichText
identifier="caption"
className={__experimentalGetElementClassName("caption")}
ref={captionRef}
tagName="figcaption"
aria-label={__("Image caption text")}
placeholder={__("Add caption")}
value={caption}
onChange={(value) => setAttributes({ caption: value })}
inlineToolbar
__unstableOnSplitAtEnd={() =>
insertBlocksAfter(
createBlock(getDefaultBlockName())
)
}
/>
)}
</>
);
},
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment