ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

repeater.js (30848B)


      1 /*jshint -W065 */
      2 var RepeaterRow = function (rowIndex, container, label) {
      3 
      4     'use strict';
      5 
      6     var self = this;
      7 
      8     this.rowIndex = rowIndex;
      9     this.container = container;
     10     this.label = label;
     11     this.header = this.container.find('.repeater-row-header'),
     12 
     13         this.header.on('click', function () {
     14             self.toggleMinimize();
     15         });
     16 
     17     this.container.on('click', '.repeater-row-remove', function () {
     18         self.remove();
     19     });
     20 
     21     this.header.on('mouseup', function () {
     22         self.container.trigger('row:start-dragging');
     23     });
     24 
     25     this.container.on('keyup change', 'input, select, textarea', function (e) {
     26         self.container.trigger('row:update', [self.rowIndex, jQuery(e.target).data('field'), e.target]);
     27     });
     28 
     29     this.setRowIndex = function (rowIndex) {
     30         this.rowIndex = rowIndex;
     31         this.container.attr('data-row', rowIndex);
     32         this.container.data('row', rowIndex);
     33         this.updateLabel();
     34     };
     35 
     36     this.toggleMinimize = function () {
     37 
     38         // Store the previous state.
     39         this.container.toggleClass('minimized');
     40         this.header.find('.dashicons').toggleClass('dashicons-arrow-up').toggleClass('dashicons-arrow-down');
     41     };
     42 
     43     this.remove = function () {
     44         this.container.slideUp(300, function () {
     45             jQuery(this).detach();
     46         });
     47         this.container.trigger('row:remove', [this.rowIndex]);
     48     };
     49 
     50     this.updateLabel = function () {
     51         var rowLabelField,
     52             rowLabel;
     53 
     54         if ('field' === this.label.type) {
     55             rowLabelField = this.container.find('.repeater-field [data-field="' + this.label.field + '"]');
     56             if ('function' === typeof rowLabelField.val) {
     57                 rowLabel = rowLabelField.val();
     58                 if ('' !== rowLabel) {
     59                     this.header.find('.repeater-row-label').text(rowLabel);
     60                     return;
     61                 }
     62             }
     63         }
     64         this.header.find('.repeater-row-label').text(this.label.value + ' ' + (this.rowIndex + 1));
     65     };
     66 
     67     this.updateLabel();
     68 };
     69 
     70 wp.customize.controlConstructor.repeater = wp.customize.Control.extend({
     71     __valueToSet: [],
     72     ready: function () {
     73 
     74         'use strict';
     75 
     76         var control = this,
     77             limit,
     78             theNewRow;
     79 
     80         // The current value set in Control Class (set in Kirki_Customize_Repeater_Control::to_json() function)
     81         var settingValue = this.params.value;
     82 
     83         // The hidden field that keeps the data saved (though we never update it)
     84         this.settingField = this.container.find('[data-customize-setting-link]').first();
     85 
     86         // The DIV that holds all the rows
     87         this.repeaterFieldsContainer = this.container.find('.repeater-fields').first();
     88 
     89         // Set number of rows to 0
     90         this.currentIndex = 0;
     91 
     92         // Save the rows objects
     93         this.rows = [];
     94 
     95         // Default limit choice
     96         limit = false;
     97         if (undefined !== this.params.choices.limit) {
     98             limit = (0 >= this.params.choices.limit) ? false : parseInt(this.params.choices.limit);
     99         }
    100 
    101         this.container.on('click', 'button.repeater-add', function (e) {
    102             e.preventDefault();
    103             if (!limit || control.currentIndex < limit) {
    104                 theNewRow = control.addRow();
    105                 theNewRow.toggleMinimize();
    106                 control.initColorPicker();
    107                 control.initDropdownPages(theNewRow);
    108             }
    109 
    110             if (limit && control.currentIndex >= limit) {
    111                 jQuery(control.selector + ' .limit').addClass('highlight');
    112                 jQuery(control.selector + ' .repeater-add').hide();
    113             }
    114         });
    115 
    116         if (limit && limit == settingValue.length) {
    117             jQuery(control.selector + ' .repeater-add').hide();
    118         }
    119 
    120 
    121         this.container.on('click', '.repeater-row-remove', function (e) {
    122             control.currentIndex--;
    123             if (!limit || control.currentIndex < limit) {
    124                 jQuery(control.selector + ' .limit').removeClass('highlight');
    125                 jQuery(control.selector + ' .repeater-add').show();
    126             }
    127         });
    128 
    129         this.container.on('click keypress', '.repeater-field-image .upload-button,.repeater-field-cropped_image .upload-button,.repeater-field-upload .upload-button', function (e) {
    130             e.preventDefault();
    131             control.$thisButton = jQuery(this);
    132             control.openFrame(e);
    133         });
    134 
    135         this.container.on('click keypress', '.repeater-field-image .remove-button,.repeater-field-cropped_image .remove-button', function (e) {
    136             e.preventDefault();
    137             control.$thisButton = jQuery(this);
    138             control.removeImage(e);
    139         });
    140 
    141         this.container.on('click keypress', '.repeater-field-upload .remove-button', function (e) {
    142             e.preventDefault();
    143             control.$thisButton = jQuery(this);
    144             control.removeFile(e);
    145         });
    146 
    147         /**
    148          * Function that loads the Mustache template
    149          */
    150         this.repeaterTemplate = _.memoize(function () {
    151             var compiled,
    152                 /*
    153                  * Underscore's default ERB-style templates are incompatible with PHP
    154                  * when asp_tags is enabled, so WordPress uses Mustache-inspired templating syntax.
    155                  *
    156                  * @see trac ticket #22344.
    157                  */
    158                 options = {
    159                     evaluate: /<#([\s\S]+?)#>/g,
    160                     interpolate: /\{\{\{([\s\S]+?)\}\}\}/g,
    161                     escape: /\{\{([^\}]+?)\}\}(?!\})/g,
    162                     variable: 'data'
    163                 };
    164 
    165             return function (data) {
    166                 compiled = _.template(control.container.find('.customize-control-repeater-content').first().html(), null, options);
    167                 return compiled(data);
    168             };
    169         });
    170 
    171         // When we load the control, the fields have not been filled up
    172         // This is the first time that we create all the rows
    173         if (settingValue.length) {
    174             _.each(settingValue, function (subValue) {
    175                 theNewRow = control.addRow(subValue, true);
    176                 control.initColorPicker();
    177                 control.initDropdownPages(theNewRow, subValue);
    178             });
    179         }
    180 
    181         // Once we have displayed the rows, we cleanup the values
    182         this.__valueToSet = settingValue;
    183         this.setValue(settingValue, true);
    184 
    185         this.repeaterFieldsContainer.sortable({
    186             handle: '.repeater-row-header',
    187             axis: "y",
    188             update: function (e, ui) {
    189                 control.sort();
    190             }
    191         });
    192 
    193     },
    194 
    195     /**
    196      * Open the media modal.
    197      */
    198     openFrame: function (event) {
    199 
    200         'use strict';
    201 
    202         if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
    203             return;
    204         }
    205 
    206         if (this.$thisButton.closest('.repeater-field').hasClass('repeater-field-cropped_image')) {
    207             this.initCropperFrame();
    208         } else {
    209             this.initFrame();
    210         }
    211 
    212         this.frame.open();
    213     },
    214 
    215     initFrame: function () {
    216 
    217         'use strict';
    218 
    219         var libMediaType = this.getMimeType();
    220 
    221         this.frame = wp.media({
    222             states: [
    223                 new wp.media.controller.Library({
    224                     library: wp.media.query({type: libMediaType}),
    225                     multiple: false,
    226                     date: false
    227                 })
    228             ]
    229         });
    230 
    231         // When a file is selected, run a callback.
    232         this.frame.on('select', this.onSelect, this);
    233     },
    234     /**
    235      * Create a media modal select frame, and store it so the instance can be reused when needed.
    236      * This is mostly a copy/paste of Core api.CroppedImageControl in /wp-admin/js/customize-control.js
    237      */
    238     initCropperFrame: function () {
    239 
    240         'use strict';
    241 
    242         // We get the field id from which this was called
    243         var currentFieldId = this.$thisButton.siblings('input.hidden-field').attr('data-field'),
    244             attrs = ['width', 'height', 'flex_width', 'flex_height'], // A list of attributes to look for
    245             libMediaType = this.getMimeType();
    246 
    247         // Make sure we got it
    248         if ('string' === typeof currentFieldId && '' !== currentFieldId) {
    249 
    250             // Make fields is defined and only do the hack for cropped_image
    251             if ('object' === typeof this.params.fields[currentFieldId] && 'cropped_image' === this.params.fields[currentFieldId].type) {
    252 
    253                 //Iterate over the list of attributes
    254                 attrs.forEach(function (el, index) {
    255 
    256                     // If the attribute exists in the field
    257                     if ('undefined' !== typeof this.params.fields[currentFieldId][el]) {
    258 
    259                         // Set the attribute in the main object
    260                         this.params[el] = this.params.fields[currentFieldId][el];
    261                     }
    262                 }.bind(this));
    263             }
    264         }
    265 
    266         this.frame = wp.media({
    267             button: {
    268                 text: 'Select and Crop',
    269                 close: false
    270             },
    271             states: [
    272                 new wp.media.controller.Library({
    273                     library: wp.media.query({type: libMediaType}),
    274                     multiple: false,
    275                     date: false,
    276                     suggestedWidth: this.params.width,
    277                     suggestedHeight: this.params.height
    278                 }),
    279                 new wp.media.controller.CustomizeImageCropper({
    280                     imgSelectOptions: this.calculateImageSelectOptions,
    281                     control: this
    282                 })
    283             ]
    284         });
    285 
    286         this.frame.on('select', this.onSelectForCrop, this);
    287         this.frame.on('cropped', this.onCropped, this);
    288         this.frame.on('skippedcrop', this.onSkippedCrop, this);
    289 
    290     },
    291 
    292     onSelect: function () {
    293 
    294         'use strict';
    295 
    296         var attachment = this.frame.state().get('selection').first().toJSON();
    297 
    298         if (this.$thisButton.closest('.repeater-field').hasClass('repeater-field-upload')) {
    299             this.setFileInRepeaterField(attachment);
    300         } else {
    301             this.setImageInRepeaterField(attachment);
    302         }
    303     },
    304 
    305     /**
    306      * After an image is selected in the media modal, switch to the cropper
    307      * state if the image isn't the right size.
    308      */
    309 
    310     onSelectForCrop: function () {
    311 
    312         'use strict';
    313 
    314         var attachment = this.frame.state().get('selection').first().toJSON();
    315 
    316         if (this.params.width === attachment.width && this.params.height === attachment.height && !this.params.flex_width && !this.params.flex_height) {
    317             this.setImageInRepeaterField(attachment);
    318         } else {
    319             this.frame.setState('cropper');
    320         }
    321     },
    322 
    323     /**
    324      * After the image has been cropped, apply the cropped image data to the setting.
    325      *
    326      * @param {object} croppedImage Cropped attachment data.
    327      */
    328     onCropped: function (croppedImage) {
    329 
    330         'use strict';
    331 
    332         this.setImageInRepeaterField(croppedImage);
    333 
    334     },
    335 
    336     /**
    337      * Returns a set of options, computed from the attached image data and
    338      * control-specific data, to be fed to the imgAreaSelect plugin in
    339      * wp.media.view.Cropper.
    340      *
    341      * @param {wp.media.model.Attachment} attachment
    342      * @param {wp.media.controller.Cropper} controller
    343      * @returns {Object} Options
    344      */
    345     calculateImageSelectOptions: function (attachment, controller) {
    346 
    347         'use strict';
    348 
    349         var control = controller.get('control'),
    350             flexWidth = !!parseInt(control.params.flex_width, 10),
    351             flexHeight = !!parseInt(control.params.flex_height, 10),
    352             realWidth = attachment.get('width'),
    353             realHeight = attachment.get('height'),
    354             xInit = parseInt(control.params.width, 10),
    355             yInit = parseInt(control.params.height, 10),
    356             ratio = xInit / yInit,
    357             xImg = realWidth,
    358             yImg = realHeight,
    359             x1,
    360             y1,
    361             imgSelectOptions;
    362 
    363         controller.set('canSkipCrop', !control.mustBeCropped(flexWidth, flexHeight, xInit, yInit, realWidth, realHeight));
    364 
    365         if (xImg / yImg > ratio) {
    366             yInit = yImg;
    367             xInit = yInit * ratio;
    368         } else {
    369             xInit = xImg;
    370             yInit = xInit / ratio;
    371         }
    372 
    373         x1 = (xImg - xInit) / 2;
    374         y1 = (yImg - yInit) / 2;
    375 
    376         imgSelectOptions = {
    377             handles: true,
    378             keys: true,
    379             instance: true,
    380             persistent: true,
    381             imageWidth: realWidth,
    382             imageHeight: realHeight,
    383             x1: x1,
    384             y1: y1,
    385             x2: xInit + x1,
    386             y2: yInit + y1
    387         };
    388 
    389         if (false === flexHeight && false === flexWidth) {
    390             imgSelectOptions.aspectRatio = xInit + ':' + yInit;
    391         }
    392         if (false === flexHeight) {
    393             imgSelectOptions.maxHeight = yInit;
    394         }
    395         if (false === flexWidth) {
    396             imgSelectOptions.maxWidth = xInit;
    397         }
    398 
    399         return imgSelectOptions;
    400     },
    401 
    402     /**
    403      * Return whether the image must be cropped, based on required dimensions.
    404      *
    405      * @param {bool} flexW
    406      * @param {bool} flexH
    407      * @param {int}  dstW
    408      * @param {int}  dstH
    409      * @param {int}  imgW
    410      * @param {int}  imgH
    411      * @return {bool}
    412      */
    413     mustBeCropped: function (flexW, flexH, dstW, dstH, imgW, imgH) {
    414 
    415         'use strict';
    416 
    417         if (true === flexW && true === flexH) {
    418             return false;
    419         }
    420 
    421         if (true === flexW && dstH === imgH) {
    422             return false;
    423         }
    424 
    425         if (true === flexH && dstW === imgW) {
    426             return false;
    427         }
    428 
    429         if (dstW === imgW && dstH === imgH) {
    430             return false;
    431         }
    432 
    433         if (imgW <= dstW) {
    434             return false;
    435         }
    436 
    437         return true;
    438     },
    439 
    440     /**
    441      * If cropping was skipped, apply the image data directly to the setting.
    442      */
    443     onSkippedCrop: function () {
    444 
    445         'use strict';
    446 
    447         var attachment = this.frame.state().get('selection').first().toJSON();
    448         this.setImageInRepeaterField(attachment);
    449 
    450     },
    451 
    452     /**
    453      * Updates the setting and re-renders the control UI.
    454      *
    455      * @param {object} attachment
    456      */
    457     setImageInRepeaterField: function (attachment) {
    458 
    459         'use strict';
    460 
    461         var $targetDiv = this.$thisButton.closest('.repeater-field-image,.repeater-field-cropped_image');
    462 
    463         $targetDiv.find('.kirki-image-attachment').html('<img src="' + attachment.url + '">').hide().slideDown('slow');
    464 
    465         $targetDiv.find('.hidden-field').val(attachment.id);
    466         this.$thisButton.text(this.$thisButton.data('alt-label'));
    467         $targetDiv.find('.remove-button').show();
    468 
    469         //This will activate the save button
    470         $targetDiv.find('input, textarea, select').trigger('change');
    471         this.frame.close();
    472 
    473     },
    474 
    475     /**
    476      * Updates the setting and re-renders the control UI.
    477      *
    478      * @param {object} attachment
    479      */
    480     setFileInRepeaterField: function (attachment) {
    481 
    482         'use strict';
    483 
    484         var $targetDiv = this.$thisButton.closest('.repeater-field-upload');
    485 
    486         $targetDiv.find('.kirki-file-attachment').html('<span class="file"><span class="dashicons dashicons-media-default"></span> ' + attachment.filename + '</span>').hide().slideDown('slow');
    487 
    488         $targetDiv.find('.hidden-field').val(attachment.id);
    489         this.$thisButton.text(this.$thisButton.data('alt-label'));
    490         $targetDiv.find('.upload-button').show();
    491         $targetDiv.find('.remove-button').show();
    492 
    493         //This will activate the save button
    494         $targetDiv.find('input, textarea, select').trigger('change');
    495         this.frame.close();
    496 
    497     },
    498 
    499     getMimeType: function () {
    500 
    501         'use strict';
    502 
    503         // We get the field id from which this was called
    504         var currentFieldId = this.$thisButton.siblings('input.hidden-field').attr('data-field'),
    505             attrs = ['mime_type']; // A list of attributes to look for
    506 
    507         // Make sure we got it
    508         if ('string' === typeof currentFieldId && '' !== currentFieldId) {
    509 
    510             // Make fields is defined and only do the hack for cropped_image
    511             if ('object' === typeof this.params.fields[currentFieldId] && 'upload' === this.params.fields[currentFieldId].type) {
    512 
    513                 // If the attribute exists in the field
    514                 if ('undefined' !== typeof this.params.fields[currentFieldId].mime_type) {
    515 
    516                     // Set the attribute in the main object
    517                     return this.params.fields[currentFieldId].mime_type;
    518                 }
    519             }
    520         }
    521 
    522         return 'image';
    523 
    524     },
    525 
    526     removeImage: function (event) {
    527 
    528         'use strict';
    529 
    530         var $targetDiv,
    531             $uploadButton;
    532 
    533         if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
    534             return;
    535         }
    536 
    537         $targetDiv = this.$thisButton.closest('.repeater-field-image,.repeater-field-cropped_image,.repeater-field-upload');
    538         $uploadButton = $targetDiv.find('.upload-button');
    539 
    540         $targetDiv.find('.kirki-image-attachment').slideUp('fast', function () {
    541             jQuery(this).show().html(jQuery(this).data('placeholder'));
    542         });
    543         $targetDiv.find('.hidden-field').val('');
    544         $uploadButton.text($uploadButton.data('label'));
    545         this.$thisButton.hide();
    546 
    547         $targetDiv.find('input, textarea, select').trigger('change');
    548 
    549     },
    550 
    551     removeFile: function (event) {
    552 
    553         'use strict';
    554 
    555         var $targetDiv,
    556             $uploadButton;
    557 
    558         if (wp.customize.utils.isKeydownButNotEnterEvent(event)) {
    559             return;
    560         }
    561 
    562         $targetDiv = this.$thisButton.closest('.repeater-field-upload');
    563         $uploadButton = $targetDiv.find('.upload-button');
    564 
    565         $targetDiv.find('.kirki-file-attachment').slideUp('fast', function () {
    566             jQuery(this).show().html(jQuery(this).data('placeholder'));
    567         });
    568         $targetDiv.find('.hidden-field').val('');
    569         $uploadButton.text($uploadButton.data('label'));
    570         this.$thisButton.hide();
    571 
    572         $targetDiv.find('input, textarea, select').trigger('change');
    573 
    574     },
    575 
    576     /**
    577      * Get the current value of the setting
    578      *
    579      * @return Object
    580      */
    581     getValue: function () {
    582 
    583         'use strict';
    584 
    585         // The setting is saved in JSON
    586 
    587         var value = [];
    588 
    589         if (_.isString(this.setting.get())) {
    590             value = JSON.parse(decodeURI(this.setting.get()));
    591         } else {
    592             value = this.setting.get();
    593         }
    594 
    595         return JSON.parse(JSON.stringify(_.toArray(value)));
    596 
    597     },
    598 
    599     /**
    600      * Set a new value for the setting
    601      *
    602      * @param newValue Object
    603      * @param refresh If we want to refresh the previewer or not
    604      */
    605 
    606 
    607     putValueInSetting: _.debounce(function () {
    608         var newValue = JSON.parse(JSON.stringify(this.__valueToSet));
    609         this.setting.set(newValue);
    610 
    611         var self = this;
    612         newValue.forEach(function (item, index) {
    613 
    614             if (!self.rows[index]) {
    615                 return;
    616             }
    617 
    618             var container = self.rows[index].container;
    619             for (var field in item) {
    620                 var oldValue = container.find('[data-field="' + field + '"]').val();
    621                 if (item.hasOwnProperty(field) && oldValue !== item[field]) {
    622                     container.find('[data-field="' + field + '"]').val(item[field]).trigger('change');
    623                 }
    624             }
    625         });
    626     }, 300),
    627 
    628 
    629     filterValue: function (newValue) {
    630         var filteredValue = newValue,
    631             filter = [];
    632 
    633         jQuery.each(this.params.fields, function (index, value) {
    634             if ('image' === value.type || 'cropped_image' === value.type || 'upload' === value.type) {
    635                 filter.push(index);
    636             }
    637         });
    638         jQuery.each(newValue, function (index, value) {
    639             jQuery.each(filter, function (ind, field) {
    640                 if ('undefined' !== typeof value[field] && 'undefined' !== typeof value[field].id) {
    641                     filteredValue[index][field] = value[field].id;
    642                 }
    643             });
    644         });
    645 
    646         return filteredValue;
    647 
    648     },
    649 
    650     normalizeValue: function (value, convertToArray) {
    651 
    652         if (_.isString(value)) {
    653 
    654             try {
    655                 value = decodeURI(value);
    656 
    657             } catch (e) {
    658 
    659             }
    660 
    661             try {
    662                 value = JSON.parse(value);
    663 
    664             } catch (e) {
    665 
    666             }
    667 
    668         }
    669 
    670         if (_.isObject(value) && convertToArray) {
    671             var hasOnlyNumberKeys = _.keys(value).map(function (k) {
    672                 return _.isNumber(parseInt(k))
    673             }).reduce(function (a, b) {
    674                 return (a && b);
    675             }, true);
    676 
    677             if (hasOnlyNumberKeys) {
    678                 var newValue = [];
    679                 _.keys(value).forEach(function (k) {
    680 
    681                     if (_.isUndefined(value[k])) {
    682                         return;
    683                     }
    684 
    685                     newValue.push(value[k]);
    686                 });
    687 
    688                 value = newValue;
    689             }
    690         }
    691 
    692         return value;
    693     },
    694 
    695     setValue: function (newValue, filtering) {
    696 
    697         'use strict';
    698 
    699         this.__valueToSet = this.getValue();
    700         // 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
    701 
    702         if (filtering) {
    703             newValue = this.filterValue(newValue);
    704         }
    705 
    706         if (this.params.choices.beforeValueSet && window[this.params.choices.beforeValueSet]) {
    707             newValue = window[this.params.choices.beforeValueSet].call(this, newValue);
    708         }
    709 
    710         newValue = this.normalizeValue(newValue, true);
    711 
    712         this.__valueToSet = newValue;
    713         this.putValueInSetting();
    714 
    715     },
    716 
    717     /**
    718      * Add a new row to repeater settings based on the structure.
    719      *
    720      * @param data (Optional) Object of field => value pairs (undefined if you want to get the default values)
    721      */
    722     addRow: function (data, silent) {
    723 
    724         'use strict';
    725 
    726         var control = this,
    727             template = control.repeaterTemplate(), // The template for the new row (defined on Kirki_Customize_Repeater_Control::render_content() ).
    728             settingValue = this.getValue(), // Get the current setting value.
    729             newRowSetting = {}, // Saves the new setting data.
    730             templateData, // Data to pass to the template
    731             newRow,
    732             i;
    733 
    734         if (template) {
    735 
    736             // The control structure is going to define the new fields
    737             // We need to clone control.params.fields. Assigning it
    738             // ould result in a reference assignment.
    739             templateData = jQuery.extend(true, {}, control.params.fields);
    740 
    741             // But if we have passed data, we'll use the data values instead
    742             if (data) {
    743                 for (i in data) {
    744                     if (data.hasOwnProperty(i) && templateData.hasOwnProperty(i)) {
    745                         templateData[i]['default'] = data[i];
    746                     }
    747                 }
    748             }
    749 
    750             templateData.index = this.currentIndex;
    751 
    752             // Append the template content
    753             template = template(templateData);
    754 
    755             // Create a new row object and append the element
    756             newRow = new RepeaterRow(
    757                 control.currentIndex,
    758                 jQuery(template).appendTo(control.repeaterFieldsContainer),
    759                 control.params.row_label
    760             );
    761 
    762             newRow.container.on('row:remove', function (e, rowIndex) {
    763                 control.deleteRow(rowIndex);
    764             });
    765 
    766             newRow.container.on('row:update', function (e, rowIndex, fieldName, element) {
    767                 control.updateField.call(control, e, rowIndex, fieldName, element);
    768                 newRow.updateLabel();
    769             });
    770 
    771             // Add the row to rows collection
    772             this.rows[this.currentIndex] = newRow;
    773 
    774             for (i in templateData) {
    775                 if (templateData.hasOwnProperty(i)) {
    776                     newRowSetting[i] = templateData[i]['default'];
    777                 }
    778             }
    779 
    780 
    781             if (!silent) {
    782                 settingValue[this.currentIndex] = newRowSetting;
    783                 this.setValue(settingValue);
    784             }
    785 
    786 
    787             this.currentIndex++;
    788 
    789             return newRow;
    790 
    791         }
    792 
    793     },
    794 
    795     sort: function () {
    796 
    797         'use strict';
    798 
    799         var control = this,
    800             $rows = this.repeaterFieldsContainer.find('.repeater-row'),
    801             newOrder = [],
    802             settings = control.getValue(),
    803             newRows = [],
    804             newSettings = [];
    805 
    806         $rows.each(function (i, element) {
    807             newOrder.push(jQuery(element).data('row'));
    808         });
    809 
    810         jQuery.each(newOrder, function (newPosition, oldPosition) {
    811             newRows[newPosition] = control.rows[oldPosition];
    812             newRows[newPosition].setRowIndex(newPosition);
    813 
    814             newSettings[newPosition] = settings[oldPosition];
    815         });
    816 
    817         control.rows = newRows;
    818         control.setValue(newSettings);
    819 
    820     },
    821 
    822     /**
    823      * Delete a row in the repeater setting
    824      *
    825      * @param index Position of the row in the complete Setting Array
    826      */
    827     deleteRow: function (index) {
    828 
    829         'use strict';
    830 
    831         var currentSettings = this.getValue(),
    832             row,
    833             i,
    834             prop;
    835 
    836         if (currentSettings[index]) {
    837 
    838             // Find the row
    839             row = this.rows[index];
    840             if (row) {
    841 
    842                 // The row exists, let's delete it
    843 
    844                 // Remove the row settings
    845                 if (_.isArray(currentSettings)) {
    846                     currentSettings.splice(index, 1)
    847                 } else {
    848                     delete currentSettings[index];
    849                 }
    850 
    851 
    852                 // Remove the row from the rows collection
    853 
    854                 if (_.isArray(this.rows)) {
    855                     this.rows.splice(index, 1)
    856                 } else {
    857                     delete this.rows[index];
    858                 }
    859 
    860 
    861                 // clean null
    862 
    863                 currentSettings = _.omit(currentSettings, _.isNull);
    864                 currentSettings = _.omit(currentSettings, _.isUndefined);
    865 
    866                 // Update the new setting values
    867                 this.setValue(currentSettings);
    868 
    869             }
    870 
    871         }
    872 
    873         // Remap the row numbers
    874         if (_.isArray(this.rows)) {
    875             this.rows.forEach(function (row, index) {
    876                 row.setRowIndex(index);
    877                 row.updateLabel();
    878             });
    879         } else {
    880             var i = 1;
    881             for (prop in this.rows) {
    882                 if (this.rows.hasOwnProperty(prop) && this.rows[prop]) {
    883                     this.rows[prop].updateLabel();
    884                     i++;
    885                 }
    886             }
    887         }
    888 
    889 
    890     },
    891 
    892     /**
    893      * Update a single field inside a row.
    894      * Triggered when a field has changed
    895      *
    896      * @param e Event Object
    897      */
    898     updateField: function (e, rowIndex, fieldId, element) {
    899 
    900         'use strict';
    901 
    902         var type,
    903             row,
    904             currentSettings;
    905 
    906         if (!this.rows[rowIndex]) {
    907             return;
    908         }
    909 
    910         if (!this.params.fields[fieldId]) {
    911             return;
    912         }
    913 
    914         type = this.params.fields[fieldId].type;
    915         row = this.rows[rowIndex];
    916         currentSettings = this.getValue();
    917 
    918         element = jQuery(element);
    919 
    920         if (undefined === typeof currentSettings[row.rowIndex][fieldId]) {
    921             return;
    922         }
    923 
    924 
    925         var value = currentSettings[row.rowIndex][fieldId];
    926         if ('checkbox' === type) {
    927 
    928             value = element.is(':checked');
    929 
    930         } else {
    931 
    932             // Update the settings
    933             value = element.val();
    934 
    935         }
    936 
    937         if (value !== currentSettings[row.rowIndex][fieldId]) {
    938             currentSettings[row.rowIndex][fieldId] = value;
    939             this.setValue(currentSettings);
    940         }
    941     },
    942 
    943     /**
    944      * Init the color picker on color fields
    945      * Called after AddRow
    946      *
    947      */
    948     initColorPicker: function () {
    949 
    950         'use strict';
    951 
    952         var control = this,
    953             colorPicker = control.container.find('.color-picker-hex'),
    954             options = {},
    955             fieldId = colorPicker.data('field');
    956 
    957         // We check if the color palette parameter is defined.
    958         if ('undefined' !== typeof fieldId && 'undefined' !== typeof control.params.fields[fieldId] && 'undefined' !== typeof control.params.fields[fieldId].palettes && 'object' === typeof control.params.fields[fieldId].palettes) {
    959             options.palettes = control.params.fields[fieldId].palettes;
    960         }
    961 
    962         // When the color picker value is changed we update the value of the field
    963         options.change = function (event, ui) {
    964 
    965             var currentPicker = jQuery(event.target),
    966                 row = currentPicker.closest('.repeater-row'),
    967                 rowIndex = row.data('row'),
    968                 currentSettings = control.getValue();
    969 
    970             currentSettings[rowIndex][currentPicker.data('field')] = ui.color.toString();
    971             control.setValue(currentSettings);
    972 
    973         };
    974 
    975         // Init the color picker
    976         if (0 !== colorPicker.length) {
    977             colorPicker.wpColorPicker(options);
    978         }
    979 
    980     },
    981 
    982     /**
    983      * Init the dropdown-pages field with selectize
    984      * Called after AddRow
    985      *
    986      * @param {object} theNewRow the row that was added to the repeater
    987      * @param {object} data the data for the row if we're initializing a pre-existing row
    988      *
    989      */
    990     initDropdownPages: function (theNewRow, data) {
    991 
    992         'use strict';
    993 
    994         var control = this,
    995             dropdown = theNewRow.container.find('.repeater-dropdown-pages select'),
    996             $select,
    997             selectize,
    998             dataField;
    999 
   1000         if (0 === dropdown.length) {
   1001             return;
   1002         }
   1003 
   1004         $select = jQuery(dropdown).selectize();
   1005         selectize = $select[0].selectize;
   1006         dataField = dropdown.data('field');
   1007 
   1008         if (data) {
   1009             selectize.setValue(data[dataField]);
   1010         }
   1011 
   1012         this.container.on('change', '.repeater-dropdown-pages select', function (event) {
   1013 
   1014             var currentDropdown = jQuery(event.target),
   1015                 row = currentDropdown.closest('.repeater-row'),
   1016                 rowIndex = row.data('row'),
   1017                 currentSettings = control.getValue();
   1018 
   1019             currentSettings[rowIndex][currentDropdown.data('field')] = jQuery(this).val();
   1020             control.setValue(currentSettings);
   1021 
   1022         });
   1023 
   1024     }
   1025 
   1026 });