aristath/kirki

View on GitHub
packages/kirki-framework/control-repeater/src/control.js

Summary

Maintainability
F
1 wk
Test Coverage
import "./control.scss";

/* global kirkiControlLoader */
/* eslint max-depth: 0 */
/* eslint no-useless-escape: 0 */
var RepeaterRow = function( rowIndex, container, label, control ) {
    var self        = this;
    this.rowIndex   = rowIndex;
    this.container  = container;
    this.label      = label;
    this.header     = this.container.find( '.repeater-row-header' );

    this.header.on( 'click', function() {
        self.toggleMinimize();
    } );

    this.container.on( 'click', '.repeater-row-remove', function() {
        self.remove();
    } );

    this.header.on( 'mousedown', function() {
        self.container.trigger( 'row:start-dragging' );
    } );

    this.container.on( 'keyup change', 'input, select, textarea', function( e ) {
        self.container.trigger( 'row:update', [ self.rowIndex, jQuery( e.target ).data( 'field' ), e.target ] );
    } );

    this.setRowIndex = function( rowNum ) {
        this.rowIndex = rowNum;
        this.container.attr( 'data-row', rowNum );
        this.container.data( 'row', rowNum );
        this.updateLabel();
    };

    this.toggleMinimize = function() {

        // Store the previous state.
        this.container.toggleClass( 'minimized' );
        this.header.find( '.dashicons' ).toggleClass( 'dashicons-arrow-up' ).toggleClass( 'dashicons-arrow-down' );
    };

    this.remove = function() {
        this.container.slideUp( 300, function() {
            jQuery( this ).detach();
        } );
        this.container.trigger( 'row:remove', [ this.rowIndex ] );
    };

    this.updateLabel = function() {
        var rowLabelField,
            rowLabel,
            rowLabelSelector;

        if ( 'field' === this.label.type ) {
            rowLabelField = this.container.find( '.repeater-field [data-field="' + this.label.field + '"]' );
            if ( _.isFunction( rowLabelField.val ) ) {
                rowLabel = rowLabelField.val();
                if ( '' !== rowLabel ) {
                    if ( ! _.isUndefined( control.params.fields[ this.label.field ] ) ) {
                        if ( ! _.isUndefined( control.params.fields[ this.label.field ].type ) ) {
                            if ( 'select' === control.params.fields[ this.label.field ].type ) {
                                if ( ! _.isUndefined( control.params.fields[ this.label.field ].choices ) && ! _.isUndefined( control.params.fields[ this.label.field ].choices[ rowLabelField.val() ] ) ) {
                                    rowLabel = control.params.fields[ this.label.field ].choices[ rowLabelField.val() ];
                                }
                            } else if ( 'radio' === control.params.fields[ this.label.field ].type || 'radio-image' === control.params.fields[ this.label.field ].type ) {
                                rowLabelSelector = control.selector + ' [data-row="' + this.rowIndex + '"] .repeater-field [data-field="' + this.label.field + '"]:checked';
                                rowLabel = jQuery( rowLabelSelector ).val();
                            }
                        }
                    }
                    this.header.find( '.repeater-row-label' ).text( rowLabel );
                    return;
                }
            }
        }
        this.header.find( '.repeater-row-label' ).text( this.label.value + ' ' + ( this.rowIndex + 1 ) );
    };
    this.updateLabel();
};

wp.customize.controlConstructor.repeater = wp.customize.Control.extend( {

    // When we're finished loading continue processing
    ready: function() {
        var control = this;

        // Init the control.
        if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
            kirkiControlLoader( control );
        } else {
            control.initKirkiControl();
        }
    },

    initKirkiControl: function( control ) {
        var limit, theNewRow, settingValue;
        control = control || this;

        // The current value set in Control Class (set in Kirki_Customize_Repeater_Control::to_json() function)
        settingValue = control.params.value;

        // The hidden field that keeps the data saved (though we never update it)
        control.settingField = control.container.find( '[data-customize-setting-link]' ).first();

        // Set the field value for the first time, we'll fill it up later
        control.setValue( [], false );

        // The DIV that holds all the rows
        control.repeaterFieldsContainer = control.container.find( '.repeater-fields' ).first();

        // Set number of rows to 0
        control.currentIndex = 0;

        // Save the rows objects
        control.rows = [];

        // Default limit choice
        limit = false;
        if ( ! _.isUndefined( control.params.choices.limit ) ) {
            limit = ( 0 >= control.params.choices.limit ) ? false : parseInt( control.params.choices.limit, 10 );
        }

        control.container.on( 'click', 'button.repeater-add', function( e ) {
            e.preventDefault();
            if ( ! limit || control.currentIndex < limit ) {
                theNewRow = control.addRow();
                theNewRow.toggleMinimize();
                control.initColorPicker();
                control.initSelect( theNewRow );
            } else {
                jQuery( control.selector + ' .limit' ).addClass( 'highlight' );
            }
        } );

        control.container.on( 'click', '.repeater-row-remove', function() {
            control.currentIndex--;
            if ( ! limit || control.currentIndex < limit ) {
                jQuery( control.selector + ' .limit' ).removeClass( 'highlight' );
            }
        } );

        control.container.on( 'click keypress', '.repeater-field-image .upload-button,.repeater-field-cropped_image .upload-button,.repeater-field-upload .upload-button', function( e ) {
            e.preventDefault();
            control.$thisButton = jQuery( this );
            control.openFrame( e );
        } );

        control.container.on( 'click keypress', '.repeater-field-image .remove-button,.repeater-field-cropped_image .remove-button', function( e ) {
            e.preventDefault();
            control.$thisButton = jQuery( this );
            control.removeImage( e );
        } );

        control.container.on( 'click keypress', '.repeater-field-upload .remove-button', function( e ) {
            e.preventDefault();
            control.$thisButton = jQuery( this );
            control.removeFile( e );
        } );

        /**
         * Function that loads the Mustache template
         */
        control.repeaterTemplate = _.memoize( function() {
            var compiled,

                /*
                 * Underscore's default ERB-style templates are incompatible with PHP
                 * when asp_tags is enabled, so WordPress uses Mustache-inspired templating syntax.
                 *
                 * @see trac ticket #22344.
                 */
                options = {
                    evaluate: /<#([\s\S]+?)#>/g,
                    interpolate: /\{\{\{([\s\S]+?)\}\}\}/g,
                    escape: /\{\{([^\}]+?)\}\}(?!\})/g,
                    variable: 'data'
                };

            return function( data ) {
                compiled = _.template( control.container.find( '.customize-control-repeater-content' ).first().html(), null, options );
                return compiled( data );
            };
        } );

        // When we load the control, the fields have not been filled up
        // This is the first time that we create all the rows
        if ( settingValue.length ) {
            _.each( settingValue, function( subValue ) {
                theNewRow = control.addRow( subValue );
                control.initColorPicker();
                control.initSelect( theNewRow, subValue );
            } );
        }

        control.repeaterFieldsContainer.sortable( {
            handle: '.repeater-row-header',
            update: function() {
                control.sort();
            }
        } );

    },

    /**
     * Open the media modal.
     *
     * @param {Object} event - The JS event.
     * @returns {void}
     */
    openFrame: function( event ) {
        if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
            return;
        }

        if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-cropped_image' ) ) {
            this.initCropperFrame();
        } else {
            this.initFrame();
        }

        this.frame.open();
    },

    initFrame: function() {
        var libMediaType = this.getMimeType();

        this.frame = wp.media( {
            states: [
            new wp.media.controller.Library( {
                    library: wp.media.query( { type: libMediaType } ),
                    multiple: false,
                    date: false
                } )
            ]
        } );

        // When a file is selected, run a callback.
        this.frame.on( 'select', this.onSelect, this );
    },

    /**
     * Create a media modal select frame, and store it so the instance can be reused when needed.
     * This is mostly a copy/paste of Core api.CroppedImageControl in /wp-admin/js/customize-control.js
     *
     * @returns {void}
     */
    initCropperFrame: function() {

        // We get the field id from which this was called
        var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' ),
            attrs          = [ 'width', 'height', 'flex_width', 'flex_height' ], // A list of attributes to look for
            libMediaType   = this.getMimeType();

        // Make sure we got it
        if ( _.isString( currentFieldId ) && '' !== currentFieldId ) {

            // Make fields is defined and only do the hack for cropped_image
            if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'cropped_image' === this.params.fields[ currentFieldId ].type ) {

                //Iterate over the list of attributes
                attrs.forEach( function( el ) {

                    // If the attribute exists in the field
                    if ( ! _.isUndefined( this.params.fields[ currentFieldId ][ el ] ) ) {

                        // Set the attribute in the main object
                        this.params[ el ] = this.params.fields[ currentFieldId ][ el ];
                    }
                }.bind( this ) );
            }
        }

        this.frame = wp.media( {
            button: {
                text: 'Select and Crop',
                close: false
            },
            states: [
                new wp.media.controller.Library( {
                    library: wp.media.query( { type: libMediaType } ),
                    multiple: false,
                    date: false,
                    suggestedWidth: this.params.width,
                    suggestedHeight: this.params.height
                } ),
                new wp.media.controller.CustomizeImageCropper( {
                    imgSelectOptions: this.calculateImageSelectOptions,
                    control: this
                } )
            ]
        } );

        this.frame.on( 'select', this.onSelectForCrop, this );
        this.frame.on( 'cropped', this.onCropped, this );
        this.frame.on( 'skippedcrop', this.onSkippedCrop, this );

    },

    onSelect: function() {
        var attachment = this.frame.state().get( 'selection' ).first().toJSON();

        if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-upload' ) ) {
            this.setFileInRepeaterField( attachment );
        } else {
            this.setImageInRepeaterField( attachment );
        }
    },

    /**
     * After an image is selected in the media modal, switch to the cropper
     * state if the image isn't the right size.
     */

    onSelectForCrop: function() {
        var attachment = this.frame.state().get( 'selection' ).first().toJSON();

        if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
            this.setImageInRepeaterField( attachment );
        } else {
            this.frame.setState( 'cropper' );
        }
    },

    /**
     * After the image has been cropped, apply the cropped image data to the setting.
     *
     * @param {object} croppedImage Cropped attachment data.
     * @returns {void}
     */
    onCropped: function( croppedImage ) {
        this.setImageInRepeaterField( croppedImage );
    },

    /**
     * Returns a set of options, computed from the attached image data and
     * control-specific data, to be fed to the imgAreaSelect plugin in
     * wp.media.view.Cropper.
     *
     * @param {wp.media.model.Attachment} attachment - The attachment from the WP API.
     * @param {wp.media.controller.Cropper} controller - Media controller.
     * @returns {Object} - Options.
     */
    calculateImageSelectOptions: function( attachment, controller ) {
        var control    = controller.get( 'control' ),
            flexWidth  = !! parseInt( control.params.flex_width, 10 ),
            flexHeight = !! parseInt( control.params.flex_height, 10 ),
            realWidth  = attachment.get( 'width' ),
            realHeight = attachment.get( 'height' ),
            xInit      = parseInt( control.params.width, 10 ),
            yInit      = parseInt( control.params.height, 10 ),
            ratio      = xInit / yInit,
            xImg       = realWidth,
            yImg       = realHeight,
            x1,
            y1,
            imgSelectOptions;

        controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );

        if ( xImg / yImg > ratio ) {
            yInit = yImg;
            xInit = yInit * ratio;
        } else {
            xInit = xImg;
            yInit = xInit / ratio;
        }

        x1 = ( xImg - xInit ) / 2;
        y1 = ( yImg - yInit ) / 2;

        imgSelectOptions = {
            handles: true,
            keys: true,
            instance: true,
            persistent: true,
            imageWidth: realWidth,
            imageHeight: realHeight,
            x1: x1,
            y1: y1,
            x2: xInit + x1,
            y2: yInit + y1
        };

        if ( false === flexHeight && false === flexWidth ) {
            imgSelectOptions.aspectRatio = xInit + ':' + yInit;
        }
        if ( false === flexHeight ) {
            imgSelectOptions.maxHeight = yInit;
        }
        if ( false === flexWidth ) {
            imgSelectOptions.maxWidth = xInit;
        }

        return imgSelectOptions;
    },

    /**
     * Return whether the image must be cropped, based on required dimensions.
     *
     * @param {bool} flexW - The flex-width.
     * @param {bool} flexH - The flex-height.
     * @param {int}  dstW - Initial point distance in the X axis.
     * @param {int}  dstH - Initial point distance in the Y axis.
     * @param {int}  imgW - Width.
     * @param {int}  imgH - Height.
     * @returns {bool} - Whether the image must be cropped or not based on required dimensions.
     */
    mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
        return ! ( ( true === flexW && true === flexH ) || ( true === flexW && dstH === imgH ) || ( true === flexH && dstW === imgW ) || ( dstW === imgW && dstH === imgH ) || ( imgW <= dstW ) );
    },

    /**
     * If cropping was skipped, apply the image data directly to the setting.
     *
     * @returns {void}
     */
    onSkippedCrop: function() {
        var attachment = this.frame.state().get( 'selection' ).first().toJSON();
        this.setImageInRepeaterField( attachment );
    },

    /**
     * Updates the setting and re-renders the control UI.
     *
     * @param {object} attachment - The attachment object.
     * @returns {void}
     */
    setImageInRepeaterField: function( attachment ) {
        var $targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image' );

        $targetDiv.find( '.kirki-image-attachment' ).html( '<img src="' + attachment.url + '">' ).hide().slideDown( 'slow' );

        $targetDiv.find( '.hidden-field' ).val( attachment.id );
        this.$thisButton.text( this.$thisButton.data( 'alt-label' ) );
        $targetDiv.find( '.remove-button' ).show();

        //This will activate the save button
        $targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
        this.frame.close();
    },

    /**
     * Updates the setting and re-renders the control UI.
     *
     * @param {object} attachment - The attachment object.
     * @returns {void}
     */
    setFileInRepeaterField: function( attachment ) {
        var $targetDiv = this.$thisButton.closest( '.repeater-field-upload' );

        $targetDiv.find( '.kirki-file-attachment' ).html( '<span class="file"><span class="dashicons dashicons-media-default"></span> ' + attachment.filename + '</span>' ).hide().slideDown( 'slow' );

        $targetDiv.find( '.hidden-field' ).val( attachment.id );
        this.$thisButton.text( this.$thisButton.data( 'alt-label' ) );
        $targetDiv.find( '.upload-button' ).show();
        $targetDiv.find( '.remove-button' ).show();

        //This will activate the save button
        $targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
        this.frame.close();
    },

    getMimeType: function() {

        // We get the field id from which this was called
        var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' );

        // Make sure we got it
        if ( _.isString( currentFieldId ) && '' !== currentFieldId ) {

            // Make fields is defined and only do the hack for cropped_image
            if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'upload' === this.params.fields[ currentFieldId ].type ) {

                // If the attribute exists in the field
                if ( ! _.isUndefined( this.params.fields[ currentFieldId ].mime_type ) ) {

                    // Set the attribute in the main object
                    return this.params.fields[ currentFieldId ].mime_type;
                }
            }
        }
        return 'image';
    },

    removeImage: function( event ) {
        var $targetDiv,
            $uploadButton;

        if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
            return;
        }

        $targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image,.repeater-field-upload' );
        $uploadButton = $targetDiv.find( '.upload-button' );

        $targetDiv.find( '.kirki-image-attachment' ).slideUp( 'fast', function() {
            jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) );
        } );
        $targetDiv.find( '.hidden-field' ).val( '' );
        $uploadButton.text( $uploadButton.data( 'label' ) );
        this.$thisButton.hide();

        $targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
    },

    removeFile: function( event ) {
        var $targetDiv,
            $uploadButton;

        if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
            return;
        }

        $targetDiv = this.$thisButton.closest( '.repeater-field-upload' );
        $uploadButton = $targetDiv.find( '.upload-button' );

        $targetDiv.find( '.kirki-file-attachment' ).slideUp( 'fast', function() {
            jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) );
        } );
        $targetDiv.find( '.hidden-field' ).val( '' );
        $uploadButton.text( $uploadButton.data( 'label' ) );
        this.$thisButton.hide();

        $targetDiv.find( 'input, textarea, select' ).trigger( 'change' );

    },

    /**
     * Get the current value of the setting
     *
     * @returns {Object} - Returns the value.
     */
    getValue: function() {

        // The setting is saved in JSON
        return JSON.parse( decodeURI( this.setting.get() ) );
    },

    /**
     * Set a new value for the setting
     *
     * @param {Object} newValue - The new value.
     * @param {bool} refresh - If we want to refresh the previewer or not
     * @param {bool} filtering - If we want to filter or not.
     * @returns {void}
     */
    setValue: function( newValue, refresh, filtering ) {

        // We need to filter the values after the first load to remove data requrired for diplay but that we don't want to save in DB
        var filteredValue = newValue,
            filter        = [];

        if ( filtering ) {
            jQuery.each( this.params.fields, function( index, value ) {
                if ( 'image' === value.type || 'cropped_image' === value.type || 'upload' === value.type ) {
                    filter.push( index );
                }
            } );
            jQuery.each( newValue, function( index, value ) {
                jQuery.each( filter, function( ind, field ) {
                    if ( ! _.isUndefined( value[ field ] ) && ! _.isUndefined( value[ field ].id ) ) {
                        filteredValue[index][ field ] = value[ field ].id;
                    }
                } );
            } );
        }

        this.setting.set( encodeURI( JSON.stringify( filteredValue ) ) );

        if ( refresh ) {

            // Trigger the change event on the hidden field so
            // previewer refresh the website on Customizer
            this.settingField.trigger( 'change' );
        }
    },

    /**
     * Add a new row to repeater settings based on the structure.
     *
     * @param {Object} data - (Optional) Object of field => value pairs (undefined if you want to get the default values)
     * @returns {Object} - Returns the new row.
     */
    addRow: function( data ) {
        var control       = this,
            template      = control.repeaterTemplate(), // The template for the new row (defined on Kirki_Customize_Repeater_Control::render_content() ).
            settingValue  = this.getValue(), // Get the current setting value.
            newRowSetting = {}, // Saves the new setting data.
            templateData, // Data to pass to the template
            newRow,
            i;

        if ( template ) {

            // The control structure is going to define the new fields
            // We need to clone control.params.fields. Assigning it
            // ould result in a reference assignment.
            templateData = jQuery.extend( true, {}, control.params.fields );

            // But if we have passed data, we'll use the data values instead
            if ( data ) {
                for ( i in data ) {
                    if ( data.hasOwnProperty( i ) && templateData.hasOwnProperty( i ) ) {
                        templateData[ i ].default = data[ i ];
                    }
                }
            }

            templateData.index = this.currentIndex;

            // Append the template content
            template = template( templateData );

            // Create a new row object and append the element
            newRow = new RepeaterRow(
                control.currentIndex,
                jQuery( template ).appendTo( control.repeaterFieldsContainer ),
                control.params.row_label,
                control
            );

            newRow.container.on( 'row:remove', function( e, rowIndex ) {
                control.deleteRow( rowIndex );
            } );

            newRow.container.on( 'row:update', function( e, rowIndex, fieldName, element ) {
                control.updateField.call( control, e, rowIndex, fieldName, element ); // eslint-disable-line no-useless-call
                newRow.updateLabel();
            } );

            // Add the row to rows collection
            this.rows[ this.currentIndex ] = newRow;

            for ( i in templateData ) {
                if ( templateData.hasOwnProperty( i ) ) {
                    newRowSetting[ i ] = templateData[ i ].default;
                }
            }

            settingValue[ this.currentIndex ] = newRowSetting;
            this.setValue( settingValue, true );

            this.currentIndex++;

            return newRow;
        }
    },

    sort: function() {
        var control     = this,
            $rows       = this.repeaterFieldsContainer.find( '.repeater-row' ),
            newOrder    = [],
            settings    = control.getValue(),
            newRows     = [],
            newSettings = [];

        $rows.each( function( i, element ) {
            newOrder.push( jQuery( element ).data( 'row' ) );
        } );

        jQuery.each( newOrder, function( newPosition, oldPosition ) {
            newRows[ newPosition ] = control.rows[ oldPosition ];
            newRows[ newPosition ].setRowIndex( newPosition );

            newSettings[ newPosition ] = settings[ oldPosition ];
        } );

        control.rows = newRows;
        control.setValue( newSettings );
    },

    /**
     * Delete a row in the repeater setting
     *
     * @param {int} index - Position of the row in the complete Setting Array
     * @returns {void}
     */
    deleteRow: function( index ) {
        var currentSettings = this.getValue(),
            row,
            prop;

        if ( currentSettings[ index ] ) {

            // Find the row
            row = this.rows[ index ];
            if ( row ) {

                // Remove the row settings
                delete currentSettings[ index ];

                // Remove the row from the rows collection
                delete this.rows[ index ];

                // Update the new setting values
                this.setValue( currentSettings, true );
            }
        }

        // Remap the row numbers
        for ( prop in this.rows ) {
            if ( this.rows.hasOwnProperty( prop ) && this.rows[ prop ] ) {
                this.rows[ prop ].updateLabel();
            }
        }
    },

    /**
     * Update a single field inside a row.
     * Triggered when a field has changed
     *
     * @param {Object} e - Event Object
     * @param {int} rowIndex - The row's index as an integer.
     * @param {string} fieldId - The field ID.
     * @param {string|Object} element - The element's identifier, or jQuery Object of the element.
     * @returns {void}
     */
    updateField: function( e, rowIndex, fieldId, element ) {
        var type,
            row,
            currentSettings;

        if ( ! this.rows[ rowIndex ] ) {
            return;
        }

        if ( ! this.params.fields[ fieldId ] ) {
            return;
        }

        type            = this.params.fields[ fieldId].type;
        row             = this.rows[ rowIndex ];
        currentSettings = this.getValue();

        element = jQuery( element );

        if ( _.isUndefined( currentSettings[ row.rowIndex ][ fieldId ] ) ) {
            return;
        }

        if ( 'checkbox' === type ) {
            currentSettings[ row.rowIndex ][ fieldId ] = element.is( ':checked' );
        } else {

            // Update the settings
            currentSettings[ row.rowIndex ][ fieldId ] = element.val();
        }
        this.setValue( currentSettings, true );
    },

    /**
     * Init the color picker on color fields
     * Called after AddRow
     *
     * @returns {void}
     */
    initColorPicker: function() {
        var control     = this,
            colorPicker = control.container.find( '.color-picker-hex' ),
            options     = {},
            fieldId     = colorPicker.data( 'field' );

        // We check if the color palette parameter is defined.
        if ( ! _.isUndefined( fieldId ) && ! _.isUndefined( control.params.fields[ fieldId ] ) && ! _.isUndefined( control.params.fields[ fieldId ].palettes ) && _.isObject( control.params.fields[ fieldId ].palettes ) ) {
            options.palettes = control.params.fields[ fieldId ].palettes;
        }

        // When the color picker value is changed we update the value of the field
        options.change = function( event, ui ) {

            var currentPicker   = jQuery( event.target ),
                row             = currentPicker.closest( '.repeater-row' ),
                rowIndex        = row.data( 'row' ),
                currentSettings = control.getValue();

            currentSettings[ rowIndex ][ currentPicker.data( 'field' ) ] = ui.color.toString();
            control.setValue( currentSettings, true );

        };

        // Init the color picker
        if ( 0 !== colorPicker.length ) {
            colorPicker.wpColorPicker( options );
        }
    },

    /**
     * Init the dropdown-pages field.
     * Called after AddRow
     *
     * @param {object} theNewRow the row that was added to the repeater
     * @param {object} data the data for the row if we're initializing a pre-existing row
     * @returns {void}
     */
    initSelect: function( theNewRow, data ) {
        var control  = this,
            dropdown = theNewRow.container.find( '.repeater-field select' ),
            dataField;

        if ( 0 === dropdown.length ) {
            return;
        }

        dataField = dropdown.data( 'field' );
        multiple  = jQuery( dropdown ).data( 'multiple' );

        data = data || {};
        data[ dataField ] = data[ dataField ] || '';

        jQuery( dropdown ).val( data[ dataField ] || jQuery( dropdown ).val() );

        this.container.on( 'change', '.repeater-field select', function( event ) {

            var currentDropdown = jQuery( event.target ),
                row             = currentDropdown.closest( '.repeater-row' ),
                rowIndex        = row.data( 'row' ),
                currentSettings = control.getValue();

            currentSettings[ rowIndex ][ currentDropdown.data( 'field' ) ] = jQuery( this ).val();
            control.setValue( currentSettings );
        } );
    }
} );