Plugin Tabs noticias
This commit is contained in:
@@ -0,0 +1,719 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "areoi/media-grid",
|
||||
"title": "Media Grid",
|
||||
"category": "areoi-strips",
|
||||
"description": "Display multiple medias in a rich gallery.",
|
||||
"keywords": [ "images", "photos" ],
|
||||
"textdomain": "default",
|
||||
"example": {
|
||||
"attributes": {
|
||||
"preview": true
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"preview": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"block_id": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
|
||||
"exclude_divider": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"exclude_pattern": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"change_pattern_color": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"pattern_color": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"hex": "#fff"
|
||||
}
|
||||
},
|
||||
"exclude_transition": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"exclude_parallax": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"default": "grid"
|
||||
},
|
||||
"style": {
|
||||
"type": "string",
|
||||
"default": "flush"
|
||||
},
|
||||
"size": {
|
||||
"type": "string",
|
||||
"default": "areoi-medium"
|
||||
},
|
||||
"card_size": {
|
||||
"type": "string",
|
||||
"default": "areoi-card-small"
|
||||
},
|
||||
"media_fit": {
|
||||
"type": "string",
|
||||
"default": "cover"
|
||||
},
|
||||
"media_height": {
|
||||
"type": "string",
|
||||
"default": "50"
|
||||
},
|
||||
"media_width": {
|
||||
"type": "string",
|
||||
"default": "100"
|
||||
},
|
||||
"media_align": {
|
||||
"type": "string",
|
||||
"default": "center"
|
||||
},
|
||||
"image_layout": {
|
||||
"type": "string",
|
||||
"default": "cover"
|
||||
},
|
||||
"columns": {
|
||||
"type": "string",
|
||||
"default": "3"
|
||||
},
|
||||
"container": {
|
||||
"type": "string",
|
||||
"default": "container"
|
||||
},
|
||||
|
||||
"prepend_display_heading": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"prepend_heading_level": {
|
||||
"type": "string",
|
||||
"default": "h2"
|
||||
},
|
||||
"prepend_heading": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_heading_color": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_display_intro": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"prepend_intro": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_intro_color": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_text_align_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_text_align_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_text_align_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_text_align_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_text_align_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_text_align_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_horizontal_align_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_horizontal_align_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_horizontal_align_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_horizontal_align_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_horizontal_align_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_horizontal_align_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_col_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_col_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_col_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_col_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_col_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"prepend_col_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
|
||||
"images": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"source": "query",
|
||||
"selector": ".blocks-gallery-item",
|
||||
"query": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "img",
|
||||
"attribute": "src"
|
||||
},
|
||||
"fullUrl": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "img",
|
||||
"attribute": "data-full-url"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "img",
|
||||
"attribute": "data-link"
|
||||
},
|
||||
"alt": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "img",
|
||||
"attribute": "alt",
|
||||
"default": ""
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"source": "attribute",
|
||||
"selector": "img",
|
||||
"attribute": "data-id"
|
||||
},
|
||||
"caption": {
|
||||
"type": "string",
|
||||
"source": "html",
|
||||
"selector": ".blocks-gallery-item__caption"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"shortCodeTransforms": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"caption": {
|
||||
"type": "string",
|
||||
"source": "html",
|
||||
"selector": ".blocks-gallery-caption"
|
||||
},
|
||||
"imageCrop": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"fixedHeight": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"linkTarget": {
|
||||
"type": "string"
|
||||
},
|
||||
"linkTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeSlug": {
|
||||
"type": "string",
|
||||
"default": "large"
|
||||
},
|
||||
"allowResize": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
|
||||
"vertical_align_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"vertical_align_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"vertical_align_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"vertical_align_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"vertical_align_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"vertical_align_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
|
||||
"horizontal_align_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"horizontal_align_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"horizontal_align_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"horizontal_align_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"horizontal_align_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"horizontal_align_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"utilities_bg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"utilities_text": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"utilities_border": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"background_display": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_color": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"rgb": {
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"background_image": {
|
||||
"type": "object",
|
||||
"default": null
|
||||
},
|
||||
"background_video": {
|
||||
"type": "object",
|
||||
"default": null
|
||||
},
|
||||
"background_display_overlay": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_overlay": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"rgb": {
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"background_horizontal_align": {
|
||||
"type": "string",
|
||||
"default": "justify-content-start"
|
||||
},
|
||||
|
||||
"height_dimension_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"height_unit_xs": {
|
||||
"type": "string",
|
||||
"default": "px"
|
||||
},
|
||||
"padding_top_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_right_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_bottom_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_left_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_top_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_right_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_bottom_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_left_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"hide_xs": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_hide_xs": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_col_xs": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
|
||||
"height_dimension_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"height_unit_sm": {
|
||||
"type": "string",
|
||||
"default": "px"
|
||||
},
|
||||
"padding_top_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_right_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_bottom_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_left_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_top_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_right_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_bottom_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_left_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"hide_sm": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_hide_sm": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_col_sm": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
|
||||
"height_dimension_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"height_unit_md": {
|
||||
"type": "string",
|
||||
"default": "px"
|
||||
},
|
||||
"padding_top_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_right_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_bottom_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_left_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_top_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_right_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_bottom_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_left_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"hide_md": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_hide_md": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_col_md": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
|
||||
"height_dimension_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"height_unit_lg": {
|
||||
"type": "string",
|
||||
"default": "px"
|
||||
},
|
||||
"padding_top_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_right_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_bottom_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_left_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_top_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_right_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_bottom_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_left_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"hide_lg": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_hide_lg": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_col_lg": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
|
||||
"height_dimension_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"height_unit_xl": {
|
||||
"type": "string",
|
||||
"default": "px"
|
||||
},
|
||||
"padding_top_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_right_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_bottom_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_left_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_top_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_right_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_bottom_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_left_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"hide_xl": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_hide_xl": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_col_xl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
|
||||
"height_dimension_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"height_unit_xxl": {
|
||||
"type": "string",
|
||||
"default": "px"
|
||||
},
|
||||
"padding_top_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_right_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_bottom_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"padding_left_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_top_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_right_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_bottom_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"margin_left_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"hide_xxl": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_hide_xxl": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"background_col_xxl": {
|
||||
"type": "string",
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"providesContext": {
|
||||
"allowResize": "allowResize",
|
||||
"imageCrop": "imageCrop",
|
||||
"fixedHeight": "fixedHeight"
|
||||
},
|
||||
"supports": {
|
||||
"anchor": true,
|
||||
"align": true,
|
||||
"html": false
|
||||
},
|
||||
"editorScript": "areoi-blocks",
|
||||
"editorStyle": "file:./index.css",
|
||||
"style": "file:../../build/style.css"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const LINK_DESTINATION_NONE = 'none';
|
||||
export const LINK_DESTINATION_MEDIA = 'media';
|
||||
export const LINK_DESTINATION_ATTACHMENT = 'attachment';
|
||||
export const LINK_DESTINATION_MEDIA_WP_CORE = 'file';
|
||||
export const LINK_DESTINATION_ATTACHMENT_WP_CORE = 'post';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -0,0 +1,985 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { map, some, omit } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { RichText, useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_MEDIA,
|
||||
LINK_DESTINATION_NONE,
|
||||
} from './constants';
|
||||
import { isGalleryV2Enabled } from './shared';
|
||||
|
||||
const DEPRECATED_LINK_DESTINATION_MEDIA = 'file';
|
||||
const DEPRECATED_LINK_DESTINATION_ATTACHMENT = 'post';
|
||||
|
||||
/**
|
||||
* Original function to determine default number of columns from a block's
|
||||
* attributes.
|
||||
*
|
||||
* Used in deprecations: v1-6, for versions of the gallery block that didn't use inner blocks.
|
||||
*
|
||||
* @param {Object} attributes Block attributes.
|
||||
* @return {number} Default number of columns for the gallery.
|
||||
*/
|
||||
export function defaultColumnsNumberV1( attributes ) {
|
||||
return Math.min( 3, attributes?.images?.length );
|
||||
}
|
||||
|
||||
/**
|
||||
* Original function to determine new href and linkDestination values for an image block from the
|
||||
* supplied Gallery link destination.
|
||||
*
|
||||
* Used in deprecations: v1-6.
|
||||
*
|
||||
* @param {Object} image Gallery image.
|
||||
* @param {string} destination Gallery's selected link destination.
|
||||
* @return {Object} New attributes to assign to image block.
|
||||
*/
|
||||
export function getHrefAndDestination( image, destination ) {
|
||||
// Need to determine the URL that the selected destination maps to.
|
||||
// Gutenberg and WordPress use different constants so the new link
|
||||
// destination also needs to be tweaked.
|
||||
switch ( destination ) {
|
||||
case DEPRECATED_LINK_DESTINATION_MEDIA:
|
||||
return {
|
||||
href: image?.source_url || image?.url, // eslint-disable-line camelcase
|
||||
linkDestination: LINK_DESTINATION_MEDIA,
|
||||
};
|
||||
case DEPRECATED_LINK_DESTINATION_ATTACHMENT:
|
||||
return {
|
||||
href: image?.link,
|
||||
linkDestination: LINK_DESTINATION_ATTACHMENT,
|
||||
};
|
||||
case LINK_DESTINATION_MEDIA:
|
||||
return {
|
||||
href: image?.source_url || image?.url, // eslint-disable-line camelcase
|
||||
linkDestination: LINK_DESTINATION_MEDIA,
|
||||
};
|
||||
case LINK_DESTINATION_ATTACHMENT:
|
||||
return {
|
||||
href: image?.link,
|
||||
linkDestination: LINK_DESTINATION_ATTACHMENT,
|
||||
};
|
||||
case LINK_DESTINATION_NONE:
|
||||
return {
|
||||
href: undefined,
|
||||
linkDestination: LINK_DESTINATION_NONE,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function runV2Migration( attributes ) {
|
||||
let linkTo = attributes.linkTo ? attributes.linkTo : 'none';
|
||||
|
||||
if ( linkTo === 'post' ) {
|
||||
linkTo = 'attachment';
|
||||
} else if ( linkTo === 'file' ) {
|
||||
linkTo = 'media';
|
||||
}
|
||||
|
||||
const imageBlocks = attributes.images.map( ( image ) => {
|
||||
return getImageBlock( image, attributes.sizeSlug, linkTo );
|
||||
} );
|
||||
|
||||
return [
|
||||
{
|
||||
...omit( attributes, [ 'images', 'ids' ] ),
|
||||
linkTo,
|
||||
allowResize: false,
|
||||
},
|
||||
imageBlocks,
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Gets an Image block from gallery image data
|
||||
*
|
||||
* Used to migrate Galleries to nested Image InnerBlocks.
|
||||
*
|
||||
* @param {Object} image Image properties.
|
||||
* @param {string} sizeSlug Gallery sizeSlug attribute.
|
||||
* @param {string} linkTo Gallery linkTo attribute.
|
||||
* @return {Object} Image block.
|
||||
*/
|
||||
export function getImageBlock( image, sizeSlug, linkTo ) {
|
||||
return createBlock( 'core/image', {
|
||||
...( image.id && { id: parseInt( image.id ) } ),
|
||||
url: image.url,
|
||||
alt: image.alt,
|
||||
caption: image.caption,
|
||||
sizeSlug,
|
||||
...getHrefAndDestination( image, linkTo ),
|
||||
} );
|
||||
}
|
||||
|
||||
const v6 = {
|
||||
attributes: {
|
||||
images: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
source: 'query',
|
||||
selector: '.blocks-gallery-item',
|
||||
query: {
|
||||
url: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'src',
|
||||
},
|
||||
fullUrl: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-full-url',
|
||||
},
|
||||
link: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-link',
|
||||
},
|
||||
alt: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'alt',
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-id',
|
||||
},
|
||||
caption: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.blocks-gallery-item__caption',
|
||||
},
|
||||
},
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'number',
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
maximum: 8,
|
||||
},
|
||||
caption: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.blocks-gallery-caption',
|
||||
},
|
||||
imageCrop: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
fixedHeight: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
linkTo: {
|
||||
type: 'string',
|
||||
},
|
||||
sizeSlug: {
|
||||
type: 'string',
|
||||
default: 'large',
|
||||
},
|
||||
},
|
||||
supports: {
|
||||
anchor: true,
|
||||
align: true,
|
||||
},
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
images,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
imageCrop,
|
||||
caption,
|
||||
linkTo,
|
||||
} = attributes;
|
||||
const className = `columns-${ columns } ${
|
||||
imageCrop ? 'is-cropped' : ''
|
||||
}`;
|
||||
|
||||
return (
|
||||
<figure { ...useBlockProps.save( { className } ) }>
|
||||
<ul className="blocks-gallery-grid">
|
||||
{ images.map( ( image ) => {
|
||||
let href;
|
||||
|
||||
switch ( linkTo ) {
|
||||
case DEPRECATED_LINK_DESTINATION_MEDIA:
|
||||
href = image.fullUrl || image.url;
|
||||
break;
|
||||
case DEPRECATED_LINK_DESTINATION_ATTACHMENT:
|
||||
href = image.link;
|
||||
break;
|
||||
}
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={ image.url }
|
||||
alt={ image.alt }
|
||||
data-id={ image.id }
|
||||
data-full-url={ image.fullUrl }
|
||||
data-link={ image.link }
|
||||
className={
|
||||
image.id ? `wp-image-${ image.id }` : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={ image.id || image.url }
|
||||
className="blocks-gallery-item"
|
||||
>
|
||||
<figure>
|
||||
{ href ? (
|
||||
<a href={ href }>{ img }</a>
|
||||
) : (
|
||||
img
|
||||
) }
|
||||
{ ! RichText.isEmpty( image.caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-item__caption"
|
||||
value={ image.caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-caption"
|
||||
value={ caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
);
|
||||
},
|
||||
migrate( attributes ) {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
return runV2Migration( attributes );
|
||||
}
|
||||
|
||||
return attributes;
|
||||
},
|
||||
};
|
||||
const v5 = {
|
||||
attributes: {
|
||||
images: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
source: 'query',
|
||||
selector: '.blocks-gallery-item',
|
||||
query: {
|
||||
url: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'src',
|
||||
},
|
||||
fullUrl: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-full-url',
|
||||
},
|
||||
link: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-link',
|
||||
},
|
||||
alt: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'alt',
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-id',
|
||||
},
|
||||
caption: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.blocks-gallery-item__caption',
|
||||
},
|
||||
},
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'number',
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
maximum: 8,
|
||||
},
|
||||
caption: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.blocks-gallery-caption',
|
||||
},
|
||||
imageCrop: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
linkTo: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
},
|
||||
sizeSlug: {
|
||||
type: 'string',
|
||||
default: 'large',
|
||||
},
|
||||
},
|
||||
supports: {
|
||||
align: true,
|
||||
},
|
||||
isEligible( { linkTo } ) {
|
||||
return ! linkTo || linkTo === 'attachment' || linkTo === 'media';
|
||||
},
|
||||
migrate( attributes ) {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
return runV2Migration( attributes );
|
||||
}
|
||||
|
||||
let linkTo = attributes.linkTo;
|
||||
|
||||
if ( ! attributes.linkTo ) {
|
||||
linkTo = 'none';
|
||||
} else if ( attributes.linkTo === 'attachment' ) {
|
||||
linkTo = 'post';
|
||||
} else if ( attributes.linkTo === 'media' ) {
|
||||
linkTo = 'file';
|
||||
}
|
||||
return {
|
||||
...attributes,
|
||||
linkTo,
|
||||
};
|
||||
},
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
images,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
imageCrop,
|
||||
caption,
|
||||
linkTo,
|
||||
} = attributes;
|
||||
|
||||
return (
|
||||
<figure
|
||||
className={ `columns-${ columns } ${
|
||||
imageCrop ? 'is-cropped' : ''
|
||||
}` }
|
||||
>
|
||||
<ul className="blocks-gallery-grid">
|
||||
{ images.map( ( image ) => {
|
||||
let href;
|
||||
|
||||
switch ( linkTo ) {
|
||||
case 'media':
|
||||
href = image.fullUrl || image.url;
|
||||
break;
|
||||
case 'attachment':
|
||||
href = image.link;
|
||||
break;
|
||||
}
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={ image.url }
|
||||
alt={ image.alt }
|
||||
data-id={ image.id }
|
||||
data-full-url={ image.fullUrl }
|
||||
data-link={ image.link }
|
||||
className={
|
||||
image.id ? `wp-image-${ image.id }` : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={ image.id || image.url }
|
||||
className="blocks-gallery-item"
|
||||
>
|
||||
<figure>
|
||||
{ href ? (
|
||||
<a href={ href }>{ img }</a>
|
||||
) : (
|
||||
img
|
||||
) }
|
||||
{ ! RichText.isEmpty( image.caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-item__caption"
|
||||
value={ image.caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-caption"
|
||||
value={ caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const v4 = {
|
||||
attributes: {
|
||||
images: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
source: 'query',
|
||||
selector: '.blocks-gallery-item',
|
||||
query: {
|
||||
url: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'src',
|
||||
},
|
||||
fullUrl: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-full-url',
|
||||
},
|
||||
link: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-link',
|
||||
},
|
||||
alt: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'alt',
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-id',
|
||||
},
|
||||
caption: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.blocks-gallery-item__caption',
|
||||
},
|
||||
},
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
},
|
||||
caption: {
|
||||
type: 'string',
|
||||
source: 'html',
|
||||
selector: '.blocks-gallery-caption',
|
||||
},
|
||||
imageCrop: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
linkTo: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
},
|
||||
},
|
||||
supports: {
|
||||
align: true,
|
||||
},
|
||||
isEligible( { ids } ) {
|
||||
return ids && ids.some( ( id ) => typeof id === 'string' );
|
||||
},
|
||||
migrate( attributes ) {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
return runV2Migration( attributes );
|
||||
}
|
||||
|
||||
return {
|
||||
...attributes,
|
||||
ids: map( attributes.ids, ( id ) => {
|
||||
const parsedId = parseInt( id, 10 );
|
||||
return Number.isInteger( parsedId ) ? parsedId : null;
|
||||
} ),
|
||||
};
|
||||
},
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
images,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
imageCrop,
|
||||
caption,
|
||||
linkTo,
|
||||
} = attributes;
|
||||
|
||||
return (
|
||||
<figure
|
||||
className={ `columns-${ columns } ${
|
||||
imageCrop ? 'is-cropped' : ''
|
||||
}` }
|
||||
>
|
||||
<ul className="blocks-gallery-grid">
|
||||
{ images.map( ( image ) => {
|
||||
let href;
|
||||
|
||||
switch ( linkTo ) {
|
||||
case 'media':
|
||||
href = image.fullUrl || image.url;
|
||||
break;
|
||||
case 'attachment':
|
||||
href = image.link;
|
||||
break;
|
||||
}
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={ image.url }
|
||||
alt={ image.alt }
|
||||
data-id={ image.id }
|
||||
data-full-url={ image.fullUrl }
|
||||
data-link={ image.link }
|
||||
className={
|
||||
image.id ? `wp-image-${ image.id }` : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={ image.id || image.url }
|
||||
className="blocks-gallery-item"
|
||||
>
|
||||
<figure>
|
||||
{ href ? (
|
||||
<a href={ href }>{ img }</a>
|
||||
) : (
|
||||
img
|
||||
) }
|
||||
{ ! RichText.isEmpty( image.caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-item__caption"
|
||||
value={ image.caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-caption"
|
||||
value={ caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
);
|
||||
},
|
||||
};
|
||||
const v3 = {
|
||||
attributes: {
|
||||
images: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
source: 'query',
|
||||
selector: 'ul.wp-block-gallery .blocks-gallery-item',
|
||||
query: {
|
||||
url: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'src',
|
||||
},
|
||||
fullUrl: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-full-url',
|
||||
},
|
||||
alt: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'alt',
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-id',
|
||||
},
|
||||
link: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-link',
|
||||
},
|
||||
caption: {
|
||||
type: 'array',
|
||||
source: 'children',
|
||||
selector: 'figcaption',
|
||||
},
|
||||
},
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
},
|
||||
imageCrop: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
linkTo: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
},
|
||||
},
|
||||
supports: {
|
||||
align: true,
|
||||
},
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
images,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
imageCrop,
|
||||
linkTo,
|
||||
} = attributes;
|
||||
return (
|
||||
<ul
|
||||
className={ `columns-${ columns } ${
|
||||
imageCrop ? 'is-cropped' : ''
|
||||
}` }
|
||||
>
|
||||
{ images.map( ( image ) => {
|
||||
let href;
|
||||
|
||||
switch ( linkTo ) {
|
||||
case 'media':
|
||||
href = image.fullUrl || image.url;
|
||||
break;
|
||||
case 'attachment':
|
||||
href = image.link;
|
||||
break;
|
||||
}
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={ image.url }
|
||||
alt={ image.alt }
|
||||
data-id={ image.id }
|
||||
data-full-url={ image.fullUrl }
|
||||
data-link={ image.link }
|
||||
className={
|
||||
image.id ? `wp-image-${ image.id }` : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={ image.id || image.url }
|
||||
className="blocks-gallery-item"
|
||||
>
|
||||
<figure>
|
||||
{ href ? <a href={ href }>{ img }</a> : img }
|
||||
{ image.caption && image.caption.length > 0 && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
value={ image.caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
migrate( attributes ) {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
return runV2Migration( attributes );
|
||||
}
|
||||
return attributes;
|
||||
},
|
||||
};
|
||||
const v2 = {
|
||||
attributes: {
|
||||
images: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
source: 'query',
|
||||
selector: 'ul.wp-block-gallery .blocks-gallery-item',
|
||||
query: {
|
||||
url: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'src',
|
||||
},
|
||||
alt: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'alt',
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-id',
|
||||
},
|
||||
link: {
|
||||
source: 'attribute',
|
||||
selector: 'img',
|
||||
attribute: 'data-link',
|
||||
},
|
||||
caption: {
|
||||
type: 'array',
|
||||
source: 'children',
|
||||
selector: 'figcaption',
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
},
|
||||
imageCrop: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
linkTo: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
},
|
||||
},
|
||||
isEligible( { images, ids } ) {
|
||||
return (
|
||||
images &&
|
||||
images.length > 0 &&
|
||||
( ( ! ids && images ) ||
|
||||
( ids && images && ids.length !== images.length ) ||
|
||||
some( images, ( id, index ) => {
|
||||
if ( ! id && ids[ index ] !== null ) {
|
||||
return true;
|
||||
}
|
||||
return parseInt( id, 10 ) !== ids[ index ];
|
||||
} ) )
|
||||
);
|
||||
},
|
||||
migrate( attributes ) {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
return runV2Migration( attributes );
|
||||
}
|
||||
return {
|
||||
...attributes,
|
||||
ids: map( attributes.images, ( { id } ) => {
|
||||
if ( ! id ) {
|
||||
return null;
|
||||
}
|
||||
return parseInt( id, 10 );
|
||||
} ),
|
||||
};
|
||||
},
|
||||
supports: {
|
||||
align: true,
|
||||
},
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
images,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
imageCrop,
|
||||
linkTo,
|
||||
} = attributes;
|
||||
return (
|
||||
<ul
|
||||
className={ `columns-${ columns } ${
|
||||
imageCrop ? 'is-cropped' : ''
|
||||
}` }
|
||||
>
|
||||
{ images.map( ( image ) => {
|
||||
let href;
|
||||
|
||||
switch ( linkTo ) {
|
||||
case 'media':
|
||||
href = image.url;
|
||||
break;
|
||||
case 'attachment':
|
||||
href = image.link;
|
||||
break;
|
||||
}
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={ image.url }
|
||||
alt={ image.alt }
|
||||
data-id={ image.id }
|
||||
data-link={ image.link }
|
||||
className={
|
||||
image.id ? `wp-image-${ image.id }` : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={ image.id || image.url }
|
||||
className="blocks-gallery-item"
|
||||
>
|
||||
<figure>
|
||||
{ href ? <a href={ href }>{ img }</a> : img }
|
||||
{ image.caption && image.caption.length > 0 && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
value={ image.caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const v1 = {
|
||||
attributes: {
|
||||
images: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
source: 'query',
|
||||
selector: 'div.wp-block-gallery figure.blocks-gallery-image img',
|
||||
query: {
|
||||
url: {
|
||||
source: 'attribute',
|
||||
attribute: 'src',
|
||||
},
|
||||
alt: {
|
||||
source: 'attribute',
|
||||
attribute: 'alt',
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
source: 'attribute',
|
||||
attribute: 'data-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
},
|
||||
imageCrop: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
linkTo: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
},
|
||||
align: {
|
||||
type: 'string',
|
||||
default: 'none',
|
||||
},
|
||||
},
|
||||
supports: {
|
||||
align: true,
|
||||
},
|
||||
save( { attributes } ) {
|
||||
const {
|
||||
images,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
align,
|
||||
imageCrop,
|
||||
linkTo,
|
||||
} = attributes;
|
||||
const className = classnames( `columns-${ columns }`, {
|
||||
alignnone: align === 'none',
|
||||
'is-cropped': imageCrop,
|
||||
} );
|
||||
return (
|
||||
<div className={ className }>
|
||||
{ images.map( ( image ) => {
|
||||
let href;
|
||||
|
||||
switch ( linkTo ) {
|
||||
case 'media':
|
||||
href = image.url;
|
||||
break;
|
||||
case 'attachment':
|
||||
href = image.link;
|
||||
break;
|
||||
}
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={ image.url }
|
||||
alt={ image.alt }
|
||||
data-id={ image.id }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<figure
|
||||
key={ image.id || image.url }
|
||||
className="blocks-gallery-image"
|
||||
>
|
||||
{ href ? <a href={ href }>{ img }</a> : img }
|
||||
</figure>
|
||||
);
|
||||
} ) }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
migrate( attributes ) {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
return runV2Migration( attributes );
|
||||
}
|
||||
|
||||
return attributes;
|
||||
},
|
||||
};
|
||||
|
||||
export default [ v6, v5, v4, v3, v2, v1 ];
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { withNotices } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import EditWithInnerBlocks from './edit';
|
||||
import EditWithoutInnerBlocks from './v1/edit';
|
||||
import { isGalleryV2Enabled } from './shared';
|
||||
|
||||
/*
|
||||
* Using a wrapper around the logic to load the edit for v1 of Gallery block
|
||||
* or the refactored version with InnerBlocks. This is to prevent conditional
|
||||
* use of hooks lint errors if adding this logic to the top of the edit component.
|
||||
*/
|
||||
function GalleryEditWrapper( props ) {
|
||||
if ( ! isGalleryV2Enabled() ) {
|
||||
return <EditWithoutInnerBlocks { ...props } />;
|
||||
}
|
||||
|
||||
return <EditWithInnerBlocks { ...props } />;
|
||||
}
|
||||
|
||||
export default compose( [ withNotices ] )( GalleryEditWrapper );
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { RichText, useInnerBlocksProps } from '@wordpress/block-editor';
|
||||
import { VisuallyHidden } from '@wordpress/components';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
import { View } from '@wordpress/primitives';
|
||||
|
||||
const allowedBlocks = [ 'areoi/media-grid-image' ];
|
||||
|
||||
export const Gallery = ( props ) => {
|
||||
const {
|
||||
attributes,
|
||||
isSelected,
|
||||
setAttributes,
|
||||
mediaPlaceholder,
|
||||
insertBlocksAfter,
|
||||
blockProps,
|
||||
} = props;
|
||||
|
||||
const { align, columns, caption, imageCrop } = attributes;
|
||||
|
||||
const { children, ...innerBlocksProps } = useInnerBlocksProps( blockProps, {
|
||||
allowedBlocks,
|
||||
orientation: 'horizontal',
|
||||
renderAppender: false,
|
||||
__experimentalLayout: { type: 'default', alignments: [] },
|
||||
} );
|
||||
|
||||
const [ captionFocused, setCaptionFocused ] = useState( false );
|
||||
|
||||
function onFocusCaption() {
|
||||
if ( ! captionFocused ) {
|
||||
setCaptionFocused( true );
|
||||
}
|
||||
}
|
||||
|
||||
function removeCaptionFocus() {
|
||||
if ( captionFocused ) {
|
||||
setCaptionFocused( false );
|
||||
}
|
||||
}
|
||||
|
||||
useEffect( () => {
|
||||
if ( ! isSelected ) {
|
||||
setCaptionFocused( false );
|
||||
}
|
||||
}, [ isSelected ] );
|
||||
|
||||
return (
|
||||
<>
|
||||
{ children }
|
||||
{ isSelected && (
|
||||
<View
|
||||
className="blocks-gallery-media-placeholder-wrapper"
|
||||
onClick={ removeCaptionFocus }
|
||||
>
|
||||
{ mediaPlaceholder }
|
||||
</View>
|
||||
) }
|
||||
<RichTextVisibilityHelper
|
||||
isHidden={ ! isSelected && RichText.isEmpty( caption ) }
|
||||
captionFocused={ captionFocused }
|
||||
onFocusCaption={ onFocusCaption }
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-caption"
|
||||
aria-label={ __( 'Gallery caption text' ) }
|
||||
placeholder={ __( 'Write gallery caption…' ) }
|
||||
value={ caption }
|
||||
onChange={ ( value ) => setAttributes( { caption: value } ) }
|
||||
inlineToolbar
|
||||
__unstableOnSplitAtEnd={ () =>
|
||||
insertBlocksAfter( createBlock( 'core/paragraph' ) )
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function RichTextVisibilityHelper( {
|
||||
isHidden,
|
||||
captionFocused,
|
||||
onFocusCaption,
|
||||
className,
|
||||
value,
|
||||
placeholder,
|
||||
tagName,
|
||||
captionRef,
|
||||
...richTextProps
|
||||
} ) {
|
||||
if ( isHidden ) {
|
||||
return <VisuallyHidden as={ RichText } { ...richTextProps } />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RichText
|
||||
ref={ captionRef }
|
||||
value={ value }
|
||||
placeholder={ placeholder }
|
||||
className={ className }
|
||||
tagName={ tagName }
|
||||
isSelected={ captionFocused }
|
||||
onClick={ onFocusCaption }
|
||||
{ ...richTextProps }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Gallery;
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { View } from 'react-native';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultColumnsNumber } from './shared';
|
||||
import styles from './gallery-styles.scss';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { BlockCaption, useInnerBlocksProps } from '@wordpress/block-editor';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { mediaUploadSync } from '@wordpress/react-native-bridge';
|
||||
import { WIDE_ALIGNMENTS } from '@wordpress/components';
|
||||
import { useResizeObserver } from '@wordpress/compose';
|
||||
|
||||
const TILE_SPACING = 8;
|
||||
|
||||
// we must limit displayed columns since readable content max-width is 580px
|
||||
const MAX_DISPLAYED_COLUMNS = 4;
|
||||
const MAX_DISPLAYED_COLUMNS_NARROW = 2;
|
||||
|
||||
export const Gallery = ( props ) => {
|
||||
const [ isCaptionSelected, setIsCaptionSelected ] = useState( false );
|
||||
const [ resizeObserver, sizes ] = useResizeObserver();
|
||||
const [ maxWidth, setMaxWidth ] = useState( 0 );
|
||||
useEffect( mediaUploadSync, [] );
|
||||
|
||||
const {
|
||||
mediaPlaceholder,
|
||||
attributes,
|
||||
images,
|
||||
isNarrow,
|
||||
onBlur,
|
||||
insertBlocksAfter,
|
||||
clientId,
|
||||
} = props;
|
||||
|
||||
useEffect( () => {
|
||||
const { width } = sizes || {};
|
||||
if ( width ) {
|
||||
setMaxWidth( width );
|
||||
}
|
||||
}, [ sizes ] );
|
||||
|
||||
const {
|
||||
align,
|
||||
columns = defaultColumnsNumber( images.length ),
|
||||
} = attributes;
|
||||
|
||||
const displayedColumns = Math.min(
|
||||
columns,
|
||||
isNarrow ? MAX_DISPLAYED_COLUMNS_NARROW : MAX_DISPLAYED_COLUMNS
|
||||
);
|
||||
|
||||
const innerBlocksProps = useInnerBlocksProps(
|
||||
{},
|
||||
{
|
||||
contentResizeMode: 'stretch',
|
||||
allowedBlocks: [ 'core/image' ],
|
||||
orientation: 'horizontal',
|
||||
renderAppender: false,
|
||||
numColumns: displayedColumns,
|
||||
marginHorizontal: TILE_SPACING,
|
||||
marginVertical: TILE_SPACING,
|
||||
__experimentalLayout: { type: 'default', alignments: [] },
|
||||
gridProperties: {
|
||||
numColumns: displayedColumns,
|
||||
},
|
||||
parentWidth: maxWidth + 2 * TILE_SPACING,
|
||||
}
|
||||
);
|
||||
|
||||
const focusGalleryCaption = () => {
|
||||
if ( ! isCaptionSelected ) {
|
||||
setIsCaptionSelected( true );
|
||||
}
|
||||
};
|
||||
|
||||
const isFullWidth = align === WIDE_ALIGNMENTS.alignments.full;
|
||||
|
||||
return (
|
||||
<View style={ isFullWidth && styles.fullWidth }>
|
||||
{ resizeObserver }
|
||||
<View { ...innerBlocksProps } />
|
||||
<View
|
||||
style={ [
|
||||
isFullWidth && styles.fullWidth,
|
||||
styles.galleryAppender,
|
||||
] }
|
||||
>
|
||||
{ mediaPlaceholder }
|
||||
</View>
|
||||
<BlockCaption
|
||||
clientId={ clientId }
|
||||
isSelected={ isCaptionSelected }
|
||||
accessible={ true }
|
||||
accessibilityLabelCreator={ ( caption ) =>
|
||||
isEmpty( caption )
|
||||
? /* translators: accessibility text. Empty gallery caption. */
|
||||
|
||||
'Gallery caption. Empty'
|
||||
: sprintf(
|
||||
/* translators: accessibility text. %s: gallery caption. */
|
||||
__( 'Gallery caption. %s' ),
|
||||
caption
|
||||
)
|
||||
}
|
||||
onFocus={ focusGalleryCaption }
|
||||
onBlur={ onBlur } // Always assign onBlur as props.
|
||||
insertBlocksAfter={ insertBlocksAfter }
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Gallery;
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { useStyleOverride } from '@wordpress/block-editor';
|
||||
|
||||
export default function GapStyles({ blockGap, clientId }) {
|
||||
// Build the CSS scoped to this block instance
|
||||
const css = `#block-${clientId} { --wp--style--unstable-gallery-gap: ${
|
||||
blockGap ?? 'var(--wp--style--block-gap, 0.5em)'
|
||||
} }`;
|
||||
|
||||
// Insert/update a <style> in the editor canvas (handles iframe, etc.)
|
||||
useStyleOverride({
|
||||
id: `gap-styles-${clientId}`, // stable id so it updates instead of duplicating
|
||||
css,
|
||||
});
|
||||
|
||||
// Nothing to render
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import url( '../content-grid/index.css' );
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { gallery as icon } from '@wordpress/icons';
|
||||
import * as areoi from '../_components/Core.js';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import deprecated from './deprecated';
|
||||
import edit from './edit-wrapper';
|
||||
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: {
|
||||
columns: 2,
|
||||
},
|
||||
innerBlocks: [
|
||||
{
|
||||
name: 'core/image',
|
||||
attributes: {
|
||||
url:
|
||||
'https://s.w.org/images/core/5.3/Glacial_lakes%2C_Bhutan.jpg',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'core/image',
|
||||
attributes: {
|
||||
url:
|
||||
'https://s.w.org/images/core/5.3/Sediment_off_the_Yucatan_Peninsula.jpg',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
transforms,
|
||||
edit,
|
||||
save,
|
||||
deprecated,
|
||||
};
|
||||
|
||||
areoi.blocks.registerBlockType( metadata, {
|
||||
icon: icon,
|
||||
edit: edit,
|
||||
save: () => {
|
||||
return (
|
||||
<areoi.editor.InnerBlocks.Content/>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* Server-side rendering of the `core/gallery` block.
|
||||
*
|
||||
* @package WordPress
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handles backwards compatibility for Gallery Blocks,
|
||||
* whose images feature a `data-id` attribute.
|
||||
*
|
||||
* Now that the Gallery Block contains inner Image Blocks,
|
||||
* we add a custom `data-id` attribute before rendering the gallery
|
||||
* so that the Image Block can pick it up in its render_callback.
|
||||
*
|
||||
* @param array $parsed_block The block being rendered.
|
||||
* @return array The migrated block object.
|
||||
*/
|
||||
function block_core_gallery_data_id_backcompatibility( $parsed_block ) {
|
||||
if ( 'core/gallery' === $parsed_block['blockName'] ) {
|
||||
foreach ( $parsed_block['innerBlocks'] as $key => $inner_block ) {
|
||||
if ( 'core/image' === $inner_block['blockName'] ) {
|
||||
if ( ! isset( $parsed_block['innerBlocks'][ $key ]['attrs']['data-id'] ) && isset( $inner_block['attrs']['id'] ) ) {
|
||||
$parsed_block['innerBlocks'][ $key ]['attrs']['data-id'] = esc_attr( $inner_block['attrs']['id'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parsed_block;
|
||||
}
|
||||
|
||||
add_filter( 'render_block_data', 'block_core_gallery_data_id_backcompatibility' );
|
||||
|
||||
/**
|
||||
* Adds a style tag for the --wp--style--unstable-gallery-gap var.
|
||||
*
|
||||
* The Gallery block needs to recalculate Image block width based on
|
||||
* the current gap setting in order to maintain the number of flex columns
|
||||
* so a css var is added to allow this.
|
||||
*
|
||||
* @param array $attributes Attributes of the block being rendered.
|
||||
* @param string $content Content of the block being rendered.
|
||||
* @return string The content of the block being rendered.
|
||||
*/
|
||||
function block_core_gallery_render( $attributes, $content ) {
|
||||
$gap = _wp_array_get( $attributes, array( 'style', 'spacing', 'blockGap' ) );
|
||||
// Skip if gap value contains unsupported characters.
|
||||
// Regex for CSS value borrowed from `safecss_filter_attr`, and used here
|
||||
// because we only want to match against the value, not the CSS attribute.
|
||||
$gap = preg_match( '%[\\\(&=}]|/\*%', $gap ) ? null : $gap;
|
||||
$id = uniqid();
|
||||
$class = 'wp-block-gallery-' . $id;
|
||||
$content = preg_replace(
|
||||
'/' . preg_quote( 'class="', '/' ) . '/',
|
||||
'class="' . $class . ' ',
|
||||
$content,
|
||||
1
|
||||
);
|
||||
$gap_value = $gap ? $gap : 'var( --wp--style--block-gap, 0.5em )';
|
||||
$style = '.' . $class . '{ --wp--style--unstable-gallery-gap: ' . $gap_value . '}';
|
||||
// Ideally styles should be loaded in the head, but blocks may be parsed
|
||||
// after that, so loading in the footer for now.
|
||||
// See https://core.trac.wordpress.org/ticket/53494.
|
||||
add_action(
|
||||
'wp_footer',
|
||||
function () use ( $style ) {
|
||||
echo '<style> ' . $style . '</style>';
|
||||
}
|
||||
);
|
||||
return $content;
|
||||
}
|
||||
/**
|
||||
* Registers the `core/gallery` block on server.
|
||||
*/
|
||||
function register_block_core_gallery() {
|
||||
register_block_type_from_metadata(
|
||||
__DIR__ . '/gallery',
|
||||
array(
|
||||
'render_callback' => 'block_core_gallery_render',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
add_action( 'init', 'register_block_core_gallery' );
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import * as areoi from '../_components/Core.js';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import {
|
||||
RichText,
|
||||
useBlockProps,
|
||||
useInnerBlocksProps,
|
||||
} from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import saveWithoutInnerBlocks from './v1/save';
|
||||
import { isGalleryV2Enabled } from './shared';
|
||||
|
||||
export default function saveWithInnerBlocks( { attributes } ) {
|
||||
if ( ! isGalleryV2Enabled() ) {
|
||||
return saveWithoutInnerBlocks( { attributes } );
|
||||
}
|
||||
|
||||
const { caption, imageCrop } = attributes;
|
||||
|
||||
let layout = attributes['layout'] ? attributes['layout'] : 'grid',
|
||||
container = attributes['container'] ? attributes['container'] : 'container',
|
||||
columns = attributes['columns'] ? attributes['columns'] : '3',
|
||||
size = attributes['card_size'] && attributes['card_size'] != 'Default' ? attributes['card_size'] : '',
|
||||
style = attributes['style'] && attributes['style'] != 'Default' ? attributes['style'] : '';
|
||||
|
||||
const classes = [
|
||||
'block-' + attributes['block_id'],
|
||||
'areoi-media-grid',
|
||||
'areoi-content-grid',
|
||||
'areoi-content-grid-' + layout,
|
||||
size,
|
||||
'areoi-content-grid-' + style,
|
||||
attributes.vertical_align_xs,
|
||||
attributes.vertical_align_sm,
|
||||
attributes.vertical_align_md,
|
||||
attributes.vertical_align_lg,
|
||||
attributes.vertical_align_xl,
|
||||
attributes.vertical_align_xxl,
|
||||
'position-relative',
|
||||
'd-flex'
|
||||
];
|
||||
|
||||
const className = classnames( classes );
|
||||
const blockProps = useBlockProps.save( { className } );
|
||||
const innerBlocksProps = useInnerBlocksProps.save( blockProps );
|
||||
|
||||
var background_row_class = areoi.helper.GetClassNameStr( [
|
||||
attributes['background_horizontal_align'] ? attributes['background_horizontal_align'] : ''
|
||||
]);
|
||||
|
||||
var background_col_class = areoi.helper.GetClassNameStr( [
|
||||
attributes['background_col_xs'] ? attributes['background_col_xs'] : '',
|
||||
attributes['background_col_sm'] ? attributes['background_col_sm'] : '',
|
||||
attributes['background_col_md'] ? attributes['background_col_md'] : '',
|
||||
attributes['background_col_lg'] ? attributes['background_col_lg'] : '',
|
||||
attributes['background_col_xl'] ? attributes['background_col_xl'] : '',
|
||||
attributes['background_col_xxl'] ? attributes['background_col_xxl'] : ''
|
||||
]);
|
||||
var background = '';
|
||||
if ( attributes['background_display'] ) {
|
||||
|
||||
background =
|
||||
<div className="areoi-background">
|
||||
<div className="container-fluid p-0">
|
||||
<div className={'row' + background_row_class }>
|
||||
<div className={ 'col' + background_col_class }>
|
||||
|
||||
{ attributes['background_color'] &&
|
||||
<div className={ areoi.helper.GetClassNameStr( [
|
||||
'areoi-background__color'
|
||||
] ) }
|
||||
style={ { 'background': areoi.helper.GetRGB( attributes['background_color']['rgb'] ) } }>
|
||||
</div>
|
||||
}
|
||||
|
||||
{ attributes['background_image'] &&
|
||||
<div className="areoi-background__image" style={ { 'background-image': 'url(' + attributes['background_image']['url'] + ')' } }></div>
|
||||
}
|
||||
|
||||
{ attributes['background_video'] &&
|
||||
<video autoplay loop playsinline muted>
|
||||
<source src={ attributes['background_video']['url'] } />
|
||||
</video>
|
||||
}
|
||||
|
||||
{ attributes['background_display_overlay'] && attributes['background_overlay'] &&
|
||||
<div className={ areoi.helper.GetClassNameStr( [
|
||||
'areoi-background__overlay'
|
||||
] ) }
|
||||
style={ { 'background': areoi.helper.GetRGB( attributes['background_overlay']['rgb'] ) } }>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var prepend = '';
|
||||
var prepend_row_class = areoi.helper.GetClassNameStr( [
|
||||
'row',
|
||||
attributes['prepend_horizontal_align_xs'] ? attributes['prepend_horizontal_align_xs'] : '',
|
||||
attributes['prepend_horizontal_align_sm'] ? attributes['prepend_horizontal_align_sm'] : '',
|
||||
attributes['prepend_horizontal_align_md'] ? attributes['prepend_horizontal_align_md'] : '',
|
||||
attributes['prepend_horizontal_align_lg'] ? attributes['prepend_horizontal_align_lg'] : '',
|
||||
attributes['prepend_horizontal_align_xl'] ? attributes['prepend_horizontal_align_xl'] : '',
|
||||
attributes['prepend_horizontal_align_xxl'] ? attributes['prepend_horizontal_align_xxl'] : '',
|
||||
]
|
||||
);
|
||||
var prepend_col_class = areoi.helper.GetClassNameStr( [
|
||||
'col',
|
||||
attributes['prepend_col_xs'] ? attributes['prepend_col_xs'] : '',
|
||||
attributes['prepend_col_sm'] ? attributes['prepend_col_sm'] : '',
|
||||
attributes['prepend_col_md'] ? attributes['prepend_col_md'] : '',
|
||||
attributes['prepend_col_lg'] ? attributes['prepend_col_lg'] : '',
|
||||
attributes['prepend_col_xl'] ? attributes['prepend_col_xl'] : '',
|
||||
attributes['prepend_col_xxl'] ? attributes['prepend_col_xxl'] : '',
|
||||
attributes['prepend_text_align_xs'] ? attributes['prepend_text_align_xs'] : '',
|
||||
attributes['prepend_text_align_sm'] ? attributes['prepend_text_align_sm'] : '',
|
||||
attributes['prepend_text_align_md'] ? attributes['prepend_text_align_md'] : '',
|
||||
attributes['prepend_text_align_lg'] ? attributes['prepend_text_align_lg'] : '',
|
||||
attributes['prepend_text_align_xl'] ? attributes['prepend_text_align_xl'] : '',
|
||||
attributes['prepend_text_align_xxl'] ? attributes['prepend_text_align_xxl'] : '',
|
||||
]
|
||||
);
|
||||
|
||||
var heading_color = attributes['prepend_heading_color'] ? attributes['prepend_heading_color'] : '';
|
||||
var intro_color = attributes['prepend_intro_color'] ? attributes['prepend_intro_color'] : '';
|
||||
|
||||
if ( attributes['prepend_display_heading'] || attributes['prepend_display_intro'] ) {
|
||||
|
||||
var prepend_heading = attributes['prepend_display_heading'] && attributes['prepend_heading'] ? '<' + attributes['prepend_heading_level'] + ' class="' + heading_color + '">' + attributes['prepend_heading'] + '</' + attributes['prepend_heading_level'] + '>' : '';
|
||||
var prepend_intro = attributes['prepend_intro'] && attributes['prepend_intro'] ? '<p class="' + intro_color + '">' + attributes['prepend_intro'] + '</p>' : '';
|
||||
|
||||
function createMarkup() {
|
||||
return {__html: prepend_heading + prepend_intro };
|
||||
}
|
||||
|
||||
prepend =
|
||||
<div className={ prepend_row_class }>
|
||||
<div className={ prepend_col_class } dangerouslySetInnerHTML={createMarkup()}></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
// return (
|
||||
// <div { ...innerBlocksProps }>
|
||||
// { background }
|
||||
|
||||
// <div className={ areoi.helper.GetClassNameStr( [ container, 'position-relative' ] ) }>
|
||||
|
||||
// <div className="row h-100">
|
||||
// <div className="col">
|
||||
|
||||
// { prepend }
|
||||
|
||||
// <div className={ areoi.helper.GetClassNameStr( [ 'row', 'areoi-content-grid-columns', 'areoi-content-grid-columns-' + columns ] ) }>
|
||||
// { innerBlocksProps.children }
|
||||
// { ! RichText.isEmpty( caption ) && (
|
||||
// <RichText.Content
|
||||
// tagName="figcaption"
|
||||
// className="blocks-gallery-caption"
|
||||
// value={ caption }
|
||||
// />
|
||||
// ) }
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
return (
|
||||
<areoi.editor.InnerBlocks.Content/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { BlockIcon } from '@wordpress/block-editor';
|
||||
import { gallery as icon } from '@wordpress/icons';
|
||||
|
||||
export const sharedIcon = <BlockIcon icon={ icon } />;
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
import { withPreferredColorScheme } from '@wordpress/compose';
|
||||
import { gallery as icon } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import styles from './styles.scss';
|
||||
|
||||
const IconWithColorScheme = withPreferredColorScheme(
|
||||
( { getStylesFromColorScheme } ) => {
|
||||
const colorSchemeStyles = getStylesFromColorScheme(
|
||||
styles.icon,
|
||||
styles.iconDark
|
||||
);
|
||||
return <Icon icon={ icon } { ...colorSchemeStyles } />;
|
||||
}
|
||||
);
|
||||
|
||||
export const sharedIcon = <IconWithColorScheme />;
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { get, pick } from 'lodash';
|
||||
|
||||
export function defaultColumnsNumber( imageCount ) {
|
||||
return imageCount ? Math.min( 3, imageCount ) : 3;
|
||||
}
|
||||
|
||||
export const pickRelevantMediaFiles = ( image, sizeSlug = 'large' ) => {
|
||||
const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] );
|
||||
imageProps.url =
|
||||
get( image, [ 'sizes', sizeSlug, 'url' ] ) ||
|
||||
get( image, [ 'media_details', 'sizes', sizeSlug, 'source_url' ] ) ||
|
||||
image.url;
|
||||
const fullUrl =
|
||||
get( image, [ 'sizes', 'full', 'url' ] ) ||
|
||||
get( image, [ 'media_details', 'sizes', 'full', 'source_url' ] );
|
||||
if ( fullUrl ) {
|
||||
imageProps.fullUrl = fullUrl;
|
||||
}
|
||||
return imageProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* The new gallery block format is not compatible with the use_BalanceTags option
|
||||
* in WP versions <= 5.8 https://core.trac.wordpress.org/ticket/54130. The
|
||||
* window.wp.galleryBlockV2Enabled flag is set in lib/compat.php. This method
|
||||
* can be removed when minimum supported WP version >=5.9.
|
||||
*/
|
||||
export function isGalleryV2Enabled() {
|
||||
// Only run the Gallery version compat check if the plugin is running, otherwise
|
||||
// assume we are in 5.9 core and enable by default.
|
||||
if ( process.env.IS_GUTENBERG_PLUGIN ) {
|
||||
// We want to fail early here, at least during beta testing phase, to ensure
|
||||
// there aren't instances where undefined values cause false negatives.
|
||||
if (
|
||||
! window.wp ||
|
||||
typeof window.wp.galleryBlockV2Enabled !== 'boolean'
|
||||
) {
|
||||
throw 'window.wp.galleryBlockV2Enabled is not defined';
|
||||
}
|
||||
return window.wp.galleryBlockV2Enabled;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import url( '../content-grid/style.css' );
|
||||
@@ -0,0 +1,171 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Gallery block Columns setting decrements columns 1`] = `
|
||||
"<!-- wp:gallery {\\"columns\\":2,\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-2 is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2001} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2001\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2002} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-3.jpeg\\" alt=\\"\\" class=\\"wp-image-2002\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block Columns setting does not increment due to maximum value 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2001} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2001\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2002} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-3.jpeg\\" alt=\\"\\" class=\\"wp-image-2002\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block cancels uploads 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":null} -->
|
||||
<figure class=\\"wp-block-image\\"><img alt=\\"\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":null} -->
|
||||
<figure class=\\"wp-block-image\\"><img alt=\\"\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block disables crop images setting 1`] = `
|
||||
"<!-- wp:gallery {\\"imageCrop\\":false,\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block finishes pending uploads upon opening the editor 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2001} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2001\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block handles failed uploads 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":1} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"file:///local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-1\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"file:///local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block inserts block 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block overrides "Link to" setting of gallery items 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"media\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":1,\\"linkDestination\\":\\"media\\"} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"file:///local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-1\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2,\\"linkDestination\\":\\"media\\"} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"file:///local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block rearranges gallery items 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2002} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-3.jpeg\\" alt=\\"\\" class=\\"wp-image-2002\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2001} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2001\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block sets caption to gallery 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image --><figcaption class=\\"blocks-gallery-caption\\"><strong>Bold</strong> <em>italic</em> <s>strikethrough</s> gallery caption</figcaption></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block sets caption to gallery items 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/><figcaption><strong>Bold</strong> <em>italic</em> <s>strikethrough</s> image caption</figcaption></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block successfully uploads items 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2001} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2001\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block takes a photo 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block uploads from free photo library 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2001} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2001\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
|
||||
exports[`Gallery block uploads from other apps 1`] = `
|
||||
"<!-- wp:gallery {\\"linkTo\\":\\"none\\"} -->
|
||||
<figure class=\\"wp-block-gallery has-nested-images columns-default is-cropped\\"><!-- wp:image {\\"id\\":2000} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-1.jpeg\\" alt=\\"\\" class=\\"wp-image-2000\\"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {\\"id\\":2001} -->
|
||||
<figure class=\\"wp-block-image\\"><img src=\\"https://test-site.files.wordpress.com/local-image-2.jpeg\\" alt=\\"\\" class=\\"wp-image-2001\\"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->"
|
||||
`;
|
||||
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
act,
|
||||
initializeEditor,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
within,
|
||||
waitForStoreResolvers,
|
||||
} from 'test/helpers';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import {
|
||||
requestMediaPicker,
|
||||
subscribeMediaUpload,
|
||||
} from '@wordpress/react-native-bridge';
|
||||
import {
|
||||
MEDIA_UPLOAD_STATE_UPLOADING,
|
||||
MEDIA_UPLOAD_STATE_SUCCEEDED,
|
||||
MEDIA_UPLOAD_STATE_FAILED,
|
||||
MEDIA_UPLOAD_STATE_RESET,
|
||||
} from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Adds a Gallery block via the block picker.
|
||||
*
|
||||
* @return {import('@testing-library/react-native').RenderAPI} A Testing Library screen.
|
||||
*/
|
||||
export const addGalleryBlock = async () => {
|
||||
const screen = await initializeEditor();
|
||||
const { getByA11yLabel, getByTestId, getByText } = screen;
|
||||
|
||||
fireEvent.press( getByA11yLabel( 'Add block' ) );
|
||||
|
||||
const blockList = getByTestId( 'InserterUI-Blocks' );
|
||||
// onScroll event used to force the FlatList to render all items
|
||||
fireEvent.scroll( blockList, {
|
||||
nativeEvent: {
|
||||
contentOffset: { y: 0, x: 0 },
|
||||
contentSize: { width: 100, height: 100 },
|
||||
layoutMeasurement: { width: 100, height: 100 },
|
||||
},
|
||||
} );
|
||||
|
||||
fireEvent.press( await waitFor( () => getByText( 'Gallery' ) ) );
|
||||
|
||||
return screen;
|
||||
};
|
||||
|
||||
/**
|
||||
* The gallery items are rendered via the FlatList of the inner block list.
|
||||
* In order to render the items of a FlatList, it's required to trigger the
|
||||
* "onLayout" event. Additionally, the call is wrapped over "waitForStoreResolvers"
|
||||
* because the gallery items request the media data associated with the image to
|
||||
* be rendered via the "getMedia" selector.
|
||||
*
|
||||
* @param {import('react-test-renderer').ReactTestInstance} galleryBlock Gallery block instance to trigger layout event.
|
||||
* @param {Object} [options] Configuration options for the event.
|
||||
* @param {number} [options.width] Width value to be passed to the event.
|
||||
*/
|
||||
export const triggerGalleryLayout = async (
|
||||
galleryBlock,
|
||||
{ width = 320 } = {}
|
||||
) =>
|
||||
waitForStoreResolvers( () =>
|
||||
fireEvent(
|
||||
within( galleryBlock ).getByTestId( 'block-list-wrapper' ),
|
||||
'layout',
|
||||
{
|
||||
nativeEvent: {
|
||||
layout: {
|
||||
width,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Initialize the editor with HTML generated of Gallery block.
|
||||
*
|
||||
* @param {Object} [options] Configuration options for the initialization.
|
||||
* @param {string} [options.html] String of block editor HTML to parse and render.
|
||||
* @param {number} [options.numberOfItems] Number of gallery items to generate or already included in the provided block editor HTML.
|
||||
* @param {Object} [options.media] Contains media data to be used in the generation.
|
||||
* @param {number} [options.width] Width to be passed when triggering the "onLayout" event on the Gallery block.
|
||||
* @param {boolean} [options.selected] Specifies if the Gallery block included in the initial HTML should be automatically selected.
|
||||
* @param {boolean} [options.useLocalUrl] Specifies if the items should use the local URL instead of the server URL.
|
||||
*
|
||||
* @return {import('@testing-library/react-native').RenderAPI} The Testing Library screen plus the Gallery block React Test instance.
|
||||
*/
|
||||
export const initializeWithGalleryBlock = async ( {
|
||||
html,
|
||||
numberOfItems = 0,
|
||||
media = [],
|
||||
width = 320,
|
||||
selected = true,
|
||||
useLocalUrl = false,
|
||||
} = {} ) => {
|
||||
const initialHtml =
|
||||
html ||
|
||||
generateGalleryBlock( numberOfItems, media, {
|
||||
useLocalUrl,
|
||||
} );
|
||||
const screen = await initializeEditor( { initialHtml } );
|
||||
const { getByA11yLabel } = screen;
|
||||
|
||||
const galleryBlock = getByA11yLabel( /Gallery Block\. Row 1/ );
|
||||
|
||||
if ( numberOfItems > 0 ) {
|
||||
await triggerGalleryLayout( galleryBlock, { width } );
|
||||
}
|
||||
|
||||
if ( selected ) {
|
||||
fireEvent.press( galleryBlock );
|
||||
}
|
||||
|
||||
return { ...screen, galleryBlock };
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a gallery item within a Gallery block.
|
||||
*
|
||||
* @param {import('react-test-renderer').ReactTestInstance} galleryBlock Gallery block instance.
|
||||
* @param {number} rowPosition Row position within the Gallery block.
|
||||
* @return {import('react-test-renderer').ReactTestInstance} Gallery item.
|
||||
*/
|
||||
export const getGalleryItem = ( galleryBlock, rowPosition ) => {
|
||||
return within( galleryBlock ).getByA11yLabel(
|
||||
new RegExp( `Image Block\\. Row ${ rowPosition }` )
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up the media upload mock functions for testing.
|
||||
*
|
||||
* @typedef {Object} MediaUploadMockFunctions
|
||||
* @property {Function} notifyUploadingState Notify uploading state for a media item.
|
||||
* @property {Function} notifySucceedState Notify succeed state for a media item.
|
||||
* @property {Function} notifyFailedState Notify failed state for a media item.
|
||||
* @property {Function} notifyResetState Notify reset state for a media item.
|
||||
*
|
||||
* @return {MediaUploadMockFunctions} Notify state functions.
|
||||
*/
|
||||
export const setupMediaUpload = () => {
|
||||
const mediaUploadListeners = [];
|
||||
subscribeMediaUpload.mockImplementation( ( callback ) => {
|
||||
mediaUploadListeners.push( callback );
|
||||
return { remove: jest.fn() };
|
||||
} );
|
||||
const notifyMediaUpload = ( payload ) =>
|
||||
mediaUploadListeners.forEach( ( listener ) => listener( payload ) );
|
||||
|
||||
return {
|
||||
notifyUploadingState: async ( mediaItem ) =>
|
||||
act( async () => {
|
||||
notifyMediaUpload( {
|
||||
state: MEDIA_UPLOAD_STATE_UPLOADING,
|
||||
mediaId: mediaItem.localId,
|
||||
progress: 0.25,
|
||||
} );
|
||||
} ),
|
||||
notifySucceedState: async ( mediaItem ) =>
|
||||
act( async () => {
|
||||
notifyMediaUpload( {
|
||||
state: MEDIA_UPLOAD_STATE_SUCCEEDED,
|
||||
mediaId: mediaItem.localId,
|
||||
mediaUrl: mediaItem.serverUrl,
|
||||
mediaServerId: mediaItem.serverId,
|
||||
} );
|
||||
} ),
|
||||
notifyFailedState: async ( mediaItem ) =>
|
||||
act( async () => {
|
||||
notifyMediaUpload( {
|
||||
state: MEDIA_UPLOAD_STATE_FAILED,
|
||||
mediaId: mediaItem.localId,
|
||||
progress: 0.5,
|
||||
} );
|
||||
} ),
|
||||
notifyResetState: async ( mediaItem ) =>
|
||||
act( async () => {
|
||||
notifyMediaUpload( {
|
||||
state: MEDIA_UPLOAD_STATE_RESET,
|
||||
mediaId: mediaItem.localId,
|
||||
progress: 0,
|
||||
} );
|
||||
} ),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Sets up Media Picker mock functions.
|
||||
*
|
||||
* @typedef {Object} MediaPickerMockFunctions
|
||||
* @property {Function} expectMediaPickerCall Checks if the request media picker function has been called with specific arguments.
|
||||
* @property {Function} mediaPickerCallback Callback function to notify the media items picked from the media picker.
|
||||
*
|
||||
* @return {MediaPickerMockFunctions} Media picker mock functions.
|
||||
*/
|
||||
export const setupMediaPicker = () => {
|
||||
let mediaPickerCallback;
|
||||
requestMediaPicker.mockImplementation(
|
||||
( source, filter, multiple, callback ) => {
|
||||
mediaPickerCallback = callback;
|
||||
}
|
||||
);
|
||||
return {
|
||||
expectMediaPickerCall: ( source, filter, multiple ) =>
|
||||
expect( requestMediaPicker ).toHaveBeenCalledWith(
|
||||
source,
|
||||
filter,
|
||||
multiple,
|
||||
mediaPickerCallback
|
||||
),
|
||||
mediaPickerCallback: async ( ...mediaItems ) =>
|
||||
act( async () =>
|
||||
mediaPickerCallback(
|
||||
mediaItems.map( ( { localId, localUrl } ) => ( {
|
||||
type: 'image',
|
||||
url: localUrl,
|
||||
id: localId,
|
||||
} ) )
|
||||
)
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the HTML of a Gallery block.
|
||||
*
|
||||
* @param {number} numberOfItems Number of gallery items to generate.
|
||||
* @param {Object} media Contains media data to be used in the generation.
|
||||
* @param {Object} [options] Configuration options for the generation.
|
||||
* @param {boolean} [options.useLocalUrl] Specifies if the items should use the local URL instead of the server URL.
|
||||
* @return {string} Gallery block HTML.
|
||||
*/
|
||||
export const generateGalleryBlock = (
|
||||
numberOfItems,
|
||||
media,
|
||||
{ useLocalUrl = false } = {}
|
||||
) => {
|
||||
const galleryItems = [ ...Array( numberOfItems ) ]
|
||||
.map( ( _, index ) => {
|
||||
const id = useLocalUrl
|
||||
? media[ index ].localId
|
||||
: media[ index ].serverId;
|
||||
const url = useLocalUrl
|
||||
? media[ index ].localUrl
|
||||
: media[ index ].serverUrl;
|
||||
return `<!-- wp:image {"id":${ id }} -->
|
||||
<figure class="wp-block-image"><img src="${ url }" alt="" class="wp-image-${ id }"/></figure>
|
||||
<!-- /wp:image -->`;
|
||||
} )
|
||||
.join( '\n\n' );
|
||||
|
||||
return `<!-- wp:gallery {"linkTo":"none"} -->
|
||||
<figure class="wp-block-gallery has-nested-images columns-default is-cropped">${ galleryItems }</figure>
|
||||
<!-- /wp:gallery -->`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the text of a caption.
|
||||
*
|
||||
* @param {import('react-test-renderer').ReactTestInstance} element Caption test instance.
|
||||
* @param {string} text Text to be set.
|
||||
*/
|
||||
export const setCaption = ( element, text ) => {
|
||||
fireEvent( element, 'focus' );
|
||||
fireEvent( element, 'onChange', {
|
||||
nativeEvent: {
|
||||
eventCount: 1,
|
||||
target: undefined,
|
||||
text,
|
||||
},
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the block settings of the current selected block.
|
||||
*
|
||||
* @param {import('@testing-library/react-native').RenderAPI} screen The Testing Library screen.
|
||||
*/
|
||||
export const openBlockSettings = async ( screen ) => {
|
||||
const { getByA11yLabel, getByTestId } = screen;
|
||||
fireEvent.press( getByA11yLabel( 'Open Settings' ) );
|
||||
await waitFor(
|
||||
() => getByTestId( 'block-settings-modal' ).props.isVisible
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,646 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
act,
|
||||
getEditorHtml,
|
||||
initializeEditor,
|
||||
fireEvent,
|
||||
within,
|
||||
} from 'test/helpers';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks';
|
||||
import { registerCoreBlocks } from '@wordpress/block-library';
|
||||
import { Platform } from '@wordpress/element';
|
||||
import {
|
||||
getOtherMediaOptions,
|
||||
requestImageFailedRetryDialog,
|
||||
requestImageUploadCancelDialog,
|
||||
} from '@wordpress/react-native-bridge';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
addGalleryBlock,
|
||||
initializeWithGalleryBlock,
|
||||
getGalleryItem,
|
||||
setupMediaUpload,
|
||||
generateGalleryBlock,
|
||||
setCaption,
|
||||
setupMediaPicker,
|
||||
triggerGalleryLayout,
|
||||
openBlockSettings,
|
||||
} from './helpers';
|
||||
|
||||
const media = [
|
||||
{
|
||||
localId: 1,
|
||||
localUrl: 'file:///local-image-1.jpeg',
|
||||
serverId: 2000,
|
||||
serverUrl: 'https://test-site.files.wordpress.com/local-image-1.jpeg',
|
||||
},
|
||||
{
|
||||
localId: 2,
|
||||
localUrl: 'file:///local-image-2.jpeg',
|
||||
serverId: 2001,
|
||||
serverUrl: 'https://test-site.files.wordpress.com/local-image-2.jpeg',
|
||||
},
|
||||
{
|
||||
localId: 3,
|
||||
localUrl: 'file:///local-image-3.jpeg',
|
||||
serverId: 2002,
|
||||
serverUrl: 'https://test-site.files.wordpress.com/local-image-3.jpeg',
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll( () => {
|
||||
// Register all core blocks
|
||||
registerCoreBlocks();
|
||||
} );
|
||||
|
||||
afterAll( () => {
|
||||
// Clean up registered blocks
|
||||
getBlockTypes().forEach( ( block ) => {
|
||||
unregisterBlockType( block.name );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'Gallery block', () => {
|
||||
it( 'inserts block', async () => {
|
||||
const { getByA11yLabel } = await addGalleryBlock();
|
||||
|
||||
expect( getByA11yLabel( /Gallery Block\. Row 1/ ) ).toBeVisible();
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'selects a gallery item', async () => {
|
||||
const { galleryBlock } = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 1,
|
||||
media,
|
||||
selected: false,
|
||||
} );
|
||||
|
||||
const galleryItem = getGalleryItem( galleryBlock, 1 );
|
||||
fireEvent.press( galleryItem );
|
||||
|
||||
expect( galleryItem ).toBeVisible();
|
||||
} );
|
||||
|
||||
it( 'shows appender button when gallery has images', async () => {
|
||||
const { galleryBlock, getByText } = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 1,
|
||||
media,
|
||||
} );
|
||||
|
||||
const appenderButton = within( galleryBlock ).getByTestId(
|
||||
'media-placeholder-appender-icon'
|
||||
);
|
||||
fireEvent.press( appenderButton );
|
||||
|
||||
expect( getByText( 'Choose from device' ) ).toBeVisible();
|
||||
expect( getByText( 'Take a Photo' ) ).toBeVisible();
|
||||
expect( getByText( 'WordPress Media Library' ) ).toBeVisible();
|
||||
} );
|
||||
|
||||
// This case is disabled until the issue (https://github.com/WordPress/gutenberg/issues/38444)
|
||||
// is addressed.
|
||||
it.skip( 'displays media options picker when selecting the block', async () => {
|
||||
// Initialize with an empty gallery
|
||||
const {
|
||||
getByA11yLabel,
|
||||
getByText,
|
||||
getByTestId,
|
||||
} = await initializeEditor( {
|
||||
initialHtml: generateGalleryBlock( 0 ),
|
||||
} );
|
||||
|
||||
// Tap on Gallery block
|
||||
fireEvent.press( getByText( 'ADD MEDIA' ) );
|
||||
|
||||
// Observe that media options picker is displayed
|
||||
expect( getByText( 'Choose images' ) ).toBeVisible();
|
||||
expect( getByText( 'WordPress Media Library' ) ).toBeVisible();
|
||||
|
||||
// Dimiss the picker
|
||||
if ( Platform.isIOS ) {
|
||||
fireEvent.press( getByText( 'Cancel' ) );
|
||||
} else {
|
||||
fireEvent( getByTestId( 'media-options-picker' ), 'backdropPress' );
|
||||
}
|
||||
|
||||
// Observe that the block is selected, this is done by checking if the block settings
|
||||
// button is visible
|
||||
const blockActionsButton = getByA11yLabel( /Open Block Actions Menu/ );
|
||||
expect( blockActionsButton ).toBeVisible();
|
||||
} );
|
||||
|
||||
// Test case related to TC001 - Close/Re-open post with an ongoing image upload
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc001
|
||||
it( 'finishes pending uploads upon opening the editor', async () => {
|
||||
const { notifyUploadingState, notifySucceedState } = setupMediaUpload();
|
||||
|
||||
// Initialize with a gallery that contains two items that are being uploaded
|
||||
const { galleryBlock } = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 2,
|
||||
media,
|
||||
useLocalUrl: true,
|
||||
} );
|
||||
|
||||
// Notify that the media items are uploading
|
||||
await notifyUploadingState( media[ 0 ] );
|
||||
await notifyUploadingState( media[ 1 ] );
|
||||
|
||||
// Check that images are showing a loading state
|
||||
expect(
|
||||
within( getGalleryItem( galleryBlock, 1 ) ).getByTestId( 'spinner' )
|
||||
).toBeVisible();
|
||||
expect(
|
||||
within( getGalleryItem( galleryBlock, 2 ) ).getByTestId( 'spinner' )
|
||||
).toBeVisible();
|
||||
|
||||
// Notify that the media items upload succeeded
|
||||
await notifySucceedState( media[ 0 ] );
|
||||
await notifySucceedState( media[ 1 ] );
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC003 - Add caption to gallery
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc003
|
||||
it( 'sets caption to gallery', async () => {
|
||||
// Initialize with a gallery that contains one item
|
||||
const { getByA11yLabel } = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 1,
|
||||
media,
|
||||
} );
|
||||
|
||||
// Check gallery item caption is not visible
|
||||
const galleryItemCaption = getByA11yLabel( /Image caption. Empty/ );
|
||||
expect( galleryItemCaption ).not.toBeVisible();
|
||||
|
||||
// Set gallery caption
|
||||
const captionField = within(
|
||||
getByA11yLabel( /Gallery caption. Empty/ )
|
||||
).getByPlaceholderText( 'Add caption' );
|
||||
setCaption(
|
||||
captionField,
|
||||
'<strong>Bold</strong> <em>italic</em> <s>strikethrough</s> gallery caption'
|
||||
);
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC004 - Add caption to gallery images
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc004
|
||||
it( 'sets caption to gallery items', async () => {
|
||||
// Initialize with a gallery that contains one item
|
||||
const { galleryBlock } = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 1,
|
||||
media,
|
||||
} );
|
||||
|
||||
// Select gallery item
|
||||
const galleryItem = getGalleryItem( galleryBlock, 1 );
|
||||
fireEvent.press( galleryItem );
|
||||
|
||||
// Set gallery item caption
|
||||
const captionField = within( galleryItem ).getByPlaceholderText(
|
||||
'Add caption'
|
||||
);
|
||||
setCaption(
|
||||
captionField,
|
||||
'<strong>Bold</strong> <em>italic</em> <s>strikethrough</s> image caption'
|
||||
);
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC005 - Choose from device (stay in editor) - Successful upload
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc005
|
||||
it( 'successfully uploads items', async () => {
|
||||
const { notifyUploadingState, notifySucceedState } = setupMediaUpload();
|
||||
const {
|
||||
expectMediaPickerCall,
|
||||
mediaPickerCallback,
|
||||
} = setupMediaPicker();
|
||||
|
||||
// Initialize with an empty gallery
|
||||
const { galleryBlock, getByText } = await initializeWithGalleryBlock();
|
||||
|
||||
// Upload images from device
|
||||
fireEvent.press( getByText( 'ADD MEDIA' ) );
|
||||
fireEvent.press( getByText( 'Choose from device' ) );
|
||||
expectMediaPickerCall( 'DEVICE_MEDIA_LIBRARY', [ 'image' ], true );
|
||||
|
||||
// Return media items picked
|
||||
await mediaPickerCallback( media[ 0 ], media[ 1 ] );
|
||||
|
||||
// Check that gallery items are visible
|
||||
await triggerGalleryLayout( galleryBlock );
|
||||
const galleryItem1 = getGalleryItem( galleryBlock, 1 );
|
||||
const galleryItem2 = getGalleryItem( galleryBlock, 2 );
|
||||
expect( galleryItem1 ).toBeVisible();
|
||||
expect( galleryItem2 ).toBeVisible();
|
||||
|
||||
// Check that images are showing a loading state
|
||||
await notifyUploadingState( media[ 0 ] );
|
||||
await notifyUploadingState( media[ 1 ] );
|
||||
expect( within( galleryItem1 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
expect( within( galleryItem2 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
|
||||
// Notify that the media items upload succeeded
|
||||
await notifySucceedState( media[ 0 ] );
|
||||
await notifySucceedState( media[ 1 ] );
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC006 - Choose from device (stay in editor) - Failed upload
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc006
|
||||
it( 'handles failed uploads', async () => {
|
||||
const { notifyUploadingState, notifyFailedState } = setupMediaUpload();
|
||||
const {
|
||||
expectMediaPickerCall,
|
||||
mediaPickerCallback,
|
||||
} = setupMediaPicker();
|
||||
|
||||
// Initialize with an empty gallery
|
||||
const { galleryBlock, getByText } = await initializeWithGalleryBlock();
|
||||
|
||||
// Upload images from device
|
||||
fireEvent.press( getByText( 'ADD MEDIA' ) );
|
||||
fireEvent.press( getByText( 'Choose from device' ) );
|
||||
expectMediaPickerCall( 'DEVICE_MEDIA_LIBRARY', [ 'image' ], true );
|
||||
|
||||
// Return media items picked
|
||||
await mediaPickerCallback( media[ 0 ], media[ 1 ] );
|
||||
|
||||
// Check that gallery items are visible
|
||||
await triggerGalleryLayout( galleryBlock );
|
||||
const galleryItem1 = getGalleryItem( galleryBlock, 1 );
|
||||
const galleryItem2 = getGalleryItem( galleryBlock, 2 );
|
||||
expect( galleryItem1 ).toBeVisible();
|
||||
expect( galleryItem2 ).toBeVisible();
|
||||
|
||||
// Check that images are showing a loading state
|
||||
await notifyUploadingState( media[ 0 ] );
|
||||
await notifyUploadingState( media[ 1 ] );
|
||||
expect( within( galleryItem1 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
expect( within( galleryItem2 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
|
||||
// Notify that the media items uploads failed
|
||||
await notifyFailedState( media[ 0 ] );
|
||||
await notifyFailedState( media[ 1 ] );
|
||||
|
||||
// Check that failed images provide the option to retry the upload
|
||||
fireEvent.press( galleryItem1 );
|
||||
fireEvent.press(
|
||||
within( galleryItem1 ).getByText( /Failed to insert media/ )
|
||||
);
|
||||
expect( requestImageFailedRetryDialog ).toHaveBeenCalledWith(
|
||||
media[ 0 ].localId
|
||||
);
|
||||
fireEvent.press( galleryItem2 );
|
||||
fireEvent.press(
|
||||
within( galleryItem2 ).getByText( /Failed to insert media/ )
|
||||
);
|
||||
expect( requestImageFailedRetryDialog ).toHaveBeenCalledWith(
|
||||
media[ 1 ].localId
|
||||
);
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC007 - Take a photo
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc007
|
||||
it( 'takes a photo', async () => {
|
||||
const { notifyUploadingState, notifySucceedState } = setupMediaUpload();
|
||||
const {
|
||||
expectMediaPickerCall,
|
||||
mediaPickerCallback,
|
||||
} = setupMediaPicker();
|
||||
|
||||
// Initialize with an empty gallery
|
||||
const { galleryBlock, getByText } = await initializeWithGalleryBlock();
|
||||
|
||||
// Take a photo
|
||||
fireEvent.press( getByText( 'ADD MEDIA' ) );
|
||||
fireEvent.press( getByText( 'Take a Photo' ) );
|
||||
expectMediaPickerCall( 'DEVICE_CAMERA', [ 'image' ], true );
|
||||
|
||||
// Return media item from photo taken
|
||||
await mediaPickerCallback( media[ 0 ] );
|
||||
|
||||
// Check gallery item is visible
|
||||
await triggerGalleryLayout( galleryBlock );
|
||||
const galleryItem = getGalleryItem( galleryBlock, 1 );
|
||||
expect( galleryItem ).toBeVisible();
|
||||
|
||||
// Check image is showing a loading state
|
||||
await notifyUploadingState( media[ 0 ] );
|
||||
expect( within( galleryItem ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
|
||||
// Notify that the media item upload succeeded
|
||||
await notifySucceedState( media[ 0 ] );
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC008 - Choose from the free photo library
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc008
|
||||
it( 'uploads from free photo library', async () => {
|
||||
const freePhotoMedia = [ ...media ].map( ( item, index ) => ( {
|
||||
...item,
|
||||
localUrl: `https://images.pexels.com/photos/110854/pexels-photo-${
|
||||
index + 1
|
||||
}.jpeg`,
|
||||
} ) );
|
||||
const { notifyUploadingState, notifySucceedState } = setupMediaUpload();
|
||||
const {
|
||||
expectMediaPickerCall,
|
||||
mediaPickerCallback,
|
||||
} = setupMediaPicker();
|
||||
|
||||
let otherMediaOptionsCallback;
|
||||
getOtherMediaOptions.mockImplementation( ( filter, callback ) => {
|
||||
otherMediaOptionsCallback = callback;
|
||||
} );
|
||||
|
||||
// Initialize with an empty gallery
|
||||
const { galleryBlock, getByText } = await initializeWithGalleryBlock();
|
||||
|
||||
// Notify other media options
|
||||
act( () =>
|
||||
otherMediaOptionsCallback( [
|
||||
{
|
||||
label: 'Free Photo Library',
|
||||
value: 'stock-photo-library',
|
||||
},
|
||||
] )
|
||||
);
|
||||
|
||||
// Upload images from free photo library
|
||||
fireEvent.press( getByText( 'ADD MEDIA' ) );
|
||||
fireEvent.press( getByText( 'Free Photo Library' ) );
|
||||
expectMediaPickerCall( 'stock-photo-library', [ 'image' ], true );
|
||||
|
||||
// Return media items picked
|
||||
await act( async () =>
|
||||
mediaPickerCallback( freePhotoMedia[ 0 ], freePhotoMedia[ 1 ] )
|
||||
);
|
||||
|
||||
// Check that gallery items are visible
|
||||
await triggerGalleryLayout( galleryBlock );
|
||||
const galleryItem1 = getGalleryItem( galleryBlock, 1 );
|
||||
const galleryItem2 = getGalleryItem( galleryBlock, 2 );
|
||||
expect( galleryItem1 ).toBeVisible();
|
||||
expect( galleryItem2 ).toBeVisible();
|
||||
|
||||
// Check that images are showing a loading state
|
||||
await notifyUploadingState( freePhotoMedia[ 0 ] );
|
||||
await notifyUploadingState( freePhotoMedia[ 1 ] );
|
||||
expect( within( galleryItem1 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
expect( within( galleryItem2 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
|
||||
// Notify that the media items upload succeeded
|
||||
await notifySucceedState( freePhotoMedia[ 0 ] );
|
||||
await notifySucceedState( freePhotoMedia[ 1 ] );
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC009 - Choose from device (stay in editor) - Cancel upload
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc009
|
||||
it( 'cancels uploads', async () => {
|
||||
const { notifyUploadingState, notifyResetState } = setupMediaUpload();
|
||||
const {
|
||||
expectMediaPickerCall,
|
||||
mediaPickerCallback,
|
||||
} = setupMediaPicker();
|
||||
|
||||
// Initialize with an empty gallery
|
||||
const { galleryBlock, getByText } = await initializeWithGalleryBlock();
|
||||
|
||||
// Upload images from device
|
||||
fireEvent.press( getByText( 'ADD MEDIA' ) );
|
||||
fireEvent.press( getByText( 'Choose from device' ) );
|
||||
expectMediaPickerCall( 'DEVICE_MEDIA_LIBRARY', [ 'image' ], true );
|
||||
|
||||
// Return media items picked
|
||||
await mediaPickerCallback( media[ 0 ], media[ 1 ] );
|
||||
|
||||
// Check that gallery items are visible
|
||||
await triggerGalleryLayout( galleryBlock );
|
||||
const galleryItem1 = getGalleryItem( galleryBlock, 1 );
|
||||
const galleryItem2 = getGalleryItem( galleryBlock, 2 );
|
||||
expect( galleryItem1 ).toBeVisible();
|
||||
expect( galleryItem2 ).toBeVisible();
|
||||
|
||||
// Check that images are showing a loading state
|
||||
await notifyUploadingState( media[ 0 ] );
|
||||
await notifyUploadingState( media[ 1 ] );
|
||||
expect( within( galleryItem1 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
expect( within( galleryItem2 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
|
||||
// Cancel uploads
|
||||
fireEvent.press( galleryItem1 );
|
||||
fireEvent.press( within( galleryItem1 ).getByTestId( 'spinner' ) );
|
||||
expect( requestImageUploadCancelDialog ).toHaveBeenCalledWith(
|
||||
media[ 0 ].localId
|
||||
);
|
||||
await notifyResetState( media[ 0 ] );
|
||||
|
||||
fireEvent.press( galleryItem2 );
|
||||
fireEvent.press( within( galleryItem2 ).getByTestId( 'spinner' ) );
|
||||
expect( requestImageUploadCancelDialog ).toHaveBeenCalledWith(
|
||||
media[ 1 ].localId
|
||||
);
|
||||
await notifyResetState( media[ 1 ] );
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC010 - Rearrange images in Gallery
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc010
|
||||
it( 'rearranges gallery items', async () => {
|
||||
// Initialize with a gallery that contains three items
|
||||
const { galleryBlock } = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 3,
|
||||
media,
|
||||
} );
|
||||
|
||||
// Rearrange items (final disposition will be: Image 3 - Image 1 - Image 2)
|
||||
const galleryItem1 = getGalleryItem( galleryBlock, 1 );
|
||||
const galleryItem3 = getGalleryItem( galleryBlock, 3 );
|
||||
|
||||
fireEvent.press( galleryItem3 );
|
||||
await act( () =>
|
||||
fireEvent.press(
|
||||
within( galleryItem3 ).getByA11yLabel(
|
||||
/Move block left from position 3 to position 2/
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
fireEvent.press( galleryItem1 );
|
||||
await act( () =>
|
||||
fireEvent.press(
|
||||
within( galleryItem1 ).getByA11yLabel(
|
||||
/Move block right from position 1 to position 2/
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC011 - Choose from Other Apps (iOS Files App)
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc011
|
||||
it( 'uploads from other apps', async () => {
|
||||
const otherAppsMedia = [ ...media ].map( ( item, index ) => ( {
|
||||
...item,
|
||||
localUrl: `file:///IMG_${ index + 1 }.JPG`,
|
||||
} ) );
|
||||
const { notifyUploadingState, notifySucceedState } = setupMediaUpload();
|
||||
const {
|
||||
expectMediaPickerCall,
|
||||
mediaPickerCallback,
|
||||
} = setupMediaPicker();
|
||||
|
||||
let otherMediaOptionsCallback;
|
||||
getOtherMediaOptions.mockImplementation( ( filter, callback ) => {
|
||||
otherMediaOptionsCallback = callback;
|
||||
} );
|
||||
|
||||
// Initialize with an empty gallery
|
||||
const { galleryBlock, getByText } = await initializeWithGalleryBlock();
|
||||
|
||||
// Notify other media options
|
||||
act( () =>
|
||||
otherMediaOptionsCallback( [
|
||||
{ label: 'Other Apps', value: 'other-files' },
|
||||
] )
|
||||
);
|
||||
|
||||
// Upload images from other apps
|
||||
fireEvent.press( getByText( 'ADD MEDIA' ) );
|
||||
fireEvent.press( getByText( 'Other Apps' ) );
|
||||
expectMediaPickerCall( 'other-files', [ 'image' ], true );
|
||||
|
||||
// Return media items picked
|
||||
await mediaPickerCallback( otherAppsMedia[ 0 ], otherAppsMedia[ 1 ] );
|
||||
|
||||
// Check that gallery items are visible
|
||||
await triggerGalleryLayout( galleryBlock );
|
||||
const galleryItem1 = getGalleryItem( galleryBlock, 1 );
|
||||
const galleryItem2 = getGalleryItem( galleryBlock, 2 );
|
||||
expect( galleryItem1 ).toBeVisible();
|
||||
expect( galleryItem2 ).toBeVisible();
|
||||
|
||||
// Check that images are showing a loading state
|
||||
await notifyUploadingState( otherAppsMedia[ 0 ] );
|
||||
await notifyUploadingState( otherAppsMedia[ 1 ] );
|
||||
expect( within( galleryItem1 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
expect( within( galleryItem2 ).getByTestId( 'spinner' ) ).toBeVisible();
|
||||
|
||||
// Notify that the media items upload succeeded
|
||||
await notifySucceedState( otherAppsMedia[ 0 ] );
|
||||
await notifySucceedState( otherAppsMedia[ 1 ] );
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test case related to TC012 - Settings - Link to
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc012
|
||||
it( 'overrides "Link to" setting of gallery items', async () => {
|
||||
// Initialize with a gallery that contains two items, the latter includes "linkDestination" attribute
|
||||
const screen = await initializeWithGalleryBlock( {
|
||||
html: `<!-- wp:gallery {"linkTo":"none"} -->
|
||||
<figure class="wp-block-gallery has-nested-images columns-default is-cropped"><!-- wp:image {"id":${ media[ 0 ].localId }} -->
|
||||
<figure class="wp-block-image"><img src="${ media[ 0 ].localUrl }" alt="" class="wp-image-${ media[ 0 ].localId }"/></figure>
|
||||
<!-- /wp:image -->
|
||||
|
||||
<!-- wp:image {"id":${ media[ 1 ].localId },"linkDestination":"attachment"} -->
|
||||
<figure class="wp-block-image"><img src="${ media[ 1 ].localUrl }" alt="" class="wp-image-${ media[ 1 ].localId }"/></figure>
|
||||
<!-- /wp:image --></figure>
|
||||
<!-- /wp:gallery -->`,
|
||||
numberOfItems: 2,
|
||||
} );
|
||||
const { getByText } = screen;
|
||||
|
||||
// Set "Link to" setting via Gallery block settings
|
||||
await openBlockSettings( screen );
|
||||
fireEvent.press( getByText( 'Link to' ) );
|
||||
fireEvent.press( getByText( 'Media File' ) );
|
||||
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
// Test cases related to TC013 - Settings - Columns
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc013
|
||||
describe( 'Columns setting', () => {
|
||||
it( 'does not increment due to maximum value', async () => {
|
||||
// Initialize with a gallery that contains three items
|
||||
const screen = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 3,
|
||||
media,
|
||||
} );
|
||||
const { getByA11yLabel } = screen;
|
||||
|
||||
await openBlockSettings( screen );
|
||||
|
||||
// Can't increment due to maximum value
|
||||
// NOTE: Default columns value is 3
|
||||
fireEvent(
|
||||
getByA11yLabel( /Columns\. Value is 3/ ),
|
||||
'accessibilityAction',
|
||||
{
|
||||
nativeEvent: { actionName: 'increment' },
|
||||
}
|
||||
);
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
it( 'decrements columns', async () => {
|
||||
// Initialize with a gallery that contains three items
|
||||
const screen = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 3,
|
||||
media,
|
||||
} );
|
||||
const { getByA11yLabel } = screen;
|
||||
|
||||
await openBlockSettings( screen );
|
||||
|
||||
// Decrement columns
|
||||
fireEvent(
|
||||
getByA11yLabel( /Columns\. Value is 3/ ),
|
||||
'accessibilityAction',
|
||||
{
|
||||
nativeEvent: { actionName: 'decrement' },
|
||||
}
|
||||
);
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
|
||||
// Test case related to TC014 - Settings - Crop images
|
||||
// Reference: https://github.com/wordpress-mobile/test-cases/blob/trunk/test-cases/gutenberg/gallery.md#tc014
|
||||
it( 'disables crop images setting', async () => {
|
||||
// Initialize with a gallery that contains one item
|
||||
const screen = await initializeWithGalleryBlock( {
|
||||
numberOfItems: 1,
|
||||
media,
|
||||
} );
|
||||
const { getByText } = screen;
|
||||
|
||||
await openBlockSettings( screen );
|
||||
|
||||
// Disable crop images setting
|
||||
fireEvent.press( getByText( 'Crop images' ) );
|
||||
expect( getEditorHtml() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import useGetNewImages from '../use-get-new-images';
|
||||
|
||||
const TestComponent = ( { images, imageData } ) => {
|
||||
const newImages = useGetNewImages( images, imageData );
|
||||
return <div title="testResult">{ JSON.stringify( newImages ) }</div>;
|
||||
};
|
||||
|
||||
describe( 'gallery block useGetNewImages hook', () => {
|
||||
it( 'returns null if no images currently in the gallery', () => {
|
||||
render( <TestComponent images={ [] } imageData={ [] } /> );
|
||||
expect( screen.getByTitle( 'testResult' ) ).toHaveTextContent( 'null' );
|
||||
} );
|
||||
|
||||
it( 'should not return images that have been loaded from the saved post content', () => {
|
||||
render(
|
||||
<TestComponent
|
||||
images={ [
|
||||
{ clientId: 'abc123', id: 1, fromSavedContent: true },
|
||||
] }
|
||||
imageData={ [ { id: 1 } ] }
|
||||
/>
|
||||
);
|
||||
expect( screen.getByTitle( 'testResult' ) ).toHaveTextContent( 'null' );
|
||||
} );
|
||||
|
||||
it( 'returns array of new images that have been added since the last render', () => {
|
||||
const { rerender } = render(
|
||||
<TestComponent
|
||||
images={ [ { clientId: 'abc123', id: 1 } ] }
|
||||
imageData={ [ { id: 1 } ] }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTitle( 'testResult' ) ).toHaveTextContent(
|
||||
'[{"clientId":"abc123","id":1}]'
|
||||
);
|
||||
|
||||
rerender(
|
||||
<TestComponent
|
||||
images={ [
|
||||
{ clientId: 'abc123', id: 1 },
|
||||
{ clientId: 'efg456', id: 2 },
|
||||
] }
|
||||
imageData={ [ { id: 1 }, { id: 2 } ] }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTitle( 'testResult' ) ).toHaveTextContent(
|
||||
'[{"clientId":"efg456","id":2}]'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'sees an image as new if it has been deleted and added again', () => {
|
||||
const { rerender } = render(
|
||||
<TestComponent
|
||||
images={ [
|
||||
{ clientId: 'abc123', id: 1 },
|
||||
{ clientId: 'efg456', id: 2 },
|
||||
] }
|
||||
imageData={ [ { id: 1 }, { id: 2 } ] }
|
||||
/>
|
||||
);
|
||||
|
||||
rerender(
|
||||
<TestComponent
|
||||
images={ [ { clientId: 'abc123', id: 1 } ] }
|
||||
imageData={ [ { id: 1 } ] }
|
||||
/>
|
||||
);
|
||||
|
||||
rerender(
|
||||
<TestComponent
|
||||
images={ [
|
||||
{ clientId: 'abc123', id: 1 },
|
||||
{ clientId: 'efg456', id: 2 },
|
||||
] }
|
||||
imageData={ [ { id: 1 }, { id: 2 } ] }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.getByTitle( 'testResult' ) ).toHaveTextContent(
|
||||
'[{"clientId":"efg456","id":2}]'
|
||||
);
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { filter, every, toString } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
import { createBlobURL } from '@wordpress/blob';
|
||||
import { addFilter } from '@wordpress/hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_NONE,
|
||||
LINK_DESTINATION_MEDIA,
|
||||
} from './constants';
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT as DEPRECATED_LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_MEDIA as DEPRECATED_LINK_DESTINATION_MEDIA,
|
||||
} from './v1/constants';
|
||||
import { pickRelevantMediaFiles, isGalleryV2Enabled } from './shared';
|
||||
|
||||
const parseShortcodeIds = ( ids ) => {
|
||||
if ( ! ids ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ids.split( ',' ).map( ( id ) => parseInt( id, 10 ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Third party block plugins don't have an easy way to detect if the
|
||||
* innerBlocks version of the Gallery is running when they run a
|
||||
* 3rdPartyBlock -> GalleryBlock transform so this tranform filter
|
||||
* will handle this. Once the innerBlocks version is the default
|
||||
* in a core release, this could be deprecated and removed after
|
||||
* plugin authors have been given time to update transforms.
|
||||
*
|
||||
* @typedef {Object} Attributes
|
||||
* @typedef {Object} Block
|
||||
* @property {Attributes} attributes The attributes of the block.
|
||||
* @param {Block} block The transformed block.
|
||||
* @return {Block} The transformed block.
|
||||
*/
|
||||
function updateThirdPartyTransformToGallery( block ) {
|
||||
if (
|
||||
isGalleryV2Enabled() &&
|
||||
block.name === 'core/gallery' &&
|
||||
block.attributes?.images.length > 0
|
||||
) {
|
||||
const innerBlocks = block.attributes.images.map(
|
||||
( { url, id, alt } ) => {
|
||||
return createBlock( 'core/image', {
|
||||
url,
|
||||
id: id ? parseInt( id, 10 ) : null,
|
||||
alt,
|
||||
sizeSlug: block.attributes.sizeSlug,
|
||||
linkDestination: block.attributes.linkDestination,
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
||||
delete block.attributes.ids;
|
||||
delete block.attributes.images;
|
||||
block.innerBlocks = innerBlocks;
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
addFilter(
|
||||
'blocks.switchToBlockType.transformedBlock',
|
||||
'core/gallery/update-third-party-transform-to',
|
||||
updateThirdPartyTransformToGallery
|
||||
);
|
||||
|
||||
/**
|
||||
* Third party block plugins don't have an easy way to detect if the
|
||||
* innerBlocks version of the Gallery is running when they run a
|
||||
* GalleryBlock -> 3rdPartyBlock transform so this transform filter
|
||||
* will handle this. Once the innerBlocks version is the default
|
||||
* in a core release, this could be deprecated and removed after
|
||||
* plugin authors have been given time to update transforms.
|
||||
*
|
||||
* @typedef {Object} Attributes
|
||||
* @typedef {Object} Block
|
||||
* @property {Attributes} attributes The attributes of the block.
|
||||
* @param {Block} toBlock The block to transform to.
|
||||
* @param {Block[]} fromBlocks The blocks to transform from.
|
||||
* @return {Block} The transformed block.
|
||||
*/
|
||||
function updateThirdPartyTransformFromGallery( toBlock, fromBlocks ) {
|
||||
const from = Array.isArray( fromBlocks ) ? fromBlocks : [ fromBlocks ];
|
||||
const galleryBlock = from.find(
|
||||
( transformedBlock ) =>
|
||||
transformedBlock.name === 'core/gallery' &&
|
||||
transformedBlock.innerBlocks.length > 0 &&
|
||||
! transformedBlock.attributes.images?.length > 0 &&
|
||||
! toBlock.name.includes( 'core/' )
|
||||
);
|
||||
|
||||
if ( galleryBlock ) {
|
||||
const images = galleryBlock.innerBlocks.map(
|
||||
( { attributes: { url, id, alt } } ) => ( {
|
||||
url,
|
||||
id: id ? parseInt( id, 10 ) : null,
|
||||
alt,
|
||||
} )
|
||||
);
|
||||
const ids = images.map( ( { id } ) => id );
|
||||
galleryBlock.attributes.images = images;
|
||||
galleryBlock.attributes.ids = ids;
|
||||
}
|
||||
|
||||
return toBlock;
|
||||
}
|
||||
addFilter(
|
||||
'blocks.switchToBlockType.transformedBlock',
|
||||
'core/gallery/update-third-party-transform-from',
|
||||
updateThirdPartyTransformFromGallery
|
||||
);
|
||||
|
||||
const transforms = {
|
||||
from: [
|
||||
{
|
||||
type: 'block',
|
||||
isMultiBlock: true,
|
||||
blocks: [ 'core/image' ],
|
||||
transform: ( attributes ) => {
|
||||
// Init the align and size from the first item which may be either the placeholder or an image.
|
||||
let { align, sizeSlug } = attributes[ 0 ];
|
||||
// Loop through all the images and check if they have the same align and size.
|
||||
align = every( attributes, [ 'align', align ] )
|
||||
? align
|
||||
: undefined;
|
||||
sizeSlug = every( attributes, [ 'sizeSlug', sizeSlug ] )
|
||||
? sizeSlug
|
||||
: undefined;
|
||||
|
||||
const validImages = filter( attributes, ( { url } ) => url );
|
||||
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
const innerBlocks = validImages.map( ( image ) => {
|
||||
return createBlock( 'core/image', image );
|
||||
} );
|
||||
|
||||
return createBlock(
|
||||
'core/gallery',
|
||||
{
|
||||
align,
|
||||
sizeSlug,
|
||||
},
|
||||
innerBlocks
|
||||
);
|
||||
}
|
||||
|
||||
return createBlock( 'core/gallery', {
|
||||
images: validImages.map(
|
||||
( { id, url, alt, caption } ) => ( {
|
||||
id: toString( id ),
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
} )
|
||||
),
|
||||
ids: validImages.map( ( { id } ) => parseInt( id, 10 ) ),
|
||||
align,
|
||||
sizeSlug,
|
||||
} );
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'shortcode',
|
||||
tag: 'gallery',
|
||||
|
||||
attributes: {
|
||||
images: {
|
||||
type: 'array',
|
||||
shortcode: ( { named: { ids } } ) => {
|
||||
if ( ! isGalleryV2Enabled() ) {
|
||||
return parseShortcodeIds( ids ).map( ( id ) => ( {
|
||||
id: toString( id ),
|
||||
} ) );
|
||||
}
|
||||
},
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
shortcode: ( { named: { ids } } ) => {
|
||||
if ( ! isGalleryV2Enabled() ) {
|
||||
return parseShortcodeIds( ids );
|
||||
}
|
||||
},
|
||||
},
|
||||
shortCodeTransforms: {
|
||||
type: 'array',
|
||||
shortcode: ( { named: { ids } } ) => {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
return parseShortcodeIds( ids ).map( ( id ) => ( {
|
||||
id: parseInt( id ),
|
||||
} ) );
|
||||
}
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
shortcode: ( { named: { columns = '3' } } ) => {
|
||||
return parseInt( columns, 10 );
|
||||
},
|
||||
},
|
||||
linkTo: {
|
||||
type: 'string',
|
||||
shortcode: ( { named: { link } } ) => {
|
||||
if ( ! isGalleryV2Enabled() ) {
|
||||
switch ( link ) {
|
||||
case 'post':
|
||||
return DEPRECATED_LINK_DESTINATION_ATTACHMENT;
|
||||
case 'file':
|
||||
return DEPRECATED_LINK_DESTINATION_MEDIA;
|
||||
default:
|
||||
return DEPRECATED_LINK_DESTINATION_ATTACHMENT;
|
||||
}
|
||||
}
|
||||
switch ( link ) {
|
||||
case 'post':
|
||||
return LINK_DESTINATION_ATTACHMENT;
|
||||
case 'file':
|
||||
return LINK_DESTINATION_MEDIA;
|
||||
default:
|
||||
return LINK_DESTINATION_NONE;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
isMatch( { named } ) {
|
||||
return undefined !== named.ids;
|
||||
},
|
||||
},
|
||||
{
|
||||
// When created by drag and dropping multiple files on an insertion point. Because multiple
|
||||
// files must not be transformed to a gallery when dropped within a gallery there is another transform
|
||||
// within the image block to handle that case. Therefore this transform has to have priority 1
|
||||
// set so that it overrrides the image block transformation when mulitple images are dropped outside
|
||||
// of a gallery block.
|
||||
type: 'files',
|
||||
priority: 1,
|
||||
isMatch( files ) {
|
||||
return (
|
||||
files.length !== 1 &&
|
||||
every(
|
||||
files,
|
||||
( file ) => file.type.indexOf( 'image/' ) === 0
|
||||
)
|
||||
);
|
||||
},
|
||||
transform( files ) {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
const innerBlocks = files.map( ( file ) =>
|
||||
createBlock( 'core/image', {
|
||||
url: createBlobURL( file ),
|
||||
} )
|
||||
);
|
||||
|
||||
return createBlock( 'core/gallery', {}, innerBlocks );
|
||||
}
|
||||
const block = createBlock( 'core/gallery', {
|
||||
images: files.map( ( file ) =>
|
||||
pickRelevantMediaFiles( {
|
||||
url: createBlobURL( file ),
|
||||
} )
|
||||
),
|
||||
} );
|
||||
return block;
|
||||
},
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
type: 'block',
|
||||
blocks: [ 'core/image' ],
|
||||
transform: ( { align, images, ids, sizeSlug }, innerBlocks ) => {
|
||||
if ( isGalleryV2Enabled() ) {
|
||||
if ( innerBlocks.length > 0 ) {
|
||||
return innerBlocks.map(
|
||||
( {
|
||||
attributes: {
|
||||
id,
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
sizeSlug: imageSizeSlug,
|
||||
linkDestination,
|
||||
href,
|
||||
linkTarget,
|
||||
},
|
||||
} ) =>
|
||||
createBlock( 'core/image', {
|
||||
id,
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
sizeSlug: imageSizeSlug,
|
||||
align,
|
||||
linkDestination,
|
||||
href,
|
||||
linkTarget,
|
||||
} )
|
||||
);
|
||||
}
|
||||
return createBlock( 'core/image', { align } );
|
||||
}
|
||||
if ( images.length > 0 ) {
|
||||
return images.map( ( { url, alt, caption }, index ) =>
|
||||
createBlock( 'core/image', {
|
||||
id: ids[ index ],
|
||||
url,
|
||||
alt,
|
||||
caption,
|
||||
align,
|
||||
sizeSlug,
|
||||
} )
|
||||
);
|
||||
}
|
||||
return createBlock( 'core/image', { 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,54 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { useState } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
|
||||
/**
|
||||
* Retrieves the extended media info for each gallery image from the store. This is used to
|
||||
* determine which image size options are available for the current gallery.
|
||||
*
|
||||
* @param {Array} innerBlockImages An array of the innerBlock images currently in the gallery.
|
||||
*
|
||||
* @return {Array} An array of media info options for each gallery image.
|
||||
*/
|
||||
export default function useGetMedia( innerBlockImages ) {
|
||||
const [ currentImageMedia, setCurrentImageMedia ] = useState( [] );
|
||||
|
||||
const imageMedia = useSelect(
|
||||
( select ) => {
|
||||
if ( ! innerBlockImages?.length ) {
|
||||
return currentImageMedia;
|
||||
}
|
||||
|
||||
const imageIds = innerBlockImages
|
||||
.map( ( imageBlock ) => imageBlock.attributes.id )
|
||||
.filter( ( id ) => id !== undefined );
|
||||
|
||||
if ( imageIds.length === 0 ) {
|
||||
return currentImageMedia;
|
||||
}
|
||||
|
||||
return select( coreStore ).getMediaItems( {
|
||||
include: imageIds.join( ',' ),
|
||||
per_page: -1,
|
||||
} );
|
||||
},
|
||||
[ innerBlockImages ]
|
||||
);
|
||||
|
||||
if (
|
||||
imageMedia?.length !== currentImageMedia?.length ||
|
||||
imageMedia?.some(
|
||||
( newImage ) =>
|
||||
! currentImageMedia.find(
|
||||
( currentImage ) => currentImage.id === newImage.id
|
||||
)
|
||||
)
|
||||
) {
|
||||
setCurrentImageMedia( imageMedia );
|
||||
return imageMedia;
|
||||
}
|
||||
return currentImageMedia;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { useMemo, useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Keeps track of images already in the gallery to allow new innerBlocks to be identified. This
|
||||
* is required so default gallery attributes can be applied without overwriting any custom
|
||||
* attributes applied to existing images.
|
||||
*
|
||||
* @param {Array} images Basic image block data taken from current gallery innerBlock
|
||||
* @param {Array} imageData The related image data for each of the current gallery images.
|
||||
*
|
||||
* @return {Array} An array of any new images that have been added to the gallery.
|
||||
*/
|
||||
export default function useGetNewImages( images, imageData ) {
|
||||
const [ currentImages, setCurrentImages ] = useState( [] );
|
||||
|
||||
return useMemo( () => getNewImages(), [ images, imageData ] );
|
||||
|
||||
function getNewImages() {
|
||||
let imagesUpdated = false;
|
||||
|
||||
// First lets check if any images have been deleted.
|
||||
const newCurrentImages = currentImages.filter( ( currentImg ) =>
|
||||
images.find( ( img ) => {
|
||||
return currentImg.clientId === img.clientId;
|
||||
} )
|
||||
);
|
||||
|
||||
if ( newCurrentImages.length < currentImages.length ) {
|
||||
imagesUpdated = true;
|
||||
}
|
||||
|
||||
// Now lets see if we have any images hydrated from saved content and if so
|
||||
// add them to currentImages state.
|
||||
images.forEach( ( image ) => {
|
||||
if (
|
||||
image.fromSavedContent &&
|
||||
! newCurrentImages.find(
|
||||
( currentImage ) => currentImage.id === image.id
|
||||
)
|
||||
) {
|
||||
imagesUpdated = true;
|
||||
newCurrentImages.push( image );
|
||||
}
|
||||
} );
|
||||
|
||||
// Now check for any new images that have been added to InnerBlocks and for which
|
||||
// we have the imageData we need for setting default block attributes.
|
||||
const newImages = images.filter(
|
||||
( image ) =>
|
||||
! newCurrentImages.find(
|
||||
( currentImage ) =>
|
||||
image.clientId &&
|
||||
currentImage.clientId === image.clientId
|
||||
) &&
|
||||
imageData?.find( ( img ) => img.id === image.id ) &&
|
||||
! image.fromSavedConent
|
||||
);
|
||||
|
||||
if ( imagesUpdated || newImages?.length > 0 ) {
|
||||
setCurrentImages( [ ...newCurrentImages, ...newImages ] );
|
||||
}
|
||||
|
||||
return newImages.length > 0 ? newImages : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { get, some } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Calculates the image sizes that are avaible for the current gallery images in order to
|
||||
* populate the 'Image size' selector.
|
||||
*
|
||||
* @param {Array} images Basic image block data taken from current gallery innerBlock
|
||||
* @param {boolean} isSelected Is the block currently selected in the editor.
|
||||
* @param {Function} getSettings Block editor store selector.
|
||||
*
|
||||
* @return {Array} An array of image size options.
|
||||
*/
|
||||
export default function useImageSizes( images, isSelected, getSettings ) {
|
||||
return useMemo( () => getImageSizing(), [ images, isSelected ] );
|
||||
|
||||
function getImageSizing() {
|
||||
if ( ! images || images.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
const { imageSizes } = getSettings();
|
||||
let resizedImages = {};
|
||||
|
||||
if ( isSelected ) {
|
||||
resizedImages = images.reduce( ( currentResizedImages, img ) => {
|
||||
if ( ! img.id ) {
|
||||
return currentResizedImages;
|
||||
}
|
||||
|
||||
const sizes = imageSizes.reduce( ( currentSizes, size ) => {
|
||||
const defaultUrl = get( img, [
|
||||
'sizes',
|
||||
size.slug,
|
||||
'url',
|
||||
] );
|
||||
const mediaDetailsUrl = get( img, [
|
||||
'media_details',
|
||||
'sizes',
|
||||
size.slug,
|
||||
'source_url',
|
||||
] );
|
||||
return {
|
||||
...currentSizes,
|
||||
[ size.slug ]: defaultUrl || mediaDetailsUrl,
|
||||
};
|
||||
}, {} );
|
||||
return {
|
||||
...currentResizedImages,
|
||||
[ parseInt( img.id, 10 ) ]: sizes,
|
||||
};
|
||||
}, {} );
|
||||
}
|
||||
return imageSizes
|
||||
.filter( ( { slug } ) =>
|
||||
some( resizedImages, ( sizes ) => sizes[ slug ] )
|
||||
)
|
||||
.map( ( { name, slug } ) => ( { value: slug, label: name } ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { every } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
|
||||
/**
|
||||
* Shortcode transforms don't currently have a tranform method and so can't use a selector to
|
||||
* retrieve the data for each image being transformer, so this selector handle this post transformation.
|
||||
*
|
||||
* @param {Array} shortCodeTransforms An array of image data passed from the shortcode transform.
|
||||
*
|
||||
* @return {Array} An array of extended image data objects for each of the shortcode transform images.
|
||||
*/
|
||||
export default function useShortCodeTransform( shortCodeTransforms ) {
|
||||
const newImageData = useSelect(
|
||||
( select ) => {
|
||||
if ( ! shortCodeTransforms || shortCodeTransforms.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
const getMedia = select( coreStore ).getMedia;
|
||||
return shortCodeTransforms.map( ( image ) => {
|
||||
const imageData = getMedia( image.id );
|
||||
if ( imageData ) {
|
||||
return {
|
||||
id: imageData.id,
|
||||
type: 'image',
|
||||
url: imageData.source_url,
|
||||
mime: imageData.mime_type,
|
||||
alt: imageData.alt_text,
|
||||
link: imageData.link,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
} );
|
||||
},
|
||||
[ shortCodeTransforms ]
|
||||
);
|
||||
|
||||
if ( ! newImageData ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( every( newImageData, ( img ) => img && img.url ) ) {
|
||||
return newImageData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_MEDIA,
|
||||
LINK_DESTINATION_NONE,
|
||||
LINK_DESTINATION_MEDIA_WP_CORE,
|
||||
LINK_DESTINATION_ATTACHMENT_WP_CORE,
|
||||
} from './constants';
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT as IMAGE_LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_MEDIA as IMAGE_LINK_DESTINATION_MEDIA,
|
||||
LINK_DESTINATION_NONE as IMAGE_LINK_DESTINATION_NONE,
|
||||
} from '../media-grid-image/constants';
|
||||
|
||||
/**
|
||||
* Determines new href and linkDestination values for an image block from the
|
||||
* supplied Gallery link destination.
|
||||
*
|
||||
* @param {Object} image Gallery image.
|
||||
* @param {string} destination Gallery's selected link destination.
|
||||
* @return {Object} New attributes to assign to image block.
|
||||
*/
|
||||
export function getHrefAndDestination( image, destination ) {
|
||||
// Gutenberg and WordPress use different constants so if image_default_link_type
|
||||
// option is set we need to map from the WP Core values.
|
||||
|
||||
switch ( destination ) {
|
||||
case LINK_DESTINATION_MEDIA_WP_CORE:
|
||||
case LINK_DESTINATION_MEDIA:
|
||||
return {
|
||||
href: image?.source_url || image?.url, // eslint-disable-line camelcase
|
||||
linkDestination: IMAGE_LINK_DESTINATION_MEDIA,
|
||||
};
|
||||
case LINK_DESTINATION_ATTACHMENT_WP_CORE:
|
||||
case LINK_DESTINATION_ATTACHMENT:
|
||||
return {
|
||||
href: image?.link,
|
||||
linkDestination: IMAGE_LINK_DESTINATION_ATTACHMENT,
|
||||
};
|
||||
case LINK_DESTINATION_NONE:
|
||||
return {
|
||||
href: undefined,
|
||||
linkDestination: IMAGE_LINK_DESTINATION_NONE,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const LINK_DESTINATION_NONE = 'none';
|
||||
export const LINK_DESTINATION_MEDIA = 'file';
|
||||
export const LINK_DESTINATION_ATTACHMENT = 'post';
|
||||
@@ -0,0 +1,481 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
get,
|
||||
isEmpty,
|
||||
map,
|
||||
reduce,
|
||||
some,
|
||||
toString,
|
||||
} from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { compose } from '@wordpress/compose';
|
||||
import {
|
||||
PanelBody,
|
||||
SelectControl,
|
||||
ToggleControl,
|
||||
withNotices,
|
||||
RangeControl,
|
||||
} from '@wordpress/components';
|
||||
import {
|
||||
MediaPlaceholder,
|
||||
InspectorControls,
|
||||
useBlockProps,
|
||||
store as blockEditorStore,
|
||||
} from '@wordpress/block-editor';
|
||||
import { Platform, useEffect, useState, useMemo } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { withViewportMatch } from '@wordpress/viewport';
|
||||
import { View } from '@wordpress/primitives';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { sharedIcon } from '../shared-icon';
|
||||
import { pickRelevantMediaFiles } from './shared';
|
||||
import { defaultColumnsNumberV1 } from '../deprecated';
|
||||
import Gallery from './gallery';
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_MEDIA,
|
||||
LINK_DESTINATION_NONE,
|
||||
} from './constants';
|
||||
|
||||
const MAX_COLUMNS = 8;
|
||||
const linkOptions = [
|
||||
{ value: LINK_DESTINATION_ATTACHMENT, label: __( 'Attachment Page' ) },
|
||||
{ value: LINK_DESTINATION_MEDIA, label: __( 'Media File' ) },
|
||||
{ value: LINK_DESTINATION_NONE, label: __( 'None' ) },
|
||||
];
|
||||
const ALLOWED_MEDIA_TYPES = [ 'image' ];
|
||||
|
||||
const PLACEHOLDER_TEXT = Platform.select( {
|
||||
web: __(
|
||||
'Drag images, upload new ones or select files from your library.'
|
||||
),
|
||||
native: __( 'ADD MEDIA' ),
|
||||
} );
|
||||
|
||||
const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.select( {
|
||||
web: {},
|
||||
native: { type: 'stepper' },
|
||||
} );
|
||||
|
||||
function GalleryEdit( props ) {
|
||||
const {
|
||||
attributes,
|
||||
clientId,
|
||||
isSelected,
|
||||
noticeUI,
|
||||
noticeOperations,
|
||||
onFocus,
|
||||
} = props;
|
||||
const {
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
imageCrop,
|
||||
images,
|
||||
linkTo,
|
||||
sizeSlug,
|
||||
} = attributes;
|
||||
const [ selectedImage, setSelectedImage ] = useState();
|
||||
const [ attachmentCaptions, setAttachmentCaptions ] = useState();
|
||||
const { __unstableMarkNextChangeAsNotPersistent } = useDispatch(
|
||||
blockEditorStore
|
||||
);
|
||||
|
||||
const {
|
||||
imageSizes,
|
||||
mediaUpload,
|
||||
getMedia,
|
||||
wasBlockJustInserted,
|
||||
} = useSelect( ( select ) => {
|
||||
const settings = select( blockEditorStore ).getSettings();
|
||||
|
||||
return {
|
||||
imageSizes: settings.imageSizes,
|
||||
mediaUpload: settings.mediaUpload,
|
||||
getMedia: select( coreStore ).getMedia,
|
||||
wasBlockJustInserted: select(
|
||||
blockEditorStore
|
||||
).wasBlockJustInserted( clientId, 'inserter_menu' ),
|
||||
};
|
||||
} );
|
||||
|
||||
const resizedImages = useMemo( () => {
|
||||
if ( isSelected ) {
|
||||
return reduce(
|
||||
attributes.ids,
|
||||
( currentResizedImages, id ) => {
|
||||
if ( ! id ) {
|
||||
return currentResizedImages;
|
||||
}
|
||||
const image = getMedia( id );
|
||||
const sizes = reduce(
|
||||
imageSizes,
|
||||
( currentSizes, size ) => {
|
||||
const defaultUrl = get( image, [
|
||||
'sizes',
|
||||
size.slug,
|
||||
'url',
|
||||
] );
|
||||
const mediaDetailsUrl = get( image, [
|
||||
'media_details',
|
||||
'sizes',
|
||||
size.slug,
|
||||
'source_url',
|
||||
] );
|
||||
return {
|
||||
...currentSizes,
|
||||
[ size.slug ]: defaultUrl || mediaDetailsUrl,
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
return {
|
||||
...currentResizedImages,
|
||||
[ parseInt( id, 10 ) ]: sizes,
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}, [ isSelected, attributes.ids, imageSizes ] );
|
||||
|
||||
function onFocusGalleryCaption() {
|
||||
setSelectedImage();
|
||||
}
|
||||
|
||||
function setAttributes( newAttrs ) {
|
||||
if ( newAttrs.ids ) {
|
||||
throw new Error(
|
||||
'The "ids" attribute should not be changed directly. It is managed automatically when "images" attribute changes'
|
||||
);
|
||||
}
|
||||
|
||||
if ( newAttrs.images ) {
|
||||
newAttrs = {
|
||||
...newAttrs,
|
||||
// Unlike images[ n ].id which is a string, always ensure the
|
||||
// ids array contains numbers as per its attribute type.
|
||||
ids: map( newAttrs.images, ( { id } ) => parseInt( id, 10 ) ),
|
||||
};
|
||||
}
|
||||
|
||||
props.setAttributes( newAttrs );
|
||||
}
|
||||
|
||||
function onSelectImage( index ) {
|
||||
return () => {
|
||||
setSelectedImage( index );
|
||||
};
|
||||
}
|
||||
|
||||
function onDeselectImage() {
|
||||
return () => {
|
||||
setSelectedImage();
|
||||
};
|
||||
}
|
||||
|
||||
function onMove( oldIndex, newIndex ) {
|
||||
const newImages = [ ...images ];
|
||||
newImages.splice( newIndex, 1, images[ oldIndex ] );
|
||||
newImages.splice( oldIndex, 1, images[ newIndex ] );
|
||||
setSelectedImage( newIndex );
|
||||
setAttributes( { images: newImages } );
|
||||
}
|
||||
|
||||
function onMoveForward( oldIndex ) {
|
||||
return () => {
|
||||
if ( oldIndex === images.length - 1 ) {
|
||||
return;
|
||||
}
|
||||
onMove( oldIndex, oldIndex + 1 );
|
||||
};
|
||||
}
|
||||
|
||||
function onMoveBackward( oldIndex ) {
|
||||
return () => {
|
||||
if ( oldIndex === 0 ) {
|
||||
return;
|
||||
}
|
||||
onMove( oldIndex, oldIndex - 1 );
|
||||
};
|
||||
}
|
||||
|
||||
function onRemoveImage( index ) {
|
||||
return () => {
|
||||
const newImages = filter( images, ( img, i ) => index !== i );
|
||||
setSelectedImage();
|
||||
setAttributes( {
|
||||
images: newImages,
|
||||
columns: attributes.columns
|
||||
? Math.min( newImages.length, attributes.columns )
|
||||
: attributes.columns,
|
||||
} );
|
||||
};
|
||||
}
|
||||
|
||||
function selectCaption( newImage ) {
|
||||
// The image id in both the images and attachmentCaptions arrays is a
|
||||
// string, so ensure comparison works correctly by converting the
|
||||
// newImage.id to a string.
|
||||
const newImageId = toString( newImage.id );
|
||||
const currentImage = find( images, { id: newImageId } );
|
||||
const currentImageCaption = currentImage
|
||||
? currentImage.caption
|
||||
: newImage.caption;
|
||||
|
||||
if ( ! attachmentCaptions ) {
|
||||
return currentImageCaption;
|
||||
}
|
||||
|
||||
const attachment = find( attachmentCaptions, {
|
||||
id: newImageId,
|
||||
} );
|
||||
|
||||
// If the attachment caption is updated.
|
||||
if ( attachment && attachment.caption !== newImage.caption ) {
|
||||
return newImage.caption;
|
||||
}
|
||||
|
||||
return currentImageCaption;
|
||||
}
|
||||
|
||||
function onSelectImages( newImages ) {
|
||||
setAttachmentCaptions(
|
||||
newImages.map( ( newImage ) => ( {
|
||||
// Store the attachmentCaption id as a string for consistency
|
||||
// with the type of the id in the images attribute.
|
||||
id: toString( newImage.id ),
|
||||
caption: newImage.caption,
|
||||
} ) )
|
||||
);
|
||||
setAttributes( {
|
||||
images: newImages.map( ( newImage ) => ( {
|
||||
...pickRelevantMediaFiles( newImage, sizeSlug ),
|
||||
caption: selectCaption( newImage, images, attachmentCaptions ),
|
||||
// The id value is stored in a data attribute, so when the
|
||||
// block is parsed it's converted to a string. Converting
|
||||
// to a string here ensures it's type is consistent.
|
||||
id: toString( newImage.id ),
|
||||
} ) ),
|
||||
columns: attributes.columns
|
||||
? Math.min( newImages.length, attributes.columns )
|
||||
: attributes.columns,
|
||||
} );
|
||||
}
|
||||
|
||||
function onUploadError( message ) {
|
||||
noticeOperations.removeAllNotices();
|
||||
noticeOperations.createErrorNotice( message );
|
||||
}
|
||||
|
||||
function setLinkTo( value ) {
|
||||
setAttributes( { linkTo: value } );
|
||||
}
|
||||
|
||||
function setColumnsNumber( value ) {
|
||||
setAttributes( { columns: value } );
|
||||
}
|
||||
|
||||
function toggleImageCrop() {
|
||||
setAttributes( { imageCrop: ! imageCrop } );
|
||||
}
|
||||
|
||||
function getImageCropHelp( checked ) {
|
||||
return checked
|
||||
? __( 'Thumbnails are cropped to align.' )
|
||||
: __( 'Thumbnails are not cropped.' );
|
||||
}
|
||||
|
||||
function setImageAttributes( index, newAttributes ) {
|
||||
if ( ! images[ index ] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAttributes( {
|
||||
images: [
|
||||
...images.slice( 0, index ),
|
||||
{
|
||||
...images[ index ],
|
||||
...newAttributes,
|
||||
},
|
||||
...images.slice( index + 1 ),
|
||||
],
|
||||
} );
|
||||
}
|
||||
|
||||
function getImagesSizeOptions() {
|
||||
return map(
|
||||
filter( imageSizes, ( { slug } ) =>
|
||||
some( resizedImages, ( sizes ) => sizes[ slug ] )
|
||||
),
|
||||
( { name, slug } ) => ( { value: slug, label: name } )
|
||||
);
|
||||
}
|
||||
|
||||
function updateImagesSize( newSizeSlug ) {
|
||||
const updatedImages = map( images, ( image ) => {
|
||||
if ( ! image.id ) {
|
||||
return image;
|
||||
}
|
||||
const url = get( resizedImages, [
|
||||
parseInt( image.id, 10 ),
|
||||
newSizeSlug,
|
||||
] );
|
||||
return {
|
||||
...image,
|
||||
...( url && { url } ),
|
||||
};
|
||||
} );
|
||||
|
||||
setAttributes( { images: updatedImages, sizeSlug: newSizeSlug } );
|
||||
}
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
Platform.OS === 'web' &&
|
||||
images &&
|
||||
images.length > 0 &&
|
||||
every( images, ( { url } ) => isBlobURL( url ) )
|
||||
) {
|
||||
const filesList = map( images, ( { url } ) => getBlobByURL( url ) );
|
||||
forEach( images, ( { url } ) => revokeBlobURL( url ) );
|
||||
mediaUpload( {
|
||||
filesList,
|
||||
onFileChange: onSelectImages,
|
||||
allowedTypes: [ 'image' ],
|
||||
} );
|
||||
}
|
||||
}, [] );
|
||||
|
||||
useEffect( () => {
|
||||
// Deselect images when deselecting the block.
|
||||
if ( ! isSelected ) {
|
||||
setSelectedImage();
|
||||
}
|
||||
}, [ isSelected ] );
|
||||
|
||||
useEffect( () => {
|
||||
// linkTo attribute must be saved so blocks don't break when changing
|
||||
// image_default_link_type in options.php.
|
||||
if ( ! linkTo ) {
|
||||
__unstableMarkNextChangeAsNotPersistent();
|
||||
setAttributes( {
|
||||
linkTo:
|
||||
window?.wp?.media?.view?.settings?.defaultProps?.link ||
|
||||
LINK_DESTINATION_NONE,
|
||||
} );
|
||||
}
|
||||
}, [ linkTo ] );
|
||||
|
||||
const hasImages = !! images.length;
|
||||
const hasImageIds = hasImages && images.some( ( image ) => !! image.id );
|
||||
|
||||
const mediaPlaceholder = (
|
||||
<MediaPlaceholder
|
||||
addToGallery={ hasImageIds }
|
||||
isAppender={ hasImages }
|
||||
disableMediaButtons={ hasImages && ! isSelected }
|
||||
icon={ ! hasImages && sharedIcon }
|
||||
labels={ {
|
||||
title: ! hasImages && __( 'Gallery' ),
|
||||
instructions: ! hasImages && PLACEHOLDER_TEXT,
|
||||
} }
|
||||
onSelect={ onSelectImages }
|
||||
accept="image/*"
|
||||
allowedTypes={ ALLOWED_MEDIA_TYPES }
|
||||
multiple
|
||||
value={ hasImageIds ? images : {} }
|
||||
onError={ onUploadError }
|
||||
notices={ hasImages ? undefined : noticeUI }
|
||||
onFocus={ onFocus }
|
||||
autoOpenMediaUpload={
|
||||
! hasImages && isSelected && wasBlockJustInserted
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const blockProps = useBlockProps();
|
||||
|
||||
if ( ! hasImages ) {
|
||||
return <View { ...blockProps }>{ mediaPlaceholder }</View>;
|
||||
}
|
||||
|
||||
const imageSizeOptions = getImagesSizeOptions();
|
||||
const shouldShowSizeOptions = hasImages && ! isEmpty( imageSizeOptions );
|
||||
|
||||
return (
|
||||
<>
|
||||
<InspectorControls>
|
||||
<PanelBody title={ __( 'Gallery settings' ) }>
|
||||
{ images.length > 1 && (
|
||||
<RangeControl
|
||||
label={ __( 'Columns' ) }
|
||||
value={ columns }
|
||||
onChange={ setColumnsNumber }
|
||||
min={ 1 }
|
||||
max={ Math.min( MAX_COLUMNS, images.length ) }
|
||||
{ ...MOBILE_CONTROL_PROPS_RANGE_CONTROL }
|
||||
required
|
||||
/>
|
||||
) }
|
||||
<ToggleControl
|
||||
label={ __( 'Crop images' ) }
|
||||
checked={ !! imageCrop }
|
||||
onChange={ toggleImageCrop }
|
||||
help={ getImageCropHelp }
|
||||
/>
|
||||
<SelectControl
|
||||
label={ __( 'Link to' ) }
|
||||
value={ linkTo }
|
||||
onChange={ setLinkTo }
|
||||
options={ linkOptions }
|
||||
hideCancelButton={ true }
|
||||
/>
|
||||
{ shouldShowSizeOptions && (
|
||||
<SelectControl
|
||||
label={ __( 'Image size' ) }
|
||||
value={ sizeSlug }
|
||||
options={ imageSizeOptions }
|
||||
onChange={ updateImagesSize }
|
||||
hideCancelButton={ true }
|
||||
/>
|
||||
) }
|
||||
</PanelBody>
|
||||
</InspectorControls>
|
||||
{ noticeUI }
|
||||
<Gallery
|
||||
{ ...props }
|
||||
selectedImage={ selectedImage }
|
||||
mediaPlaceholder={ mediaPlaceholder }
|
||||
onMoveBackward={ onMoveBackward }
|
||||
onMoveForward={ onMoveForward }
|
||||
onRemoveImage={ onRemoveImage }
|
||||
onSelectImage={ onSelectImage }
|
||||
onDeselectImage={ onDeselectImage }
|
||||
onSetImageAttributes={ setImageAttributes }
|
||||
blockProps={ blockProps }
|
||||
// This prop is used by gallery.native.js.
|
||||
onFocusGalleryCaption={ onFocusGalleryCaption }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose( [
|
||||
withNotices,
|
||||
withViewportMatch( { isNarrow: '< small' } ),
|
||||
] )( GalleryEdit );
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { StyleSheet, TouchableOpacity } from 'react-native';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import style from './gallery-image-style.scss';
|
||||
|
||||
export function Button( props ) {
|
||||
const {
|
||||
icon,
|
||||
iconSize = 24,
|
||||
onClick,
|
||||
disabled,
|
||||
'aria-disabled': ariaDisabled,
|
||||
accessibilityLabel = 'button',
|
||||
style: customStyle,
|
||||
} = props;
|
||||
|
||||
const buttonStyle = StyleSheet.compose( style.buttonActive, customStyle );
|
||||
|
||||
const isDisabled = disabled || ariaDisabled;
|
||||
|
||||
const { fill } = isDisabled ? style.buttonDisabled : style.button;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={ buttonStyle }
|
||||
activeOpacity={ 0.7 }
|
||||
accessibilityLabel={ accessibilityLabel }
|
||||
accessibilityRole={ 'button' }
|
||||
onPress={ onClick }
|
||||
disabled={ isDisabled }
|
||||
>
|
||||
<Icon icon={ icon } fill={ fill } size={ iconSize } />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,109 @@
|
||||
$gallery-image-container-height: 150px;
|
||||
$overlay-border-width: 2px;
|
||||
$caption-background-color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
.galleryImageContainer {
|
||||
flex: 1;
|
||||
height: $gallery-image-container-height;
|
||||
overflow: hidden;
|
||||
background-color: $gray-lighten-30;
|
||||
}
|
||||
|
||||
.galleryImageContainerDark {
|
||||
background-color: $gray-90;
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
fill: $gray-0;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.buttonDisabled {
|
||||
fill: $gray-30;
|
||||
}
|
||||
|
||||
.buttonActive {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
border-color: $gray-70;
|
||||
background-color: $gray-70;
|
||||
}
|
||||
|
||||
.moverButtonContainer {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
background-color: $gray-70;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-right-color: $gray-30;
|
||||
border-right-width: 1px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toolbarContainer {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 5px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.captionContainer {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
position: absolute;
|
||||
bottom: $overlay-border-width;
|
||||
left: $overlay-border-width;
|
||||
right: $overlay-border-width;
|
||||
top: 45;
|
||||
}
|
||||
|
||||
@mixin caption-shared {
|
||||
font-size: 12px;
|
||||
background-color: #0000;
|
||||
color: #fff;
|
||||
font-family: $default-regular-font;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption {
|
||||
@include caption-shared;
|
||||
background-color: $caption-background-color;
|
||||
padding-top: $grid-unit;
|
||||
padding-bottom: $grid-unit;
|
||||
}
|
||||
|
||||
.captionPlaceholder {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
// expand caption container to compensate for overlay border
|
||||
.captionExpandedContainer {
|
||||
// constrain height to gallery image height for caption scroll
|
||||
max-height: $gallery-image-container-height;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: $grid-unit;
|
||||
padding-bottom: $overlay-border-width + $grid-unit;
|
||||
padding-left: $overlay-border-width;
|
||||
padding-right: $overlay-border-width;
|
||||
// use caption background color on container when expanded
|
||||
background-color: $caption-background-color;
|
||||
}
|
||||
|
||||
.captionExpanded {
|
||||
@include caption-shared;
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { get, omit } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { Button, Spinner, ButtonGroup } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { BACKSPACE, DELETE } from '@wordpress/keycodes';
|
||||
import { withSelect, withDispatch } from '@wordpress/data';
|
||||
import {
|
||||
RichText,
|
||||
MediaPlaceholder,
|
||||
store as blockEditorStore,
|
||||
} from '@wordpress/block-editor';
|
||||
import { isBlobURL } from '@wordpress/blob';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import {
|
||||
closeSmall,
|
||||
chevronLeft,
|
||||
chevronRight,
|
||||
edit,
|
||||
image as imageIcon,
|
||||
} from '@wordpress/icons';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { pickRelevantMediaFiles } from './shared';
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_MEDIA,
|
||||
} from './constants';
|
||||
|
||||
const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url );
|
||||
|
||||
class GalleryImage extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.onSelectImage = this.onSelectImage.bind( this );
|
||||
this.onRemoveImage = this.onRemoveImage.bind( this );
|
||||
this.bindContainer = this.bindContainer.bind( this );
|
||||
this.onEdit = this.onEdit.bind( this );
|
||||
this.onSelectImageFromLibrary = this.onSelectImageFromLibrary.bind(
|
||||
this
|
||||
);
|
||||
this.onSelectCustomURL = this.onSelectCustomURL.bind( this );
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
};
|
||||
}
|
||||
|
||||
bindContainer( ref ) {
|
||||
this.container = ref;
|
||||
}
|
||||
|
||||
onSelectImage() {
|
||||
if ( ! this.props.isSelected ) {
|
||||
this.props.onSelect();
|
||||
}
|
||||
}
|
||||
|
||||
onRemoveImage( event ) {
|
||||
if (
|
||||
this.container === this.container.ownerDocument.activeElement &&
|
||||
this.props.isSelected &&
|
||||
[ BACKSPACE, DELETE ].indexOf( event.keyCode ) !== -1
|
||||
) {
|
||||
event.preventDefault();
|
||||
this.props.onRemove();
|
||||
}
|
||||
}
|
||||
|
||||
onEdit() {
|
||||
this.setState( {
|
||||
isEditing: true,
|
||||
} );
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
image,
|
||||
url,
|
||||
__unstableMarkNextChangeAsNotPersistent,
|
||||
} = this.props;
|
||||
if ( image && ! url ) {
|
||||
__unstableMarkNextChangeAsNotPersistent();
|
||||
this.props.setAttributes( {
|
||||
url: image.source_url,
|
||||
alt: image.alt_text,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
deselectOnBlur() {
|
||||
this.props.onDeselect();
|
||||
}
|
||||
|
||||
onSelectImageFromLibrary( media ) {
|
||||
const { setAttributes, id, url, alt, caption, sizeSlug } = this.props;
|
||||
if ( ! media || ! media.url ) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mediaAttributes = pickRelevantMediaFiles( media, sizeSlug );
|
||||
|
||||
// If the current image is temporary but an alt text was meanwhile
|
||||
// written by the user, make sure the text is not overwritten.
|
||||
if ( isTemporaryImage( id, url ) ) {
|
||||
if ( alt ) {
|
||||
mediaAttributes = omit( mediaAttributes, [ 'alt' ] );
|
||||
}
|
||||
}
|
||||
|
||||
// If a caption text was meanwhile written by the user,
|
||||
// make sure the text is not overwritten by empty captions.
|
||||
if ( caption && ! get( mediaAttributes, [ 'caption' ] ) ) {
|
||||
mediaAttributes = omit( mediaAttributes, [ 'caption' ] );
|
||||
}
|
||||
|
||||
setAttributes( mediaAttributes );
|
||||
this.setState( {
|
||||
isEditing: false,
|
||||
} );
|
||||
}
|
||||
|
||||
onSelectCustomURL( newURL ) {
|
||||
const { setAttributes, url } = this.props;
|
||||
if ( newURL !== url ) {
|
||||
setAttributes( {
|
||||
url: newURL,
|
||||
id: undefined,
|
||||
} );
|
||||
this.setState( {
|
||||
isEditing: false,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
url,
|
||||
alt,
|
||||
id,
|
||||
linkTo,
|
||||
link,
|
||||
isFirstItem,
|
||||
isLastItem,
|
||||
isSelected,
|
||||
caption,
|
||||
onRemove,
|
||||
onMoveForward,
|
||||
onMoveBackward,
|
||||
setAttributes,
|
||||
'aria-label': ariaLabel,
|
||||
} = this.props;
|
||||
const { isEditing } = this.state;
|
||||
|
||||
let href;
|
||||
|
||||
switch ( linkTo ) {
|
||||
case LINK_DESTINATION_MEDIA:
|
||||
href = url;
|
||||
break;
|
||||
case LINK_DESTINATION_ATTACHMENT:
|
||||
href = link;
|
||||
break;
|
||||
}
|
||||
|
||||
const img = (
|
||||
// Disable reason: Image itself is not meant to be interactive, but should
|
||||
// direct image selection and unfocus caption fields.
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
<>
|
||||
<img
|
||||
src={ url }
|
||||
alt={ alt }
|
||||
data-id={ id }
|
||||
onKeyDown={ this.onRemoveImage }
|
||||
tabIndex="0"
|
||||
aria-label={ ariaLabel }
|
||||
ref={ this.bindContainer }
|
||||
/>
|
||||
{ isBlobURL( url ) && <Spinner /> }
|
||||
</>
|
||||
/* eslint-enable jsx-a11y/no-noninteractive-element-interactions */
|
||||
);
|
||||
|
||||
const className = classnames( {
|
||||
'is-selected': isSelected,
|
||||
'is-transient': isBlobURL( url ),
|
||||
} );
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
|
||||
<figure
|
||||
className={ className }
|
||||
onClick={ this.onSelectImage }
|
||||
onFocus={ this.onSelectImage }
|
||||
>
|
||||
{ ! isEditing && ( href ? <a href={ href }>{ img }</a> : img ) }
|
||||
{ isEditing && (
|
||||
<MediaPlaceholder
|
||||
labels={ { title: __( 'Edit gallery image' ) } }
|
||||
icon={ imageIcon }
|
||||
onSelect={ this.onSelectImageFromLibrary }
|
||||
onSelectURL={ this.onSelectCustomURL }
|
||||
accept="image/*"
|
||||
allowedTypes={ [ 'image' ] }
|
||||
value={ { id, src: url } }
|
||||
/>
|
||||
) }
|
||||
<ButtonGroup className="block-library-gallery-item__inline-menu is-left">
|
||||
<Button
|
||||
icon={ chevronLeft }
|
||||
onClick={ isFirstItem ? undefined : onMoveBackward }
|
||||
label={ __( 'Move image backward' ) }
|
||||
aria-disabled={ isFirstItem }
|
||||
disabled={ ! isSelected }
|
||||
/>
|
||||
<Button
|
||||
icon={ chevronRight }
|
||||
onClick={ isLastItem ? undefined : onMoveForward }
|
||||
label={ __( 'Move image forward' ) }
|
||||
aria-disabled={ isLastItem }
|
||||
disabled={ ! isSelected }
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className="block-library-gallery-item__inline-menu is-right">
|
||||
<Button
|
||||
icon={ edit }
|
||||
onClick={ this.onEdit }
|
||||
label={ __( 'Replace image' ) }
|
||||
disabled={ ! isSelected }
|
||||
/>
|
||||
<Button
|
||||
icon={ closeSmall }
|
||||
onClick={ onRemove }
|
||||
label={ __( 'Remove image' ) }
|
||||
disabled={ ! isSelected }
|
||||
/>
|
||||
</ButtonGroup>
|
||||
{ ! isEditing && ( isSelected || caption ) && (
|
||||
<RichText
|
||||
tagName="figcaption"
|
||||
aria-label={ __( 'Image caption text' ) }
|
||||
placeholder={ isSelected ? __( 'Add caption' ) : null }
|
||||
value={ caption }
|
||||
onChange={ ( newCaption ) =>
|
||||
setAttributes( { caption: newCaption } )
|
||||
}
|
||||
inlineToolbar
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose( [
|
||||
withSelect( ( select, ownProps ) => {
|
||||
const { getMedia } = select( coreStore );
|
||||
const { id } = ownProps;
|
||||
|
||||
return {
|
||||
image: id ? getMedia( parseInt( id, 10 ) ) : null,
|
||||
};
|
||||
} ),
|
||||
withDispatch( ( dispatch ) => {
|
||||
const { __unstableMarkNextChangeAsNotPersistent } = dispatch(
|
||||
blockEditorStore
|
||||
);
|
||||
return {
|
||||
__unstableMarkNextChangeAsNotPersistent,
|
||||
};
|
||||
} ),
|
||||
] )( GalleryImage );
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
ScrollView,
|
||||
TouchableWithoutFeedback,
|
||||
} from 'react-native';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import {
|
||||
requestImageFailedRetryDialog,
|
||||
requestImageUploadCancelDialog,
|
||||
requestImageFullscreenPreview,
|
||||
} from '@wordpress/react-native-bridge';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { Image } from '@wordpress/components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Caption, MediaUploadProgress } from '@wordpress/block-editor';
|
||||
import { getProtocol } from '@wordpress/url';
|
||||
import { withPreferredColorScheme } from '@wordpress/compose';
|
||||
import { arrowLeft, arrowRight, warning } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Button from './gallery-button';
|
||||
import style from './gallery-image-style.scss';
|
||||
|
||||
const { compose } = StyleSheet;
|
||||
|
||||
const separatorStyle = compose( style.separator, {
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
} );
|
||||
const buttonStyle = compose( style.button, { aspectRatio: 1 } );
|
||||
const ICON_SIZE_ARROW = 15;
|
||||
|
||||
class GalleryImage extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.onSelectImage = this.onSelectImage.bind( this );
|
||||
this.onSelectCaption = this.onSelectCaption.bind( this );
|
||||
this.onMediaPressed = this.onMediaPressed.bind( this );
|
||||
this.onCaptionChange = this.onCaptionChange.bind( this );
|
||||
this.onSelectMedia = this.onSelectMedia.bind( this );
|
||||
|
||||
this.updateMediaProgress = this.updateMediaProgress.bind( this );
|
||||
this.finishMediaUploadWithSuccess = this.finishMediaUploadWithSuccess.bind(
|
||||
this
|
||||
);
|
||||
this.finishMediaUploadWithFailure = this.finishMediaUploadWithFailure.bind(
|
||||
this
|
||||
);
|
||||
this.renderContent = this.renderContent.bind( this );
|
||||
|
||||
this.state = {
|
||||
captionSelected: false,
|
||||
isUploadInProgress: false,
|
||||
didUploadFail: false,
|
||||
};
|
||||
}
|
||||
|
||||
onSelectCaption() {
|
||||
if ( ! this.state.captionSelected ) {
|
||||
this.setState( {
|
||||
captionSelected: true,
|
||||
} );
|
||||
}
|
||||
|
||||
if ( ! this.props.isSelected ) {
|
||||
this.props.onSelect();
|
||||
}
|
||||
}
|
||||
|
||||
onMediaPressed() {
|
||||
const { id, url, isSelected } = this.props;
|
||||
const {
|
||||
captionSelected,
|
||||
isUploadInProgress,
|
||||
didUploadFail,
|
||||
} = this.state;
|
||||
|
||||
this.onSelectImage();
|
||||
|
||||
if ( isUploadInProgress ) {
|
||||
requestImageUploadCancelDialog( id );
|
||||
} else if (
|
||||
didUploadFail ||
|
||||
( id && getProtocol( url ) === 'file:' )
|
||||
) {
|
||||
requestImageFailedRetryDialog( id );
|
||||
} else if ( isSelected && ! captionSelected ) {
|
||||
requestImageFullscreenPreview( url );
|
||||
}
|
||||
}
|
||||
|
||||
onSelectImage() {
|
||||
if ( ! this.props.isBlockSelected ) {
|
||||
this.props.onSelectBlock();
|
||||
}
|
||||
|
||||
if ( ! this.props.isSelected ) {
|
||||
this.props.onSelect();
|
||||
}
|
||||
|
||||
if ( this.state.captionSelected ) {
|
||||
this.setState( {
|
||||
captionSelected: false,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
onSelectMedia( media ) {
|
||||
const { setAttributes } = this.props;
|
||||
setAttributes( media );
|
||||
}
|
||||
|
||||
onCaptionChange( caption ) {
|
||||
const { setAttributes } = this.props;
|
||||
setAttributes( { caption } );
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
const { isSelected, image, url } = this.props;
|
||||
if ( image && ! url ) {
|
||||
this.props.setAttributes( {
|
||||
url: image.source_url,
|
||||
alt: image.alt_text,
|
||||
} );
|
||||
}
|
||||
|
||||
// Unselect the caption so when the user selects other image and comeback
|
||||
// the caption is not immediately selected.
|
||||
if (
|
||||
this.state.captionSelected &&
|
||||
! isSelected &&
|
||||
prevProps.isSelected
|
||||
) {
|
||||
this.setState( {
|
||||
captionSelected: false,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
updateMediaProgress() {
|
||||
if ( ! this.state.isUploadInProgress ) {
|
||||
this.setState( { isUploadInProgress: true } );
|
||||
}
|
||||
}
|
||||
|
||||
finishMediaUploadWithSuccess( payload ) {
|
||||
this.setState( {
|
||||
isUploadInProgress: false,
|
||||
didUploadFail: false,
|
||||
} );
|
||||
|
||||
this.props.setAttributes( {
|
||||
id: payload.mediaServerId,
|
||||
url: payload.mediaUrl,
|
||||
} );
|
||||
}
|
||||
|
||||
finishMediaUploadWithFailure() {
|
||||
this.setState( {
|
||||
isUploadInProgress: false,
|
||||
didUploadFail: true,
|
||||
} );
|
||||
}
|
||||
|
||||
renderContent( params ) {
|
||||
const {
|
||||
url,
|
||||
isFirstItem,
|
||||
isLastItem,
|
||||
isSelected,
|
||||
caption,
|
||||
onRemove,
|
||||
onMoveForward,
|
||||
onMoveBackward,
|
||||
'aria-label': ariaLabel,
|
||||
isCropped,
|
||||
getStylesFromColorScheme,
|
||||
isRTL,
|
||||
} = this.props;
|
||||
|
||||
const { isUploadInProgress, captionSelected } = this.state;
|
||||
const { isUploadFailed, retryMessage } = params;
|
||||
const resizeMode = isCropped ? 'cover' : 'contain';
|
||||
|
||||
const captionPlaceholderStyle = getStylesFromColorScheme(
|
||||
style.captionPlaceholder,
|
||||
style.captionPlaceholderDark
|
||||
);
|
||||
|
||||
const shouldShowCaptionEditable = ! isUploadFailed && isSelected;
|
||||
const shouldShowCaptionExpanded =
|
||||
! isUploadFailed && ! isSelected && !! caption;
|
||||
|
||||
const captionContainerStyle = shouldShowCaptionExpanded
|
||||
? style.captionExpandedContainer
|
||||
: style.captionContainer;
|
||||
|
||||
const captionStyle = shouldShowCaptionExpanded
|
||||
? style.captionExpanded
|
||||
: style.caption;
|
||||
|
||||
const mediaPickerOptions = [
|
||||
{
|
||||
destructiveButton: true,
|
||||
id: 'removeImage',
|
||||
label: __( 'Remove' ),
|
||||
onPress: onRemove,
|
||||
separated: true,
|
||||
value: 'removeImage',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
alt={ ariaLabel }
|
||||
height={ style.image.height }
|
||||
isSelected={ isSelected }
|
||||
isUploadFailed={ isUploadFailed }
|
||||
isUploadInProgress={ isUploadInProgress }
|
||||
mediaPickerOptions={ mediaPickerOptions }
|
||||
onSelectMediaUploadOption={ this.onSelectMedia }
|
||||
resizeMode={ resizeMode }
|
||||
url={ url }
|
||||
retryMessage={ retryMessage }
|
||||
retryIcon={ warning }
|
||||
/>
|
||||
|
||||
{ ! isUploadInProgress && isSelected && (
|
||||
<View style={ style.toolbarContainer }>
|
||||
<View style={ style.toolbar }>
|
||||
<View style={ style.moverButtonContainer }>
|
||||
<Button
|
||||
style={ buttonStyle }
|
||||
icon={ isRTL ? arrowRight : arrowLeft }
|
||||
iconSize={ ICON_SIZE_ARROW }
|
||||
onClick={
|
||||
isFirstItem ? undefined : onMoveBackward
|
||||
}
|
||||
accessibilityLabel={ __(
|
||||
'Move Image Backward'
|
||||
) }
|
||||
aria-disabled={ isFirstItem }
|
||||
disabled={ ! isSelected }
|
||||
/>
|
||||
<View style={ separatorStyle } />
|
||||
<Button
|
||||
style={ buttonStyle }
|
||||
icon={ isRTL ? arrowLeft : arrowRight }
|
||||
iconSize={ ICON_SIZE_ARROW }
|
||||
onClick={
|
||||
isLastItem ? undefined : onMoveForward
|
||||
}
|
||||
accessibilityLabel={ __(
|
||||
'Move Image Forward'
|
||||
) }
|
||||
aria-disabled={ isLastItem }
|
||||
disabled={ ! isSelected }
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) }
|
||||
|
||||
{ ! isUploadInProgress &&
|
||||
( shouldShowCaptionEditable ||
|
||||
shouldShowCaptionExpanded ) && (
|
||||
<View style={ captionContainerStyle }>
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
keyboardShouldPersistTaps="handled"
|
||||
bounces={ false }
|
||||
>
|
||||
<Caption
|
||||
inlineToolbar
|
||||
isSelected={ isSelected && captionSelected }
|
||||
onChange={ this.onCaptionChange }
|
||||
onFocus={ this.onSelectCaption }
|
||||
placeholder={
|
||||
isSelected ? __( 'Add caption' ) : null
|
||||
}
|
||||
placeholderTextColor={
|
||||
captionPlaceholderStyle.color
|
||||
}
|
||||
style={ captionStyle }
|
||||
value={ caption }
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
onRemove,
|
||||
getStylesFromColorScheme,
|
||||
isSelected,
|
||||
} = this.props;
|
||||
|
||||
const containerStyle = getStylesFromColorScheme(
|
||||
style.galleryImageContainer,
|
||||
style.galleryImageContainerDark
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={ this.onMediaPressed }
|
||||
accessible={ ! isSelected } // We need only child views to be accessible after the selection.
|
||||
accessibilityLabel={ this.accessibilityLabelImageContainer() } // if we don't set this explicitly it reads system provided accessibilityLabels of all child components and those include pretty technical words which don't make sense
|
||||
accessibilityRole={ 'imagebutton' } // this makes VoiceOver to read a description of image provided by system on iOS and lets user know this is a button which conveys the message of tappablity
|
||||
>
|
||||
<View style={ containerStyle }>
|
||||
<MediaUploadProgress
|
||||
mediaId={ id }
|
||||
onUpdateMediaProgress={ this.updateMediaProgress }
|
||||
onFinishMediaUploadWithSuccess={
|
||||
this.finishMediaUploadWithSuccess
|
||||
}
|
||||
onFinishMediaUploadWithFailure={
|
||||
this.finishMediaUploadWithFailure
|
||||
}
|
||||
onMediaUploadStateReset={ onRemove }
|
||||
renderContent={ this.renderContent }
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
accessibilityLabelImageContainer() {
|
||||
const { caption, 'aria-label': ariaLabel } = this.props;
|
||||
|
||||
return isEmpty( caption )
|
||||
? ariaLabel
|
||||
: ariaLabel +
|
||||
'. ' +
|
||||
sprintf(
|
||||
/* translators: accessibility text. %s: image caption. */
|
||||
__( 'Image caption. %s' ),
|
||||
caption
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withPreferredColorScheme( GalleryImage );
|
||||
@@ -0,0 +1,8 @@
|
||||
.galleryTilesContainerSelected {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
margin-left: $block-edge-to-content;
|
||||
margin-right: $block-edge-to-content;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { RichText } from '@wordpress/block-editor';
|
||||
import { VisuallyHidden } from '@wordpress/components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { createBlock } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import GalleryImage from './gallery-image';
|
||||
import { defaultColumnsNumberV1 } from '../deprecated';
|
||||
|
||||
export const Gallery = ( props ) => {
|
||||
const {
|
||||
attributes,
|
||||
isSelected,
|
||||
setAttributes,
|
||||
selectedImage,
|
||||
mediaPlaceholder,
|
||||
onMoveBackward,
|
||||
onMoveForward,
|
||||
onRemoveImage,
|
||||
onSelectImage,
|
||||
onDeselectImage,
|
||||
onSetImageAttributes,
|
||||
insertBlocksAfter,
|
||||
blockProps,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
align,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
caption,
|
||||
imageCrop,
|
||||
images,
|
||||
} = attributes;
|
||||
|
||||
return (
|
||||
<figure
|
||||
{ ...blockProps }
|
||||
className={ classnames( blockProps.className, {
|
||||
[ `align${ align }` ]: align,
|
||||
[ `columns-${ columns }` ]: columns,
|
||||
'is-cropped': imageCrop,
|
||||
} ) }
|
||||
>
|
||||
<ul className="blocks-gallery-grid">
|
||||
{ images.map( ( img, index ) => {
|
||||
const ariaLabel = sprintf(
|
||||
/* translators: 1: the order number of the image. 2: the total number of images. */
|
||||
__( 'image %1$d of %2$d in gallery' ),
|
||||
index + 1,
|
||||
images.length
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
className="blocks-gallery-item"
|
||||
key={ img.id ? `${ img.id }-${ index }` : img.url }
|
||||
>
|
||||
<GalleryImage
|
||||
url={ img.url }
|
||||
alt={ img.alt }
|
||||
id={ img.id }
|
||||
isFirstItem={ index === 0 }
|
||||
isLastItem={ index + 1 === images.length }
|
||||
isSelected={
|
||||
isSelected && selectedImage === index
|
||||
}
|
||||
onMoveBackward={ onMoveBackward( index ) }
|
||||
onMoveForward={ onMoveForward( index ) }
|
||||
onRemove={ onRemoveImage( index ) }
|
||||
onSelect={ onSelectImage( index ) }
|
||||
onDeselect={ onDeselectImage( index ) }
|
||||
setAttributes={ ( attrs ) =>
|
||||
onSetImageAttributes( index, attrs )
|
||||
}
|
||||
caption={ img.caption }
|
||||
aria-label={ ariaLabel }
|
||||
sizeSlug={ attributes.sizeSlug }
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
{ mediaPlaceholder }
|
||||
<RichTextVisibilityHelper
|
||||
isHidden={ ! isSelected && RichText.isEmpty( caption ) }
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-caption"
|
||||
aria-label={ __( 'Gallery caption text' ) }
|
||||
placeholder={ __( 'Write gallery caption…' ) }
|
||||
value={ caption }
|
||||
onChange={ ( value ) => setAttributes( { caption: value } ) }
|
||||
inlineToolbar
|
||||
__unstableOnSplitAtEnd={ () =>
|
||||
insertBlocksAfter( createBlock( 'core/paragraph' ) )
|
||||
}
|
||||
/>
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
|
||||
function RichTextVisibilityHelper( { isHidden, ...richTextProps } ) {
|
||||
return isHidden ? (
|
||||
<VisuallyHidden as={ RichText } { ...richTextProps } />
|
||||
) : (
|
||||
<RichText { ...richTextProps } />
|
||||
);
|
||||
}
|
||||
|
||||
export default Gallery;
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { View } from 'react-native';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import GalleryImage from './gallery-image';
|
||||
import { defaultColumnsNumberV1 } from '../deprecated';
|
||||
import styles from './gallery-styles.scss';
|
||||
import Tiles from './tiles';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
BlockCaption,
|
||||
store as blockEditorStore,
|
||||
} from '@wordpress/block-editor';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { mediaUploadSync } from '@wordpress/react-native-bridge';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { alignmentHelpers } from '@wordpress/components';
|
||||
|
||||
const TILE_SPACING = 15;
|
||||
|
||||
// we must limit displayed columns since readable content max-width is 580px
|
||||
const MAX_DISPLAYED_COLUMNS = 4;
|
||||
const MAX_DISPLAYED_COLUMNS_NARROW = 2;
|
||||
|
||||
const { isFullWidth } = alignmentHelpers;
|
||||
|
||||
export const Gallery = ( props ) => {
|
||||
const [ isCaptionSelected, setIsCaptionSelected ] = useState( false );
|
||||
useEffect( mediaUploadSync, [] );
|
||||
|
||||
const isRTL = useSelect( ( select ) => {
|
||||
return !! select( blockEditorStore ).getSettings().isRTL;
|
||||
}, [] );
|
||||
|
||||
const {
|
||||
clientId,
|
||||
selectedImage,
|
||||
mediaPlaceholder,
|
||||
onBlur,
|
||||
onMoveBackward,
|
||||
onMoveForward,
|
||||
onRemoveImage,
|
||||
onSelectImage,
|
||||
onSetImageAttributes,
|
||||
onFocusGalleryCaption,
|
||||
attributes,
|
||||
isSelected,
|
||||
isNarrow,
|
||||
onFocus,
|
||||
insertBlocksAfter,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
align,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
imageCrop,
|
||||
images,
|
||||
} = attributes;
|
||||
|
||||
// limit displayed columns when isNarrow is true (i.e. when viewport width is
|
||||
// less than "small", where small = 600)
|
||||
const displayedColumns = isNarrow
|
||||
? Math.min( columns, MAX_DISPLAYED_COLUMNS_NARROW )
|
||||
: Math.min( columns, MAX_DISPLAYED_COLUMNS );
|
||||
|
||||
const selectImage = ( index ) => {
|
||||
return () => {
|
||||
if ( isCaptionSelected ) {
|
||||
setIsCaptionSelected( false );
|
||||
}
|
||||
// We need to fully invoke the curried function here.
|
||||
onSelectImage( index )();
|
||||
};
|
||||
};
|
||||
|
||||
const focusGalleryCaption = () => {
|
||||
if ( ! isCaptionSelected ) {
|
||||
setIsCaptionSelected( true );
|
||||
}
|
||||
onFocusGalleryCaption();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={ { flex: 1 } }>
|
||||
<Tiles
|
||||
columns={ displayedColumns }
|
||||
spacing={ TILE_SPACING }
|
||||
style={
|
||||
isSelected
|
||||
? styles.galleryTilesContainerSelected
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{ images.map( ( img, index ) => {
|
||||
const ariaLabel = sprintf(
|
||||
/* translators: 1: the order number of the image. 2: the total number of images. */
|
||||
__( 'image %1$d of %2$d in gallery' ),
|
||||
index + 1,
|
||||
images.length
|
||||
);
|
||||
|
||||
return (
|
||||
<GalleryImage
|
||||
key={ img.id ? `${ img.id }-${ index }` : img.url }
|
||||
url={ img.url }
|
||||
alt={ img.alt }
|
||||
id={ parseInt( img.id, 10 ) } // make id an integer explicitly
|
||||
isCropped={ imageCrop }
|
||||
isFirstItem={ index === 0 }
|
||||
isLastItem={ index + 1 === images.length }
|
||||
isSelected={ isSelected && selectedImage === index }
|
||||
isBlockSelected={ isSelected }
|
||||
onMoveBackward={ onMoveBackward( index ) }
|
||||
onMoveForward={ onMoveForward( index ) }
|
||||
onRemove={ onRemoveImage( index ) }
|
||||
onSelect={ selectImage( index ) }
|
||||
onSelectBlock={ onFocus }
|
||||
setAttributes={ ( attrs ) =>
|
||||
onSetImageAttributes( index, attrs )
|
||||
}
|
||||
caption={ img.caption }
|
||||
aria-label={ ariaLabel }
|
||||
isRTL={ isRTL }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
</Tiles>
|
||||
<View style={ isFullWidth( align ) && styles.fullWidth }>
|
||||
{ mediaPlaceholder }
|
||||
</View>
|
||||
<BlockCaption
|
||||
clientId={ clientId }
|
||||
isSelected={ isCaptionSelected }
|
||||
accessible={ true }
|
||||
accessibilityLabelCreator={ ( caption ) =>
|
||||
isEmpty( caption )
|
||||
? /* translators: accessibility text. Empty gallery caption. */
|
||||
'Gallery caption. Empty'
|
||||
: sprintf(
|
||||
/* translators: accessibility text. %s: gallery caption. */
|
||||
__( 'Gallery caption. %s' ),
|
||||
caption
|
||||
)
|
||||
}
|
||||
onFocus={ focusGalleryCaption }
|
||||
onBlur={ onBlur } // Always assign onBlur as props.
|
||||
insertBlocksAfter={ insertBlocksAfter }
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Gallery;
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { RichText, useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultColumnsNumberV1 } from '../deprecated';
|
||||
import {
|
||||
LINK_DESTINATION_ATTACHMENT,
|
||||
LINK_DESTINATION_MEDIA,
|
||||
} from './constants';
|
||||
|
||||
export default function saveV1( { attributes } ) {
|
||||
const {
|
||||
images,
|
||||
columns = defaultColumnsNumberV1( attributes ),
|
||||
imageCrop,
|
||||
caption,
|
||||
linkTo,
|
||||
} = attributes;
|
||||
const className = `columns-${ columns } ${ imageCrop ? 'is-cropped' : '' }`;
|
||||
|
||||
return (
|
||||
<figure { ...useBlockProps.save( { className } ) }>
|
||||
<ul className="blocks-gallery-grid">
|
||||
{ images.map( ( image ) => {
|
||||
let href;
|
||||
|
||||
switch ( linkTo ) {
|
||||
case LINK_DESTINATION_MEDIA:
|
||||
href = image.fullUrl || image.url;
|
||||
break;
|
||||
case LINK_DESTINATION_ATTACHMENT:
|
||||
href = image.link;
|
||||
break;
|
||||
}
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={ image.url }
|
||||
alt={ image.alt }
|
||||
data-id={ image.id }
|
||||
data-full-url={ image.fullUrl }
|
||||
data-link={ image.link }
|
||||
className={
|
||||
image.id ? `wp-image-${ image.id }` : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={ image.id || image.url }
|
||||
className="blocks-gallery-item"
|
||||
>
|
||||
<figure>
|
||||
{ href ? <a href={ href }>{ img }</a> : img }
|
||||
{ ! RichText.isEmpty( image.caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-item__caption"
|
||||
value={ image.caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
</li>
|
||||
);
|
||||
} ) }
|
||||
</ul>
|
||||
{ ! RichText.isEmpty( caption ) && (
|
||||
<RichText.Content
|
||||
tagName="figcaption"
|
||||
className="blocks-gallery-caption"
|
||||
value={ caption }
|
||||
/>
|
||||
) }
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { get, pick } from 'lodash';
|
||||
|
||||
export const pickRelevantMediaFiles = ( image, sizeSlug = 'large' ) => {
|
||||
const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] );
|
||||
imageProps.url =
|
||||
get( image, [ 'sizes', sizeSlug, 'url' ] ) ||
|
||||
get( image, [ 'media_details', 'sizes', sizeSlug, 'source_url' ] ) ||
|
||||
image.url;
|
||||
const fullUrl =
|
||||
get( image, [ 'sizes', 'full', 'url' ] ) ||
|
||||
get( image, [ 'media_details', 'sizes', 'full', 'source_url' ] );
|
||||
if ( fullUrl ) {
|
||||
imageProps.fullUrl = fullUrl;
|
||||
}
|
||||
return imageProps;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
.containerStyle {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tileStyle {
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
|
||||
/**
|
||||
* WordPress dependencies
|
||||
*/
|
||||
import { Children } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import styles from './tiles-styles.scss';
|
||||
|
||||
function Tiles( props ) {
|
||||
const { columns, children, spacing = 10, style } = props;
|
||||
|
||||
const { compose } = StyleSheet;
|
||||
|
||||
const tileCount = Children.count( children );
|
||||
const lastTile = tileCount - 1;
|
||||
const lastRow = Math.floor( lastTile / columns );
|
||||
|
||||
const wrappedChildren = Children.map( children, ( child, index ) => {
|
||||
/**
|
||||
* Since we don't have `calc()`, we must calculate our spacings here in
|
||||
* order to preserve even spacing between tiles and equal width for tiles
|
||||
* in a given row.
|
||||
*
|
||||
* In order to ensure equal sizing of tile contents, we distribute the
|
||||
* spacing such that each tile has an equal "share" of the fixed spacing. To
|
||||
* keep the tiles properly aligned within their rows, we calculate the left
|
||||
* and right paddings based on the tile's relative position within the row.
|
||||
*
|
||||
* Note: we use padding instead of margins so that the fixed spacing is
|
||||
* included within the relative spacing (i.e. width percentage), and
|
||||
* wrapping behavior is preserved.
|
||||
*
|
||||
* - The left most tile in a row must have left padding of zero.
|
||||
* - The right most tile in a row must have a right padding of zero.
|
||||
*
|
||||
* The values of these left and right paddings are interpolated for tiles in
|
||||
* between. The right padding is complementary with the left padding of the
|
||||
* next tile (i.e. the right padding of [tile n] + the left padding of
|
||||
* [tile n + 1] will be equal for all tiles except the last one in a given
|
||||
* row).
|
||||
*/
|
||||
|
||||
const row = Math.floor( index / columns );
|
||||
const rowLength =
|
||||
row === lastRow ? ( lastTile % columns ) + 1 : columns;
|
||||
const indexInRow = index % columns;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={ [
|
||||
styles.tileStyle,
|
||||
{
|
||||
width: `${ 100 / rowLength }%`,
|
||||
paddingLeft: spacing * ( indexInRow / rowLength ),
|
||||
paddingRight:
|
||||
spacing * ( 1 - ( indexInRow + 1 ) / rowLength ),
|
||||
paddingTop: row === 0 ? 0 : spacing / 2,
|
||||
paddingBottom: row === lastRow ? 0 : spacing / 2,
|
||||
},
|
||||
] }
|
||||
>
|
||||
{ child }
|
||||
</View>
|
||||
);
|
||||
} );
|
||||
|
||||
const containerStyle = compose( styles.containerStyle, style );
|
||||
|
||||
return <View style={ containerStyle }>{ wrappedChildren }</View>;
|
||||
}
|
||||
|
||||
export default Tiles;
|
||||
Reference in New Issue
Block a user