PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.9.1
Secure Custom Fields v6.9.1
6.9.1 6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / assets / src / js / _field-group-field.js
secure-custom-fields / assets / src / js Last commit date
bindings 6 months ago commands 1 week ago pro 1 week ago _acf-compatibility.js 1 year ago _acf-condition-types.js 7 months ago _acf-condition.js 1 year ago _acf-conditions.js 1 year ago _acf-field-accordion.js 6 months ago _acf-field-button-group.js 7 months ago _acf-field-checkbox.js 1 month ago _acf-field-color-picker.js 7 months ago _acf-field-date-picker.js 1 month ago _acf-field-date-time-picker.js 1 month ago _acf-field-file.js 1 month ago _acf-field-google-map.js 6 months ago _acf-field-icon-picker.js 1 month ago _acf-field-image.js 1 month ago _acf-field-link.js 1 year ago _acf-field-oembed.js 1 month ago _acf-field-page-link.js 1 year ago _acf-field-post-object.js 1 year ago _acf-field-radio.js 1 month ago _acf-field-range.js 1 year ago _acf-field-relationship.js 1 month ago _acf-field-select.js 1 month ago _acf-field-tab.js 3 weeks ago _acf-field-taxonomy.js 7 months ago _acf-field-time-picker.js 1 month ago _acf-field-true-false.js 1 month ago _acf-field-url.js 1 year ago _acf-field-user.js 1 year ago _acf-field-wysiwyg.js 1 month ago _acf-field.js 1 year ago _acf-fields.js 1 year ago _acf-helpers.js 1 year ago _acf-hooks.js 1 year ago _acf-internal-post-type.js 7 months ago _acf-media.js 1 week ago _acf-modal.js 1 year ago _acf-model.js 1 year ago _acf-notice.js 7 months ago _acf-panel.js 1 year ago _acf-popup.js 7 months ago _acf-postbox.js 1 year ago _acf-screen.js 10 months ago _acf-select2.js 1 year ago _acf-tinymce.js 6 months ago _acf-tooltip.js 10 months ago _acf-unload.js 1 year ago _acf-validation.js 1 month ago _acf.js 7 months ago _browse-fields-modal.js 7 months ago _field-group-compatibility.js 1 year ago _field-group-conditions.js 1 year ago _field-group-field.js 3 months ago _field-group-fields.js 10 months ago _field-group-locations.js 1 year ago _field-group-settings.js 1 year ago _field-group.js 10 months ago acf-escaped-html-notice.js 1 year ago acf-field-group.js 1 year ago acf-input.js 1 year ago acf-internal-post-type.js 1 year ago acf.js 1 year ago
_field-group-field.js
1192 lines
1 ( function ( $, undefined ) {
2 acf.FieldObject = acf.Model.extend( {
3 // class used to avoid nested event triggers
4 eventScope: '.acf-field-object',
5
6 // variable for field type select2
7 fieldTypeSelect2: false,
8
9 // events
10 events: {
11 'click .copyable': 'onClickCopy',
12 'click .handle': 'onClickEdit',
13 'click .close-field': 'onClickEdit',
14 'click a[data-key="acf_field_settings_tabs"]':
15 'onChangeSettingsTab',
16 'click .delete-field': 'onClickDelete',
17 'click .duplicate-field': 'duplicate',
18 'click .move-field': 'move',
19 'click .browse-fields': 'browseFields',
20
21 'focus .edit-field': 'onFocusEdit',
22 'blur .edit-field, .row-options a': 'onBlurEdit',
23
24 'change .field-type': 'onChangeType',
25 'change .field-required': 'onChangeRequired',
26 'blur .field-label': 'onChangeLabel',
27 'blur .field-name': 'onChangeName',
28
29 change: 'onChange',
30 changed: 'onChanged',
31 },
32
33 // data
34 data: {
35 // Similar to ID, but used for HTML purposes.
36 // It is possible for a new field to have an ID of 0, but an id of 'field_123' */
37 id: 0,
38
39 // The field key ('field_123')
40 key: '',
41
42 // The field type (text, image, etc)
43 type: '',
44
45 // The $post->ID of this field
46 //ID: 0,
47
48 // The field's parent
49 //parent: 0,
50
51 // The menu order
52 //menu_order: 0
53 },
54
55 setup: function ( $field ) {
56 // set $el
57 this.$el = $field;
58
59 // inherit $field data (id, key, type)
60 this.inherit( $field );
61
62 // load additional props
63 // - this won't trigger 'changed'
64 this.prop( 'ID' );
65 this.prop( 'parent' );
66 this.prop( 'menu_order' );
67 },
68
69 $input: function ( name ) {
70 return $( '#' + this.getInputId() + '-' + name );
71 },
72
73 $meta: function () {
74 return this.$( '.meta:first' );
75 },
76
77 $handle: function () {
78 return this.$( '.handle:first' );
79 },
80
81 $settings: function () {
82 return this.$( '.settings:first' );
83 },
84
85 $setting: function ( name ) {
86 return this.$(
87 '.acf-field-settings:first .acf-field-setting-' + name
88 );
89 },
90
91 $fieldTypeSelect: function () {
92 return this.$( '.field-type' );
93 },
94
95 $fieldLabel: function () {
96 return this.$( '.field-label' );
97 },
98
99 getParent: function () {
100 return acf.getFieldObjects( { child: this.$el, limit: 1 } ).pop();
101 },
102
103 getParents: function () {
104 return acf.getFieldObjects( { child: this.$el } );
105 },
106
107 getFields: function () {
108 return acf.getFieldObjects( { parent: this.$el } );
109 },
110
111 getInputName: function () {
112 return 'acf_fields[' + this.get( 'id' ) + ']';
113 },
114
115 getInputId: function () {
116 return 'acf_fields-' + this.get( 'id' );
117 },
118
119 newInput: function ( name, value ) {
120 // vars
121 var inputId = this.getInputId();
122 var inputName = this.getInputName();
123
124 // append name
125 if ( name ) {
126 inputId += '-' + name;
127 inputName += '[' + name + ']';
128 }
129
130 // create input (avoid HTML + JSON value issues)
131 var $input = $( '<input />' ).attr( {
132 id: inputId,
133 name: inputName,
134 value: value,
135 } );
136 this.$( '> .meta' ).append( $input );
137
138 // return
139 return $input;
140 },
141
142 getProp: function ( name ) {
143 // check data
144 if ( this.has( name ) ) {
145 return this.get( name );
146 }
147
148 // get input value
149 var $input = this.$input( name );
150 var value = $input.length ? $input.val() : null;
151
152 // set data silently (cache)
153 this.set( name, value, true );
154
155 // return
156 return value;
157 },
158
159 setProp: function ( name, value ) {
160 // get input
161 var $input = this.$input( name );
162 var prevVal = $input.val();
163
164 // create if new
165 if ( ! $input.length ) {
166 $input = this.newInput( name, value );
167 }
168
169 // remove
170 if ( value === null ) {
171 $input.remove();
172
173 // update
174 } else {
175 $input.val( value );
176 }
177
178 //console.log('setProp', name, value, this);
179
180 // set data silently (cache)
181 if ( ! this.has( name ) ) {
182 //console.log('setting silently');
183 this.set( name, value, true );
184
185 // set data allowing 'change' event to fire
186 } else {
187 //console.log('setting loudly!');
188 this.set( name, value );
189 }
190
191 // return
192 return this;
193 },
194
195 prop: function ( name, value ) {
196 if ( value !== undefined ) {
197 return this.setProp( name, value );
198 } else {
199 return this.getProp( name );
200 }
201 },
202
203 props: function ( props ) {
204 Object.keys( props ).map( function ( key ) {
205 this.setProp( key, props[ key ] );
206 }, this );
207 },
208
209 getLabel: function () {
210 // get label with empty default
211 var label = this.prop( 'label' );
212 if ( label === '' ) {
213 label = acf.__( '(no label)' );
214 }
215
216 // return
217 return label;
218 },
219
220 getName: function () {
221 return this.prop( 'name' );
222 },
223
224 getType: function () {
225 return this.prop( 'type' );
226 },
227
228 getTypeLabel: function () {
229 var type = this.prop( 'type' );
230 var types = acf.get( 'fieldTypes' );
231 return types[ type ] ? types[ type ].label : type;
232 },
233
234 getKey: function () {
235 return this.prop( 'key' );
236 },
237
238 initialize: function () {
239 this.checkCopyable();
240 },
241
242 makeCopyable: function ( text ) {
243 if ( ! navigator.clipboard )
244 return (
245 '<span class="copyable copy-unsupported">' +
246 text +
247 '</span>'
248 );
249 return '<span class="copyable">' + text + '</span>';
250 },
251
252 checkCopyable: function () {
253 if ( ! navigator.clipboard ) {
254 this.$el.find( '.copyable' ).addClass( 'copy-unsupported' );
255 }
256 },
257
258 initializeFieldTypeSelect2: function () {
259 if ( this.fieldTypeSelect2 ) return;
260
261 // Support disabling via filter.
262 if ( this.$fieldTypeSelect().hasClass( 'disable-select2' ) ) return;
263
264 // Check for a full modern version of select2, bail loading if not found with a console warning.
265 try {
266 $.fn.select2.amd.require( 'select2/compat/dropdownCss' );
267 } catch ( err ) {
268 console.warn(
269 'ACF was not able to load the full version of select2 due to a conflicting version provided by another plugin or theme taking precedence. Select2 fields may not work as expected.'
270 );
271 return;
272 }
273
274 this.fieldTypeSelect2 = acf.newSelect2( this.$fieldTypeSelect(), {
275 field: false,
276 ajax: false,
277 multiple: false,
278 allowNull: false,
279 suppressFilters: true,
280 dropdownCssClass: 'field-type-select-results',
281 templateResult: function ( selection ) {
282 if (
283 selection.loading ||
284 ( selection.element &&
285 selection.element.nodeName === 'OPTGROUP' )
286 ) {
287 var $selection = $(
288 '<span class="acf-selection"></span>'
289 );
290 $selection.html( acf.strEscape( selection.text ) );
291 } else {
292 var $selection = $(
293 '<i class="field-type-icon field-type-icon-' +
294 selection.id.replaceAll( '_', '-' ) +
295 '"></i><span class="acf-selection has-icon">' +
296 acf.strEscape( selection.text ) +
297 '</span>'
298 );
299 }
300 $selection.data( 'element', selection.element );
301 return $selection;
302 },
303 templateSelection: function ( selection ) {
304 var $selection = $(
305 '<i class="field-type-icon field-type-icon-' +
306 selection.id.replaceAll( '_', '-' ) +
307 '"></i><span class="acf-selection has-icon">' +
308 acf.strEscape( selection.text ) +
309 '</span>'
310 );
311 $selection.data( 'element', selection.element );
312 return $selection;
313 },
314 } );
315
316 this.fieldTypeSelect2.on( 'select2:open', function () {
317 $(
318 '.field-type-select-results input.select2-search__field'
319 ).attr( 'placeholder', acf.__( 'Type to search...' ) );
320 } );
321
322 this.fieldTypeSelect2.on( 'change', function ( e ) {
323 $( e.target )
324 .parents( 'ul:first' )
325 .find( 'button.browse-fields' )
326 .prop( 'disabled', true );
327 } );
328
329 // When typing happens on the li element above the select2.
330 this.fieldTypeSelect2.$el
331 .parent()
332 .on(
333 'keydown',
334 '.select2-selection.select2-selection--single',
335 this.onKeyDownSelect
336 );
337 },
338
339 addProFields: function () {
340 // Don't run if on pro.
341 if ( acf.get( 'is_pro' ) ) {
342 return;
343 }
344
345 // Make sure we haven't appended these fields before.
346 var $fieldTypeSelect = this.$fieldTypeSelect();
347 if ( $fieldTypeSelect.hasClass( 'acf-free-field-type' ) ) return;
348
349 // Loop over each pro field type and append it to the select.
350 const PROFieldTypes = acf.get( 'PROFieldTypes' );
351 if ( typeof PROFieldTypes !== 'object' ) return;
352
353 const $layoutGroup = $fieldTypeSelect
354 .find( 'optgroup option[value="group"]' )
355 .parent();
356
357 const $contentGroup = $fieldTypeSelect
358 .find( 'optgroup option[value="image"]' )
359 .parent();
360
361 for ( const [ name, field ] of Object.entries( PROFieldTypes ) ) {
362 const $useGroup =
363 field.category === 'content' ? $contentGroup : $layoutGroup;
364 const $existing = $useGroup.children(
365 '[value="' + name + '"]'
366 );
367 const label = `${ acf.strEscape(
368 field.label
369 ) } (${ acf.strEscape( acf.__( 'PRO Only' ) ) })`;
370
371 if ( $existing.length ) {
372 // Already added by pro, update existing option.
373 $existing.text( label );
374
375 // Don't disable if already selected (prevents re-save from overriding field type).
376 if ( $fieldTypeSelect.val() !== name ) {
377 $existing.attr( 'disabled', 'disabled' );
378 }
379 } else {
380 // Append new disabled option.
381 $useGroup.append(
382 `<option value="null" disabled="disabled">${ label }</option>`
383 );
384 }
385 }
386
387 $fieldTypeSelect.addClass( 'acf-free-field-type' );
388 },
389
390 render: function () {
391 // vars
392 var $handle = this.$( '.handle:first' );
393 var menu_order = this.prop( 'menu_order' );
394 var label = acf.strEscape( this.getLabel() );
395 var name = this.prop( 'name' );
396 var type = this.getTypeLabel();
397 var key = this.prop( 'key' );
398 var required = this.$input( 'required' ).prop( 'checked' );
399
400 // update menu order
401 $handle.find( '.acf-icon' ).html( parseInt( menu_order ) + 1 );
402
403 // update required
404 if ( required ) {
405 label += ' <span class="acf-required">*</span>';
406 }
407
408 // update label
409 $handle.find( '.li-field-label strong a' ).html( label );
410 let shouldConvertToLowercase = name === name.toLowerCase();
411 shouldConvertToLowercase = acf.applyFilters(
412 'convert_field_name_to_lowercase',
413 shouldConvertToLowercase,
414 this
415 );
416
417 // update name
418 $handle
419 .find( '.li-field-name' )
420 .html(
421 this.makeCopyable(
422 acf.strSanitize( name, shouldConvertToLowercase )
423 )
424 );
425
426 // update type
427 const iconName = acf.strSlugify( this.getType() );
428 $handle.find( '.field-type-label' ).text( ' ' + type );
429 $handle
430 .find( '.field-type-icon' )
431 .removeClass()
432 .addClass( 'field-type-icon field-type-icon-' + iconName );
433
434 // update key
435 $handle.find( '.li-field-key' ).html( this.makeCopyable( key ) );
436
437 // action for 3rd party customization
438 acf.doAction( 'render_field_object', this );
439 },
440
441 refresh: function () {
442 acf.doAction( 'refresh_field_object', this );
443 },
444
445 isOpen: function () {
446 return this.$el.hasClass( 'open' );
447 },
448
449 onClickCopy: function ( e ) {
450 e.stopPropagation();
451 if ( ! navigator.clipboard || $( e.target ).is( 'input' ) ) return;
452
453 // Find the value to copy depending on input or text elements.
454 let copyValue;
455 if ( $( e.target ).hasClass( 'acf-input-wrap' ) ) {
456 copyValue = $( e.target ).find( 'input' ).first().val();
457 } else {
458 copyValue = $( e.target ).text().trim();
459 }
460
461 navigator.clipboard.writeText( copyValue ).then( () => {
462 $( e.target ).closest( '.copyable' ).addClass( 'copied' );
463 setTimeout( function () {
464 $( e.target )
465 .closest( '.copyable' )
466 .removeClass( 'copied' );
467 }, 2000 );
468 } );
469 },
470
471 onClickEdit: function ( e ) {
472 const $target = $( e.target );
473
474 if (
475 $target.parent().hasClass( 'row-options' ) &&
476 ! $target.hasClass( 'edit-field' )
477 ) {
478 return;
479 }
480
481 this.isOpen() ? this.close() : this.open();
482 },
483
484 onChangeSettingsTab: function () {
485 const $settings = this.$el.children( '.settings' );
486 acf.doAction( 'show', $settings );
487 },
488
489 /**
490 * Adds 'active' class to row options nearest to the target.
491 */
492 onFocusEdit: function ( e ) {
493 var $rowOptions = $( e.target )
494 .closest( 'li' )
495 .find( '.row-options' );
496 $rowOptions.addClass( 'active' );
497 },
498
499 /**
500 * Removes 'active' class from row options if links in same row options area are no longer in focus.
501 */
502 onBlurEdit: function ( e ) {
503 var focusDelayMilliseconds = 50;
504 var $rowOptionsBlurElement = $( e.target )
505 .closest( 'li' )
506 .find( '.row-options' );
507
508 // Timeout so that `activeElement` gives the new element in focus instead of the body.
509 setTimeout( function () {
510 var $rowOptionsFocusElement = $( document.activeElement )
511 .closest( 'li' )
512 .find( '.row-options' );
513 if ( ! $rowOptionsBlurElement.is( $rowOptionsFocusElement ) ) {
514 $rowOptionsBlurElement.removeClass( 'active' );
515 }
516 }, focusDelayMilliseconds );
517 },
518
519 open: function () {
520 // vars
521 var $settings = this.$el.children( '.settings' );
522
523 // initialise field type select
524 this.addProFields();
525 this.initializeFieldTypeSelect2();
526
527 // action (open)
528 acf.doAction( 'open_field_object', this );
529 this.trigger( 'openFieldObject' );
530
531 // action (show)
532 acf.doAction( 'show', $settings );
533
534 this.hideEmptyTabs();
535
536 // open
537 $settings.slideDown();
538 this.$el.addClass( 'open' );
539 },
540
541 onKeyDownSelect: function ( e ) {
542 // Omit events from special keys.
543 if (
544 ! (
545 ( e.which >= 186 && e.which <= 222 ) || // punctuation and special characters
546 [
547 8, 9, 13, 16, 17, 18, 19, 20, 27, 32, 33, 34, 35, 36,
548 37, 38, 39, 40, 45, 46, 91, 92, 93, 144, 145,
549 ].includes( e.which ) || // Special keys
550 ( e.which >= 112 && e.which <= 123 )
551 )
552 ) {
553 // Function keys
554 $( this )
555 .closest( '.select2-container' )
556 .siblings( 'select:enabled' )
557 .select2( 'open' );
558 return;
559 }
560 },
561
562 close: function () {
563 // vars
564 var $settings = this.$el.children( '.settings' );
565
566 // close
567 $settings.slideUp();
568 this.$el.removeClass( 'open' );
569
570 // action (close)
571 acf.doAction( 'close_field_object', this );
572 this.trigger( 'closeFieldObject' );
573
574 // action (hide)
575 acf.doAction( 'hide', $settings );
576 },
577
578 serialize: function () {
579 return acf.serialize( this.$el, this.getInputName() );
580 },
581
582 save: function ( type ) {
583 // defaults
584 type = type || 'settings'; // meta, settings
585
586 // vars
587 var save = this.getProp( 'save' );
588
589 // bail if already saving settings
590 if ( save === 'settings' ) {
591 return;
592 }
593
594 // prop
595 this.setProp( 'save', type );
596
597 // debug
598 this.$el.attr( 'data-save', type );
599
600 // action
601 acf.doAction( 'save_field_object', this, type );
602 },
603
604 submit: function () {
605 // vars
606 var inputName = this.getInputName();
607 var save = this.get( 'save' );
608
609 // close
610 if ( this.isOpen() ) {
611 this.close();
612 }
613
614 // allow all inputs to save
615 if ( save == 'settings' ) {
616 // do nothing
617 // allow only meta inputs to save
618 } else if ( save == 'meta' ) {
619 this.$( '> .settings [name^="' + inputName + '"]' ).remove();
620
621 // prevent all inputs from saving
622 } else {
623 this.$( '[name^="' + inputName + '"]' ).remove();
624 }
625
626 // action
627 acf.doAction( 'submit_field_object', this );
628 },
629
630 onChange: function ( e, $el ) {
631 // save settings
632 this.save();
633
634 // action for 3rd party customization
635 acf.doAction( 'change_field_object', this );
636 },
637
638 onChanged: function ( e, $el, name, value ) {
639 if ( this.getType() === $el.attr( 'data-type' ) ) {
640 $( 'button.acf-btn.browse-fields' ).prop( 'disabled', false );
641 }
642
643 // ignore 'save'
644 if ( name == 'save' ) {
645 return;
646 }
647
648 // save meta
649 if ( [ 'menu_order', 'parent' ].indexOf( name ) > -1 ) {
650 this.save( 'meta' );
651
652 // save field
653 } else {
654 this.save();
655 }
656
657 // render
658 if (
659 [
660 'menu_order',
661 'label',
662 'required',
663 'name',
664 'type',
665 'key',
666 ].indexOf( name ) > -1
667 ) {
668 this.render();
669 }
670
671 // action for 3rd party customization
672 acf.doAction( 'change_field_object_' + name, this, value );
673 },
674
675 onChangeLabel: function ( e, $el ) {
676 // set
677 const label = $el.val();
678 const safeLabel = acf.strEscape( label );
679 this.set( 'label', safeLabel );
680
681 // render name
682 if ( this.prop( 'name' ) == '' ) {
683 var name = acf.applyFilters(
684 'generate_field_object_name',
685 acf.strSanitize( label ),
686 this
687 );
688 this.prop( 'name', name );
689 }
690 },
691
692 onChangeName: function ( e, $el ) {
693 const id = this.get( 'id' );
694 let forceSanitize = false;
695 // If id is not a number or is zero, force sanitize
696 if ( typeof id !== 'number' || id === 0 ) {
697 forceSanitize = true;
698 }
699
700 // Get the input's value attribute
701 const valueAttr = $el.val();
702
703 // If value is a lowercase string, force sanitize
704 if (
705 typeof valueAttr === 'string' &&
706 valueAttr === valueAttr.toLowerCase()
707 ) {
708 forceSanitize = true;
709 }
710
711 forceSanitize = acf.applyFilters(
712 'convert_field_name_to_lowercase',
713 forceSanitize,
714 this
715 );
716
717 // Sanitize the input value (force if needed)
718 const sanitized = acf.strSanitize( $el.val(), forceSanitize );
719
720 // Set the sanitized value back to the input
721 $el.val( sanitized );
722
723 // Update the field's name property
724 this.set( 'name', sanitized );
725
726 // Warn if the name starts with "field_"
727 if ( sanitized.startsWith( 'field_' ) ) {
728 alert(
729 acf.__(
730 'The string "field_" may not be used at the start of a field name'
731 )
732 );
733 }
734 },
735
736 onChangeRequired: function ( e, $el ) {
737 // set
738 var required = $el.prop( 'checked' ) ? 1 : 0;
739 this.set( 'required', required );
740 },
741
742 delete: function ( args ) {
743 // defaults
744 args = acf.parseArgs( args, {
745 animate: true,
746 } );
747
748 // add to remove list
749 var id = this.prop( 'ID' );
750
751 if ( id ) {
752 var $input = $( '#_acf_delete_fields' );
753 var newVal = $input.val() + '|' + id;
754 $input.val( newVal );
755 }
756
757 // action
758 acf.doAction( 'delete_field_object', this );
759
760 // animate
761 if ( args.animate ) {
762 this.removeAnimate();
763 } else {
764 this.remove();
765 }
766 },
767
768 onClickDelete: function ( e, $el ) {
769 // Bypass confirmation when holding down "shift" key.
770 if ( e.shiftKey ) {
771 return this.delete();
772 }
773
774 // add class
775 this.$el.addClass( '-hover' );
776
777 // add tooltip
778 var tooltip = acf.newTooltip( {
779 confirmRemove: true,
780 target: $el,
781 context: this,
782 confirm: function () {
783 this.delete();
784 },
785 cancel: function () {
786 this.$el.removeClass( '-hover' );
787 },
788 } );
789 },
790
791 removeAnimate: function () {
792 // vars
793 var field = this;
794 var $list = this.$el.parent();
795 var $fields = acf.findFieldObjects( {
796 sibling: this.$el,
797 } );
798
799 // remove
800 acf.remove( {
801 target: this.$el,
802 endHeight: $fields.length ? 0 : 50,
803 complete: function () {
804 field.remove();
805 acf.doAction( 'removed_field_object', field, $list );
806 },
807 } );
808
809 // action
810 acf.doAction( 'remove_field_object', field, $list );
811 },
812
813 duplicate: function () {
814 // vars
815 var newKey = acf.uniqid( 'field_' );
816
817 // duplicate
818 var $newField = acf.duplicate( {
819 target: this.$el,
820 search: this.get( 'id' ),
821 replace: newKey,
822 } );
823
824 // set new key
825 $newField.attr( 'data-key', newKey );
826
827 // get instance
828 var newField = acf.getFieldObject( $newField );
829
830 // update newField label / name
831 var label = newField.prop( 'label' );
832 var name = newField.prop( 'name' );
833 var end = name.split( '_' ).pop();
834 var copy = acf.__( 'copy' );
835
836 // increase suffix "1"
837 if ( acf.isNumeric( end ) ) {
838 var i = end * 1 + 1;
839 label = label.replace( end, i );
840 name = name.replace( end, i );
841
842 // increase suffix "(copy1)"
843 } else if ( end.indexOf( copy ) === 0 ) {
844 var i = end.replace( copy, '' ) * 1;
845 i = i ? i + 1 : 2;
846
847 // replace
848 label = label.replace( end, copy + i );
849 name = name.replace( end, copy + i );
850
851 // add default "(copy)"
852 } else {
853 label += ' (' + copy + ')';
854 name += '_' + copy;
855 }
856
857 newField.prop( 'ID', 0 );
858 newField.prop( 'label', label );
859 newField.prop( 'name', name );
860 newField.prop( 'key', newKey );
861
862 // close the current field if it's open.
863 if ( this.isOpen() ) {
864 this.close();
865 }
866
867 // open the new field and initialise correctly.
868 newField.open();
869
870 // focus label
871 var $label = newField.$setting( 'label input' );
872 setTimeout( function () {
873 $label.trigger( 'focus' );
874 }, 251 );
875
876 // action
877 acf.doAction( 'duplicate_field_object', this, newField );
878 acf.doAction( 'append_field_object', newField );
879 },
880
881 wipe: function () {
882 // vars
883 var prevId = this.get( 'id' );
884 var prevKey = this.get( 'key' );
885 var newKey = acf.uniqid( 'field_' );
886
887 // rename
888 acf.rename( {
889 target: this.$el,
890 search: prevId,
891 replace: newKey,
892 } );
893
894 // data
895 this.set( 'id', newKey );
896 this.set( 'prevId', prevId );
897 this.set( 'prevKey', prevKey );
898
899 // props
900 this.prop( 'key', newKey );
901 this.prop( 'ID', 0 );
902
903 // attr
904 this.$el.attr( 'data-key', newKey );
905 this.$el.attr( 'data-id', newKey );
906
907 // action
908 acf.doAction( 'wipe_field_object', this );
909 },
910
911 move: function () {
912 // helper
913 var hasChanged = function ( field ) {
914 return field.get( 'save' ) == 'settings';
915 };
916
917 // vars
918 var changed = hasChanged( this );
919
920 // has sub fields changed
921 if ( ! changed ) {
922 acf.getFieldObjects( {
923 parent: this.$el,
924 } ).map( function ( field ) {
925 changed = hasChanged( field ) || field.changed;
926 } );
927 }
928
929 // bail early if changed
930 if ( changed ) {
931 alert(
932 acf.__(
933 'This field cannot be moved until its changes have been saved'
934 )
935 );
936 return;
937 }
938
939 // step 1.
940 var id = this.prop( 'ID' );
941 var field = this;
942 var popup = false;
943 var step1 = function () {
944 // popup
945 popup = acf.newPopup( {
946 title: acf.__( 'Move Custom Field' ),
947 loading: true,
948 width: '300px',
949 openedBy: field.$el.find( '.move-field' ),
950 } );
951
952 // ajax
953 var ajaxData = {
954 action: 'acf/field_group/move_field',
955 field_id: id,
956 };
957
958 // get HTML
959 $.ajax( {
960 url: acf.get( 'ajaxurl' ),
961 data: acf.prepareForAjax( ajaxData ),
962 type: 'post',
963 dataType: 'html',
964 success: step2,
965 } );
966 };
967
968 var step2 = function ( html ) {
969 // update popup
970 popup.loading( false );
971 popup.content( html );
972
973 // submit form
974 popup.on( 'submit', 'form', step3 );
975 };
976
977 var step3 = function ( e, $el ) {
978 // prevent
979 e.preventDefault();
980
981 // disable
982 acf.startButtonLoading( popup.$( '.button' ) );
983
984 // ajax
985 var ajaxData = {
986 action: 'acf/field_group/move_field',
987 field_id: id,
988 field_group_id: popup.$( 'select' ).val(),
989 };
990
991 // get HTML
992 $.ajax( {
993 url: acf.get( 'ajaxurl' ),
994 data: acf.prepareForAjax( ajaxData ),
995 type: 'post',
996 dataType: 'html',
997 success: step4,
998 } );
999 };
1000
1001 var step4 = function ( html ) {
1002 popup.content( html );
1003
1004 if ( wp.a11y && wp.a11y.speak && acf.__ ) {
1005 wp.a11y.speak(
1006 acf.__( 'Field moved to other group' ),
1007 'polite'
1008 );
1009 }
1010
1011 popup.$( '.acf-close-popup' ).trigger( 'focus' );
1012
1013 field.removeAnimate();
1014 };
1015
1016 // start
1017 step1();
1018 },
1019
1020 browseFields: function ( e, $el ) {
1021 e.preventDefault();
1022
1023 const modal = acf.newBrowseFieldsModal( {
1024 openedBy: this,
1025 } );
1026 },
1027
1028 onChangeType: function ( e, $el ) {
1029 // clea previous timout
1030 if ( this.changeTimeout ) {
1031 clearTimeout( this.changeTimeout );
1032 }
1033
1034 // set new timeout
1035 // - prevents changing type multiple times whilst user types in newType
1036 this.changeTimeout = this.setTimeout( function () {
1037 this.changeType( $el.val() );
1038 }, 300 );
1039 },
1040
1041 changeType: function ( newType ) {
1042 var prevType = this.prop( 'type' );
1043 var prevClass = acf.strSlugify( 'acf-field-object-' + prevType );
1044 var newClass = acf.strSlugify( 'acf-field-object-' + newType );
1045
1046 // Update props.
1047 this.$el.removeClass( prevClass ).addClass( newClass );
1048 this.$el.attr( 'data-type', newType );
1049 this.$el.data( 'type', newType );
1050
1051 // Abort XHR if this field is already loading AJAX data.
1052 if ( this.has( 'xhr' ) ) {
1053 this.get( 'xhr' ).abort();
1054 }
1055
1056 // Store old settings so they can be reused later.
1057 const $oldSettings = {};
1058
1059 this.$el
1060 .find(
1061 '.acf-field-settings:first > .acf-field-settings-main > .acf-field-type-settings'
1062 )
1063 .each( function () {
1064 let tab = $( this ).data( 'parent-tab' );
1065 let $tabSettings = $( this ).children().removeData();
1066
1067 $oldSettings[ tab ] = $tabSettings;
1068
1069 $tabSettings.detach();
1070 } );
1071
1072 this.set( 'settings-' + prevType, $oldSettings );
1073
1074 // Show the settings if we already have them cached.
1075 if ( this.has( 'settings-' + newType ) ) {
1076 let $newSettings = this.get( 'settings-' + newType );
1077
1078 this.showFieldTypeSettings( $newSettings );
1079 this.set( 'type', newType );
1080 return;
1081 }
1082
1083 // Add loading spinner.
1084 const $loading = $(
1085 '<div class="acf-field"><div class="acf-input"><div class="acf-loading"></div></div></div>'
1086 );
1087 this.$el
1088 .find(
1089 '.acf-field-settings-main-general .acf-field-type-settings'
1090 )
1091 .before( $loading );
1092
1093 const ajaxData = {
1094 action: 'acf/field_group/render_field_settings',
1095 field: this.serialize(),
1096 prefix: this.getInputName(),
1097 };
1098
1099 // Get the settings for this field type over AJAX.
1100 var xhr = $.ajax( {
1101 url: acf.get( 'ajaxurl' ),
1102 data: acf.prepareForAjax( ajaxData ),
1103 type: 'post',
1104 dataType: 'json',
1105 context: this,
1106 success: function ( response ) {
1107 if ( ! acf.isAjaxSuccess( response ) ) {
1108 return;
1109 }
1110
1111 this.showFieldTypeSettings( response.data );
1112 },
1113 complete: function () {
1114 // also triggered by xhr.abort();
1115 $loading.remove();
1116 this.set( 'type', newType );
1117 //this.refresh();
1118 },
1119 } );
1120
1121 // set
1122 this.set( 'xhr', xhr );
1123 },
1124
1125 showFieldTypeSettings: function ( settings ) {
1126 if ( 'object' !== typeof settings ) {
1127 return;
1128 }
1129
1130 const self = this;
1131 const tabs = Object.keys( settings );
1132
1133 tabs.forEach( ( tab ) => {
1134 const $tab = self.$el.find(
1135 '.acf-field-settings-main-' +
1136 tab.replace( '_', '-' ) +
1137 ' .acf-field-type-settings'
1138 );
1139 let tabContent = '';
1140
1141 if (
1142 [ 'object', 'string' ].includes( typeof settings[ tab ] )
1143 ) {
1144 tabContent = settings[ tab ];
1145 }
1146
1147 $tab.prepend( tabContent );
1148 acf.doAction( 'append', $tab );
1149 } );
1150
1151 this.hideEmptyTabs();
1152 },
1153
1154 updateParent: function () {
1155 // vars
1156 var ID = acf.get( 'post_id' );
1157
1158 // check parent
1159 var parent = this.getParent();
1160 if ( parent ) {
1161 ID = parseInt( parent.prop( 'ID' ) ) || parent.prop( 'key' );
1162 }
1163
1164 // update
1165 this.prop( 'parent', ID );
1166 },
1167
1168 hideEmptyTabs: function () {
1169 const $settings = this.$settings();
1170 const $tabs = $settings.find(
1171 '.acf-field-settings:first > .acf-field-settings-main'
1172 );
1173
1174 $tabs.each( function () {
1175 const $tabContent = $( this );
1176 const tabName = $tabContent
1177 .find( '.acf-field-type-settings:first' )
1178 .data( 'parentTab' );
1179 const $tabLink = $settings
1180 .find( '.acf-settings-type-' + tabName )
1181 .first();
1182
1183 if ( $.trim( $tabContent.text() ) === '' ) {
1184 $tabLink.hide();
1185 } else if ( $tabLink.is( ':hidden' ) ) {
1186 $tabLink.show();
1187 }
1188 } );
1189 },
1190 } );
1191 } )( jQuery );
1192