Plugin Tabs noticias
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "areoi/media-grid-image",
|
||||
"title": "Image",
|
||||
"category": "areoi-components",
|
||||
"parent": [ "areoi/media-grid" ],
|
||||
"usesContext": [ "allowResize", "imageCrop", "fixedHeight" ],
|
||||
"description": "Insert an image to make a visual statement.",
|
||||
"keywords": [ "img", "photo", "picture" ],
|
||||
"textdomain": "default",
|
||||
"example": {
|
||||
"attributes": {
|
||||
"preview": true
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"preview": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"block_id": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"parent_id": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"align": {
|
||||
"type": "string"
|
||||
},
|
||||
"media_fit": {
|
||||
"type": "string",
|
||||
"default": "cover"
|
||||
},
|
||||
"media_height": {
|
||||
"type": "string",
|
||||
"default": "50"
|
||||
},
|
||||
"media_width": {
|
||||
"type": "string",
|
||||
"default": "100"
|
||||
},
|
||||
"media_align": {
|
||||
"type": "string",
|
||||
"default": "center"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "img",
|
||||
"attribute": "alt",
|
||||
"default": ""
|
||||
},
|
||||
"caption": {
|
||||
"type": "string",
|
||||
"source": "html",
|
||||
"selector": "figcaption"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "img",
|
||||
"attribute": "title"
|
||||
},
|
||||
"href": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "figure > a",
|
||||
"attribute": "href"
|
||||
},
|
||||
"rel": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "figure > a",
|
||||
"attribute": "rel"
|
||||
},
|
||||
"linkClass": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "figure > a",
|
||||
"attribute": "class"
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"width": {
|
||||
"type": "number"
|
||||
},
|
||||
"height": {
|
||||
"type": "number"
|
||||
},
|
||||
"sizeSlug": {
|
||||
"type": "string"
|
||||
},
|
||||
"linkDestination": {
|
||||
"type": "string"
|
||||
},
|
||||
"linkTarget": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "figure > a",
|
||||
"attribute": "target"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"anchor": true,
|
||||
"color": {
|
||||
"__experimentalDuotone": "img",
|
||||
"text": false,
|
||||
"background": false
|
||||
},
|
||||
"__experimentalBorder": {
|
||||
"radius": true,
|
||||
"__experimentalDefaultControls": {
|
||||
"radius": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"editorStyle": "wp-block-image-editor",
|
||||
"style": "wp-block-image"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
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;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { RichText, useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
const blockAttributes = {
|
||||
align: {
|
||||
type: 'string',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'src',
|
||||
},
|
||||
alt: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'alt',
|
||||
default: '',
|
||||
},
|
||||
caption: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: 'figcaption',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'figure > a',
|
||||
attribute: 'href',
|
||||
},
|
||||
rel: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'figure > a',
|
||||
attribute: 'rel',
|
||||
},
|
||||
linkClass: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'figure > a',
|
||||
attribute: 'class',
|
||||
},
|
||||
id: {
|
||||
type: 'number',
|
||||
},
|
||||
width: {
|
||||
type: 'number',
|
||||
},
|
||||
height: {
|
||||
type: 'number',
|
||||
},
|
||||
linkDestination: {
|
||||
type: 'string',
|
||||
},
|
||||
linkTarget: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'figure > a',
|
||||
attribute: 'target',
|
||||
},
|
||||
};
|
||||
|
||||
const blockSupports = {
|
||||
anchor: true,
|
||||
color: {
|
||||
__experimentalDuotone: 'img',
|
||||
text: false,
|
||||
background: false,
|
||||
},
|
||||
__experimentalBorder: {
|
||||
radius: true,
|
||||
__experimentalDefaultControls: {
|
||||
radius: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const deprecated = [
|
||||
{
|
||||
attributes: {
|
||||
...blockAttributes,
|
||||
title: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'title',
|
||||
},
|
||||
sizeSlug: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
supports: blockSupports,
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
align,
|
||||
href,
|
||||
rel,
|
||||
linkClass,
|
||||
width,
|
||||
height,
|
||||
id,
|
||||
linkTarget,
|
||||
sizeSlug,
|
||||
title,
|
||||
} = attributes;
|
||||
|
||||
const newRel = isEmpty( rel ) ? undefined : rel;
|
||||
|
||||
const classes = classnames( {
|
||||
[ `align${ align }` ]: align,
|
||||
[ `size-${ sizeSlug }` ]: sizeSlug,
|
||||
'is-resized': width || height,
|
||||
} );
|
||||
|
||||
const image = (
|
||||
<img
|
||||
src={ url }
|
||||
alt={ alt }
|
||||
className={ id ? `wp-image-${ id }` : null }
|
||||
width={ width }
|
||||
height={ height }
|
||||
title={ title }
|
||||
/>
|
||||
);
|
||||
|
||||
const figure = (
|
||||
<>
|
||||
{ href ? (
|
||||
<a
|
||||
className={ linkClass }
|
||||
href={ href }
|
||||
target={ linkTarget }
|
||||
rel={ newRel }
|
||||
>
|
||||
{ image }
|
||||
</a>
|
||||
) : (
|
||||
image
|
||||
) }
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
value={ caption }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
|
||||
if ( 'left' === align || 'right' === align || 'center' === align ) {
|
||||
return (
|
||||
<div { ...useBlockProps.save() }>
|
||||
<figure className={ classes }>{ figure }</figure>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<figure { ...useBlockProps.save( { className: classes } ) }>
|
||||
{ figure }
|
||||
</figure>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
attributes: blockAttributes,
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
align,
|
||||
href,
|
||||
width,
|
||||
height,
|
||||
id,
|
||||
} = attributes;
|
||||
|
||||
const classes = classnames( {
|
||||
[ `align${ align }` ]: align,
|
||||
'is-resized': width || height,
|
||||
} );
|
||||
|
||||
const image = (
|
||||
<img
|
||||
src={ url }
|
||||
alt={ alt }
|
||||
className={ id ? `wp-image-${ id }` : null }
|
||||
width={ width }
|
||||
height={ height }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<figure className={ classes }>
|
||||
{ href ? <a href={ href }>{ image }</a> : image }
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
value={ caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
attributes: blockAttributes,
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
align,
|
||||
href,
|
||||
width,
|
||||
height,
|
||||
id,
|
||||
} = attributes;
|
||||
|
||||
const image = (
|
||||
<img
|
||||
src={ url }
|
||||
alt={ alt }
|
||||
className={ id ? `wp-image-${ id }` : null }
|
||||
width={ width }
|
||||
height={ height }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<figure className={ align ? `align${ align }` : null }>
|
||||
{ href ? <a href={ href }>{ image }</a> : image }
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
value={ caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
attributes: blockAttributes,
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
align,
|
||||
href,
|
||||
width,
|
||||
height,
|
||||
} = attributes;
|
||||
const extraImageProps = width || height ? { width, height } : {};
|
||||
const image = (
|
||||
<img src={ url } alt={ alt } { ...extraImageProps } />
|
||||
);
|
||||
|
||||
let figureStyle = {};
|
||||
|
||||
if ( width ) {
|
||||
figureStyle = { width };
|
||||
} else if ( align === 'left' || align === 'right' ) {
|
||||
figureStyle = { maxWidth: '50%' };
|
||||
}
|
||||
|
||||
return (
|
||||
<figure
|
||||
className={ align ? `align${ align }` : null }
|
||||
style={ figureStyle }
|
||||
>
|
||||
{ href ? <a href={ href }>{ image }</a> : image }
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
value={ caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default deprecated;
|
||||
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { get, has, omit, pick } from 'lodash';
|
||||
import * as areoi from '../_components/Core.js';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob';
|
||||
import { withNotices } from '@wordpress/components';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import {
|
||||
BlockAlignmentControl,
|
||||
BlockControls,
|
||||
BlockIcon,
|
||||
MediaPlaceholder,
|
||||
useBlockProps,
|
||||
store as blockEditorStore,
|
||||
} from '@wordpress/block-editor';
|
||||
import { useEffect, useRef, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { image as icon } from '@wordpress/icons';
|
||||
|
||||
/* global wp */
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Image from './image';
|
||||
|
||||
/**
|
||||
* Module constants
|
||||
*/
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_CUSTOM,
|
||||
LINK_DESTINATION_MEDIA,
|
||||
LINK_DESTINATION_NONE,
|
||||
ALLOWED_MEDIA_TYPES,
|
||||
} from './constants';
|
||||
|
||||
export const pickRelevantMediaFiles = ( image, size ) => {
|
||||
const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] );
|
||||
imageProps.url =
|
||||
get( image, [ 'sizes', size, 'url' ] ) ||
|
||||
get( image, [ 'media_details', 'sizes', size, 'source_url' ] ) ||
|
||||
image.url;
|
||||
return imageProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Is the URL a temporary blob URL? A blob URL is one that is used temporarily
|
||||
* while the image is being uploaded and will not have an id yet allocated.
|
||||
*
|
||||
* @param {number=} id The id of the image.
|
||||
* @param {string=} url The url of the image.
|
||||
*
|
||||
* @return {boolean} Is the URL a Blob URL
|
||||
*/
|
||||
const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url );
|
||||
|
||||
/**
|
||||
* Is the url for the image hosted externally. An externally hosted image has no
|
||||
* id and is not a blob url.
|
||||
*
|
||||
* @param {number=} id The id of the image.
|
||||
* @param {string=} url The url of the image.
|
||||
*
|
||||
* @return {boolean} Is the url an externally hosted url?
|
||||
*/
|
||||
export const isExternalImage = ( id, url ) => url && ! id && ! isBlobURL( url );
|
||||
|
||||
/**
|
||||
* Checks if WP generated default image size. Size generation is skipped
|
||||
* when the image is smaller than the said size.
|
||||
*
|
||||
* @param {Object} image
|
||||
* @param {string} defaultSize
|
||||
*
|
||||
* @return {boolean} Whether or not it has default image size.
|
||||
*/
|
||||
function hasDefaultSize( image, defaultSize ) {
|
||||
return (
|
||||
has( image, [ 'sizes', defaultSize, 'url' ] ) ||
|
||||
has( image, [ 'media_details', 'sizes', defaultSize, 'source_url' ] )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a media attachment object has been "destroyed",
|
||||
* that is, removed from the media library. The core Media Library
|
||||
* adds a `destroyed` property to a deleted attachment object in the media collection.
|
||||
*
|
||||
* @param {number} id The attachment id.
|
||||
*
|
||||
* @return {boolean} Whether the image has been destroyed.
|
||||
*/
|
||||
export function isMediaDestroyed( id ) {
|
||||
const attachment = wp?.media?.attachment( id ) || {};
|
||||
return attachment.destroyed;
|
||||
}
|
||||
|
||||
export function ImageEdit( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
isSelected,
|
||||
className,
|
||||
noticeUI,
|
||||
insertBlocksAfter,
|
||||
noticeOperations,
|
||||
onReplace,
|
||||
context,
|
||||
clientId,
|
||||
} ) {
|
||||
const {
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
align,
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
sizeSlug,
|
||||
} = attributes;
|
||||
const [ temporaryURL, setTemporaryURL ] = useState();
|
||||
|
||||
const altRef = useRef();
|
||||
useEffect( () => {
|
||||
altRef.current = alt;
|
||||
}, [ alt ] );
|
||||
|
||||
const captionRef = useRef();
|
||||
useEffect( () => {
|
||||
captionRef.current = caption;
|
||||
}, [ caption ] );
|
||||
|
||||
const ref = useRef();
|
||||
const { imageDefaultSize, mediaUpload } = useSelect( ( select ) => {
|
||||
const { getSettings } = select( blockEditorStore );
|
||||
return pick( getSettings(), [ 'imageDefaultSize', 'mediaUpload' ] );
|
||||
}, [] );
|
||||
|
||||
// A callback passed to MediaUpload,
|
||||
// fired when the media modal closes.
|
||||
function onCloseModal() {
|
||||
if ( isMediaDestroyed( attributes?.id ) ) {
|
||||
setAttributes( {
|
||||
url: undefined,
|
||||
id: undefined,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Runs an error callback if the image does not load.
|
||||
If the error callback is triggered, we infer that that image
|
||||
has been deleted.
|
||||
*/
|
||||
function onImageError( isReplaced = false ) {
|
||||
// If the image block was not replaced with an embed,
|
||||
// clear the attributes and trigger the placeholder.
|
||||
if ( ! isReplaced ) {
|
||||
setAttributes( {
|
||||
url: undefined,
|
||||
id: undefined,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
function onUploadError( message ) {
|
||||
noticeOperations.removeAllNotices();
|
||||
noticeOperations.createErrorNotice( message );
|
||||
setAttributes( {
|
||||
src: undefined,
|
||||
id: undefined,
|
||||
url: undefined,
|
||||
} );
|
||||
setTemporaryURL( undefined );
|
||||
}
|
||||
|
||||
function onSelectImage( media ) {
|
||||
if ( ! media || ! media.url ) {
|
||||
setAttributes( {
|
||||
url: undefined,
|
||||
alt: undefined,
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
caption: undefined,
|
||||
} );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isBlobURL( media.url ) ) {
|
||||
setTemporaryURL( media.url );
|
||||
return;
|
||||
}
|
||||
|
||||
setTemporaryURL();
|
||||
|
||||
let mediaAttributes = pickRelevantMediaFiles( media, imageDefaultSize );
|
||||
|
||||
// If a caption text was meanwhile written by the user,
|
||||
// make sure the text is not overwritten by empty captions.
|
||||
if ( captionRef.current && ! get( mediaAttributes, [ 'caption' ] ) ) {
|
||||
mediaAttributes = omit( mediaAttributes, [ 'caption' ] );
|
||||
}
|
||||
|
||||
let additionalAttributes;
|
||||
// Reset the dimension attributes if changing to a different image.
|
||||
if ( ! media.id || media.id !== id ) {
|
||||
additionalAttributes = {
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
// Fallback to size "full" if there's no default image size.
|
||||
// It means the image is smaller, and the block will use a full-size URL.
|
||||
sizeSlug: hasDefaultSize( media, imageDefaultSize )
|
||||
? imageDefaultSize
|
||||
: 'full',
|
||||
};
|
||||
} else {
|
||||
// Keep the same url when selecting the same file, so "Image Size"
|
||||
// option is not changed.
|
||||
additionalAttributes = { url };
|
||||
}
|
||||
|
||||
// Check if default link setting should be used.
|
||||
let linkDestination = attributes.linkDestination;
|
||||
if ( ! linkDestination ) {
|
||||
// Use the WordPress option to determine the proper default.
|
||||
// The constants used in Gutenberg do not match WP options so a little more complicated than ideal.
|
||||
// TODO: fix this in a follow up PR, requires updating media-text and ui component.
|
||||
switch (
|
||||
wp?.media?.view?.settings?.defaultProps?.link ||
|
||||
LINK_DESTINATION_NONE
|
||||
) {
|
||||
case 'file':
|
||||
case LINK_DESTINATION_MEDIA:
|
||||
linkDestination = LINK_DESTINATION_MEDIA;
|
||||
break;
|
||||
case 'post':
|
||||
case LINK_DESTINATION_ATTACHMENT:
|
||||
linkDestination = LINK_DESTINATION_ATTACHMENT;
|
||||
break;
|
||||
case LINK_DESTINATION_CUSTOM:
|
||||
linkDestination = LINK_DESTINATION_CUSTOM;
|
||||
break;
|
||||
case LINK_DESTINATION_NONE:
|
||||
linkDestination = LINK_DESTINATION_NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the image is linked to it's media.
|
||||
let href;
|
||||
switch ( linkDestination ) {
|
||||
case LINK_DESTINATION_MEDIA:
|
||||
href = media.url;
|
||||
break;
|
||||
case LINK_DESTINATION_ATTACHMENT:
|
||||
href = media.link;
|
||||
break;
|
||||
}
|
||||
mediaAttributes.href = href;
|
||||
|
||||
setAttributes( {
|
||||
...mediaAttributes,
|
||||
...additionalAttributes,
|
||||
linkDestination,
|
||||
} );
|
||||
}
|
||||
|
||||
function onSelectURL( newURL ) {
|
||||
if ( newURL !== url ) {
|
||||
setAttributes( {
|
||||
url: newURL,
|
||||
id: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
sizeSlug: imageDefaultSize,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
function updateAlignment( nextAlign ) {
|
||||
const extraUpdatedAttributes = [ 'wide', 'full' ].includes( nextAlign )
|
||||
? { width: undefined, height: undefined }
|
||||
: {};
|
||||
setAttributes( {
|
||||
...extraUpdatedAttributes,
|
||||
align: nextAlign,
|
||||
} );
|
||||
}
|
||||
|
||||
let isTemp = isTemporaryImage( id, url );
|
||||
|
||||
// Upload a temporary image on mount.
|
||||
useEffect( () => {
|
||||
if ( ! isTemp ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = getBlobByURL( url );
|
||||
|
||||
if ( file ) {
|
||||
mediaUpload( {
|
||||
filesList: [ file ],
|
||||
onFileChange: ( [ img ] ) => {
|
||||
onSelectImage( img );
|
||||
},
|
||||
allowedTypes: ALLOWED_MEDIA_TYPES,
|
||||
onError: ( message ) => {
|
||||
isTemp = false;
|
||||
onUploadError( message );
|
||||
},
|
||||
} );
|
||||
}
|
||||
}, [] );
|
||||
|
||||
// If an image is temporary, revoke the Blob url when it is uploaded (and is
|
||||
// no longer temporary).
|
||||
useEffect( () => {
|
||||
if ( isTemp ) {
|
||||
setTemporaryURL( url );
|
||||
return;
|
||||
}
|
||||
revokeBlobURL( temporaryURL );
|
||||
}, [ isTemp, url ] );
|
||||
|
||||
const isExternal = isExternalImage( id, url );
|
||||
const src = isExternal ? url : undefined;
|
||||
|
||||
var fit_class = attributes['media_fit'] ? attributes['media_fit'] : 'cover';
|
||||
var align_class = attributes['media_align'] ? attributes['media_align'] : 'center';
|
||||
var media_height = attributes['media_fit'] && attributes['media_fit'] == 'set' ? ( attributes['media_height'] ? attributes['media_height'] : '50' ) : false;
|
||||
var media_width = attributes['media_fit'] && attributes['media_fit'] == 'set' ? ( attributes['media_width'] ? attributes['media_width'] : '100' ) : false;
|
||||
|
||||
var style = {};
|
||||
if ( media_height ) {
|
||||
style['max-height'] = media_height + 'px';
|
||||
}
|
||||
if ( media_width ) {
|
||||
style['max-width'] = media_width + 'px';
|
||||
}
|
||||
|
||||
const mediaPreview = !! url && (
|
||||
<div class="areoi-media position-relative">
|
||||
<div class={ 'areoi-media-container ' + fit_class + ' ' + align_class }>
|
||||
<img
|
||||
alt={ __( 'Edit image' ) }
|
||||
title={ __( 'Edit image' ) }
|
||||
className={ 'edit-image-preview' }
|
||||
src={ url }
|
||||
style={ style }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const classes = classnames( className, {
|
||||
'is-transient': temporaryURL,
|
||||
'is-resized': !! width || !! height,
|
||||
[ `size-${ sizeSlug }` ]: sizeSlug,
|
||||
} );
|
||||
|
||||
const blockProps = useBlockProps( {
|
||||
ref,
|
||||
className: classes + ' areoi-content-grid-item',
|
||||
} );
|
||||
|
||||
function onChange( key, value ) {
|
||||
setAttributes( { [key]: value } );
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{ areoi.DisplayPreview( areoi, attributes, onChange, 'media-grid-image' ) }
|
||||
|
||||
{ !attributes.preview &&
|
||||
<figure { ...blockProps }>
|
||||
{ ( temporaryURL || url ) && (
|
||||
<Image
|
||||
temporaryURL={ temporaryURL }
|
||||
attributes={ attributes }
|
||||
setAttributes={ setAttributes }
|
||||
isSelected={ isSelected }
|
||||
insertBlocksAfter={ insertBlocksAfter }
|
||||
onReplace={ onReplace }
|
||||
onSelectImage={ onSelectImage }
|
||||
onSelectURL={ onSelectURL }
|
||||
onUploadError={ onUploadError }
|
||||
containerRef={ ref }
|
||||
context={ context }
|
||||
clientId={ clientId }
|
||||
onCloseModal={ onCloseModal }
|
||||
onImageLoadError={ onImageError }
|
||||
/>
|
||||
) }
|
||||
{ ! url && (
|
||||
<BlockControls group="block">
|
||||
<BlockAlignmentControl
|
||||
value={ align }
|
||||
onChange={ updateAlignment }
|
||||
/>
|
||||
</BlockControls>
|
||||
) }
|
||||
|
||||
<MediaPlaceholder
|
||||
icon={ <BlockIcon icon={ icon } /> }
|
||||
onSelect={ onSelectImage }
|
||||
onSelectURL={ onSelectURL }
|
||||
notices={ noticeUI }
|
||||
onError={ onUploadError }
|
||||
onClose={ onCloseModal }
|
||||
accept="image/*"
|
||||
allowedTypes={ ALLOWED_MEDIA_TYPES }
|
||||
value={ { id, src } }
|
||||
mediaPreview={ mediaPreview }
|
||||
disableMediaButtons={ temporaryURL || url }
|
||||
/>
|
||||
|
||||
</figure>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withNotices( ImageEdit );
|
||||
@@ -0,0 +1,896 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { View, TouchableWithoutFeedback } from 'react-native';
|
||||
import { useRoute } from '@react-navigation/native';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Component, useEffect } from '@wordpress/element';
|
||||
import {
|
||||
requestMediaImport,
|
||||
mediaUploadSync,
|
||||
requestImageFailedRetryDialog,
|
||||
requestImageUploadCancelDialog,
|
||||
requestImageFullscreenPreview,
|
||||
setFeaturedImage,
|
||||
} from '@wordpress/react-native-bridge';
|
||||
import {
|
||||
Icon,
|
||||
PanelBody,
|
||||
ToolbarButton,
|
||||
ToolbarGroup,
|
||||
Image,
|
||||
WIDE_ALIGNMENTS,
|
||||
LinkSettingsNavigation,
|
||||
BottomSheet,
|
||||
BottomSheetTextControl,
|
||||
BottomSheetSelectControl,
|
||||
FooterMessageControl,
|
||||
FooterMessageLink,
|
||||
Badge,
|
||||
} from '@wordpress/components';
|
||||
import {
|
||||
BlockCaption,
|
||||
MediaPlaceholder,
|
||||
MediaUpload,
|
||||
MediaUploadProgress,
|
||||
MEDIA_TYPE_IMAGE,
|
||||
BlockControls,
|
||||
InspectorControls,
|
||||
BlockAlignmentToolbar,
|
||||
BlockStyles,
|
||||
store as blockEditorStore,
|
||||
blockSettingsScreens,
|
||||
} from '@wordpress/block-editor';
|
||||
import { __, _x, sprintf } from '@wordpress/i18n';
|
||||
import { getProtocol, hasQueryArg } from '@wordpress/url';
|
||||
import { doAction, hasAction } from '@wordpress/hooks';
|
||||
import { compose, withPreferredColorScheme } from '@wordpress/compose';
|
||||
import { withSelect, withDispatch } from '@wordpress/data';
|
||||
import {
|
||||
image as placeholderIcon,
|
||||
replace,
|
||||
fullscreen,
|
||||
textColor,
|
||||
} from '@wordpress/icons';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
import { store as editPostStore } from '@wordpress/edit-post';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import styles from './styles.scss';
|
||||
import { getUpdatedLinkTargetSettings } from './utils';
|
||||
|
||||
import {
|
||||
LINK_DESTINATION_NONE,
|
||||
LINK_DESTINATION_CUSTOM,
|
||||
LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_MEDIA,
|
||||
MEDIA_ID_NO_FEATURED_IMAGE_SET,
|
||||
} from './constants';
|
||||
|
||||
const getUrlForSlug = ( image, sizeSlug ) => {
|
||||
if ( ! sizeSlug ) {
|
||||
return undefined;
|
||||
}
|
||||
return image?.media_details?.sizes?.[ sizeSlug ]?.source_url;
|
||||
};
|
||||
|
||||
function LinkSettings( {
|
||||
attributes,
|
||||
image,
|
||||
isLinkSheetVisible,
|
||||
setMappedAttributes,
|
||||
} ) {
|
||||
const route = useRoute();
|
||||
const { href: url, label, linkDestination, linkTarget, rel } = attributes;
|
||||
|
||||
// Persist attributes passed from child screen.
|
||||
useEffect( () => {
|
||||
const { inputValue: newUrl } = route.params || {};
|
||||
|
||||
let newLinkDestination;
|
||||
switch ( newUrl ) {
|
||||
case attributes.url:
|
||||
newLinkDestination = LINK_DESTINATION_MEDIA;
|
||||
break;
|
||||
case image?.link:
|
||||
newLinkDestination = LINK_DESTINATION_ATTACHMENT;
|
||||
break;
|
||||
case '':
|
||||
newLinkDestination = LINK_DESTINATION_NONE;
|
||||
break;
|
||||
default:
|
||||
newLinkDestination = LINK_DESTINATION_CUSTOM;
|
||||
break;
|
||||
}
|
||||
|
||||
setMappedAttributes( {
|
||||
url: newUrl,
|
||||
linkDestination: newLinkDestination,
|
||||
} );
|
||||
}, [ route.params?.inputValue ] );
|
||||
|
||||
let valueMask;
|
||||
switch ( linkDestination ) {
|
||||
case LINK_DESTINATION_MEDIA:
|
||||
valueMask = __( 'Media File' );
|
||||
break;
|
||||
case LINK_DESTINATION_ATTACHMENT:
|
||||
valueMask = __( 'Attachment Page' );
|
||||
break;
|
||||
case LINK_DESTINATION_CUSTOM:
|
||||
valueMask = __( 'Custom URL' );
|
||||
break;
|
||||
default:
|
||||
valueMask = __( 'None' );
|
||||
break;
|
||||
}
|
||||
|
||||
const linkSettingsOptions = {
|
||||
url: {
|
||||
valueMask,
|
||||
autoFocus: false,
|
||||
autoFill: false,
|
||||
},
|
||||
openInNewTab: {
|
||||
label: __( 'Open in new tab' ),
|
||||
},
|
||||
linkRel: {
|
||||
label: __( 'Link Rel' ),
|
||||
placeholder: _x( 'None', 'Link rel attribute value placeholder' ),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<PanelBody title={ __( 'Link Settings' ) }>
|
||||
<LinkSettingsNavigation
|
||||
isVisible={ isLinkSheetVisible }
|
||||
url={ url }
|
||||
rel={ rel }
|
||||
label={ label }
|
||||
linkTarget={ linkTarget }
|
||||
setAttributes={ setMappedAttributes }
|
||||
withBottomSheet={ false }
|
||||
hasPicker
|
||||
options={ linkSettingsOptions }
|
||||
showIcon={ false }
|
||||
onLinkCellPressed={ ( { navigation } ) => {
|
||||
navigation.navigate(
|
||||
blockSettingsScreens.imageLinkDestinations,
|
||||
{
|
||||
inputValue: attributes.href,
|
||||
linkDestination: attributes.linkDestination,
|
||||
imageUrl: attributes.url,
|
||||
attachmentPageUrl: image?.link,
|
||||
}
|
||||
);
|
||||
} }
|
||||
/>
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
||||
|
||||
const UPLOAD_STATE_IDLE = 0;
|
||||
const UPLOAD_STATE_UPLOADING = 1;
|
||||
const UPLOAD_STATE_SUCCEEDED = 2;
|
||||
const UPLOAD_STATE_FAILED = 3;
|
||||
|
||||
export class ImageEdit extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
this.state = {
|
||||
isCaptionSelected: false,
|
||||
uploadStatus: UPLOAD_STATE_IDLE,
|
||||
isAnimatedGif: false,
|
||||
};
|
||||
|
||||
this.replacedFeaturedImage = false;
|
||||
|
||||
this.finishMediaUploadWithSuccess = this.finishMediaUploadWithSuccess.bind(
|
||||
this
|
||||
);
|
||||
this.finishMediaUploadWithFailure = this.finishMediaUploadWithFailure.bind(
|
||||
this
|
||||
);
|
||||
this.mediaUploadStateReset = this.mediaUploadStateReset.bind( this );
|
||||
this.onSelectMediaUploadOption = this.onSelectMediaUploadOption.bind(
|
||||
this
|
||||
);
|
||||
this.updateMediaProgress = this.updateMediaProgress.bind( this );
|
||||
this.updateImageURL = this.updateImageURL.bind( this );
|
||||
this.onSetNewTab = this.onSetNewTab.bind( this );
|
||||
this.onSetSizeSlug = this.onSetSizeSlug.bind( this );
|
||||
this.onImagePressed = this.onImagePressed.bind( this );
|
||||
this.onSetFeatured = this.onSetFeatured.bind( this );
|
||||
this.onFocusCaption = this.onFocusCaption.bind( this );
|
||||
this.updateAlignment = this.updateAlignment.bind( this );
|
||||
this.accessibilityLabelCreator = this.accessibilityLabelCreator.bind(
|
||||
this
|
||||
);
|
||||
this.setMappedAttributes = this.setMappedAttributes.bind( this );
|
||||
this.onSizeChangeValue = this.onSizeChangeValue.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { attributes, setAttributes } = this.props;
|
||||
// This will warn when we have `id` defined, while `url` is undefined.
|
||||
// This may help track this issue: https://github.com/wordpress-mobile/WordPress-Android/issues/9768
|
||||
// where a cancelled image upload was resulting in a subsequent crash.
|
||||
if ( attributes.id && ! attributes.url ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn( 'Attributes has id with no url.' );
|
||||
}
|
||||
|
||||
// Detect any pasted image and start an upload.
|
||||
if (
|
||||
! attributes.id &&
|
||||
attributes.url &&
|
||||
getProtocol( attributes.url ) === 'file:'
|
||||
) {
|
||||
requestMediaImport( attributes.url, ( id, url ) => {
|
||||
if ( url ) {
|
||||
setAttributes( { id, url } );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// Make sure we mark any temporary images as failed if they failed while
|
||||
// the editor wasn't open.
|
||||
if (
|
||||
attributes.id &&
|
||||
attributes.url &&
|
||||
getProtocol( attributes.url ) === 'file:'
|
||||
) {
|
||||
mediaUploadSync();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// This action will only exist if the user pressed the trash button on the block holder.
|
||||
if (
|
||||
hasAction( 'blocks.onRemoveBlockCheckUpload' ) &&
|
||||
this.state.uploadStatus === UPLOAD_STATE_UPLOADING
|
||||
) {
|
||||
doAction(
|
||||
'blocks.onRemoveBlockCheckUpload',
|
||||
this.props.attributes.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate( previousProps ) {
|
||||
const {
|
||||
image,
|
||||
attributes,
|
||||
setAttributes,
|
||||
featuredImageId,
|
||||
} = this.props;
|
||||
if ( ! previousProps.image && image ) {
|
||||
const url =
|
||||
getUrlForSlug( image, attributes?.sizeSlug ) ||
|
||||
image.source_url;
|
||||
setAttributes( { url } );
|
||||
}
|
||||
|
||||
const { id } = attributes;
|
||||
const { id: previousId } = previousProps.attributes;
|
||||
|
||||
// The media changed and the previous media was set as the Featured Image,
|
||||
// we must keep track of the previous media's featured status to act on it
|
||||
// once the new media has a finalized ID.
|
||||
if (
|
||||
!! id &&
|
||||
id !== previousId &&
|
||||
!! featuredImageId &&
|
||||
featuredImageId === previousId
|
||||
) {
|
||||
this.replacedFeaturedImage = true;
|
||||
}
|
||||
|
||||
// The media changed and now has a finalized ID (e.g. upload completed), we
|
||||
// should attempt to replace the featured image if applicable.
|
||||
if (
|
||||
this.replacedFeaturedImage &&
|
||||
!! image &&
|
||||
this.canImageBeFeatured()
|
||||
) {
|
||||
this.replacedFeaturedImage = false;
|
||||
setFeaturedImage( id );
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps( props, state ) {
|
||||
// Avoid a UI flicker in the toolbar by insuring that isCaptionSelected
|
||||
// is updated immediately any time the isSelected prop becomes false.
|
||||
return {
|
||||
isCaptionSelected: props.isSelected && state.isCaptionSelected,
|
||||
};
|
||||
}
|
||||
|
||||
accessibilityLabelCreator( caption ) {
|
||||
// Checks if caption is empty.
|
||||
return ( typeof caption === 'string' && caption.trim().length === 0 ) ||
|
||||
caption === undefined ||
|
||||
caption === null
|
||||
? /* translators: accessibility text. Empty image caption. */
|
||||
'Image caption. Empty'
|
||||
: sprintf(
|
||||
/* translators: accessibility text. %s: image caption. */
|
||||
__( 'Image caption. %s' ),
|
||||
caption
|
||||
);
|
||||
}
|
||||
|
||||
onImagePressed() {
|
||||
const { attributes, image } = this.props;
|
||||
|
||||
if ( this.state.uploadStatus === UPLOAD_STATE_UPLOADING ) {
|
||||
requestImageUploadCancelDialog( attributes.id );
|
||||
} else if (
|
||||
attributes.id &&
|
||||
getProtocol( attributes.url ) === 'file:'
|
||||
) {
|
||||
requestImageFailedRetryDialog( attributes.id );
|
||||
} else if ( ! this.state.isCaptionSelected ) {
|
||||
requestImageFullscreenPreview(
|
||||
attributes.url,
|
||||
image && image.source_url
|
||||
);
|
||||
}
|
||||
|
||||
this.setState( {
|
||||
isCaptionSelected: false,
|
||||
} );
|
||||
}
|
||||
|
||||
updateMediaProgress( payload ) {
|
||||
const { setAttributes } = this.props;
|
||||
if ( payload.mediaUrl ) {
|
||||
setAttributes( { url: payload.mediaUrl } );
|
||||
}
|
||||
|
||||
if ( this.state.uploadStatus !== UPLOAD_STATE_UPLOADING ) {
|
||||
this.setState( { uploadStatus: UPLOAD_STATE_UPLOADING } );
|
||||
}
|
||||
}
|
||||
|
||||
finishMediaUploadWithSuccess( payload ) {
|
||||
const { setAttributes } = this.props;
|
||||
|
||||
setAttributes( { url: payload.mediaUrl, id: payload.mediaServerId } );
|
||||
this.setState( { uploadStatus: UPLOAD_STATE_SUCCEEDED } );
|
||||
|
||||
this.setState( {
|
||||
isAnimatedGif: payload.mediaUrl.toLowerCase().includes( '.gif' ),
|
||||
} );
|
||||
}
|
||||
|
||||
finishMediaUploadWithFailure( payload ) {
|
||||
const { setAttributes } = this.props;
|
||||
|
||||
setAttributes( { id: payload.mediaId } );
|
||||
this.setState( { uploadStatus: UPLOAD_STATE_FAILED } );
|
||||
}
|
||||
|
||||
mediaUploadStateReset() {
|
||||
const { setAttributes } = this.props;
|
||||
|
||||
setAttributes( { id: null, url: null } );
|
||||
this.setState( { uploadStatus: UPLOAD_STATE_IDLE } );
|
||||
}
|
||||
|
||||
updateImageURL( url ) {
|
||||
this.props.setAttributes( {
|
||||
url,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
} );
|
||||
}
|
||||
|
||||
updateAlignment( nextAlign ) {
|
||||
const extraUpdatedAttributes = Object.values(
|
||||
WIDE_ALIGNMENTS.alignments
|
||||
).includes( nextAlign )
|
||||
? { width: undefined, height: undefined }
|
||||
: {};
|
||||
this.props.setAttributes( {
|
||||
...extraUpdatedAttributes,
|
||||
align: nextAlign,
|
||||
} );
|
||||
}
|
||||
|
||||
onSetNewTab( value ) {
|
||||
const updatedLinkTarget = getUpdatedLinkTargetSettings(
|
||||
value,
|
||||
this.props.attributes
|
||||
);
|
||||
this.props.setAttributes( updatedLinkTarget );
|
||||
}
|
||||
|
||||
onSetSizeSlug( sizeSlug ) {
|
||||
const { image, setAttributes } = this.props;
|
||||
|
||||
const url = getUrlForSlug( image, sizeSlug );
|
||||
if ( ! url ) {
|
||||
return null;
|
||||
}
|
||||
setAttributes( {
|
||||
url,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
sizeSlug,
|
||||
} );
|
||||
}
|
||||
|
||||
onSelectMediaUploadOption( media ) {
|
||||
const { imageDefaultSize } = this.props;
|
||||
const { id, url, destination } = this.props.attributes;
|
||||
const mediaAttributes = {
|
||||
id: media.id,
|
||||
url: media.url,
|
||||
caption: media.caption,
|
||||
};
|
||||
|
||||
let additionalAttributes;
|
||||
// Reset the dimension attributes if changing to a different image.
|
||||
if ( ! media.id || media.id !== id ) {
|
||||
additionalAttributes = {
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
sizeSlug: imageDefaultSize,
|
||||
};
|
||||
} else {
|
||||
// Keep the same url when selecting the same file, so "Image Size" option is not changed.
|
||||
additionalAttributes = { url };
|
||||
}
|
||||
|
||||
let href;
|
||||
switch ( destination ) {
|
||||
case LINK_DESTINATION_MEDIA:
|
||||
href = media.url;
|
||||
break;
|
||||
case LINK_DESTINATION_ATTACHMENT:
|
||||
href = media.link;
|
||||
break;
|
||||
}
|
||||
mediaAttributes.href = href;
|
||||
|
||||
this.props.setAttributes( {
|
||||
...mediaAttributes,
|
||||
...additionalAttributes,
|
||||
} );
|
||||
|
||||
this.setState( {
|
||||
isAnimatedGif: media.url.toLowerCase().includes( '.gif' ),
|
||||
} );
|
||||
}
|
||||
|
||||
onFocusCaption() {
|
||||
if ( this.props.onFocus ) {
|
||||
this.props.onFocus();
|
||||
}
|
||||
if ( ! this.state.isCaptionSelected ) {
|
||||
this.setState( {
|
||||
isCaptionSelected: true,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
getPlaceholderIcon() {
|
||||
return (
|
||||
<Icon
|
||||
icon={ placeholderIcon }
|
||||
{ ...this.props.getStylesFromColorScheme(
|
||||
styles.iconPlaceholder,
|
||||
styles.iconPlaceholderDark
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
const { attributes } = this.props;
|
||||
const { align, width } = attributes;
|
||||
|
||||
return Object.values( WIDE_ALIGNMENTS.alignments ).includes( align )
|
||||
? '100%'
|
||||
: width;
|
||||
}
|
||||
|
||||
setMappedAttributes( { url: href, linkDestination, ...restAttributes } ) {
|
||||
const { setAttributes } = this.props;
|
||||
if ( ! href && ! linkDestination ) {
|
||||
linkDestination = LINK_DESTINATION_NONE;
|
||||
} else if ( ! linkDestination ) {
|
||||
linkDestination = LINK_DESTINATION_CUSTOM;
|
||||
}
|
||||
|
||||
return href === undefined || href === this.props.attributes.href
|
||||
? setAttributes( restAttributes )
|
||||
: setAttributes( {
|
||||
...restAttributes,
|
||||
linkDestination,
|
||||
href,
|
||||
} );
|
||||
}
|
||||
|
||||
getAltTextSettings() {
|
||||
const {
|
||||
attributes: { alt },
|
||||
} = this.props;
|
||||
|
||||
const updateAlt = ( newAlt ) => {
|
||||
this.props.setAttributes( { alt: newAlt } );
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheetTextControl
|
||||
initialValue={ alt }
|
||||
onChange={ updateAlt }
|
||||
placeholder={ __( 'Add alt text' ) }
|
||||
label={ __( 'Alt Text' ) }
|
||||
icon={ textColor }
|
||||
footerNote={
|
||||
<>
|
||||
{ __(
|
||||
'Describe the purpose of the image. Leave empty if the image is purely decorative.'
|
||||
) }
|
||||
<FooterMessageLink
|
||||
href={
|
||||
'https://www.w3.org/WAI/tutorials/images/decision-tree/'
|
||||
}
|
||||
value={ __( 'What is alt text?' ) }
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onSizeChangeValue( newValue ) {
|
||||
this.onSetSizeSlug( newValue );
|
||||
}
|
||||
|
||||
onSetFeatured( mediaId ) {
|
||||
const { closeSettingsBottomSheet } = this.props;
|
||||
setFeaturedImage( mediaId );
|
||||
closeSettingsBottomSheet();
|
||||
}
|
||||
|
||||
getFeaturedButtonPanel( isFeaturedImage ) {
|
||||
const { attributes, getStylesFromColorScheme } = this.props;
|
||||
|
||||
const setFeaturedButtonStyle = getStylesFromColorScheme(
|
||||
styles.setFeaturedButton,
|
||||
styles.setFeaturedButtonDark
|
||||
);
|
||||
|
||||
const removeFeaturedButton = () => (
|
||||
<BottomSheet.Cell
|
||||
label={ __( 'Remove as Featured Image' ) }
|
||||
labelStyle={ [
|
||||
setFeaturedButtonStyle,
|
||||
styles.removeFeaturedButton,
|
||||
] }
|
||||
cellContainerStyle={ styles.setFeaturedButtonCellContainer }
|
||||
separatorType={ 'none' }
|
||||
onPress={ () =>
|
||||
this.onSetFeatured( MEDIA_ID_NO_FEATURED_IMAGE_SET )
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const setFeaturedButton = () => (
|
||||
<BottomSheet.Cell
|
||||
label={ __( 'Set as Featured Image' ) }
|
||||
labelStyle={ setFeaturedButtonStyle }
|
||||
cellContainerStyle={ styles.setFeaturedButtonCellContainer }
|
||||
separatorType={ 'none' }
|
||||
onPress={ () => this.onSetFeatured( attributes.id ) }
|
||||
/>
|
||||
);
|
||||
|
||||
return isFeaturedImage ? removeFeaturedButton() : setFeaturedButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Featured images must be set to a successfully uploaded self-hosted image,
|
||||
* which has an ID.
|
||||
*
|
||||
* @return {boolean} Boolean indicating whether or not the current may be set as featured.
|
||||
*/
|
||||
canImageBeFeatured() {
|
||||
const {
|
||||
attributes: { id },
|
||||
} = this.props;
|
||||
return (
|
||||
typeof id !== 'undefined' &&
|
||||
this.state.uploadStatus !== UPLOAD_STATE_UPLOADING &&
|
||||
this.state.uploadStatus !== UPLOAD_STATE_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isCaptionSelected } = this.state;
|
||||
const {
|
||||
attributes,
|
||||
isSelected,
|
||||
image,
|
||||
clientId,
|
||||
imageDefaultSize,
|
||||
context,
|
||||
featuredImageId,
|
||||
wasBlockJustInserted,
|
||||
} = this.props;
|
||||
const { align, url, alt, id, sizeSlug, className } = attributes;
|
||||
const hasImageContext = context
|
||||
? Object.keys( context ).length > 0
|
||||
: false;
|
||||
|
||||
const imageSizes = Array.isArray( this.props.imageSizes )
|
||||
? this.props.imageSizes
|
||||
: [];
|
||||
// Only map available image sizes for the user to choose.
|
||||
const sizeOptions = imageSizes
|
||||
.filter( ( { slug } ) => getUrlForSlug( image, slug ) )
|
||||
.map( ( { name, slug } ) => ( { value: slug, label: name } ) );
|
||||
|
||||
let selectedSizeOption = sizeSlug || imageDefaultSize;
|
||||
let sizeOptionsValid = sizeOptions.find(
|
||||
( option ) => option.value === selectedSizeOption
|
||||
);
|
||||
|
||||
if ( ! sizeOptionsValid ) {
|
||||
// Default to 'full' size if the default large size is not available.
|
||||
sizeOptionsValid = sizeOptions.find(
|
||||
( option ) => option.value === 'full'
|
||||
);
|
||||
selectedSizeOption = 'full';
|
||||
}
|
||||
|
||||
const canImageBeFeatured = this.canImageBeFeatured();
|
||||
const isFeaturedImage =
|
||||
canImageBeFeatured && featuredImageId === attributes.id;
|
||||
|
||||
const getToolbarEditButton = ( open ) => (
|
||||
<BlockControls>
|
||||
<ToolbarGroup>
|
||||
<ToolbarButton
|
||||
title={ __( 'Edit image' ) }
|
||||
icon={ replace }
|
||||
onClick={ open }
|
||||
/>
|
||||
</ToolbarGroup>
|
||||
<BlockAlignmentToolbar
|
||||
value={ align }
|
||||
onChange={ this.updateAlignment }
|
||||
/>
|
||||
</BlockControls>
|
||||
);
|
||||
|
||||
const getInspectorControls = () => (
|
||||
<InspectorControls>
|
||||
<PanelBody title={ __( 'Image settings' ) } />
|
||||
<PanelBody style={ styles.panelBody }>
|
||||
<BlockStyles clientId={ clientId } url={ url } />
|
||||
</PanelBody>
|
||||
<PanelBody>
|
||||
{ image && sizeOptionsValid && (
|
||||
<BottomSheetSelectControl
|
||||
icon={ fullscreen }
|
||||
label={ __( 'Size' ) }
|
||||
options={ sizeOptions }
|
||||
onChange={ this.onSizeChangeValue }
|
||||
value={ selectedSizeOption }
|
||||
/>
|
||||
) }
|
||||
{ this.getAltTextSettings() }
|
||||
</PanelBody>
|
||||
<LinkSettings
|
||||
attributes={ this.props.attributes }
|
||||
image={ this.props.image }
|
||||
isLinkSheetVisible={ this.state.isLinkSheetVisible }
|
||||
setMappedAttributes={ this.setMappedAttributes }
|
||||
/>
|
||||
<PanelBody
|
||||
title={ __( 'Featured Image' ) }
|
||||
titleStyle={ styles.featuredImagePanelTitle }
|
||||
>
|
||||
{ canImageBeFeatured &&
|
||||
this.getFeaturedButtonPanel( isFeaturedImage ) }
|
||||
<FooterMessageControl
|
||||
label={ __(
|
||||
'Changes to featured image will not be affected by the undo/redo buttons.'
|
||||
) }
|
||||
cellContainerStyle={
|
||||
styles.setFeaturedButtonCellContainer
|
||||
}
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
);
|
||||
|
||||
if ( ! url ) {
|
||||
return (
|
||||
<View style={ styles.content }>
|
||||
<MediaPlaceholder
|
||||
allowedTypes={ [ MEDIA_TYPE_IMAGE ] }
|
||||
onSelect={ this.onSelectMediaUploadOption }
|
||||
icon={ this.getPlaceholderIcon() }
|
||||
onFocus={ this.props.onFocus }
|
||||
autoOpenMediaUpload={
|
||||
isSelected && wasBlockJustInserted
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const alignToFlex = {
|
||||
left: 'flex-start',
|
||||
center: 'center',
|
||||
right: 'flex-end',
|
||||
full: 'center',
|
||||
wide: 'center',
|
||||
};
|
||||
|
||||
const additionalImageProps = {
|
||||
height: '100%',
|
||||
resizeMode: context?.imageCrop ? 'cover' : 'contain',
|
||||
};
|
||||
|
||||
const imageContainerStyles = [
|
||||
context?.fixedHeight && styles.fixedHeight,
|
||||
];
|
||||
|
||||
const badgeLabelShown = isFeaturedImage || this.state.isAnimatedGif;
|
||||
let badgeLabelText = '';
|
||||
if ( isFeaturedImage ) {
|
||||
badgeLabelText = __( 'Featured' );
|
||||
} else if ( this.state.isAnimatedGif ) {
|
||||
badgeLabelText = __( 'GIF' );
|
||||
}
|
||||
|
||||
const getImageComponent = ( openMediaOptions, getMediaOptions ) => (
|
||||
<Badge label={ badgeLabelText } show={ badgeLabelShown }>
|
||||
<TouchableWithoutFeedback
|
||||
accessible={ ! isSelected }
|
||||
onPress={ this.onImagePressed }
|
||||
onLongPress={ openMediaOptions }
|
||||
disabled={ ! isSelected }
|
||||
>
|
||||
<View style={ styles.content }>
|
||||
{ isSelected && getInspectorControls() }
|
||||
{ isSelected && getMediaOptions() }
|
||||
{ ! this.state.isCaptionSelected &&
|
||||
getToolbarEditButton( openMediaOptions ) }
|
||||
<MediaUploadProgress
|
||||
coverUrl={ url }
|
||||
mediaId={ id }
|
||||
onUpdateMediaProgress={ this.updateMediaProgress }
|
||||
onFinishMediaUploadWithSuccess={
|
||||
this.finishMediaUploadWithSuccess
|
||||
}
|
||||
onFinishMediaUploadWithFailure={
|
||||
this.finishMediaUploadWithFailure
|
||||
}
|
||||
onMediaUploadStateReset={
|
||||
this.mediaUploadStateReset
|
||||
}
|
||||
renderContent={ ( {
|
||||
isUploadInProgress,
|
||||
isUploadFailed,
|
||||
retryMessage,
|
||||
} ) => {
|
||||
return (
|
||||
<View style={ imageContainerStyles }>
|
||||
<Image
|
||||
align={
|
||||
align && alignToFlex[ align ]
|
||||
}
|
||||
alt={ alt }
|
||||
isSelected={
|
||||
isSelected &&
|
||||
! isCaptionSelected
|
||||
}
|
||||
isUploadFailed={ isUploadFailed }
|
||||
isUploadInProgress={
|
||||
isUploadInProgress
|
||||
}
|
||||
onSelectMediaUploadOption={
|
||||
this.onSelectMediaUploadOption
|
||||
}
|
||||
openMediaOptions={
|
||||
openMediaOptions
|
||||
}
|
||||
retryMessage={ retryMessage }
|
||||
url={ url }
|
||||
shapeStyle={
|
||||
styles[ className ] || className
|
||||
}
|
||||
width={ this.getWidth() }
|
||||
{ ...( hasImageContext
|
||||
? additionalImageProps
|
||||
: {} ) }
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} }
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<BlockCaption
|
||||
clientId={ this.props.clientId }
|
||||
isSelected={ this.state.isCaptionSelected }
|
||||
accessible
|
||||
accessibilityLabelCreator={ this.accessibilityLabelCreator }
|
||||
onFocus={ this.onFocusCaption }
|
||||
onBlur={ this.props.onBlur } // Always assign onBlur as props.
|
||||
insertBlocksAfter={ this.props.insertBlocksAfter }
|
||||
/>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
return (
|
||||
<MediaUpload
|
||||
allowedTypes={ [ MEDIA_TYPE_IMAGE ] }
|
||||
isReplacingMedia={ true }
|
||||
onSelect={ this.onSelectMediaUploadOption }
|
||||
render={ ( { open, getMediaOptions } ) => {
|
||||
return getImageComponent( open, getMediaOptions );
|
||||
} }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose( [
|
||||
withSelect( ( select, props ) => {
|
||||
const { getMedia } = select( coreStore );
|
||||
const { getSettings, wasBlockJustInserted } = select(
|
||||
blockEditorStore
|
||||
);
|
||||
const { getEditedPostAttribute } = select( 'core/editor' );
|
||||
const {
|
||||
attributes: { id, url },
|
||||
isSelected,
|
||||
clientId,
|
||||
} = props;
|
||||
const { imageSizes, imageDefaultSize } = getSettings();
|
||||
const isNotFileUrl = id && getProtocol( url ) !== 'file:';
|
||||
const featuredImageId = getEditedPostAttribute( 'featured_media' );
|
||||
|
||||
const shouldGetMedia =
|
||||
( isSelected && isNotFileUrl ) ||
|
||||
// Edge case to update the image after uploading if the block gets unselected
|
||||
// Check if it's the original image and not the resized one with queryparams.
|
||||
( ! isSelected &&
|
||||
isNotFileUrl &&
|
||||
url &&
|
||||
! hasQueryArg( url, 'w' ) );
|
||||
|
||||
return {
|
||||
image: shouldGetMedia ? getMedia( id ) : null,
|
||||
imageSizes,
|
||||
imageDefaultSize,
|
||||
featuredImageId,
|
||||
wasBlockJustInserted: wasBlockJustInserted(
|
||||
clientId,
|
||||
'inserter_menu'
|
||||
),
|
||||
};
|
||||
} ),
|
||||
withDispatch( ( dispatch ) => {
|
||||
return {
|
||||
closeSettingsBottomSheet() {
|
||||
dispatch( editPostStore ).closeGeneralSidebar();
|
||||
},
|
||||
};
|
||||
} ),
|
||||
withPreferredColorScheme,
|
||||
] )( ImageEdit );
|
||||
@@ -0,0 +1,140 @@
|
||||
figure.wp-block-image:not(.wp-block) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wp-block-image {
|
||||
position: relative;
|
||||
|
||||
.is-applying img,
|
||||
&.is-transient img {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
figcaption img {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
// Shown while image is being uploaded
|
||||
.components-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -9px;
|
||||
margin-left: -9px;
|
||||
}
|
||||
|
||||
&:not(.is-style-rounded) > div:not(.components-placeholder) {
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
// This is necessary for the editor resize handles to accurately work on a non-floated, non-resized, small image.
|
||||
.wp-block-image .components-resizable-box__container {
|
||||
display: inline-block;
|
||||
img {
|
||||
display: block;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.block-editor-block-list__block[data-type="core/image"] .block-editor-block-toolbar .block-editor-url-input__button-modal {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: -$border-width 0;
|
||||
|
||||
@include break-small() {
|
||||
margin: -$border-width;
|
||||
}
|
||||
}
|
||||
|
||||
[data-align="wide"] > .wp-block-image,
|
||||
[data-align="full"] > .wp-block-image {
|
||||
img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block[data-align="left"],
|
||||
.wp-block[data-align="center"],
|
||||
.wp-block[data-align="right"] {
|
||||
> .wp-block-image {
|
||||
display: table;
|
||||
|
||||
> figcaption {
|
||||
display: table-caption;
|
||||
caption-side: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block[data-align="left"] > .wp-block-image {
|
||||
margin-right: 1em;
|
||||
margin-left: 0;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.wp-block[data-align="right"] > .wp-block-image {
|
||||
margin-left: 1em;
|
||||
margin-right: 0;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.wp-block[data-align="center"] > .wp-block-image {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wp-block-image__crop-area {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wp-block-image__crop-icon {
|
||||
padding: 0 8px;
|
||||
min-width: 48px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block-image__zoom {
|
||||
.components-popover__content {
|
||||
overflow: visible;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.components-range-control {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.components-base-control__field {
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block-image__aspect-ratio {
|
||||
height: $grid-unit-60 - $border-width - $border-width;
|
||||
margin-bottom: -$grid-unit-10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.components-button {
|
||||
width: $button-size;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { get, filter, map, pick, includes } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { isBlobURL } from '@wordpress/blob';
|
||||
import {
|
||||
ExternalLink,
|
||||
PanelBody,
|
||||
ResizableBox,
|
||||
Spinner,
|
||||
TextareaControl,
|
||||
TextControl,
|
||||
ToolbarButton,
|
||||
} from '@wordpress/components';
|
||||
import { useViewportMatch, usePrevious } from '@wordpress/compose';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import {
|
||||
BlockControls,
|
||||
InspectorControls,
|
||||
RichText,
|
||||
__experimentalImageSizeControl as ImageSizeControl,
|
||||
__experimentalImageURLInputUI as ImageURLInputUI,
|
||||
MediaReplaceFlow,
|
||||
store as blockEditorStore,
|
||||
BlockAlignmentControl,
|
||||
__experimentalImageEditor as ImageEditor,
|
||||
__experimentalImageEditingProvider as ImageEditingProvider,
|
||||
} from '@wordpress/block-editor';
|
||||
import { useEffect, useMemo, useState, useRef } from '@wordpress/element';
|
||||
import { __, sprintf, isRTL } from '@wordpress/i18n';
|
||||
import { getFilename } from '@wordpress/url';
|
||||
import { createBlock, switchToBlockType } from '@wordpress/blocks';
|
||||
import { crop, overlayText, upload } 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, isMediaDestroyed } from './edit';
|
||||
|
||||
/**
|
||||
* Module constants
|
||||
*/
|
||||
import { MIN_SIZE, ALLOWED_MEDIA_TYPES } from './constants';
|
||||
|
||||
export default function Image( {
|
||||
temporaryURL,
|
||||
attributes: {
|
||||
url = '',
|
||||
alt,
|
||||
caption,
|
||||
align,
|
||||
id,
|
||||
href,
|
||||
rel,
|
||||
linkClass,
|
||||
linkDestination,
|
||||
title,
|
||||
width,
|
||||
height,
|
||||
linkTarget,
|
||||
sizeSlug,
|
||||
media_fit,
|
||||
media_align,
|
||||
media_height,
|
||||
media_width
|
||||
},
|
||||
setAttributes,
|
||||
isSelected,
|
||||
insertBlocksAfter,
|
||||
onReplace,
|
||||
onCloseModal,
|
||||
onSelectImage,
|
||||
onSelectURL,
|
||||
onUploadError,
|
||||
containerRef,
|
||||
context,
|
||||
clientId,
|
||||
onImageLoadError,
|
||||
} ) {
|
||||
const imageRef = useRef();
|
||||
const captionRef = useRef();
|
||||
const prevUrl = usePrevious( url );
|
||||
const { allowResize = true } = context;
|
||||
const { getBlock } = useSelect( blockEditorStore );
|
||||
|
||||
const { image, multiImageSelection } = useSelect(
|
||||
( select ) => {
|
||||
const { getMedia } = select( coreStore );
|
||||
const { getMultiSelectedBlockClientIds, getBlockName } = select(
|
||||
blockEditorStore
|
||||
);
|
||||
const multiSelectedClientIds = getMultiSelectedBlockClientIds();
|
||||
return {
|
||||
image:
|
||||
id && isSelected
|
||||
? getMedia( id, { context: 'view' } )
|
||||
: null,
|
||||
multiImageSelection:
|
||||
multiSelectedClientIds.length &&
|
||||
multiSelectedClientIds.every(
|
||||
( _clientId ) =>
|
||||
getBlockName( _clientId ) === 'core/image'
|
||||
),
|
||||
};
|
||||
},
|
||||
[ id, isSelected ]
|
||||
);
|
||||
const {
|
||||
canInsertCover,
|
||||
imageEditing,
|
||||
imageSizes,
|
||||
maxWidth,
|
||||
mediaUpload,
|
||||
} = useSelect(
|
||||
( select ) => {
|
||||
const {
|
||||
getBlockRootClientId,
|
||||
getSettings,
|
||||
canInsertBlockType,
|
||||
} = select( blockEditorStore );
|
||||
|
||||
const rootClientId = getBlockRootClientId( clientId );
|
||||
const settings = pick( getSettings(), [
|
||||
'imageEditing',
|
||||
'imageSizes',
|
||||
'maxWidth',
|
||||
'mediaUpload',
|
||||
] );
|
||||
|
||||
return {
|
||||
...settings,
|
||||
canInsertCover: canInsertBlockType(
|
||||
'core/cover',
|
||||
rootClientId
|
||||
),
|
||||
};
|
||||
},
|
||||
[ clientId ]
|
||||
);
|
||||
const { replaceBlocks, toggleSelection } = useDispatch( blockEditorStore );
|
||||
const { createErrorNotice, createSuccessNotice } = useDispatch(
|
||||
noticesStore
|
||||
);
|
||||
const isLargeViewport = useViewportMatch( 'medium' );
|
||||
const isWideAligned = includes( [ 'wide', 'full' ], align );
|
||||
const [
|
||||
{ loadedNaturalWidth, loadedNaturalHeight },
|
||||
setLoadedNaturalSize,
|
||||
] = useState( {} );
|
||||
const [ isEditingImage, setIsEditingImage ] = useState( false );
|
||||
const [ externalBlob, setExternalBlob ] = useState();
|
||||
const clientWidth = useClientWidth( containerRef, [ align ] );
|
||||
const isResizable = allowResize && ! ( isWideAligned && isLargeViewport );
|
||||
const imageSizeOptions = map(
|
||||
filter( imageSizes, ( { slug } ) =>
|
||||
get( image, [ 'media_details', 'sizes', slug, 'source_url' ] )
|
||||
),
|
||||
( { name, slug } ) => ( { value: slug, label: name } )
|
||||
);
|
||||
|
||||
// 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 || externalBlob ) {
|
||||
return;
|
||||
}
|
||||
|
||||
window
|
||||
.fetch( url )
|
||||
.then( ( response ) => response.blob() )
|
||||
.then( ( blob ) => setExternalBlob( blob ) )
|
||||
// Do nothing, cannot upload.
|
||||
.catch( () => {} );
|
||||
}, [ id, url, isSelected, externalBlob ] );
|
||||
|
||||
// Focus the caption after inserting an image from the placeholder. This is
|
||||
// done to preserve the behaviour of focussing the first tabbable element
|
||||
// when a block is mounted. Previously, the image block would remount when
|
||||
// the placeholder is removed. Maybe this behaviour could be removed.
|
||||
useEffect( () => {
|
||||
if ( url && ! prevUrl && isSelected ) {
|
||||
captionRef.current.focus();
|
||||
}
|
||||
}, [ url, prevUrl ] );
|
||||
|
||||
// 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
|
||||
// witdth 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 } } );
|
||||
const shouldReplace = undefined !== embedBlock;
|
||||
|
||||
if ( shouldReplace ) {
|
||||
onReplace( embedBlock );
|
||||
}
|
||||
|
||||
onImageLoadError( shouldReplace );
|
||||
}
|
||||
|
||||
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 = get( 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 ( isSelected && isMediaDestroyed( id ) ) {
|
||||
onImageLoadError();
|
||||
}
|
||||
}, [ isSelected ] );
|
||||
|
||||
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">
|
||||
<BlockAlignmentControl
|
||||
value={ align }
|
||||
onChange={ updateAlignment }
|
||||
/>
|
||||
{ ! multiImageSelection && ! isEditingImage && (
|
||||
<ImageURLInputUI
|
||||
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 }
|
||||
onCloseModal={ onCloseModal }
|
||||
/>
|
||||
</BlockControls>
|
||||
) }
|
||||
<InspectorControls>
|
||||
<PanelBody title={ __( 'Image settings' ) }>
|
||||
{ ! multiImageSelection && (
|
||||
<TextareaControl
|
||||
label={ __( 'Alt text (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>
|
||||
{ __(
|
||||
'Leave empty if the image is purely decorative.'
|
||||
) }
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) }
|
||||
<ImageSizeControl
|
||||
onChangeImage={ updateImage }
|
||||
onChange={ ( value ) => setAttributes( value ) }
|
||||
slug={ sizeSlug }
|
||||
width={ width }
|
||||
height={ height }
|
||||
imageSizeOptions={ imageSizeOptions }
|
||||
isResizable={ isResizable }
|
||||
imageWidth={ naturalWidth }
|
||||
imageHeight={ naturalHeight }
|
||||
/>
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
<InspectorControls __experimentalGroup="advanced">
|
||||
<TextControl
|
||||
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' );
|
||||
}
|
||||
|
||||
var fit_class = media_fit ? media_fit : 'cover';
|
||||
var align_class = media_align ? media_align : 'center';
|
||||
var media_height = media_fit && media_fit == 'set' ? ( media_height ? media_height : '50' ) : false;
|
||||
var media_width = media_fit && media_fit == 'set' ? ( media_width ? media_width : '100' ) : false;
|
||||
|
||||
var style = {};
|
||||
if ( media_height ) {
|
||||
style['max-height'] = media_height + 'px';
|
||||
}
|
||||
if ( media_width ) {
|
||||
style['max-width'] = media_width + 'px';
|
||||
}
|
||||
|
||||
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 }
|
||||
style={ style }
|
||||
/>
|
||||
{ temporaryURL && <Spinner /> }
|
||||
</>
|
||||
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
);
|
||||
|
||||
img = (
|
||||
<div class="areoi-media position-relative">
|
||||
<div class={ 'areoi-media-container ' + fit_class + ' ' + align_class }>
|
||||
{ img }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ /* Hide controls during upload to avoid component remount,
|
||||
which causes duplicated image upload. */ }
|
||||
{ ! temporaryURL && controls }
|
||||
{ img }
|
||||
{ ( ! RichText.isEmpty( caption ) || isSelected ) && (
|
||||
<RichText
|
||||
ref={ captionRef }
|
||||
tagName="figcaption"
|
||||
aria-label={ __( 'Image caption text' ) }
|
||||
placeholder={ __( 'Add caption' ) }
|
||||
value={ caption }
|
||||
onChange={ ( value ) =>
|
||||
setAttributes( { caption: value } )
|
||||
}
|
||||
inlineToolbar
|
||||
__unstableOnSplitAtEnd={ () =>
|
||||
insertBlocksAfter( createBlock( 'core/paragraph' ) )
|
||||
}
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { image as icon } from '@wordpress/icons';
|
||||
import * as areoi from '../_components/Core.js';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import deprecated from './deprecated';
|
||||
import edit from './edit';
|
||||
import metadata from './block.json';
|
||||
import save from './save';
|
||||
import transforms from './transforms';
|
||||
|
||||
const { name } = metadata;
|
||||
|
||||
export { metadata, name };
|
||||
|
||||
export const settings = {
|
||||
icon,
|
||||
example: {
|
||||
attributes: {
|
||||
sizeSlug: 'large',
|
||||
url: 'https://s.w.org/images/core/5.3/MtBlanc1.jpg',
|
||||
// translators: Caption accompanying an image of the Mont Blanc, which serves as an example for the Image block.
|
||||
caption: __( 'Mont Blanc appears—still, snowy, and serene.' ),
|
||||
},
|
||||
},
|
||||
__experimentalLabel( attributes, { context } ) {
|
||||
if ( context === 'accessibility' ) {
|
||||
const { caption, alt, url } = attributes;
|
||||
|
||||
if ( ! url ) {
|
||||
return __( 'Empty' );
|
||||
}
|
||||
|
||||
if ( ! alt ) {
|
||||
return caption || '';
|
||||
}
|
||||
|
||||
// This is intended to be read by a screen reader.
|
||||
// A period simply means a pause, no need to translate it.
|
||||
return alt + ( caption ? '. ' + caption : '' );
|
||||
}
|
||||
},
|
||||
getEditWrapperProps( attributes ) {
|
||||
return {
|
||||
'data-align': attributes.align,
|
||||
};
|
||||
},
|
||||
transforms,
|
||||
edit,
|
||||
save,
|
||||
deprecated,
|
||||
};
|
||||
|
||||
areoi.blocks.registerBlockType( metadata, {
|
||||
icon: icon,
|
||||
edit: edit,
|
||||
save: () => {
|
||||
return (
|
||||
<areoi.editor.InnerBlocks.Content/>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Server-side rendering of the `core/image` block.
|
||||
*
|
||||
* @package WordPress
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders the `core/image` block on the server,
|
||||
* adding a data-id attribute to the element if core/gallery has added on pre-render.
|
||||
*
|
||||
* @param array $attributes The block attributes.
|
||||
* @param string $content The block content.
|
||||
* @return string Returns the block content with the data-id attribute added.
|
||||
*/
|
||||
function render_block_core_image( $attributes, $content ) {
|
||||
if ( isset( $attributes['data-id'] ) ) {
|
||||
// Add the data-id="$id" attribute to the img element
|
||||
// to provide backwards compatibility for the Gallery Block,
|
||||
// which now wraps Image Blocks within innerBlocks.
|
||||
// The data-id attribute is added in a core/gallery `render_block_data` hook.
|
||||
$data_id_attribute = 'data-id="' . esc_attr( $attributes['data-id'] ) . '"';
|
||||
if ( false === strpos( $content, $data_id_attribute ) ) {
|
||||
$content = str_replace( '<img', '<img ' . $data_id_attribute . ' ', $content );
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registers the `core/image` block on server.
|
||||
*/
|
||||
function register_block_core_image() {
|
||||
register_block_type_from_metadata(
|
||||
__DIR__ . '/image',
|
||||
array(
|
||||
'render_callback' => 'render_block_core_image',
|
||||
)
|
||||
);
|
||||
}
|
||||
add_action( 'init', 'register_block_core_image' );
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { RichText, useBlockProps } from '@wordpress/block-editor';
|
||||
import * as areoi from '../_components/Core.js';
|
||||
export default function save( { attributes } ) {
|
||||
const {
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
align,
|
||||
href,
|
||||
rel,
|
||||
linkClass,
|
||||
width,
|
||||
height,
|
||||
id,
|
||||
linkTarget,
|
||||
sizeSlug,
|
||||
title,
|
||||
} = attributes;
|
||||
|
||||
const newRel = isEmpty( rel ) ? undefined : rel;
|
||||
|
||||
const className = 'areoi-content-grid-item';
|
||||
|
||||
const image = (
|
||||
<img
|
||||
src={ url }
|
||||
alt={ alt }
|
||||
className={ id ? `wp-image-${ id }` : null }
|
||||
width={ width }
|
||||
height={ height }
|
||||
title={ title }
|
||||
/>
|
||||
);
|
||||
|
||||
const figure = (
|
||||
<>
|
||||
|
||||
{ href ? (
|
||||
<a
|
||||
className={ linkClass }
|
||||
href={ href }
|
||||
target={ linkTarget }
|
||||
rel={ newRel }
|
||||
>
|
||||
{ image }
|
||||
</a>
|
||||
) : (
|
||||
<div>{image}</div>
|
||||
) }
|
||||
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content tagName="figcaption" value={ caption } />
|
||||
) }
|
||||
|
||||
|
||||
</>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <figure { ...useBlockProps.save( { className: className } ) }>
|
||||
// { figure }
|
||||
// </figure>
|
||||
// );
|
||||
|
||||
return (
|
||||
<areoi.editor.InnerBlocks.Content/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
.wp-block-image {
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
&:not(.is-style-rounded) {
|
||||
> a,
|
||||
img {
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.aligncenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.alignfull img,
|
||||
&.alignwide img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.alignleft,
|
||||
&.alignright,
|
||||
&.aligncenter,
|
||||
.alignleft,
|
||||
.alignright,
|
||||
.aligncenter {
|
||||
display: table;
|
||||
|
||||
> figcaption {
|
||||
display: table-caption;
|
||||
caption-side: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.alignleft {
|
||||
/*rtl:ignore*/
|
||||
float: left;
|
||||
/*rtl:ignore*/
|
||||
margin-left: 0;
|
||||
margin-right: 1em;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.alignright {
|
||||
/*rtl:ignore*/
|
||||
float: right;
|
||||
/*rtl:ignore*/
|
||||
margin-right: 0;
|
||||
margin-left: 1em;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
// This is needed for classic themes where the align class is not on the container.
|
||||
.aligncenter {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
// Supply caption styles to images, even if the theme hasn't opted in.
|
||||
// Reason being: the new markup, <figcaptions>, are not likely to be styled in the majority of existing themes,
|
||||
// so we supply the styles so as to not appear broken or unstyled in those themes.
|
||||
figcaption {
|
||||
@include caption-style();
|
||||
}
|
||||
|
||||
// Variations
|
||||
&.is-style-rounded img,
|
||||
.is-style-rounded img {
|
||||
// We use an absolute pixel to prevent the oval shape that a value of 50% would give
|
||||
// to rectangular images. A pill-shape is better than otherwise.
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
// The following variation is deprecated.
|
||||
// The CSS is kept here for the time being, to support blocks using the old variation.
|
||||
&.is-style-circle-mask img {
|
||||
// We use an absolute pixel to prevent the oval shape that a value of 50% would give
|
||||
// to rectangular images. A pill-shape is better than otherwise.
|
||||
border-radius: 9999px;
|
||||
|
||||
// If a browser supports it, we will switch to using a circular SVG mask.
|
||||
// The stylelint override is necessary to use the SVG inline here.
|
||||
@supports (mask-image: none) or (-webkit-mask-image: none) {
|
||||
/* stylelint-disable */
|
||||
mask-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="50"/></svg>');
|
||||
/* stylelint-enable */
|
||||
mask-mode: alpha;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-position: center;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wp-block-image figure {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// @format
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contentCentered {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iconPlaceholder {
|
||||
fill: $gray-dark;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.iconPlaceholderDark {
|
||||
fill: $white;
|
||||
}
|
||||
|
||||
.is-style-rounded {
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panelBody {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: $grid-unit;
|
||||
}
|
||||
|
||||
.fixedHeight {
|
||||
height: 150;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.featuredImagePanelTitle {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.setFeaturedButtonCellContainer {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.setFeaturedButton {
|
||||
text-align: left;
|
||||
color: $blue-50;
|
||||
padding: $grid-unit-15 0;
|
||||
}
|
||||
|
||||
.setFeaturedButtonDark {
|
||||
color: $blue-30;
|
||||
}
|
||||
|
||||
.removeFeaturedButton {
|
||||
color: $alert-red;
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
initializeEditor,
|
||||
getEditorHtml,
|
||||
render,
|
||||
} from 'test/helpers';
|
||||
import { Image } from 'react-native';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks';
|
||||
import {
|
||||
setFeaturedImage,
|
||||
sendMediaUpload,
|
||||
subscribeMediaUpload,
|
||||
} from '@wordpress/react-native-bridge';
|
||||
import { select } from '@wordpress/data';
|
||||
import { store as editorStore } from '@wordpress/editor';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
import '@wordpress/jest-console';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { registerCoreBlocks } from '../..';
|
||||
import ImageEdit from '../edit';
|
||||
|
||||
let uploadCallBack;
|
||||
subscribeMediaUpload.mockImplementation( ( callback ) => {
|
||||
uploadCallBack = callback;
|
||||
} );
|
||||
sendMediaUpload.mockImplementation( ( payload ) => {
|
||||
uploadCallBack( payload );
|
||||
} );
|
||||
|
||||
/**
|
||||
* Immediately invoke delayed functions. A better alternative would be using
|
||||
* fake timers and test the delay itself. However, fake timers does not work
|
||||
* with our custom waitFor implementation.
|
||||
*/
|
||||
jest.mock( 'lodash', () => {
|
||||
const actual = jest.requireActual( 'lodash' );
|
||||
return { ...actual, delay: ( cb ) => cb() };
|
||||
} );
|
||||
|
||||
const apiFetchPromise = Promise.resolve( {} );
|
||||
|
||||
const clipboardPromise = Promise.resolve( '' );
|
||||
Clipboard.getString.mockImplementation( () => clipboardPromise );
|
||||
|
||||
beforeAll( () => {
|
||||
registerCoreBlocks();
|
||||
|
||||
// Mock Image.getSize to avoid failed attempt to size non-existant image
|
||||
const getSizeSpy = jest.spyOn( Image, 'getSize' );
|
||||
getSizeSpy.mockImplementation( ( _url, callback ) => callback( 300, 200 ) );
|
||||
} );
|
||||
|
||||
afterAll( () => {
|
||||
getBlockTypes().forEach( ( { name } ) => {
|
||||
unregisterBlockType( name );
|
||||
} );
|
||||
|
||||
// Restore mocks.
|
||||
Image.getSize.mockRestore();
|
||||
} );
|
||||
|
||||
describe( 'Image Block', () => {
|
||||
it( 'sets link to None', async () => {
|
||||
const initialHtml = `
|
||||
<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"media","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default">
|
||||
<a href="https://cldup.com/cXyG__fTLN.jpg">
|
||||
<img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/>
|
||||
</a>
|
||||
<figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
const screen = await initializeEditor( { initialHtml } );
|
||||
// We must await the image fetch via `getMedia`
|
||||
await act( () => apiFetchPromise );
|
||||
|
||||
fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
|
||||
// Awaiting navigation event seemingly required due to React Navigation bug
|
||||
// https://git.io/Ju35Z
|
||||
await act( () =>
|
||||
fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
|
||||
);
|
||||
fireEvent.press( screen.getByText( 'Media File' ) );
|
||||
fireEvent.press( screen.getByText( 'None' ) );
|
||||
|
||||
const expectedHtml = `<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"none","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/><figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
expect( getEditorHtml() ).toBe( expectedHtml );
|
||||
} );
|
||||
|
||||
it( 'sets link to Media File', async () => {
|
||||
const initialHtml = `
|
||||
<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"none","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default">
|
||||
<img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/>
|
||||
<figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
const screen = await initializeEditor( { initialHtml } );
|
||||
// We must await the image fetch via `getMedia`
|
||||
await act( () => apiFetchPromise );
|
||||
|
||||
fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
|
||||
// Awaiting navigation event seemingly required due to React Navigation bug
|
||||
// https://git.io/Ju35Z
|
||||
await act( () =>
|
||||
fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
|
||||
);
|
||||
fireEvent.press( screen.getByText( 'None' ) );
|
||||
fireEvent.press( screen.getByText( 'Media File' ) );
|
||||
|
||||
const expectedHtml = `<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"media","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default"><a href="https://cldup.com/cXyG__fTLN.jpg"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/></a><figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
expect( getEditorHtml() ).toBe( expectedHtml );
|
||||
} );
|
||||
|
||||
it( 'sets link to Custom URL', async () => {
|
||||
const initialHtml = `
|
||||
<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"none","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default">
|
||||
<img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/>
|
||||
<figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
const screen = await initializeEditor( { initialHtml } );
|
||||
// We must await the image fetch via `getMedia`
|
||||
await act( () => apiFetchPromise );
|
||||
|
||||
fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
|
||||
// Awaiting navigation event seemingly required due to React Navigation bug
|
||||
// https://git.io/Ju35Z
|
||||
await act( () =>
|
||||
fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
|
||||
);
|
||||
fireEvent.press( screen.getByText( 'None' ) );
|
||||
fireEvent.press( screen.getByText( 'Custom URL' ) );
|
||||
// Await asynchronous fetch of clipboard
|
||||
await act( () => clipboardPromise );
|
||||
fireEvent.changeText(
|
||||
screen.getByPlaceholderText( 'Search or type URL' ),
|
||||
'wordpress.org'
|
||||
);
|
||||
fireEvent.press( screen.getByA11yLabel( 'Apply' ) );
|
||||
|
||||
const expectedHtml = `<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"custom","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default"><a href="http://wordpress.org"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/></a><figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
expect( getEditorHtml() ).toBe( expectedHtml );
|
||||
} );
|
||||
|
||||
it( 'swaps the link between destinations', async () => {
|
||||
const initialHtml = `
|
||||
<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"none","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default">
|
||||
<img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/>
|
||||
<figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
const screen = await initializeEditor( { initialHtml } );
|
||||
// We must await the image fetch via `getMedia`
|
||||
await act( () => apiFetchPromise );
|
||||
|
||||
fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
|
||||
// Awaiting navigation event seemingly required due to React Navigation bug
|
||||
// https://git.io/Ju35Z
|
||||
await act( () =>
|
||||
fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
|
||||
);
|
||||
fireEvent.press( screen.getByText( 'None' ) );
|
||||
fireEvent.press( screen.getByText( 'Media File' ) );
|
||||
fireEvent.press( screen.getByText( 'Custom URL' ) );
|
||||
// Await asynchronous fetch of clipboard
|
||||
await act( () => clipboardPromise );
|
||||
fireEvent.changeText(
|
||||
screen.getByPlaceholderText( 'Search or type URL' ),
|
||||
'wordpress.org'
|
||||
);
|
||||
fireEvent.press( screen.getByA11yLabel( 'Apply' ) );
|
||||
fireEvent.press( screen.getByText( 'Custom URL' ) );
|
||||
// Await asynchronous fetch of clipboard
|
||||
await act( () => clipboardPromise );
|
||||
fireEvent.press( screen.getByText( 'Media File' ) );
|
||||
|
||||
const expectedHtml = `<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"media","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default"><a href="https://cldup.com/cXyG__fTLN.jpg"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/></a><figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
expect( getEditorHtml() ).toBe( expectedHtml );
|
||||
} );
|
||||
|
||||
it( 'does not display the Link To URL within the Custom URL input when set to Media File and query parameters are present', async () => {
|
||||
const initialHtml = `
|
||||
<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"media","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default">
|
||||
<a href="https://cldup.com/cXyG__fTLN.jpg">
|
||||
<img src="https://cldup.com/cXyG__fTLN.jpg?w=683" alt="" class="wp-image-1"/>
|
||||
</a>
|
||||
<figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
const screen = await initializeEditor( { initialHtml } );
|
||||
// We must await the image fetch via `getMedia`
|
||||
await act( () => apiFetchPromise );
|
||||
|
||||
fireEvent.press( screen.getByA11yLabel( /Image Block/ ) );
|
||||
// Awaiting navigation event seemingly required due to React Navigation bug
|
||||
// https://git.io/Ju35Z
|
||||
await act( () =>
|
||||
fireEvent.press( screen.getByA11yLabel( 'Open Settings' ) )
|
||||
);
|
||||
fireEvent.press( screen.getByText( 'Media File' ) );
|
||||
|
||||
expect( screen.queryByA11yLabel( /https:\/\/cldup\.com/ ) ).toBeNull();
|
||||
} );
|
||||
|
||||
it( 'sets link target', async () => {
|
||||
const initialHtml = `
|
||||
<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"custom","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default">
|
||||
<a href="https://wordpress.org">
|
||||
<img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/>
|
||||
</a>
|
||||
<figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
const screen = await initializeEditor( { initialHtml } );
|
||||
// We must await the image fetch via `getMedia`
|
||||
await act( () => apiFetchPromise );
|
||||
|
||||
const imageBlock = screen.getByA11yLabel( /Image Block/ );
|
||||
fireEvent.press( imageBlock );
|
||||
|
||||
const settingsButton = screen.getByA11yLabel( 'Open Settings' );
|
||||
// Awaiting navigation event seemingly required due to React Navigation bug
|
||||
// https://git.io/Ju35Z
|
||||
await act( () => fireEvent.press( settingsButton ) );
|
||||
|
||||
const linkTargetButton = screen.getByText( 'Open in new tab' );
|
||||
fireEvent.press( linkTargetButton );
|
||||
|
||||
const expectedHtml = `<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"custom","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default"><a href="https://wordpress.org" target="_blank" rel="noreferrer noopener"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/></a><figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
expect( getEditorHtml() ).toBe( expectedHtml );
|
||||
} );
|
||||
|
||||
it( 'unset link target', async () => {
|
||||
const initialHtml = `
|
||||
<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"custom","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default">
|
||||
<a href="https://wordpress.org" target="_blank" rel="noreferrer noopener">
|
||||
<img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/>
|
||||
</a>
|
||||
<figcaption>Mountain</figcaption>
|
||||
</figure>
|
||||
<!-- /wp:image -->`;
|
||||
const screen = await initializeEditor( { initialHtml } );
|
||||
// We must await the image fetch via `getMedia`
|
||||
await act( () => apiFetchPromise );
|
||||
|
||||
const imageBlock = screen.getByA11yLabel( /Image Block/ );
|
||||
fireEvent.press( imageBlock );
|
||||
|
||||
const settingsButton = screen.getByA11yLabel( 'Open Settings' );
|
||||
// Awaiting navigation event seemingly required due to React Navigation bug
|
||||
// https://git.io/Ju35Z
|
||||
await act( () => fireEvent.press( settingsButton ) );
|
||||
|
||||
const linkTargetButton = screen.getByText( 'Open in new tab' );
|
||||
fireEvent.press( linkTargetButton );
|
||||
|
||||
const expectedHtml = `<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"custom","className":"is-style-default"} -->
|
||||
<figure class="wp-block-image size-large is-style-default"><a href="https://wordpress.org"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/></a><figcaption>Mountain</figcaption></figure>
|
||||
<!-- /wp:image -->`;
|
||||
expect( getEditorHtml() ).toBe( expectedHtml );
|
||||
} );
|
||||
|
||||
describe( "when replacing media for an image set as the post's featured image", () => {
|
||||
function mockFeaturedMedia( featuredImageId ) {
|
||||
jest.spyOn(
|
||||
select( editorStore ),
|
||||
'getEditedPostAttribute'
|
||||
).mockImplementation( ( attributeName ) =>
|
||||
attributeName === 'featured_media' ? featuredImageId : undefined
|
||||
);
|
||||
}
|
||||
|
||||
function mockGetMedia( media ) {
|
||||
jest.spyOn( select( coreStore ), 'getMedia' ).mockReturnValueOnce(
|
||||
media
|
||||
);
|
||||
}
|
||||
|
||||
it( 'does not prompt to replace featured image during a new image upload', () => {
|
||||
// Arrange
|
||||
const INITIAL_IMAGE = { id: 1, url: 'mock-url-1' };
|
||||
const NEW_IMAGE_PENDING = { id: 2, url: 'mock-url-2' };
|
||||
mockFeaturedMedia( INITIAL_IMAGE.id );
|
||||
const screen = render( <ImageEdit attributes={ INITIAL_IMAGE } /> );
|
||||
|
||||
// Act
|
||||
screen.update( <ImageEdit attributes={ NEW_IMAGE_PENDING } /> );
|
||||
const MEDIA_UPLOAD_STATE_UPLOADING = 1;
|
||||
sendMediaUpload( {
|
||||
state: MEDIA_UPLOAD_STATE_UPLOADING,
|
||||
mediaId: NEW_IMAGE_PENDING.id,
|
||||
progress: 0.1,
|
||||
} );
|
||||
|
||||
// Assert
|
||||
expect( setFeaturedImage ).not.toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( 'does not prompt to replace featured image after a new image upload fails', () => {
|
||||
// Arrange
|
||||
const INITIAL_IMAGE = { id: 1, url: 'mock-url-1' };
|
||||
const NEW_IMAGE_PENDING = { id: 2, url: 'mock-url-2' };
|
||||
mockFeaturedMedia( INITIAL_IMAGE.id );
|
||||
const screen = render(
|
||||
<ImageEdit
|
||||
attributes={ INITIAL_IMAGE }
|
||||
setAttributes={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
// Act
|
||||
screen.update(
|
||||
<ImageEdit
|
||||
attributes={ NEW_IMAGE_PENDING }
|
||||
setAttributes={ () => {} }
|
||||
/>
|
||||
);
|
||||
const MEDIA_UPLOAD_STATE_UPLOADING = 1;
|
||||
sendMediaUpload( {
|
||||
state: MEDIA_UPLOAD_STATE_UPLOADING,
|
||||
mediaId: NEW_IMAGE_PENDING.id,
|
||||
progress: 0.1,
|
||||
} );
|
||||
const MEDIA_UPLOAD_STATE_FAILED = 3;
|
||||
sendMediaUpload( {
|
||||
state: MEDIA_UPLOAD_STATE_FAILED,
|
||||
mediaId: NEW_IMAGE_PENDING.id,
|
||||
} );
|
||||
|
||||
// Assert
|
||||
expect( setFeaturedImage ).not.toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( 'prompts to replace featured image after a new image upload succeeds', () => {
|
||||
// Arrange
|
||||
const INITIAL_IMAGE = { id: 1, url: 'mock-url-1' };
|
||||
const NEW_IMAGE_PENDING = { id: 2, url: 'mock-url-2' };
|
||||
const NEW_IMAGE_RESOLVED = { id: 3, url: 'mock-url-2' };
|
||||
mockFeaturedMedia( INITIAL_IMAGE.id );
|
||||
const screen = render( <ImageEdit attributes={ INITIAL_IMAGE } /> );
|
||||
|
||||
// Act
|
||||
screen.update( <ImageEdit attributes={ NEW_IMAGE_PENDING } /> );
|
||||
mockGetMedia( { id: NEW_IMAGE_RESOLVED.id } );
|
||||
screen.update(
|
||||
<ImageEdit
|
||||
attributes={ NEW_IMAGE_RESOLVED }
|
||||
setAttributes={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect( setFeaturedImage ).toHaveBeenCalledTimes( 1 );
|
||||
expect( setFeaturedImage ).toHaveBeenCalledWith(
|
||||
NEW_IMAGE_RESOLVED.id
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'prompts to replace featured image for a cached image', () => {
|
||||
// Arrange
|
||||
const INITIAL_IMAGE = { id: 1, url: 'mock-url-1' };
|
||||
const NEW_IMAGE_RESOLVED = { id: 3, url: 'mock-url-2' };
|
||||
mockFeaturedMedia( INITIAL_IMAGE.id );
|
||||
const screen = render(
|
||||
<ImageEdit
|
||||
attributes={ INITIAL_IMAGE }
|
||||
setAttributes={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
// Act
|
||||
mockGetMedia( { id: NEW_IMAGE_RESOLVED.id } );
|
||||
screen.update(
|
||||
<ImageEdit
|
||||
attributes={ NEW_IMAGE_RESOLVED }
|
||||
setAttributes={ () => {} }
|
||||
/>
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect( setFeaturedImage ).toHaveBeenCalledTimes( 1 );
|
||||
expect( setFeaturedImage ).toHaveBeenCalledWith(
|
||||
NEW_IMAGE_RESOLVED.id
|
||||
);
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { stripFirstImage } from '../transforms';
|
||||
|
||||
describe( 'stripFirstImage', () => {
|
||||
test( 'should do nothing if no image is present', () => {
|
||||
expect( stripFirstImage( {}, { shortcode: { content: '' } } ) ).toEqual(
|
||||
''
|
||||
);
|
||||
expect(
|
||||
stripFirstImage( {}, { shortcode: { content: 'Tucson' } } )
|
||||
).toEqual( 'Tucson' );
|
||||
expect(
|
||||
stripFirstImage( {}, { shortcode: { content: '<em>Tucson</em>' } } )
|
||||
).toEqual( '<em>Tucson</em>' );
|
||||
} );
|
||||
|
||||
test( 'should strip out image when leading as expected', () => {
|
||||
expect(
|
||||
stripFirstImage( {}, { shortcode: { content: '<img>' } } )
|
||||
).toEqual( '' );
|
||||
expect(
|
||||
stripFirstImage( {}, { shortcode: { content: '<img>Image!' } } )
|
||||
).toEqual( 'Image!' );
|
||||
expect(
|
||||
stripFirstImage(
|
||||
{},
|
||||
{ shortcode: { content: '<img src="image.png">Image!' } }
|
||||
)
|
||||
).toEqual( 'Image!' );
|
||||
} );
|
||||
|
||||
test( 'should strip out image when not in leading position as expected', () => {
|
||||
expect(
|
||||
stripFirstImage( {}, { shortcode: { content: 'Before<img>' } } )
|
||||
).toEqual( 'Before' );
|
||||
expect(
|
||||
stripFirstImage(
|
||||
{},
|
||||
{ shortcode: { content: 'Before<img>Image!' } }
|
||||
)
|
||||
).toEqual( 'BeforeImage!' );
|
||||
expect(
|
||||
stripFirstImage(
|
||||
{},
|
||||
{ shortcode: { content: 'Before<img src="image.png">Image!' } }
|
||||
)
|
||||
).toEqual( 'BeforeImage!' );
|
||||
} );
|
||||
|
||||
test( 'should strip out only the first of many images', () => {
|
||||
expect(
|
||||
stripFirstImage( {}, { shortcode: { content: '<img><img>' } } )
|
||||
).toEqual( '<img>' );
|
||||
} );
|
||||
|
||||
test( 'should strip out the first image and its wrapping parents', () => {
|
||||
expect(
|
||||
stripFirstImage(
|
||||
{},
|
||||
{ shortcode: { content: '<p><a><img></a></p><p><img></p>' } }
|
||||
)
|
||||
).toEqual( '<p><img></p>' );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,3 @@
|
||||
.wp-block-image figcaption {
|
||||
@include caption-style-theme();
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { every } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { createBlobURL } from '@wordpress/blob';
|
||||
import { createBlock, getBlockAttributes } from '@wordpress/blocks';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export function stripFirstImage( attributes, { shortcode } ) {
|
||||
const { body } = document.implementation.createHTMLDocument( '' );
|
||||
|
||||
body.innerHTML = shortcode.content;
|
||||
|
||||
let nodeToRemove = body.querySelector( 'img' );
|
||||
|
||||
// If an image has parents, find the topmost node to remove.
|
||||
while (
|
||||
nodeToRemove &&
|
||||
nodeToRemove.parentNode &&
|
||||
nodeToRemove.parentNode !== body
|
||||
) {
|
||||
nodeToRemove = nodeToRemove.parentNode;
|
||||
}
|
||||
|
||||
if ( nodeToRemove ) {
|
||||
nodeToRemove.parentNode.removeChild( nodeToRemove );
|
||||
}
|
||||
|
||||
return body.innerHTML.trim();
|
||||
}
|
||||
|
||||
function getFirstAnchorAttributeFormHTML( html, attributeName ) {
|
||||
const { body } = document.implementation.createHTMLDocument( '' );
|
||||
|
||||
body.innerHTML = html;
|
||||
|
||||
const { firstElementChild } = body;
|
||||
|
||||
if ( firstElementChild && firstElementChild.nodeName === 'A' ) {
|
||||
return firstElementChild.getAttribute( attributeName ) || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const imageSchema = {
|
||||
img: {
|
||||
attributes: [ 'src', 'alt', 'title' ],
|
||||
classes: [
|
||||
'alignleft',
|
||||
'aligncenter',
|
||||
'alignright',
|
||||
'alignnone',
|
||||
/^wp-image-\d+$/,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const schema = ( { phrasingContentSchema } ) => ( {
|
||||
figure: {
|
||||
require: [ 'img' ],
|
||||
children: {
|
||||
...imageSchema,
|
||||
a: {
|
||||
attributes: [ 'href', 'rel', 'target' ],
|
||||
children: imageSchema,
|
||||
},
|
||||
figcaption: {
|
||||
children: phrasingContentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
} );
|
||||
|
||||
const transforms = {
|
||||
from: [
|
||||
{
|
||||
type: 'raw',
|
||||
isMatch: ( node ) =>
|
||||
node.nodeName === 'FIGURE' && !! node.querySelector( 'img' ),
|
||||
schema,
|
||||
transform: ( node ) => {
|
||||
// Search both figure and image classes. Alignment could be
|
||||
// set on either. ID is set on the image.
|
||||
const className =
|
||||
node.className +
|
||||
' ' +
|
||||
node.querySelector( 'img' ).className;
|
||||
const alignMatches = /(?:^|\s)align(left|center|right)(?:$|\s)/.exec(
|
||||
className
|
||||
);
|
||||
const anchor = node.id === '' ? undefined : node.id;
|
||||
const align = alignMatches ? alignMatches[ 1 ] : undefined;
|
||||
const idMatches = /(?:^|\s)wp-image-(\d+)(?:$|\s)/.exec(
|
||||
className
|
||||
);
|
||||
const id = idMatches ? Number( idMatches[ 1 ] ) : undefined;
|
||||
const anchorElement = node.querySelector( 'a' );
|
||||
const linkDestination =
|
||||
anchorElement && anchorElement.href ? 'custom' : undefined;
|
||||
const href =
|
||||
anchorElement && anchorElement.href
|
||||
? anchorElement.href
|
||||
: undefined;
|
||||
const rel =
|
||||
anchorElement && anchorElement.rel
|
||||
? anchorElement.rel
|
||||
: undefined;
|
||||
const linkClass =
|
||||
anchorElement && anchorElement.className
|
||||
? anchorElement.className
|
||||
: undefined;
|
||||
const attributes = getBlockAttributes(
|
||||
'core/image',
|
||||
node.outerHTML,
|
||||
{
|
||||
align,
|
||||
id,
|
||||
linkDestination,
|
||||
href,
|
||||
rel,
|
||||
linkClass,
|
||||
anchor,
|
||||
}
|
||||
);
|
||||
return createBlock( 'core/image', attributes );
|
||||
},
|
||||
},
|
||||
{
|
||||
// Note: when dragging and dropping multiple files onto a gallery this overrides the
|
||||
// gallery transform in order to add new images to the gallery instead of
|
||||
// creating a new gallery.
|
||||
type: 'files',
|
||||
isMatch( files ) {
|
||||
// The following check is intended to catch non-image files when dropped together with images.
|
||||
if (
|
||||
files.some(
|
||||
( file ) => file.type.indexOf( 'image/' ) === 0
|
||||
) &&
|
||||
files.some(
|
||||
( file ) => file.type.indexOf( 'image/' ) !== 0
|
||||
)
|
||||
) {
|
||||
const { createErrorNotice } = dispatch( noticesStore );
|
||||
createErrorNotice(
|
||||
__(
|
||||
'If uploading to a gallery all files need to be image formats'
|
||||
),
|
||||
{ id: 'gallery-transform-invalid-file' }
|
||||
);
|
||||
}
|
||||
return every(
|
||||
files,
|
||||
( file ) => file.type.indexOf( 'image/' ) === 0
|
||||
);
|
||||
},
|
||||
transform( files ) {
|
||||
const blocks = files.map( ( file ) => {
|
||||
return createBlock( 'core/image', {
|
||||
url: createBlobURL( file ),
|
||||
} );
|
||||
} );
|
||||
return blocks;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shortcode',
|
||||
tag: 'caption',
|
||||
attributes: {
|
||||
url: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
attribute: 'src',
|
||||
selector: 'img',
|
||||
},
|
||||
alt: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
attribute: 'alt',
|
||||
selector: 'img',
|
||||
},
|
||||
caption: {
|
||||
shortcode: stripFirstImage,
|
||||
},
|
||||
href: {
|
||||
shortcode: ( attributes, { shortcode } ) => {
|
||||
return getFirstAnchorAttributeFormHTML(
|
||||
shortcode.content,
|
||||
'href'
|
||||
);
|
||||
},
|
||||
},
|
||||
rel: {
|
||||
shortcode: ( attributes, { shortcode } ) => {
|
||||
return getFirstAnchorAttributeFormHTML(
|
||||
shortcode.content,
|
||||
'rel'
|
||||
);
|
||||
},
|
||||
},
|
||||
linkClass: {
|
||||
shortcode: ( attributes, { shortcode } ) => {
|
||||
return getFirstAnchorAttributeFormHTML(
|
||||
shortcode.content,
|
||||
'class'
|
||||
);
|
||||
},
|
||||
},
|
||||
id: {
|
||||
type: 'number',
|
||||
shortcode: ( { named: { id } } ) => {
|
||||
if ( ! id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
return parseInt( id.replace( 'attachment_', '' ), 10 );
|
||||
},
|
||||
},
|
||||
align: {
|
||||
type: 'string',
|
||||
shortcode: ( { named: { align = 'alignnone' } } ) => {
|
||||
return align.replace( 'align', '' );
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default transforms;
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import webTransforms from './transforms.js';
|
||||
import transformationCategories from '../transformationCategories';
|
||||
|
||||
const transforms = {
|
||||
...webTransforms,
|
||||
supportedMobileTransforms: transformationCategories.media,
|
||||
};
|
||||
|
||||
export default transforms;
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
|
||||
export default function useClientWidth( ref, dependencies ) {
|
||||
const [ clientWidth, setClientWidth ] = useState();
|
||||
|
||||
function calculateClientWidth() {
|
||||
setClientWidth( ref.current.clientWidth );
|
||||
}
|
||||
|
||||
useEffect( calculateClientWidth, dependencies );
|
||||
useEffect( () => {
|
||||
const { defaultView } = ref.current.ownerDocument;
|
||||
|
||||
defaultView.addEventListener( 'resize', calculateClientWidth );
|
||||
|
||||
return () => {
|
||||
defaultView.removeEventListener( 'resize', calculateClientWidth );
|
||||
};
|
||||
}, [] );
|
||||
|
||||
return clientWidth;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isEmpty, each, get } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { NEW_TAB_REL } from './constants';
|
||||
|
||||
export function removeNewTabRel( currentRel ) {
|
||||
let newRel = currentRel;
|
||||
|
||||
if ( currentRel !== undefined && ! isEmpty( newRel ) ) {
|
||||
if ( ! isEmpty( newRel ) ) {
|
||||
each( NEW_TAB_REL, ( relVal ) => {
|
||||
const regExp = new RegExp( '\\b' + relVal + '\\b', 'gi' );
|
||||
newRel = newRel.replace( regExp, '' );
|
||||
} );
|
||||
|
||||
// Only trim if NEW_TAB_REL values was replaced.
|
||||
if ( newRel !== currentRel ) {
|
||||
newRel = newRel.trim();
|
||||
}
|
||||
|
||||
if ( isEmpty( newRel ) ) {
|
||||
newRel = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newRel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the link target settings to be stored.
|
||||
*
|
||||
* @param {boolean} value The new link target value.
|
||||
* @param {Object} attributes Block attributes.
|
||||
* @param {Object} attributes.rel Image block's rel attribute.
|
||||
*
|
||||
* @return {Object} Updated link target settings.
|
||||
*/
|
||||
export function getUpdatedLinkTargetSettings( value, { rel } ) {
|
||||
const linkTarget = value ? '_blank' : undefined;
|
||||
|
||||
let updatedRel;
|
||||
if ( ! linkTarget && ! rel ) {
|
||||
updatedRel = undefined;
|
||||
} else {
|
||||
updatedRel = removeNewTabRel( rel );
|
||||
}
|
||||
|
||||
return {
|
||||
linkTarget,
|
||||
rel: updatedRel,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines new Image block attributes size selection.
|
||||
*
|
||||
* @param {Object} image Media file object for gallery image.
|
||||
* @param {string} size Selected size slug to apply.
|
||||
*/
|
||||
export function getImageSizeAttributes( image, size ) {
|
||||
const url = get( image, [ 'media_details', 'sizes', size, 'source_url' ] );
|
||||
|
||||
if ( url ) {
|
||||
return { url, width: undefined, height: undefined, sizeSlug: size };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
Reference in New Issue
Block a user