Plugin Directory

Changeset 3423082


Ignore:
Timestamp:
12/18/2025 04:17:50 PM (3 months ago)
Author:
weblazer
Message:

Release version 1.0.4: Add Loop and Autoplay features

Location:
native-blocks-carousel/trunk
Files:
3 deleted
7 edited

Legend:

Unmodified
Added
Removed
  • native-blocks-carousel/trunk/any-block-carousel-slider.php

    r3421232 r3423082  
    55 * GitHub Plugin URI: https://github.com/WEBLAZER/native-blocks-carousel
    66 * Description: Transform any WordPress block into a performant carousel with pure CSS. Zero JavaScript, works with Gallery, Grid, Post Template, and Group blocks.
    7  * Version: 1.0.3.6
     7 * Version: 1.0.4
    88 * Author: weblazer
    99 * Author URI: https://profiles.wordpress.org/weblazer/
     
    2929}
    3030
    31 define('ANY_BLOCK_CAROUSEL_SLIDER_VERSION', '1.0.3.6');
     31define('ANY_BLOCK_CAROUSEL_SLIDER_VERSION', '1.0.4');
    3232define('ANY_BLOCK_CAROUSEL_SLIDER_PLUGIN_FILE', __FILE__);
    3333define('ANY_BLOCK_CAROUSEL_SLIDER_PLUGIN_URL', plugin_dir_url(__FILE__));
  • native-blocks-carousel/trunk/assets/css/carousel.css

    r3421254 r3423082  
    77 *
    88 * @package AnyBlockCarouselSlider
    9  * @version 1.0.2
     9 * @version 1.0.4
    1010 * @author weblazer35
    1111 */
     
    3535    --carousel-padding-right: 0px;
    3636    --carousel-padding-top: 0px;
    37     /* Default carousel padding-top */
    3837    --carousel-padding-bottom: 0px;
    39     /* Default carousel padding-bottom */
    4038}
    4139
     
    4644/* Force every parent containing a carousel to use position: relative */
    4745/* Required so ::scroll-button and ::scroll-marker are positioned correctly */
    48 /* ::scroll-button tends to position relative to a distant ancestor, not the carousel itself */
    4946:has(.abcs) {
    5047    position: relative !important;
    5148}
    5249
    53 /* Also force the direct parent to be positioned relative */
    54 /* Important to correctly anchor buttons to the container */
    5550.abcs {
    5651    position: relative !important;
    5752}
    5853
    59 /* S'assurer que le parent direct du carousel est aussi en position relative */
    60 /* Double safety for cases where :has() is not supported everywhere */
    61 /* IMPORTANT: the parent must be the positioning context for the buttons */
     54/* Ensure the direct parent is also positioned relative */
    6255*:has(> .abcs) {
    6356    position: relative !important;
    64     /* Ensure the parent can contain positioned buttons */
    6557    overflow: visible;
    66     /* S'assurer que le parent contraint la largeur du carousel */
    6758    max-width: 100%;
    6859    box-sizing: border-box;
    6960}
    7061
     62/* Main carousel styles - grouped selectors for editor compatibility */
    7163.abcs,
    7264.block-editor-block-list__layout .abcs,
    7365.editor-styles-wrapper .abcs {
    74     /* Layout */
    7566    position: relative !important;
    7667    width: 100%;
     
    8172    align-items: stretch;
    8273    justify-content: flex-start;
    83     /* Utiliser le gap pour l'espacement entre les slides */
    8474    gap: var(--wp--style--block-gap, 1rem);
    8575    padding: 1rem 0px;
    8676
    87     /* Scroll */
    8877    overflow-x: scroll;
    8978    overflow-y: visible;
     
    9281    -webkit-overflow-scrolling: touch;
    9382
    94     /* Scroll padding pour respecter le padding horizontal */
    95     /* Automatically set by PHP/JavaScript based on padding-left/right */
    9683    scroll-padding-left: var(--carousel-scroll-padding-left, 0);
    9784    scroll-padding-right: var(--carousel-scroll-padding-right, 0);
    9885
    99     /* Masquer les scrollbars */
    10086    -ms-overflow-style: none;
    10187    scrollbar-width: none;
    10288    -webkit-scrollbar: none;
    10389
    104     /* Scroll markers (future CSS feature) */
    10590    scroll-marker-group: after;
    10691
    107     /* Force the carousel to be the positioning context for its pseudo-elements */
    108     /* These three properties create a robust positioning context */
    10992    isolation: isolate;
    11093    contain: layout !important;
    11194    transform: translateZ(0);
    112     /* Forces a new positioning context + GPU acceleration */
    11395}
    11496
     
    120102.block-editor-block-list__layout .abcs>*,
    121103.editor-styles-wrapper .abcs>* {
    122     /* Layout */
    123104    position: relative;
    124105    flex: 0 0 auto;
    125106    width: 100%;
    126107    height: auto;
    127 
    128     /* Scroll snap */
    129108    scroll-snap-align: start;
    130     scroll-snap-stop: normal;
    131 
    132     /* Spacing */
     109    scroll-snap-stop: always;
    133110    margin: 0px !important;
    134111    padding: 0px;
     
    144121}
    145122
    146 /* Backgrounds */
    147123.abcs .has-background,
    148124.block-editor-block-list__layout .abcs .has-background,
     
    155131   ========================================================================== */
    156132
    157 /* Disable flex-wrap and the gallery column system */
    158133.wp-block-gallery.abcs,
    159134.block-editor-block-list__layout .wp-block-gallery.abcs,
     
    163138}
    164139
    165 /* Chaque figure dans la galerie carousel */
    166140.wp-block-gallery.abcs>.wp-block-image,
    167141.block-editor-block-list__layout .wp-block-gallery.abcs>.wp-block-image,
    168142.editor-styles-wrapper .wp-block-gallery.abcs>.wp-block-image {
    169143    flex: 0 0 auto;
    170     /* Default width: 6 images visible on wide desktop (> 1280px) */
    171144    width: calc(16.666% - var(--wp--style--block-gap, 1rem) * 5 / 6);
    172145    min-width: calc(16.666% - var(--wp--style--block-gap, 1rem) * 5 / 6);
     
    175148}
    176149
    177 /* Images dans la galerie carousel */
    178150.wp-block-gallery.abcs>.wp-block-image img,
    179151.block-editor-block-list__layout .wp-block-gallery.abcs>.wp-block-image img,
     
    188160   ========================================================================== */
    189161
    190 /* Convertir le CSS Grid en Flexbox pour le carousel */
    191162.is-layout-grid.abcs,
    192163.block-editor-block-list__layout .is-layout-grid.abcs,
     
    197168}
    198169
    199 /* Child elements in a carousel grid */
    200 /* Grids in carousel mode display items with a fixed width */
    201 /* Use --carousel-grid-item-width to customise */
    202170.is-layout-grid.abcs>*,
    203171.block-editor-block-list__layout .is-layout-grid.abcs>*,
    204172.editor-styles-wrapper .is-layout-grid.abcs>* {
    205173    flex: 0 0 auto;
    206     /* Default width sized for 3 visible columns */
    207174    width: var(--carousel-grid-item-width, calc(33.333% - var(--wp--style--block-gap, 1rem) * 2 / 3));
    208175    min-width: var(--carousel-grid-item-width, calc(33.333% - var(--wp--style--block-gap, 1rem) * 2 / 3));
     
    227194
    228195/* Helper classes to define the number of visible columns in the carousel grid */
    229 /* Add these classes to the Grid block to control item width */
    230 
    231 /* abcs-cols-1: 1 visible item (full width) */
     196/* Simplified: use a generic rule with --carousel-grid-item-width variable */
     197.is-layout-grid.abcs[class*="abcs-cols-"]>* {
     198    width: var(--carousel-grid-item-width);
     199    min-width: var(--carousel-grid-item-width);
     200}
     201
     202/* Define --carousel-grid-item-width for each column count */
    232203.is-layout-grid.abcs.abcs-cols-1>* {
    233204    --carousel-grid-item-width: 100%;
    234     width: 100%;
    235     min-width: 100%;
    236 }
    237 
    238 /* abcs-cols-2: 2 visible items */
     205}
     206
    239207.is-layout-grid.abcs.abcs-cols-2>* {
    240208    --carousel-grid-item-width: calc(50% - var(--wp--style--block-gap, 1rem) / 2);
    241     width: calc(50% - var(--wp--style--block-gap, 1rem) / 2);
    242     min-width: calc(50% - var(--wp--style--block-gap, 1rem) / 2);
    243 }
    244 
    245 /* abcs-cols-3: 3 visible items (default) */
     209}
     210
    246211.is-layout-grid.abcs.abcs-cols-3>* {
    247212    --carousel-grid-item-width: calc(33.333% - var(--wp--style--block-gap, 1rem) * 2 / 3);
    248     width: calc(33.333% - var(--wp--style--block-gap, 1rem) * 2 / 3);
    249     min-width: calc(33.333% - var(--wp--style--block-gap, 1rem) * 2 / 3);
    250 }
    251 
    252 /* abcs-cols-4: 4 visible items */
     213}
     214
    253215.is-layout-grid.abcs.abcs-cols-4>* {
    254216    --carousel-grid-item-width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4);
    255     width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4);
    256     min-width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4);
    257 }
    258 
    259 /* abcs-cols-5: 5 visible items */
     217}
     218
    260219.is-layout-grid.abcs.abcs-cols-5>* {
    261220    --carousel-grid-item-width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5);
    262     width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5);
    263     min-width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5);
    264 }
    265 
    266 /* abcs-cols-6: 6 visible items */
     221}
     222
    267223.is-layout-grid.abcs.abcs-cols-6>* {
    268224    --carousel-grid-item-width: calc(16.666% - var(--wp--style--block-gap, 1rem) * 5 / 6);
    269     width: calc(16.666% - var(--wp--style--block-gap, 1rem) * 5 / 6);
    270     min-width: calc(16.666% - var(--wp--style--block-gap, 1rem) * 5 / 6);
    271 }
    272 
    273 /* abcs-cols-7: 7 visible items */
     225}
     226
    274227.is-layout-grid.abcs.abcs-cols-7>* {
    275228    --carousel-grid-item-width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7);
    276     width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7);
    277     min-width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7);
    278 }
    279 
    280 /* abcs-cols-8: 8 visible items */
     229}
     230
    281231.is-layout-grid.abcs.abcs-cols-8>* {
    282232    --carousel-grid-item-width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8);
    283     width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8);
    284     min-width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8);
    285 }
    286 
    287 /* abcs-cols-9: 9 visible items */
     233}
     234
    288235.is-layout-grid.abcs.abcs-cols-9>* {
    289236    --carousel-grid-item-width: calc(11.111% - var(--wp--style--block-gap, 1rem) * 8 / 9);
    290     width: calc(11.111% - var(--wp--style--block-gap, 1rem) * 8 / 9);
    291     min-width: calc(11.111% - var(--wp--style--block-gap, 1rem) * 8 / 9);
    292 }
    293 
    294 /* abcs-cols-10: 10 visible items */
     237}
     238
    295239.is-layout-grid.abcs.abcs-cols-10>* {
    296240    --carousel-grid-item-width: calc(10% - var(--wp--style--block-gap, 1rem) * 9 / 10);
    297     width: calc(10% - var(--wp--style--block-gap, 1rem) * 9 / 10);
    298     min-width: calc(10% - var(--wp--style--block-gap, 1rem) * 9 / 10);
    299 }
    300 
    301 /* abcs-cols-11: 11 visible items */
     241}
     242
    302243.is-layout-grid.abcs.abcs-cols-11>* {
    303244    --carousel-grid-item-width: calc(9.090% - var(--wp--style--block-gap, 1rem) * 10 / 11);
    304     width: calc(9.090% - var(--wp--style--block-gap, 1rem) * 10 / 11);
    305     min-width: calc(9.090% - var(--wp--style--block-gap, 1rem) * 10 / 11);
    306 }
    307 
    308 /* abcs-cols-12: 12 visible items */
     245}
     246
    309247.is-layout-grid.abcs.abcs-cols-12>* {
    310248    --carousel-grid-item-width: calc(8.333% - var(--wp--style--block-gap, 1rem) * 11 / 12);
    311     width: calc(8.333% - var(--wp--style--block-gap, 1rem) * 11 / 12);
    312     min-width: calc(8.333% - var(--wp--style--block-gap, 1rem) * 11 / 12);
    313 }
    314 
    315 /* abcs-cols-13: 13 visible items */
     249}
     250
    316251.is-layout-grid.abcs.abcs-cols-13>* {
    317252    --carousel-grid-item-width: calc(7.692% - var(--wp--style--block-gap, 1rem) * 12 / 13);
    318     width: calc(7.692% - var(--wp--style--block-gap, 1rem) * 12 / 13);
    319     min-width: calc(7.692% - var(--wp--style--block-gap, 1rem) * 12 / 13);
    320 }
    321 
    322 /* abcs-cols-14: 14 visible items */
     253}
     254
    323255.is-layout-grid.abcs.abcs-cols-14>* {
    324256    --carousel-grid-item-width: calc(7.142% - var(--wp--style--block-gap, 1rem) * 13 / 14);
    325     width: calc(7.142% - var(--wp--style--block-gap, 1rem) * 13 / 14);
    326     min-width: calc(7.142% - var(--wp--style--block-gap, 1rem) * 13 / 14);
    327 }
    328 
    329 /* abcs-cols-15: 15 visible items */
     257}
     258
    330259.is-layout-grid.abcs.abcs-cols-15>* {
    331260    --carousel-grid-item-width: calc(6.666% - var(--wp--style--block-gap, 1rem) * 14 / 15);
    332     width: calc(6.666% - var(--wp--style--block-gap, 1rem) * 14 / 15);
    333     min-width: calc(6.666% - var(--wp--style--block-gap, 1rem) * 14 / 15);
    334 }
    335 
    336 /* abcs-cols-16: 16 visible items */
     261}
     262
    337263.is-layout-grid.abcs.abcs-cols-16>* {
    338264    --carousel-grid-item-width: calc(6.25% - var(--wp--style--block-gap, 1rem) * 15 / 16);
    339     width: calc(6.25% - var(--wp--style--block-gap, 1rem) * 15 / 16);
    340     min-width: calc(6.25% - var(--wp--style--block-gap, 1rem) * 15 / 16);
    341265}
    342266
    343267/* abcs-min-width: use the minimum width defined in the grid */
    344 /* WordPress generates: grid-template-columns: repeat(auto-fill, minmax(XXXpx, 1fr)) */
    345 /* On doit extraire cette valeur et l'utiliser comme largeur fixe en mode Flexbox */
    346268.is-layout-grid.abcs.abcs-min-width>*,
    347269.block-editor-block-list__layout .is-layout-grid.abcs.abcs-min-width>*,
    348 .editor-styles-wrapper .is-layout-grid.abcs.abcs-min-width>* {
    349     /* Largeur fixe avec adaptation sur mobile */
    350     flex: 0 0 auto !important;
    351     /* Use min() to respect the fixed width unless the viewport is smaller */
    352     width: min(var(--carousel-min-width, 200px), 100%) !important;
    353     min-width: min(var(--carousel-min-width, 200px), 100%) !important;
    354     max-width: 100% !important;
    355 }
    356 
    357 /* Specific rules for Post Template (children are <li class="wp-block-post">) */
     270.editor-styles-wrapper .is-layout-grid.abcs.abcs-min-width>*,
    358271.wp-block-post-template.abcs.abcs-min-width .wp-block-post,
    359272.block-editor-block-list__layout .wp-block-post-template.abcs.abcs-min-width .wp-block-post,
    360 .editor-styles-wrapper .wp-block-post-template.abcs.abcs-min-width .wp-block-post {
    361     flex: 0 0 auto !important;
    362     width: min(var(--carousel-min-width, 200px), 100%) !important;
    363     min-width: min(var(--carousel-min-width, 200px), 100%) !important;
    364     max-width: 100% !important;
    365 }
    366 
    367 /* Specific rules for Group blocks (children can be <div> or other elements) */
     273.editor-styles-wrapper .wp-block-post-template.abcs.abcs-min-width .wp-block-post,
    368274.wp-block-group.abcs.abcs-min-width>*,
    369275.block-editor-block-list__layout .wp-block-group.abcs.abcs-min-width>*,
     
    379285   ========================================================================== */
    380286
    381 /* En mode Auto (abcs-min-width), le snap se fait sur le centre */
    382287.abcs.abcs-min-width>*,
    383288.block-editor-block-list__layout .abcs.abcs-min-width>*,
    384 .editor-styles-wrapper .abcs.abcs-min-width>* {
    385     scroll-snap-align: center !important;
    386 }
    387 
    388 /* ==========================================================================
    389    SCROLL SNAP POUR POST TEMPLATE EN MODE LIST VIEW
    390    ========================================================================== */
    391 
    392 /* Post Template en mode list view (sans is-layout-grid) : snap sur le centre */
     289.editor-styles-wrapper .abcs.abcs-min-width>*,
    393290.wp-block-post-template.abcs:not(.is-layout-grid) .wp-block-post,
    394291.block-editor-block-list__layout .wp-block-post-template.abcs:not(.is-layout-grid) .wp-block-post,
    395 .editor-styles-wrapper .wp-block-post-template.abcs:not(.is-layout-grid) .wp-block-post {
    396     scroll-snap-align: center !important;
    397 }
    398 
    399 /* Specific rules for Post Template in Auto mode */
     292.editor-styles-wrapper .wp-block-post-template.abcs:not(.is-layout-grid) .wp-block-post,
    400293.wp-block-post-template.abcs.abcs-min-width .wp-block-post,
    401294.block-editor-block-list__layout .wp-block-post-template.abcs.abcs-min-width .wp-block-post,
    402 .editor-styles-wrapper .wp-block-post-template.abcs.abcs-min-width .wp-block-post {
    403     scroll-snap-align: center !important;
    404 }
    405 
    406 /* Specific rules for Group in Auto mode */
     295.editor-styles-wrapper .wp-block-post-template.abcs.abcs-min-width .wp-block-post,
    407296.wp-block-group.abcs.abcs-min-width>*,
    408297.block-editor-block-list__layout .wp-block-group.abcs.abcs-min-width>*,
    409 .editor-styles-wrapper .wp-block-group.abcs.abcs-min-width>* {
    410     scroll-snap-align: center !important;
    411 }
    412 
    413 /* Specific rules for Gallery in Auto mode */
     298.editor-styles-wrapper .wp-block-group.abcs.abcs-min-width>*,
    414299.wp-block-gallery.abcs.abcs-min-width>.wp-block-image,
    415300.block-editor-block-list__layout .wp-block-gallery.abcs.abcs-min-width>.wp-block-image,
     
    423308
    424309/* Fallback : indicateur de pagination pour navigateurs non compatibles */
    425 /* Objectif : signaler visuellement le carrousel sans boutons cliquables */
    426310:has(> .abcs)::after {
    427311    content: '';
     
    456340}
    457341
    458 
    459 
    460342/* Sur les navigateurs compatibles, masquer les fallbacks et afficher les vrais boutons */
    461343@supports selector(::scroll-button(*)) {
    462 
    463     /* Masquer les fallbacks visuels */
    464344    :has(> .abcs)::after {
    465345        display: none;
     
    474354        line-height: 1em;
    475355        border-radius: 100rem;
    476 
    477         /* Apparence */
    478356        background-color: var(--carousel-button-bg);
    479357        border: 2px solid var(--carousel-button-bg);
     
    481359        opacity: 1;
    482360        visibility: visible;
    483 
    484         /* Interaction */
    485361        cursor: pointer;
    486362        text-align: center;
    487363        display: inline-block;
    488 
    489         /* Effets */
    490364        box-shadow: var(--carousel-shadow);
    491365        transition: all var(--carousel-transition-duration) var(--carousel-transition-easing);
    492 
    493         /* Prepare for SVG */
    494366        font-size: 0px;
    495367        overflow: hidden;
    496 
    497         /* Ensure left and right are not set by default */
    498368        left: auto;
    499369        right: auto;
     
    513383    }
    514384
    515     /* Boutons - Prendre en compte le padding pour TOUS les carrousels */
    516     /* Simple approach: rely on padding variables already defined by JavaScript */
    517     /* Le calcul se fait dans le CSS avec calc() */
     385    /* Keep scroll buttons enabled when loop is active */
     386    .abcs[data-abcs-loop="true"]::scroll-button(*):disabled {
     387        opacity: 1;
     388        visibility: visible;
     389        pointer-events: auto;
     390        cursor: pointer;
     391    }
     392
    518393    .abcs::scroll-button(left) {
    519394        left: calc(var(--carousel-padding-left, 0px) + var(--carousel-button-offset, 0px));
    520395        content: '';
    521396        transform: translate(-50%, -50%);
    522         /* Use the dynamically generated CSS variable with a fallback to the white SVG */
    523397        background-image: var(--carousel-button-arrow-left, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='white' d='M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z'/%3E%3C/svg%3E"));
    524398        background-repeat: no-repeat;
     
    532406        content: '';
    533407        transform: translate(50%, -50%);
    534         /* Use the dynamically generated CSS variable with a fallback to the white SVG */
    535408        background-image: var(--carousel-button-arrow-right, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 512'%3E%3Cpath fill='white' d='M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z'/%3E%3C/svg%3E"));
    536409        background-repeat: no-repeat;
     
    555428
    556429/* ==========================================================================
    557    MARQUEURS DE SCROLL (CSS natif futur) - NE PAS MODIFIER
     430   MARQUEURS DE SCROLL (CSS natif futur)
    558431   ========================================================================== */
    559432
    560433@supports selector(::scroll-marker-group) {
    561 
    562     /* Masquer les fallbacks visuels */
    563434    :has(> .abcs)::after {
    564435        display: none;
     
    568439        position: absolute;
    569440        z-index: var(--carousel-z-index);
    570         /* Positionner par rapport au contenu, en ignorant le padding-bottom */
    571         /* bottom: offset + padding-bottom pour compenser le padding du carousel */
    572         /* The variable --carousel-padding-bottom is set dynamically by PHP/JavaScript */
    573441        bottom: calc(var(--carousel-marker-bottom-offset) + var(--carousel-padding-bottom, 1rem));
    574442        left: var(--carousel-padding-left, 0);
     
    601469    }
    602470
     471
    603472    .abcs>*::scroll-marker:hover {
    604473        opacity: 0.75;
     
    615484   ========================================================================== */
    616485
    617 /*
    618    Rules derived from analysing WordPress CSS:
     486/* Rules derived from analysing WordPress CSS:
    619487   - Primary breakpoint: 600px (mobile/tablet)
    620488   - Secondary breakpoint: 782px (tablet/desktop)
     
    623491
    624492/* ==========================================================================
    625    POST TEMPLATE - MATCHES WORDPRESS BEHAVIOUR
    626    ========================================================================== */
    627 
    628 /* Mobile (default) - 1 column */
    629 /* Exclude min-width mode which has its own rules */
    630 .wp-block-post-template.is-layout-grid.abcs:not(.abcs-min-width) .wp-block-post {
     493   POST TEMPLATE, GALLERY, GROUP - RESPONSIVE COLUMNS
     494   ========================================================================== */
     495
     496/* Mobile (default) - 1 column for Post Template and Group */
     497.wp-block-post-template.is-layout-grid.abcs:not(.abcs-min-width) .wp-block-post,
     498.wp-block-group.is-layout-grid.abcs:not(.abcs-min-width)>* {
    631499    width: 100% !important;
    632500    min-width: 100% !important;
    633501}
    634502
     503/* Mobile (default) - 2 columns for Gallery */
     504.wp-block-gallery.abcs .wp-block-image {
     505    width: calc(50% - var(--wp--style--block-gap, 1rem) / 2) !important;
     506    min-width: calc(50% - var(--wp--style--block-gap, 1rem) / 2) !important;
     507}
     508
    635509/* From 600px onwards - behaviour determined by column count */
     510/* Generic rule for all block types with column classes */
    636511@media (min-width: 600px) {
    637512
    638     /* 2 columns */
     513    /* Post Template columns */
    639514    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-2 .wp-block-post {
    640         width: calc(50% - var(--wp--style--block-gap, 1.25em) / 2) !important;
    641         min-width: calc(50% - var(--wp--style--block-gap, 1.25em) / 2) !important;
    642     }
    643 
    644     /* 3 columns */
     515        --carousel-grid-item-width: calc(50% - var(--wp--style--block-gap, 1.25em) / 2);
     516    }
     517
    645518    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-3 .wp-block-post {
    646         width: calc(33.33333% - var(--wp--style--block-gap, 1.25em) * 2 / 3) !important;
    647         min-width: calc(33.33333% - var(--wp--style--block-gap, 1.25em) * 2 / 3) !important;
    648     }
    649 
    650     /* 4 columns */
     519        --carousel-grid-item-width: calc(33.33333% - var(--wp--style--block-gap, 1.25em) * 2 / 3);
     520    }
     521
    651522    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-4 .wp-block-post {
    652         width: calc(25% - var(--wp--style--block-gap, 1.25em) * 3 / 4) !important;
    653         min-width: calc(25% - var(--wp--style--block-gap, 1.25em) * 3 / 4) !important;
    654     }
    655 
    656     /* 5 columns */
     523        --carousel-grid-item-width: calc(25% - var(--wp--style--block-gap, 1.25em) * 3 / 4);
     524    }
     525
    657526    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-5 .wp-block-post {
    658         width: calc(20% - var(--wp--style--block-gap, 1.25em) * 4 / 5) !important;
    659         min-width: calc(20% - var(--wp--style--block-gap, 1.25em) * 4 / 5) !important;
    660     }
    661 
    662     /* 6 columns */
     527        --carousel-grid-item-width: calc(20% - var(--wp--style--block-gap, 1.25em) * 4 / 5);
     528    }
     529
    663530    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-6 .wp-block-post {
    664         width: calc(16.66667% - var(--wp--style--block-gap, 1.25em) * 5 / 6) !important;
    665         min-width: calc(16.66667% - var(--wp--style--block-gap, 1.25em) * 5 / 6) !important;
    666     }
    667 
    668     /* 7 columns */
     531        --carousel-grid-item-width: calc(16.66667% - var(--wp--style--block-gap, 1.25em) * 5 / 6);
     532    }
     533
    669534    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-7 .wp-block-post {
    670         width: calc(14.285% - var(--wp--style--block-gap, 1.25em) * 6 / 7) !important;
    671         min-width: calc(14.285% - var(--wp--style--block-gap, 1.25em) * 6 / 7) !important;
    672     }
    673 
    674     /* 8 columns */
     535        --carousel-grid-item-width: calc(14.285% - var(--wp--style--block-gap, 1.25em) * 6 / 7);
     536    }
     537
    675538    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-8 .wp-block-post {
    676         width: calc(12.5% - var(--wp--style--block-gap, 1.25em) * 7 / 8) !important;
    677         min-width: calc(12.5% - var(--wp--style--block-gap, 1.25em) * 7 / 8) !important;
    678     }
    679 
    680     /* 9 columns */
     539        --carousel-grid-item-width: calc(12.5% - var(--wp--style--block-gap, 1.25em) * 7 / 8);
     540    }
     541
    681542    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-9 .wp-block-post {
    682         width: calc(11.111% - var(--wp--style--block-gap, 1.25em) * 8 / 9) !important;
    683         min-width: calc(11.111% - var(--wp--style--block-gap, 1.25em) * 8 / 9) !important;
    684     }
    685 
    686     /* 10 columns */
     543        --carousel-grid-item-width: calc(11.111% - var(--wp--style--block-gap, 1.25em) * 8 / 9);
     544    }
     545
    687546    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-10 .wp-block-post {
    688         width: calc(10% - var(--wp--style--block-gap, 1.25em) * 9 / 10) !important;
    689         min-width: calc(10% - var(--wp--style--block-gap, 1.25em) * 9 / 10) !important;
    690     }
    691 
    692     /* 11 columns */
     547        --carousel-grid-item-width: calc(10% - var(--wp--style--block-gap, 1.25em) * 9 / 10);
     548    }
     549
    693550    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-11 .wp-block-post {
    694         width: calc(9.090% - var(--wp--style--block-gap, 1.25em) * 10 / 11) !important;
    695         min-width: calc(9.090% - var(--wp--style--block-gap, 1.25em) * 10 / 11) !important;
    696     }
    697 
    698     /* 12 columns */
     551        --carousel-grid-item-width: calc(9.090% - var(--wp--style--block-gap, 1.25em) * 10 / 11);
     552    }
     553
    699554    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-12 .wp-block-post {
    700         width: calc(8.333% - var(--wp--style--block-gap, 1.25em) * 11 / 12) !important;
    701         min-width: calc(8.333% - var(--wp--style--block-gap, 1.25em) * 11 / 12) !important;
    702     }
    703 
    704     /* 13 columns */
     555        --carousel-grid-item-width: calc(8.333% - var(--wp--style--block-gap, 1.25em) * 11 / 12);
     556    }
     557
    705558    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-13 .wp-block-post {
    706         width: calc(7.692% - var(--wp--style--block-gap, 1.25em) * 12 / 13) !important;
    707         min-width: calc(7.692% - var(--wp--style--block-gap, 1.25em) * 12 / 13) !important;
    708     }
    709 
    710     /* 14 columns */
     559        --carousel-grid-item-width: calc(7.692% - var(--wp--style--block-gap, 1.25em) * 12 / 13);
     560    }
     561
    711562    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-14 .wp-block-post {
    712         width: calc(7.142% - var(--wp--style--block-gap, 1.25em) * 13 / 14) !important;
    713         min-width: calc(7.142% - var(--wp--style--block-gap, 1.25em) * 13 / 14) !important;
    714     }
    715 
    716     /* 15 columns */
     563        --carousel-grid-item-width: calc(7.142% - var(--wp--style--block-gap, 1.25em) * 13 / 14);
     564    }
     565
    717566    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-15 .wp-block-post {
    718         width: calc(6.666% - var(--wp--style--block-gap, 1.25em) * 14 / 15) !important;
    719         min-width: calc(6.666% - var(--wp--style--block-gap, 1.25em) * 14 / 15) !important;
    720     }
    721 
    722     /* 16 columns */
     567        --carousel-grid-item-width: calc(6.666% - var(--wp--style--block-gap, 1.25em) * 14 / 15);
     568    }
     569
    723570    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-16 .wp-block-post {
    724         width: calc(6.25% - var(--wp--style--block-gap, 1.25em) * 15 / 16) !important;
    725         min-width: calc(6.25% - var(--wp--style--block-gap, 1.25em) * 15 / 16) !important;
    726     }
    727 }
    728 
    729 /* Comportement responsive natif WordPress pour tablette */
    730 /* Between 600px and 1200px, Post Template always shows 2 columns */
     571        --carousel-grid-item-width: calc(6.25% - var(--wp--style--block-gap, 1.25em) * 15 / 16);
     572    }
     573
     574    /* Apply width and min-width using the variable */
     575    .wp-block-post-template.is-layout-grid.abcs[class*="abcs-cols-"] .wp-block-post {
     576        width: var(--carousel-grid-item-width) !important;
     577        min-width: var(--carousel-grid-item-width) !important;
     578    }
     579
     580    /* Gallery columns */
     581    .wp-block-gallery.abcs.abcs-cols-1 .wp-block-image {
     582        --carousel-grid-item-width: 100%;
     583    }
     584
     585    .wp-block-gallery.abcs.abcs-cols-2 .wp-block-image {
     586        --carousel-grid-item-width: calc(50% - var(--wp--style--block-gap, 1rem) / 2);
     587    }
     588
     589    .wp-block-gallery.abcs.abcs-cols-3 .wp-block-image {
     590        --carousel-grid-item-width: calc(33.33333% - var(--wp--style--block-gap, 1rem) * 2 / 3);
     591    }
     592
     593    .wp-block-gallery.abcs.abcs-cols-4 .wp-block-image {
     594        --carousel-grid-item-width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4);
     595    }
     596
     597    .wp-block-gallery.abcs.abcs-cols-5 .wp-block-image {
     598        --carousel-grid-item-width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5);
     599    }
     600
     601    .wp-block-gallery.abcs.abcs-cols-6 .wp-block-image {
     602        --carousel-grid-item-width: calc(16.66667% - var(--wp--style--block-gap, 1rem) * 5 / 6);
     603    }
     604
     605    .wp-block-gallery.abcs.abcs-cols-7 .wp-block-image {
     606        --carousel-grid-item-width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7);
     607    }
     608
     609    .wp-block-gallery.abcs.abcs-cols-8 .wp-block-image {
     610        --carousel-grid-item-width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8);
     611    }
     612
     613    .wp-block-gallery.abcs[class*="abcs-cols-"] .wp-block-image {
     614        width: var(--carousel-grid-item-width) !important;
     615        min-width: var(--carousel-grid-item-width) !important;
     616    }
     617
     618    /* Group columns */
     619    .wp-block-group.is-layout-grid.abcs.abcs-cols-2>* {
     620        --carousel-grid-item-width: calc(50% - var(--wp--style--block-gap, 1rem) / 2);
     621    }
     622
     623    .wp-block-group.is-layout-grid.abcs.abcs-cols-3>* {
     624        --carousel-grid-item-width: calc(33.33333% - var(--wp--style--block-gap, 1rem) * 2 / 3);
     625    }
     626
     627    .wp-block-group.is-layout-grid.abcs.abcs-cols-4>* {
     628        --carousel-grid-item-width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4);
     629    }
     630
     631    .wp-block-group.is-layout-grid.abcs.abcs-cols-5>* {
     632        --carousel-grid-item-width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5);
     633    }
     634
     635    .wp-block-group.is-layout-grid.abcs.abcs-cols-6>* {
     636        --carousel-grid-item-width: calc(16.66667% - var(--wp--style--block-gap, 1rem) * 5 / 6);
     637    }
     638
     639    .wp-block-group.is-layout-grid.abcs.abcs-cols-7>* {
     640        --carousel-grid-item-width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7);
     641    }
     642
     643    .wp-block-group.is-layout-grid.abcs.abcs-cols-8>* {
     644        --carousel-grid-item-width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8);
     645    }
     646
     647    .wp-block-group.is-layout-grid.abcs.abcs-cols-9>* {
     648        --carousel-grid-item-width: calc(11.111% - var(--wp--style--block-gap, 1rem) * 8 / 9);
     649    }
     650
     651    .wp-block-group.is-layout-grid.abcs.abcs-cols-10>* {
     652        --carousel-grid-item-width: calc(10% - var(--wp--style--block-gap, 1rem) * 9 / 10);
     653    }
     654
     655    .wp-block-group.is-layout-grid.abcs.abcs-cols-11>* {
     656        --carousel-grid-item-width: calc(9.090% - var(--wp--style--block-gap, 1rem) * 10 / 11);
     657    }
     658
     659    .wp-block-group.is-layout-grid.abcs.abcs-cols-12>* {
     660        --carousel-grid-item-width: calc(8.333% - var(--wp--style--block-gap, 1rem) * 11 / 12);
     661    }
     662
     663    .wp-block-group.is-layout-grid.abcs.abcs-cols-13>* {
     664        --carousel-grid-item-width: calc(7.692% - var(--wp--style--block-gap, 1rem) * 12 / 13);
     665    }
     666
     667    .wp-block-group.is-layout-grid.abcs.abcs-cols-14>* {
     668        --carousel-grid-item-width: calc(7.142% - var(--wp--style--block-gap, 1rem) * 13 / 14);
     669    }
     670
     671    .wp-block-group.is-layout-grid.abcs.abcs-cols-15>* {
     672        --carousel-grid-item-width: calc(6.666% - var(--wp--style--block-gap, 1rem) * 14 / 15);
     673    }
     674
     675    .wp-block-group.is-layout-grid.abcs.abcs-cols-16>* {
     676        --carousel-grid-item-width: calc(6.25% - var(--wp--style--block-gap, 1rem) * 15 / 16);
     677    }
     678
     679    .wp-block-group.is-layout-grid.abcs[class*="abcs-cols-"]>* {
     680        width: var(--carousel-grid-item-width) !important;
     681        min-width: var(--carousel-grid-item-width) !important;
     682    }
     683}
     684
     685/* Tablet behavior: force 2 columns for Post Template and Group (3+ columns) */
    731686@media (min-width: 600px) and (max-width: 1200px) {
    732687
    733     /* 3 columns and up: force 2 columns on tablets */
    734688    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-3 .wp-block-post,
    735689    .wp-block-post-template.is-layout-grid.abcs.abcs-cols-4 .wp-block-post,
     
    749703        min-width: calc(50% - var(--wp--style--block-gap, 1.25em) / 2) !important;
    750704    }
    751 }
    752 
    753 /* ==========================================================================
    754    GALLERY - MATCHES WORDPRESS BEHAVIOUR
    755    ========================================================================== */
    756 
    757 /* Mobile (default) - 2 columns for galleries */
    758 .wp-block-gallery.abcs .wp-block-image {
    759     width: calc(50% - var(--wp--style--block-gap, 1rem) / 2) !important;
    760     min-width: calc(50% - var(--wp--style--block-gap, 1rem) / 2) !important;
    761 }
    762 
    763 /* From 600px onwards - behaviour determined by column count */
    764 @media (min-width: 600px) {
    765 
    766     /* 1 column */
    767     .wp-block-gallery.abcs.abcs-cols-1 .wp-block-image {
    768         width: 100% !important;
    769         min-width: 100% !important;
    770     }
    771 
    772     /* 2 columns */
    773     .wp-block-gallery.abcs.abcs-cols-2 .wp-block-image {
    774         width: calc(50% - var(--wp--style--block-gap, 1rem) / 2) !important;
    775         min-width: calc(50% - var(--wp--style--block-gap, 1rem) / 2) !important;
    776     }
    777 
    778     /* 3 columns */
    779     .wp-block-gallery.abcs.abcs-cols-3 .wp-block-image {
    780         width: calc(33.33333% - var(--wp--style--block-gap, 1rem) * 2 / 3) !important;
    781         min-width: calc(33.33333% - var(--wp--style--block-gap, 1rem) * 2 / 3) !important;
    782     }
    783 
    784     /* 4 columns */
    785     .wp-block-gallery.abcs.abcs-cols-4 .wp-block-image {
    786         width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4) !important;
    787         min-width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4) !important;
    788     }
    789 
    790     /* 5 columns */
    791     .wp-block-gallery.abcs.abcs-cols-5 .wp-block-image {
    792         width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5) !important;
    793         min-width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5) !important;
    794     }
    795 
    796     /* 6 columns */
    797     .wp-block-gallery.abcs.abcs-cols-6 .wp-block-image {
    798         width: calc(16.66667% - var(--wp--style--block-gap, 1rem) * 5 / 6) !important;
    799         min-width: calc(16.66667% - var(--wp--style--block-gap, 1rem) * 5 / 6) !important;
    800     }
    801 
    802     /* 7 columns */
    803     .wp-block-gallery.abcs.abcs-cols-7 .wp-block-image {
    804         width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7) !important;
    805         min-width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7) !important;
    806     }
    807 
    808     /* 8 columns */
    809     .wp-block-gallery.abcs.abcs-cols-8 .wp-block-image {
    810         width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8) !important;
    811         min-width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8) !important;
    812     }
    813 }
    814 
    815 /* ==========================================================================
    816    GROUP - MATCHES WORDPRESS BEHAVIOUR
    817    ========================================================================== */
    818 
    819 /* Mobile (default) - 1 column */
    820 /* Exclude min-width mode which has its own rules */
    821 .wp-block-group.is-layout-grid.abcs:not(.abcs-min-width)>* {
    822     width: 100% !important;
    823     min-width: 100% !important;
    824 }
    825 
    826 /* From 600px onwards - behaviour determined by column count */
    827 @media (min-width: 600px) {
    828 
    829     /* 2 columns */
    830     .wp-block-group.is-layout-grid.abcs.abcs-cols-2>* {
    831         width: calc(50% - var(--wp--style--block-gap, 1rem) / 2) !important;
    832         min-width: calc(50% - var(--wp--style--block-gap, 1rem) / 2) !important;
    833     }
    834 
    835     /* 3 columns */
    836     .wp-block-group.is-layout-grid.abcs.abcs-cols-3>* {
    837         width: calc(33.33333% - var(--wp--style--block-gap, 1rem) * 2 / 3) !important;
    838         min-width: calc(33.33333% - var(--wp--style--block-gap, 1rem) * 2 / 3) !important;
    839     }
    840 
    841     /* 4 columns */
    842     .wp-block-group.is-layout-grid.abcs.abcs-cols-4>* {
    843         width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4) !important;
    844         min-width: calc(25% - var(--wp--style--block-gap, 1rem) * 3 / 4) !important;
    845     }
    846 
    847     /* 5 columns */
    848     .wp-block-group.is-layout-grid.abcs.abcs-cols-5>* {
    849         width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5) !important;
    850         min-width: calc(20% - var(--wp--style--block-gap, 1rem) * 4 / 5) !important;
    851     }
    852 
    853     /* 6 columns */
    854     .wp-block-group.is-layout-grid.abcs.abcs-cols-6>* {
    855         width: calc(16.66667% - var(--wp--style--block-gap, 1rem) * 5 / 6) !important;
    856         min-width: calc(16.66667% - var(--wp--style--block-gap, 1rem) * 5 / 6) !important;
    857     }
    858 
    859     /* 7 columns */
    860     .wp-block-group.is-layout-grid.abcs.abcs-cols-7>* {
    861         width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7) !important;
    862         min-width: calc(14.285% - var(--wp--style--block-gap, 1rem) * 6 / 7) !important;
    863     }
    864 
    865     /* 8 columns */
    866     .wp-block-group.is-layout-grid.abcs.abcs-cols-8>* {
    867         width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8) !important;
    868         min-width: calc(12.5% - var(--wp--style--block-gap, 1rem) * 7 / 8) !important;
    869     }
    870 
    871     /* 9 columns */
    872     .wp-block-group.is-layout-grid.abcs.abcs-cols-9>* {
    873         width: calc(11.111% - var(--wp--style--block-gap, 1rem) * 8 / 9) !important;
    874         min-width: calc(11.111% - var(--wp--style--block-gap, 1rem) * 8 / 9) !important;
    875     }
    876 
    877     /* 10 columns */
    878     .wp-block-group.is-layout-grid.abcs.abcs-cols-10>* {
    879         width: calc(10% - var(--wp--style--block-gap, 1rem) * 9 / 10) !important;
    880         min-width: calc(10% - var(--wp--style--block-gap, 1rem) * 9 / 10) !important;
    881     }
    882 
    883     /* 11 columns */
    884     .wp-block-group.is-layout-grid.abcs.abcs-cols-11>* {
    885         width: calc(9.090% - var(--wp--style--block-gap, 1rem) * 10 / 11) !important;
    886         min-width: calc(9.090% - var(--wp--style--block-gap, 1rem) * 10 / 11) !important;
    887     }
    888 
    889     /* 12 columns */
    890     .wp-block-group.is-layout-grid.abcs.abcs-cols-12>* {
    891         width: calc(8.333% - var(--wp--style--block-gap, 1rem) * 11 / 12) !important;
    892         min-width: calc(8.333% - var(--wp--style--block-gap, 1rem) * 11 / 12) !important;
    893     }
    894 
    895     /* 13 columns */
    896     .wp-block-group.is-layout-grid.abcs.abcs-cols-13>* {
    897         width: calc(7.692% - var(--wp--style--block-gap, 1rem) * 12 / 13) !important;
    898         min-width: calc(7.692% - var(--wp--style--block-gap, 1rem) * 12 / 13) !important;
    899     }
    900 
    901     /* 14 columns */
    902     .wp-block-group.is-layout-grid.abcs.abcs-cols-14>* {
    903         width: calc(7.142% - var(--wp--style--block-gap, 1rem) * 13 / 14) !important;
    904         min-width: calc(7.142% - var(--wp--style--block-gap, 1rem) * 13 / 14) !important;
    905     }
    906 
    907     /* 15 columns */
    908     .wp-block-group.is-layout-grid.abcs.abcs-cols-15>* {
    909         width: calc(6.666% - var(--wp--style--block-gap, 1rem) * 14 / 15) !important;
    910         min-width: calc(6.666% - var(--wp--style--block-gap, 1rem) * 14 / 15) !important;
    911     }
    912 
    913     /* 16 columns */
    914     .wp-block-group.is-layout-grid.abcs.abcs-cols-16>* {
    915         width: calc(6.25% - var(--wp--style--block-gap, 1rem) * 15 / 16) !important;
    916         min-width: calc(6.25% - var(--wp--style--block-gap, 1rem) * 15 / 16) !important;
    917     }
    918 }
    919 
    920 /* Comportement responsive natif WordPress pour tablette - GROUP */
    921 /* Between 600px and 1200px, Group grids always display 2 columns */
    922 @media (min-width: 600px) and (max-width: 1200px) {
    923 
    924     /* 3 columns and up: force 2 columns on tablets */
     705
    925706    .wp-block-group.is-layout-grid.abcs.abcs-cols-3>*,
    926707    .wp-block-group.is-layout-grid.abcs.abcs-cols-4>*,
     
    946727   ========================================================================== */
    947728
    948 /* Button and marker adjustments based on screen size */
    949729@media (max-width: 782px) {
    950730    :root {
     
    963743    }
    964744
    965     /* Reduce the gap on mobile */
    966745    .abcs {
    967746        gap: var(--wp--style--block-gap, 0.5rem);
    968747        padding: 0.75rem 0px;
    969748    }
    970 }
    971 
    972 @media (max-width: 480px) {
    973     :root {
    974         --carousel-button-size: 2rem;
    975         --carousel-button-offset: 0.25rem;
    976         --carousel-marker-size: 0.4rem;
    977         --carousel-marker-gap: 0.35rem;
    978         --carousel-marker-bottom-offset: -1.5rem;
    979     }
    980 
    981     /* Reduce the gap even further */
    982     .abcs {
    983         gap: var(--wp--style--block-gap, 0.25rem);
    984         padding: 1rem 0px;
    985     }
    986 }
    987 
    988 @media (max-width: 375px) {
    989     :root {
    990         --carousel-button-size: 1.75rem;
    991         --carousel-marker-size: 0.35rem;
    992     }
    993 }
    994 
    995 /* ==========================================================================
    996    GUTENBERG EDITOR STYLES
    997    ========================================================================== */
    998 
    999 /* Toggle Control */
    1000 .cfg-abcs-toggle-control {
    1001     margin-bottom: 1rem;
    1002 }
    1003 
    1004 .cfg-abcs-toggle-control .components-base-control__label {
    1005     font-weight: 600;
    1006     color: #1e1e1e;
    1007 }
    1008 
    1009 .cfg-abcs-toggle-control .components-base-control__help {
    1010     font-size: 12px;
    1011     color: #757575;
    1012     margin-top: 4px;
    1013 }
    1014 
    1015 .cfg-abcs-toggle-control .components-toggle-control__label[for*="carousel"] {
    1016     color: #007cba;
    1017     font-weight: 600;
    1018 }
    1019 
    1020 /* Panel Carousel */
    1021 .cfg-abcs-panel .components-panel__body-title {
    1022     font-weight: 600;
    1023 }
    1024 
    1025 .cfg-abcs-panel-first {
    1026     order: -1000 !important;
    1027 }
    1028 
    1029 .block-editor-block-inspector .cfg-abcs-panel-first {
    1030     order: -1000 !important;
    1031     margin-bottom: 16px;
    1032 }
    1033 
    1034 /* Fix editor click behaviour */
    1035 .block-editor-block-list__layout .abcs>*:not(:first-child),
    1036 .editor-styles-wrapper .abcs>*:not(:first-child) {
    1037     pointer-events: none;
    1038 }
    1039 
    1040 /* ==========================================================================
    1041    PERFORMANCE ET OPTIMISATIONS
    1042    ========================================================================== */
    1043 
    1044 /* Optimisation du rendu */
    1045 .abcs,
    1046 .block-editor-block-list__layout .abcs,
    1047 .editor-styles-wrapper .abcs {
    1048     will-change: scroll-position;
    1049     contain: layout style;
    1050 }
    1051 
    1052 /* Optimise rendered elements */
    1053 .abcs>*,
    1054 .block-editor-block-list__layout .abcs>*,
    1055 .editor-styles-wrapper .abcs>* {
    1056     will-change: transform;
    1057 }
    1058 
    1059 /* ==========================================================================
    1060    ACCESSIBILITY
    1061    ========================================================================== */
    1062 
    1063 /* Honour reduced motion preferences */
    1064 @media (prefers-reduced-motion: reduce) {
    1065 
    1066     .abcs,
    1067     .block-editor-block-list__layout .abcs,
    1068     .editor-styles-wrapper .abcs {
    1069         scroll-behavior: auto;
    1070     }
    1071 
    1072     .abcs::scroll-button(*),
    1073     *:has(>.abcs)::after {
    1074         transition: none;
    1075     }
    1076 
    1077     .abcs>*::scroll-marker {
    1078         transition: none;
    1079     }
    1080 }
    1081 
    1082 /* High contrast */
    1083 @media (prefers-contrast: high) {
    1084 
    1085     .abcs::scroll-button(*),
    1086     *:has(>.abcs)::after {
    1087         border-width: 3px;
    1088     }
    1089 
    1090     .abcs>*::scroll-marker {
    1091         border: 2px solid currentColor;
    1092     }
    1093 }
    1094 
    1095 @media (max-width: 600px) {
    1096749
    1097750    .abcs.abcs-min-width>*,
     
    1112765    }
    1113766}
     767
     768@media (max-width: 480px) {
     769    :root {
     770        --carousel-button-size: 2rem;
     771        --carousel-button-offset: 0.25rem;
     772        --carousel-marker-size: 0.4rem;
     773        --carousel-marker-gap: 0.35rem;
     774        --carousel-marker-bottom-offset: -1.5rem;
     775    }
     776
     777    .abcs {
     778        gap: var(--wp--style--block-gap, 0.25rem);
     779        padding: 1rem 0px;
     780    }
     781}
     782
     783@media (max-width: 375px) {
     784    :root {
     785        --carousel-button-size: 1.75rem;
     786        --carousel-marker-size: 0.35rem;
     787    }
     788}
     789
     790/* ==========================================================================
     791   GUTENBERG EDITOR STYLES
     792   ========================================================================== */
     793
     794.cfg-abcs-toggle-control {
     795    margin-bottom: 1rem;
     796}
     797
     798.cfg-abcs-toggle-control .components-base-control__label {
     799    font-weight: 600;
     800    color: #1e1e1e;
     801}
     802
     803.cfg-abcs-toggle-control .components-base-control__help {
     804    font-size: 12px;
     805    color: #757575;
     806    margin-top: 4px;
     807}
     808
     809.cfg-abcs-toggle-control .components-toggle-control__label[for*="carousel"] {
     810    color: #007cba;
     811    font-weight: 600;
     812}
     813
     814.cfg-abcs-panel .components-panel__body-title {
     815    font-weight: 600;
     816}
     817
     818.cfg-abcs-panel-first {
     819    order: -1000 !important;
     820}
     821
     822.block-editor-block-inspector .cfg-abcs-panel-first {
     823    order: -1000 !important;
     824    margin-bottom: 16px;
     825}
     826
     827.block-editor-block-list__layout .abcs>*:not(:first-child),
     828.editor-styles-wrapper .abcs>*:not(:first-child) {
     829    pointer-events: none;
     830}
     831
     832/* ==========================================================================
     833   PERFORMANCE ET OPTIMISATIONS
     834   ========================================================================== */
     835
     836.abcs,
     837.block-editor-block-list__layout .abcs,
     838.editor-styles-wrapper .abcs {
     839    will-change: scroll-position;
     840    contain: layout style;
     841}
     842
     843.abcs>*,
     844.block-editor-block-list__layout .abcs>*,
     845.editor-styles-wrapper .abcs>* {
     846    will-change: transform;
     847}
     848
     849/* ==========================================================================
     850   ACCESSIBILITY
     851   ========================================================================== */
     852
     853@media (prefers-reduced-motion: reduce) {
     854
     855    .abcs,
     856    .block-editor-block-list__layout .abcs,
     857    .editor-styles-wrapper .abcs {
     858        scroll-behavior: auto;
     859    }
     860
     861    .abcs::scroll-button(*),
     862    *:has(>.abcs)::after {
     863        transition: none;
     864    }
     865
     866    .abcs>*::scroll-marker {
     867        transition: none;
     868    }
     869}
     870
     871@media (prefers-contrast: high) {
     872
     873    .abcs::scroll-button(*),
     874    *:has(>.abcs)::after {
     875        border-width: 3px;
     876    }
     877
     878    .abcs>*::scroll-marker {
     879        border: 2px solid currentColor;
     880    }
     881}
  • native-blocks-carousel/trunk/assets/js/carousel-editor.js

    r3395721 r3423082  
    88  const { Fragment, useEffect, useMemo, createElement, RawHTML } = wp.element;
    99  const { InspectorControls, BlockListBlock } = wp.blockEditor;
    10   const { PanelBody, ToggleControl, Tooltip, __experimentalToggleGroupControl: ToggleGroupControl, __experimentalToggleGroupControlOption: ToggleGroupControlOption, __experimentalToggleGroupControlOptionIcon: ToggleGroupControlOptionIcon } = wp.components;
     10  const { PanelBody, ToggleControl, RangeControl, Tooltip, __experimentalToggleGroupControl: ToggleGroupControl, __experimentalToggleGroupControlOption: ToggleGroupControlOption, __experimentalToggleGroupControlOptionIcon: ToggleGroupControlOptionIcon } = wp.components;
    1111  const { __ } = wp.i18n;
    1212
     
    198198          default: true,
    199199        },
     200        carouselLoop: {
     201          type: 'boolean',
     202          default: false,
     203        },
     204        carouselAutoplay: {
     205          type: 'boolean',
     206          default: false,
     207        },
     208        carouselAutoplayDelay: {
     209          type: 'number',
     210          default: 3000,
     211        },
    200212      },
    201213    };
     
    219231        carouselShowArrows = true,
    220232        carouselShowMarkers = true,
     233        carouselLoop = false,
     234        carouselAutoplay = false,
     235        carouselAutoplayDelay = 3000,
    221236      } = attributes;
    222237      const normalizedArrowStyle = wp.element.useMemo(
     
    738753              })
    739754              : null,
     755            carouselEnabled
     756              ? createElement(ToggleControl, {
     757                label: __('Enable loop', 'native-blocks-carousel'),
     758                checked: carouselLoop,
     759                __nextHasNoMarginBottom: true,
     760                onChange: (value) => {
     761                  setAttributes({ carouselLoop: value });
     762                },
     763                help: carouselLoop
     764                  ? __(
     765                    'The carousel will loop infinitely, returning to the first slide after the last.',
     766                    'native-blocks-carousel'
     767                  )
     768                  : __(
     769                    'Loop is disabled. The carousel stops at the end.',
     770                    'native-blocks-carousel'
     771                  ),
     772              })
     773              : null,
     774            carouselEnabled
     775              ? createElement(ToggleControl, {
     776                label: __('Enable autoplay', 'native-blocks-carousel'),
     777                checked: carouselAutoplay,
     778                __nextHasNoMarginBottom: true,
     779                onChange: (value) => {
     780                  setAttributes({ carouselAutoplay: value });
     781                },
     782                help: carouselAutoplay
     783                  ? __(
     784                    'The carousel will automatically scroll through slides.',
     785                    'native-blocks-carousel'
     786                  )
     787                  : __(
     788                    'Autoplay is disabled. Users must navigate manually.',
     789                    'native-blocks-carousel'
     790                  ),
     791              })
     792              : null,
     793            carouselEnabled && carouselAutoplay
     794              ? createElement(RangeControl, {
     795                label: __('Autoplay delay (ms)', 'native-blocks-carousel'),
     796                value: carouselAutoplayDelay,
     797                onChange: (value) => {
     798                  setAttributes({ carouselAutoplayDelay: value || 3000 });
     799                },
     800                min: 1000,
     801                max: 10000,
     802                step: 100,
     803                __nextHasNoMarginBottom: true,
     804                help: __(
     805                  'Time in milliseconds between each slide transition.',
     806                  'native-blocks-carousel'
     807                ),
     808              })
     809              : null,
    740810            carouselEnabled && carouselShowArrows
    741811              ? createElement(
     
    917987          },
    918988          'data-abcs-arrow-style': attributes.carouselArrowStyle || DEFAULT_ARROW_STYLE,
     989          'data-abcs-loop': attributes.carouselLoop ? 'true' : 'false',
     990          'data-abcs-autoplay': attributes.carouselAutoplay ? 'true' : 'false',
     991          'data-abcs-autoplay-delay': attributes.carouselAutoplayDelay || 3000,
    919992        },
    920993      };
     
    10411114
    10421115  /**
     1116   * Injects CSS variables into a <style> tag instead of inline style attribute.
     1117   * This is a better practice than using element.style.setProperty() on documentElement.
     1118   *
     1119   * @param {Object} variables - Object with CSS variable names as keys and values as values
     1120   * @param {Document|HTMLElement} docContext - Document context (for iframe support)
     1121   */
     1122  function injectCssVariablesInStyleTag(variables, docContext) {
     1123    const doc = docContext && docContext.ownerDocument ? docContext.ownerDocument : (docContext || document);
     1124    const head = doc.head || doc.getElementsByTagName('head')[0];
     1125
     1126    if (!head) return;
     1127
     1128    // Find or create the style tag for carousel variables
     1129    let styleTag = doc.getElementById('carousel-dynamic-variables');
     1130
     1131    if (!styleTag) {
     1132      styleTag = doc.createElement('style');
     1133      styleTag.id = 'carousel-dynamic-variables';
     1134      styleTag.type = 'text/css';
     1135      head.appendChild(styleTag);
     1136    }
     1137
     1138    // Build CSS with all variables
     1139    let css = ':root {';
     1140    for (const [key, value] of Object.entries(variables)) {
     1141      if (value !== null && value !== undefined && value !== '') {
     1142        css += `\n  ${key}: ${value};`;
     1143      }
     1144    }
     1145    css += '\n}';
     1146
     1147    styleTag.textContent = css;
     1148  }
     1149
     1150  /**
     1151   * Checks if a color value matches WordPress core default button colors.
     1152   * This helps avoid applying core defaults when theme has no custom button styles.
     1153   *
     1154   * @param {string} color - Color value to check (can be rgb, rgba, hex, etc.)
     1155   * @returns {boolean} True if the color matches core defaults
     1156   */
     1157  function isWordPressCoreDefaultColor(color) {
     1158    if (!color) return false;
     1159
     1160    // WordPress core default button colors
     1161    // Background: rgb(50, 55, 60) = #32373c
     1162    // Text: rgb(255, 255, 255) = #fff
     1163    const coreDefaults = [
     1164      'rgb(50, 55, 60)',
     1165      'rgba(50, 55, 60, 1)',
     1166      '#32373c',
     1167      'rgb(255, 255, 255)',
     1168      'rgba(255, 255, 255, 1)',
     1169      '#ffffff',
     1170      '#fff'
     1171    ];
     1172
     1173    // Normalize the color for comparison
     1174    const normalizedColor = color.toLowerCase().trim();
     1175
     1176    return coreDefaults.some(defaultColor => {
     1177      const normalizedDefault = defaultColor.toLowerCase().trim();
     1178      return normalizedColor === normalizedDefault || normalizedColor.includes(normalizedDefault);
     1179    });
     1180  }
     1181
     1182  /**
    10431183   * Dynamically updates button colors based on computed styles.
    10441184   * Mirrors WordPress behaviour that reads computed styles directly.
     1185   * Only applies colors if they come from theme customizations, not WordPress core defaults.
    10451186   */
    10461187  function updateButtonColorsFromTheme() {
    10471188    const root = document.documentElement;
    10481189    let buttonBg = '';
    1049     let buttonColor = '#fff';
     1190    let buttonColor = '';
    10501191
    10511192    // Main method: read from a real WordPress button inside the editor
     
    10741215      const computedColor = buttonComputedStyle.color;
    10751216
    1076       // Use computed colors when they are valid
    1077       if (computedBg && computedBg !== 'rgba(0, 0, 0, 0)' && computedBg !== 'transparent') {
     1217      // Use computed colors when they are valid AND not WordPress core defaults
     1218      if (computedBg && computedBg !== 'rgba(0, 0, 0, 0)' && computedBg !== 'transparent' && !isWordPressCoreDefaultColor(computedBg)) {
    10781219        buttonBg = computedBg;
    10791220      }
    10801221
    1081       if (computedColor && computedColor !== 'rgba(0, 0, 0, 0)') {
     1222      if (computedColor && computedColor !== 'rgba(0, 0, 0, 0)' && !isWordPressCoreDefaultColor(computedColor)) {
    10821223        buttonColor = computedColor;
    10831224      }
     
    10951236        const computedColor = buttonComputedStyle.color;
    10961237
    1097         if (computedBg && computedBg !== 'rgba(0, 0, 0, 0)' && computedBg !== 'transparent') {
     1238        // Use computed colors when they are valid AND not WordPress core defaults
     1239        if (computedBg && computedBg !== 'rgba(0, 0, 0, 0)' && computedBg !== 'transparent' && !isWordPressCoreDefaultColor(computedBg)) {
    10981240          buttonBg = computedBg;
    10991241        }
    11001242
    1101         if (computedColor && computedColor !== 'rgba(0, 0, 0, 0)') {
     1243        if (computedColor && computedColor !== 'rgba(0, 0, 0, 0)' && !isWordPressCoreDefaultColor(computedColor)) {
    11021244          buttonColor = computedColor;
    11031245        }
     
    11061248
    11071249    // Apply the retrieved colors to the carousel CSS variables
     1250    // Use style tag instead of inline style attribute (better practice)
     1251    const docContext = root.ownerDocument || document;
     1252    const variables = {};
     1253
    11081254    if (buttonBg && buttonBg !== 'rgba(0, 0, 0, 0)' && buttonBg !== 'transparent' && buttonBg !== '') {
    1109       root.style.setProperty('--carousel-button-bg', buttonBg);
    1110     }
    1111 
    1112     const docContext = root.ownerDocument || document;
     1255      variables['--carousel-button-bg'] = buttonBg;
     1256    }
    11131257
    11141258    if (buttonColor && buttonColor !== 'rgba(0, 0, 0, 0)' && buttonColor !== '') {
    1115       root.style.setProperty('--carousel-button-color', buttonColor);
     1259      variables['--carousel-button-color'] = buttonColor;
    11161260
    11171261      // Generate arrow SVGs using the button text color
     
    11201264      const defaultRightArrow = generateArrowSvg('right', arrowColor, DEFAULT_ARROW_STYLE);
    11211265
    1122       root.style.setProperty('--carousel-button-arrow-left', `url("${defaultLeftArrow}")`);
    1123       root.style.setProperty('--carousel-button-arrow-right', `url("${defaultRightArrow}")`);
     1266      variables['--carousel-button-arrow-left'] = `url("${defaultLeftArrow}")`;
     1267      variables['--carousel-button-arrow-right'] = `url("${defaultRightArrow}")`;
     1268    }
     1269
     1270    // Inject all variables at once in a style tag
     1271    if (Object.keys(variables).length > 0) {
     1272      injectCssVariablesInStyleTag(variables, docContext);
    11241273    }
    11251274
     
    14091558
    14101559      // Check whether the colors changed
     1560      // Also check that colors are not WordPress core defaults
    14111561      const bgChanged = currentBg !== lastButtonBg &&
    14121562        currentBg !== 'rgba(0, 0, 0, 0)' &&
    14131563        currentBg !== 'transparent' &&
    1414         currentBg !== '';
     1564        currentBg !== '' &&
     1565        !isWordPressCoreDefaultColor(currentBg);
    14151566      const colorChanged = currentColor !== lastButtonColor &&
    14161567        currentColor !== 'rgba(0, 0, 0, 0)' &&
    1417         currentColor !== '';
     1568        currentColor !== '' &&
     1569        !isWordPressCoreDefaultColor(currentColor);
    14181570
    14191571      // Update if a change is detected
     
    14231575
    14241576        // Apply colors inside the appropriate document (iframe or main page)
    1425         if (currentBg && currentBg !== 'rgba(0, 0, 0, 0)' && currentBg !== 'transparent') {
    1426           root.style.setProperty('--carousel-button-bg', currentBg);
    1427         }
    1428 
    1429         if (currentColor && currentColor !== 'rgba(0, 0, 0, 0)') {
    1430           root.style.setProperty('--carousel-button-color', currentColor);
     1577        // Use style tag instead of inline style attribute (better practice)
     1578        const variables = {};
     1579
     1580        if (currentBg && currentBg !== 'rgba(0, 0, 0, 0)' && currentBg !== 'transparent' && !isWordPressCoreDefaultColor(currentBg)) {
     1581          variables['--carousel-button-bg'] = currentBg;
     1582        }
     1583
     1584        if (currentColor && currentColor !== 'rgba(0, 0, 0, 0)' && !isWordPressCoreDefaultColor(currentColor)) {
     1585          variables['--carousel-button-color'] = currentColor;
    14311586
    14321587          // Generate arrow SVGs using the button text color
     
    14351590          const rightArrowSvg = generateArrowSvg('right', arrowColor, DEFAULT_ARROW_STYLE);
    14361591
    1437           root.style.setProperty('--carousel-button-arrow-left', `url("${leftArrowSvg}")`);
    1438           root.style.setProperty('--carousel-button-arrow-right', `url("${rightArrowSvg}")`);
     1592          variables['--carousel-button-arrow-left'] = `url("${leftArrowSvg}")`;
     1593          variables['--carousel-button-arrow-right'] = `url("${rightArrowSvg}")`;
     1594        }
     1595
     1596        // Inject all variables at once in a style tag
     1597        if (Object.keys(variables).length > 0) {
     1598          injectCssVariablesInStyleTag(variables, doc);
    14391599        }
    14401600
     
    15891749                        const color = computedStyle.color;
    15901750
    1591                         if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
    1592                           document.documentElement.style.setProperty('--carousel-button-bg', bg);
     1751                        // Only apply colors if they are not WordPress core defaults
     1752                        // Use style tag instead of inline style attribute (better practice)
     1753                        const variables = {};
     1754
     1755                        if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent' && !isWordPressCoreDefaultColor(bg)) {
     1756                          variables['--carousel-button-bg'] = bg;
    15931757                        }
    1594                         if (color && color !== 'rgba(0, 0, 0, 0)') {
    1595                           document.documentElement.style.setProperty('--carousel-button-color', color);
     1758                        if (color && color !== 'rgba(0, 0, 0, 0)' && !isWordPressCoreDefaultColor(color)) {
     1759                          variables['--carousel-button-color'] = color;
     1760
     1761                          // Generate arrow SVGs using the button text color
     1762                          const arrowColor = convertColorToHexForSvg(color, document);
     1763                          const leftArrowSvg = generateArrowSvg('left', arrowColor, DEFAULT_ARROW_STYLE);
     1764                          const rightArrowSvg = generateArrowSvg('right', arrowColor, DEFAULT_ARROW_STYLE);
     1765
     1766                          variables['--carousel-button-arrow-left'] = `url("${leftArrowSvg}")`;
     1767                          variables['--carousel-button-arrow-right'] = `url("${rightArrowSvg}")`;
    15961768
    15971769                          applyArrowIconsToCarousels(color, document);
     1770                        }
     1771
     1772                        // Inject all variables at once in a style tag
     1773                        if (Object.keys(variables).length > 0) {
     1774                          injectCssVariablesInStyleTag(variables, document);
    15981775                        }
    15991776                      }
  • native-blocks-carousel/trunk/assets/js/carousel-frontend-init.js

    r3395126 r3423082  
    44 *
    55 * @package AnyBlockCarouselSlider
    6  * @version 1.0.2
     6 * @version 1.0.4
    77 * @author weblazer35
    88 */
     
    441441            carousel.style.setProperty('--carousel-button-arrow-left', 'url("' + leftArrowSvg + '")');
    442442            carousel.style.setProperty('--carousel-button-arrow-right', 'url("' + rightArrowSvg + '")');
    443         if (carousel.dataset) {
    444             carousel.dataset.abcsCarouselArrowStyle = styleKey;
    445             carousel.dataset.abcsArrowStyle = styleKey;
    446         }
     443            if (carousel.dataset) {
     444                carousel.dataset.abcsCarouselArrowStyle = styleKey;
     445                carousel.dataset.abcsArrowStyle = styleKey;
     446            }
    447447
    448448            if (parent) {
    449             if (parent.dataset) {
    450                 parent.dataset.abcsCarouselArrowStyle = styleKey;
    451                 parent.dataset.abcsArrowStyle = styleKey;
    452             }
     449                if (parent.dataset) {
     450                    parent.dataset.abcsCarouselArrowStyle = styleKey;
     451                    parent.dataset.abcsArrowStyle = styleKey;
     452                }
    453453                parent.style.setProperty('--carousel-button-arrow-left', 'url("' + leftArrowSvg + '")');
    454454                parent.style.setProperty('--carousel-button-arrow-right', 'url("' + rightArrowSvg + '")');
    455455            }
    456456        });
     457    }
     458
     459    /**
     460     * Injects CSS variables into a <style> tag instead of inline style attribute.
     461     * This is a better practice than using element.style.setProperty() on documentElement.
     462     *
     463     * @param {Object} variables - Object with CSS variable names as keys and values as values
     464     */
     465    function injectCssVariablesInStyleTag(variables) {
     466        const head = document.head || document.getElementsByTagName('head')[0];
     467
     468        if (!head) return;
     469
     470        // Find or create the style tag for carousel variables
     471        let styleTag = document.getElementById('carousel-dynamic-variables');
     472
     473        if (!styleTag) {
     474            styleTag = document.createElement('style');
     475            styleTag.id = 'carousel-dynamic-variables';
     476            styleTag.type = 'text/css';
     477            head.appendChild(styleTag);
     478        }
     479
     480        // Build CSS with all variables
     481        let css = ':root {';
     482        for (const [key, value] of Object.entries(variables)) {
     483            if (value !== null && value !== undefined && value !== '') {
     484                css += '\n  ' + key + ': ' + value + ';';
     485            }
     486        }
     487        css += '\n}';
     488
     489        styleTag.textContent = css;
    457490    }
    458491
     
    488521            const defaultRightArrow = generateArrowSvg('right', arrowColor, DEFAULT_ARROW_STYLE);
    489522
    490             // Inject CSS variables on :root
    491             root.style.setProperty('--carousel-button-arrow-left', 'url("' + defaultLeftArrow + '")');
    492             root.style.setProperty('--carousel-button-arrow-right', 'url("' + defaultRightArrow + '")');
     523            // Inject CSS variables in a style tag instead of inline style attribute
     524            const variables = {
     525                '--carousel-button-arrow-left': 'url("' + defaultLeftArrow + '")',
     526                '--carousel-button-arrow-right': 'url("' + defaultRightArrow + '")'
     527            };
     528            injectCssVariablesInStyleTag(variables);
    493529        }
    494530
    495531        // Do not force the style on each carousel here; they are normalised later
     532    }
     533
     534    /**
     535     * Initializes autoplay for carousels with autoplay enabled.
     536     * Handles automatic scrolling, pause on hover/interaction, and stop at end.
     537     */
     538    // Store intervals and state outside function to persist across calls
     539    const autoplayIntervals = new WeakMap();
     540    const autoplayPaused = new WeakMap();
     541    const interactionTimeout = new WeakMap();
     542    const autoplayInitialized = new WeakSet();
     543    const loopResetSetup = new WeakSet();
     544    const isAutoScrollingMap = new WeakMap(); // Track autoplay scrolling state
     545
     546    /**
     547     * Setup loop functionality: keep buttons visible and handle reset when clicking Next at the end
     548     * Simple approach: listen for clicks and check if we're at the end, then reset
     549     */
     550    function setupLoopReset(carousel) {
     551        // Skip if already setup
     552        if (loopResetSetup.has(carousel)) {
     553            return;
     554        }
     555
     556        const isLoopEnabled = carousel.getAttribute('data-abcs-loop') === 'true';
     557        if (!isLoopEnabled) {
     558            return;
     559        }
     560
     561        // Skip if carousel has no children
     562        if (!carousel.firstElementChild) {
     563            return;
     564        }
     565
     566        let isResetting = false; // Flag to prevent multiple resets
     567
     568        // Get autoplay delay for this carousel (if autoplay is enabled)
     569        const autoplayDelay = parseInt(carousel.getAttribute('data-abcs-autoplay-delay'), 10) || 3000;
     570
     571        // Function to check if we're at the end
     572        function isAtEnd() {
     573            const threshold = 5;
     574            return carousel.scrollLeft + carousel.offsetWidth >= carousel.scrollWidth - threshold;
     575        }
     576
     577        // Function to check if we're at the start
     578        function isAtStart() {
     579            const threshold = 5;
     580            return carousel.scrollLeft <= threshold;
     581        }
     582
     583        // Function to reset to start
     584        function resetToStart() {
     585            if (isResetting) {
     586                return;
     587            }
     588            isResetting = true;
     589            carousel.style.scrollBehavior = 'auto';
     590            carousel.scrollTo({
     591                left: 0,
     592                behavior: 'auto'
     593            });
     594            carousel.style.scrollBehavior = '';
     595            setTimeout(function () {
     596                isResetting = false;
     597            }, 100);
     598        }
     599
     600        // Function to reset to end
     601        function resetToEnd() {
     602            if (isResetting) {
     603                return;
     604            }
     605            isResetting = true;
     606            carousel.style.scrollBehavior = 'auto';
     607            carousel.scrollTo({
     608                left: carousel.scrollWidth,
     609                behavior: 'auto'
     610            });
     611            carousel.style.scrollBehavior = '';
     612            setTimeout(function () {
     613                isResetting = false;
     614            }, 100);
     615        }
     616
     617        // Track if we were already at boundaries BEFORE any interaction
     618        // This distinguishes "arriving at the end" from "already at the end"
     619        let wasAtEndBeforeInteraction = false;
     620        let wasAtStartBeforeInteraction = false;
     621        let clickTimeout = null;
     622        let scrollTimeout = null;
     623        let previousScrollLeft = carousel.scrollLeft;
     624
     625        // Update boundary flags on scroll (to track when we reach boundaries)
     626        function updateBoundaryFlags() {
     627            // Only update if we're not resetting
     628            if (!isResetting) {
     629                wasAtEndBeforeInteraction = isAtEnd();
     630                wasAtStartBeforeInteraction = isAtStart();
     631            }
     632        }
     633
     634        // Listen for clicks on the carousel (this will catch clicks on scroll buttons)
     635        function handleClick(e) {
     636            if (isResetting) {
     637                return;
     638            }
     639
     640            // Clear any pending click timeout
     641            if (clickTimeout) {
     642                clearTimeout(clickTimeout);
     643            }
     644
     645            // Store state BEFORE the click - this is crucial!
     646            // We use the flag that was set BEFORE this click, not the current state
     647            const wasAtEndBeforeClick = wasAtEndBeforeInteraction;
     648            const wasAtStartBeforeClick = wasAtStartBeforeInteraction;
     649
     650            // Only reset if we were ALREADY at the end BEFORE the click
     651            // This allows the first click to show the last slide, and the second click to reset
     652            if (wasAtEndBeforeClick) {
     653                clickTimeout = setTimeout(function () {
     654                    if (isResetting) {
     655                        return;
     656                    }
     657                    // If still at end after click, the button couldn't scroll - reset to start
     658                    if (isAtEnd()) {
     659                        resetToStart();
     660                    }
     661                }, 400); // Longer delay to let scroll-button try to scroll
     662            }
     663            // Only reset if we were ALREADY at the start BEFORE the click
     664            else if (wasAtStartBeforeClick) {
     665                clickTimeout = setTimeout(function () {
     666                    if (isResetting) {
     667                        return;
     668                    }
     669                    // If still at start after click, the button couldn't scroll - reset to end
     670                    if (isAtStart()) {
     671                        resetToEnd();
     672                    }
     673                }, 400);
     674            }
     675
     676            // After the click, update flags for next time (but don't reset now)
     677            // This ensures that if we just arrived at the end, the next click will trigger reset
     678            setTimeout(function () {
     679                updateBoundaryFlags();
     680            }, 500); // Wait for scroll to complete before updating flags
     681        }
     682
     683        // Handle scroll events - only reset if we were ALREADY at the end before scrolling
     684        function handleScroll() {
     685            if (isResetting) {
     686                previousScrollLeft = carousel.scrollLeft;
     687                return;
     688            }
     689
     690            const currentScrollLeft = carousel.scrollLeft;
     691            const isScrollingForward = currentScrollLeft > previousScrollLeft;
     692            const isScrollingBackward = currentScrollLeft < previousScrollLeft;
     693            const scrollDelta = Math.abs(currentScrollLeft - previousScrollLeft);
     694
     695            // Update boundary flags as we scroll
     696            updateBoundaryFlags();
     697
     698            previousScrollLeft = currentScrollLeft;
     699
     700            // Clear any pending scroll timeout
     701            if (scrollTimeout) {
     702                clearTimeout(scrollTimeout);
     703            }
     704
     705            // Check if this is autoplay scrolling
     706            const isAutoplayActive = isAutoScrollingMap.get(carousel);
     707
     708            // Ignore tiny scrolls (scroll-snap adjustments) - they're less than 10px
     709            // BUT allow autoplay scrolls even if tiny (autoplay is programmatic, not scroll-snap)
     710            const isSignificantScroll = scrollDelta > 10 || isAutoplayActive;
     711
     712            // Only reset if we were ALREADY at the end BEFORE this scroll AND still at end
     713            // AND it's a significant scroll (not just scroll-snap micro-adjustments)
     714            // OR if it's autoplay (which can have small scrolls)
     715            // This prevents reset when scrolling normally towards the end or from scroll-snap
     716            if (wasAtEndBeforeInteraction && isAtEnd() && isScrollingForward && isSignificantScroll) {
     717                scrollTimeout = setTimeout(function () {
     718                    if (isAtEnd() && !isResetting && wasAtEndBeforeInteraction) {
     719                        // We were already at the end and tried to scroll forward - reset to start
     720                        // If autoplay is active, add delay before reset equal to autoplay delay
     721                        if (isAutoplayActive) {
     722                            setTimeout(function () {
     723                                if (!isResetting) {
     724                                    resetToStart();
     725                                }
     726                            }, autoplayDelay);
     727                        } else {
     728                            resetToStart();
     729                        }
     730                    }
     731                }, 200);
     732            }
     733            // Only reset if we were ALREADY at the start BEFORE this scroll AND still at start
     734            else if (wasAtStartBeforeInteraction && isAtStart() && isScrollingBackward && isSignificantScroll) {
     735                scrollTimeout = setTimeout(function () {
     736                    if (isAtStart() && !isResetting && wasAtStartBeforeInteraction) {
     737                        // We were already at the start and tried to scroll backward - reset to end
     738                        resetToEnd();
     739                    }
     740                }, 200);
     741            }
     742        }
     743
     744        // Initialize boundary flags
     745        updateBoundaryFlags();
     746
     747        // Listen for clicks and scroll events
     748        carousel.addEventListener('click', handleClick);
     749        carousel.addEventListener('scroll', handleScroll, { passive: true });
     750
     751        // Store handler references for cleanup
     752        carousel._abcsLoopClickHandler = handleClick;
     753        carousel._abcsLoopScrollHandler = handleScroll;
     754
     755        // Store cleanup function
     756        carousel._abcsLoopCleanup = function () {
     757            if (carousel._abcsLoopClickHandler) {
     758                carousel.removeEventListener('click', carousel._abcsLoopClickHandler);
     759            }
     760            if (carousel._abcsLoopScrollHandler) {
     761                carousel.removeEventListener('scroll', carousel._abcsLoopScrollHandler);
     762            }
     763            if (clickTimeout) {
     764                clearTimeout(clickTimeout);
     765            }
     766            if (scrollTimeout) {
     767                clearTimeout(scrollTimeout);
     768            }
     769            loopResetSetup.delete(carousel);
     770        };
     771
     772        loopResetSetup.add(carousel);
     773    }
     774
     775    function initAutoplay() {
     776        const carousels = document.querySelectorAll('.abcs[data-abcs-autoplay="true"]');
     777
     778        carousels.forEach(function (carousel) {
     779            // Skip if already initialized
     780            if (autoplayInitialized.has(carousel)) {
     781                return;
     782            }
     783
     784            // Skip if carousel has no children
     785            if (!carousel.firstElementChild) {
     786                return;
     787            }
     788
     789            const autoplayDelay = parseInt(carousel.getAttribute('data-abcs-autoplay-delay'), 10) || 3000;
     790            const isLoopEnabled = carousel.getAttribute('data-abcs-loop') === 'true';
     791
     792            // Setup loop reset if loop is enabled
     793            if (isLoopEnabled) {
     794                setupLoopReset(carousel);
     795            }
     796            let intervalId = null;
     797            let isPaused = false;
     798            let isHoverPaused = false;
     799            let interactionTimeoutId = null;
     800            let isAutoScrolling = false; // Flag to track if scroll is from autoplay
     801            const RESUME_DELAY = 2000; // Resume autoplay after 2 seconds of no interaction
     802
     803            // Calculate slide width including gap
     804            function getSlideWidth() {
     805                const firstChild = carousel.firstElementChild;
     806                if (!firstChild) {
     807                    return 0;
     808                }
     809                const computedStyle = window.getComputedStyle(carousel);
     810                const gap = computedStyle.getPropertyValue('gap') || computedStyle.getPropertyValue('--wp--style--block-gap') || '1rem';
     811                // Convert gap to pixels if it has a unit
     812                let gapValue = 0;
     813                if (gap && gap !== 'normal') {
     814                    // Try to parse as number (for px values)
     815                    const gapNum = parseFloat(gap);
     816                    if (!isNaN(gapNum)) {
     817                        // If gap contains 'rem' or 'em', convert to pixels
     818                        if (gap.includes('rem')) {
     819                            const rootFontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16;
     820                            gapValue = gapNum * rootFontSize;
     821                        } else if (gap.includes('em')) {
     822                            const parentFontSize = parseFloat(computedStyle.fontSize) || 16;
     823                            gapValue = gapNum * parentFontSize;
     824                        } else {
     825                            // Assume px or unitless
     826                            gapValue = gapNum;
     827                        }
     828                    }
     829                }
     830                return firstChild.offsetWidth + gapValue;
     831            }
     832
     833            // Check if carousel is at the end
     834            function isAtEnd() {
     835                // If loop is enabled, never consider it at the end
     836                if (isLoopEnabled) {
     837                    return false;
     838                }
     839                const threshold = 5; // Small threshold for rounding errors
     840                return carousel.scrollLeft + carousel.offsetWidth >= carousel.scrollWidth - threshold;
     841            }
     842
     843
     844            // Scroll to next slide
     845            function scrollToNext() {
     846                const slideWidth = getSlideWidth();
     847                if (slideWidth === 0) {
     848                    return;
     849                }
     850
     851                const currentScroll = carousel.scrollLeft;
     852                const nextScroll = currentScroll + slideWidth;
     853
     854                // If loop is disabled, check if we're at the end
     855                if (!isLoopEnabled) {
     856                    const threshold = 5;
     857                    const isAtEndNow = currentScroll + carousel.offsetWidth >= carousel.scrollWidth - threshold;
     858                    if (isAtEndNow) {
     859                        // Stop autoplay if at the end and loop is disabled
     860                        if (intervalId) {
     861                            clearInterval(intervalId);
     862                            intervalId = null;
     863                            autoplayIntervals.delete(carousel);
     864                        }
     865                        return;
     866                    }
     867                }
     868
     869                // With loop enabled, the reset handler will detect the end and jump to start
     870                // We just scroll normally - the handler manages the reset
     871                isAutoScrolling = true;
     872                isAutoScrollingMap.set(carousel, true);
     873                carousel.scrollTo({
     874                    left: nextScroll,
     875                    behavior: 'smooth'
     876                });
     877
     878                // Keep the flag active longer to allow handler to detect end during smooth scroll
     879                setTimeout(function () {
     880                    isAutoScrolling = false;
     881                    isAutoScrollingMap.set(carousel, false);
     882                }, 700); // Slightly longer to ensure smooth scroll completes
     883            }
     884
     885            // Start autoplay
     886            function startAutoplay() {
     887                // If loop is disabled, check if we're at the end
     888                if (intervalId || (!isLoopEnabled && isAtEnd())) {
     889                    return;
     890                }
     891
     892                // Start autoplay (loop reset is already set up if needed)
     893                intervalId = setInterval(function () {
     894                    if (!isPaused) {
     895                        scrollToNext();
     896                    }
     897                }, autoplayDelay);
     898                autoplayIntervals.set(carousel, intervalId);
     899            }
     900
     901            // Pause autoplay
     902            function pauseAutoplay() {
     903                isPaused = true;
     904                autoplayPaused.set(carousel, true);
     905            }
     906
     907            // Resume autoplay
     908            function resumeAutoplay() {
     909                // If loop is disabled and we're at the end, don't resume
     910                if (!isLoopEnabled && isAtEnd()) {
     911                    return;
     912                }
     913                isPaused = false;
     914                autoplayPaused.delete(carousel);
     915            }
     916
     917            // Handle interaction - pause and resume after delay
     918            function handleInteraction() {
     919                // Ignore scroll events from autoplay
     920                if (isAutoScrolling || isAutoScrollingMap.get(carousel)) {
     921                    return;
     922                }
     923
     924                pauseAutoplay();
     925
     926                // Clear existing timeout
     927                if (interactionTimeoutId) {
     928                    clearTimeout(interactionTimeoutId);
     929                }
     930
     931                // Resume after delay (only if not hover paused)
     932                interactionTimeoutId = setTimeout(function () {
     933                    if (!isHoverPaused) {
     934                        resumeAutoplay();
     935                    }
     936                    interactionTimeoutId = null;
     937                }, RESUME_DELAY);
     938
     939                interactionTimeout.set(carousel, interactionTimeoutId);
     940            }
     941
     942            // Event listeners for pause on hover
     943            const handleMouseEnter = function () {
     944                isHoverPaused = true;
     945                pauseAutoplay();
     946            };
     947            const handleMouseLeave = function () {
     948                isHoverPaused = false;
     949                // Resume if loop is enabled or if not at the end
     950                if (isLoopEnabled || !isAtEnd()) {
     951                    resumeAutoplay();
     952                }
     953            };
     954            carousel.addEventListener('mouseenter', handleMouseEnter);
     955            carousel.addEventListener('mouseleave', handleMouseLeave);
     956
     957            // Event listeners for pause on interaction
     958            carousel.addEventListener('scroll', handleInteraction, { passive: true });
     959            carousel.addEventListener('touchstart', handleInteraction, { passive: true });
     960            carousel.addEventListener('mousedown', handleInteraction);
     961
     962            // Pause when clicking on scroll buttons (handled via parent click events)
     963            // Note: ::scroll-button are pseudo-elements, so we listen on the carousel itself
     964
     965            // Start autoplay
     966            startAutoplay();
     967
     968            // Mark as initialized
     969            autoplayInitialized.add(carousel);
     970
     971            // Cleanup function (stored for potential future use)
     972            carousel._abcsAutoplayCleanup = function () {
     973                if (intervalId) {
     974                    clearInterval(intervalId);
     975                    intervalId = null;
     976                    autoplayIntervals.delete(carousel);
     977                }
     978                if (interactionTimeoutId) {
     979                    clearTimeout(interactionTimeoutId);
     980                    interactionTimeoutId = null;
     981                    interactionTimeout.delete(carousel);
     982                }
     983                carousel.removeEventListener('mouseenter', handleMouseEnter);
     984                carousel.removeEventListener('mouseleave', handleMouseLeave);
     985                carousel.removeEventListener('scroll', handleInteraction);
     986                carousel.removeEventListener('touchstart', handleInteraction);
     987                carousel.removeEventListener('mousedown', handleInteraction);
     988            };
     989        });
    496990    }
    497991
     
    504998        injectArrowSvgs();
    505999        applyArrowIconsToCarousels(null, document);
     1000
     1001        // Setup loop reset for all carousels with loop enabled
     1002        const loopCarousels = document.querySelectorAll('.abcs[data-abcs-loop="true"]');
     1003        loopCarousels.forEach(function (carousel) {
     1004            if (carousel.firstElementChild) {
     1005                setupLoopReset(carousel);
     1006            }
     1007        });
     1008
     1009        initAutoplay();
    5061010    }
    5071011
     
    5541058                    injectArrowSvgs();
    5551059                    applyArrowIconsToCarousels(null, document);
     1060                    initAutoplay();
    5561061                });
    5571062            });
    5581063        }
    5591064    });
     1065
     1066    // Handle dynamically added carousels with MutationObserver
     1067    function setupAutoplayObserver() {
     1068        // Only set up observer if MutationObserver is available
     1069        if (typeof MutationObserver === 'undefined') {
     1070            return;
     1071        }
     1072
     1073        const observer = new MutationObserver(function (mutations) {
     1074            let shouldInitAutoplay = false;
     1075            let shouldInitLoop = false;
     1076            mutations.forEach(function (mutation) {
     1077                mutation.addedNodes.forEach(function (node) {
     1078                    if (node.nodeType === 1) { // Element node
     1079                        // Check if the added node is a carousel
     1080                        if (node.classList && node.classList.contains('abcs')) {
     1081                            if (node.getAttribute('data-abcs-autoplay') === 'true') {
     1082                                shouldInitAutoplay = true;
     1083                            }
     1084                            if (node.getAttribute('data-abcs-loop') === 'true') {
     1085                                shouldInitLoop = true;
     1086                            }
     1087                        }
     1088                        // Check for carousels within the added node
     1089                        if (node.querySelectorAll) {
     1090                            const autoplayCarousels = node.querySelectorAll('.abcs[data-abcs-autoplay="true"]');
     1091                            if (autoplayCarousels.length > 0) {
     1092                                shouldInitAutoplay = true;
     1093                            }
     1094                            const loopCarousels = node.querySelectorAll('.abcs[data-abcs-loop="true"]');
     1095                            if (loopCarousels.length > 0) {
     1096                                shouldInitLoop = true;
     1097                            }
     1098                        }
     1099                    }
     1100                });
     1101            });
     1102
     1103            if (shouldInitAutoplay) {
     1104                // Use requestAnimationFrame to ensure DOM is ready
     1105                requestAnimationFrame(function () {
     1106                    initAutoplay();
     1107                });
     1108            }
     1109            if (shouldInitLoop) {
     1110                // Setup loop reset for new carousels
     1111                requestAnimationFrame(function () {
     1112                    const loopCarousels = document.querySelectorAll('.abcs[data-abcs-loop="true"]');
     1113                    loopCarousels.forEach(function (carousel) {
     1114                        if (carousel.firstElementChild && !loopResetSetup.has(carousel)) {
     1115                            setupLoopReset(carousel);
     1116                        }
     1117                    });
     1118                });
     1119            }
     1120        });
     1121
     1122        // Start observing the document body for added nodes
     1123        observer.observe(document.body, {
     1124            childList: true,
     1125            subtree: true
     1126        });
     1127    }
     1128
     1129    // Set up observer after initial load
     1130    if (document.readyState === 'loading') {
     1131        document.addEventListener('DOMContentLoaded', function () {
     1132            setupAutoplayObserver();
     1133        });
     1134    } else {
     1135        setupAutoplayObserver();
     1136    }
    5601137
    5611138    if (typeof window !== 'undefined') {
     
    5681145            applyArrowIconsToCarousels(color, context || document, normalizedConfig);
    5691146        };
     1147        window.abcsCarousel.initAutoplay = initAutoplay;
    5701148    }
    5711149
  • native-blocks-carousel/trunk/includes/Renderer.php

    r3395126 r3423082  
    5151        $this->maybeAddPaddingVariables($custom_styles, $block, $block_content);
    5252
    53         if (empty($custom_styles)) {
     53        // Get loop and autoplay attributes
     54        $loop = $block['attrs']['carouselLoop'] ?? false;
     55        $autoplay = $block['attrs']['carouselAutoplay'] ?? false;
     56        $autoplay_delay = $block['attrs']['carouselAutoplayDelay'] ?? 3000;
     57
     58        $has_styles = !empty($custom_styles);
     59        $has_loop_attrs = $loop;
     60        $has_autoplay_attrs = $autoplay;
     61
     62        // If no styles and no loop/autoplay attributes, return early
     63        if (!$has_styles && !$has_loop_attrs && !$has_autoplay_attrs) {
    5464            return $block_content;
    5565        }
    5666
    57         $styles_string = $this->buildStylesString($custom_styles);
     67        $styles_string = $has_styles ? $this->buildStylesString($custom_styles) : '';
     68
    5869        if (\class_exists('\\WP_HTML_Tag_Processor')) {
    5970            $processor = new \WP_HTML_Tag_Processor($block_content);
    6071
    6172            if ($processor->next_tag(['class_name' => 'abcs'])) {
    62                 $existing_style = $processor->get_attribute('style');
    63                 $processor->set_attribute('style', $this->mergeStyleAttribute($existing_style, $styles_string));
     73                if ($has_styles) {
     74                    $existing_style = $processor->get_attribute('style');
     75                    $processor->set_attribute('style', $this->mergeStyleAttribute($existing_style, $styles_string));
     76                }
     77
     78                // Add loop data attribute
     79                if ($has_loop_attrs) {
     80                    $processor->set_attribute('data-abcs-loop', $loop ? 'true' : 'false');
     81                }
     82
     83                // Add autoplay data attributes
     84                if ($has_autoplay_attrs) {
     85                    $processor->set_attribute('data-abcs-autoplay', $autoplay ? 'true' : 'false');
     86                    $processor->set_attribute('data-abcs-autoplay-delay', (string) $autoplay_delay);
     87                }
    6488
    6589                $modified_content = $processor->get_updated_html();
     
    7195        }
    7296
     97        // Fallback to regex if WP_HTML_Tag_Processor is not available
    7398        $pattern = '/(<(?:div|ul|figure)\s+[^>]*class="[^"]*\babcs\b[^"]*"[^>]*?)(?:\s+style="([^"]*)")?(\s*>)/i';
    7499
    75         $replacement = function (array $matches) use ($styles_string) {
     100        $replacement = function (array $matches) use ($styles_string, $loop, $autoplay, $autoplay_delay, $has_styles, $has_loop_attrs, $has_autoplay_attrs) {
    76101            $tag_start = $matches[1];
    77102            $existing_style = $matches[2] ?? '';
    78103            $tag_end = $matches[3];
    79104
    80             $new_style = $this->mergeStyleAttribute($existing_style, $styles_string);
    81 
    82             return $tag_start . ' style="' . $new_style . '"' . $tag_end;
     105            $result = $tag_start;
     106
     107            // Add style attribute if needed
     108            if ($has_styles) {
     109                $existing_style_trimmed = '' !== $existing_style ? \trim($existing_style) : '';
     110                if ('' !== $existing_style_trimmed && ';' !== \substr($existing_style_trimmed, -1)) {
     111                    $existing_style_trimmed .= ';';
     112                }
     113                $new_style = $existing_style_trimmed . $styles_string;
     114                $result .= ' style="' . \esc_attr($new_style) . '"';
     115            } elseif ($existing_style) {
     116                $result .= ' style="' . \esc_attr($existing_style) . '"';
     117            }
     118
     119            // Add loop data attribute
     120            if ($has_loop_attrs) {
     121                $result .= ' data-abcs-loop="' . \esc_attr($loop ? 'true' : 'false') . '"';
     122            }
     123
     124            // Add autoplay data attributes
     125            if ($has_autoplay_attrs) {
     126                $result .= ' data-abcs-autoplay="' . \esc_attr($autoplay ? 'true' : 'false') . '"';
     127                $result .= ' data-abcs-autoplay-delay="' . \esc_attr((string) $autoplay_delay) . '"';
     128            }
     129
     130            $result .= $tag_end;
     131
     132            return $result;
    83133        };
    84134
  • native-blocks-carousel/trunk/includes/ThemeStyles.php

    r3395126 r3423082  
    2525    public function injectButtonColors(): void
    2626    {
    27         $theme_json = \WP_Theme_JSON_Resolver::get_merged_data();
    28         $styles = $theme_json->get_stylesheet();
    29         $settings = $theme_json->get_data();
    30 
     27        // Step 1: Get merged data (includes core + theme + user styles)
     28        // This is needed to get user styles (custom styles from site-editor)
     29        $merged_data = \WP_Theme_JSON_Resolver::get_merged_data();
     30        $styles = $merged_data->get_stylesheet();
     31        $merged_settings = $merged_data->get_data();
     32       
     33        // Get theme data separately (to check if theme has custom styles)
     34        $theme_data = \WP_Theme_JSON_Resolver::get_theme_data();
     35        $theme_settings = $theme_data->get_data();
     36       
     37        // Get user data separately (to check if user has custom styles)
     38        $user_data = \WP_Theme_JSON_Resolver::get_user_data();
     39        $user_settings = $user_data->get_data();
     40
     41        // Initialize variables to store detected colors
    3142        $button_bg = '';
    3243        $button_color = '';
    3344
    34         if (isset($settings['styles']['elements']['button']['color']['background'])) {
    35             $button_bg = $settings['styles']['elements']['button']['color']['background'];
    36         }
    37 
    38         if (isset($settings['styles']['elements']['button']['color']['text'])) {
    39             $button_color = $settings['styles']['elements']['button']['color']['text'];
    40         }
    41 
    42         if (empty($button_bg) && \preg_match('/.wp-element-button[^{]*\{[^}]*background-color:\s*([^;]+)/s', $styles, $matches)) {
     45        // Step 2: Priority 1 - Check user styles (custom styles from site-editor)
     46        // User styles have the highest priority and should be used if they exist
     47        // Path: styles.elements.button.color.background
     48        if (isset($user_settings['styles']['elements']['button']['color']['background'])) {
     49            $button_bg = $user_settings['styles']['elements']['button']['color']['background'];
     50        }
     51
     52        if (isset($user_settings['styles']['elements']['button']['color']['text'])) {
     53            $button_color = $user_settings['styles']['elements']['button']['color']['text'];
     54        }
     55
     56        // Step 3: Priority 2 - Check theme styles (from theme.json)
     57        // Only use theme styles if user styles are not defined
     58        // Path: styles.elements.button.color.background
     59        if (empty($button_bg) && isset($theme_settings['styles']['elements']['button']['color']['background'])) {
     60            $button_bg = $theme_settings['styles']['elements']['button']['color']['background'];
     61        }
     62
     63        if (empty($button_color) && isset($theme_settings['styles']['elements']['button']['color']['text'])) {
     64            $button_color = $theme_settings['styles']['elements']['button']['color']['text'];
     65        }
     66
     67        // Step 4: Priority 3 - Fallback method - extract colors from compiled CSS
     68        // IMPORTANT: Only use this fallback if theme OR user has explicitly defined button styles
     69        // We check if theme or user has button element styles to avoid using WordPress core defaults
     70        // If neither theme nor user has button styles defined, we skip the fallback to avoid core defaults
     71        $has_custom_button_styles = (
     72            (isset($theme_settings['styles']['elements']['button']) && !empty($theme_settings['styles']['elements']['button']))
     73            || (isset($user_settings['styles']['elements']['button']) && !empty($user_settings['styles']['elements']['button']))
     74        );
     75       
     76        // This regex searches for .wp-element-button class and extracts background-color value
     77        // Pattern: .wp-element-button { ... background-color: VALUE; ... }
     78        // Only extract if theme or user has defined button styles (to avoid core defaults)
     79        if (empty($button_bg) && $has_custom_button_styles && \preg_match('/.wp-element-button[^{]*\{[^}]*background-color:\s*([^;]+)/s', $styles, $matches)) {
    4380            $button_bg = \trim($matches[1]);
    4481        }
    4582
    46         if (empty($button_color) && \preg_match('/.wp-element-button[^{]*\{[^}]*color:\s*([^;]+)/s', $styles, $matches)) {
     83        // Extract text color from compiled CSS if not found in theme.json or user styles
     84        // Pattern: .wp-element-button { ... color: VALUE; ... }
     85        // Only extract if theme or user has defined button styles
     86        if (empty($button_color) && $has_custom_button_styles && \preg_match('/.wp-element-button[^{]*\{[^}]*color:\s*([^;]+)/s', $styles, $matches)) {
    4787            $button_color = \trim($matches[1]);
    4888        }
    4989
     90        // Step 5: Resolve CSS variables to concrete values
     91        // If the color is a CSS variable (e.g., var(--wp--preset--color--primary)),
     92        // try to resolve it to its actual value (e.g., #007cba)
    5093        $button_bg = $this->resolveCssVariable($button_bg, $styles);
    5194        $button_color = $this->resolveCssVariable($button_color, $styles);
    5295
     96        // Step 5.5: Filter out WordPress core default colors
     97        // IMPORTANT: Only filter if colors don't come from user styles
     98        // If user has explicitly set colors in site-editor, we should respect them
     99        // even if they match core defaults (user might want to use core defaults intentionally)
     100        $colors_from_user = !empty($user_settings['styles']['elements']['button'] ?? []);
     101       
     102        if (!$colors_from_user) {
     103            // Only filter core defaults if colors don't come from user styles
     104            // This prevents using core defaults that might be stored in theme styles
     105            $button_bg = $this->filterCoreDefaultColors($button_bg);
     106            $button_color = $this->filterCoreDefaultColors($button_color);
     107        }
     108
     109        // Step 6: Build CSS custom properties block
     110        // Create :root selector to define global CSS variables
    53111        $custom_css = ':root {';
    54112
     113        // Add background color variable if found
     114        // esc_attr() sanitizes the value for safe output
    55115        if (!empty($button_bg)) {
    56116            $custom_css .= '--carousel-button-bg: ' . \esc_attr($button_bg) . ';';
    57117        }
    58118
     119        // Add text color variable if found
    59120        if (!empty($button_color)) {
    60121            $custom_css .= '--carousel-button-color: ' . \esc_attr($button_color) . ';';
     
    63124        $custom_css .= '}';
    64125
     126        // Step 7: Inject the CSS into WordPress stylesheet
     127        // Only inject if at least one color was successfully detected
     128        // wp_add_inline_style() appends CSS to the specified stylesheet handle
    65129        if (!empty($button_bg) || !empty($button_color)) {
    66130            \wp_add_inline_style('any-block-carousel-slider', $custom_css);
     
    78142    private function resolveCssVariable(string $value, string $styles): string
    79143    {
     144        // If value is empty or not a CSS variable, return as-is
     145        // CSS variables use the format: var(--variable-name)
    80146        if (empty($value) || false === \strpos($value, 'var(')) {
    81147            return $value;
    82148        }
    83149
     150        // Extract the variable name from var(--variable-name) format
     151        // Example: var(--wp--preset--color--primary) → --wp--preset--color--primary
    84152        if (\preg_match('/var\(([^)]+)\)/', $value, $var_match)) {
    85153            $var_name = \trim($var_match[1]);
     154           
     155            // Search for the variable definition in the compiled stylesheet
     156            // Pattern: --variable-name: VALUE;
     157            // preg_quote() escapes special regex characters in the variable name
    86158            if (\preg_match('/' . \preg_quote($var_name, '/') . ':\s*([^;]+)/s', $styles, $color_match)) {
     159                // Return the resolved concrete value (e.g., #007cba instead of var(--wp--preset--color--primary))
    87160                return \trim($color_match[1]);
    88161            }
    89162        }
    90163
     164        // If variable couldn't be resolved, return original value
     165        // This allows CSS to handle the variable at runtime
    91166        return $value;
    92167    }
     168
     169    /**
     170     * Filters out WordPress core default button colors.
     171     *
     172     * Even if colors are detected from theme or user styles, ignore them
     173     * if they match WordPress core defaults. This prevents using core defaults
     174     * that might be stored in user global styles.
     175     *
     176     * @param string $color Color value to check (can be rgb, rgba, hex, etc.)
     177     * @return string Empty string if color matches core defaults, original value otherwise
     178     */
     179    private function filterCoreDefaultColors(string $color): string
     180    {
     181        if (empty($color)) {
     182            return $color;
     183        }
     184
     185        // WordPress core default button colors
     186        // Background: rgb(50, 55, 60) = #32373c
     187        // Text: rgb(255, 255, 255) = #fff = #ffffff
     188        $core_defaults = [
     189            'rgb(50, 55, 60)',
     190            'rgba(50, 55, 60, 1)',
     191            'rgba(50, 55, 60,1)',
     192            '#32373c',
     193            'rgb(255, 255, 255)',
     194            'rgba(255, 255, 255, 1)',
     195            'rgba(255, 255, 255,1)',
     196            '#ffffff',
     197            '#fff',
     198        ];
     199
     200        // Normalize the color for comparison
     201        $normalized_color = \strtolower(\trim($color));
     202
     203        // Check if color matches any core default
     204        foreach ($core_defaults as $default) {
     205            $normalized_default = \strtolower(\trim($default));
     206            if ($normalized_color === $normalized_default) {
     207                // Return empty string to indicate this is a core default and should be ignored
     208                return '';
     209            }
     210        }
     211
     212        // Also check for rgb/rgba values that might have different formatting
     213        // e.g., "rgb(50,55,60)" or "rgb( 50 , 55 , 60 )"
     214        if (\preg_match('/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i', $color, $matches)) {
     215            $r = (int) $matches[1];
     216            $g = (int) $matches[2];
     217            $b = (int) $matches[3];
     218
     219            // Check if it matches core background default
     220            if ($r === 50 && $g === 55 && $b === 60) {
     221                return '';
     222            }
     223
     224            // Check if it matches core text default
     225            if ($r === 255 && $g === 255 && $b === 255) {
     226                return '';
     227            }
     228        }
     229
     230        return $color;
     231    }
    93232}
  • native-blocks-carousel/trunk/readme.txt

    r3421234 r3423082  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.0.3.6
     7Stable tag: 1.0.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    4545
    4646* **100% CSS** – Smooth carousel slider with `scroll-snap`, `::scroll-button`, and `::scroll-marker`. No script to bundle.
     47* **Loop functionality** – Enable infinite scrolling with seamless reset to start/end when reaching boundaries.
     48* **Autoplay support** – Automatic slide progression with configurable delay and pause on hover/interaction.
    4749* **Smart responsive** – Automatically handles visible columns, spacing, and control sizes according to WordPress breakpoints (1280, 1024, 782, 600, 480, 375).
    4850* **Two width modes** – Manual mode (fixed column count) and Auto mode (fixed width like 320px) with automatic detection.
     
    6163= Advanced customization =
    6264
     65* **Loop mode** – Enable infinite scrolling: when reaching the end, the carousel seamlessly resets to the beginning (and vice versa). Navigation buttons remain active at all times.
     66* **Autoplay** – Automatic slide progression with configurable delay (default: 3000ms). Autoplay pauses on hover and user interaction, and stops at the end when loop is disabled.
    6367* **Manual mode (fixed columns)** – Ideal for article carousel sliders: 1 to 6 columns depending on screen sizes.
    6468* **Auto mode (fixed width)** – Perfect for card-based sliders (posts, testimonials, product highlights) with pixel-perfect widths like 280px, 320px, or 360px.
     
    178182* 🔗 Simplified WordPress Playground link in "Try it now" section.
    179183* 📝 Updated readme.txt to refresh WordPress.org cache.
     184
     185= 1.0.4 - 2025-01-XX =
     186* ✨ Added Loop functionality: infinite carousel scrolling with seamless reset to start/end.
     187* ✨ Added Autoplay functionality: automatic slide progression with configurable delay.
     188* 🎯 Loop keeps navigation buttons visible even at carousel boundaries.
     189* ⏱️ Autoplay respects configured delay before resetting when loop is enabled.
     190* 🎨 Improved scroll detection to ignore scroll-snap micro-adjustments.
     191* 🐛 Fixed premature reset triggers when scrolling towards carousel end.
     192* 🛠️ Enhanced boundary detection for better loop and autoplay behavior.
    180193
    181194= 1.0.3.2 - 2025-11-13 =
     
    221234== Upgrade Notice ==
    222235
     236= 1.0.4 =
     237Recommended update: adds Loop and Autoplay features for enhanced carousel functionality. Loop enables infinite scrolling, and Autoplay provides automatic slide progression with configurable timing.
     238
    223239= 1.0.3 =
    224240Recommended update: fixes dynamic arrow style updates in the editor and improves compatibility with Site Editor iframes.
Note: See TracChangeset for help on using the changeset viewer.