Loading editor/widgets/slider/slider.cpp +69 −19 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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"); Loading @@ -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"); { Loading Loading @@ -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 Loading @@ -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 { Loading Loading @@ -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"); Loading Loading @@ -669,6 +690,7 @@ extern "C" { track.appendChild(slide); cur = cur->nextElement(); } } wrapper.appendChild(track); Loading Loading @@ -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") { Loading @@ -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();"; } Loading Loading
editor/widgets/slider/slider.cpp +69 −19 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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"); Loading @@ -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"); { Loading Loading @@ -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 Loading @@ -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 { Loading Loading @@ -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"); Loading Loading @@ -669,6 +690,7 @@ extern "C" { track.appendChild(slide); cur = cur->nextElement(); } } wrapper.appendChild(track); Loading Loading @@ -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") { Loading @@ -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();"; } Loading