Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Legacy Widget design iterations #30889

Merged
merged 22 commits into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
81c0e41
Legacy Widget: Show Preview by default and add Apply and Cancel buttons
noisysocks Apr 15, 2021
57574d5
Legacy Widget: Add 'No preview available' placeholder
noisysocks Apr 16, 2021
937cb36
Legacy Widget: Update form and 'No preview available' placeholder sty…
noisysocks Apr 16, 2021
08a4bb2
Add border to Legacy Widget form
noisysocks Apr 20, 2021
e3d40b0
Legacy Widget: Move useForm() up a level
noisysocks Apr 20, 2021
dab5776
Legacy Widget: Avoid unmounting preview so that transition is smooth
noisysocks Apr 20, 2021
5df66a6
Hide NoPreview when not selected
noisysocks Apr 20, 2021
4823aa6
Don't show hover effect when Legacy Widget is loading
noisysocks Apr 20, 2021
ab576b1
Fix jumping text in NoPreview
noisysocks Apr 20, 2021
7e1cce2
Improve placeholder state
noisysocks Apr 20, 2021
c5489a4
Show placeholder while preview iframe is loading so there's no jankiness
noisysocks Apr 20, 2021
7b25133
Fix Cancel not clearing out changes
noisysocks Apr 20, 2021
762ce49
Add margin between label and input for most legacy widgets
noisysocks Apr 21, 2021
5d13c96
Move Apply & Cancel buttons to 'other' group
noisysocks Apr 21, 2021
d180193
Tidy up LegacyWidgetInspectorCard
noisysocks Apr 21, 2021
37d88e3
Fix PHP tests
noisysocks Apr 21, 2021
a661ea1
Fix block inspector not opening when block is selected
noisysocks Apr 22, 2021
7205c09
Fix multisite PHP unit tests
noisysocks Apr 22, 2021
88e3ff7
Remove Apply / Cancel flow
noisysocks Apr 20, 2021
8938af8
Update Legacy Widget E2E test
noisysocks Apr 26, 2021
ef77899
Remove content.key
noisysocks Apr 27, 2021
4491cc5
Use innerText to determine if preview is empty
noisysocks Apr 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 53 additions & 18 deletions lib/class-wp-rest-widget-types-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,58 @@ public function encode_form_data( $request ) {
);
}

$serialized_instance = serialize( $instance );

$response = array(
'form' => trim(
$this->get_widget_form(
$widget_object,
$instance
)
),
'preview' => trim(
$this->get_widget_preview(
$widget_object,
$instance
)
),
'instance' => array(
'encoded' => base64_encode( $serialized_instance ),
'hash' => wp_hash( $serialized_instance ),
),
);

if ( ! empty( $widget_object->show_instance_in_rest ) ) {
// Use new stdClass so that JSON result is {} and not [].
$response['instance']['raw'] = empty( $instance ) ? new stdClass : $instance;
}

return rest_ensure_response( $response );
}

/**
* Returns the output of WP_Widget::widget() when called with the provided
* instance. Used by encode_form_data() to preview a widget.
* @param WP_Widget $widget_object Widget object to call widget() on.
* @param array $instance Widget instance settings.
* @return string
*/
private function get_widget_preview( $widget_object, $instance ) {
ob_start();
the_widget( get_class( $widget_object ), $instance );
return ob_get_clean();
}

/**
* Returns the output of WP_Widget::form() when called with the provided
* instance. Used by encode_form_data() to preview a widget's form.
*
* @param WP_Widget $widget_object Widget object to call widget() on.
* @param array $instance Widget instance settings.
* @return string
*/
private function get_widget_form( $widget_object, $instance ) {
ob_start();

/** This filter is documented in wp-includes/class-wp-widget.php */
Expand All @@ -475,24 +527,7 @@ public function encode_form_data( $request ) {
);
}

$form = ob_get_clean();

$serialized_instance = serialize( $instance );

$response = array(
'form' => trim( $form ),
'instance' => array(
'encoded' => base64_encode( $serialized_instance ),
'hash' => wp_hash( $serialized_instance ),
),
);

if ( ! empty( $widget_object->show_instance_in_rest ) ) {
// Use new stdClass so that JSON result is {} and not [].
$response['instance']['raw'] = empty( $instance ) ? new stdClass : $instance;
}

return rest_ensure_response( $response );
return ob_get_clean();
}

/**
Expand Down
140 changes: 17 additions & 123 deletions packages/block-library/src/legacy-widget/edit/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,12 @@ import { debounce } from 'lodash';
/**
* WordPress dependencies
*/
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { __ } from '@wordpress/i18n';
import {
useEffect,
useRef,
useState,
useCallback,
RawHTML,
} from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { useEffect, useRef, useCallback, RawHTML } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';

export default function Form( { id, idBase, instance, setInstance } ) {
const { html, setFormData } = useForm( {
id,
idBase,
instance,
setInstance,
} );

export default function Form( { id, idBase, content, setFormData } ) {
const setFormDataDebounced = useCallback( debounce( setFormData, 300 ), [
setFormData,
] );
Expand All @@ -36,127 +20,37 @@ export default function Form( { id, idBase, instance, setInstance } ) {
<Control
id={ id }
idBase={ idBase }
html={ html }
content={ content }
onChange={ setFormDataDebounced }
onSave={ setFormData }
// Force a remount when the widget's form HTML changes. This clears
// out any mutations to the DOM that widget scripts have made.
key={ content }
/>
);
}

function useForm( { id, idBase, instance, setInstance } ) {
const isStillMounted = useRef( false );
const [ html, setHTML ] = useState( null );
const [ formData, setFormData ] = useState( null );

useEffect( () => {
isStillMounted.current = true;
return () => ( isStillMounted.current = false );
}, [] );

const { createNotice } = useDispatch( noticesStore );

useEffect( () => {
const performFetch = async () => {
if ( id ) {
// Updating a widget that does not extend WP_Widget.
try {
let widget;
if ( formData ) {
widget = await apiFetch( {
path: `/wp/v2/widgets/${ id }?context=edit`,
method: 'PUT',
data: {
form_data: formData,
},
} );
} else {
widget = await apiFetch( {
path: `/wp/v2/widgets/${ id }?context=edit`,
method: 'GET',
} );
}
if ( isStillMounted.current ) {
setHTML( widget.rendered_form );
}
} catch ( error ) {
createNotice(
'error',
error?.message ??
__( 'An error occured while updating the widget.' )
);
}
} else if ( idBase ) {
// Updating a widget that extends WP_Widget.
try {
const response = await apiFetch( {
path: `/wp/v2/widget-types/${ idBase }/encode`,
method: 'POST',
data: {
instance,
form_data: formData,
},
} );
if ( isStillMounted.current ) {
setInstance( response.instance );
// Only set HTML the first time so that we don't cause a
// focus loss by remounting the form.
setHTML(
( previousHTML ) => previousHTML ?? response.form
);
}
} catch ( error ) {
createNotice(
'error',
error?.message ??
__( 'An error occured while updating the widget.' )
);
}
}
};
performFetch();
}, [
id,
idBase,
setInstance,
formData,
// Do not trigger when `instance` changes so that we don't make two API
// requests when there is form input.
] );

return { html, setFormData };
}

function Control( { id, idBase, html, onChange, onSave } ) {
function Control( { id, idBase, content, onChange, onSave } ) {
const controlRef = useRef();
const formRef = useRef();

// Trigger 'widget-added' when widget is ready and 'widget-updated' when
// widget changes. This event is what widgets' scripts use to initialize,
// attach events, etc. The event must be fired using jQuery's event bus as
// this is what widget scripts expect. If jQuery is not loaded, do nothing -
// some widgets will still work regardless.
const hasBeenAdded = useRef( false );
// Trigger 'widget-added' when widget is ready. This event is what widgets'
// scripts use to initialize, attach events, etc. The event must be fired
// using jQuery's event bus as this is what widget scripts expect. If jQuery
// is not loaded, do nothing - some widgets will still work regardless.
useEffect( () => {
if ( ! window.jQuery ) {
return;
}

const { jQuery: $ } = window;

if ( html ) {
$( document ).trigger(
hasBeenAdded.current ? 'widget-updated' : 'widget-added',
[ $( controlRef.current ) ]
);
hasBeenAdded.current = true;
if ( content ) {
$( document ).trigger( 'widget-added', [
$( controlRef.current ),
] );
}
}, [
html,
// Include id and idBase in the deps so that widget-updated is triggered
// if they change.
id,
idBase,
] );
}, [ content ] );

// Prefer jQuery 'change' event instead of the native 'change' event because
// many widgets use jQuery's event bus to trigger an update.
Expand Down Expand Up @@ -230,7 +124,7 @@ function Control( { id, idBase, html, onChange, onSave } ) {
className="add_new"
value=""
/>
<RawHTML className="widget-content">{ html }</RawHTML>
<RawHTML className="widget-content">{ content }</RawHTML>
{ id && (
<Button type="submit" isPrimary>
{ __( 'Save' ) }
Expand Down
Loading