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 (
+
+ );
+ }
+}
+
+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 (
+
+ );
+}
+
+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' ),
]
},
{