diff --git a/CHANGELOG.md b/CHANGELOG.md index 30c3aac..b7dc5a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,14 +11,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added a Style Switcher block with selectable theme style variations and configurable icon display behaviour. +- 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. ### Changed +- Changed Back to Top implementation from a standalone custom block to a `core/button` variation. +- Changed Back to Top frontend targeting to use a dedicated wrapper class (`is-back-to-top`) for reliable JS and CSS behaviour. +- Changed Back to Top visibility to always display (removed scroll-threshold hide/show behaviour). ### Deprecated ### Removed +- Removed the standalone Back to Top block source in favour of variation-based implementation. ### Fixed +- Fixed editor runtime errors from invalid React component handling in Back to Top editor integration. +- Fixed strict mode error in Back to Top animation loop by replacing `arguments.callee` with a named animation step. ### Security diff --git a/README.md b/README.md index 957d31b..ad277aa 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,16 @@ composer run phpcs --- +## Back to Top variation (how to use) + +1. In the editor, insert a Button block. +2. Select the Back to Top variation from block styles/variations. +3. Publish or update the page. + +When clicked on the frontend, the button smoothly scrolls to the top of the page. + +--- + ## AI workflows | Folder | Purpose | diff --git a/build/css/back-to-top.asset.php b/build/css/back-to-top.asset.php new file mode 100644 index 0000000..941e541 --- /dev/null +++ b/build/css/back-to-top.asset.php @@ -0,0 +1 @@ + array(), 'version' => 'd4bf1d0478307697fd97'); diff --git a/build/css/button-icon.asset.php b/build/css/button-icon.asset.php new file mode 100644 index 0000000..141d624 --- /dev/null +++ b/build/css/button-icon.asset.php @@ -0,0 +1 @@ + array(), 'version' => 'ee8a3fb648b3838658c7'); diff --git a/build/css/style-back-to-top-rtl.css b/build/css/style-back-to-top-rtl.css new file mode 100644 index 0000000..694df9a --- /dev/null +++ b/build/css/style-back-to-top-rtl.css @@ -0,0 +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}} diff --git a/build/css/style-back-to-top.css b/build/css/style-back-to-top.css new file mode 100644 index 0000000..35ecbab --- /dev/null +++ b/build/css/style-back-to-top.css @@ -0,0 +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}} diff --git a/build/css/style-button-icon-rtl.css b/build/css/style-button-icon-rtl.css new file mode 100644 index 0000000..ad02bd4 --- /dev/null +++ b/build/css/style-button-icon-rtl.css @@ -0,0 +1 @@ +.wp-block-button.has-ls-button-icon .wp-block-button__link{align-items:center;display:inline-flex;position:relative}.wp-block-button.has-ls-button-icon .wp-block-button__link:after,.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link:before{color:var(--ls-button-fill-icon-colour,currentColor);display:grid;font-size:var(--ls-button-fill-icon-font-size);font-weight:700;inset-block:var(--ls-button-fill-icon-block-inset);line-height:1;place-items:center;position:absolute;transition:color .5s cubic-bezier(.4,0,.2,1);width:var(--ls-button-fill-icon-well-size)}.wp-block-button.has-ls-button-icon .wp-block-button__link:after{content:"→";inset-inline-end:var(--ls-button-fill-icon-inline-end)}.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link:before{content:"→";inset-inline-start:var(--ls-button-fill-icon-inline-end)}.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link:after{content:none}.wp-block-button.has-ls-button-icon-arrow-right .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-arrow-right.has-ls-button-icon-position-left .wp-block-button__link:before{content:"→"!important}.wp-block-button.has-ls-button-icon-arrow-left .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-arrow-left.has-ls-button-icon-position-left .wp-block-button__link:before{content:"←"!important}.wp-block-button.has-ls-button-icon-chevron-right .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-chevron-right.has-ls-button-icon-position-left .wp-block-button__link:before{content:"›"!important}.wp-block-button.has-ls-button-icon-arrow-up .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-arrow-up.has-ls-button-icon-position-left .wp-block-button__link:before{content:"↑"!important}.wp-block-button.has-ls-button-icon-arrow-down .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-arrow-down.has-ls-button-icon-position-left .wp-block-button__link:before{content:"↓"!important}.wp-block-button.has-ls-button-icon-external .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-external.has-ls-button-icon-position-left .wp-block-button__link:before{content:"↗"!important} diff --git a/build/css/style-button-icon.css b/build/css/style-button-icon.css new file mode 100644 index 0000000..ad02bd4 --- /dev/null +++ b/build/css/style-button-icon.css @@ -0,0 +1 @@ +.wp-block-button.has-ls-button-icon .wp-block-button__link{align-items:center;display:inline-flex;position:relative}.wp-block-button.has-ls-button-icon .wp-block-button__link:after,.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link:before{color:var(--ls-button-fill-icon-colour,currentColor);display:grid;font-size:var(--ls-button-fill-icon-font-size);font-weight:700;inset-block:var(--ls-button-fill-icon-block-inset);line-height:1;place-items:center;position:absolute;transition:color .5s cubic-bezier(.4,0,.2,1);width:var(--ls-button-fill-icon-well-size)}.wp-block-button.has-ls-button-icon .wp-block-button__link:after{content:"→";inset-inline-end:var(--ls-button-fill-icon-inline-end)}.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link:before{content:"→";inset-inline-start:var(--ls-button-fill-icon-inline-end)}.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link:after{content:none}.wp-block-button.has-ls-button-icon-arrow-right .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-arrow-right.has-ls-button-icon-position-left .wp-block-button__link:before{content:"→"!important}.wp-block-button.has-ls-button-icon-arrow-left .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-arrow-left.has-ls-button-icon-position-left .wp-block-button__link:before{content:"←"!important}.wp-block-button.has-ls-button-icon-chevron-right .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-chevron-right.has-ls-button-icon-position-left .wp-block-button__link:before{content:"›"!important}.wp-block-button.has-ls-button-icon-arrow-up .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-arrow-up.has-ls-button-icon-position-left .wp-block-button__link:before{content:"↑"!important}.wp-block-button.has-ls-button-icon-arrow-down .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-arrow-down.has-ls-button-icon-position-left .wp-block-button__link:before{content:"↓"!important}.wp-block-button.has-ls-button-icon-external .wp-block-button__link:after,.wp-block-button.has-ls-button-icon-external.has-ls-button-icon-position-left .wp-block-button__link:before{content:"↗"!important} diff --git a/build/js/back-to-top-view.asset.php b/build/js/back-to-top-view.asset.php new file mode 100644 index 0000000..e436f46 --- /dev/null +++ b/build/js/back-to-top-view.asset.php @@ -0,0 +1 @@ + array(), 'version' => '84a35a129b8cf8105805'); diff --git a/build/js/back-to-top-view.js b/build/js/back-to-top-view.js new file mode 100644 index 0000000..a5110dd --- /dev/null +++ b/build/js/back-to-top-view.js @@ -0,0 +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 diff --git a/build/js/back-to-top.asset.php b/build/js/back-to-top.asset.php new file mode 100644 index 0000000..e1920d0 --- /dev/null +++ b/build/js/back-to-top.asset.php @@ -0,0 +1 @@ + array('wp-blocks', 'wp-hooks', 'wp-i18n'), 'version' => '9d676336fdab1bdceaa8'); diff --git a/build/js/back-to-top.js b/build/js/back-to-top.js new file mode 100644 index 0000000..ebf3889 --- /dev/null +++ b/build/js/back-to-top.js @@ -0,0 +1 @@ +(()=>{"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 diff --git a/build/js/button-icon.asset.php b/build/js/button-icon.asset.php new file mode 100644 index 0000000..ed79d89 --- /dev/null +++ b/build/js/button-icon.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => 'd7bb9b5a3d9658d15028'); diff --git a/build/js/button-icon.js b/build/js/button-icon.js new file mode 100644 index 0000000..d7c589d --- /dev/null +++ b/build/js/button-icon.js @@ -0,0 +1 @@ +(()=>{"use strict";const n=window.wp.hooks,t=window.wp.compose,o=window.wp.blockEditor,l=window.wp.components,e=window.wp.element,i=window.wp.i18n,s=window.ReactJSXRuntime;function r(){return(0,s.jsx)(l.SVG,{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",children:(0,s.jsx)(l.Path,{fill:"currentColor",d:"M4 11h12.17l-3.58-3.59L14 6l6 6-6 6-1.41-1.41L16.17 13H4v-2z"})})}function c(){return(0,s.jsx)(l.SVG,{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",children:(0,s.jsx)(l.Path,{fill:"currentColor",d:"M20 11H7.83l3.58-3.59L10 6l-6 6 6 6 1.41-1.41L7.83 13H20v-2z"})})}function u(){return(0,s.jsx)(l.SVG,{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",children:(0,s.jsx)(l.Path,{fill:"currentColor",d:"m10 6 6 6-6 6-1.41-1.41L13.17 12 8.59 7.41 10 6z"})})}function a(){return(0,s.jsx)(l.SVG,{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",children:(0,s.jsx)(l.Path,{fill:"currentColor",d:"M11 20V7.83l-3.59 3.58L6 10l6-6 6 6-1.41 1.41L13 7.83V20h-2z"})})}function h(){return(0,s.jsx)(l.SVG,{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",children:(0,s.jsx)(l.Path,{fill:"currentColor",d:"M11 4v12.17l-3.59-3.58L6 14l6 6 6-6-1.41-1.41L13 16.17V4h-2z"})})}function d(){return(0,s.jsx)(l.SVG,{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",children:(0,s.jsx)(l.Path,{fill:"currentColor",d:"M14 3h7v7h-2V6.41l-9.29 9.3-1.42-1.42 9.3-9.29H14V3zM5 5h6v2H7v10h10v-4h2v6H5V5z"})})}const w=[{label:(0,i.__)("Arrow Right","ls-plugin"),value:"arrow-right",char:"→",icon:(0,s.jsx)(r,{})},{label:(0,i.__)("Arrow Left","ls-plugin"),value:"arrow-left",char:"←",icon:(0,s.jsx)(c,{})},{label:(0,i.__)("Chevron Right","ls-plugin"),value:"chevron-right",char:"›",icon:(0,s.jsx)(u,{})},{label:(0,i.__)("Arrow Up","ls-plugin"),value:"arrow-up",char:"↑",icon:(0,s.jsx)(a,{})},{label:(0,i.__)("Arrow Down","ls-plugin"),value:"arrow-down",char:"↓",icon:(0,s.jsx)(h,{})},{label:(0,i.__)("External Link","ls-plugin"),value:"external",char:"↗",icon:(0,s.jsx)(d,{})}];function x(...n){return n.filter(Boolean).join(" ")}(0,n.addFilter)("blocks.registerBlockType","ls-plugin/button-icons/add-attributes",function(n){return"core/button"!==n.name?n:{...n,attributes:{...n.attributes,lsButtonIcon:{type:"string"},lsButtonIconPositionLeft:{type:"boolean",default:!1}}}}),(0,n.addFilter)("editor.BlockEdit","ls-plugin/button-icons/add-inspector-controls",function(n){return t=>{if("core/button"!==t.name)return(0,s.jsx)(n,{...t});const{attributes:r,setAttributes:c}=t,{lsButtonIcon:u,lsButtonIconPositionLeft:a}=r;return(0,s.jsxs)(e.Fragment,{children:[(0,s.jsx)(n,{...t}),(0,s.jsx)(o.InspectorControls,{children:(0,s.jsxs)(l.PanelBody,{title:(0,i.__)("Button Icon","ls-plugin"),className:"ls-plugin-button-icon-picker",initialOpen:!0,children:[(0,s.jsx)(l.PanelRow,{children:(0,s.jsx)(l.__experimentalGrid,{columns:"4",gap:"8px",children:w.map(n=>(0,s.jsx)(l.Button,{label:n.label,isPressed:u===n.value,onClick:()=>c({lsButtonIcon:u===n.value?void 0:n.value}),children:n.icon},n.value))})}),u&&(0,s.jsx)(l.PanelRow,{children:(0,s.jsx)(l.ToggleControl,{label:(0,i.__)("Show icon on left","ls-plugin"),checked:a,onChange:()=>c({lsButtonIconPositionLeft:!a})})})]})})]})}});const p=(0,t.createHigherOrderComponent)(n=>t=>{const{name:o,attributes:l}=t;if("core/button"!==o||!l?.lsButtonIcon)return(0,s.jsx)(n,{...t});const e=x(t.className,"has-ls-button-icon",l.lsButtonIcon?`has-ls-button-icon-${l.lsButtonIcon}`:"",l.lsButtonIconPositionLeft?"has-ls-button-icon-position-left":"");return(0,s.jsx)(n,{...t,className:e})},"addClasses");(0,n.addFilter)("editor.BlockListBlock","ls-plugin/button-icons/add-classes",p),(0,n.addFilter)("blocks.getSaveContent.extraProps","ls-plugin/button-icons/add-save-props",function(n,t,o){return"core/button"===t.name&&o?.lsButtonIcon?{...n,className:x(n.className,"has-ls-button-icon",o.lsButtonIcon?`has-ls-button-icon-${o.lsButtonIcon}`:"",o.lsButtonIconPositionLeft?"has-ls-button-icon-position-left":"")}:n})})(); \ No newline at end of file diff --git a/ls-plugin.php b/ls-plugin.php index 66f6994..38207bf 100644 --- a/ls-plugin.php +++ b/ls-plugin.php @@ -50,3 +50,121 @@ function ls_plugin_init() { $style_switcher->register_hooks(); } add_action( 'plugins_loaded', 'ls_plugin_init' ); + +/** + * Enqueues Button Icon editor assets. + * + * @return void + */ +function ls_plugin_enqueue_button_icon_editor_assets() { + $asset_path = LS_PLUGIN_PLUGIN_DIR . 'build/js/button-icon.asset.php'; + + if ( ! file_exists( $asset_path ) ) { + return; + } + + $asset = include $asset_path; + + wp_enqueue_script( + 'ls-plugin-button-icon', + LS_PLUGIN_PLUGIN_URL . 'build/js/button-icon.js', + $asset['dependencies'] ?? array(), + $asset['version'] ?? LS_PLUGIN_VERSION, + true + ); +} +add_action( 'enqueue_block_editor_assets', 'ls_plugin_enqueue_button_icon_editor_assets' ); + +/** + * Enqueues Button Icon shared styles for front end and editor. + * + * @return void + */ +function ls_plugin_enqueue_button_icon_styles() { + $asset_path = LS_PLUGIN_PLUGIN_DIR . 'build/css/button-icon.asset.php'; + + if ( ! file_exists( $asset_path ) ) { + return; + } + + $asset = include $asset_path; + + wp_enqueue_style( + 'ls-plugin-button-icon', + LS_PLUGIN_PLUGIN_URL . 'build/css/style-button-icon.css', + $asset['dependencies'] ?? array(), + $asset['version'] ?? LS_PLUGIN_VERSION + ); +} +add_action( 'enqueue_block_assets', 'ls_plugin_enqueue_button_icon_styles' ); + +/** + * Enqueues Back to Top variation editor assets. + * + * @return void + */ +function ls_plugin_enqueue_back_to_top_editor_assets() { + $asset_path = LS_PLUGIN_PLUGIN_DIR . 'build/js/back-to-top.asset.php'; + + if ( ! file_exists( $asset_path ) ) { + return; + } + + $asset = include $asset_path; + + wp_enqueue_script( + 'ls-plugin-back-to-top', + LS_PLUGIN_PLUGIN_URL . 'build/js/back-to-top.js', + $asset['dependencies'] ?? array(), + $asset['version'] ?? LS_PLUGIN_VERSION, + true + ); +} +add_action( 'enqueue_block_editor_assets', 'ls_plugin_enqueue_back_to_top_editor_assets' ); + +/** + * Enqueues Back to Top view script for smooth scrolling on front end. + * + * @return void + */ +function ls_plugin_enqueue_back_to_top_view_script() { + $asset_path = LS_PLUGIN_PLUGIN_DIR . 'build/js/back-to-top-view.asset.php'; + + if ( ! file_exists( $asset_path ) ) { + return; + } + + $asset = include $asset_path; + + wp_enqueue_script( + 'ls-plugin-back-to-top-view', + LS_PLUGIN_PLUGIN_URL . 'build/js/back-to-top-view.js', + $asset['dependencies'] ?? array(), + $asset['version'] ?? LS_PLUGIN_VERSION, + true + ); +} +add_action( 'wp_enqueue_scripts', 'ls_plugin_enqueue_back_to_top_view_script' ); + +/** + * Enqueues Back to Top shared styles for front end and editor. + * + * @return void + */ +function ls_plugin_enqueue_back_to_top_styles() { + $asset_path = LS_PLUGIN_PLUGIN_DIR . 'build/css/back-to-top.asset.php'; + + if ( ! file_exists( $asset_path ) ) { + return; + } + + $asset = include $asset_path; + + wp_enqueue_style( + 'ls-plugin-back-to-top', + LS_PLUGIN_PLUGIN_URL . 'build/css/style-back-to-top.css', + $asset['dependencies'] ?? array(), + $asset['version'] ?? LS_PLUGIN_VERSION + ); +} +add_action( 'enqueue_block_assets', 'ls_plugin_enqueue_back_to_top_styles' ); diff --git a/src/plugins/back-to-top/index.js b/src/plugins/back-to-top/index.js new file mode 100644 index 0000000..53cdcef --- /dev/null +++ b/src/plugins/back-to-top/index.js @@ -0,0 +1,88 @@ +/** + * Back to Top Variation for core/button + * Registers a "Back to Top" variation of the core Button block with smooth scrolling. + */ + +import { addFilter } from '@wordpress/hooks'; +import { registerBlockVariation } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; + +/** + * Register Back to Top as a core/button variation + */ +registerBlockVariation( 'core/button', { + name: 'back-to-top', + title: __( 'Back to Top', 'ls-plugin' ), + icon: 'arrow-up', + description: __( + 'A button that scrolls to the top of the page with smooth animation.', + 'ls-plugin' + ), + attributes: { + text: __( 'Back to Top', 'ls-plugin' ), + isBackToTop: true, + backToTopPositionMode: 'scroll', + backToTopScrollThreshold: 50, + }, + isActive: ( blockAttributes ) => blockAttributes.isBackToTop === true, +} ); + +/** + * Add back-to-top attributes to core/button + */ +addFilter( + 'blocks.registerBlockType', + 'ls-plugin/add-back-to-top-attributes', + ( settings ) => { + if ( settings.name !== 'core/button' ) { + return settings; + } + + return { + ...settings, + attributes: { + ...settings.attributes, + isBackToTop: { + type: 'boolean', + default: false, + }, + backToTopPositionMode: { + type: 'string', + default: 'scroll', + }, + backToTopScrollThreshold: { + type: 'number', + default: 50, + }, + }, + }; + } +); + +/** + * 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 data attributes to saved button markup + */ +addFilter( + 'blocks.getSaveContent.extraProps', + 'ls-plugin/back-to-top-save-props', + ( extraProps, blockType, attributes ) => { + if ( blockType.name !== 'core/button' || ! attributes.isBackToTop ) { + return extraProps; + } + + const classes = [ extraProps.className, 'is-back-to-top' ] + .filter( Boolean ) + .join( ' ' ); + + return { + ...extraProps, + className: classes, + 'data-back-to-top-mode': attributes.backToTopPositionMode || 'scroll', + }; + } +); diff --git a/src/plugins/back-to-top/style.css b/src/plugins/back-to-top/style.css new file mode 100644 index 0000000..3cd0049 --- /dev/null +++ b/src/plugins/back-to-top/style.css @@ -0,0 +1,41 @@ +/** + * Back to Top Button Styles + * Minimal styling for Back to Top variation of core/button + * Handles positioning modes and visibility transitions + */ +/* Back to Top button specific positioning */ +.wp-block-button.is-back-to-top { + /* Fixed mode (sticky to footer) */ + /* Smooth transitions for hover state */ +} +.wp-block-button.is-back-to-top[data-back-to-top-mode=fixed] { + position: fixed; + bottom: 2rem; + 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 0.2s ease-in-out; +} +.wp-block-button.is-back-to-top .wp-block-button__link:hover { + transform: translateY(-2px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); +} +.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; + } +} +/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/src/plugins/back-to-top/style.css.map b/src/plugins/back-to-top/style.css.map new file mode 100644 index 0000000..9d3620c --- /dev/null +++ b/src/plugins/back-to-top/style.css.map @@ -0,0 +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 diff --git a/src/plugins/back-to-top/style.scss b/src/plugins/back-to-top/style.scss new file mode 100644 index 0000000..c751b07 --- /dev/null +++ b/src/plugins/back-to-top/style.scss @@ -0,0 +1,44 @@ +/** + * Back to Top Button Styles + * Minimal styling for Back to Top variation of core/button + * Handles positioning modes and visibility transitions + */ + +/* Back to Top button specific positioning */ +.wp-block-button.is-back-to-top { + /* Fixed mode (sticky to footer) */ + &[data-back-to-top-mode="fixed"] { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 40; + + @media ( max-width: 640px ) { + bottom: 1rem; + right: 1rem; + } + } + + /* Smooth transitions for hover state */ + .wp-block-button__link { + transition: all 0.2s ease-in-out; + + &:hover { + transform: translateY( -2px ); + box-shadow: 0 4px 6px -1px rgba( 0, 0, 0, 0.1 ), + 0 2px 4px -2px rgba( 0, 0, 0, 0.1 ); + } + + &:active { + transform: translateY( 0 ); + } + + @media ( prefers-reduced-motion: reduce ) { + transition: none; + + &:hover { + transform: none; + } + } + } +} diff --git a/src/plugins/back-to-top/view.js b/src/plugins/back-to-top/view.js new file mode 100644 index 0000000..17548c7 --- /dev/null +++ b/src/plugins/back-to-top/view.js @@ -0,0 +1,123 @@ +/** + * Smooth scroll utility with back-to-top support and anchor link smooth scrolling. + * Works with core/button Back to Top variation. + * Accessible, performant, and supports prefers-reduced-motion. + */ + +( function () { + // Check if the browser prefers reduced motion + const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ).matches; + + + // Calculate offset for sticky headers + const getStickyHeaderOffset = () => { + const header = document.querySelector( 'header[sticky="true"], [data-sticky="true"], .is-sticky' ); + if ( ! header ) return 0; + + const rect = header.getBoundingClientRect(); + return Math.max( 0, rect.height + 20 ); // Add 20px padding + }; + + // Smooth scroll to target element or position + const smoothScrollTo = ( target, duration = 600 ) => { + if ( prefersReducedMotion ) { + // For users who prefer reduced motion, use instant scroll + if ( typeof target === 'number' ) { + window.scrollTo( 0, target ); + } else if ( target instanceof HTMLElement ) { + target.scrollIntoView(); + } + return; + } + + const startY = window.scrollY; + const endY = + typeof target === 'number' + ? target + : target.getBoundingClientRect().top + startY - getStickyHeaderOffset(); + const distance = endY - startY; + const startTime = performance.now(); + + const easeInOutCubic = ( progress ) => { + return progress < 0.5 + ? 4 * progress * progress * progress + : ( progress - 1 ) * ( 2 * progress - 2 ) * ( 2 * progress - 2 ) + 1; + }; + + const step = ( currentTime ) => { + const elapsed = currentTime - startTime; + const progress = Math.min( elapsed / duration, 1 ); + const currentY = startY + distance * easeInOutCubic( progress ); + + window.scrollTo( 0, currentY ); + + if ( progress < 1 ) { + requestAnimationFrame( step ); + } + }; + + requestAnimationFrame( step ); + }; + + // Initialize smooth scrolling for anchor links + const initAnchorLinks = () => { + document.addEventListener( 'click', ( e ) => { + const link = e.target.closest( 'a[href*="#"]' ); + if ( ! link || link.hostname !== window.location.hostname || link.pathname !== window.location.pathname ) return; + + const href = link.getAttribute( 'href' ); + const hash = href.substring( href.indexOf( '#' ) ); + + // Skip if it's just a hash or empty + if ( hash === '#' || hash === '' ) return; + + // Check if the target exists + const target = document.querySelector( hash ); + if ( ! target ) return; + + e.preventDefault(); + smoothScrollTo( target ); + + // Update URL without triggering scroll + window.history.pushState( null, '', hash ); + } ); + }; + + // Initialize back-to-top functionality + const initBackToTop = () => { + const wrappers = document.querySelectorAll( + '.wp-block-button.is-back-to-top' + ); + + wrappers.forEach( ( wrapper ) => { + // Find the inner link or button + const link = wrapper.querySelector( '.wp-block-button__link' ); + if ( ! link ) return; + + link.addEventListener( 'click', ( e ) => { + e.preventDefault(); + smoothScrollTo( 0, prefersReducedMotion ? 0 : 600 ); + } ); + } ); + }; + + // DOM ready check + const ready = ( callback ) => { + if ( + document.readyState === 'loading' || + document.readyState === 'interactive' + ) { + document.addEventListener( 'DOMContentLoaded', callback ); + } else { + callback(); + } + }; + + // Initialize on DOM ready + ready( () => { + initAnchorLinks(); + initBackToTop(); + } ); +} )(); diff --git a/src/plugins/button-icon/icons.js b/src/plugins/button-icon/icons.js new file mode 100644 index 0000000..b0cab69 --- /dev/null +++ b/src/plugins/button-icon/icons.js @@ -0,0 +1,109 @@ +import { Path, SVG } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +function ArrowRightIcon() { + return ( + + + + ); +} + +function ArrowLeftIcon() { + return ( + + + + ); +} + +function ChevronRightIcon() { + return ( + + + + ); +} + +function ArrowUpIcon() { + return ( + + + + ); +} + +function ArrowDownIcon() { + return ( + + + + ); +} + +function ExternalIcon() { + return ( + + + + ); +} + +export const BUTTON_ICONS = [ + { + label: __( 'Arrow Right', 'ls-plugin' ), + value: 'arrow-right', + char: '→', + icon: , + }, + { + label: __( 'Arrow Left', 'ls-plugin' ), + value: 'arrow-left', + char: '←', + icon: , + }, + { + label: __( 'Chevron Right', 'ls-plugin' ), + value: 'chevron-right', + char: '›', + icon: , + }, + { + label: __( 'Arrow Up', 'ls-plugin' ), + value: 'arrow-up', + char: '↑', + icon: , + }, + { + label: __( 'Arrow Down', 'ls-plugin' ), + value: 'arrow-down', + char: '↓', + icon: , + }, + { + label: __( 'External Link', 'ls-plugin' ), + value: 'external', + char: '↗', + icon: , + }, +]; + +export function getButtonIconChar( iconSlug ) { + const icon = BUTTON_ICONS.find( ( item ) => item.value === iconSlug ); + return icon ? icon.char : ''; +} diff --git a/src/plugins/button-icon/index.js b/src/plugins/button-icon/index.js new file mode 100644 index 0000000..e066269 --- /dev/null +++ b/src/plugins/button-icon/index.js @@ -0,0 +1,159 @@ +import { addFilter } from '@wordpress/hooks'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { InspectorControls } from '@wordpress/block-editor'; +import { Button, PanelBody, PanelRow, ToggleControl, __experimentalGrid as Grid } from '@wordpress/components'; +import { Fragment } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +import { BUTTON_ICONS } from './icons.js'; + +function joinClasses( ...classNames ) { + return classNames.filter( Boolean ).join( ' ' ); +} + +function addAttributes( settings ) { + if ( 'core/button' !== settings.name ) { + return settings; + } + + return { + ...settings, + attributes: { + ...settings.attributes, + lsButtonIcon: { + type: 'string', + }, + lsButtonIconPositionLeft: { + type: 'boolean', + default: false, + }, + }, + }; +} + +addFilter( + 'blocks.registerBlockType', + 'ls-plugin/button-icons/add-attributes', + addAttributes +); + +function addInspectorControls( BlockEdit ) { + return ( props ) => { + if ( 'core/button' !== props.name ) { + return ; + } + + const { attributes, setAttributes } = props; + const { lsButtonIcon, lsButtonIconPositionLeft } = attributes; + + return ( + + + + + + + { BUTTON_ICONS.map( ( icon ) => ( + + ) ) } + + + { lsButtonIcon && ( + + + setAttributes( { + lsButtonIconPositionLeft: + ! lsButtonIconPositionLeft, + } ) + } + /> + + ) } + + + + ); + }; +} + +addFilter( + 'editor.BlockEdit', + 'ls-plugin/button-icons/add-inspector-controls', + addInspectorControls +); + +const addClasses = createHigherOrderComponent( ( BlockListBlock ) => { + return ( props ) => { + const { name, attributes } = props; + + if ( 'core/button' !== name || ! attributes?.lsButtonIcon ) { + return ; + } + + const classes = joinClasses( + props.className, + 'has-ls-button-icon', + attributes.lsButtonIcon + ? `has-ls-button-icon-${ attributes.lsButtonIcon }` + : '', + attributes.lsButtonIconPositionLeft + ? 'has-ls-button-icon-position-left' + : '' + ); + + return ; + }; +}, 'addClasses' ); + +addFilter( + 'editor.BlockListBlock', + 'ls-plugin/button-icons/add-classes', + addClasses +); + +function addSaveProps( extraProps, blockType, attributes ) { + if ( 'core/button' !== blockType.name || ! attributes?.lsButtonIcon ) { + return extraProps; + } + + return { + ...extraProps, + className: joinClasses( + extraProps.className, + 'has-ls-button-icon', + attributes.lsButtonIcon + ? `has-ls-button-icon-${ attributes.lsButtonIcon }` + : '', + attributes.lsButtonIconPositionLeft + ? 'has-ls-button-icon-position-left' + : '' + ), + }; +} + +addFilter( + 'blocks.getSaveContent.extraProps', + 'ls-plugin/button-icons/add-save-props', + addSaveProps +); diff --git a/src/plugins/button-icon/style.css b/src/plugins/button-icon/style.css new file mode 100644 index 0000000..7917eb2 --- /dev/null +++ b/src/plugins/button-icon/style.css @@ -0,0 +1,65 @@ +@charset "UTF-8"; +.wp-block-button.has-ls-button-icon .wp-block-button__link { + display: inline-flex; + align-items: center; + position: relative; +} + +.wp-block-button.has-ls-button-icon .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link::before { + color: var(--ls-button-fill-icon-colour, currentColor); + display: grid; + font-size: var(--ls-button-fill-icon-font-size); + font-weight: 700; + inset-block: var(--ls-button-fill-icon-block-inset); + line-height: 1; + place-items: center; + position: absolute; + transition: color 0.5s cubic-bezier(0.4, 0, 0.2, 1); + width: var(--ls-button-fill-icon-well-size); +} + +.wp-block-button.has-ls-button-icon .wp-block-button__link::after { + content: "→"; + inset-inline-end: var(--ls-button-fill-icon-inline-end); +} + +.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "→"; + inset-inline-start: var(--ls-button-fill-icon-inline-end); +} + +.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link::after { + content: none; +} + +.wp-block-button.has-ls-button-icon-arrow-right .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-arrow-right.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "→" !important; +} + +.wp-block-button.has-ls-button-icon-arrow-left .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-arrow-left.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "←" !important; +} + +.wp-block-button.has-ls-button-icon-chevron-right .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-chevron-right.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "›" !important; +} + +.wp-block-button.has-ls-button-icon-arrow-up .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-arrow-up.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "↑" !important; +} + +.wp-block-button.has-ls-button-icon-arrow-down .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-arrow-down.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "↓" !important; +} + +.wp-block-button.has-ls-button-icon-external .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-external.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "↗" !important; +} +/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/src/plugins/button-icon/style.css.map b/src/plugins/button-icon/style.css.map new file mode 100644 index 0000000..ce250e9 --- /dev/null +++ b/src/plugins/button-icon/style.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["style.css","style.scss"],"names":[],"mappings":"AAAA,gBAAgB;ACAhB;EACC,oBAAA;EACA,mBAAA;EACA,kBAAA;ADED;;ACCA;;EAEC,sDAAA;EACA,aAAA;EACA,+CAAA;EACA,gBAAA;EACA,mDAAA;EACA,cAAA;EACA,mBAAA;EACA,kBAAA;EACA,mDAAA;EACA,2CAAA;ADED;;ACCA;EACC,YAAA;EACA,uDAAA;ADED;;ACCA;EACC,YAAA;EACA,yDAAA;ADED;;ACCA;EACC,aAAA;ADED;;ACCA;;EAEC,uBAAA;ADED;;ACCA;;EAEC,uBAAA;ADED;;ACCA;;EAEC,uBAAA;ADED;;ACCA;;EAEC,uBAAA;ADED;;ACCA;;EAEC,uBAAA;ADED;;ACCA;;EAEC,uBAAA;ADED","file":"style.css"} \ No newline at end of file diff --git a/src/plugins/button-icon/style.scss b/src/plugins/button-icon/style.scss new file mode 100644 index 0000000..1223f93 --- /dev/null +++ b/src/plugins/button-icon/style.scss @@ -0,0 +1,63 @@ +.wp-block-button.has-ls-button-icon .wp-block-button__link { + display: inline-flex; + align-items: center; + position: relative; +} + +.wp-block-button.has-ls-button-icon .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link::before { + color: var(--ls-button-fill-icon-colour, currentColor); + display: grid; + font-size: var(--ls-button-fill-icon-font-size); + font-weight: 700; + inset-block: var(--ls-button-fill-icon-block-inset); + line-height: 1; + place-items: center; + position: absolute; + transition: color .5s cubic-bezier(.4, 0, .2, 1); + width: var(--ls-button-fill-icon-well-size); +} + +.wp-block-button.has-ls-button-icon .wp-block-button__link::after { + content: "\2192"; + inset-inline-end: var(--ls-button-fill-icon-inline-end); +} + +.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "\2192"; + inset-inline-start: var(--ls-button-fill-icon-inline-end); +} + +.wp-block-button.has-ls-button-icon.has-ls-button-icon-position-left .wp-block-button__link::after { + content: none; +} + +.wp-block-button.has-ls-button-icon-arrow-right .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-arrow-right.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "→" !important; +} + +.wp-block-button.has-ls-button-icon-arrow-left .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-arrow-left.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "←" !important; +} + +.wp-block-button.has-ls-button-icon-chevron-right .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-chevron-right.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "›" !important; +} + +.wp-block-button.has-ls-button-icon-arrow-up .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-arrow-up.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "↑" !important; +} + +.wp-block-button.has-ls-button-icon-arrow-down .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-arrow-down.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "↓" !important; +} + +.wp-block-button.has-ls-button-icon-external .wp-block-button__link::after, +.wp-block-button.has-ls-button-icon-external.has-ls-button-icon-position-left .wp-block-button__link::before { + content: "↗" !important; +} diff --git a/webpack.config.cjs b/webpack.config.cjs index c60621c..4bfcd00 100644 --- a/webpack.config.cjs +++ b/webpack.config.cjs @@ -6,6 +6,11 @@ module.exports = { ...defaultConfig, entry: () => ( { ...( typeof defaultConfig.entry === 'function' ? defaultConfig.entry() : defaultConfig.entry ), + 'js/button-icon': path.resolve( process.cwd(), 'src/plugins/button-icon', 'index.js' ), + 'css/button-icon': path.resolve( process.cwd(), 'src/plugins/button-icon', 'style.scss' ), + 'js/back-to-top': path.resolve( process.cwd(), 'src/plugins/back-to-top', 'index.js' ), + 'js/back-to-top-view': path.resolve( process.cwd(), 'src/plugins/back-to-top', 'view.js' ), + 'css/back-to-top': path.resolve( process.cwd(), 'src/plugins/back-to-top', 'style.scss' ), 'js/style-switcher': path.resolve( process.cwd(), 'src/js', 'style-switcher.js' ), } ), output: {