WordPress.org

Make WordPress Core

Opened 4 years ago

Closed 3 years ago

Last modified 3 years ago

#31320 closed enhancement (fixed)

Make customizer JavaScript API available during the live preview

Reported by: Fab1en Owned by:
Milestone: 4.5 Priority: normal
Severity: normal Version: 4.1
Component: Customize Keywords:
Focuses: javascript Cc:

Description

[30102] introduced some handy JS methods to deal with the customizer controls, sections and panel. However, it seams to me that those new shiny methods are not available on the live preview side, within the iframe.

This JS code is loaded in the preview with the customize_preview_init hook :

// Update the site title
wp.customize( 'blogname', function( value ) {
	value.bind( function( newval ) {
		$( '#accueil > h1' ).html( newval );
	} );
} );

and it works great. But then I want to do this :

// link elements to their customizer control
$( '#accueil > h1' ).click(function(){
	wp.customize.control( 'blogname' ).focus()
});

and I get a JS error Uncaught TypeError: undefined is not a function

But, the API is available from the parent frame :

// link elements to their customizer control
$( '#accueil > h1' ).click(function(){
	frame.top.wp.customize.control( 'blogname' ).focus()
});

This DOES work and has the expected behavior (which is really cool !).

Wouldn't it be possible to pass the entire API to the preview iframe ?

Change History (15)

#1 @Aniruddh
4 years ago

  • Keywords 2nd-opinion added

If you have your JS loaded via customize_control_init hook then the wp object should be available in your JS file.

#2 follow-ups: @Fab1en
4 years ago

Yes, wp object is here, wp.customize is defined but wp.customize.control is not, whereas frame.top.wp.customize.control is.
So simply copying the whole customizer API to the passed wp.customize object would be fine.

#3 in reply to: ↑ 2 @Aniruddh
4 years ago

  • Resolution set to worksforme
  • Status changed from new to closed

Replying to Fab1en:

Yes, wp object is here, wp.customize is defined but wp.customize.control is not

I can access wp.customize.control in the JS file I enqueued via customize_control_init hook.

#4 @celloexpressions
4 years ago

  • Keywords needs-patch added; 2nd-opinion removed
  • Resolution worksforme deleted
  • Status changed from closed to reopened

customize_controls_init is for the controls (the Customizer window). This ticket is about making some of that available in the Customizer preview window (the front-end, essentially), where scripts implement live-previewing functionality customize-preview-init.

This seems related to #29288 and like a good idea, but I'd defer to @westonruter in terms of implementation details. Also not sure how Customizer Transactions may impact this.

#5 in reply to: ↑ 2 ; follow-up: @westonruter
4 years ago

Replying to Fab1en:

Yes, wp object is here, wp.customize is defined but wp.customize.control is not, whereas frame.top.wp.customize.control is.
So simply copying the whole customizer API to the passed wp.customize object would be fine.

There are a lot of objects that get created in the Customizer pane: objects for settings, controls, sections, and panels. It would be a waste to have these duplicated in two separate window objects. I think we would be good to eliminate the copies of the values in the preview as well. So this is absolutely related to #29288. Transactions wouldn't change anything here.

For the example provided of focusing on a control, there was a specific implementation of this done for widgets in the Customizer: you can shift+click on a widget in the preview and it will focus on the widget control in the pane. When clicking in the preview, it does this:

wp.customize.preview.send( 'focus-widget-control', widgetId );

And then in the Customizer pane it does:

wp.customize.previewer.bind( 'focus-widget-control', wp.customize.Widgets.focusWidgetFormControl );

So this is an ad hoc way of accessing methods on the objects in the parent. It's easy to implement.

If we wanted a generalized framework for accessing parent methods we could do something like the following in the Customize preview:

var commandCallbacks = {}, commandId = 0;
wp.customize.parentProxyCommand = function ( objectType, objectId, method, args, callback ) {
        commandId += 1;
        commandCallbacks[ commandId ] = callback;
        wp.customize.preview.send( 'customize-parent-proxy-command', objectType, objectId, args, commandId );
};
wp.customize.preview.bind( 'customize-parent-proxy-command', function ( commandId, retval ) {
        var callback = commandCallbacks[ commandId ];
        if ( callback ) {
                callback( retval );
                delete commandCallbacks[ commandId ];
        }
} );

And then in the Customize pane:

wp.customize.previewer.bind( 'customize-parent-proxy-command', function ( commandId, objectType, objectId, method, args ) {
        wp.customize[ objectType ]( id, function ( object ) {
                var retval = object[ method ].apply( object, args );
                wp.customize.previewer.send( 'customize-parent-proxy-response', commandId, retval );
        } );
} );

Then to call the focus method on the pane's blogname control from within the preview, you could do:

wp.customize.parentProxyCommand( 'control', 'blogname', 'focus' );

You could also get the return value and pass arguments to whatever method you call as well. Obviously the JS API could be nicer here :-) Too many positional parameters. We could even implement the same wp.customize.control( 'blogname' ).focus() interface in the preview, but it would not refer to any blogname located within the preview, but would instead call the above asynchronous postMessage logic.

#6 follow-up: @Fab1en
4 years ago

Thanks @westonruter for this detailed explanation, and sorry for the response delay. I have finally managed to take some time to dive into this, and still cannot achieve what I want.

In the example you pointed, there is a monkey patch that makes all the magic happen :

/**
* Capture the instance of the Preview since it is private
*/
OldPreview = api.Preview;
api.Preview = OldPreview.extend( {
    initialize: function( params, options ) {
        api.WidgetCustomizerPreview.preview = this;
        OldPreview.prototype.initialize.call( this, params, options );
    }
} );

But in my case, wp.customize.preview is undefined at first. If I wrap the command into a wp.customize() call, it is defined but has no effect.

wp.customize( 'blogname', function( value ) {
    wp.customize.preview.send( 'please-do-this-for-me', 'first-param' );
} );

Doing step-by-step into the JavaScript call, I found that the event is sent but never received by the customizer pane. In api.Messenger.receive, this.targetWindow() is null so the message is not posted. What's wrong ?

If I add a timeout, then things begin to work :

setTimeout( function( ) {
    wp.customize.preview.send( 'please-do-this-for-me', 'first-param' );
}, 2000 );

Which event should I listen in the preview iframe to be sure that the customizer API is fully functional ?

#7 in reply to: ↑ 6 ; follow-up: @ocean90
4 years ago

Replying to Fab1en:

Which event should I listen in the preview iframe to be sure that the customizer API is fully functional ?

[30891] has introduced a preview-ready event which you can use.
Related: #30890, [30893].

#8 in reply to: ↑ 7 @Fab1en
4 years ago

Replying to ocean90:

[30891] has introduced a preview-ready event which you can use.

Ok, this event can tell me when wp.customize.preview is defined. But more time is needed to initialize the listening part of the logic.

In the customizer pane, I have this :

wp.customize.previewer.bind( 'please-do-this-for-me', function(params){
    console.log('OK, I\'ll do it', params);
} );

If I don't put a timer and send the event upon preview-ready event, it is not received.

wp.customize.bind( 'preview-ready', function(){
    wp.customize.preview.send( 'please-do-this-for-me', 'first-param' );
});

Is it possible that my event listening gets removed before preview is ready ?

#9 @westonruter
4 years ago

@Fab1en Try listening for the active message sent from the parent, as was implemented in [33134], so something like:

wp.customize.preview.bind( 'active', function() {
    wp.customize.preview.send( 'please-do-this-for-me', 'first-param' );
});

#10 in reply to: ↑ 5 @sidati
3 years ago

Thank you @westonruter very mush,
your code was very helpful and enlighten, but please correct this "typo", you want to bind the customize-parent-proxy-response not customize-parent-proxy-command

<?php
wp.customize.preview.bind( 'customize-parent-proxy-RESPONSE', function ( commandId, retval ) {
        var callback = commandCallbacks[ commandId ];
        if ( callback ) {
                callback( retval );
                delete commandCallbacks[ commandId ];
        }
} );

Also the order of the variables is incorrect and you mis-placed the code, the code you said is for Previewer is actually for the customiser pane and vice versa.

Again thanks for taking time to write a such code.

Sidati,
Regards

#11 @sidati
3 years ago

  • Focuses performance added

@westonruter,
As i see in the code the widgets/sidebars are initialized inside the API ready event, so its impossible (without using setTimeout) to bind an action to the API ready event, after the widgets/sidebars are ready because they are attached to the same event ready.

So is there any plan to add another event after widgets/sidebars are initialized and ready maybe a widgets-ready or sidebars-ready event :).

And as temporary solution, can you please provide a perfect duration to set it up in the setTimeout function.

Regards,
Sidati

#12 @westonruter
3 years ago

  • Focuses performance removed
  • Keywords needs-patch removed
  • Milestone changed from Awaiting Review to 4.5
  • Resolution set to fixed
  • Status changed from reopened to closed

@sidati Hi, there aren't any plans to add a widgets-ready or sidebars-ready event. I don't see a use case for why it would be necessary. If you want to ensure that your code fires after widgets are initialized, you can do:

wp.customize.bind( 'ready', _.defer( function() { /* your code here */ } ) );

@Fab1en: Also, your original use case of wanting to focus on a control from the preview is not generally available by sending a message from the preview like this:

wp.customize.preview.send( 'focus-control-for-setting', 'blogname' );

Support for sending this message was added in [36586] for #27355 in the implementation of selective refresh. Shift-clicking on any partial registered for selective refresh will send this focus-control-for-setting automatically. But you can use it yourself manually outside the context of partials.

#13 in reply to: ↑ description @westonruter
3 years ago

To respond to your last question:

Replying to Fab1en:

Wouldn't it be possible to pass the entire API to the preview iframe ?

I think it makes more sense to expose the interfaces needed from the pane via message passing as was done via focus-control-for-setting.

#14 @sidati
3 years ago

How about adding an event when the customize pane is fully rendered, i believe this is good idea, cause in my code i need to hide some sidebars sections, and it seems they rendered too late even with the _.defer function. the only way i get it work is with a setTimeout with 5000ms delay, and i'm afraid this duration depends how mush settings/widgets are existed.

Also, have a look at this comment : https://core.trac.wordpress.org/ticket/31320#comment:10.

#15 @westonruter
3 years ago

@sidati I think you should take a different approach, to listen for when the relevant section is added, and then act appropriately. Though I'm not totally sure about what you're looking to accomplish, here's an example this code added to the pane:

wp.customize.previewer.bind( 'disable-sidebar', function( sidebarId ) {
    // The following code will execute once the section exists.
    wp.customize.section( 'sidebar-widgets-' + sidebarId, function( section ) { 
        section.active.set( false );
    } );
} );

This wouldn't depend on any ready event. Just send the disable-sidebar message after the preview is active. Alternatively, you can do this also from PHP at a lower level, by filtering customize_section_active.

In any case, I suggest you continue any further discussion via the WordPress support forums, the WP stack exchange, or create a new ticket if there turns out to be an issue with the Customizer platform. Thanks.

Note: See TracTickets for help on using tickets.