Changeset 3439496
- Timestamp:
- 01/14/2026 12:00:38 PM (2 months ago)
- Location:
- synced-pattern-popups/trunk
- Files:
-
- 7 edited
-
assets/css/admin.css (modified) (3 diffs)
-
assets/css/modal.css (modified) (3 diffs)
-
assets/js/modal.js (modified) (14 diffs)
-
includes/class-sppopups-admin.php (modified) (4 diffs)
-
includes/class-sppopups-plugin.php (modified) (4 diffs)
-
readme.txt (modified) (3 diffs)
-
sppopups.php (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
synced-pattern-popups/trunk/assets/css/admin.css
r3437030 r3439496 18 18 19 19 /* Scroll indicator shadows */ 20 20 21 .sppopups-table-wrapper::before, 21 22 .sppopups-table-wrapper::after { … … 569 570 } 570 571 572 .sppopups-tab-nav-link .dashicons-external { 573 font-size: 16px; 574 width: 20px; 575 height: 20px; 576 vertical-align: middle; 577 } 578 571 579 .sppopups-tab-content-wrapper { 572 580 margin-top: 0; … … 1031 1039 color: #0a4b78; 1032 1040 } 1041 1042 /* Gallery Block Editor Styles - Using standard TextControl, no custom styles needed */ -
synced-pattern-popups/trunk/assets/css/modal.css
r3436798 r3439496 59 59 /* Create stacking context for gradient border */ 60 60 isolation: isolate; 61 /* Smooth transitions for size changes */ 62 transition: max-width 0.5s cubic-bezier(0.4, 0, 0.2, 1); 61 63 } 62 64 … … 250 252 } 251 253 254 /* Remove padding for gallery image wrapper */ 255 .sppopups-content > .sppopups-gallery-image-wrapper { 256 padding: 0; 257 } 258 252 259 .sppopups-loading { 253 260 display: flex; … … 470 477 } 471 478 479 /* Gallery Modal Styles */ 480 481 /* Gallery image container - holds crossfade layers */ 482 .sppopups-gallery-image-container { 483 position: relative; 484 width: 100%; 485 min-height: 200px; 486 height: auto; /* Allow height to be set by JS */ 487 transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1), min-height 0.5s cubic-bezier(0.4, 0, 0.2, 1); 488 /* Remove flexbox centering - image will fill container exactly */ 489 } 490 491 /* Gallery image wrapper - each layer in crossfade */ 492 .sppopups-gallery-image-wrapper { 493 position: absolute; 494 top: 0; 495 left: 0; 496 width: 100%; 497 height: 100%; 498 /* Remove flexbox centering - image will fill container exactly */ 499 padding: 0; 500 box-sizing: border-box; 501 opacity: 0; 502 transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1); 503 pointer-events: none; 504 z-index: 1; 505 } 506 507 /* Active/visible layer */ 508 .sppopups-gallery-image-wrapper.active { 509 opacity: 1; 510 pointer-events: auto; 511 z-index: 1; 512 } 513 514 /* Layer fading in (on top during transition) */ 515 .sppopups-gallery-image-wrapper.fading-in { 516 opacity: 0; 517 z-index: 2; 518 pointer-events: none; 519 } 520 521 .sppopups-gallery-image-wrapper.fading-in.active { 522 opacity: 1; 523 pointer-events: auto; /* Enable interactions when active */ 524 } 525 526 /* Layer fading out (below during transition) */ 527 .sppopups-gallery-image-wrapper.fading-out { 528 opacity: 0; 529 z-index: 1; 530 pointer-events: none; 531 } 532 533 .sppopups-gallery-image { 534 width: 100%; 535 height: 100%; 536 object-fit: contain; 537 display: block; 538 margin: 0; 539 padding: 0; 540 } 541 542 /* Ensure gallery image container has no padding and fills content area */ 543 .sppopups-modal:has(.sppopups-gallery-image-container) .sppopups-content { 544 padding: 0; 545 overflow: hidden; /* Prevent scrollbars during transitions */ 546 } 547 548 .sppopups-modal:has(.sppopups-gallery-image-container) .sppopups-content > .sppopups-gallery-image-container { 549 padding: 0; 550 margin: 0; 551 } 552 553 /* Gallery navigation buttons (overlay) - hidden by default, show on hover/touch */ 554 .sppopups-gallery-nav { 555 position: absolute; 556 top: 50%; 557 transform: translateY(-50%); 558 background: rgba(255, 255, 255, 0.9); 559 border: 1px solid rgba(0, 0, 0, 0.1); 560 border-radius: 50%; 561 cursor: pointer; 562 padding: 12px; 563 color: #666; 564 transition: all 0.2s ease; 565 z-index: 10; 566 line-height: 1; 567 width: 48px; 568 height: 48px; 569 display: flex; 570 align-items: center; 571 justify-content: center; 572 min-width: 44px; 573 min-height: 44px; 574 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 575 opacity: 0; 576 pointer-events: auto; /* Buttons should always be clickable when visible */ 577 } 578 579 /* Show navigation buttons on hover or touch */ 580 .sppopups-gallery-image-wrapper:hover .sppopups-gallery-nav, 581 .sppopups-gallery-nav:hover { 582 opacity: 1; 583 pointer-events: auto; 584 } 585 586 /* Show on touch devices */ 587 @media (hover: none) and (pointer: coarse) { 588 .sppopups-gallery-nav { 589 opacity: 1; 590 pointer-events: auto; 591 } 592 } 593 594 .sppopups-gallery-nav:hover { 595 color: #000; 596 background: rgba(255, 255, 255, 1); 597 transform: translateY(-50%) scale(1.1); 598 } 599 600 .sppopups-gallery-nav:active { 601 transform: translateY(-50%) scale(0.95); 602 } 603 604 .sppopups-gallery-nav:focus { 605 outline: 2px solid #5E53C0; 606 outline-offset: 2px; 607 } 608 609 .sppopups-gallery-nav svg { 610 width: 24px; 611 height: 24px; 612 } 613 614 .sppopups-gallery-nav--prev { 615 left: 20px; 616 } 617 618 .sppopups-gallery-nav--next { 619 right: 20px; 620 } 621 622 /* Gallery footer - smart flex layout */ 623 .sppopups-gallery-footer { 624 display: grid; 625 grid-template-columns: auto 1fr auto; 626 align-items: center; 627 gap: 16px; 628 width: 100%; 629 } 630 631 /* Adjust grid when navigation group is empty (hidden) */ 632 .sppopups-gallery-footer:has(.sppopups-gallery-nav-group:empty) { 633 grid-template-columns: 1fr auto; 634 } 635 636 /* Adjust grid when close button is hidden */ 637 .sppopups-gallery-footer:not(:has(.sppopups-close-footer)) { 638 grid-template-columns: auto 1fr; 639 } 640 641 /* Adjust grid when both navigation and close button are hidden */ 642 .sppopups-gallery-footer:has(.sppopups-gallery-nav-group:empty):not(:has(.sppopups-close-footer)) { 643 grid-template-columns: 1fr; 644 } 645 646 /* Fallback for browsers without :has() support */ 647 .sppopups-gallery-footer .sppopups-gallery-nav-group:empty { 648 display: none; 649 } 650 651 /* Navigation button group (left) */ 652 .sppopups-gallery-nav-group { 653 display: flex; 654 align-items: center; 655 gap: 8px; 656 flex-shrink: 0; 657 } 658 659 /* Caption (center, flexible) */ 660 .sppopups-gallery-caption { 661 min-width: 0; 662 color: #666; 663 font-size: 14px; 664 line-height: 1.5; 665 text-align: center; 666 padding: 0 16px; 667 overflow: hidden; 668 text-overflow: ellipsis; 669 /* Allow multi-line for HTML content, but limit height */ 670 max-height: 3em; 671 display: -webkit-box; 672 -webkit-line-clamp: 2; 673 -webkit-box-orient: vertical; 674 white-space: normal; 675 word-wrap: break-word; 676 } 677 678 /* If caption contains HTML, allow it to display properly */ 679 .sppopups-gallery-caption p { 680 margin: 0; 681 padding: 0; 682 } 683 684 .sppopups-gallery-caption p:first-child { 685 margin-top: 0; 686 } 687 688 .sppopups-gallery-caption p:last-child { 689 margin-bottom: 0; 690 } 691 692 /* If caption is empty, still take up space */ 693 .sppopups-gallery-caption:empty { 694 display: block; 695 } 696 697 /* Close button (right) */ 698 .sppopups-gallery-footer .sppopups-close-footer { 699 flex-shrink: 0; 700 margin-left: auto; 701 } 702 703 /* Responsive: stack on small screens */ 704 @media screen and (max-width: 480px) { 705 .sppopups-gallery-footer { 706 grid-template-columns: 1fr; 707 grid-template-rows: auto auto auto; 708 gap: 12px; 709 } 710 711 .sppopups-gallery-nav-group { 712 justify-content: flex-start; 713 } 714 715 .sppopups-gallery-caption { 716 text-align: left; 717 padding: 0; 718 } 719 720 .sppopups-gallery-footer .sppopups-close-footer { 721 margin-left: 0; 722 width: 100%; 723 } 724 } 725 726 /* Gallery footer navigation buttons - smaller, left-aligned */ 727 .sppopups-gallery-nav-footer { 728 background: none; 729 border: 1px solid rgba(0, 0, 0, 0.2); 730 border-radius: 4px; 731 color: #666; 732 font-size: 12px; 733 cursor: pointer; 734 padding: 6px 12px; 735 transition: all 0.2s ease; 736 text-decoration: none; 737 font-family: inherit; 738 line-height: 1.4; 739 white-space: nowrap; 740 } 741 742 .sppopups-gallery-nav-footer:hover:not(:disabled) { 743 color: #000; 744 border-color: rgba(0, 0, 0, 0.4); 745 background: rgba(0, 0, 0, 0.05); 746 } 747 748 .sppopups-gallery-nav-footer:focus { 749 outline: 2px solid #5E53C0; 750 outline-offset: 2px; 751 } 752 753 .sppopups-gallery-nav-footer:disabled { 754 opacity: 0.5; 755 cursor: not-allowed; 756 } 757 758 /* Gallery modal container - size is set dynamically via JavaScript based on sppopupModalSize setting */ 759 /* No fixed max-width here - JavaScript will set it */ 760 761 /* Gallery responsive styles */ 762 @media screen and (max-width: 480px) { 763 .sppopups-gallery-nav { 764 width: 44px; 765 height: 44px; 766 padding: 10px; 767 } 768 769 .sppopups-gallery-nav--prev { 770 left: 10px; 771 } 772 773 .sppopups-gallery-nav--next { 774 right: 10px; 775 } 776 777 .sppopups-gallery-footer { 778 flex-direction: column; 779 gap: 12px; 780 } 781 782 .sppopups-gallery-nav-footer { 783 width: 100%; 784 justify-content: center; 785 } 786 787 .sppopups-gallery-caption { 788 order: -1; 789 width: 100%; 790 padding: 0; 791 font-size: 13px; 792 } 793 } 794 795 @media screen and (min-width: 481px) and (max-width: 768px) { 796 .sppopups-gallery-nav--prev { 797 left: 15px; 798 } 799 800 .sppopups-gallery-nav--next { 801 right: 15px; 802 } 803 } 804 805 /* Gallery in landscape mode */ 806 @media screen and (max-height: 500px) and (orientation: landscape) { 807 .sppopups-gallery-image { 808 max-height: 60vh; 809 } 810 811 .sppopups-gallery-nav { 812 width: 40px; 813 height: 40px; 814 padding: 8px; 815 } 816 817 .sppopups-gallery-nav svg { 818 width: 20px; 819 height: 20px; 820 } 821 } 822 823 /* Reduced motion for gallery */ 824 @media (prefers-reduced-motion: reduce) { 825 .sppopups-gallery-nav { 826 transition: none; 827 } 828 829 .sppopups-gallery-nav:hover { 830 transform: translateY(-50%); 831 } 832 833 .sppopups-gallery-nav-footer { 834 transition: none; 835 } 836 837 .sppopups-gallery-image-wrapper { 838 transition: none !important; 839 } 840 841 .sppopups-gallery-image { 842 transition: none !important; 843 } 844 } 845 846 /* Frontend gallery images - pointer cursor on hover */ 847 /* Target gallery links with our data attribute */ 848 figure[data-sppopup-gallery=true] figure { 849 cursor: pointer; 850 } -
synced-pattern-popups/trunk/assets/js/modal.js
r3436798 r3439496 48 48 var loadedScripts = new Set(); 49 49 50 // Cache commonly queried DOM elements 51 var titleElement = null; // Will be cached on first use 52 53 // Cache focusable elements selector (static, never changes) 54 var FOCUSABLE_SELECTORS = [ 55 'a[href]', 56 'button:not([disabled])', 57 'textarea:not([disabled])', 58 'input:not([disabled])', 59 'select:not([disabled])', 60 '[tabindex]:not([tabindex="-1"])' 61 ].join(', '); 62 50 63 /** 51 64 * Validate URL is safe for injection (same origin or relative) … … 82 95 return false; 83 96 } 97 } 98 99 /** 100 * Get image URL from image object (with fallback) 101 * 102 * @param {object} image Image object 103 * @return {string} Image URL or empty string 104 */ 105 function getImageUrl(image) { 106 if (!image) { 107 return ''; 108 } 109 return image.fullUrl || image.url || ''; 84 110 } 85 111 … … 462 488 } 463 489 464 // Selectors for focusable elements 465 var selectors = [ 466 'a[href]', 467 'button:not([disabled])', 468 'textarea:not([disabled])', 469 'input:not([disabled])', 470 'select:not([disabled])', 471 '[tabindex]:not([tabindex="-1"])' 472 ].join(', '); 473 474 var focusable = Array.prototype.slice.call(card.querySelectorAll(selectors)); 490 var focusable = Array.prototype.slice.call(card.querySelectorAll(FOCUSABLE_SELECTORS)); 475 491 476 492 // Filter out elements that are not visible … … 528 544 529 545 /** 530 * Open modal and load content 531 * 532 * @param {number} patternId Synced pattern ID 546 * Setup modal state (common initialization logic) 547 * 533 548 * @param {number|null} maxWidth Optional max-width in pixels 534 */ 535 function openModal(patternId, maxWidth) { 536 if (!patternId || !Number.isInteger(Number(patternId)) || patternId <= 0) { 537 console.error('Synced Pattern Popups: Invalid pattern ID'); 538 return; 539 } 540 549 * @param {string} loadingContent HTML content to show while loading 550 */ 551 function setupModalState(maxWidth, loadingContent) { 541 552 // Save scroll position BEFORE any DOM changes (for all screen sizes) 542 553 savedScrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; … … 549 560 550 561 // Calculate and apply max-width with 6% margin 551 var calculatedWidth = calculateMaxWidth(maxWidth); 552 container.style.maxWidth = calculatedWidth + 'px'; 562 if (maxWidth !== null) { 563 var calculatedWidth = calculateMaxWidth(maxWidth); 564 container.style.maxWidth = calculatedWidth + 'px'; 565 } else { 566 container.style.maxWidth = ''; 567 } 553 568 554 569 // Prevent body scroll by saving scroll position (for all screen sizes) … … 565 580 modal.style.display = 'flex'; 566 581 body.classList.add('sppopups-open'); 567 content.innerHTML = loading Html;582 content.innerHTML = loadingContent || loadingHtml; 568 583 569 584 // Focus modal container or close button for accessibility … … 578 593 } 579 594 }, 0); 595 } 596 597 /** 598 * Get or cache title element 599 * 600 * @return {HTMLElement|null} Title element or null 601 */ 602 function getTitleElement() { 603 if (!titleElement || !modal.contains(titleElement)) { 604 titleElement = modal.querySelector('#sppopups-title'); 605 } 606 return titleElement; 607 } 608 609 /** 610 * Open modal and load content 611 * 612 * @param {number} patternId Synced pattern ID 613 * @param {number|null} maxWidth Optional max-width in pixels 614 */ 615 function openModal(patternId, maxWidth) { 616 if (!patternId || !Number.isInteger(Number(patternId)) || patternId <= 0) { 617 console.error('Synced Pattern Popups: Invalid pattern ID'); 618 return; 619 } 620 621 setupModalState(maxWidth, loadingHtml); 580 622 581 623 // Prepare form data for POST request … … 601 643 602 644 // Update dialog title from AJAX response 603 var titleEl ement = modal.querySelector('#sppopups-title');604 if (titleEl ement&& data.success && data.data && data.data.title) {605 titleEl ement.textContent = data.data.title;606 } else if (titleEl ement) {645 var titleEl = getTitleElement(); 646 if (titleEl && data.success && data.data && data.data.title) { 647 titleEl.textContent = data.data.title; 648 } else if (titleEl) { 607 649 // Fallback title 608 titleEl ement.textContent = 'Popup';650 titleEl.textContent = 'Popup'; 609 651 } 610 652 … … 731 773 container.style.maxWidth = ''; 732 774 currentMaxWidth = null; 775 776 // Reset gallery state 777 if (window.sppopupsGallery && window.sppopupsGallery.resetGalleryState) { 778 window.sppopupsGallery.resetGalleryState(); 779 } 780 781 // Reset close button visibility 782 if (closeBtn) { 783 closeBtn.style.display = ''; 784 } 785 786 // Reset footer to default 787 var footer = modal.querySelector('.sppopups-footer'); 788 if (footer) { 789 var closeLabel = 'Close modal'; 790 try { 791 closeLabel = footer.querySelector('.sppopups-close-footer') ? 792 footer.querySelector('.sppopups-close-footer').getAttribute('aria-label') : 'Close modal'; 793 } catch (e) { 794 // Use default 795 } 796 797 footer.innerHTML = '<button class="sppopups-close-footer" type="button" aria-label="' + closeLabel + '">Close →</button>'; 798 799 // Reattach close footer button listener 800 var newCloseFooterBtn = footer.querySelector('.sppopups-close-footer'); 801 if (newCloseFooterBtn) { 802 newCloseFooterBtn.addEventListener('click', closeModal); 803 // Update global reference 804 closeFooterBtn = newCloseFooterBtn; 805 } 806 } 733 807 734 808 // Restore scroll position BEFORE removing body styles and restoring focus … … 776 850 closeModal(); 777 851 return; 852 } 853 854 // Gallery navigation with arrow keys 855 if (window.sppopupsGallery && window.sppopupsGallery.isGalleryMode && window.sppopupsGallery.isGalleryMode()) { 856 if (e.key === 'ArrowLeft') { 857 e.preventDefault(); 858 window.sppopupsGallery.navigateGallery('prev'); 859 return; 860 } 861 if (e.key === 'ArrowRight') { 862 e.preventDefault(); 863 window.sppopupsGallery.navigateGallery('next'); 864 return; 865 } 778 866 } 779 867 … … 869 957 e.stopPropagation(); 870 958 openModal(patternData.id, patternData.maxWidth); 959 return; 960 } 961 962 // Check for gallery image click (handles clicks on figure, image, or links within) 963 // This works regardless of whether Core created links or not, and handles randomized order 964 var galleryItem = trigger.closest('[data-image-id], [data-image-index]'); 965 if (galleryItem) { 966 var gallery = galleryItem.closest('[data-sppopup-gallery]'); 967 if (gallery) { 968 var galleryDataAttr = gallery.getAttribute('data-gallery-data'); 969 if (galleryDataAttr) { 970 try { 971 var galleryData = JSON.parse(galleryDataAttr); 972 973 // Identify clicked image by ID (preferred) or by matching image src URL 974 var clickedImageId = galleryItem.getAttribute('data-image-id'); 975 var clickedImageIndex = -1; 976 977 // Try to find by ID first (most reliable) 978 if (clickedImageId) { 979 for (var i = 0; i < galleryData.images.length; i++) { 980 if (galleryData.images[i].id && parseInt(galleryData.images[i].id, 10) === parseInt(clickedImageId, 10)) { 981 clickedImageIndex = i; 982 break; 983 } 984 } 985 } 986 987 // Fallback: Find by matching image src URL (handles cases where ID might not match) 988 if (clickedImageIndex === -1) { 989 var clickedImg = galleryItem.querySelector('img'); 990 if (clickedImg && clickedImg.src) { 991 // Normalize clicked image URL (remove query params, hash, normalize) 992 var clickedSrc = clickedImg.src.split('?')[0].split('#')[0]; 993 // Extract filename for more reliable matching 994 var clickedFilename = clickedSrc.split('/').pop(); 995 996 for (var j = 0; j < galleryData.images.length; j++) { 997 var imageUrl = getImageUrl(galleryData.images[j]); 998 if (imageUrl) { 999 // Normalize gallery image URL 1000 var compareUrl = imageUrl.split('?')[0].split('#')[0]; 1001 var compareFilename = compareUrl.split('/').pop(); 1002 1003 // Match by full URL or by filename (more reliable) 1004 if (clickedSrc === compareUrl || 1005 clickedSrc.endsWith(compareUrl) || 1006 compareUrl.endsWith(clickedSrc) || 1007 (clickedFilename && compareFilename && clickedFilename === compareFilename)) { 1008 clickedImageIndex = j; 1009 break; 1010 } 1011 } 1012 } 1013 } 1014 } 1015 1016 // Final fallback: Use data-image-index if available (for backward compatibility) 1017 if (clickedImageIndex === -1) { 1018 var fallbackIndex = galleryItem.getAttribute('data-image-index'); 1019 if (fallbackIndex) { 1020 clickedImageIndex = parseInt(fallbackIndex, 10); 1021 } 1022 } 1023 1024 if (galleryData && Array.isArray(galleryData.images) && clickedImageIndex >= 0 && clickedImageIndex < galleryData.images.length) { 1025 // Prevent default navigation (important for links) 1026 e.preventDefault(); 1027 e.stopPropagation(); 1028 if (window.sppopupsGallery && window.sppopupsGallery.openGalleryModal) { 1029 window.sppopupsGallery.openGalleryModal(galleryData, clickedImageIndex); 1030 } 1031 return; 1032 } 1033 } catch (err) { 1034 console.error('Synced Pattern Popups: Error parsing gallery data:', err); 1035 } 1036 } 1037 } 871 1038 } 872 1039 }); … … 912 1079 } 913 1080 914 // Save scroll position BEFORE any DOM changes 915 savedScrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 916 917 // Store the element that triggered the modal for focus restoration 918 lastActiveElement = document.activeElement; 919 920 // Reset max-width to default (no custom width for TLDR) 921 currentMaxWidth = null; 922 if (container && container.style) { 923 container.style.maxWidth = ''; 924 } 925 926 // Prevent body scroll by saving scroll position 927 if (body && body.style) { 928 body.style.top = '-' + savedScrollPosition + 'px'; 929 } 930 931 // Update ARIA attributes 932 if (modal) { 933 modal.setAttribute('aria-hidden', 'false'); 934 modal.setAttribute('aria-busy', 'true'); 935 } 936 937 // Hide background from assistive technology 938 hideBackgroundFromAT(); 939 940 if (modal) { 941 modal.classList.add('active'); 942 modal.style.display = 'flex'; 943 } 944 if (body && body.classList) { 945 body.classList.add('sppopups-open'); 946 } 947 // Use custom loading message for TLDR 948 content.innerHTML = '<div class="sppopups-loading"><div class="sppopups-spinner"></div><p>Generating TLDR</p></div>'; 949 950 // Focus modal container or close button for accessibility 951 setTimeout(function() { 952 if (closeBtn) { 953 focusWithoutScroll(closeBtn); 954 } else { 955 focusWithoutScroll(modal); 956 } 957 }, 0); 1081 // Use common modal setup (no custom max-width for TLDR) 1082 setupModalState(null, '<div class="sppopups-loading"><div class="sppopups-spinner"></div><p>Generating TLDR</p></div>'); 958 1083 959 1084 // Prepare form data … … 980 1105 981 1106 // Update dialog title from AJAX response 982 var titleEl ement = modal.querySelector('#sppopups-title');983 if (titleEl ement&& data.success && data.data && data.data.title) {984 titleEl ement.textContent = data.data.title;985 } else if (titleEl ement) {986 titleEl ement.textContent = 'TLDR';1107 var titleEl = getTitleElement(); 1108 if (titleEl && data.success && data.data && data.data.title) { 1109 titleEl.textContent = data.data.title; 1110 } else if (titleEl) { 1111 titleEl.textContent = 'TLDR'; 987 1112 } 988 1113 … … 1011 1136 var calculatedWidth = calculateMaxWidth(currentMaxWidth); 1012 1137 container.style.maxWidth = calculatedWidth + 'px'; 1013 }, 1 00); // Debounce resize events1138 }, 150); // Increased debounce for better performance 1014 1139 } 1015 1140 }); 1141 1142 // Initialize gallery module if available 1143 if (window.sppopupsGallery && window.sppopupsGallery.init) { 1144 window.sppopupsGallery.init({ 1145 modal: modal, 1146 content: content, 1147 container: container, 1148 closeBtn: closeBtn, 1149 closeModal: closeModal, 1150 setupModalState: setupModalState, 1151 getTitleElement: getTitleElement, 1152 focusWithoutScroll: focusWithoutScroll, 1153 currentMaxWidth: function() { return currentMaxWidth; }, 1154 setCurrentMaxWidth: function(width) { currentMaxWidth = width; } 1155 }); 1156 } 1157 1016 1158 })(); 1017 1159 -
synced-pattern-popups/trunk/includes/class-sppopups-admin.php
r3437030 r3439496 289 289 </a> 290 290 <a href="<?php echo esc_url( 'https://wordpress.org/support/plugin/synced-pattern-popups/' ); ?>" class="sppopups-tab-nav-link" target="_blank" rel="noopener noreferrer"> 291 <?php esc_html_e( 'Get Support', 'synced-pattern-popups' ); ?> 291 <?php esc_html_e( 'Get Support', 'synced-pattern-popups' ); ?> <span class="dashicons dashicons-external"></span> 292 292 </a> 293 293 </nav> … … 522 522 add_meta_box( 523 523 'simplest-popup-support', 524 __( 'Synced Pattern Popup Support', 'synced-pattern-popups' ),524 __( 'Synced Pattern Popups', 'synced-pattern-popups' ), 525 525 array( $this, 'render_popup_support_metabox' ), 526 526 $post_type, … … 540 540 wp_nonce_field( 'sppopups_support_metabox', 'sppopups_support_nonce' ); 541 541 542 // Get current value 543 $current_value = get_post_meta( $post->ID, '_sppopups_support', true ); 544 if ( empty( $current_value ) ) { 545 $current_value = 'default'; 542 // Get current values 543 $modal_assets = get_post_meta( $post->ID, '_sppopups_modal_assets', true ); 544 if ( empty( $modal_assets ) ) { 545 $modal_assets = 'auto-detect'; 546 } 547 548 $gallery_assets = get_post_meta( $post->ID, '_sppopups_gallery_assets', true ); 549 if ( empty( $gallery_assets ) ) { 550 $gallery_assets = 'auto-detect'; 546 551 } 547 552 ?> 548 553 <div class="sppopups-support-metabox"> 549 <fieldset> 550 <label> 551 <input type="radio" name="sppopups_support" value="default" <?php checked( $current_value, 'default' ); ?> /> 552 <strong><?php esc_html_e( 'Default', 'synced-pattern-popups' ); ?></strong> 554 <div style="margin-bottom: 16px;"> 555 <label style="display: block; margin-bottom: 4px; font-weight: 600;"> 556 <?php esc_html_e( 'Modal Assets:', 'synced-pattern-popups' ); ?> 553 557 </label> 554 <br> 555 <label> 556 <input type="radio" name="sppopups_support" value="forced" <?php checked( $current_value, 'forced' ); ?> /> 557 <strong><?php esc_html_e( 'Forced On', 'synced-pattern-popups' ); ?></strong> 558 <fieldset style="margin: 0; padding: 0; border: 0;"> 559 <label style="display: inline-block; margin-right: 12px;"> 560 <input type="radio" name="sppopups_modal_assets" value="auto-detect" <?php checked( $modal_assets, 'auto-detect' ); ?> /> 561 <?php esc_html_e( 'Auto-Detect', 'synced-pattern-popups' ); ?> 562 </label> 563 <label style="display: inline-block;"> 564 <input type="radio" name="sppopups_modal_assets" value="loaded" <?php checked( $modal_assets, 'loaded' ); ?> /> 565 <?php esc_html_e( 'Loaded', 'synced-pattern-popups' ); ?> 566 </label> 567 </fieldset> 568 </div> 569 570 <div style="margin-bottom: 12px;"> 571 <label style="display: block; margin-bottom: 4px; font-weight: 600;"> 572 <?php esc_html_e( 'Gallery Assets:', 'synced-pattern-popups' ); ?> 558 573 </label> 559 </fieldset> 560 <p class="description" style="margin-top: 12px; margin-bottom: 0;"> 561 <?php esc_html_e( 'In most cases you can leave this on Default. Use Forced On if your trigger link/class is injected dynamically (e.g., forms, AJAX, page builders) and the popup assets aren\'t loading.', 'synced-pattern-popups' ); ?> 562 </p> 574 <fieldset style="margin: 0; padding: 0; border: 0;"> 575 <label style="display: inline-block; margin-right: 12px;"> 576 <input type="radio" name="sppopups_gallery_assets" value="auto-detect" <?php checked( $gallery_assets, 'auto-detect' ); ?> /> 577 <?php esc_html_e( 'Auto-Detect', 'synced-pattern-popups' ); ?> 578 </label> 579 <label style="display: inline-block;"> 580 <input type="radio" name="sppopups_gallery_assets" value="loaded" <?php checked( $gallery_assets, 'loaded' ); ?> /> 581 <?php esc_html_e( 'Loaded', 'synced-pattern-popups' ); ?> 582 </label> 583 </fieldset> 584 </div> 563 585 </div> 564 586 <?php … … 597 619 } 598 620 599 // Get and sanitize thevalue600 $ value = isset( $_POST['sppopups_support'] ) ? sanitize_text_field( wp_unslash( $_POST['sppopups_support'] ) ) : 'default';601 $allowed = array( 'default', 'forced' );621 // Get and sanitize modal assets value 622 $modal_assets = isset( $_POST['sppopups_modal_assets'] ) ? sanitize_text_field( wp_unslash( $_POST['sppopups_modal_assets'] ) ) : 'auto-detect'; 623 $allowed_modal = array( 'auto-detect', 'loaded' ); 602 624 603 if ( ! in_array( $value, $allowed, true ) ) { 604 $value = 'default'; 625 if ( ! in_array( $modal_assets, $allowed_modal, true ) ) { 626 $modal_assets = 'auto-detect'; 627 } 628 629 // Get and sanitize gallery assets value 630 $gallery_assets = isset( $_POST['sppopups_gallery_assets'] ) ? sanitize_text_field( wp_unslash( $_POST['sppopups_gallery_assets'] ) ) : 'auto-detect'; 631 $allowed_gallery = array( 'auto-detect', 'loaded' ); 632 633 if ( ! in_array( $gallery_assets, $allowed_gallery, true ) ) { 634 $gallery_assets = 'auto-detect'; 605 635 } 606 636 607 637 // Update post meta 608 update_post_meta( $post_id, '_sppopups_support', $value ); 638 update_post_meta( $post_id, '_sppopups_modal_assets', $modal_assets ); 639 update_post_meta( $post_id, '_sppopups_gallery_assets', $gallery_assets ); 640 641 // Backward compatibility: Remove old meta key if it exists 642 delete_post_meta( $post_id, '_sppopups_support' ); 609 643 } 610 644 -
synced-pattern-popups/trunk/includes/class-sppopups-plugin.php
r3436798 r3439496 153 153 } 154 154 155 // Check if post has forced popup supportenabled155 // Check if post has forced modal assets loading enabled 156 156 if ( is_singular() ) { 157 157 global $post; 158 if ( $post && get_post_meta( $post->ID, '_sppopups_support', true ) === 'forced' ) { 159 return true; 158 if ( $post ) { 159 $modal_assets = get_post_meta( $post->ID, '_sppopups_modal_assets', true ); 160 if ( 'loaded' === $modal_assets ) { 161 return true; 162 } 160 163 } 161 164 } … … 173 176 if ( $this->content_has_triggers( $post->post_content ) ) { 174 177 return true; 178 } 179 180 // Check for Gallery blocks with sppopup linkTo option 181 if ( has_block( 'core/gallery', $post->post_content ) ) { 182 $blocks = parse_blocks( $post->post_content ); 183 foreach ( $blocks as $block ) { 184 if ( 'core/gallery' === $block['blockName'] && 185 isset( $block['attrs']['linkTo'] ) && 186 'sppopup' === $block['attrs']['linkTo'] ) { 187 return true; 188 } 189 } 175 190 } 176 191 } … … 240 255 } 241 256 257 // Check for gallery with sppopup link option 258 if ( preg_match( '/data-sppopup-gallery/', $content ) ) { 259 return true; 260 } 261 242 262 return false; 243 263 } 244 264 245 265 /** 266 * Check if page contains galleries with sppopup link option 267 * 268 * @return bool True if galleries are present 269 */ 270 private function has_galleries() { 271 // Get page content 272 global $post; 273 if ( ! $post || ! isset( $post->post_content ) ) { 274 return false; 275 } 276 277 // Check for Gallery blocks with sppopup linkTo option (same logic as has_popup_triggers) 278 if ( has_block( 'core/gallery', $post->post_content ) ) { 279 $blocks = parse_blocks( $post->post_content ); 280 foreach ( $blocks as $block ) { 281 if ( 'core/gallery' === $block['blockName'] && 282 isset( $block['attrs']['linkTo'] ) && 283 'sppopup' === $block['attrs']['linkTo'] ) { 284 return true; 285 } 286 } 287 } 288 289 return false; 290 } 291 292 /** 246 293 * Enqueue CSS and JavaScript assets 247 294 */ 248 295 public function enqueue_assets() { 249 // Only enqueue if page contains popup triggers 250 if ( ! $this->has_popup_triggers() ) { 296 // Check post meta for forced asset loading 297 $force_modal_assets = false; 298 $force_gallery_assets = false; 299 300 if ( is_singular() ) { 301 global $post; 302 if ( $post ) { 303 $modal_assets = get_post_meta( $post->ID, '_sppopups_modal_assets', true ); 304 $gallery_assets = get_post_meta( $post->ID, '_sppopups_gallery_assets', true ); 305 306 if ( 'loaded' === $modal_assets ) { 307 $force_modal_assets = true; 308 } 309 if ( 'loaded' === $gallery_assets ) { 310 $force_gallery_assets = true; 311 // Gallery assets depend on modal assets, so force modal assets too 312 $force_modal_assets = true; 313 } 314 } 315 } 316 317 // Check if page contains popup triggers (only if not forced) 318 if ( ! $force_modal_assets && ! $this->has_popup_triggers() ) { 251 319 return; 252 320 } 253 321 254 // Enqueue CSS 255 wp_enqueue_style( 256 'simplest-popup-modal', 257 SPPOPUPS_PLUGIN_URL . 'assets/css/modal.css', 258 array(), 259 SPPOPUPS_VERSION 260 ); 261 262 // Enqueue JavaScript 263 wp_enqueue_script( 264 'simplest-popup-modal', 265 SPPOPUPS_PLUGIN_URL . 'assets/js/modal.js', 266 array(), 267 SPPOPUPS_VERSION, 268 true 269 ); 322 // Enqueue CSS (always enqueue if modal assets are forced or triggers found) 323 if ( $force_modal_assets || $this->has_popup_triggers() ) { 324 wp_enqueue_style( 325 'simplest-popup-modal', 326 SPPOPUPS_PLUGIN_URL . 'assets/css/modal.css', 327 array(), 328 SPPOPUPS_VERSION 329 ); 330 } 331 332 // Check if galleries are present 333 $has_galleries = $this->has_galleries(); 334 335 // Enqueue gallery JavaScript if galleries are present OR forced to load 336 if ( $has_galleries || $force_gallery_assets ) { 337 wp_enqueue_script( 338 'sppopups-gallery', 339 SPPOPUPS_PLUGIN_URL . 'assets/js/gallery.js', 340 array(), 341 SPPOPUPS_VERSION, 342 true 343 ); 344 } 345 346 // Enqueue JavaScript (always enqueue if modal assets are forced or triggers found) 347 if ( $force_modal_assets || $this->has_popup_triggers() ) { 348 wp_enqueue_script( 349 'simplest-popup-modal', 350 SPPOPUPS_PLUGIN_URL . 'assets/js/modal.js', 351 ( $has_galleries || $force_gallery_assets ) ? array( 'sppopups-gallery' ) : array(), 352 SPPOPUPS_VERSION, 353 true 354 ); 355 } 270 356 271 357 // Get style URLs for JavaScript injection … … 447 533 } 448 534 535 // Register modal assets meta 449 536 register_post_meta( 450 537 $post_type, 451 '_sppopups_ support',538 '_sppopups_modal_assets', 452 539 array( 453 540 'type' => 'string', 454 541 'single' => true, 455 'sanitize_callback' => array( $this, 'sanitize_ popup_support' ),542 'sanitize_callback' => array( $this, 'sanitize_assets_setting' ), 456 543 'show_in_rest' => true, 457 544 'auth_callback' => function() { 458 545 return current_user_can( 'edit_posts' ); 459 546 }, 460 'default' => ' default',547 'default' => 'auto-detect', 461 548 ) 462 549 ); 463 } 464 } 465 466 /** 467 * Sanitize popup support meta value 550 551 // Register gallery assets meta 552 register_post_meta( 553 $post_type, 554 '_sppopups_gallery_assets', 555 array( 556 'type' => 'string', 557 'single' => true, 558 'sanitize_callback' => array( $this, 'sanitize_assets_setting' ), 559 'show_in_rest' => true, 560 'auth_callback' => function() { 561 return current_user_can( 'edit_posts' ); 562 }, 563 'default' => 'auto-detect', 564 ) 565 ); 566 } 567 } 568 569 /** 570 * Sanitize assets setting meta value 468 571 * 469 572 * @param string $value Meta value 470 573 * @return string Sanitized value 574 */ 575 public function sanitize_assets_setting( $value ) { 576 $allowed = array( 'auto-detect', 'loaded' ); 577 return in_array( $value, $allowed, true ) ? $value : 'auto-detect'; 578 } 579 580 /** 581 * Sanitize popup support meta value (deprecated, kept for backward compatibility) 582 * 583 * @param string $value Meta value 584 * @return string Sanitized value 585 * @deprecated Use sanitize_assets_setting instead 471 586 */ 472 587 public function sanitize_popup_support( $value ) { -
synced-pattern-popups/trunk/readme.txt
r3438579 r3439496 4 4 Requires at least: 5.8 5 5 Tested up to: 6.9 6 Stable tag: 1. 1.36 Stable tag: 1.2.0 7 7 License: GPLv2 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 198 198 199 199 == Changelog == 200 201 = 1.2.0 = 202 * New: Added support for the Core Gallery Block with full modal integration! 203 * New: Gallery block settings panel with modal size, close buttons, and image navigation options 204 * New: Per-post asset loading controls (Modal Assets and Gallery Assets) with Auto-Detect and Loaded options 205 * New: Added two wireframe Synced Patterns: "More Details" and "Terms & Conditions". 206 * Refactor: Centralized modal state management into a new `modalState` object in `modal.js`. 207 * Refactor: Migrated all scattered global state variables (e.g., `currentMaxWidth`, `lastActiveElement`, `loadedStyles`) into the `modalState` object. 208 * Refactor: Extracted gallery functionality into separate `gallery.js` module for better code organization and conditional loading 209 * Refactor: Added comprehensive JSDoc comments to gallery module and `modalState` object for improved documentation. 210 * Refactor: Implemented robust validation for `maxWidth` (100-5000px) within the `modalState` setter. 211 * Fixed: Corrected an issue where gallery images in "Random order" would open the wrong image in the modal. The system now reliably identifies the clicked image by its ID, regardless of randomization. 212 * Fixed: Gallery assets now automatically force modal assets to load when set to "Loaded" (dependency requirement) 213 * Fixed: Addressed Plugin Check error by removing the discouraged `load_plugin_textdomain()` call. 214 * Fixed: Resolved Plugin Check warning by ensuring the text domain (`synced-pattern-popups`) consistently matches across all plugin files. 215 * Improved: Enhanced code organization and maintainability through centralized state management and module separation. 216 * Improved: Gallery images now have smooth crossfade transitions and responsive modal sizing 200 217 201 218 = 1.1.3 = … … 254 271 == Upgrade Notice == 255 272 273 = 1.2.0 = 274 Major refactor of modal state management for improved stability, maintainability, and debugging. Fixes an issue with gallery random order. Addresses Plugin Check warnings. Recommended for all users. 275 256 276 = 1.1.3 = 257 277 Admin UI improvements and review notice feature. Settings page now has consistent width constraints, and a friendly review notice will appear after 10 days to encourage user feedback. All existing functionality remains unchanged. -
synced-pattern-popups/trunk/sppopups.php
r3437030 r3439496 3 3 * Plugin Name: Synced Pattern Popups 4 4 * Description: A lightweight modal popup system that loads WordPress Synced Pattern content on demand. Trigger with class "spp-trigger-{id}". 5 * Version: 1. 1.35 * Version: 1.2.0 6 6 * Author: Matt Cromwell 7 7 * Author URI: https://www.mattcromwell.com … … 19 19 20 20 // Define plugin constants 21 define( 'SPPOPUPS_VERSION', '1. 1.3' );21 define( 'SPPOPUPS_VERSION', '1.2.0' ); 22 22 define( 'SPPOPUPS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 23 23 define( 'SPPOPUPS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 36 36 require_once SPPOPUPS_PLUGIN_DIR . 'includes/class-sppopups-plugin.php'; 37 37 require_once SPPOPUPS_PLUGIN_DIR . 'includes/class-sppopups-review-notice.php'; 38 require_once SPPOPUPS_PLUGIN_DIR . 'includes/class-sppopups-shipped-patterns.php'; 39 require_once SPPOPUPS_PLUGIN_DIR . 'includes/class-sppopups-gallery.php'; 38 40 39 // Register activation hook to set review notice trigger date 40 register_activation_hook( __FILE__, array( 'SPPopups_Review_Notice', 'set_trigger_date' ));41 // Register activation hook to set review notice trigger date and ensure shipped patterns 42 register_activation_hook( __FILE__, 'sppopups_activate' ); 41 43 42 44 // Register uninstall hook for cleanup … … 47 49 48 50 /** 51 * Plugin activation handler 52 */ 53 function sppopups_activate() { 54 // Set review notice trigger date 55 SPPopups_Review_Notice::set_trigger_date(); 56 57 // Ensure shipped patterns exist on activation 58 SPPopups_Shipped_Patterns::activate(); 59 } 60 61 /** 49 62 * Initialize the plugin 50 63 */ 51 64 function sppopups_init() { 52 // Load plugin textdomain53 load_plugin_textdomain(54 'synced-pattern-popups',55 false,56 dirname( plugin_basename( __FILE__ ) ) . '/languages'57 );58 59 65 // Initialize settings 60 66 $settings = new SPPopups_Settings(); … … 65 71 $plugin->init(); 66 72 73 // Initialize gallery integration 74 $gallery = new SPPopups_Gallery(); 75 $gallery->init(); 76 67 77 // Initialize review notice (admin only) 68 78 if ( is_admin() ) { 69 79 $review_notice = new SPPopups_Review_Notice(); 70 80 $review_notice->init(); 81 82 // Ensure shipped patterns exist (runs on install and when version changes) 83 $shipped_patterns = new SPPopups_Shipped_Patterns(); 84 add_action( 'admin_init', array( $shipped_patterns, 'maybe_ensure_patterns' ) ); 71 85 } 72 86 }
Note: See TracChangeset
for help on using the changeset viewer.