Mettre à jour le formulaire de widget après un glisser-déposer (bug de sauvegarde WP)

15

J'ai publié un rapport de bug à ce sujet il y a quelques mois ( sur WordPress trac (Widget Instance Form Update Bug) ) 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.

Fondamentalement, le problème est que si vous déposez un widget dans une barre latérale, le formulaire de widget n'est pas mis à jour jusqu'à ce que vous appuyiez manuellement sur Enregistrer (ou recharger la page).

Cela rend inutilisable tout le code de la form()fonction qui s'appuie sur l'ID d'instance du widget pour faire quelque chose (jusqu'à ce que vous appuyiez sur le bouton Enregistrer). Toutes les choses comme les requêtes ajax, les choses jQuery comme les sélecteurs de couleurs et ainsi de suite ne fonctionneront pas immédiatement, car à partir de cette fonction, il semblerait que l'instance de widget n'a pas encore été initialisée.

Une mauvaise solution serait de déclencher automatiquement le bouton d'enregistrement 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 .needfixclasse 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 enregistré de nombreux widgets, le navigateur consommera beaucoup de CPU, car Livequery vérifie les changements DOM chaque seconde (même si je n'ai pas spécifiquement testé cela, c'est juste mon hypothèse :)

Des suggestions pour une meilleure façon de corriger le bogue?

onetrickpony
la source
Au lieu de déclencher une soumission de sauvegarde complète, ne serait-il pas plus logique de regarder à l'intérieur ce qui déclenche le bouton Enregistrer pour fournir l'ID nécessaire, séparer ce code et l'appeler à la fin de l'opération de suppression?
hakre

Réponses:

5

Je me suis récemment battu avec une situation similaire. 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 la requête en direct, mais si vous dites qu'elle vérifie le DOM à chaque seconde, je pourrais avoir une solution moins intense pour vous:

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 sélecteur ou un objet jQuery et elle renverra l'ID d'instance de l'instance actuelle. Je ne pouvais pas trouver d'autre moyen de contourner ce problème. Heureux d'entendre que je ne suis pas le seul :)

mfields
la source
7

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 requête ajax de sauvegarde de widget, juste après la fin d'une requête de sauvegarde de widget (s'il n'y a pas eu de réponse avec le formulaire html).

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

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

$(document).bind('saved_widget', function(event, widget){
  // For example: $(widget).colorpicker() ....
});
onetrickpony
la source
3
Notez qu'à partir de jQuery 1.8, la méthode .ajaxComplete () ne doit être attachée qu'au document. - api.jquery.com/ajaxComplete Ainsi, la première ligne de votre extrait doit se lire: $ (document) .ajaxComplete (fonction (événement, XMLHttpRequest, ajaxOptions) {Au moins pour WP 3.6+
David Laing
3

Ran dans récemment et il semble que dans l'interface traditionnelle "widgets.php" toute initialisation javascript devrait être exécutée directement pour les widgets existants (ceux de la #widgets-rightdiv), et indirectement via l' widget-addedévénement pour les widgets nouvellement ajoutés; alors que dans l'interface de personnalisation "custom.php", tous les widgets - existants et nouveaux - sont envoyés à l' widget-addedévénement et peuvent donc simplement être initialisés à cet endroit. Sur la base de cela, voici une extension de la WP_Widgetclasse qui facilite l'ajout de l'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' );
} );
bonger
la source
2

Je pense qu'il existe quelque chose dans Wordpress 3.9 qui pourrait vous aider. C'est le rappel mis à jour du widget . Utilisez-le comme ceci (coffeescript):

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