Perbarui formulir widget setelah seret-dan-jatuhkan (bug penyimpan WP)

15

Saya telah memposting laporan bug tentang ini beberapa bulan yang lalu ( di WordPress trac (Widget Instance Form Update Bug) ) dan saya pikir saya akan mencoba menulisnya juga di sini. Mungkin seseorang memiliki solusi yang lebih baik untuk masalah ini daripada saya.

Pada dasarnya masalahnya adalah bahwa jika Anda menjatuhkan widget ke bilah sisi, bentuk widget tidak diperbarui sampai Anda secara manual menekan save (atau memuat ulang halaman).

Ini membuat semua kode dari form()fungsi yang tidak dapat digunakan bergantung pada ID instance widget untuk melakukan sesuatu (sampai Anda menekan tombol simpan). Segala hal seperti permintaan ajax, hal-hal jQuery seperti colorpickers dan sebagainya tidak akan langsung berfungsi, karena dari fungsi itu akan tampak bahwa instance widget belum diinisialisasi.

Perbaikan kotor akan memicu tombol simpan secara otomatis menggunakan sesuatu seperti livequery :

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

dan tambahkan .needfixkelas form()jika instance widget tidak terlihat diinisialisasi:

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

Salah satu kelemahan dari solusi ini adalah bahwa jika Anda memiliki banyak widget terdaftar, browser akan memakan banyak CPU, karena livequery memeriksa perubahan DOM setiap detik (tho saya tidak secara khusus menguji ini, itu hanya asumsi saya :)

Ada saran untuk cara yang lebih baik untuk memperbaiki bug?

onetrickpony
sumber
Alih-alih memicu pengiriman lengkap, bukankah lebih masuk akal untuk melihat ke dalam apa yang menekan tombol simpan untuk memberikan ID yang diperlukan, pisahkan kode itu dan panggil itu di akhir operasi penjatuhan?
hakre

Jawaban:

5

Saya melakukan pertempuran dengan situasi yang sama baru-baru ini. Ajax di widget bukan lelucon! Perlu menulis beberapa kode yang cukup gila untuk menyelesaikan berbagai hal. Saya tidak terbiasa dengan kueri langsung, tetapi jika Anda mengatakan itu memeriksa DOM setiap detik, saya mungkin memiliki solusi yang kurang intens untuk Anda:

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

Anda bisa melewatkan fungsi ini sebagai pemilih atau objek jQuery dan itu akan mengembalikan ID instance dari instance saat ini. Saya tidak dapat menemukan cara lain untuk mengatasi masalah ini. Senang mendengar saya bukan satu-satunya :)

ladang
sumber
7

Saya tidak suka menjawab pertanyaan saya sendiri, tetapi saya merasa ini adalah solusi terbaik sejauh ini:

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

  }

});

Ini akan mengaktifkan permintaan widget-save ajax, tepat setelah permintaan widget-save selesai (jika tidak ada respons dengan formulir html).

Perlu ditambahkan dalam jQuery(document).ready()fungsi.

Sekarang, jika Anda ingin dengan mudah melampirkan kembali fungsi javascript Anda ke elemen DOM baru yang ditambahkan oleh fungsi bentuk widget cukup ikatkan ke acara "Saved_widget":

$(document).bind('saved_widget', function(event, widget){
  // For example: $(widget).colorpicker() ....
});
onetrickpony
sumber
3
Perhatikan bahwa Pada jQuery 1.8, metode .ajaxComplete () hanya boleh dilampirkan ke dokumen. - api.jquery.com/ajaxComplete Dengan demikian baris pertama dari snippet Anda seharusnya berbunyi: $ (dokumen) .ajaxComplete (fungsi (event, XMLHttpRequest, ajaxOptions) {Setidaknya untuk WP 3.6+
David Laing
3

Mengalami hal ini baru-baru ini dan tampaknya dalam antarmuka "widgets.php" tradisional, inisialisasi javascript apa pun harus dijalankan langsung untuk widget yang ada (yang ada di #widgets-rightdiv), dan secara tidak langsung melalui widget-addedacara untuk widget yang baru ditambahkan; sedangkan di antarmuka customizer "custom.php" semua widget - yang ada dan yang baru - dikirim widget-addedacara sehingga hanya dapat diinisialisasi di sana. Berdasarkan ini berikut ini adalah perpanjangan dari WP_Widgetkelas yang membuatnya mudah untuk menambahkan inisialisasi javascript untuk bentuk widget ini dengan menimpa satu fungsi, 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
    }
}

Contoh widget uji menggunakan ini:

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
sumber
2

Saya pikir ada sesuatu di Wordpress 3.9 yang mungkin membantu Anda. Ini adalah callback yang diperbarui widget . Gunakan seperti ini (naskah kopi):

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