|
7 | 7 | 'click [data-name="add-layout"]': 'onClickAdd', |
8 | 8 | 'click [data-name="duplicate-layout"]': 'onClickDuplicate', |
9 | 9 | 'click [data-name="collapse-layout"]': 'onClickCollapse', |
| 10 | + 'click [data-name="remove-layout"]': 'onClickRemove', |
10 | 11 | 'click [data-name="more-layout-actions"]': 'onClickMoreActions', |
11 | 12 | 'click .acf-fc-expand-all': 'onClickExpandAll', |
12 | 13 | 'click .acf-fc-collapse-all': 'onClickCollapseAll', |
|
56 | 57 | }, |
57 | 58 |
|
58 | 59 | $button: function () { |
59 | | - return this.$( '.acf-actions:last .button' ); |
| 60 | + return this.$( |
| 61 | + '.acf-fc-top-actions:first .button, .acf-actions:last .button' |
| 62 | + ); |
60 | 63 | }, |
61 | 64 |
|
62 | 65 | $popup: function () { |
|
131 | 134 | }, |
132 | 135 |
|
133 | 136 | allowAdd: function () { |
134 | | - var max = parseInt( this.get( 'max' ) ); |
135 | | - return ! max || max > this.val(); |
| 137 | + return ! this.isFull(); |
136 | 138 | }, |
137 | 139 |
|
138 | 140 | isFull: function () { |
139 | | - var max = parseInt( this.get( 'max' ) ); |
140 | | - return max && this.val() >= max; |
| 141 | + const max = parseInt( this.get( 'max' ) ); |
| 142 | + return max && this.countLayouts() >= max; |
141 | 143 | }, |
142 | 144 |
|
143 | 145 | addSortable: function ( self ) { |
|
233 | 235 | } else { |
234 | 236 | this.$control().removeClass( '-empty' ); |
235 | 237 | } |
| 238 | + this.maybeDisableAddLayout(); |
236 | 239 |
|
237 | 240 | // max |
238 | 241 | if ( this.isFull() ) { |
|
241 | 244 | this.$button().removeClass( 'disabled' ); |
242 | 245 | } |
243 | 246 | }, |
| 247 | + maybeDisableAddLayout: function () { |
| 248 | + const self = this; |
| 249 | + const isFull = this.isFull(); |
| 250 | + const buttonLabel = this.$control().data( 'button-label' ); |
| 251 | + const duplicateLabel = acf.__( 'Duplicate' ); |
| 252 | + const maxRowsReached = acf |
| 253 | + .__( 'Maximum rows reached ({max})' ) |
| 254 | + .replace( '{max}', this.get( 'max' ) ); |
| 255 | + |
| 256 | + // Disable/enable main add buttons |
| 257 | + if ( isFull ) { |
| 258 | + this.$button().addClass( 'disabled' ); |
| 259 | + } else { |
| 260 | + this.$button().removeClass( 'disabled' ); |
| 261 | + } |
| 262 | + |
| 263 | + // Process each layout |
| 264 | + this.$layouts().each( function () { |
| 265 | + const $layout = $( this ); |
| 266 | + const $addButton = $layout.find( |
| 267 | + '[data-name="add-layout"]:first' |
| 268 | + ); |
| 269 | + const $duplicateButton = $layout.find( |
| 270 | + '[data-name="duplicate-layout"]:first' |
| 271 | + ); |
| 272 | + const layoutMax = $layout.data( 'max' ) || 0; |
| 273 | + const $enableToggle = $( '.acf-toggle-layout.enable' ); |
| 274 | + |
| 275 | + // Handle field-level max |
| 276 | + if ( isFull ) { |
| 277 | + $addButton |
| 278 | + .addClass( 'disabled' ) |
| 279 | + .attr( 'title', maxRowsReached ); |
| 280 | + $duplicateButton |
| 281 | + .addClass( 'disabled' ) |
| 282 | + .attr( 'title', maxRowsReached ); |
| 283 | + $enableToggle.addClass( 'disabled' ); |
| 284 | + } else { |
| 285 | + $addButton |
| 286 | + .removeClass( 'disabled' ) |
| 287 | + .attr( 'title', buttonLabel ); |
| 288 | + $duplicateButton |
| 289 | + .removeClass( 'disabled' ) |
| 290 | + .attr( 'title', duplicateLabel ); |
| 291 | + $enableToggle.removeClass( 'disabled' ); |
| 292 | + } |
| 293 | + |
| 294 | + // Handle layout-specific max |
| 295 | + if ( layoutMax ) { |
| 296 | + const layoutName = $layout.data( 'layout' ) || ''; |
| 297 | + const layoutCount = self.countLayouts( layoutName ); |
| 298 | + |
| 299 | + if ( layoutCount >= layoutMax ) { |
| 300 | + const maxLayoutReached = acf |
| 301 | + .__( |
| 302 | + 'Maximum {label} {identifier} reached ({max})' |
| 303 | + ) |
| 304 | + .replace( '{label}', layoutName ) |
| 305 | + .replace( |
| 306 | + '{identifier}', |
| 307 | + acf._n( 'layout', 'layouts', layoutMax ) |
| 308 | + ) |
| 309 | + .replace( '{max}', layoutMax ); |
| 310 | + |
| 311 | + $duplicateButton |
| 312 | + .addClass( 'disabled' ) |
| 313 | + .attr( 'title', maxLayoutReached ); |
| 314 | + $enableToggle.addClass( 'disabled' ); |
| 315 | + } |
| 316 | + } |
| 317 | + } ); |
| 318 | + }, |
244 | 319 |
|
245 | 320 | setActiveLayout: function ( $layout ) { |
246 | 321 | // Remove active-layout class from all layouts |
|
267 | 342 |
|
268 | 343 | countLayouts: function ( name ) { |
269 | 344 | return this.$layouts().filter( function () { |
270 | | - return $( this ).data( 'layout' ) === name; |
| 345 | + const $layout = $( this ); |
| 346 | + return ( |
| 347 | + ( ! name || $layout.data( 'layout' ) === name ) && |
| 348 | + '0' !== $layout.attr( 'data-enabled' ) |
| 349 | + ); |
271 | 350 | } ).length; |
272 | 351 | }, |
273 | | - |
274 | 352 | countLayoutsByName: function ( currentLayout ) { |
275 | 353 | const layoutMax = currentLayout.data( 'max' ); |
276 | 354 | if ( ! layoutMax ) { |
|
487 | 565 | // Disable the layout |
488 | 566 | layout.attr( 'data-enabled', '0' ); |
489 | 567 | disabledInput.val( '1' ); |
490 | | - } else { |
491 | | - // Enable the layout |
| 568 | + } else if ( |
| 569 | + layout.attr( 'data-enabled' ) === '0' && |
| 570 | + this.countLayoutsByName( layout.first() ) && |
| 571 | + ! this.isFull() |
| 572 | + ) { |
| 573 | + // Enable the layout only if validation passes |
492 | 574 | layout.attr( 'data-enabled', '1' ); |
493 | 575 | disabledInput.val( '0' ); |
494 | 576 | } |
495 | 577 |
|
496 | 578 | // Trigger change event to save the state |
497 | 579 | this.$input().trigger( 'change' ); |
| 580 | + this.maybeDisableAddLayout(); |
498 | 581 | }, |
499 | 582 | onClickRenameLayout: function ( event, layout ) { |
500 | 583 | const currentName = layout |
501 | 584 | .find( '.acf-fc-layout-custom-label:first' ) |
502 | 585 | .val(); |
503 | | - |
504 | 586 | const popupOptions = { |
505 | 587 | context: this, |
506 | 588 | title: acf.__( 'Rename Layout' ), |
|
532 | 614 | const titleElement = layout.find( '.acf-fc-layout-title:first' ); |
533 | 615 |
|
534 | 616 | // Update the visible title |
535 | | - titleElement.text( newName ); |
| 617 | + titleElement.html( acf.escHtml( newName ) ); |
536 | 618 |
|
537 | 619 | if ( newName.length ) { |
538 | 620 | // Mark as renamed with custom label |
|
541 | 623 | // Restore original title if name is empty |
542 | 624 | let originalTitle = layout |
543 | 625 | .find( '.acf-fc-layout-original-title:first' ) |
544 | | - .text() |
| 626 | + .html() |
545 | 627 | .trim(); |
546 | 628 |
|
547 | 629 | // Remove parentheses from original title |
|
550 | 632 | originalTitle.length - 1 |
551 | 633 | ); |
552 | 634 |
|
553 | | - titleElement.text( originalTitle ); |
| 635 | + titleElement.html( acf.escHtml( originalTitle ) ); |
554 | 636 | layout.attr( 'data-renamed', '0' ); |
555 | 637 | } |
556 | 638 |
|
557 | 639 | // Trigger change event to save the state |
558 | 640 | this.$input().trigger( 'change' ); |
559 | 641 | }, |
560 | | - |
561 | 642 | validateRemove: function () { |
562 | 643 | // return true if allowed |
563 | 644 | if ( this.allowRemove() ) { |
|
585 | 666 | }, |
586 | 667 |
|
587 | 668 | onClickRemove: function ( e, $el ) { |
| 669 | + const $layout = $el.closest( '.layout' ); |
| 670 | + |
588 | 671 | // Bypass confirmation when holding down "shift" key. |
589 | 672 | if ( e.shiftKey ) { |
590 | | - return this.removeLayout( $el ); |
| 673 | + return this.removeLayout( $layout ); |
591 | 674 | } |
| 675 | + |
592 | 676 | // add class |
593 | | - $el.addClass( '-hover' ); |
| 677 | + $layout.addClass( '-hover' ); |
594 | 678 |
|
595 | 679 | // add tooltip |
596 | 680 | const tooltipOptions = { |
597 | 681 | confirmRemove: true, |
598 | 682 | context: this, |
599 | 683 | title: acf.__( 'Delete Layout' ), |
600 | | - text: acf.__( 'Are you sure you want to delete this layout?' ), |
| 684 | + text: acf.__( 'Are you sure you want to delete the layout?' ), |
601 | 685 | textConfirm: acf.__( 'Delete' ), |
602 | 686 | textCancel: acf.__( 'Cancel' ), |
603 | | - openedBy: $el |
| 687 | + openedBy: $layout |
604 | 688 | .find( 'a[data-name="more-layout-actions"]' ) |
605 | 689 | .first(), |
606 | 690 | width: '500px', |
607 | | - confirm: function () { |
608 | | - this.removeLayout( $el ); |
| 691 | + confirm: function ( e, $el ) { |
| 692 | + this.removeLayout( $layout ); |
609 | 693 | }, |
610 | 694 | cancel: function () { |
611 | | - $el.removeClass( '-hover' ); |
| 695 | + $layout.removeClass( '-hover' ); |
612 | 696 | }, |
613 | 697 | }; |
| 698 | + |
614 | 699 | // Check if layout has a custom label |
615 | | - const customLabel = $el.data( 'label' ); |
616 | | - if ( customLabel && customLabel.length ) { |
| 700 | + const customLabel = $layout.data( 'label' ); |
| 701 | + if ( customLabel.length ) { |
617 | 702 | // Customize the popup title and text with the layout label |
618 | 703 | tooltipOptions.title = acf |
619 | 704 | .__( 'Delete %s' ) |
620 | | - .replace( '%s', acf.strEscape( customLabel ) ); |
| 705 | + .replace( '%s', acf.escHtml( customLabel ) ); |
621 | 706 | tooltipOptions.text = acf |
622 | 707 | .__( 'Are you sure you want to delete %s?' ) |
623 | 708 | .replace( '%s', customLabel ); |
|
626 | 711 | // Create and show the confirmation popup |
627 | 712 | acf.newPopup( tooltipOptions ); |
628 | 713 | }, |
629 | | - |
630 | 714 | removeLayout: function ( $layout ) { |
631 | 715 | // reference |
632 | 716 | var self = this; |
|
684 | 768 | confirm: function ( e, $el ) { |
685 | 769 | // Check if the clicked element is a toggle action |
686 | 770 | const action = $el.data( 'action' ); |
687 | | - if ( action === 'remove-layout' ) { |
688 | | - this.onClickRemove( e, $layout ); |
689 | | - } |
690 | 771 | if ( action === 'toggle-layout' ) { |
691 | 772 | this.onClickToggleLayout( e, $layout ); |
692 | 773 | } |
|
822 | 903 | this.position(); |
823 | 904 | }, |
824 | 905 | show: function () { |
825 | | - const $flexibleContent = this.get( 'target' ).closest( |
826 | | - '.acf-flexible-content' |
827 | | - ); |
828 | | - $( $flexibleContent ).append( this.$el ); |
| 906 | + $( 'body' ).append( this.$el ); |
829 | 907 | }, |
830 | 908 | position: function () { |
831 | 909 | const $popup = this.$el; |
832 | 910 | const $target = this.get( 'target' ); |
833 | 911 | const $container = $target.closest( '.acf-flexible-content' ); |
834 | 912 | positionPopup( $popup, $target, $container, 8 ); |
835 | 913 | }, |
836 | | - } ); |
837 | | - |
838 | | - /** |
| 914 | + } ); /** |
839 | 915 | * MoreLayoutActionsPopup |
840 | 916 | * |
841 | 917 | * Popup for showing more layout actions (remove, toggle, rename) |
|
858 | 934 | this.$el.removeClass( 'enable-layout' ); |
859 | 935 | } |
860 | 936 |
|
| 937 | + const context = this.get( 'context' ) || this; |
861 | 938 | const self = this; |
862 | 939 | setTimeout( function () { |
863 | 940 | self.$el.find( 'a' ).first().trigger( 'focus' ); |
| 941 | + context.maybeDisableAddLayout(); |
864 | 942 | }, 1 ); |
865 | 943 | }, |
866 | 944 |
|
867 | 945 | show: function () { |
868 | | - const $layout = this.get( 'target' ).closest( '.layout' ); |
869 | | - $layout.append( this.$el ); |
| 946 | + $( 'body' ).append( this.$el ); |
870 | 947 | }, |
871 | | - |
872 | 948 | position: function () { |
873 | 949 | const $popup = this.$el; |
874 | 950 | const $target = this.get( 'target' ); |
|
1015 | 1091 | if ( ! $target.length || ! $container.length ) return; |
1016 | 1092 |
|
1017 | 1093 | const targetOffset = $target.offset(); |
1018 | | - const containerOffset = $container.offset(); |
1019 | 1094 | const targetWidth = $target.outerWidth(); |
1020 | 1095 | const targetHeight = $target.outerHeight(); |
1021 | 1096 | const popupWidth = $popup.outerWidth(); |
|
1025 | 1100 | const windowHeight = $( window ).height(); |
1026 | 1101 |
|
1027 | 1102 | let left, positionClass; |
1028 | | - let top = |
1029 | | - targetOffset.top - containerOffset.top + targetHeight + offset; |
| 1103 | + let top = targetOffset.top + targetHeight + offset; |
1030 | 1104 | let isAbove = false; |
1031 | 1105 |
|
1032 | 1106 | // Check if popup would be cut off at bottom of viewport |
|
1035 | 1109 | windowScrollTop + windowHeight && |
1036 | 1110 | targetOffset.top - popupHeight - offset > windowScrollTop |
1037 | 1111 | ) { |
1038 | | - top = targetOffset.top - containerOffset.top - popupHeight - offset; |
| 1112 | + top = targetOffset.top - popupHeight - offset; |
1039 | 1113 | isAbove = true; |
1040 | 1114 | } |
1041 | 1115 |
|
1042 | 1116 | if ( isRTL ) { |
1043 | | - left = targetOffset.left - containerOffset.left; |
| 1117 | + left = targetOffset.left; |
1044 | 1118 | positionClass = isAbove ? 'bottom-left' : 'top-left'; |
1045 | 1119 | } else { |
1046 | | - left = |
1047 | | - targetOffset.left - |
1048 | | - containerOffset.left + |
1049 | | - targetWidth - |
1050 | | - popupWidth; |
| 1120 | + left = targetOffset.left + targetWidth - popupWidth; |
1051 | 1121 | positionClass = isAbove ? 'bottom-right' : 'top-right'; |
1052 | 1122 | } |
1053 | 1123 |
|
1054 | 1124 | $popup |
1055 | 1125 | .removeClass( 'top-right bottom-right top-left bottom-left' ) |
1056 | 1126 | .css( { position: 'absolute', top: top, left: left } ) |
1057 | 1127 | .addClass( positionClass ); |
1058 | | - }; |
1059 | | - |
1060 | | - /** |
| 1128 | + }; /** |
1061 | 1129 | * conditions |
1062 | 1130 | * |
1063 | 1131 | * description |
|
0 commit comments