Skip to content

Commit

Permalink
Tiled galleries: Overhaul (#29559)
Browse files Browse the repository at this point in the history
This is a major overhaul of tiled galleries.

It provides working Mosaic, Square, and Circle layouts.

Column layout is disabled.

Many fixes and improvements are included. Some are:

- Improve alignment handling
- Improve block style handling
- Consistent edit/save (prevent block parse invalidations)
- Consistent layout structure across styles
- Replace rough translation of PHP mosaic calculations with simplified
  JS implementation
- Much more…
  • Loading branch information
sirreal authored Dec 27, 2018
1 parent f182283 commit 38897e7
Show file tree
Hide file tree
Showing 57 changed files with 1,685 additions and 2,507 deletions.
15 changes: 8 additions & 7 deletions client/gutenberg/extensions/tiled-gallery/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,24 @@ export const DEFAULT_GALLERY_WIDTH = 580;
export const DEFAULT_LAYOUT = 'rectangular';
export const LAYOUT_STYLES = [
{
label: _x( 'Tiled mosaic', 'Tiled gallery layout' ),
name: 'rectangular',
isDefault: true,
label: _x( 'Tiled mosaic', 'Tiled gallery layout' ),
name: DEFAULT_LAYOUT,
},
{
label: _x( 'Tiled columns', 'Tiled gallery layout' ),
name: 'columns',
label: _x( 'Circles', 'Tiled gallery layout' ),
name: 'circle',
},
{
label: _x( 'Square tiles', 'Tiled gallery layout' ),
name: 'square',
},
/* Unimplemented
{
label: _x( 'Circles', 'Tiled gallery layout' ),
name: 'circle',
label: _x( 'Tiled columns', 'Tiled gallery layout' ),
name: 'columns',
},
*/
];
export const LAYOUTS = [ 'rectangular', 'columns', 'square', 'circle' ];
export const MAX_COLUMNS = 20;
export const TILE_MARGIN = 2;
245 changes: 111 additions & 134 deletions client/gutenberg/extensions/tiled-gallery/edit.jsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,56 @@
/** @format */

/**
* External Dependencies
*/
import { filter, pick } from 'lodash';
import { Component, Fragment } from '@wordpress/element';
import { filter, get, pick } from 'lodash';
import {
BlockControls,
InspectorControls,
MediaPlaceholder,
MediaUpload,
mediaUpload,
} from '@wordpress/editor';
import {
DropZone,
FormFileUpload,
IconButton,
PanelBody,
RangeControl,
SelectControl,
ToggleControl,
Toolbar,
withNotices,
} from '@wordpress/components';
import {
BlockControls,
InspectorControls,
MediaPlaceholder,
mediaUpload,
MediaUpload,
} from '@wordpress/editor';
import { create } from '@wordpress/rich-text';

/**
* Internal dependencies
*/
import Layout from './layout';
import { __ } from 'gutenberg/extensions/presets/jetpack/utils/i18n';
import { ALLOWED_MEDIA_TYPES, MAX_COLUMNS, DEFAULT_COLUMNS } from './constants';
import GalleryGrid from './gallery-grid';
import GalleryImage from './gallery-image';
import { getActiveStyleName } from './layouts';
import { ALLOWED_MEDIA_TYPES, LAYOUT_STYLES, MAX_COLUMNS } from './constants';
import { getActiveStyleName } from 'gutenberg/extensions/utils';

const linkOptions = [
{ value: 'attachment', label: __( 'Attachment Page' ) },
{ value: 'media', label: __( 'Media File' ) },
{ value: 'none', label: __( 'None' ) },
];

// @TODO keep here or move to ./layout ?
function layoutSupportsColumns( layout ) {
return [ 'circle', 'square' ].includes( layout );
}

export function defaultColumnsNumber( attributes ) {
return Math.min( DEFAULT_COLUMNS, attributes.images.length );
return Math.min( 3, attributes.images.length );
}

const pickRelevantMediaFiles = image => {
let { caption } = image;

if ( typeof caption !== 'object' ) {
caption = create( { html: caption } );
}

return Object.assign( pick( image, [ [ 'alt' ], [ 'id' ], [ 'link' ], [ 'url' ] ] ), caption );
export const pickRelevantMediaFiles = image => {
const imageProps = pick( image, [ [ 'alt' ], [ 'id' ], [ 'link' ], [ 'caption' ] ] );
imageProps.url =
get( image, [ 'sizes', 'large', 'url' ] ) ||
get( image, [ 'media_details', 'sizes', 'large', 'source_url' ] ) ||
image.url;
return imageProps;
};

class TiledGalleryEdit extends Component {
Expand All @@ -61,101 +66,95 @@ class TiledGalleryEdit extends Component {
return null;
}

handleAddFiles = files => {
setAttributes( attributes ) {
if ( attributes.ids ) {
throw new Error(
'The "ids" attribute should not be changed directly. It is managed automatically when "images" attribute changes'
);
}

if ( attributes.images ) {
attributes = {
...attributes,
ids: attributes.images.map( ( { id } ) => parseInt( id, 10 ) ),
};
}

this.props.setAttributes( attributes );
}

addFiles = files => {
const currentImages = this.props.attributes.images || [];
const { noticeOperations, setAttributes } = this.props;
const { noticeOperations } = this.props;
mediaUpload( {
allowedTypes: ALLOWED_MEDIA_TYPES,
filesList: files,
onFileChange: images => {
const imagesNormalized = images.map( image => pickRelevantMediaFiles( image ) );
setAttributes( {
images: currentImages.concat( imagesNormalized ),
} );
this.setAttributes( { images: currentImages.concat( imagesNormalized ) } );
},
onError: noticeOperations.createErrorNotice,
} );
};

handleColumnCountChange = columns => this.props.setAttributes( { columns } );

handleCropImageToggle = () =>
this.props.setAttributes( { imageCrop: ! this.props.attributes.imageCrop } );

handleFormFileUpload = event => this.handleAddFiles( event.target.files );

handleLinkToChange = linkTo => this.props.setAttributes( { linkTo } );

handleRemveImageByIndex = index => () => {
onRemoveImage = index => () => {
const images = filter( this.props.attributes.images, ( img, i ) => index !== i );
const { columns } = this.props.attributes;
this.setState( { selectedImage: null } );
this.props.setAttributes( {
this.setState( {
selectedImage: null,
} );
this.setAttributes( {
images,
columns: columns ? Math.min( images.length, columns ) : columns,
} );
};

handleSelectImageByIndex = index => () => {
onSelectImage = index => () => {
if ( this.state.selectedImage !== index ) {
this.setState( {
selectedImage: index,
} );
}
};

handleSelectImages = images =>
this.props.setAttributes( {
images: images.map( image => pickRelevantMediaFiles( image ) ),
} );
onSelectImages = images =>
this.setAttributes( { images: images.map( image => pickRelevantMediaFiles( image ) ) } );

handleSetImageAttributesByIndex = index => newAttributes => {
const { attributes, setAttributes } = this.props;
const { images = [] } = attributes;
setColumnsNumber = value => this.setAttributes( { columns: value } );

setImageAttributes = index => attributes => {
const {
attributes: { images },
} = this.props;
if ( ! images[ index ] ) {
return;
}

setAttributes( {
this.setAttributes( {
images: [
...images.slice( 0, index ),
{
...images[ index ],
...newAttributes,
},
{ ...images[ index ], ...attributes },
...images.slice( index + 1 ),
],
} );
};

getImageCropHelp( checked ) {
return checked ? __( 'Thumbnails are cropped to align.' ) : __( 'Thumbnails are not cropped.' );
}
setLinkTo = value => this.setAttributes( { linkTo: value } );

uploadFromFiles = event => this.addFiles( event.target.files );

render() {
const { selectedImage } = this.state;

const { attributes, isSelected, className, noticeOperations, noticeUI } = this.props;
const { align, columns = defaultColumnsNumber( attributes ), images, linkTo } = attributes;

const {
images,
columns = defaultColumnsNumber( attributes ),
align,
imageCrop,
linkTo,
} = attributes;

const layoutsSupportingColumns = [ 'square', 'circle' ];

const dropZone = <DropZone onFilesDrop={ this.handleAddFiles } />;
const dropZone = <DropZone onFilesDrop={ this.addFiles } />;

const controls = (
<BlockControls>
{ !! images.length && (
<Toolbar>
<MediaUpload
onSelect={ this.handleSelectImages }
onSelect={ this.onSelectImages }
allowedTypes={ ALLOWED_MEDIA_TYPES }
multiple
gallery
Expand Down Expand Up @@ -185,7 +184,7 @@ class TiledGalleryEdit extends Component {
title: __( 'Tiled gallery' ),
name: __( 'images' ),
} }
onSelect={ this.handleSelectImages }
onSelect={ this.onSelectImages }
accept="image/*"
allowedTypes={ ALLOWED_MEDIA_TYPES }
multiple
Expand All @@ -196,86 +195,64 @@ class TiledGalleryEdit extends Component {
);
}

const renderGalleryImage = index => {
if ( ! images[ index ] ) {
return null;
}

const image = images[ index ];

return (
<GalleryImage
alt={ image.alt }
caption={ image.caption }
id={ image.id }
isSelected={ isSelected && selectedImage === index }
onRemove={ this.handleRemveImageByIndex( index ) }
onSelect={ this.handleSelectImageByIndex( index ) }
setAttributes={ this.handleSetImageAttributesByIndex( index ) }
url={ image.url }
/>
);
};

const layout = getActiveStyleName( this.props.className );
const layoutStyle = getActiveStyleName( LAYOUT_STYLES, attributes.className );

return (
<Fragment>
{ controls }
<InspectorControls>
<PanelBody title={ __( 'Tiled gallery settings' ) }>
{ images.length > 1 && (
<RangeControl
label={ __( 'Columns' ) }
value={ columns }
onChange={ this.handleColumnCountChange }
min={ 1 }
disabled={ ! layoutsSupportingColumns.includes( layout ) }
max={ Math.min( MAX_COLUMNS, images.length ) }
/>
) }
<ToggleControl
label={ __( 'Crop images' ) }
checked={ !! imageCrop }
onChange={ this.handleCropImageToggle }
help={ this.getImageCropHelp }
/>
{ /* @TODO disable with title comment, don't remove */ layoutSupportsColumns(
layoutStyle
) &&
images.length > 1 && (
<RangeControl
label={ __( 'Columns' ) }
value={ columns }
onChange={ this.setColumnsNumber }
min={ 1 }
max={ Math.min( MAX_COLUMNS, images.length ) }
/>
) }
<SelectControl
label={ __( 'Link to' ) }
label={ __( 'Link To' ) }
value={ linkTo }
onChange={ this.handleLinkToChange }
options={ [
{ value: 'attachment', label: __( 'Attachment page' ) },
{ value: 'media', label: __( 'Media file' ) },
{ value: 'none', label: __( 'None' ) },
] }
onChange={ this.setLinkTo }
options={ linkOptions }
/>
</PanelBody>
</InspectorControls>

{ noticeUI }
{ dropZone }
<GalleryGrid

<Layout
align={ align }
className={ className }
columns={ columns }
imageCrop={ imageCrop }
images={ images }
layout={ layout }
renderGalleryImage={ renderGalleryImage }
layoutStyle={ layoutStyle }
linkTo={ linkTo }
onRemoveImage={ this.onRemoveImage }
onSelectImage={ this.onSelectImage }
selectedImage={ isSelected ? selectedImage : null }
setImageAttributes={ this.setImageAttributes }
>
{ dropZone }
{ isSelected && (
<FormFileUpload
multiple
isLarge
className="block-library-gallery-add-item-button"
onChange={ this.handleFormFileUpload }
accept="image/*"
icon="insert"
>
{ __( 'Upload an image' ) }
</FormFileUpload>
<div className="tiled-gallery__add-item">
<FormFileUpload
multiple
isLarge
className="tiled-gallery__add-item-button"
onChange={ this.uploadFromFiles }
accept="image/*"
icon="insert"
>
{ __( 'Upload an image' ) }
</FormFileUpload>
</div>
) }
</GalleryGrid>
</Layout>
</Fragment>
);
}
Expand Down
Loading

0 comments on commit 38897e7

Please sign in to comment.