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