Plugin Directory

Changeset 3439496


Ignore:
Timestamp:
01/14/2026 12:00:38 PM (2 months ago)
Author:
webdevmattcrom
Message:

v1.2.0 released! Includes Core Gallery block support, and ships two Synced Patterns.

Location:
synced-pattern-popups/trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • synced-pattern-popups/trunk/assets/css/admin.css

    r3437030 r3439496  
    1818
    1919/* Scroll indicator shadows */
     20
    2021.sppopups-table-wrapper::before,
    2122.sppopups-table-wrapper::after {
     
    569570}
    570571
     572.sppopups-tab-nav-link .dashicons-external {
     573    font-size: 16px;
     574    width: 20px;
     575    height: 20px;
     576    vertical-align: middle;
     577}
     578
    571579.sppopups-tab-content-wrapper {
    572580    margin-top: 0;
     
    10311039    color: #0a4b78;
    10321040}
     1041
     1042/* Gallery Block Editor Styles - Using standard TextControl, no custom styles needed */
  • synced-pattern-popups/trunk/assets/css/modal.css

    r3436798 r3439496  
    5959    /* Create stacking context for gradient border */
    6060    isolation: isolate;
     61    /* Smooth transitions for size changes */
     62    transition: max-width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
    6163}
    6264
     
    250252}
    251253
     254/* Remove padding for gallery image wrapper */
     255.sppopups-content > .sppopups-gallery-image-wrapper {
     256    padding: 0;
     257}
     258
    252259.sppopups-loading {
    253260    display: flex;
     
    470477}
    471478
     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 */
     848figure[data-sppopup-gallery=true] figure {
     849    cursor: pointer;
     850}
  • synced-pattern-popups/trunk/assets/js/modal.js

    r3436798 r3439496  
    4848    var loadedScripts = new Set();
    4949
     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
    5063    /**
    5164     * Validate URL is safe for injection (same origin or relative)
     
    8295            return false;
    8396        }
     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 || '';
    84110    }
    85111
     
    462488        }
    463489       
    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));
    475491       
    476492        // Filter out elements that are not visible
     
    528544
    529545    /**
    530      * Open modal and load content
    531      *
    532      * @param {number} patternId Synced pattern ID
     546     * Setup modal state (common initialization logic)
     547     *
    533548     * @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) {
    541552        // Save scroll position BEFORE any DOM changes (for all screen sizes)
    542553        savedScrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
     
    549560       
    550561        // 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        }
    553568
    554569        // Prevent body scroll by saving scroll position (for all screen sizes)
     
    565580        modal.style.display = 'flex';
    566581        body.classList.add('sppopups-open');
    567         content.innerHTML = loadingHtml;
     582        content.innerHTML = loadingContent || loadingHtml;
    568583       
    569584        // Focus modal container or close button for accessibility
     
    578593            }
    579594        }, 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);
    580622
    581623        // Prepare form data for POST request
     
    601643               
    602644                // Update dialog title from AJAX response
    603                 var titleElement = modal.querySelector('#sppopups-title');
    604                 if (titleElement && data.success && data.data && data.data.title) {
    605                     titleElement.textContent = data.data.title;
    606                 } else if (titleElement) {
     645                var titleEl = getTitleElement();
     646                if (titleEl && data.success && data.data && data.data.title) {
     647                    titleEl.textContent = data.data.title;
     648                } else if (titleEl) {
    607649                    // Fallback title
    608                     titleElement.textContent = 'Popup';
     650                    titleEl.textContent = 'Popup';
    609651                }
    610652               
     
    731773        container.style.maxWidth = '';
    732774        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        }
    733807       
    734808        // Restore scroll position BEFORE removing body styles and restoring focus
     
    776850            closeModal();
    777851            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            }
    778866        }
    779867       
     
    869957            e.stopPropagation();
    870958            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            }
    8711038        }
    8721039    });
     
    9121079        }
    9131080
    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>');
    9581083
    9591084        // Prepare form data
     
    9801105
    9811106            // Update dialog title from AJAX response
    982             var titleElement = modal.querySelector('#sppopups-title');
    983             if (titleElement && data.success && data.data && data.data.title) {
    984                 titleElement.textContent = data.data.title;
    985             } else if (titleElement) {
    986                 titleElement.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';
    9871112            }
    9881113
     
    10111136                var calculatedWidth = calculateMaxWidth(currentMaxWidth);
    10121137                container.style.maxWidth = calculatedWidth + 'px';
    1013             }, 100); // Debounce resize events
     1138            }, 150); // Increased debounce for better performance
    10141139        }
    10151140    });
     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
    10161158})();
    10171159
  • synced-pattern-popups/trunk/includes/class-sppopups-admin.php

    r3437030 r3439496  
    289289                </a>
    290290                <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>
    292292                </a>
    293293            </nav>
     
    522522            add_meta_box(
    523523                'simplest-popup-support',
    524                 __( 'Synced Pattern Popup Support', 'synced-pattern-popups' ),
     524                __( 'Synced Pattern Popups', 'synced-pattern-popups' ),
    525525                array( $this, 'render_popup_support_metabox' ),
    526526                $post_type,
     
    540540        wp_nonce_field( 'sppopups_support_metabox', 'sppopups_support_nonce' );
    541541
    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';
    546551        }
    547552        ?>
    548553        <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' ); ?>
    553557                </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' ); ?>
    558573                </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>
    563585        </div>
    564586        <?php
     
    597619        }
    598620
    599         // Get and sanitize the value
    600         $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' );
    602624       
    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';
    605635        }
    606636
    607637        // 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' );
    609643    }
    610644
  • synced-pattern-popups/trunk/includes/class-sppopups-plugin.php

    r3436798 r3439496  
    153153        }
    154154
    155         // Check if post has forced popup support enabled
     155        // Check if post has forced modal assets loading enabled
    156156        if ( is_singular() ) {
    157157            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                }
    160163            }
    161164        }
     
    173176            if ( $this->content_has_triggers( $post->post_content ) ) {
    174177                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                }
    175190            }
    176191        }
     
    240255        }
    241256
     257        // Check for gallery with sppopup link option
     258        if ( preg_match( '/data-sppopup-gallery/', $content ) ) {
     259            return true;
     260        }
     261
    242262        return false;
    243263    }
    244264
    245265    /**
     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    /**
    246293     * Enqueue CSS and JavaScript assets
    247294     */
    248295    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() ) {
    251319            return;
    252320        }
    253321
    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        }
    270356
    271357        // Get style URLs for JavaScript injection
     
    447533            }
    448534
     535            // Register modal assets meta
    449536            register_post_meta(
    450537                $post_type,
    451                 '_sppopups_support',
     538                '_sppopups_modal_assets',
    452539                array(
    453540                    'type'              => 'string',
    454541                    'single'            => true,
    455                     'sanitize_callback' => array( $this, 'sanitize_popup_support' ),
     542                    'sanitize_callback' => array( $this, 'sanitize_assets_setting' ),
    456543                    'show_in_rest'      => true,
    457544                    'auth_callback'     => function() {
    458545                        return current_user_can( 'edit_posts' );
    459546                    },
    460                     'default'           => 'default',
     547                    'default'           => 'auto-detect',
    461548                )
    462549            );
    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
    468571     *
    469572     * @param string $value Meta value
    470573     * @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
    471586     */
    472587    public function sanitize_popup_support( $value ) {
  • synced-pattern-popups/trunk/readme.txt

    r3438579 r3439496  
    44Requires at least: 5.8
    55Tested up to: 6.9
    6 Stable tag: 1.1.3
     6Stable tag: 1.2.0
    77License: GPLv2 or later
    88License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    198198
    199199== 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
    200217
    201218= 1.1.3 =
     
    254271== Upgrade Notice ==
    255272
     273= 1.2.0 =
     274Major 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
    256276= 1.1.3 =
    257277Admin 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  
    33 * Plugin Name: Synced Pattern Popups
    44 * Description: A lightweight modal popup system that loads WordPress Synced Pattern content on demand. Trigger with class "spp-trigger-{id}".
    5  * Version: 1.1.3
     5 * Version: 1.2.0
    66 * Author: Matt Cromwell
    77 * Author URI: https://www.mattcromwell.com
     
    1919
    2020// Define plugin constants
    21 define( 'SPPOPUPS_VERSION', '1.1.3' );
     21define( 'SPPOPUPS_VERSION', '1.2.0' );
    2222define( 'SPPOPUPS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2323define( 'SPPOPUPS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
     
    3636require_once SPPOPUPS_PLUGIN_DIR . 'includes/class-sppopups-plugin.php';
    3737require_once SPPOPUPS_PLUGIN_DIR . 'includes/class-sppopups-review-notice.php';
     38require_once SPPOPUPS_PLUGIN_DIR . 'includes/class-sppopups-shipped-patterns.php';
     39require_once SPPOPUPS_PLUGIN_DIR . 'includes/class-sppopups-gallery.php';
    3840
    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
     42register_activation_hook( __FILE__, 'sppopups_activate' );
    4143
    4244// Register uninstall hook for cleanup
     
    4749
    4850/**
     51 * Plugin activation handler
     52 */
     53function 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/**
    4962 * Initialize the plugin
    5063 */
    5164function sppopups_init() {
    52     // Load plugin textdomain
    53     load_plugin_textdomain(
    54         'synced-pattern-popups',
    55         false,
    56         dirname( plugin_basename( __FILE__ ) ) . '/languages'
    57     );
    58 
    5965    // Initialize settings
    6066    $settings = new SPPopups_Settings();
     
    6571    $plugin->init();
    6672
     73    // Initialize gallery integration
     74    $gallery = new SPPopups_Gallery();
     75    $gallery->init();
     76
    6777    // Initialize review notice (admin only)
    6878    if ( is_admin() ) {
    6979        $review_notice = new SPPopups_Review_Notice();
    7080        $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' ) );
    7185    }
    7286}
Note: See TracChangeset for help on using the changeset viewer.