gocodebox/lifterlms

View on GitHub
assets/js/partials/_metabox-field-repeater.js

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * LifterLMS Admin Metabox Repeater Field
 *
 * @package LifterLMS/Scripts/Partials
 *
 * @since 3.11.0
 * @version 5.3.2
 */

this.repeaters = {

    /**
     * Reference to the parent metabox class
     *
     * @type {Object}
     */
    metaboxes: this,

    /**
     * A jQuery selector for all repeater elements on the current screen
     *
     * @type {Object}
     */
    $repeaters: null,

    /**
     * Init
     *
     * @since 3.11.0
     * @since 3.23.0 Unknown.
     *
     * @return {void}
     */
    init: function() {

        var self = this;

        self.$repeaters = $( '.llms-mb-list.repeater' );

        if ( self.$repeaters.length ) {

            // Wait for tinyMCE just in case their editors in the repeaters.
            LLMS.wait_for(
                function() {
                    return ( 'undefined' !== typeof tinyMCE );
                },
                function() {
                    self.load();
                    self.bind();
                }
            );

            /**
             * On click of any post submit buttons add some data to the submit button
             * so we can see which button to trigger after repeaters are finished.
             */
            $( '#post input[type="submit"], #post-preview' ).on( 'click', function() {
                $( this ).attr( 'data-llms-clicked', 'yes' );
            } );

            // Handle post submission.
            $( '#post' ).on( 'submit', self.handle_submit );

        }

    },

    /**
     * Bind DOM Events
     *
     * @since 3.11.0
     * @since 3.13.0 Unknown.
     * @since 5.3.2 Don't remove the model's mceEditor instance (it's removed before cloning a row now).
     *
     * @return {void}
     */
    bind: function() {

        var self = this;

        self.$repeaters.each( function() {

            var $repeater = $( this ),
                $rows     = $repeater.find( '.llms-repeater-rows' );

            // For the repeater + button.
            $repeater.find( '.llms-repeater-new-btn' ).on( 'click', function() {
                self.add_row( $repeater, null, true );
            } );

            // Make repeater rows sortable.
            $rows.sortable( {
                handle: '.llms-drag-handle',
                items: '.llms-repeater-row',
                start: function( event, ui ) {
                    $rows.addClass( 'dragging' );
                },
                stop: function( event, ui ) {
                    $rows.removeClass( 'dragging' );

                    var $eds = ui.item.find( 'textarea.wp-editor-area' );
                    $eds.each( function() {
                        var ed_id = $( this ).attr( 'id' );
                        tinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, ed_id );
                        tinyMCE.EditorManager.execCommand( 'mceAddEditor', true, ed_id );
                    } );

                    self.save( $repeater );
                },
            } );

            $repeater.on( 'click', '.llms-repeater-remove', function( e ) {
                e.stopPropagation();
                var $row = $( this ).closest( '.llms-repeater-row' );
                if ( window.confirm( LLMS.l10n.translate( 'Are you sure you want to delete this row? This cannot be undone.' ) ) ) {
                    $row.remove();
                    setTimeout( function() {
                        self.save( $repeater );
                    }, 1 );
                }
            } );

        } );

    },

    /**
     * Add a new row to a repeater rows group
     *
     * @since 3.11.0
     * @since 5.3.2 Use `self.clone_row()` to retrieve the model's base HTML for the row to be added.
     *
     * @param {Object}  $repeater A jQuery selector for the repeater to add a row to.
     * @param {Object}  data      Optional object of data to fill fields in the row with.
     * @param {Boolean} expand    If true, will automatically open the row after adding it to the dom.
     * @return {void}
     */
    add_row: function( $repeater, data, expand ) {

        var self      = this,
            $rows     = $repeater.find( '.llms-repeater-rows' ),
            $model    = $repeater.find( '.llms-repeater-model' ),
            $row      = self.clone_row( $model.find( '.llms-repeater-row' ) ),
            new_index = $repeater.find( '.llms-repeater-row' ).length,
            editor    = self.reindex( $row, new_index );

        if ( data ) {
            $.each( data, function( key, val ) {

                var $field = $row.find( '[name^="' + key + '"]' );

                if ( $field.hasClass( 'llms-select2-student' ) ) {
                    $.each( val, function( i, data ) {
                        $field.append( '<option value="' + data.key + '" selected="selected">' + data.title + '</option>' )
                    } );
                    $field.trigger( 'change' );
                } else {
                    $field.val( val );
                }

            } );
        }

        setTimeout( function() {
            self.bind_row( $row );
        }, 1 );

        $rows.append( $row );
        if ( expand ) {
            $row.find( '.llms-collapsible-header' ).trigger( 'click' );
        }
        tinyMCE.EditorManager.execCommand( 'mceAddEditor', true, editor );

        $repeater.trigger( 'llms-new-repeater-row', {
            $row: $row,
            data: data,
        } );

    },

    /**
     * Bind DOM events for a single repeater row
     *
     * @since 3.11.0
     * @since 3.13.0 Unknown.
     *
     * @param {Object} $row A jQuery selector for the row.
     * @return {void}
     */
    bind_row: function( $row ) {

        this.bind_row_header( $row );

        $row.find( '.llms-select2' ).llmsSelect2( {
            width: '100%',
        } );

        $row.find( '.llms-select2-student' ).llmsStudentsSelect2();

        this.metaboxes.bind_datepickers( $row.find( '.llms-datepicker' ) );
        this.metaboxes.bind_controllers( $row.find( '[data-is-controller]' ) );
        // This.metaboxes.bind_merge_code_buttons( $row.find( '.llms-merge-code-wrapper' ) );.
    },

    /**
     * Bind row header events
     *
     * @since 3.11.0
     *
     * @param {Object} $row jQuery selector for the row.
     * @return {void}
     */
    bind_row_header: function( $row ) {

        // Handle the title field binding.
        var $title = $row.find( '.llms-repeater-title' ),
            $field = $row.find( '.llms-collapsible-header-title-field' );

        $title.attr( 'data-default', $title.text() );

        $field.on( 'keyup focusout blur', function() {
            var val = $( this ).val();
            if ( ! val ) {
                val = $title.attr( 'data-default' );
            }
            $title.text( val );
        } ).trigger( 'keyup' );

    },

    /**
     * Create a copy of the model's row after removing any tinyMCE editor instances present in the model.
     *
     * @since 5.3.2
     *
     * @param {Object} $row A jQuery object of the row to be cloned.
     * @return {Object} A clone of the jQuery object.
     */
    clone_row: function( $row ) {

        $ed = $row.find( '.editor textarea' );
        if ( $ed.length ) {
            tinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, $ed.attr( 'id' ) );
        }

        return $row.clone()

    },

    /**
     * Handle WP Post form submission to ensure repeaters are saved before submitting the form to save/publish the post
     *
     * @since 3.11.0
     * @since 3.23.0 Unknown.
     *
     * @param {Object} e An event object.
     * @return {void}
     */
    handle_submit: function( e ) {

        // Get the button used to submit the form.
        var $btn     = $( '#post [data-llms-clicked="yes"]' ),
            $spinner = $btn.parent().find( '.spinner' );

        if ( $btn.is( '#post-preview' ) ) {
            $btn.removeAttr( 'data-llms-clicked' );
            return;
        }

        e.preventDefault();

        // Core UX to prevent multi-click/or the appearance of a delay.
        $( '#post input[type="submit"]' ).addClass( 'disabled' ).attr( 'disabled', 'disabled' );
        $spinner.addClass( 'is-active' );

        var self = window.llms.metaboxes.repeaters,
            i    = 0,
            wait;

        self.$repeaters.each( function() {
            self.save( $( this ) );
        } );

        wait = setInterval( function() {

            if ( i >= 59 || ! $( '.llms-mb-list.repeater.processing' ).length ) {

                clearInterval( wait );
                $( '#post' ).off( 'submit', this.handle_submit );
                $spinner.removeClass( 'is-active' );
                $btn.removeClass( 'disabled' ).removeAttr( 'disabled' ).trigger( 'click' );

            } else {

                i++;

            }

        }, 1000 );

    },

    /**
     * Load repeater data from the server and create rows in the DOM
     *
     * @since 3.11.0
     * @since 3.12.1 Unknown.
     *
     * @return {void}
     */
    load: function() {

        var self = this;

        self.$repeaters.each( function() {

            var $repeater = $( this );

            // Ensure the repeater is only loaded once to prevent duplicates resulting from duplicating binding.
            // On certain sites which I cannot quite explain...
            if ( $repeater.hasClass( 'is-loaded' ) || $repeater.hasClass( 'processing' ) ) {
                return;
            }

            self.store( $repeater, 'load', function( data ) {

                $repeater.addClass( 'is-loaded' );

                $.each( data.data, function( i, obj ) {
                    self.add_row( $repeater, obj, false );
                } );

                // For each row within the repeater.
                $repeater.find( '.llms-repeater-rows .llms-repeater-row' ).each( function() {
                    self.bind_row( $( this ) );
                } );

            } );

        } );

    },

    /**
     * Reindex a row
     *
     * Renames ids, attrs, and etc...
     *
     * Used when cloning the model for new rows.
     *
     * @since 3.11.0
     *
     * @param {Object} $row  jQuery selector for the row.
     * @param {string} index The index (or id) to use when renaming.
     * @return {string}
     */
    reindex: function( $row, index ) {

        var old_index = $row.attr( 'data-row-order' ),
            $ed       = $row.find( '.llms-mb-list.editor textarea' );

        tinyMCE.EditorManager.execCommand( 'mceRemoveEditor', true, $ed.attr( 'id' ) );

        function replace_attr( $el, attr ) {
            $el.each( function() {
                var str = $( this ).attr( attr );
                $( this ).attr( attr, str.replace( old_index, index ) );
            } );
        };

        $row.attr( 'data-row-order', index );

        replace_attr( $row, 'data-row-order' );

        replace_attr( $row.find( 'button.insert-media' ), 'data-editor' );

        replace_attr( $row.find( 'input[name^="_llms"], textarea[name^="_llms"], select[name^="_llms"]' ), 'id' );
        replace_attr( $row.find( 'input[name^="_llms"], textarea[name^="_llms"], select[name^="_llms"]' ), 'name' );
        replace_attr( $row.find( '[data-controller]' ), 'data-controller' );
        replace_attr( $row.find( '[data-controller]' ), 'data-controller' );
        replace_attr( $row.find( 'button.wp-switch-editor' ), 'data-wp-editor-id' );
        replace_attr( $row.find( 'button.wp-switch-editor' ), 'id' );
        replace_attr( $row.find( '.wp-editor-tools' ), 'id' );
        replace_attr( $row.find( '.wp-editor-container' ), 'id' );

        return $ed.attr( 'id' );

    },

    /**
     * Save a single repeaters data to the server
     *
     * @since 3.11.0
     * @since 3.13.0 Unknown.
     *
     * @param {Object} $repeater jQuery selector for a repeater element.
     * @return {void}
     */
    save: function( $repeater ) {
        $repeater.trigger( 'llms-repeater-before-save', { $el: $repeater } );
        this.store( $repeater, 'save' );
    },

    /**
     * Convert a repeater element into an array of objects that can be saved to the database
     *
     * @since 3.11.0
     *
     * @param {Object} $repeater A jQuery selector for a repeater element.
     * @return {void}
     */
    serialize: function( $repeater ) {

        var rows = [];

        $repeater.find( '.llms-repeater-rows .llms-repeater-row' ).each( function() {

            var obj = {};

            // Easy...
            $( this ).find( 'input[name^="_llms"], select[name^="_llms"]' ).each( function() {
                obj[ $( this ).attr( 'name' ) ] = $( this ).val();
            } );

            // Check if the textarea is a tinyMCE instance.
            $( this ).find( 'textarea[name^="_llms"]' ).each( function() {

                var name = $( this ).attr( 'name' );

                // If it is an editor.
                if ( tinyMCE.editors[ name ] ) {
                    obj[ name ] = tinyMCE.editors[ name ].getContent();
                    // Grab the val of the textarea.
                } else {
                    obj[ name ] = $( this ).val();
                }

            } );

            rows.push( obj );

        } );

        return rows;

    },

    /**
     * AJAX method for interacting with the repeater's handler on the server
     *
     * @since 3.11.0
     *
     * @param {Object}   $repeater jQuery selector for the repeater element.
     * @param {string}   action    Action to call [save|load].
     * @param {Function} cb        Callback function.
     * @return {void}
     */
    store: function( $repeater, action, cb ) {

        cb       = cb || function(){};
        var self = this,
            data = {
                action: $repeater.find( '.llms-repeater-field-handler' ).val(),
                store_action: action,
        };

        if ( 'save' === action ) {
            data.rows = self.serialize( $repeater );
        }

        LLMS.Ajax.call( {
            data: data,
            beforeSend: function() {

                $repeater.addClass( 'processing' );
                LLMS.Spinner.start( $repeater );

            },
            success: function( r ) {

                cb( r );
                LLMS.Spinner.stop( $repeater );
                $repeater.removeClass( 'processing' );

            }

        } );

    }

};
this.repeaters.init();