Commit f1546bf6 authored by Jan Koester's avatar Jan Koester
Browse files

new slider

parent 6584bee3
Loading
Loading
Loading
Loading
+69 −19
Original line number Diff line number Diff line
@@ -46,7 +46,7 @@ extern "C" {
            std::string height          = "400px";
            std::string backgroundColor = "";
            std::string borderRadius    = "0";
            std::string transition      = "fade";    // fade, slide, carousel
            std::string transition      = "fade";    // fade, slide, carousel, carousel_multi
            std::string itemsPerView    = "1";       // slide mode only: slides visible side-by-side
            std::string m_itemsPerView  = "";        // mobile override; empty = same as itemsPerView
            std::string interval        = "5000";    // ms between slides
@@ -183,8 +183,8 @@ extern "C" {
                addField("background_gradient_angle", "Gradient Angle", "text", "e.g. 135deg", "desktop");
                addField("background_gradient_custom", "Gradient Custom", "text", "e.g. linear-gradient(135deg, #ff0000, #0000ff)", "desktop");
                addField("border_radius", "Border Radius", "text", "e.g. 8px");
                addField("transition", "Transition", "select", nullptr, "all", mkOpts({"fade","slide","carousel"}));
                addField("items_per_view", "Items Per View (Slide mode only)", "select", nullptr, "desktop",
                addField("transition", "Transition", "select", nullptr, "all", mkOpts({"fade","slide","carousel","carousel_multi"}));
                addField("items_per_view", "Items Per View (Slide / Karussell mehrfach)", "select", nullptr, "desktop",
                         mkOpts({"1","2","3","4","5","6"}), "simple");
                addField("interval", "Interval (ms)", "text", "e.g. 5000");
                addField("autoplay", "Autoplay", "checkbox", nullptr, "all", nullptr, "simple");
@@ -198,7 +198,7 @@ extern "C" {
                addField("m_background_gradient_custom", "Mobile Gradient Custom", "text", "e.g. linear-gradient(135deg, #ff0000, #0000ff)", "mobile");
                addField("m_width", "Mobile Width", "text", "e.g. 100%", "mobile");
                addField("m_height", "Mobile Height", "text", "e.g. 400px", "mobile");
                addField("m_items_per_view", "Items Per View (Mobile, Slide mode only)", "select", nullptr, "mobile",
                addField("m_items_per_view", "Items Per View (Mobile, Slide / Karussell mehrfach)", "select", nullptr, "mobile",
                         mkOpts({"1","2","3"}), "simple");
                addField("hidden", "Hidden", "checkbox", nullptr, "desktop", nullptr, "simple");
                {
@@ -559,7 +559,7 @@ extern "C" {
                    counter = counter->nextElement();
                }

                // Items Per View -- only the plain "slide" transition supports
                // Items Per View -- "slide" and "carousel_multi" both support
                // multiple side-by-side items: "fade" stacks every slide in one
                // grid cell and "carousel" always centers exactly one active
                // slide with peeking neighbors, neither of which has a
@@ -572,16 +572,26 @@ extern "C" {
                    try { mobileItemsPerViewN = std::stoi(m_itemsPerView); } catch (...) { mobileItemsPerViewN = itemsPerViewN; }
                    if (mobileItemsPerViewN < 1) mobileItemsPerViewN = 1;
                }
                bool multiItem = (transition != "fade" && transition != "carousel" && itemsPerViewN > 1);
                // Dots/prev/next always page by whole groups of itemsPerViewN
                bool multiItem = (transition == "slide" || transition == "carousel_multi") && itemsPerViewN > 1;
                // "carousel_multi" steps by a single slide at a time instead of
                // by whole pages, and loops seamlessly (no jump-back-to-start)
                // by rendering the slide set three times (prev/real/next) and
                // silently resetting the scroll position once the transition
                // carries it into a clone block that looks pixel-identical to
                // the real one. Only worth doing once there are more slides
                // than fit in one view -- otherwise there's nothing to loop.
                bool loopMulti   = (transition == "carousel_multi");
                bool loopEnabled = loopMulti && slideCount > itemsPerViewN;
                // Dots/prev/next normally page by whole groups of itemsPerViewN
                // slides (the desktop setting) -- the mobile override only
                // changes how many of those slides are visually shown per
                // page, not the paging granularity itself, since the page
                // count is baked into the server-rendered dots and can't
                // change at the mobile breakpoint.
                int pageCount = multiItem
                    ? (slideCount + itemsPerViewN - 1) / itemsPerViewN
                    : slideCount;
                // change at the mobile breakpoint. The seamless loop instead
                // pages one real slide at a time, so it gets one dot per slide.
                int pageCount = loopEnabled
                    ? slideCount
                    : (multiItem ? (slideCount + itemsPerViewN - 1) / itemsPerViewN : slideCount);
                if (pageCount < 1) pageCount = 1;

                auto gradientCss = [](const std::string &type, const std::string &angle, const std::string &c1, const std::string &c2) -> std::string {
@@ -637,7 +647,18 @@ extern "C" {
                track.setAttribute("class", trackCls.c_str());
                track.setAttribute("style", trackStyle.c_str());

                // Render each child as a slide
                // Render each child as a slide. For the seamless-loop
                // carousel_multi mode, the whole slide set is rendered three
                // times in a row (prev-clone / real / next-clone) so the JS
                // can keep translating the track continuously across the
                // boundary and silently reset back into the middle (real)
                // copy once it lands on a clone that looks pixel-identical --
                // see the loopEnabled JS branch below. Note this duplicates
                // whatever the child widgets render, id attributes included,
                // so it's only intended for simple, non-interactive slide
                // content like images.
                int cloneRounds = loopEnabled ? 3 : 1;
                for (int round = 0; round < cloneRounds; round++) {
                    const EditPlugin *cur = child;
                    while (cur) {
                        libhtmlpp::HtmlElement slide("div");
@@ -669,6 +690,7 @@ extern "C" {
                        track.appendChild(slide);
                        cur = cur->nextElement();
                    }
                }

                wrapper.appendChild(track);

@@ -742,17 +764,40 @@ extern "C" {
                        // Items Per View: each slide is a 1/perView fraction of the
                        // container, where perView switches between the desktop and
                        // mobile setting at the same 768px breakpoint the rest of
                        // this widget's responsive CSS uses. Paging always advances
                        // by whole pages of the DESKTOP perView count (see `units`
                        // above, computed server-side) -- translating the track by
                        // one full container width per page is correct regardless
                        // of perView, since perView slides of width w/perView
                        // together always span exactly w.
                        // this widget's responsive CSS uses. For the plain "slide"
                        // mode, paging advances by whole pages of the DESKTOP perView
                        // count (see `units` above, computed server-side) -- translating
                        // the track by one full container width per page is correct
                        // regardless of perView, since perView slides of width w/perView
                        // together always span exactly w. "carousel_multi" instead pages
                        // one slide at a time (see the loopEnabled branch below).
                        js << "var desktopPV=" << itemsPerViewN << ",mobilePV=" << mobileItemsPerViewN << ";";
                        js << "function currentPV(){return window.innerWidth<=768?mobilePV:desktopPV;}";
                        js << "function setSizes(){var w=el.offsetWidth,pv=currentPV(),sw=w/pv;for(var j=0;j<n;j++){slides[j].style.width=sw+'px';slides[j].style.flex='0 0 '+sw+'px';}}";
                    }
                    js << "setSizes();";
                    if (loopEnabled) {
                        // Seamless single-step infinite loop: the slide set was
                        // rendered three times (prev-clone/real/next-clone), so
                        // `slides.length` (n) is 3x the real slide count. `cur`
                        // starts in the middle (real) copy and simply keeps
                        // incrementing/decrementing with the transition running;
                        // once it settles into a clone block (which looks
                        // pixel-identical to the real block at the same relative
                        // offset) the transitionend handler snaps it back into the
                        // middle block with the transition switched off for that
                        // one frame, so the loop never visibly jumps.
                        js << "var realN=" << slideCount << ";cur=realN;";
                        js << "var track=el.querySelector('." << trackCls << "');";
                        js << "function applyTransform(){var pv=currentPV(),sw=el.offsetWidth/pv;if(track)track.style.transform='translateX(-'+(cur*sw)+'px)';}";
                        js << "function updateDots(){var real=((cur%realN)+realN)%realN;for(var j=0;j<dots.length;j++){dots[j].style.background=j===real?'white':'rgba(255,255,255,0.4)';}}";
                        js << "function show(i){cur=i;applyTransform();updateDots();}";
                        js << "function snapNow(){if(!track)return;track.style.transition='none';applyTransform();track.offsetHeight;track.style.transition='transform 0.5s ease-in-out';}";
                        js << "if(track)track.addEventListener('transitionend',function(){";
                        js << "  if(cur>=2*realN){cur-=realN;snapNow();}";
                        js << "  else if(cur<realN){cur+=realN;snapNow();}";
                        js << "});";
                    } else {
                        js << "function show(i){";
                        js << "  cur=((i%units)+units)%units;";
                        if (transition == "fade") {
@@ -770,16 +815,21 @@ extern "C" {
                        js << "    dots[j].style.background=j===cur?'white':'rgba(255,255,255,0.4)';";
                        js << "  }";
                        js << "}";
                    }
                    js << "function next(){show(cur+1);}";
                    js << "function prev(){show(cur-1);}";
                    if (loopEnabled) {
                        js << "function goTo(i){show(realN+((i%realN)+realN)%realN);}";
                    } else {
                        js << "function goTo(i){show(i);}";
                    }
                    js << "function stopAuto(){if(timer){clearInterval(timer);timer=null;}}";
                    js << "function startAuto(){";
                    js << "  if(timer)clearInterval(timer);";
                    js << "  timer=setInterval(next," << interval << ");";
                    js << "}";
                    js << "el.addEventListener('click',function(){stopAuto();});";
                    js << "show(0);";
                    js << "show(cur);";
                    if (autoplay && pageCount > 1) {
                        js << "startAuto();";
                    }