diff --git a/blocks/library/gallery/gallery-image.js b/blocks/library/gallery/gallery-image.js
new file mode 100644
index 00000000000000..c2aac32ef7b518
--- /dev/null
+++ b/blocks/library/gallery/gallery-image.js
@@ -0,0 +1,8 @@
+
+export default function GalleryImage( props ) {
+ return (
+
+
+
+ );
+}
diff --git a/blocks/library/gallery/index.js b/blocks/library/gallery/index.js
new file mode 100644
index 00000000000000..8f0941260d9ad5
--- /dev/null
+++ b/blocks/library/gallery/index.js
@@ -0,0 +1,142 @@
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import { registerBlockType, query as hpq } from '../../api';
+
+import Placeholder from 'components/placeholder';
+import MediaUploadButton from '../../media-upload-button';
+
+import GalleryImage from './gallery-image';
+
+const { query, attr } = hpq;
+
+const editMediaLibrary = ( attributes, setAttributes ) => {
+ const frameConfig = {
+ frame: 'post',
+ title: wp.i18n.__( 'Update Gallery media' ),
+ button: {
+ text: wp.i18n.__( 'Select' ),
+ },
+ multiple: true,
+ state: 'gallery-edit',
+ selection: new wp.media.model.Selection( attributes.images, { multiple: true } ),
+ };
+
+ const editFrame = wp.media( frameConfig );
+ function updateFn() {
+ setAttributes( {
+ images: this.frame.state().attributes.library.models.map( ( a ) => {
+ return a.attributes;
+ } ),
+ } );
+ }
+
+ editFrame.on( 'insert', updateFn );
+ editFrame.state( 'gallery-edit' ).on( 'update', updateFn );
+ editFrame.open( 'gutenberg-gallery' );
+};
+
+/**
+ * Returns an attribute setter with behavior that if the target value is
+ * already the assigned attribute value, it will be set to undefined.
+ *
+ * @param {string} align Alignment value
+ * @return {Function} Attribute setter
+ */
+function toggleAlignment( align ) {
+ return ( attributes, setAttributes ) => {
+ const nextAlign = attributes.align === align ? undefined : align;
+ setAttributes( { align: nextAlign } );
+ };
+}
+
+registerBlockType( 'core/gallery', {
+ title: wp.i18n.__( 'Gallery' ),
+ icon: 'format-gallery',
+ category: 'common',
+
+ attributes: {
+ images:
+ query( 'div.blocks-gallery figure.blocks-gallery-image img', {
+ url: attr( 'src' ),
+ alt: attr( 'alt' ),
+ } ),
+ },
+
+ controls: [
+ {
+ icon: 'format-image',
+ title: wp.i18n.__( 'Edit Gallery' ),
+ onClick: editMediaLibrary,
+ },
+ {
+ icon: 'align-left',
+ title: wp.i18n.__( 'Align left' ),
+ isActive: ( { align } ) => 'left' === align,
+ onClick: toggleAlignment( 'left' ),
+ },
+ {
+ icon: 'align-center',
+ title: wp.i18n.__( 'Align center' ),
+ isActive: ( { align } ) => ! align || 'center' === align,
+ onClick: toggleAlignment( 'center' ),
+ },
+ {
+ icon: 'align-right',
+ title: wp.i18n.__( 'Align right' ),
+ isActive: ( { align } ) => 'right' === align,
+ onClick: toggleAlignment( 'right' ),
+ },
+ {
+ icon: 'align-full-width',
+ title: wp.i18n.__( 'Wide width' ),
+ isActive: ( { align } ) => 'wide' === align,
+ onClick: toggleAlignment( 'wide' ),
+ },
+ ],
+
+ edit( { attributes, setAttributes } ) {
+ const { images, align = 'none' } = attributes;
+ if ( ! images ) {
+ const setMediaUrl = ( imgs ) => setAttributes( { images: imgs } );
+ return (
+
+
+ { wp.i18n.__( 'Insert from Media Library' ) }
+
+
+ );
+ }
+
+ return (
+
+ { images.map( ( img, i ) => (
+
+ ) ) }
+
+ );
+ },
+
+ save( { attributes } ) {
+ const { images, align = 'none' } = attributes;
+
+ return (
+
+ { images.map( ( img, i ) => (
+
+ ) ) }
+
+ );
+ },
+
+} );
diff --git a/blocks/library/gallery/style.scss b/blocks/library/gallery/style.scss
new file mode 100644
index 00000000000000..d07d3b801db5c2
--- /dev/null
+++ b/blocks/library/gallery/style.scss
@@ -0,0 +1,37 @@
+
+.blocks-gallery {
+
+ display: flex;
+ flex-wrap: wrap;
+
+ .blocks-gallery-image {
+
+ margin: 8px;
+
+ img {
+ max-width: 120px;
+ }
+ }
+}
+
+.blocks-gallery.is-placeholder {
+ margin: -15px;
+ padding: 6em 0;
+ border: 2px solid $light-gray-500;
+ text-align: center;
+}
+
+.blocks-gallery__placeholder-label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+
+ .dashicon {
+ margin-right: 1ch;
+ }
+}
+
+.blocks-gallery__placeholder-instructions {
+ margin: 1.8em 0;
+}
diff --git a/blocks/library/index.js b/blocks/library/index.js
index 5f1b28bcf894d2..c611bf9984bcea 100644
--- a/blocks/library/index.js
+++ b/blocks/library/index.js
@@ -11,4 +11,5 @@ import './pullquote';
import './table';
import './preformatted';
import './code';
+import './gallery';
import './latest-posts';
diff --git a/blocks/test/fixtures/core-gallery.html b/blocks/test/fixtures/core-gallery.html
new file mode 100644
index 00000000000000..233b319fba5713
--- /dev/null
+++ b/blocks/test/fixtures/core-gallery.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/blocks/test/fixtures/core-gallery.json b/blocks/test/fixtures/core-gallery.json
new file mode 100644
index 00000000000000..10cd14286ac919
--- /dev/null
+++ b/blocks/test/fixtures/core-gallery.json
@@ -0,0 +1,12 @@
+[
+ {
+ "uid": "_uid_0",
+ "name": "core/gallery",
+ "attributes": {
+ "images": [
+ { "url": "https://cldup.com/uuUqE_dXzy.jpg", "alt": "title" },
+ { "url": "https://cldup.com/uuUqE_dXzy.jpg", "alt": "title" }
+ ]
+ }
+ }
+]
diff --git a/blocks/test/fixtures/core-gallery.serialized.html b/blocks/test/fixtures/core-gallery.serialized.html
new file mode 100644
index 00000000000000..d3f126352a25b0
--- /dev/null
+++ b/blocks/test/fixtures/core-gallery.serialized.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/blocks/test/full-content.js b/blocks/test/full-content.js
index 0798782fb5e3db..4bdf65b03bac7f 100644
--- a/blocks/test/full-content.js
+++ b/blocks/test/full-content.js
@@ -53,7 +53,10 @@ function normalizeReactTree( element ) {
return element.map( child => normalizeReactTree( child ) );
}
- if ( isObject( element ) ) {
+ // Check if we got an object first, then if it actually has a `type` like a
+ // React component. Sometimes we get other stuff here, which probably
+ // indicates a bug.
+ if ( isObject( element ) && element.type ) {
const toReturn = {
type: element.type,
};