block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "wlc/progress-bar",
"title": "Progress Bar",
"category": "widgets",
"icon": "admin-generic",
"description": "A block to display progress bar",
"keywords": [
"progress",
"bar"
],
"supports": {
"interactivity": true
},
"attributes": {
"type": {
"type": "string",
"default": "line"
},
"title": {
"type": "string"
},
"counterValue": {
"type": "number",
"default": 60
},
"showCounter": {
"type": "boolean",
"default": false
},
"width": {
"type": "number",
"default": 300
},
"height": {
"type": "number"
},
"circleBorderWidth": {
"type": "number",
"default": 15
},
"titleColor": {
"type": "string",
"default": "#000"
},
"counterTextColor": {
"type": "string",
"default": "#000"
},
"background": {
"type": "string",
"default": "#F4F4F4"
},
"fill": {
"type": "string",
"default": "#75E2C2"
},
"animate": {
"type": "boolean",
"default": false
}
},
"version": "1.0.0",
"textdomain": "WLC",
"editorStyle": "file:./index.css",
"editorScript": "file:./index.js",
"viewScriptModule": "file:/view.js",
"style": "file:./style-index.css"
}
edit.js
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
InspectorControls,
RichText,
} from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
ToggleControl,
ColorPicker,
RangeControl,
} from '@wordpress/components';
const edit = ( { attributes, setAttributes } ) => {
const { type, title, counterValue, showCounter, width, height, circleBorderWidth, titleColor, counterTextColor, background, fill, animate } = attributes;
const blockProps = useBlockProps();
const circleRightRotation = counterValue <= 50 ? ( counterValue / 50 ) * 180 : 180;
const circleLeftRotation = counterValue > 50 ? 180 + ( ( counterValue - 50 ) / 50 ) * 180 : 0;
return (
<div { ...blockProps }>
<InspectorControls>
<PanelBody title={ __( 'Progress Bar Settings', 'WLC' ) }>
<SelectControl
label={ __( 'Type', 'WLC' ) }
value={ type }
options={ [
{ label: __( 'Line', 'WLC' ), value: 'line' },
{ label: __( 'Circle', 'WLC' ), value: 'circle' },
{ label: __( 'Circle Fill', 'WLC' ), value: 'circle-fill' },
{ label: __( 'Box', 'WLC' ), value: 'box' },
] }
onChange={ value => setAttributes( { type: value } ) }
/>
<RangeControl
label={ __( 'Counter Value (%)', 'WLC' ) }
value={ counterValue }
onChange={ ( value ) => setAttributes( { counterValue: value } ) }
min={ 0 }
max={ 100 }
/>
<ToggleControl
label={ __( 'Show Counter', 'WLC' ) }
checked={ showCounter }
onChange={ value => setAttributes( { showCounter: value } ) }
/>
<ToggleControl
label={ __( 'Animate', 'WLC' ) }
checked={ animate }
onChange={ value => setAttributes( { animate: value } ) }
/>
{ ( type !== 'line' ) && (
<RangeControl
label={ __( 'Width (px)', 'WLC' ) }
value={ width }
onChange={ ( value ) => setAttributes( { width: value } ) }
min={ 0 }
max={ 500 }
/>
) }
{ ( type == 'line' || type == 'box' ) && (
<RangeControl
label={ __( 'Height (px)', 'WLC' ) }
value={ height }
initialPosition={ type == 'line' ? 15 : 300 }
onChange={ ( value ) => setAttributes( { height: value } ) }
min={ 0 }
max={ type == 'line' ? 50 : 500 }
/>
) }
{ ( type == 'circle' ) && (
<RangeControl
label={ __( 'Border Width (px)', 'WLC' ) }
value={ circleBorderWidth }
onChange={ ( value ) => setAttributes( { circleBorderWidth: value } ) }
min={ 1 }
max={ 50 }
/>
) }
</PanelBody>
<PanelBody title={ __( 'Colors', 'WLC' ) } initialOpen={ false }>
<div className="components-base-control">
<div className="components-base-control__field">
<label className="components-base-control__label">
{ __( 'Title Color', 'WLC' ) }
</label>
<ColorPicker
color={ titleColor }
onChangeComplete={ ( value ) => setAttributes( { titleColor: value.hex } ) }
/>
</div>
</div>
{ showCounter && (
<div className="components-base-control">
<div className="components-base-control__field">
<label className="components-base-control__label">
{ __( 'Counter Text Color', 'WLC' ) }
</label>
<ColorPicker
color={ counterTextColor }
onChangeComplete={ ( value ) => setAttributes( { counterTextColor: value.hex } ) }
/>
</div>
</div>
) }
<div className="components-base-control">
<div className="components-base-control__field">
<label className="components-base-control__label">
{ __( 'Background', 'WLC' ) }
</label>
<ColorPicker
color={ background }
onChangeComplete={ ( value ) => setAttributes( { background: value.hex } ) }
/>
</div>
</div>
<div className="components-base-control">
<div className="components-base-control__field">
<label className="components-base-control__label">
{ __( 'Fill', 'WLC' ) }
</label>
<ColorPicker
color={ fill }
onChangeComplete={ ( value ) => setAttributes( { fill: value.hex } ) }
/>
</div>
</div>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
{ type == 'line' && (
<div className="progressbar-line">
<div className="progressbar-line__header">
<RichText
tagName="p"
placeholder={ __( 'Type title...', 'WLC' ) }
className="progressbar-line__title"
value={ title }
onChange={ ( value ) => setAttributes( { title: value } ) }
style={ { color: titleColor } }
/>
{ showCounter && (
<p className="progressbar-line__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
) }
</div>
<div className="progressbar-line__inner" style={ { height: `${height}px`, backgroundColor: background } }>
<div className="progressbar-line__fill" style={ { backgroundColor: fill, width: counterValue + `%` } }></div>
</div>
</div>
) }
{ ( type == 'circle' ) && (
<div className="progressbar-circle" style={ { width: `${width}px`, height: `${width}px` } }>
<div className="progressbar-circle__fill" style={ counterValue > 50 ? { clipPath: 'inset(0px)' } : {} }>
<div
className="progressbar-circle__fill-left"
style={ { borderColor: fill, borderWidth: circleBorderWidth, transform: `rotate(${circleLeftRotation}deg)` } }
></div>
<div
className="progressbar-circle__fill-right"
style={ { borderColor: fill, borderWidth: circleBorderWidth, transform: `rotate(${circleRightRotation}deg)` } }
></div>
</div>
<div className="progressbar-circle__shape" style={ { borderColor: background, borderWidth: circleBorderWidth } }></div>
<div className="progressbar-circle__content">
<RichText
tagName="p"
placeholder={ __( 'Type title...', 'WLC' ) }
className="progressbar-circle__title"
value={ title }
onChange={ ( value ) => setAttributes( { title: value } ) }
style={ { color: titleColor } }
/>
{ showCounter && (
<p className="progressbar-circle__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
) }
</div>
</div>
) }
{ ( type == 'circle-fill' ) && (
<div className="progressbar-circle circle-fill" style={ { width: `${width}px`, height: `${width}px` } }>
<div className="progressbar-circle__fill" style={ counterValue > 50 ? { clipPath: 'inset(0px)' } : {} }>
<div
className="progressbar-circle__fill-left"
style={ { backgroundColor: fill, transform: `rotate(${circleLeftRotation}deg)` } }
></div>
<div
className="progressbar-circle__fill-right"
style={ { backgroundColor: fill, transform: `rotate(${circleRightRotation}deg)` } }
></div>
</div>
<div className="progressbar-circle__shape" style={ { backgroundColor: background } }></div>
<div className="progressbar-circle__content">
<RichText
tagName="p"
placeholder={ __( 'Type title...', 'WLC' ) }
className="progressbar-circle__title"
value={ title }
onChange={ ( value ) => setAttributes( { title: value } ) }
style={ { color: titleColor } }
/>
{ showCounter && (
<p className="progressbar-circle__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
) }
</div>
</div>
) }
{ type == 'box' && (
<div className="progressbar-box">
<div className="progressbar-box__inner" style={ { width: `${width}px`, height: `${height}px`, backgroundColor: background } }>
<div className="progressbar-box__fill" style={ { backgroundColor: fill, height: counterValue + `%` } }></div>
<div className="progressbar-box__content">
{ showCounter && (
<p className="progressbar-box__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
) }
</div>
</div>
<RichText
tagName="p"
placeholder={ __( 'Type title...', 'WLC' ) }
className="progressbar-box__title"
value={ title }
onChange={ ( value ) => setAttributes( { title: value } ) }
style={ { color: titleColor } }
/>
</div>
) }
</div>
</div>
);
}
export default edit;
index.js
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import './style.scss';
import edit from './edit';
import save from './save';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit,
save,
} );
save.js
/**
* WordPress dependencies
*/
import {
useBlockProps,
RichText,
} from '@wordpress/block-editor';
const save = ( { attributes } ) => {
const { type, title, counterValue, showCounter, width, height, circleBorderWidth, titleColor, counterTextColor, background, fill, animate } = attributes;
const blockProps = useBlockProps.save();
const circleRightRotation = counterValue <= 50 ? ( counterValue / 50 ) * 180 : 180;
const circleLeftRotation = counterValue > 50 ? 180 + ( ( counterValue - 50 ) / 50 ) * 180 : 0;
const maxAngle = Math.max(circleLeftRotation,circleRightRotation);
return (
<div { ...blockProps }
data-wp-interactive="wlc/progress-bar"
data-wp-run="callbacks.animateBar"
data-wp-context={ JSON.stringify({ animate, isAnimating: false }) }
>
{ type == 'line' && (
<div className="progressbar-line">
<div className="progressbar-line__header">
<RichText.Content
tagName="p"
className="progressbar-line__title"
value={ title }
style={ { color: titleColor } }
/>
{ showCounter && (
<p className="progressbar-line__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
) }
</div>
<div className="progressbar-line__inner" style={ { height: `${height}px`, backgroundColor: background } }>
<div className="progressbar-line__fill" style={ { backgroundColor: fill, '--width': `${counterValue}%` } }></div>
</div>
</div>
) }
{ ( type == 'circle' ) && (
<div className="progressbar-circle" style={ { width: `${width}px`, height: `${width}px` } }>
<div className="progressbar-circle__fill" style={ counterValue > 50 ? { clipPath: 'inset(0px)', '--angle': `${maxAngle}deg` } : {'--angle': `${maxAngle}deg`} }>
<div
className="progressbar-circle__fill-left"
style={ { borderColor: fill, borderWidth: circleBorderWidth, transform: `rotate(${circleLeftRotation}deg)` } }
></div>
<div
className="progressbar-circle__fill-right"
style={ { borderColor: fill, borderWidth: circleBorderWidth, transform: `rotate(${circleRightRotation}deg)` } }
></div>
</div>
<div className="progressbar-circle__shape" style={ { borderColor: background, borderWidth: circleBorderWidth } }></div>
<div className="progressbar-circle__content">
{ title && (
<RichText.Content
tagName="p"
className="progressbar-circle__title"
value={ title }
style={ { color: titleColor } }
/>
) }
{ showCounter && (
<p className="progressbar-circle__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
) }
</div>
</div>
) }
{ ( type == 'circle-fill' ) && (
<div className="progressbar-circle circle-fill" style={ { width: `${width}px`, height: `${width}px` } }>
<div className="progressbar-circle__fill" style={ counterValue > 50 ? { clipPath: 'inset(0px)', '--angle': `${maxAngle}deg` } : {'--angle': `${maxAngle}deg`} }>
<div
className="progressbar-circle__fill-left"
style={ { backgroundColor: fill, transform: `rotate(${circleLeftRotation}deg)` } }
></div>
<div
className="progressbar-circle__fill-right"
style={ { backgroundColor: fill, transform: `rotate(${circleRightRotation}deg)` } }
></div>
</div>
<div className="progressbar-circle__shape" style={ { backgroundColor: background } }></div>
<div className="progressbar-circle__content">
{ title && (
<RichText.Content
tagName="p"
className="progressbar-circle__title"
value={ title }
style={ { color: titleColor } }
/>
) }
{ showCounter && (
<p className="progressbar-circle__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
) }
</div>
</div>
) }
{ type == 'box' && (
<div className="progressbar-box" style={ { width: `${width}px` } }>
<div className="progressbar-box__inner" style={ { height: `${height}px`, backgroundColor: background } }>
<div className="progressbar-box__fill" style={ { backgroundColor: fill, '--height': `${counterValue}%` } }></div>
<div className="progressbar-box__content">
{ showCounter && (
<p className="progressbar-box__value" style={ { color: counterTextColor } }>{ counterValue }%</p>
) }
</div>
</div>
<RichText.Content
tagName="p"
className="progressbar-box__title"
value={ title }
style={ { color: titleColor } }
/>
</div>
) }
</div>
);
}
export default save;
style.scss
@property --angle {
syntax: "<angle>";
inherits: true;
initial-value: 0deg;
}
.progressbar-line {
&__header {
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
&__title {
font-size: 18px;
margin: 0;
}
&__value {
font-size: 18px;
margin: 0;
}
&__inner {
height: 15px;
background: #f4f4f4;
position: relative;
}
&__fill {
top: 0;
left: 0;
bottom: 0;
background: #000;
position: absolute;
width: var(--width);
&.animate {
width: 0;
}
&.animating {
animation: 3s width both;
}
}
@keyframes width {
from {
width: 0;
}
to {
width: var(--width);
}
}
}
.progressbar-circle {
width: 250px;
height: 250px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
&.circle-fill {
.progressbar-circle__fill-left,
.progressbar-circle__fill-right,
.progressbar-circle__shape {
border-width: 0;
}
.progressbar-circle__value {
font-size: 26px;
font-weight: bold;
margin: 0;
}
}
&__fill {
clip-path: inset(0 0 0 50%);
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
position: absolute;
&-left,
&-right {
clip-path: inset(0 50% 0 0);
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 15px solid #000;
border-radius: 50%;
box-sizing: border-box;
position: absolute;
}
&-left {
transform: rotate(0deg);
}
&-right {
transform: rotate(180deg);
}
&.animate {
mask-image: conic-gradient( #000 0deg, #000 0deg, transparent 0deg, transparent 360deg );
}
&.animating {
mask-image: conic-gradient( #000 0deg, #000 var(--angle), transparent var(--angle), transparent 360deg );
animation: 3s angle both;
}
@keyframes angle {
from {
--angle: 0deg;
}
}
}
&__shape {
width: 100%;
height: 100%;
border: 15px solid #eee;
border-radius: 50%;
box-sizing: border-box;
}
&__content {
text-align: center;
position: absolute;
}
}
.progressbar-box {
&__inner {
width: 100%;
height: 310px;
background: #f4f4f4;
margin: 0 auto;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
&__fill {
right: 0;
left: 0;
bottom: 0;
background: #000;
position: absolute;
height: var(--height);
&.animate {
height: 0;
}
&.animating {
animation: 3s height both;
}
@keyframes height{
from {
height: 0;
}
to {
height: var(--height);
}
}
}
&__content {
position: relative;
}
&__value {
font-size: 26px;
font-weight: bold;
margin: 0;
}
&__title {
text-align: center;
margin-top: 15px;
}
}
view.js
import { getElement, getContext, store, useState, useEffect } from '@wordpress/interactivity'
const useAnimation = () => {
const [ animation, setAnimation ] = useState(false);
useEffect( () => {
const context = getContext();
const { ref } = getElement();
ref.querySelector('[class$="__fill"]').classList.toggle('animate', context.animate );
setAnimation( context.animate );
}, []);
return animation;
}
const useInView = () => {
const [ inView, setInView ] = useState(false);
useEffect( () => {
const { ref } = getElement();
const observer = new IntersectionObserver( ( [ entry ] ) => {
setInView( entry.isIntersecting );
}, { threshold: 1 } );
observer.observe( ref );
return () => ref && observer.unobserve( ref );
}, []);
return inView;
}
const animate = () => {
const context = getContext();
context.isAnimating = true;
const { ref } = getElement();
ref.querySelector('[class ~= "animate"]').classList.add('animating');
}
store( 'wlc/progress-bar', {
callbacks: {
animateBar: () => {
const withAnimation = useAnimation();
const inView = useInView();
useEffect( () => {
const context = getContext();
if ( withAnimation && inView && !context.isAnimating ) {
animate();
}
})
}
}
} )