Actualizar el formulario del widget después de arrastrar y soltar (error de guardado de WP)

15

Publiqué un informe de error sobre esto hace unos meses ( en WordPress trac (Error de actualización de formulario de instancia de widget) ) y pensé en intentar escribir sobre él aquí también. Quizás alguien tenga una mejor solución a este problema que yo.

Básicamente, el problema es que si suelta un widget en una barra lateral, el formulario del widget no se actualiza hasta que presiona guardar manualmente (o vuelve a cargar la página).

Esto hace inutilizable todo el código de la form()función que se basa en el ID de la instancia del widget para hacer algo (hasta que presione el botón Guardar). Cualquier cosa como solicitudes de ajax, cosas de jQuery como selectores de color, etc., no funcionarán de inmediato, porque desde esa función parecería que la instancia del widget aún no se ha inicializado.

Una solución sucia sería activar el botón Guardar automáticamente usando algo como livequery :

$("#widgets-right .needfix").livequery(function(){
  var widget = $(this).closest('div.widget');
  wpWidgets.save(widget, 0, 1, 0);
  return false;
});

y agregue la .needfixclase form()si la instancia del widget no parece inicializada:

 <div <?php if(!is_numeric($this->number)): ?>class="needfix"<?php endif; ?>
   ...
 </div>

Un inconveniente de esta solución es que si tiene muchos widgets registrados, el navegador consumirá mucha CPU, porque livequery verifica los cambios de DOM cada segundo (aunque no lo probé específicamente, es solo mi suposición :)

¿Alguna sugerencia para una mejor manera de corregir el error?

Pony de un solo truco
fuente
En lugar de activar un envío de guardado completo, ¿no tendría más sentido mirar dentro de lo que al presionar el botón Guardar se activa para proporcionar la identificación necesaria, separar ese código y llamarlo al final de la operación de eliminación?
Hakre

Respuestas:

5

Luché con una situación similar recientemente. ¡Ajax en widgets no es broma! Necesito escribir un código bastante loco para que las cosas funcionen en todas las instancias. No estoy familiarizado con la consulta en vivo, pero si dice que comprueba el DOM cada segundo, podría tener una solución menos intensa para usted:

var get_widget_id = function ( selector ) {
    var selector, widget_id = false;
    var id_attr = $( selector ).closest( 'form' ).find( 'input[name="widget-id"]' ).val();
    if ( typeof( id_attr ) != 'undefined' ) {
        var parts = id_attr.split( '-' );
        widget_id = parts[parts.length-1];
    }
    return parseInt( widget_id );
};

Puede pasar esta función un selector o un objeto jQuery y devolverá el ID de instancia de la instancia actual. No pude encontrar otra forma de solucionar este problema. Me alegra saber que no soy el único :)

mfields
fuente
7

No me gusta responder mi propia pregunta, pero creo que esta es la mejor solución hasta ahora:

$('#widgets-right').ajaxComplete(function(event, XMLHttpRequest, ajaxOptions){

  // determine which ajax request is this (we're after "save-widget")
  var request = {}, pairs = ajaxOptions.data.split('&'), i, split, widget;

  for(i in pairs){
    split = pairs[i].split('=');
    request[decodeURIComponent(split[0])] = decodeURIComponent(split[1]);
  }

  // only proceed if this was a widget-save request
  if(request.action && (request.action === 'save-widget')){

    // locate the widget block
    widget = $('input.widget-id[value="' + request['widget-id'] + '"]').parents('.widget');

    // trigger manual save, if this was the save request 
    // and if we didn't get the form html response (the wp bug)
    if(!XMLHttpRequest.responseText)
      wpWidgets.save(widget, 0, 1, 0);

    // we got an response, this could be either our request above,
    // or a correct widget-save call, so fire an event on which we can hook our js
    else
      $(document).trigger('saved_widget', widget);

  }

});

Esto activará la solicitud ajax de guardar widget, justo después de que se haya completado una solicitud de guardar widget (si no hubo respuesta con el formulario html).

Necesita ser agregado en la jQuery(document).ready()función.

Ahora, si desea volver a adjuntar fácilmente sus funciones de JavaScript a los nuevos elementos DOM agregados por la función de formulario de widget, simplemente vincúlelos al evento "saved_widget":

$(document).bind('saved_widget', function(event, widget){
  // For example: $(widget).colorpicker() ....
});
Pony de un solo truco
fuente
3
Tenga en cuenta que a partir de jQuery 1.8, el método .ajaxComplete () solo debe adjuntarse al documento. - api.jquery.com/ajaxComplete Por lo tanto, la primera línea de su fragmento debe leer: $ (document) .ajaxComplete (function (event, XMLHttpRequest, ajaxOptions) {Al menos para WP 3.6+
David Laing
3

Me encontré con esto recientemente y parece que en la interfaz tradicional "widgets.php" cualquier inicialización de JavaScript debe ejecutarse directamente para los widgets existentes (aquellos en el #widgets-rightdiv), e indirectamente a través del widget-addedevento para widgets recién agregados; mientras que en la interfaz "personalizar.php" del personalizador, todos los widgets, existentes y nuevos, envían el widget-addedevento, por lo que solo se pueden inicializar allí. En base a esto, la siguiente es una extensión de la WP_Widgetclase que facilita la adición de la inicialización de JavaScript al formulario de un widget anulando una función form_javascript_init():

class WPSE_JS_Widget extends WP_Widget { // For widgets using javascript in form().
    var $js_ns = 'wpse'; // Javscript namespace.
    var $js_init_func = ''; // Name of javascript init function to call. Initialized in constructor.
    var $is_customizer = false; // Whether in customizer or not. Set on 'load-customize.php' action (if any).

    public function __construct( $id_base, $name, $widget_options = array(), $control_options = array(), $js_ns = '' ) {
        parent::__construct( $id_base, $name, $widget_options, $control_options );
        if ( $js_ns ) {
            $this->js_ns = $js_ns;
        }
        $this->js_init_func = $this->js_ns . '.' . $this->id_base . '_init';
        add_action( 'load-widgets.php', array( $this, 'load_widgets_php' ) );
        add_action( 'load-customize.php', array( $this, 'load_customize_php' ) );
    }

    // Called on 'load-widgets.php' action added in constructor.
    public function load_widgets_php() {
        add_action( 'in_widget_form', array( $this, 'form_maybe_call_javascript_init' ) );
        add_action( 'admin_print_scripts', array( $this, 'admin_print_scripts' ), PHP_INT_MAX );
    }

    // Called on 'load-customize.php' action added in constructor.
    public function load_customize_php() {
        $this->is_customizer = true;
        // Don't add 'in_widget_form' action as customizer sends 'widget-added' event to existing widgets too.
        add_action( 'admin_print_scripts', array( $this, 'admin_print_scripts' ), PHP_INT_MAX );
    }

    // Form javascript initialization code here. "widget" and "widget_id" available.
    public function form_javascript_init() {
    }

    // Called on 'in_widget_form' action (ie directly after form()) when in traditional widgets interface.
    // Run init directly unless we're newly added.
    public function form_maybe_call_javascript_init( $callee_this ) {
        if ( $this === $callee_this && '__i__' !== $this->number ) {
            ?>
            <script type="text/javascript">
            jQuery(function ($) {
                <?php echo $this->js_init_func; ?>(null, $('#widgets-right [id$="<?php echo $this->id; ?>"]'));
            });
            </script>
            <?php
        }
    }

    // Called on 'admin_print_scripts' action added in constructor.
    public function admin_print_scripts() {
        ?>
        <script type="text/javascript">
        var <?php echo $this->js_ns; ?> = <?php echo $this->js_ns; ?> || {}; // Our namespace.
        jQuery(function ($) {
            <?php echo $this->js_init_func; ?> = function (e, widget) {
                var widget_id = widget.attr('id');
                if (widget_id.search(/^widget-[0-9]+_<?php echo $this->id_base; ?>-[0-9]+$/) === -1) { // Check it's our widget.
                    return;
                }
                <?php $this->form_javascript_init(); ?>
            };
            $(document).on('widget-added', <?php echo $this->js_init_func; ?>); // Call init on widget add.
        });
        </script>
        <?php
    }
}

Un widget de prueba de ejemplo que usa esto:

class WPSE_Test_Widget extends WPSE_JS_Widget {
    var $defaults; // Form defaults. Initialized in constructor.

    function __construct() {
        parent::__construct( 'wpse_test_widget', __( 'WPSE: Test Widget' ), array( 'description' => __( 'Test init of javascript.' ) ) );
        $this->defaults = array(
            'one' => false,
            'two' => false,
            'color' => '#123456',
        );
        add_action( 'admin_enqueue_scripts', function ( $hook_suffix ) {
            if ( ! in_array( $hook_suffix, array( 'widgets.php', 'customize.php' ) ) ) return;
            wp_enqueue_script( 'wp-color-picker' ); wp_enqueue_style( 'wp-color-picker' );
        } );
    }

    function widget( $args, $instance ) {
        extract( $args );
        extract( wp_parse_args( $instance, $this->defaults ) );

        echo $before_widget, '<p style="color:', $color, ';">', $two ? 'Two' : ( $one ? 'One' : 'None' ), '</p>', $after_widget;
    }

    function update( $new_instance, $old_instance ) {
        $new_instance['one'] = isset( $new_instance['one'] ) ? 1 : 0;
        $new_instance['two'] = isset( $new_instance['two'] ) ? 1 : 0;
        return $new_instance;
    }

    function form( $instance ) {
        extract( wp_parse_args( $instance, $this->defaults ) );
        ?>
        <div class="wpse_test">
            <p class="one">
                <input class="checkbox" type="checkbox" <?php checked( $one ); disabled( $two ); ?> id="<?php echo $this->get_field_id( 'one' ); ?>" name="<?php echo $this->get_field_name( 'one' ); ?>" />
                <label for="<?php echo $this->get_field_id( 'one' ); ?>"><?php _e( 'One?' ); ?></label>
            </p>
            <p class="two">
                <input class="checkbox" type="checkbox" <?php checked( $two ); disabled( $one ); ?> id="<?php echo $this->get_field_id( 'two' ); ?>" name="<?php echo $this->get_field_name( 'two' ); ?>" />
                <label for="<?php echo $this->get_field_id( 'two' ); ?>"><?php _e( 'Two?' ); ?></label>
            </p>
            <p class="color">
                <input type="text" value="<?php echo htmlspecialchars( $color ); ?>" id="<?php echo $this->get_field_id( 'color' ); ?>" name="<?php echo $this->get_field_name( 'color' ); ?>" />
            </p>
        </div>
        <?php
    }

    // Form javascript initialization code here. "widget" and "widget_id" available.
    function form_javascript_init() {
        ?>
            $('.one input', widget).change(function (event) { $('.two input', widget).prop('disabled', this.checked); });
            $('.two input', widget).change(function (event) { $('.one input', widget).prop('disabled', this.checked); });
            $('.color input', widget).wpColorPicker({
                <?php if ( $this->is_customizer ) ?> change: _.throttle( function () { $(this).trigger('change'); }, 1000, {leading: false} )
            });
        <?php
    }
}

add_action( 'widgets_init', function () {
    register_widget( 'WPSE_Test_Widget' );
} );
bonger
fuente
2

Creo que existe algo en Wordpress 3.9 que podría ayudarte. Es la devolución de llamada actualizada por widget . Úselo así (coffeescript):

$(document).on 'widget-updated', (event, widget) ->
    doWhatINeed() if widget[0].id.match(/my_widget_name/)
Tyler Collier
fuente