diff --git a/modules/tiled-gallery/block-edit.jsx b/modules/tiled-gallery/block-edit.jsx new file mode 100644 index 0000000000000..0d94c884af211 --- /dev/null +++ b/modules/tiled-gallery/block-edit.jsx @@ -0,0 +1,217 @@ +/*global wp*/ + +/** + * External Dependencies + */ +import React from 'react'; +import pick from 'lodash/pick'; + +/** + * WordPress dependencies (npm) + */ +const { Component } = wp.element; +const { __ } = wp.i18n; +const { mediaUpload } = wp.utils; + +/** + * WordPress dependencies + */ +const { + IconButton, + DropZone, + Toolbar, + PanelBody, + RangeControl, + SelectControl, +} = wp.components; +const { + MediaUpload, + ImagePlaceholder, + InspectorControls, + BlockControls, +} = wp.blocks; + +/** + * Internal dependencies + */ +import JetpackGalleryBlockSave from './block-save.jsx'; + +const MAX_COLUMNS = 8; +const linkOptions = [ + { value: 'attachment', label: __( 'Attachment Page' ) }, + { value: 'media', label: __( 'Media File' ) }, + { value: 'none', label: __( 'None' ) }, +]; + +class JetpackGalleryBlockEditor extends Component { + constructor() { + super( ...arguments ); + + this.onSelectImage = this.onSelectImage.bind( this ); + this.onSelectImages = this.onSelectImages.bind( this ); + this.setLinkTo = this.setLinkTo.bind( this ); + this.setColumnsNumber = this.setColumnsNumber.bind( this ); + this.setImageAttributes = this.setImageAttributes.bind( this ); + this.addFiles = this.addFiles.bind( this ); + + this.state = { + selectedImage: null, + }; + } + + onSelectImage( index ) { + return () => { + if ( this.state.selectedImage !== index ) { + this.setState( { + selectedImage: index, + } ); + } + }; + } + + onSelectImages( images ) { + this.props.setAttributes( { + images: images.map( ( image ) => pick( image, [ 'alt', 'caption', 'id', 'url', 'link' ] ) ), + } ); + } + + setLinkTo( value ) { + this.props.setAttributes( { linkTo: value } ); + } + + setColumnsNumber( value ) { + this.props.setAttributes( { columns: value } ); + } + + setImageAttributes( index, attributes ) { + const { attributes: { images }, setAttributes } = this.props; + if ( ! images[ index ] ) { + return; + } + setAttributes( { + images: [ + ...images.slice( 0, index ), + { + ...images[ index ], + ...attributes, + }, + ...images.slice( index + 1 ), + ], + } ); + } + + addFiles( files ) { + const currentImages = this.props.attributes.images || []; + const { setAttributes } = this.props; + mediaUpload( + files, + ( images ) => { + setAttributes( { + images: currentImages.concat( images ), + } ); + }, + 'image', + ); + } + + componentWillReceiveProps( nextProps ) { + // Deselect images when deselecting the block + if ( ! nextProps.isSelected && this.props.isSelected ) { + this.setState( { + selectedImage: null, + captionSelected: false, + } ); + } + } + + render() { + const { attributes, isSelected, className } = this.props; + const { images, columns, linkTo } = attributes; + + const dropZone = ( + + ); + + const controls = ( + isSelected && ( + + { !! images.length && ( + + img.id ) } + render={ function( { open } ) { + return ( + + ); + } + } + /> + + ) } + + ) + ); + + if ( images.length === 0 ) { + return [ + controls, + , + ]; + } + + // To avoid users accidentally navigating out of Gutenberg by clicking an image, we disable linkTo in the editor view here by forcing 'none'. + const imageTiles = ( + + ); + + return [ + controls, + isSelected && ( + + + { images.length > 1 && } + + + + ), + imageTiles, + dropZone, + ]; + } +} + +export default JetpackGalleryBlockEditor; diff --git a/modules/tiled-gallery/block-save.jsx b/modules/tiled-gallery/block-save.jsx new file mode 100644 index 0000000000000..3ec68e43a5606 --- /dev/null +++ b/modules/tiled-gallery/block-save.jsx @@ -0,0 +1,18 @@ +/** + * External Dependencies + */ +import React from 'react'; + +/** + * Internal dependencies + */ +import TiledGalleryLayoutSquare from './block/components/tiled-gallery-layout-square.jsx'; + +function JetpackGalleryBlockSave( { attributes } ) { + return ( + + ); +} + +export default JetpackGalleryBlockSave; + diff --git a/modules/tiled-gallery/block.jsx b/modules/tiled-gallery/block.jsx new file mode 100644 index 0000000000000..6b61377b9cb21 --- /dev/null +++ b/modules/tiled-gallery/block.jsx @@ -0,0 +1,104 @@ +/*global wp*/ + +/** + * WordPress dependencies + */ +const { __ } = wp.i18n; + +/** + * Internal dependencies + */ +import JetpackGalleryBlockEditor from './block-edit.jsx'; +import JetpackGalleryBlockSave from './block-save.jsx'; + +const JetpackGalleryBlockType = 'jetpack/gallery'; + +const settings = { + title: __( 'Jetpack Gallery' ), + icon: 'format-gallery', + category: 'layout', + + attributes: { + columns: { + type: 'integer', + 'default': 3, + }, + linkTo: { + type: 'string', + 'default': 'none', + }, + images: { + type: 'array', + 'default': [], + source: 'query', + selector: '.tiled-gallery-item', + query: { + width: { + source: 'attribute', + selector: 'img', + attribute: 'data-original-width', + }, + height: { + source: 'attribute', + selector: 'img', + attribute: 'data-original-height', + }, + url: { + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + 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: 'array', + source: 'children', + selector: 'figcaption', + }, + }, + }, + }, + + transforms: { + from: [ + { + type: 'block', + blocks: [ 'core/gallery' ], + transform: function( content ) { + return wp.blocks.createBlock( JetpackGalleryBlockType, content ); + }, + }, + ], + to: [ + { + type: 'block', + blocks: [ 'core/gallery' ], + transform: function( content ) { + return wp.blocks.createBlock( 'core/gallery', content ); + }, + }, + ], + }, + + edit: JetpackGalleryBlockEditor, + save: JetpackGalleryBlockSave +}; + +wp.blocks.registerBlockType( + JetpackGalleryBlockType, + settings +); diff --git a/modules/tiled-gallery/block/components/tiled-gallery-item.jsx b/modules/tiled-gallery/block/components/tiled-gallery-item.jsx new file mode 100644 index 0000000000000..4a9d5b6f1a175 --- /dev/null +++ b/modules/tiled-gallery/block/components/tiled-gallery-item.jsx @@ -0,0 +1,99 @@ +/*global wp*/ + +/** + * WordPress dependencies (npm) + */ +const { withSelect } = wp.data; +const { Component } = wp.element; + +/** + * External Dependencies + */ +import React from 'react'; +import get from 'lodash/get'; + +class TiledGalleryImage extends Component { + + componentWillReceiveProps( { image, width, height } ) { + // very carefully set width & height attributes once only (to avoid recurse)! + if ( image && ! width && ! height && this.props.setAttributes ) { + const mediaInfo = get( image, [ 'media_details' ], { width: null, height: null } ); + this.props.setAttributes( { + width: mediaInfo.width, + height: mediaInfo.height + } ); + } + } + + render() { + const { url, alt, id, link, width, height, caption } = this.props; + const styleAttr = { + width: width + 'px', + height: height + 'px', + }; + + return ( +
+ + + { + { caption && caption.length > 0 &&
{ caption }
} +
+ ); + } +} + +function TiledGalleryItem( props ) { + const classes = [ 'tiled-gallery-item' ]; + classes.push( 'tiled-gallery-item-small' ); + + let href; + switch ( props.linkTo ) { + case 'media': + href = props.url; + break; + case 'attachment': + href = props.link; + break; + } + + const img = ( + + ); + + return ( +
+ { href ? { img } : img } +
+ ); +} + +export default withSelect( ( select, ownProps ) => { + const { getMedia } = select( 'core' ); + const { id } = ownProps; + + return { + image: id ? getMedia( id ) : null, + }; +} )( TiledGalleryItem ); + diff --git a/modules/tiled-gallery/block/components/tiled-gallery-layout-square.jsx b/modules/tiled-gallery/block/components/tiled-gallery-layout-square.jsx new file mode 100644 index 0000000000000..4c905512d8aab --- /dev/null +++ b/modules/tiled-gallery/block/components/tiled-gallery-layout-square.jsx @@ -0,0 +1,157 @@ +/*global wp*/ + +/** + * WordPress dependencies (npm) + */ +const { Component } = wp.element; + +/** + * External Dependencies + */ +import React from 'react'; + +/** + * Internal dependencies + */ +import TiledGalleryItem from './tiled-gallery-item.jsx'; + +// hard coded for now - ideally we'd inject $content_width +// not sure how critical this is, likely necessary to work nicely with themes +const CONTENT_WIDTH = 520; + +function TiledGallerySquareGroup( { group_size, id, url, link, width, height, caption, linkTo, setAttributes } ) { + const styleAttr = { + width: group_size + 'px', + height: group_size + 'px', + }; + return ( +
+ +
+ ); +} + +class TiledGalleryLayoutSquare extends Component { + + computeItems() { + const { columns, images } = this.props; + + const content_width = CONTENT_WIDTH; // todo: get content width + const images_per_row = ( columns > 1 ? columns : 1 ); + const margin = 2; + + const margin_space = ( images_per_row * margin ) * 2; + const size = Math.floor( ( content_width - margin_space ) / images_per_row ); + let remainder_size = size; + let img_size = remainder_size; + const remainder = images.length % images_per_row; + let remainder_space = 0; + if ( remainder > 0 ) { + remainder_space = ( remainder * margin ) * 2; + remainder_size = Math.floor( ( content_width - remainder_space ) / remainder ); + } + + let c = 1; + let items_in_row = 0; + const rows = []; + let row = { + images: [], + }; + for ( const image of images ) { + if ( remainder > 0 && c <= remainder ) { + img_size = remainder_size; + } else { + img_size = size; + } + + image.width = image.height = img_size; + + row.images.push( image ); + c++; + items_in_row++; + + if ( images_per_row === items_in_row || ( remainder + 1 ) === c ) { + rows.push( row ); + items_in_row = 0; + + row.height = img_size + margin * 2; + row.width = content_width; + row.group_size = img_size + 2 * margin; + + row = { + images: [], + }; + } + } + + if ( row.images.length > 0 ) { + row.height = img_size + margin * 2; + row.width = content_width; + row.group_size = img_size + 2 * margin; + + rows.push( row ); + } + + return rows; + } + + render() { + const rows = this.computeItems(); + const linkTo = this.props.linkTo; + + return ( +
+ { rows.map( ( row, index ) => { + const styleAttr = { + width: row.width + 'px', + height: row.height + 'px', + }; + const setMyAttributes = ( attrs ) => this.setImageAttributes( index, attrs ); + + return ( +
+ { row.images.map( ( image ) => ( + + ) ) } +
+ ); + } ) } +
+ ); + } +} + +export default TiledGalleryLayoutSquare; + diff --git a/modules/tiled-gallery/tiled-gallery.php b/modules/tiled-gallery/tiled-gallery.php index ac4d294ba3387..2ea72e5d47d06 100644 --- a/modules/tiled-gallery/tiled-gallery.php +++ b/modules/tiled-gallery/tiled-gallery.php @@ -14,10 +14,10 @@ class Jetpack_Tiled_Gallery { public function __construct() { add_action( 'admin_init', array( $this, 'settings_api_init' ) ); + add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_block_editor_assets' ) ); + add_action( 'enqueue_block_assets', array( __CLASS__, 'enqueue_block_assets' ) ); add_filter( 'jetpack_gallery_types', array( $this, 'jetpack_gallery_types' ), 9 ); add_filter( 'jetpack_default_gallery_type', array( $this, 'jetpack_default_gallery_type' ) ); - - } public function tiles_enabled() { @@ -228,6 +228,26 @@ static function get_talaveras() { return self::$talaveras; } + /** + * Enqueue js for our jetpack/gallery block + */ + public static function enqueue_block_editor_assets() { + wp_register_script( + 'jetpack-tiled-gallery-block', + plugins_url( '_inc/build/modules-tiled-gallery-block.js', JETPACK__PLUGIN_FILE ), // this is built as a new webpack entry point + array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-components' ) + ); + wp_enqueue_script( 'jetpack-tiled-gallery-block' ); + } + + + /** + * Enqueue the existing css & js for the block (we are using it for Gutenberg also) + */ + public static function enqueue_block_assets() { + self::default_scripts_and_styles(); + } + /** * Add a checkbox field to the Carousel section in Settings > Media * for setting tiled galleries as the default. diff --git a/modules/tiled-gallery/tiled-gallery/tiled-gallery.css b/modules/tiled-gallery/tiled-gallery/tiled-gallery.css index b4cdc576c1466..8fb2e224bc313 100644 --- a/modules/tiled-gallery/tiled-gallery/tiled-gallery.css +++ b/modules/tiled-gallery/tiled-gallery/tiled-gallery.css @@ -31,6 +31,9 @@ text-decoration: none; width: auto; } +.tiled-gallery .tiled-gallery-item figure { + margin: 0; +} .tiled-gallery .tiled-gallery-item img, .tiled-gallery .tiled-gallery-item img:hover { /* Needs to reset some properties for theme compatibility */ background: none; diff --git a/webpack.config.js b/webpack.config.js index 1946cf252ffed..af37b2c611636 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,7 +10,8 @@ const webpackConfig = { // The key is used as the name of the script. entry: { admin: './_inc/client/admin.js', - 'static': './_inc/client/static.jsx' + 'static': './_inc/client/static.jsx', + 'modules-tiled-gallery-block': './modules/tiled-gallery/block.jsx', }, output: { path: path.join( __dirname, '_inc/build' ), @@ -42,6 +43,7 @@ const webpackConfig = { include: [ path.join( __dirname, 'test' ), path.join( __dirname, '_inc/client' ), + path.join( __dirname, 'modules/tiled-gallery' ), ] }, {