diff --git a/.eslintignore b/.eslintignore
index 3aa71080db424e..164ae5ac2c7efd 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,4 +2,3 @@ build
coverage
vendor
node_modules
-/assets/js
diff --git a/assets/js/meta-box-resize.js b/assets/js/meta-box-resize.js
deleted file mode 100644
index 005e13acce6caa..00000000000000
--- a/assets/js/meta-box-resize.js
+++ /dev/null
@@ -1,47 +0,0 @@
-( function() {
- var observer;
-
- if ( ! window.MutationObserver || ! document.getElementById( 'post' ) || ! window.parent ) {
- return;
- }
-
- var previousWidth, previousHeight;
-
- function sendResize() {
- var form = document.getElementById( 'post' );
- var location = form.dataset.location;
- var newWidth = form.scrollWidth;
- var newHeight = form.scrollHeight;
-
- // Exit early if height has not been impacted.
- if ( newWidth === previousWidth && newHeight === previousHeight ) {
- return;
- }
-
- window.parent.postMessage( {
- action: 'resize',
- source: 'meta-box',
- location: location,
- width: newWidth,
- height: newHeight
- }, '*' );
-
- previousWidth = newWidth;
- previousHeight = newHeight;
- }
-
- observer = new MutationObserver( sendResize );
- observer.observe( document.getElementById( 'post' ), {
- attributes: true,
- attributeOldValue: true,
- characterData: true,
- characterDataOldValue: true,
- childList: true,
- subtree: true
- } );
-
- window.addEventListener( 'load', sendResize, true );
- window.addEventListener( 'resize', sendResize, true );
-
- sendResize();
-} )();
diff --git a/bin/build-plugin-zip.sh b/bin/build-plugin-zip.sh
index b86bb82bd068d0..4828c1d50f0b5e 100755
--- a/bin/build-plugin-zip.sh
+++ b/bin/build-plugin-zip.sh
@@ -93,7 +93,6 @@ mv gutenberg.tmp.php gutenberg.php
status "Creating archive..."
zip -r gutenberg.zip \
gutenberg.php \
- assets/js/*.js \
lib/*.php \
blocks/library/*/*.php \
post-content.js \
diff --git a/docs/meta-box.md b/docs/meta-box.md
index ef43591dcf4302..f48dc84dab0349 100644
--- a/docs/meta-box.md
+++ b/docs/meta-box.md
@@ -5,14 +5,6 @@ the superior developer and user experience of blocks however, especially once,
block templates are available, **converting PHP meta boxes to blocks is highly
encouraged!**
-## Breakdown
-
-Each meta box area is rendered by a React component containing an iframe.
-Each iframe will render a partial page containing only meta boxes for that area.
-Meta box data is collected and used for conditional rendering. The meta box areas
-will appear as toggle-able panels labeled "Extended Settings". More on this in
-the MetaBoxIframe component section.
-
### Meta Box Data Collection
On each Gutenberg page load, the global state of post.php is mimicked, this is
@@ -50,21 +42,16 @@ this might now be possible. Test with ACF to make sure.
`INITIALIZE_META_BOX_STATE` comes in, the store will update any active meta box
areas by setting the `isActive` flag to `true`. Once this happens React will
check for the new props sent in by Redux on the `MetaBox` component. If that
-`MetaBox` is now active, instead of rendering null, a `MetaBoxIframe` component will
+`MetaBox` is now active, instead of rendering null, a `MetaBoxArea` component will
be rendered. The `MetaBox` component is the container component that mediates
-between the `MetaBoxIframe` and the Redux Store. *If no meta boxes are active,
+between the `MetaBoxArea` and the Redux Store. *If no meta boxes are active,
nothing happens. This will be the default behavior, as all core meta boxes have
been stripped.*
-#### MetaBoxIframe Component
+#### MetaBoxArea Component
-When the component renders it will store a ref to the iframe, the component will
-set up a listener for post messages to handle resizing. `assets/js/meta-box-resize.js` is
-loaded inside the iframe and will send up postMessages for resizing, which the
-`MetaBoxIframe` Component will use to manage its state. A mutation observer will
-also be created when the iframe loads. The observer will detect whether, any
-DOM changes have happened in the iframe, input and change event listeners will
-also be attached to check for changes.
+When the component renders it will store a ref to the metaboxes container,
+calls the page rendering the metaboxes and watches input and changes.
The change detection will store the current form's `FormData`, then whenever a
change is detected the current form data will be checked vs, the original form
@@ -84,19 +71,15 @@ submitted. This removes any unnecessary requests being made. No extra revisions,
are created either by the meta box submissions. A Redux action will trigger on
`REQUEST_POST_UPDATE` for any dirty meta box. See `editor/effects.js`. The
`REQUEST_META_BOX_UPDATES` action will set that meta boxes' state to `isUpdating`,
-the `isUpdating` prop will be sent into the `MetaBoxIframe` and cause a form
-submission. The iframe will clone itself and perform a double buffer right
-before the main iframe submits its data. After loading, the original change
-detection process is fired again to handle the new state.
+the `isUpdating` prop will be sent into the `MetaBoxArea` and cause a form
+submission.
Since the meta box updating is being triggered on post save success, we check to
see if the post is saving and display an updating overlay, to prevent users from
changing the form values while the meta box is submitting. The saving overlay
could be made transparent, to give a more seamless effect.
-### Iframe serving a partial page.
-
-Each iframe will point to an individual source. These are partial pages being
+Each `MetaBoxArea` will point to an individual source. These are partial pages being
served by post.php. Why this approach? By using post.php directly, we don't have
to worry as much about getting the global state 100% correct for each and every
use case of a meta box, especially when it comes to saving. Essentially, when
@@ -117,10 +100,7 @@ area is served. So an example url would look like:
This url is automatically passed into React via a `_wpMetaBoxUrl` global variable.
The partial page is very similar to post.php and pretty much imitates it and
after rendering the meta boxes via `do_meta_boxes()` it imitates `admin_footer`,
-exits early, and does some hook clean up. There are two extra files that are
-enqueued. One is the js file from `assets/js/meta-box-resize.js`, which resizes the iframe.
-The other is a stylesheet that is generated by webpack from `editor/meta-boxes/meta-box-iframe.scss`
-and built into `editor/build/meta-box-iframe.css`
+exits early, and does some hook clean up.
These styles make use of some of the SASS variables, so that as the Gutenberg
UI updates so will the meta boxes.
diff --git a/editor/actions.js b/editor/actions.js
index 7c061149132a61..a35fb7ba337ff2 100644
--- a/editor/actions.js
+++ b/editor/actions.js
@@ -443,6 +443,20 @@ export function handleMetaBoxReload( location ) {
};
}
+/**
+ * Returns an action object used to signify that a meta box finished loading.
+ *
+ * @param {String} location Location of meta box: 'normal', 'side'.
+ *
+ * @return {Object} Action object
+ */
+export function metaBoxLoaded( location ) {
+ return {
+ type: 'META_BOX_LOADED',
+ location,
+ };
+}
+
/**
* Returns an action object used to request meta box update.
*
diff --git a/editor/assets/stylesheets/_z-index.scss b/editor/assets/stylesheets/_z-index.scss
index e2eef2725391ad..629e37628f6e1f 100644
--- a/editor/assets/stylesheets/_z-index.scss
+++ b/editor/assets/stylesheets/_z-index.scss
@@ -15,6 +15,8 @@ $z-layers: (
'.editor-inserter__tabs': 1,
'.editor-inserter__tab.is-active': 1,
'.components-panel__header': 1,
+ '.editor-meta-boxes-area.is-loading:before': 1,
+ '.editor-meta-boxes-area .spinner': 2,
'.blocks-format-toolbar__link-modal': 2,
'.editor-block-contextual-toolbar': 2,
'.editor-block-switcher__menu': 2,
diff --git a/editor/components/meta-boxes/index.js b/editor/components/meta-boxes/index.js
index 89933411045f82..09f8f0008453a5 100644
--- a/editor/components/meta-boxes/index.js
+++ b/editor/components/meta-boxes/index.js
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
/**
* Internal dependencies
*/
-import MetaBoxesIframe from './meta-boxes-iframe';
+import MetaBoxesArea from './meta-boxes-area';
import MetaBoxesPanel from './meta-boxes-panel';
import { getMetaBox } from '../../selectors';
@@ -15,7 +15,7 @@ function MetaBoxes( { location, isActive, usePanel = false } ) {
return null;
}
- const element = ;
+ const element = ;
if ( ! usePanel ) {
return element;
diff --git a/editor/components/meta-boxes/meta-boxes-area/index.js b/editor/components/meta-boxes/meta-boxes-area/index.js
new file mode 100644
index 00000000000000..26a6002a0b9466
--- /dev/null
+++ b/editor/components/meta-boxes/meta-boxes-area/index.js
@@ -0,0 +1,161 @@
+/**
+ * External dependencies
+ */
+import { isEqual } from 'lodash';
+import classnames from 'classnames';
+import { connect } from 'react-redux';
+
+/**
+ * WordPress dependencies
+ */
+import { addQueryArgs } from '@wordpress/url';
+import { Component } from '@wordpress/element';
+import { Spinner } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import { handleMetaBoxReload, metaBoxStateChanged, metaBoxLoaded } from '../../../actions';
+import { getMetaBox, isSavingPost } from '../../../selectors';
+
+class MetaBoxesArea extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.state = {
+ loading: true,
+ };
+ this.originalFormData = [];
+ this.bindNode = this.bindNode.bind( this );
+ this.checkState = this.checkState.bind( this );
+ }
+
+ bindNode( node ) {
+ this.node = node;
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchMetaboxes();
+ }
+
+ componentWillUnmout() {
+ this.mounted = false;
+ this.unbindFormEvents();
+ }
+
+ unbindFormEvents() {
+ if ( this.form ) {
+ this.form.removeEventListener( 'change', this.checkState );
+ this.form.removeEventListener( 'input', this.checkState );
+ }
+ }
+
+ componentWillReceiveProps( nextProps ) {
+ if ( nextProps.isUpdating && ! this.props.isUpdating ) {
+ this.setState( { loading: true } );
+ const { location } = nextProps;
+ const headers = new window.Headers();
+ const fetchOptions = {
+ method: 'POST',
+ headers,
+ body: new window.FormData( this.form ),
+ credentials: 'include',
+ };
+ const request = window.fetch( addQueryArgs( window._wpMetaBoxUrl, { meta_box: location } ), fetchOptions );
+ this.onMetaboxResponse( request, false );
+ this.unbindFormEvents();
+ }
+ }
+
+ fetchMetaboxes() {
+ const { location } = this.props;
+ const request = window.fetch( addQueryArgs( window._wpMetaBoxUrl, { meta_box: location } ), { credentials: 'include' } );
+ this.onMetaboxResponse( request );
+ }
+
+ onMetaboxResponse( request, initial = true ) {
+ request.then( ( response ) => response.text() )
+ .then( ( body ) => {
+ if ( ! this.mounted ) {
+ return;
+ }
+ jQuery( this.node ).html( body );
+ this.form = this.node.querySelector( '.meta-box-form' );
+ this.form.onSubmit = ( event ) => event.preventDefault();
+ this.originalFormData = this.getFormData();
+ this.form.addEventListener( 'change', this.checkState );
+ this.form.addEventListener( 'input', this.checkState );
+ this.setState( { loading: false } );
+ if ( ! initial ) {
+ this.props.metaBoxReloaded( this.props.location );
+ } else {
+ this.props.metaBoxLoaded( this.props.location );
+ }
+ } );
+ }
+
+ getFormData() {
+ const data = new window.FormData( this.form );
+ const entries = Array.from( data.entries() );
+ return entries;
+ }
+
+ checkState() {
+ const { loading } = this.state;
+ const { isDirty, changedMetaBoxState, location } = this.props;
+
+ const newIsDirty = ! isEqual( this.originalFormData, this.getFormData() );
+
+ /**
+ * If we are not updating, then if dirty and equal to original, then set not dirty.
+ * If we are not updating, then if not dirty and not equal to original, set as dirty.
+ */
+ if ( ! loading && isDirty !== newIsDirty ) {
+ changedMetaBoxState( location, newIsDirty );
+ }
+ }
+
+ render() {
+ const { location } = this.props;
+ const { loading } = this.state;
+
+ const classes = classnames(
+ 'editor-meta-boxes-area',
+ `is-${ location }`,
+ {
+ 'is-loading': loading,
+ }
+ );
+
+ return (
+
+ { loading && }
+
+
+ );
+ }
+}
+
+function mapStateToProps( state, ownProps ) {
+ const metaBox = getMetaBox( state, ownProps.location );
+ const { isDirty, isUpdating } = metaBox;
+
+ return {
+ isDirty,
+ isUpdating,
+ isPostSaving: isSavingPost( state ) ? true : false,
+ };
+}
+
+function mapDispatchToProps( dispatch ) {
+ return {
+ // Used to set the reference to the MetaBox in redux, fired when the component mounts.
+ metaBoxReloaded: ( location ) => dispatch( handleMetaBoxReload( location ) ),
+ changedMetaBoxState: ( location, hasChanged ) => dispatch( metaBoxStateChanged( location, hasChanged ) ),
+ metaBoxLoaded: ( location ) => dispatch( metaBoxLoaded( location ) ),
+ };
+}
+
+export default connect( mapStateToProps, mapDispatchToProps )( MetaBoxesArea );
diff --git a/editor/components/meta-boxes/meta-boxes-area/style.scss b/editor/components/meta-boxes/meta-boxes-area/style.scss
new file mode 100644
index 00000000000000..cddb4e6b4a30a8
--- /dev/null
+++ b/editor/components/meta-boxes/meta-boxes-area/style.scss
@@ -0,0 +1,93 @@
+
+.editor-meta-boxes-area {
+ position: relative;
+
+ /* Match width and positioning of the meta boxes. Override default styles. */
+ #poststuff {
+ margin: 0 auto;
+ padding-top: 0;
+ min-width: auto;
+ }
+
+ #post {
+ margin: 0;
+ }
+
+ /* Override Default meta box stylings */
+
+ #poststuff h3.hndle,
+ #poststuff .stuffbox > h3,
+ #poststuff h2.hndle { /* WordPress selectors yolo */
+ border-bottom: 1px solid $light-gray-500;
+ box-sizing: border-box;
+ color: $dark-gray-500;
+ font-weight: 600;
+ outline: none;
+ padding: 15px;
+ position: relative;
+ width: 100%;
+ }
+
+ .postbox {
+ border: 0;
+ color: $dark-gray-500;
+ margin-bottom: 0;
+ }
+
+ .postbox > .inside {
+ border-bottom: 1px solid $light-gray-500;
+ color: $dark-gray-500;
+ padding: 15px;
+ margin: 0;
+ }
+
+ input {
+ max-width: 300px;
+ }
+
+ input,
+ select,
+ textarea {
+ background: inherit;
+ border: 1px solid $light-gray-500;
+ border-radius: 4px;
+ box-shadow: none;
+ color: $dark-gray-800;
+ display: inline-block;
+ font-family: inherit;
+ font-size: 13px;
+ line-height: 24px;
+ outline: none;
+ padding: 4px;
+ }
+
+ input:hover,
+ select:hover,
+ textarea:hover {
+ border: 1px solid $light-gray-700;
+ }
+
+ .postbox .handlediv {
+ height: 44px;
+ width: 44px;
+ }
+
+ &.is-loading:before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ content: '';
+ z-index: 1;
+ background: transparent;
+ z-index: z-index( '.editor-meta-boxes-area.is-loading:before');
+ }
+
+ .spinner {
+ position: absolute;
+ top: 10px;
+ right: 20px;
+ z-index: z-index( '.editor-meta-boxes-area .spinner');
+ }
+}
diff --git a/editor/components/meta-boxes/meta-boxes-iframe/index.js b/editor/components/meta-boxes/meta-boxes-iframe/index.js
deleted file mode 100644
index c7b1252b7adfc2..00000000000000
--- a/editor/components/meta-boxes/meta-boxes-iframe/index.js
+++ /dev/null
@@ -1,265 +0,0 @@
-/**
- * External dependencies
- */
-import { isEqual } from 'lodash';
-import classnames from 'classnames';
-import { connect } from 'react-redux';
-
-/**
- * WordPress dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { addQueryArgs } from '@wordpress/url';
-import { Component } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import './style.scss';
-import './meta-box-iframe.scss';
-import { handleMetaBoxReload, metaBoxStateChanged } from '../../../actions';
-import { getMetaBox, isSavingPost } from '../../../selectors';
-
-class MetaBoxesIframe extends Component {
- constructor() {
- super( ...arguments );
-
- this.state = {
- width: 0,
- height: 0,
- };
-
- this.originalFormData = [];
- this.hasLoaded = false;
- this.formData = [];
- this.form = null;
-
- this.checkMessageForResize = this.checkMessageForResize.bind( this );
- this.handleDoubleBuffering = this.handleDoubleBuffering.bind( this );
- this.handleMetaBoxReload = this.handleMetaBoxReload.bind( this );
- this.checkMetaBoxState = this.checkMetaBoxState.bind( this );
- this.observeChanges = this.observeChanges.bind( this );
- this.bindNode = this.bindNode.bind( this );
- this.isSaving = this.isSaving.bind( this );
- }
-
- bindNode( node ) {
- this.node = node;
- }
-
- componentDidMount() {
- /**
- * Sets up an event listener for resizing. The resizing occurs inside
- * the iframe, see gutenberg/assets/js/meta-box-resize.js
- */
- window.addEventListener( 'message', this.checkMessageForResize, false );
-
- // Initially set node to not display anything so that when it loads, we can see it.
- this.node.style.display = 'none';
-
- this.node.addEventListener( 'load', this.observeChanges );
- }
-
- componentWillReceiveProps( nextProps ) {
- // Exit early if updating, or not while the post is saving.
- if ( ! nextProps.isUpdating || nextProps.isPostSaving ) {
- return;
- }
-
- const iframe = this.node;
-
- this.clonedNode = iframe.cloneNode( true );
- this.clonedNode.classList.add( 'is-updating' );
- this.hideNode( this.clonedNode );
- const parent = iframe.parentNode;
-
- parent.appendChild( this.clonedNode );
-
- /**
- * When the dom content has loaded for the cloned iframe handle the
- * double buffering.
- */
- this.clonedNode.addEventListener( 'load', this.handleDoubleBuffering );
- }
-
- handleDoubleBuffering() {
- const { node, clonedNode, form } = this;
-
- form.submit();
-
- const cloneForm = clonedNode.contentWindow.document.getElementById( 'post' );
- // Make the cloned state match the current state visually.
- cloneForm.parentNode.replaceChild( form, cloneForm );
-
- this.showNode( clonedNode );
- this.hideNode( node );
-
- node.addEventListener( 'load', this.handleMetaBoxReload );
- }
-
- hideNode( node ) {
- node.classList.add( 'is-hidden' );
- }
-
- showNode( node ) {
- node.classList.remove( 'is-hidden' );
- }
-
- componentWillUnmount() {
- const iframe = this.node;
- iframe.removeEventListener( 'message', this.checkMessageForResize );
-
- if ( this.form ) {
- this.form.removeEventListener( 'input', this.checkMetaBoxState );
- this.form.removeEventListener( 'change', this.checkMetaBoxState );
- }
-
- this.node.removeEventListener( 'load', this.observeChanges );
- }
-
- observeChanges() {
- const node = this.node;
-
- // The standard post.php form ID post should probably be mimicked.
- this.form = this.node.contentWindow.document.getElementById( 'post' );
-
- // If the iframe has not already loaded before.
- if ( ! this.hasLoaded ) {
- node.style.display = 'block';
- this.originalFormData = this.getFormData();
- this.hasLoaded = true;
- }
-
- this.form.addEventListener( 'change', this.checkMetaBoxState );
- this.form.addEventListener( 'input', this.checkMetaBoxState );
- }
-
- getFormData() {
- const form = this.form;
-
- const data = new window.FormData( form );
- const entries = Array.from( data.entries() );
- return entries;
- }
-
- checkMetaBoxState() {
- const { isUpdating, isDirty, changedMetaBoxState, location } = this.props;
-
- const isStateEqual = isEqual( this.originalFormData, this.getFormData() );
-
- /**
- * If we are not updating, then if dirty and equal to original, then set not dirty.
- * If we are not updating, then if not dirty and not equal to original, set as dirty.
- */
- if ( ! isUpdating && ( isDirty === isStateEqual ) ) {
- changedMetaBoxState( location, ! isDirty );
- }
- }
-
- handleMetaBoxReload( event ) {
- // Remove the reloading event listener once the meta box has loaded.
- event.target.removeEventListener( 'load', this.handleMetaBoxReload );
-
- if ( this.clonedNode ) {
- this.showNode( this.node );
- this.hideNode( this.clonedNode );
- this.clonedNode.removeEventListener( 'load', this.handleDoubleBuffering );
- this.clonedNode.parentNode.removeChild( this.clonedNode );
- delete this.clonedNode;
- }
-
- this.originalFormData = this.getFormData();
- this.props.metaBoxReloaded( this.props.location );
- }
-
- checkMessageForResize( event ) {
- const iframe = this.node;
-
- // Attempt to parse the message data as JSON if passed as string
- let data = event.data || {};
- if ( 'string' === typeof data ) {
- try {
- data = JSON.parse( data );
- } catch ( e ) {} // eslint-disable-line no-empty
- }
-
- // Check to make sure the meta box matches this location.
- if ( data.source !== 'meta-box' || data.location !== this.props.location ) {
- return;
- }
-
- // Verify that the mounted element is the source of the message
- if ( ! iframe || iframe.contentWindow !== event.source ) {
- return;
- }
-
- // Update the state only if the message is formatted as we expect, i.e.
- // as an object with a 'resize' action, width, and height
- const { action, width, height } = data;
- const { width: oldWidth, height: oldHeight } = this.state;
-
- if ( 'resize' === action && ( oldWidth !== width || oldHeight !== height ) ) {
- this.setState( { width, height } );
- }
- }
-
- isSaving() {
- const { isUpdating, isDirty, isPostSaving } = this.props;
- return isUpdating || ( isDirty && isPostSaving );
- }
-
- render() {
- const { location } = this.props;
- const { width, height } = this.state;
- const isSaving = this.isSaving();
-
- const classes = classnames(
- 'editor-meta-boxes-iframe',
- `is-${ location }`
- );
-
- const overlayClasses = classnames(
- 'editor-meta-boxes-iframe__loading-overlay',
- { 'is-visible': isSaving }
- );
-
- const iframeClasses = classnames( { 'is-updating': isSaving } );
-
- return (
-