From 774a74e886526cdf589778c1fef7c2efda6e67ef Mon Sep 17 00:00:00 2001 From: Warwick Date: Thu, 16 Apr 2026 13:16:49 +0200 Subject: [PATCH 1/3] Add sticky positioning mode for Back to Top button with configurable visibility - Add inspector controls for position mode selection (Inline/Scroll, Sticky, Fixed) - Add scroll-based visibility with adjustable threshold (0-100%) for both sticky and fixed modes - Add centered fixed positioning for sticky mode - Update CHANGELOG.md with new features --- CHANGELOG.md | 2 + build/css/style-back-to-top-rtl.css | 2 +- build/css/style-back-to-top.css | 2 +- build/js/back-to-top-view.asset.php | 2 +- build/js/back-to-top-view.js | 2 +- build/js/back-to-top.asset.php | 2 +- build/js/back-to-top.js | 3 +- src/plugins/back-to-top/index.js | 91 +++++++++++++++++++++++++-- src/plugins/back-to-top/style.css | 39 +++++++++++- src/plugins/back-to-top/style.css.map | 2 +- src/plugins/back-to-top/style.scss | 39 +++++++++++- src/plugins/back-to-top/view.js | 37 +++++++++++ 12 files changed, 210 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8689e20..9ebdd38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a Button Icon selector panel for core Button blocks, including left/right positioning and up/down icon options. - Added a Back to Top option as a `core/button` variation so users inherit native Button styling controls and icon compatibility. - Added smooth scrolling support for Back to Top button clicks and internal anchor links using vanilla JavaScript. +- Added sticky positioning mode for Back to Top button with scroll-based visibility control. +- Added inspector controls for Back to Top button with position mode selection (Inline/Scroll, Sticky, Fixed) and configurable visibility threshold (0-100%). - Linkable Group Blocks support for `core/group`, `core/column`, and `core/cover`, including custom URLs and current-post linking from the block toolbar. - Added plugin-managed SCF Local JSON handling and validation utilities for field groups, post types, and taxonomies. - Added an SCF field-group schema for repository validation workflows. diff --git a/build/css/style-back-to-top-rtl.css b/build/css/style-back-to-top-rtl.css index 694df9a..48e2e3f 100644 --- a/build/css/style-back-to-top-rtl.css +++ b/build/css/style-back-to-top-rtl.css @@ -1 +1 @@ -.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:2rem;position:fixed;left:2rem;z-index:40}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:1rem;left:1rem}}.wp-block-button.is-back-to-top .wp-block-button__link{transition:all .2s ease-in-out}.wp-block-button.is-back-to-top .wp-block-button__link:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);transform:translateY(-2px)}.wp-block-button.is-back-to-top .wp-block-button__link:active{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top .wp-block-button__link{transition:none}.wp-block-button.is-back-to-top .wp-block-button__link:hover{transform:none}} +.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:2rem;opacity:0;position:fixed;left:2rem;transition:opacity .3s ease-in-out,visibility .3s ease-in-out;visibility:hidden;z-index:40}.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed].is-visible{opacity:1;visibility:visible}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:1rem;left:1rem}}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{transition:none}}.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{bottom:2rem;right:50%;opacity:0;position:fixed;transform:translateX(50%);transition:opacity .3s ease-in-out,visibility .3s ease-in-out;visibility:hidden;z-index:40}.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky].is-visible{opacity:1;visibility:visible}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{bottom:1rem}}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{transition:none}}.wp-block-button.is-back-to-top .wp-block-button__link{transition:all .2s ease-in-out}.wp-block-button.is-back-to-top .wp-block-button__link:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);transform:translateY(-2px)}.wp-block-button.is-back-to-top .wp-block-button__link:active{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top .wp-block-button__link{transition:none}.wp-block-button.is-back-to-top .wp-block-button__link:hover{transform:none}} diff --git a/build/css/style-back-to-top.css b/build/css/style-back-to-top.css index 35ecbab..b40c754 100644 --- a/build/css/style-back-to-top.css +++ b/build/css/style-back-to-top.css @@ -1 +1 @@ -.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:2rem;position:fixed;right:2rem;z-index:40}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:1rem;right:1rem}}.wp-block-button.is-back-to-top .wp-block-button__link{transition:all .2s ease-in-out}.wp-block-button.is-back-to-top .wp-block-button__link:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);transform:translateY(-2px)}.wp-block-button.is-back-to-top .wp-block-button__link:active{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top .wp-block-button__link{transition:none}.wp-block-button.is-back-to-top .wp-block-button__link:hover{transform:none}} +.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:2rem;opacity:0;position:fixed;right:2rem;transition:opacity .3s ease-in-out,visibility .3s ease-in-out;visibility:hidden;z-index:40}.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed].is-visible{opacity:1;visibility:visible}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{bottom:1rem;right:1rem}}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed]{transition:none}}.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{bottom:2rem;left:50%;opacity:0;position:fixed;transform:translateX(-50%);transition:opacity .3s ease-in-out,visibility .3s ease-in-out;visibility:hidden;z-index:40}.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky].is-visible{opacity:1;visibility:visible}@media(max-width:640px){.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{bottom:1rem}}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky]{transition:none}}.wp-block-button.is-back-to-top .wp-block-button__link{transition:all .2s ease-in-out}.wp-block-button.is-back-to-top .wp-block-button__link:hover{box-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);transform:translateY(-2px)}.wp-block-button.is-back-to-top .wp-block-button__link:active{transform:translateY(0)}@media(prefers-reduced-motion:reduce){.wp-block-button.is-back-to-top .wp-block-button__link{transition:none}.wp-block-button.is-back-to-top .wp-block-button__link:hover{transform:none}} diff --git a/build/js/back-to-top-view.asset.php b/build/js/back-to-top-view.asset.php index e436f46..6109c71 100644 --- a/build/js/back-to-top-view.asset.php +++ b/build/js/back-to-top-view.asset.php @@ -1 +1 @@ - array(), 'version' => '84a35a129b8cf8105805'); + array(), 'version' => 'c253ae222498d3166830'); diff --git a/build/js/back-to-top-view.js b/build/js/back-to-top-view.js index a5110dd..00f0366 100644 --- a/build/js/back-to-top-view.js +++ b/build/js/back-to-top-view.js @@ -1 +1 @@ -(()=>{"use strict";!function(){const t=window.matchMedia("(prefers-reduced-motion: reduce)").matches,e=(e,n=600)=>{if(t)return void("number"==typeof e?window.scrollTo(0,e):e instanceof HTMLElement&&e.scrollIntoView());const o=window.scrollY,r=("number"==typeof e?e:e.getBoundingClientRect().top+o-(()=>{const t=document.querySelector('header[sticky="true"], [data-sticky="true"], .is-sticky');if(!t)return 0;const e=t.getBoundingClientRect();return Math.max(0,e.height+20)})())-o,c=performance.now(),i=t=>{const e=t-c,a=Math.min(e/n,1),s=o+r*(t=>t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1)(a);window.scrollTo(0,s),a<1&&requestAnimationFrame(i)};requestAnimationFrame(i)};var n;n=()=>{document.addEventListener("click",t=>{const n=t.target.closest('a[href*="#"]');if(!n||n.hostname!==window.location.hostname||n.pathname!==window.location.pathname)return;const o=n.getAttribute("href"),r=o.substring(o.indexOf("#"));if("#"===r||""===r)return;const c=document.querySelector(r);c&&(t.preventDefault(),e(c),window.history.pushState(null,"",r))}),document.querySelectorAll(".wp-block-button.is-back-to-top").forEach(n=>{const o=n.querySelector(".wp-block-button__link");o&&o.addEventListener("click",n=>{n.preventDefault(),e(0,t?0:600)})})},"loading"===document.readyState||"interactive"===document.readyState?document.addEventListener("DOMContentLoaded",n):n()}()})(); \ No newline at end of file +(()=>{"use strict";!function(){const t=window.matchMedia("(prefers-reduced-motion: reduce)").matches,e=(e,n=600)=>{if(t)return void("number"==typeof e?window.scrollTo(0,e):e instanceof HTMLElement&&e.scrollIntoView());const o=window.scrollY,i=("number"==typeof e?e:e.getBoundingClientRect().top+o-(()=>{const t=document.querySelector('header[sticky="true"], [data-sticky="true"], .is-sticky');if(!t)return 0;const e=t.getBoundingClientRect();return Math.max(0,e.height+20)})())-o,r=performance.now(),c=t=>{const e=t-r,s=Math.min(e/n,1),a=o+i*(t=>t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1)(s);window.scrollTo(0,a),s<1&&requestAnimationFrame(c)};requestAnimationFrame(c)};var n;n=()=>{document.addEventListener("click",t=>{const n=t.target.closest('a[href*="#"]');if(!n||n.hostname!==window.location.hostname||n.pathname!==window.location.pathname)return;const o=n.getAttribute("href"),i=o.substring(o.indexOf("#"));if("#"===i||""===i)return;const r=document.querySelector(i);r&&(t.preventDefault(),e(r),window.history.pushState(null,"",i))}),document.querySelectorAll(".wp-block-button.is-back-to-top").forEach(n=>{const o=n.querySelector(".wp-block-button__link");if(!o)return;o.addEventListener("click",n=>{n.preventDefault(),e(0,t?0:600)});const i=n.getAttribute("data-back-to-top-mode");if("sticky"===i||"fixed"===i){const t=parseInt(n.getAttribute("data-scroll-threshold")||"75",10),e=()=>{const e=window.scrollY,o=window.innerHeight;e>=(document.documentElement.scrollHeight-o)*(t/100)?n.classList.add("is-visible"):n.classList.remove("is-visible")};let o=!1;window.addEventListener("scroll",()=>{o||(window.requestAnimationFrame(()=>{e(),o=!1}),o=!0)}),e()}})},"loading"===document.readyState||"interactive"===document.readyState?document.addEventListener("DOMContentLoaded",n):n()}()})(); \ No newline at end of file diff --git a/build/js/back-to-top.asset.php b/build/js/back-to-top.asset.php index e1920d0..67cceee 100644 --- a/build/js/back-to-top.asset.php +++ b/build/js/back-to-top.asset.php @@ -1 +1 @@ - array('wp-blocks', 'wp-hooks', 'wp-i18n'), 'version' => '9d676336fdab1bdceaa8'); + array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-hooks', 'wp-i18n'), 'version' => '13562b683257c4748a6b'); diff --git a/build/js/back-to-top.js b/build/js/back-to-top.js index ebf3889..c5f850a 100644 --- a/build/js/back-to-top.js +++ b/build/js/back-to-top.js @@ -1 +1,2 @@ -(()=>{"use strict";const o=window.wp.hooks,t=window.wp.blocks,e=window.wp.i18n;(0,t.registerBlockVariation)("core/button",{name:"back-to-top",title:(0,e.__)("Back to Top","ls-plugin"),icon:"arrow-up",description:(0,e.__)("A button that scrolls to the top of the page with smooth animation.","ls-plugin"),attributes:{text:(0,e.__)("Back to Top","ls-plugin"),isBackToTop:!0,backToTopPositionMode:"scroll",backToTopScrollThreshold:50},isActive:o=>!0===o.isBackToTop}),(0,o.addFilter)("blocks.registerBlockType","ls-plugin/add-back-to-top-attributes",o=>"core/button"!==o.name?o:{...o,attributes:{...o.attributes,isBackToTop:{type:"boolean",default:!1},backToTopPositionMode:{type:"string",default:"scroll"},backToTopScrollThreshold:{type:"number",default:50}}}),(0,o.addFilter)("blocks.getSaveContent.extraProps","ls-plugin/back-to-top-save-props",(o,t,e)=>{if("core/button"!==t.name||!e.isBackToTop)return o;const a=[o.className,"is-back-to-top"].filter(Boolean).join(" ");return{...o,className:a,"data-back-to-top-mode":e.backToTopPositionMode||"scroll"}})})(); \ No newline at end of file +(()=>{"use strict";const o=window.wp.hooks,t=window.wp.blocks,e=window.wp.i18n,l=window.wp.blockEditor,i=window.wp.components,s=window.wp.compose,n=window.ReactJSXRuntime;(0,t.registerBlockVariation)("core/button",{name:"back-to-top",title:(0,e.__)("Back to Top","ls-plugin"),icon:"arrow-up",description:(0,e.__)("A button that scrolls to the top of the page with smooth animation.","ls-plugin"),attributes:{text:(0,e.__)("Back to Top","ls-plugin"),isBackToTop:!0,backToTopPositionMode:"scroll",backToTopScrollThreshold:50},isActive:o=>!0===o.isBackToTop}),(0,o.addFilter)("blocks.registerBlockType","ls-plugin/add-back-to-top-attributes",o=>"core/button"!==o.name?o:{...o,attributes:{...o.attributes,isBackToTop:{type:"boolean",default:!1},backToTopPositionMode:{type:"string",default:"scroll"},backToTopScrollThreshold:{type:"number",default:50}}});const a=(0,s.createHigherOrderComponent)(o=>t=>{const{attributes:s,setAttributes:a,name:c}=t;return"core/button"===c&&s.isBackToTop?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(o,{...t}),(0,n.jsx)(l.InspectorControls,{children:(0,n.jsxs)(i.PanelBody,{title:(0,e.__)("Back to Top Settings","ls-plugin"),initialOpen:!0,children:[(0,n.jsx)(i.SelectControl,{label:(0,e.__)("Position Mode","ls-plugin"),value:s.backToTopPositionMode||"scroll",options:[{label:(0,e.__)("Inline (Scroll)","ls-plugin"),value:"scroll"},{label:(0,e.__)("Sticky (Fixed, Center)","ls-plugin"),value:"sticky"},{label:(0,e.__)("Fixed (Bottom Right)","ls-plugin"),value:"fixed"}],onChange:o=>a({backToTopPositionMode:o}),help:(0,e.__)("Choose how the button is positioned. Sticky mode appears centered after scrolling 75% of the page.","ls-plugin")}),"sticky"===s.backToTopPositionMode&&(0,n.jsx)(i.RangeControl,{label:(0,e.__)("Visibility Threshold (%)","ls-plugin"),value:s.backToTopScrollThreshold||75,onChange:o=>a({backToTopScrollThreshold:o}),min:0,max:100,step:5,help:(0,e.sprintf)(/* translators: %d: threshold percentage */ /* translators: %d: threshold percentage */ +(0,e.__)("Button appears after scrolling %d%% of the page.","ls-plugin"),s.backToTopScrollThreshold||75)})]})})]}):(0,n.jsx)(o,{...t})},"withBackToTopControls");(0,o.addFilter)("editor.BlockEdit","ls-plugin/with-back-to-top-controls",a),(0,o.addFilter)("blocks.getSaveContent.extraProps","ls-plugin/back-to-top-save-props",(o,t,e)=>{if("core/button"!==t.name||!e.isBackToTop)return o;const l=[o.className,"is-back-to-top"].filter(Boolean).join(" "),i={...o,className:l,"data-back-to-top-mode":e.backToTopPositionMode||"scroll"};return"sticky"!==e.backToTopPositionMode&&"fixed"!==e.backToTopPositionMode||(i["data-scroll-threshold"]=e.backToTopScrollThreshold||75),i})})(); \ No newline at end of file diff --git a/src/plugins/back-to-top/index.js b/src/plugins/back-to-top/index.js index 53cdcef..a0b7a35 100644 --- a/src/plugins/back-to-top/index.js +++ b/src/plugins/back-to-top/index.js @@ -5,7 +5,10 @@ import { addFilter } from '@wordpress/hooks'; import { registerBlockVariation } from '@wordpress/blocks'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; +import { InspectorControls } from '@wordpress/block-editor'; +import { PanelBody, SelectControl, RangeControl } from '@wordpress/components'; +import { createHigherOrderComponent } from '@wordpress/compose'; /** * Register Back to Top as a core/button variation @@ -60,9 +63,82 @@ addFilter( ); /** - * Add back-to-top inspector controls and editor classes - * We don't need to override BlockEdit - the attributes filter handles everything + * Add back-to-top inspector controls */ +const withBackToTopControls = createHigherOrderComponent( ( BlockEdit ) => { + return ( props ) => { + const { attributes, setAttributes, name } = props; + + // Only apply to core/button blocks with isBackToTop enabled + if ( name !== 'core/button' || ! attributes.isBackToTop ) { + return ; + } + + return ( + <> + + + + + setAttributes( { backToTopPositionMode: value } ) + } + help={ __( + 'Choose how the button is positioned. Sticky mode appears centered after scrolling 75% of the page.', + 'ls-plugin' + ) } + /> + { attributes.backToTopPositionMode === 'sticky' && ( + + setAttributes( { backToTopScrollThreshold: value } ) + } + min={ 0 } + max={ 100 } + step={ 5 } + help={ sprintf( + /* translators: %d: threshold percentage */ + __( + 'Button appears after scrolling %d%% of the page.', + 'ls-plugin' + ), + attributes.backToTopScrollThreshold || 75 + ) } + /> + ) } + + + + ); + }; +}, 'withBackToTopControls' ); + +addFilter( + 'editor.BlockEdit', + 'ls-plugin/with-back-to-top-controls', + withBackToTopControls +); /** * Add back-to-top data attributes to saved button markup @@ -79,10 +155,17 @@ addFilter( .filter( Boolean ) .join( ' ' ); - return { + const props = { ...extraProps, className: classes, 'data-back-to-top-mode': attributes.backToTopPositionMode || 'scroll', }; + + // Add scroll threshold for sticky and fixed modes + if ( attributes.backToTopPositionMode === 'sticky' || attributes.backToTopPositionMode === 'fixed' ) { + props[ 'data-scroll-threshold' ] = attributes.backToTopScrollThreshold || 75; + } + + return props; } ); diff --git a/src/plugins/back-to-top/style.css b/src/plugins/back-to-top/style.css index 3cd0049..d6159ad 100644 --- a/src/plugins/back-to-top/style.css +++ b/src/plugins/back-to-top/style.css @@ -5,7 +5,8 @@ */ /* Back to Top button specific positioning */ .wp-block-button.is-back-to-top { - /* Fixed mode (sticky to footer) */ + /* Fixed mode (bottom right, appears on scroll) */ + /* Sticky mode (centered above footer, appears on scroll) */ /* Smooth transitions for hover state */ } .wp-block-button.is-back-to-top[data-back-to-top-mode=fixed] { @@ -13,6 +14,13 @@ bottom: 2rem; right: 2rem; z-index: 40; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; +} +.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed].is-visible { + opacity: 1; + visibility: visible; } @media (max-width: 640px) { .wp-block-button.is-back-to-top[data-back-to-top-mode=fixed] { @@ -20,6 +28,35 @@ right: 1rem; } } +@media (prefers-reduced-motion: reduce) { + .wp-block-button.is-back-to-top[data-back-to-top-mode=fixed] { + transition: none; + } +} +.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky] { + position: fixed; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + z-index: 40; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; +} +.wp-block-button.is-back-to-top[data-back-to-top-mode=sticky].is-visible { + opacity: 1; + visibility: visible; +} +@media (max-width: 640px) { + .wp-block-button.is-back-to-top[data-back-to-top-mode=sticky] { + bottom: 1rem; + } +} +@media (prefers-reduced-motion: reduce) { + .wp-block-button.is-back-to-top[data-back-to-top-mode=sticky] { + transition: none; + } +} .wp-block-button.is-back-to-top .wp-block-button__link { transition: all 0.2s ease-in-out; } diff --git a/src/plugins/back-to-top/style.css.map b/src/plugins/back-to-top/style.css.map index 9d3620c..24b282e 100644 --- a/src/plugins/back-to-top/style.css.map +++ b/src/plugins/back-to-top/style.css.map @@ -1 +1 @@ -{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA;;;;EAAA;AAMA,4CAAA;AACA;EACC,kCAAA;EAaA,uCAAA;ACZD;ADAC;EACC,eAAA;EACA,YAAA;EACA,WAAA;EACA,WAAA;ACEF;ADAE;EAND;IAOE,YAAA;IACA,WAAA;ECGD;AACF;ADCC;EACC,gCAAA;ACCF;ADCE;EACC,2BAAA;EACA,gFAAA;ACCH;ADGE;EACC,wBAAA;ACDH;ADIE;EAbD;IAcE,gBAAA;ECDD;EDGC;IACC,eAAA;ECDF;AACF","file":"style.css"} \ No newline at end of file +{"version":3,"sources":["style.scss","style.css"],"names":[],"mappings":"AAAA;;;;EAAA;AAMA,4CAAA;AACA;EACC,iDAAA;EAyBA,2DAAA;EAyBA,uCAAA;AChDD;ADDC;EACC,eAAA;EACA,YAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;EACA,kBAAA;EACA,iEAAA;ACGF;ADDE;EACC,UAAA;EACA,mBAAA;ACGH;ADAE;EAdD;IAeE,YAAA;IACA,WAAA;ECGD;AACF;ADDE;EAnBD;IAoBE,gBAAA;ECID;AACF;ADAC;EACC,eAAA;EACA,YAAA;EACA,SAAA;EACA,2BAAA;EACA,WAAA;EACA,UAAA;EACA,kBAAA;EACA,iEAAA;ACEF;ADAE;EACC,UAAA;EACA,mBAAA;ACEH;ADCE;EAfD;IAgBE,YAAA;ECED;AACF;ADAE;EAnBD;IAoBE,gBAAA;ECGD;AACF;ADCC;EACC,gCAAA;ACCF;ADCE;EACC,2BAAA;EACA,gFAAA;ACCH;ADGE;EACC,wBAAA;ACDH;ADIE;EAbD;IAcE,gBAAA;ECDD;EDGC;IACC,eAAA;ECDF;AACF","file":"style.css"} \ No newline at end of file diff --git a/src/plugins/back-to-top/style.scss b/src/plugins/back-to-top/style.scss index c751b07..8fbdaad 100644 --- a/src/plugins/back-to-top/style.scss +++ b/src/plugins/back-to-top/style.scss @@ -6,17 +6,54 @@ /* Back to Top button specific positioning */ .wp-block-button.is-back-to-top { - /* Fixed mode (sticky to footer) */ + /* Fixed mode (bottom right, appears on scroll) */ &[data-back-to-top-mode="fixed"] { position: fixed; bottom: 2rem; right: 2rem; z-index: 40; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; + + &.is-visible { + opacity: 1; + visibility: visible; + } @media ( max-width: 640px ) { bottom: 1rem; right: 1rem; } + + @media ( prefers-reduced-motion: reduce ) { + transition: none; + } + } + + /* Sticky mode (centered above footer, appears on scroll) */ + &[data-back-to-top-mode="sticky"] { + position: fixed; + bottom: 2rem; + left: 50%; + transform: translateX( -50% ); + z-index: 40; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; + + &.is-visible { + opacity: 1; + visibility: visible; + } + + @media ( max-width: 640px ) { + bottom: 1rem; + } + + @media ( prefers-reduced-motion: reduce ) { + transition: none; + } } /* Smooth transitions for hover state */ diff --git a/src/plugins/back-to-top/view.js b/src/plugins/back-to-top/view.js index 17548c7..fdd74ed 100644 --- a/src/plugins/back-to-top/view.js +++ b/src/plugins/back-to-top/view.js @@ -100,6 +100,43 @@ e.preventDefault(); smoothScrollTo( 0, prefersReducedMotion ? 0 : 600 ); } ); + + // Handle sticky/fixed mode visibility based on scroll + const mode = wrapper.getAttribute( 'data-back-to-top-mode' ); + if ( mode === 'sticky' || mode === 'fixed' ) { + const thresholdPercent = parseInt( wrapper.getAttribute( 'data-scroll-threshold' ) || '75', 10 ); + + const handleScroll = () => { + const scrolled = window.scrollY; + const viewportHeight = window.innerHeight; + const documentHeight = document.documentElement.scrollHeight; + + // Calculate threshold percentage of scrollable distance + const scrollableDistance = documentHeight - viewportHeight; + const threshold = scrollableDistance * ( thresholdPercent / 100 ); + + if ( scrolled >= threshold ) { + wrapper.classList.add( 'is-visible' ); + } else { + wrapper.classList.remove( 'is-visible' ); + } + }; + + // Use throttle for performance + let ticking = false; + window.addEventListener( 'scroll', () => { + if ( ! ticking ) { + window.requestAnimationFrame( () => { + handleScroll(); + ticking = false; + } ); + ticking = true; + } + } ); + + // Check initial scroll position + handleScroll(); + } } ); }; From d21ea787fc28157cf026d590d96e83c3e474d59f Mon Sep 17 00:00:00 2001 From: Warwick Date: Thu, 16 Apr 2026 13:25:38 +0200 Subject: [PATCH 2/3] fix: update Back to Top button to support scroll-based visibility for Sticky and Fixed modes --- build/js/back-to-top.asset.php | 2 +- build/js/back-to-top.js | 4 ++-- src/plugins/back-to-top/index.js | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/js/back-to-top.asset.php b/build/js/back-to-top.asset.php index 67cceee..2813d14 100644 --- a/build/js/back-to-top.asset.php +++ b/build/js/back-to-top.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-hooks', 'wp-i18n'), 'version' => '13562b683257c4748a6b'); + array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-hooks', 'wp-i18n'), 'version' => 'fea66ba289b98b6e8f9d'); diff --git a/build/js/back-to-top.js b/build/js/back-to-top.js index c5f850a..6f4ffa4 100644 --- a/build/js/back-to-top.js +++ b/build/js/back-to-top.js @@ -1,2 +1,2 @@ -(()=>{"use strict";const o=window.wp.hooks,t=window.wp.blocks,e=window.wp.i18n,l=window.wp.blockEditor,i=window.wp.components,s=window.wp.compose,n=window.ReactJSXRuntime;(0,t.registerBlockVariation)("core/button",{name:"back-to-top",title:(0,e.__)("Back to Top","ls-plugin"),icon:"arrow-up",description:(0,e.__)("A button that scrolls to the top of the page with smooth animation.","ls-plugin"),attributes:{text:(0,e.__)("Back to Top","ls-plugin"),isBackToTop:!0,backToTopPositionMode:"scroll",backToTopScrollThreshold:50},isActive:o=>!0===o.isBackToTop}),(0,o.addFilter)("blocks.registerBlockType","ls-plugin/add-back-to-top-attributes",o=>"core/button"!==o.name?o:{...o,attributes:{...o.attributes,isBackToTop:{type:"boolean",default:!1},backToTopPositionMode:{type:"string",default:"scroll"},backToTopScrollThreshold:{type:"number",default:50}}});const a=(0,s.createHigherOrderComponent)(o=>t=>{const{attributes:s,setAttributes:a,name:c}=t;return"core/button"===c&&s.isBackToTop?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(o,{...t}),(0,n.jsx)(l.InspectorControls,{children:(0,n.jsxs)(i.PanelBody,{title:(0,e.__)("Back to Top Settings","ls-plugin"),initialOpen:!0,children:[(0,n.jsx)(i.SelectControl,{label:(0,e.__)("Position Mode","ls-plugin"),value:s.backToTopPositionMode||"scroll",options:[{label:(0,e.__)("Inline (Scroll)","ls-plugin"),value:"scroll"},{label:(0,e.__)("Sticky (Fixed, Center)","ls-plugin"),value:"sticky"},{label:(0,e.__)("Fixed (Bottom Right)","ls-plugin"),value:"fixed"}],onChange:o=>a({backToTopPositionMode:o}),help:(0,e.__)("Choose how the button is positioned. Sticky mode appears centered after scrolling 75% of the page.","ls-plugin")}),"sticky"===s.backToTopPositionMode&&(0,n.jsx)(i.RangeControl,{label:(0,e.__)("Visibility Threshold (%)","ls-plugin"),value:s.backToTopScrollThreshold||75,onChange:o=>a({backToTopScrollThreshold:o}),min:0,max:100,step:5,help:(0,e.sprintf)(/* translators: %d: threshold percentage */ /* translators: %d: threshold percentage */ -(0,e.__)("Button appears after scrolling %d%% of the page.","ls-plugin"),s.backToTopScrollThreshold||75)})]})})]}):(0,n.jsx)(o,{...t})},"withBackToTopControls");(0,o.addFilter)("editor.BlockEdit","ls-plugin/with-back-to-top-controls",a),(0,o.addFilter)("blocks.getSaveContent.extraProps","ls-plugin/back-to-top-save-props",(o,t,e)=>{if("core/button"!==t.name||!e.isBackToTop)return o;const l=[o.className,"is-back-to-top"].filter(Boolean).join(" "),i={...o,className:l,"data-back-to-top-mode":e.backToTopPositionMode||"scroll"};return"sticky"!==e.backToTopPositionMode&&"fixed"!==e.backToTopPositionMode||(i["data-scroll-threshold"]=e.backToTopScrollThreshold||75),i})})(); \ No newline at end of file +(()=>{"use strict";const o=window.wp.hooks,t=window.wp.blocks,e=window.wp.i18n,l=window.wp.blockEditor,i=window.wp.components,s=window.wp.compose,n=window.ReactJSXRuntime;(0,t.registerBlockVariation)("core/button",{name:"back-to-top",title:(0,e.__)("Back to Top","ls-plugin"),icon:"arrow-up",description:(0,e.__)("A button that scrolls to the top of the page with smooth animation.","ls-plugin"),attributes:{text:(0,e.__)("Back to Top","ls-plugin"),isBackToTop:!0,backToTopPositionMode:"scroll",backToTopScrollThreshold:50},isActive:o=>!0===o.isBackToTop}),(0,o.addFilter)("blocks.registerBlockType","ls-plugin/add-back-to-top-attributes",o=>"core/button"!==o.name?o:{...o,attributes:{...o.attributes,isBackToTop:{type:"boolean",default:!1},backToTopPositionMode:{type:"string",default:"scroll"},backToTopScrollThreshold:{type:"number",default:50}}});const a=(0,s.createHigherOrderComponent)(o=>t=>{const{attributes:s,setAttributes:a,name:c}=t;return"core/button"===c&&s.isBackToTop?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(o,{...t}),(0,n.jsx)(l.InspectorControls,{children:(0,n.jsxs)(i.PanelBody,{title:(0,e.__)("Back to Top Settings","ls-plugin"),initialOpen:!0,children:[(0,n.jsx)(i.SelectControl,{label:(0,e.__)("Position Mode","ls-plugin"),value:s.backToTopPositionMode||"scroll",options:[{label:(0,e.__)("Inline (Scroll)","ls-plugin"),value:"scroll"},{label:(0,e.__)("Sticky (Fixed, Center)","ls-plugin"),value:"sticky"},{label:(0,e.__)("Fixed (Bottom Right)","ls-plugin"),value:"fixed"}],onChange:o=>a({backToTopPositionMode:o}),help:(0,e.__)("Choose how the button is positioned. Sticky and Fixed modes support scroll-based visibility.","ls-plugin")}),("sticky"===s.backToTopPositionMode||"fixed"===s.backToTopPositionMode)&&(0,n.jsx)(i.RangeControl,{label:(0,e.__)("Visibility Threshold (%)","ls-plugin"),value:s.backToTopScrollThreshold??75,onChange:o=>a({backToTopScrollThreshold:o}),min:0,max:100,step:5,help:(0,e.sprintf)(/* translators: %d: threshold percentage */ /* translators: %d: threshold percentage */ +(0,e.__)("Button appears after scrolling %d%% of the page.","ls-plugin"),s.backToTopScrollThreshold??75)})]})})]}):(0,n.jsx)(o,{...t})},"withBackToTopControls");(0,o.addFilter)("editor.BlockEdit","ls-plugin/with-back-to-top-controls",a),(0,o.addFilter)("blocks.getSaveContent.extraProps","ls-plugin/back-to-top-save-props",(o,t,e)=>{if("core/button"!==t.name||!e.isBackToTop)return o;const l=[o.className,"is-back-to-top"].filter(Boolean).join(" "),i={...o,className:l,"data-back-to-top-mode":e.backToTopPositionMode||"scroll"};return"sticky"!==e.backToTopPositionMode&&"fixed"!==e.backToTopPositionMode||(i["data-scroll-threshold"]=e.backToTopScrollThreshold??75),i})})(); \ No newline at end of file diff --git a/src/plugins/back-to-top/index.js b/src/plugins/back-to-top/index.js index a0b7a35..c054f83 100644 --- a/src/plugins/back-to-top/index.js +++ b/src/plugins/back-to-top/index.js @@ -103,14 +103,14 @@ const withBackToTopControls = createHigherOrderComponent( ( BlockEdit ) => { setAttributes( { backToTopPositionMode: value } ) } help={ __( - 'Choose how the button is positioned. Sticky mode appears centered after scrolling 75% of the page.', + 'Choose how the button is positioned. Sticky and Fixed modes support scroll-based visibility.', 'ls-plugin' ) } /> - { attributes.backToTopPositionMode === 'sticky' && ( + { ( attributes.backToTopPositionMode === 'sticky' || attributes.backToTopPositionMode === 'fixed' ) && ( setAttributes( { backToTopScrollThreshold: value } ) } @@ -123,7 +123,7 @@ const withBackToTopControls = createHigherOrderComponent( ( BlockEdit ) => { 'Button appears after scrolling %d%% of the page.', 'ls-plugin' ), - attributes.backToTopScrollThreshold || 75 + attributes.backToTopScrollThreshold ?? 75 ) } /> ) } @@ -163,7 +163,7 @@ addFilter( // Add scroll threshold for sticky and fixed modes if ( attributes.backToTopPositionMode === 'sticky' || attributes.backToTopPositionMode === 'fixed' ) { - props[ 'data-scroll-threshold' ] = attributes.backToTopScrollThreshold || 75; + props[ 'data-scroll-threshold' ] = attributes.backToTopScrollThreshold ?? 75; } return props; From 62750f425bd0de5cc68d16da53eb520bd31806fd Mon Sep 17 00:00:00 2001 From: Warwick Date: Thu, 16 Apr 2026 13:28:06 +0200 Subject: [PATCH 3/3] Refactor: use single shared scroll listener for all Back to Top buttons - Collect all sticky/fixed mode buttons during initialization - Create one scroll listener that handles all instances - Improves performance when multiple buttons are present on a page --- build/js/back-to-top-view.asset.php | 2 +- build/js/back-to-top-view.js | 2 +- src/plugins/back-to-top/view.js | 56 ++++++++++++++++------------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/build/js/back-to-top-view.asset.php b/build/js/back-to-top-view.asset.php index 6109c71..6bf11ff 100644 --- a/build/js/back-to-top-view.asset.php +++ b/build/js/back-to-top-view.asset.php @@ -1 +1 @@ - array(), 'version' => 'c253ae222498d3166830'); + array(), 'version' => '2f878e4e9811d7262c21'); diff --git a/build/js/back-to-top-view.js b/build/js/back-to-top-view.js index 00f0366..a340be3 100644 --- a/build/js/back-to-top-view.js +++ b/build/js/back-to-top-view.js @@ -1 +1 @@ -(()=>{"use strict";!function(){const t=window.matchMedia("(prefers-reduced-motion: reduce)").matches,e=(e,n=600)=>{if(t)return void("number"==typeof e?window.scrollTo(0,e):e instanceof HTMLElement&&e.scrollIntoView());const o=window.scrollY,i=("number"==typeof e?e:e.getBoundingClientRect().top+o-(()=>{const t=document.querySelector('header[sticky="true"], [data-sticky="true"], .is-sticky');if(!t)return 0;const e=t.getBoundingClientRect();return Math.max(0,e.height+20)})())-o,r=performance.now(),c=t=>{const e=t-r,s=Math.min(e/n,1),a=o+i*(t=>t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1)(s);window.scrollTo(0,a),s<1&&requestAnimationFrame(c)};requestAnimationFrame(c)};var n;n=()=>{document.addEventListener("click",t=>{const n=t.target.closest('a[href*="#"]');if(!n||n.hostname!==window.location.hostname||n.pathname!==window.location.pathname)return;const o=n.getAttribute("href"),i=o.substring(o.indexOf("#"));if("#"===i||""===i)return;const r=document.querySelector(i);r&&(t.preventDefault(),e(r),window.history.pushState(null,"",i))}),document.querySelectorAll(".wp-block-button.is-back-to-top").forEach(n=>{const o=n.querySelector(".wp-block-button__link");if(!o)return;o.addEventListener("click",n=>{n.preventDefault(),e(0,t?0:600)});const i=n.getAttribute("data-back-to-top-mode");if("sticky"===i||"fixed"===i){const t=parseInt(n.getAttribute("data-scroll-threshold")||"75",10),e=()=>{const e=window.scrollY,o=window.innerHeight;e>=(document.documentElement.scrollHeight-o)*(t/100)?n.classList.add("is-visible"):n.classList.remove("is-visible")};let o=!1;window.addEventListener("scroll",()=>{o||(window.requestAnimationFrame(()=>{e(),o=!1}),o=!0)}),e()}})},"loading"===document.readyState||"interactive"===document.readyState?document.addEventListener("DOMContentLoaded",n):n()}()})(); \ No newline at end of file +(()=>{"use strict";!function(){const t=window.matchMedia("(prefers-reduced-motion: reduce)").matches,e=(e,n=600)=>{if(t)return void("number"==typeof e?window.scrollTo(0,e):e instanceof HTMLElement&&e.scrollIntoView());const o=window.scrollY,r=("number"==typeof e?e:e.getBoundingClientRect().top+o-(()=>{const t=document.querySelector('header[sticky="true"], [data-sticky="true"], .is-sticky');if(!t)return 0;const e=t.getBoundingClientRect();return Math.max(0,e.height+20)})())-o,i=performance.now(),c=t=>{const e=t-i,s=Math.min(e/n,1),a=o+r*(t=>t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1)(s);window.scrollTo(0,a),s<1&&requestAnimationFrame(c)};requestAnimationFrame(c)};var n;n=()=>{document.addEventListener("click",t=>{const n=t.target.closest('a[href*="#"]');if(!n||n.hostname!==window.location.hostname||n.pathname!==window.location.pathname)return;const o=n.getAttribute("href"),r=o.substring(o.indexOf("#"));if("#"===r||""===r)return;const i=document.querySelector(r);i&&(t.preventDefault(),e(i),window.history.pushState(null,"",r))}),(()=>{const n=document.querySelectorAll(".wp-block-button.is-back-to-top"),o=[];if(n.forEach(n=>{const r=n.querySelector(".wp-block-button__link");if(!r)return;r.addEventListener("click",n=>{n.preventDefault(),e(0,t?0:600)});const i=n.getAttribute("data-back-to-top-mode");if("sticky"===i||"fixed"===i){const t=parseInt(n.getAttribute("data-scroll-threshold")||"75",10);o.push({wrapper:n,thresholdPercent:t})}}),o.length>0){const t=()=>{const t=window.scrollY,e=window.innerHeight,n=document.documentElement.scrollHeight-e;o.forEach(({wrapper:e,thresholdPercent:o})=>{t>=n*(o/100)?e.classList.add("is-visible"):e.classList.remove("is-visible")})};let e=!1;window.addEventListener("scroll",()=>{e||(window.requestAnimationFrame(()=>{t(),e=!1}),e=!0)}),t()}})()},"loading"===document.readyState||"interactive"===document.readyState?document.addEventListener("DOMContentLoaded",n):n()}()})(); \ No newline at end of file diff --git a/src/plugins/back-to-top/view.js b/src/plugins/back-to-top/view.js index fdd74ed..6ff9618 100644 --- a/src/plugins/back-to-top/view.js +++ b/src/plugins/back-to-top/view.js @@ -91,6 +91,9 @@ '.wp-block-button.is-back-to-top' ); + // Collect buttons that need scroll visibility handling + const scrollVisibilityButtons = []; + wrappers.forEach( ( wrapper ) => { // Find the inner link or button const link = wrapper.querySelector( '.wp-block-button__link' ); @@ -105,14 +108,19 @@ const mode = wrapper.getAttribute( 'data-back-to-top-mode' ); if ( mode === 'sticky' || mode === 'fixed' ) { const thresholdPercent = parseInt( wrapper.getAttribute( 'data-scroll-threshold' ) || '75', 10 ); - - const handleScroll = () => { - const scrolled = window.scrollY; - const viewportHeight = window.innerHeight; - const documentHeight = document.documentElement.scrollHeight; - - // Calculate threshold percentage of scrollable distance - const scrollableDistance = documentHeight - viewportHeight; + scrollVisibilityButtons.push( { wrapper, thresholdPercent } ); + } + } ); + + // Set up a single shared scroll listener for all buttons + if ( scrollVisibilityButtons.length > 0 ) { + const handleScroll = () => { + const scrolled = window.scrollY; + const viewportHeight = window.innerHeight; + const documentHeight = document.documentElement.scrollHeight; + const scrollableDistance = documentHeight - viewportHeight; + + scrollVisibilityButtons.forEach( ( { wrapper, thresholdPercent } ) => { const threshold = scrollableDistance * ( thresholdPercent / 100 ); if ( scrolled >= threshold ) { @@ -120,24 +128,24 @@ } else { wrapper.classList.remove( 'is-visible' ); } - }; - - // Use throttle for performance - let ticking = false; - window.addEventListener( 'scroll', () => { - if ( ! ticking ) { - window.requestAnimationFrame( () => { - handleScroll(); - ticking = false; - } ); - ticking = true; - } } ); + }; + + // Use throttle for performance + let ticking = false; + window.addEventListener( 'scroll', () => { + if ( ! ticking ) { + window.requestAnimationFrame( () => { + handleScroll(); + ticking = false; + } ); + ticking = true; + } + } ); - // Check initial scroll position - handleScroll(); - } - } ); + // Check initial scroll position + handleScroll(); + } }; // DOM ready check