Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion js/gallery-block.asset.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
'wp-element',
'wp-i18n'
),
'version' => 'f4dd3a6a83f3bee704eb'
'version' => 'b84b3ec7a0124b944242'
);
2 changes: 1 addition & 1 deletion js/gallery-block.js

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion php/media/class-gallery.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ public function block_editor_scripts_styles() {
}

/**
* Enqueue the gallery block editor styles.
* Enqueue the gallery block editor styles and the gallery library for the editor canvas.
*
* Registered on `enqueue_block_assets` so the styles are injected into the
* iframed block editor correctly. As of WordPress 7.1 the post editor is
Expand All @@ -300,6 +300,21 @@ public function block_editor_styles() {
return;
}

// The gallery block preview is rendered inside the editor canvas, which
// is an iframe in the iframed block editor. The Product Gallery library
// must be loaded in that document too, since the widget looks up its
// container in its own document and validates its config against its
// own frame's globals.
if ( $this->maybe_enqueue_scripts() ) {
wp_enqueue_script(
self::GALLERY_LIBRARY_HANDLE,
CLOUDINARY_ENDPOINTS_GALLERY,
array(),
$this->plugin->version,
true
);
}

wp_enqueue_style(
'cloudinary-gallery-block-css',
$this->plugin->dir_url . 'css/gallery-block.css',
Expand Down
125 changes: 110 additions & 15 deletions src/js/gallery-block/edit.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* global cloudinaryGalleryApi CLD_GALLERY_CONFIG CLD_REST_ENDPOINT */
/* global CLD_GALLERY_CONFIG CLD_REST_ENDPOINT */

/**
* External dependencies
Expand All @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { Spinner } from '@wordpress/components';
import '@wordpress/components/build-style/style.css';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
import {
InspectorControls,
MediaPlaceholder,
Expand All @@ -25,7 +25,12 @@ import {
import '../../css/gallery.scss';
import Controls from './controls';
import { ALLOWED_MEDIA_TYPES } from './options';
import { generateId, setupAttributesForRendering, showNotice } from './utils';
import {
generateId,
parseCustomSettings,
setupAttributesForRendering,
showNotice,
} from './utils';

const dot = new Dot( '_' );

Expand All @@ -40,6 +45,38 @@ const galleryWidgetConfig = ( config, container ) => ( {
zoom: false,
} );

/**
* Get the Product Gallery widget factory of a given window.
*
* @param {Window} win The window to look the widget up in.
* @return {((config: Object) => Object)|undefined} The `galleryWidget` factory, if loaded.
*/
const getGalleryWidget = ( win ) => win?.cloudinary?.galleryWidget;

/**
* Get the delivery settings of the connected Cloudinary account.
*
* The front end always applies them over the block attributes when it
* renders a gallery, so the preview does too. Blocks may have no cloud name
* saved (e.g. created outside the editor) or a stale one, and the custom
* domain settings are not block attributes at all.
*
* @return {Object} The cloud name and custom domain settings that are set.
*/
const getDeliveryConfig = () => {
if ( 'undefined' === typeof CLD_GALLERY_CONFIG ) {
return {};
}

const { cloudName, secureDistribution, privateCdn } = CLD_GALLERY_CONFIG;

return Object.fromEntries(
Object.entries( { cloudName, secureDistribution, privateCdn } ).filter(
( [ , value ] ) => undefined !== value
)
);
};

// Prefix for the generated gallery container class. A literal prefix keeps
// the value a valid, unique CSS selector. Prior to the Block API v2 upgrade
// the block's `className` prop was used, but that prop is no longer passed to
Expand All @@ -49,6 +86,7 @@ const CONTAINER_PREFIX = 'cld-gallery-';
const Edit = ( { setAttributes, attributes, isSelected } ) => {
const [ errorMessage, setErrorMessage ] = useState( null );
const [ loading, setLoading ] = useState( false );
const containerRef = useRef( null );

const preparedAttributes = useMemo( () => {
// Do not override block settings with defaults on existing ones.
Expand Down Expand Up @@ -108,30 +146,84 @@ const Edit = ( { setAttributes, attributes, isSelected } ) => {
setErrorMessage( null );
}

if ( attributes.selectedImages.length ) {
let gallery;
const container = containerRef.current;

if ( ! attributes.selectedImages.length || ! container ) {
return;
}

// In the iframed block editor the container lives in the editor
// canvas iframe, so the widget of that frame has to be used: it looks
// the container up in its own document and validates its config with
// checks bound to its own frame.
const win = container.ownerDocument.defaultView || window;

// Recreate the config in the target frame, so that its objects pass
// the widget's validation there.
const toFrame = ( config ) =>
win === window
? config
: win.JSON.parse( JSON.stringify( config ) );

const { customSettings, ...attributesConfig } =
setupAttributesForRendering( attributes );
const config = { ...attributesConfig, ...getDeliveryConfig() };
const custom = parseCustomSettings( customSettings );

let gallery;
let cancelled = false;

const render = () => {
if ( cancelled ) {
return;
}

const galleryWidget = getGalleryWidget( win );

const { customSettings, ...config } =
setupAttributesForRendering( attributes );
// The library failed to load in this frame: don't leave the
// spinner running.
if ( ! galleryWidget ) {
setLoading( false );
return;
}

// Shallow merge, matching how the front end applies Custom
// Settings over the block attributes.
try {
gallery = cloudinary.galleryWidget(
galleryWidgetConfig(
{ ...config, ...customSettings },
attributes.container
gallery = galleryWidget(
toFrame(
galleryWidgetConfig(
{ ...config, ...custom },
attributes.container
)
)
);
} catch {
gallery = cloudinary.galleryWidget(
galleryWidgetConfig( config, attributes.container )
gallery = galleryWidget(
toFrame(
galleryWidgetConfig( config, attributes.container )
)
);
}

gallery.render();
setLoading( false );
};

return () => gallery.destroy();
if ( getGalleryWidget( win ) ) {
render();
} else if ( 'complete' !== win.document.readyState ) {
// The library may still be loading in the editor canvas.
win.addEventListener( 'load', render, { once: true } );
} else {
setLoading( false );
}

return () => {
cancelled = true;
win.removeEventListener( 'load', render );
gallery?.destroy();
};
}, [ errorMessage, attributes, setAttributes ] );

const hasImages = !! attributes.selectedImages.length;
Expand All @@ -157,7 +249,10 @@ const Edit = ( { setAttributes, attributes, isSelected } ) => {
return (
<div { ...blockProps }>
<>
<div className={ attributes.container }></div>
<div
ref={ containerRef }
className={ attributes.container }
></div>
<div className="wp-block-cloudinary-gallery">
<MediaPlaceholder
labels={ {
Expand Down
28 changes: 28 additions & 0 deletions src/js/gallery-block/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,31 @@ export const setupAttributesForRendering = ( attributes ) => {

return config;
};

/**
* Parse the block's Custom Settings into an object.
*
* The `customSettings` attribute is stored as JSON text. It is decoded the
* same way the front end does before being merged over the gallery config.
* Invalid JSON, or JSON that is not an object, is ignored.
*
* @param {string|Object} customSettings The Custom Settings attribute value.
* @return {Object} The parsed settings, or an empty object.
*/
export const parseCustomSettings = ( customSettings ) => {
let parsed = customSettings;

if ( 'string' === typeof customSettings ) {
try {
parsed = JSON.parse( customSettings );
} catch {
return {};
}
}

if ( ! parsed || 'object' !== typeof parsed || Array.isArray( parsed ) ) {
return {};
}

return parsed;
};
58 changes: 57 additions & 1 deletion tests/e2e/cloudinary-gallery-delivery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const { test, expect } = require( './fixtures' );
const { ensureCloudinaryConnected } = require( './utils/connection' );
const { expectCloudinaryUrl, srcsetUrls } = require( './utils/delivery' );
const {
GALLERY_CONTAINER,
WIDGET_TIMEOUT,
collectImageUrls,
galleryBlock,
openGallery: openGalleryAt,
Expand All @@ -28,7 +30,7 @@ let cloudName;
* State shared by the tests in this file. The layout test edits the post
* created in beforeAll, so the tests run in order (see `mode: 'serial'`).
*
* @type {{ postId: number, postLink: string, attachmentIds: number[], publicIds: string[] }|null}
* @type {{ postId: number, postLink: string, attachmentIds: number[], publicIds: string[], selectedImages: Object[] }|null}
*/
let created = null;

Expand Down Expand Up @@ -97,6 +99,7 @@ test.describe( 'Cloudinary Gallery block delivery', { tag: '@serial' }, () => {
postLink: post.link,
attachmentIds,
publicIds: selectedImages.map( ( image ) => image.publicId ),
selectedImages,
};
} );

Expand Down Expand Up @@ -260,4 +263,57 @@ test.describe( 'Cloudinary Gallery block delivery', { tag: '@serial' }, () => {
}
}
} );

test( 'renders the preview in the editor canvas with Custom Settings applied', async ( {
admin,
editor,
requestUtils,
} ) => {
// The block markup has no `cloudName`, like a block created outside
// the editor: the preview has to use the connected account's.
await requestUtils.rest( {
method: 'POST',
path: `/wp/v2/posts/${ created.postId }`,
data: {
content: galleryBlock( created.selectedImages, {
mode: 'expanded',
columns: INITIAL_COLUMNS,
customSettings: {
transformation: { effect: 'grayscale' },
},
} ),
},
} );

await admin.editPost( created.postId );

// Relies on the iframed editor (WordPress 7.1+, or blocks on API v3):
// the editor canvas is an iframe, and the widget has to render inside
// it, not in the outer admin document.
const gallery = editor.canvas.locator(
`.${ GALLERY_CONTAINER } [data-test="gallery-wrap"]`
);
await expect(
gallery,
'the Gallery Widget should render the editor preview'
).toBeVisible( { timeout: WIDGET_TIMEOUT } );

await expect(
gallery.locator(
'[data-test="gallery-viewer-wrap"] [data-test="image-wrap"][data-ready="true"]'
)
).toHaveCount( IMAGE_COUNT, { timeout: WIDGET_TIMEOUT } );

const attrs = await collectImageUrls(
gallery.locator( '[data-test="gallery-viewer-wrap"] img' )
);
expect( attrs.length ).toBeGreaterThan( 0 );
for ( const { src } of attrs ) {
expectCloudinaryUrl( src, cloudName );
expect(
src,
'the preview should apply the Custom Settings transformation'
).toContain( 'e_grayscale' );
}
} );
} );
17 changes: 12 additions & 5 deletions tests/e2e/utils/gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,25 @@ const WIDGET_TIMEOUT = 30_000;
* match the block's `save()` output exactly, or the editor flags the block
* as invalid when a test opens the post.
*
* @param {Object[]} selectedImages Response of the plugin's `image_data` endpoint.
* @param {Object} layout Layout attributes.
* @param {string} layout.mode `expanded` or `classic`.
* @param {number} layout.columns Column count (expanded mode only).
* @param {Object[]} selectedImages Response of the plugin's `image_data` endpoint.
* @param {Object} layout Layout attributes.
* @param {string} layout.mode `expanded` or `classic`.
* @param {number} layout.columns Column count (expanded mode only).
* @param {Object} [layout.customSettings] Custom Settings, stored as JSON text like the block does.
* @return {string} Block markup.
*/
function galleryBlock( selectedImages, { mode, columns } ) {
function galleryBlock( selectedImages, { mode, columns, customSettings } ) {
const attributes = {
container: GALLERY_CONTAINER,
selectedImages,
displayProps_mode: mode,
displayProps_columns: columns,
};

if ( customSettings ) {
attributes.customSettings = JSON.stringify( customSettings );
}

return (
`<!-- wp:cloudinary/gallery ${ JSON.stringify( attributes ) } -->\n` +
`<div class="${ GALLERY_CONTAINER }"></div>\n` +
Expand Down Expand Up @@ -90,6 +95,8 @@ async function collectImageUrls( images ) {
}

module.exports = {
GALLERY_CONTAINER,
WIDGET_TIMEOUT,
collectImageUrls,
galleryBlock,
openGallery,
Expand Down
Loading