web-dev-qa-db-fra.com

Mise à jour du formulaire de widget après un glisser-déposer (bogue d'enregistrement de WP)

J'ai posté un rapport de bogue à ce sujet il y a quelques mois ( sur WordPress trac (bug de mise à jour du formulaire d'instance de widget) ) et j'ai pensé que j'essaierais d'écrire à ce sujet ici aussi. Peut-être que quelqu'un a une meilleure solution à ce problème que moi.

En gros, le problème est que, si vous déposez un widget dans une barre latérale, le formulaire de widget ne sera pas mis à jour tant que vous n’aurez pas appuyé sur Enregistrer (ou rechargé la page).

Cela rend inutilisable tout le code de la fonction form() qui s'appuie sur l'ID d'instance du widget pour faire quelque chose (jusqu'à ce que vous appuyiez sur le bouton d'enregistrement). Les éléments tels que les demandes ajax, les éléments jQuery tels que colorpickers, etc., ne fonctionneront pas tout de suite, car il apparaît de cette fonction que l'instance de widget n'a pas encore été initialisée.

Un correctif peut consister à déclencher automatiquement le bouton de sauvegarde en utilisant quelque chose comme livequery :

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

et ajoutez la classe .needfix dans form() si l'instance de widget ne semble pas initialisée:

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

Un inconvénient de cette solution est que si vous avez beaucoup de widgets enregistrés, le navigateur consommera beaucoup de ressources processeur, car livequery vérifie les changements de DOM toutes les secondes (bien que je n’aie pas testé cela de manière spécifique, c’est juste mon hypothèse :)

Des suggestions pour un meilleur moyen de corriger le bogue?

14
onetrickpony

Je me suis battu avec une situation similaire récemment. Ajax dans les widgets n'est pas une blague! Besoin d'écrire du code assez fou pour que les choses fonctionnent entre les instances. Je ne connais pas bien la requête en direct, mais si vous dites qu'il vérifie le DOM toutes les secondes, j'aurais peut-être une solution moins intense:

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 );
};

Vous pouvez transmettre à cette fonction un objet sélecteur ou jQuery qui renverra l'ID d'instance de l'instance actuelle. Je ne pouvais trouver aucune autre solution à ce problème. Content d'entendre que je ne suis pas le seul :)

5
mfields

Je n'aime pas répondre à ma propre question, mais je pense que c'est la meilleure solution jusqu'à présent:

$('#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);

  }

});

Cela déclenchera la demande ajax de widget-save, juste après la fin d'une demande de sauvegarde de widget (s'il n'y a pas eu de réponse sous la forme html).

Il doit être ajouté dans la fonction jQuery(document).ready().

Désormais, si vous souhaitez réattacher facilement vos fonctions javascript aux nouveaux éléments DOM ajoutés par la fonction de formulaire de widget, il vous suffit de les lier à l'événement "saved_widget":

$(document).bind('saved_widget', function(event, widget){
  // For example: $(widget).colorpicker() ....
});
7
onetrickpony

Récemment, il semble que, dans l'interface traditionnelle "widgets.php", toute initialisation javascript devrait être exécutée directement pour les widgets existants (ceux du code #widgets-right div) et indirectement via l'événement widget-added pour les widgets récemment ajoutés; alors que dans l'interface de personnalisation "personnaliser.php" tous les widgets - existants et nouveaux - reçoivent l'événement widget-added afin qu'ils puissent simplement y être initialisés. Voici une extension de la classe WP_Widget qui facilite l'ajout d'initialisation javascript au formulaire d'un widget en remplaçant une fonction, 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 exemple de widget de test utilisant ceci:

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' );
} );
3
bonger

Je pense que quelque chose existe dans Wordpress 3.9 qui pourrait vous aider. C'est le widget-updated callback. Utilisez-le comme ceci (coffeescript):

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