Number Counter

over than

50

Customers in this year

over than

120

orders per day

20

years in industry

block.json

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "wlc/number-counter",
    "title": "Number Counter",
    "category": "theme",
    "icon": "dashboard",
    "description": "Number Counter",
    "keywords": [ "number", "counter" ],
    "version": "1.0.0",
    "textdomain": "WLC",
    "supports": {
        "renaming": true,
        "interactivity": true,
        "align": true,
        "anchor": true,
        "color": {
            "gradients": true
        },
        "shadow": true,
        "spacing": {
            "margin": true,
            "padding": true
        },
        "typography": true
    },
    "attributes": {
        "startValue": {
            "type": "string",
            "default": "0"
        },
        "endValue": {
            "type": "string",
            "source": "attribute",
            "attribute": "data-raw-value",
            "selector": "span.value",
            "default": "0"
        },
        "prefix": {
            "type": "string",
            "source": "text",
            "selector": "span.prefix"
        },
        "suffix": {
            "type": "string",
            "source": "text",
            "selector": "span.suffix"
        },
        "textAbove": {
            "type": "string",
            "source": "text",
            "selector": "p.text-above"
        },
        "textBelow": {
            "type": "string",
            "source": "text",
            "selector": "p.text-below"
        },
        "duration": {
            "type" : "string",
            "default": "5000"
        },
        "easing": {
            "enum": [ "linear", "easeIn", "easeInOut", "easeOut" ],
            "default": "linear"
        },
        "thousandsSeparator": {
            "enum": [".", ",", " ", "" ],
            "default": ""
        },
        "decimalsSeparator": {
            "enum": [",", "." ],
            "default": "."
        },
        "textAlign": {
            "type": "string",
            "default": "center"
        },
        "styles": {
            "type": "object",
            "default": {
                "textAbove": {
                    "fontSize": "1rem",
                    "lineHeight": "1.5",
                    "fontStyle": "normal"
                },
                "prefix": {
                    "fontSize": "2rem",
                    "lineHeight": "1.5",
                    "fontStyle": "normal"
                },
                "value": {
                    "fontSize": "2rem",
                    "lineHeight": "1.5",
                    "fontWeight": "700",
                    "fontStyle": "normal"
                },
                "suffix": {
                    "fontSize": "2rem",
                    "lineHeight": "1.5",
                    "fontStyle": "normal"
                },
                "textBelow": {
                    "fontSize": "1rem",
                    "lineHeight": "1.5",
                    "fontStyle": "normal"
                }
            }
        }
    },
    "editorScript": "file:./index.js",
    "viewScriptModule": "file:./view.js",
    "style": "file:./style-index.css"
}

edit.js

import { 
    useBlockProps,
    InspectorControls,
    BlockControls,
    AlignmentControl,
    RichText,
    FontSizePicker,
 } from '@wordpress/block-editor'
import { 
    PanelBody,
    TextControl,
    RadioControl,
    SelectControl,
    ColorPalette,
    Dropdown,
    Button,
    ColorIndicator,
} from '@wordpress/components'
import { select } from '@wordpress/data';
import { __ } from '@wordpress/i18n'
import { addSeparators } from './helpers'

const edit = ( { attributes, setAttributes } ) => {
    
    const { 
        startValue, endValue,
        prefix, suffix,
        textAbove, textBelow,
        thousandsSeparator, decimalsSeparator,
        duration, easing,
        textAlign,
        styles,
    } = attributes;
    const blockProps = useBlockProps({ className: 'wlc-number-counter'});

    const globalSettings = select( "core/editor" ).getEditorSettings();

    const set = ( attr ) => {
        const prop = Object.keys(attr).pop();
        return value => setAttributes( { [prop]: value } )
    }

    const displayedValue = addSeparators( endValue, thousandsSeparator, decimalsSeparator);

    const SettingsContainer = props => {

        const style = styles[props.el]
        const set = (prop,value) => {
            style[prop] = value
            setAttributes( { styles: { ...styles, style } } )
        }

        return (
            <>
                <FontSizePicker
                    value={ style.fontSize }
                    onChange={ value => set('fontSize', value ) }
                />
                <TextControl
                    label={ __('Line Height', 'WLC' ) }
                    value={ style.lineHeight }
                    type="number"
                    step="0.1"
                    onChange={ value => set('lineHeight', value ) }
	            />
                <SelectControl
                    label={ __('Font weight', 'WLC' ) }
                    value={ style.fontWeight }
                    options={ [
                        { label: __('Thin', 'WLC'), value: 100},
                        { label: __('Extra Light', 'WLC'), value: 200},
                        { label: __('Light', 'WLC'), value: 300},
                        { label: __('Regular', 'WLC'), value: 400},
                        { label: __('Medium', 'WLC'), value: 500},
                        { label: __('Semi Bold', 'WLC'), value: 600},
                        { label: __('Bold', 'WLC'), value: 700},
                        { label: __('Extra Bold', 'WLC'), value: 800},
                    ] }
                    onChange={ value => set('fontWeight', value ) }
                />
                <RadioControl
                    label={ __('Font style', 'WLC' ) }
                    selected={ style.fontStyle }
                    options = { [
                        { label: __('Normal', 'WLC' ), value: "normal" },
                        { label: __('Italic', 'WLC' ), value: "italic" },
                    ]}
                    onChange={ value => set('fontStyle', value ) }
                />
            </>
        )
    }

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __('Values', 'WLC' ) }>
                    <TextControl
                        label={ __('Start Value', 'WLC' ) }
                        type="number"
                        value={ startValue }
                        onChange={ set({ startValue }) }
                    />
                    <TextControl
                        label={ __('End Value', 'WLC' ) }
                        type="number"
                        value={ endValue }
                        onChange={ set({ endValue }) }
                    />
                    <TextControl
                        label={ __('Prefix', 'WLC' ) }
                        value={ prefix }
                        onChange={ set({ prefix }) }
                    />
                    <TextControl
                        label={ __('Suffix', 'WLC' ) }
                        value={ suffix }
                        onChange={ set({ suffix }) }
                    />
                </PanelBody>
                <PanelBody title={ __('Number formatting', 'WLC' ) } initialOpen={ false }>
                    <RadioControl
                        label={ __('Displayed decimal separator', 'WLC' ) }
                        selected={ decimalsSeparator }
                        options = { [
                            { label: __('Dot', 'WLC' ), value: "." },
                            { label: __('Comma', 'WLC' ), value: "," }
                        ]}
                        onChange={ set({ decimalsSeparator }) }
                    />
                    <RadioControl
                        label={ __('Displayed thousands separator', 'WLC' ) }
                        selected={ thousandsSeparator }
                        options = { [
                            { label: __('Dot', 'WLC' ), value: "." },
                            { label: __('Comma', 'WLC' ), value: "," },
                            { label: __('Space', 'WLC' ), value: " " },
                            { label: __('None', 'WLC' ), value: "" }
                        ]}
                        onChange={ set({ thousandsSeparator }) }
                    />
                </PanelBody>
                <PanelBody title={ __('Animation', 'WLC' ) }  initialOpen={ false }>
                    <TextControl
                        label={ __('Duration', 'WLC' ) }
                        type="number"
                        value={ duration }
                        onChange={ set({ duration }) }
                    />
                    <RadioControl
                        label={ __('Easing', 'WLC' ) }
                        selected={ easing }
                        options = { [
                            { label: __('Linear', 'WLC' ), value: "linear" },
                            { label: __('EaseIn', 'WLC' ), value: "easeIn" },
                            { label: __('EaseInOut', 'WLC' ), value: "easeInOut" },
                            { label: __('EaseOut', 'WLC' ), value: "easeOut" }
                        ]}
                        onChange={ set({ easing }) }
                    />
                </PanelBody>
            </InspectorControls>
            <InspectorControls group="styles">
                <PanelBody title={ __('Text above value', 'WLC' ) } initialOpen={ false }>
                    <SettingsContainer el="textAbove"/>
                    <Dropdown
                        popoverProps={ { placement: 'left-start' } }
                        renderToggle={ ( { isOpen, onToggle } ) => (
                        <Button
                            onClick={ onToggle }
                            aria-expanded={ isOpen }
                        >
                            <ColorIndicator colorValue={styles.textAbove.color} />&nbsp;{ __( 'Select color', 'WLC' ) }
                        </Button>
                        ) }
                        renderContent={ () =>
                            <ColorPalette
                                value={ styles.textAbove.color }
                                colors= { globalSettings.colors }
                                onChange={ value => setAttributes( {
                                    styles: { ...styles, textAbove: { ...styles.textAbove, color: value } }
                                } ) }
                                enableAlpha
                                clearable
                                defaultValue="#000"
                            />
                        }
                    />
                </PanelBody>
                <PanelBody title={ __('Value Prefix', 'WLC' ) } initialOpen={ false }>
                    <SettingsContainer el="prefix"/>
                    <Dropdown
                        popoverProps={ { placement: 'left-start' } }
                        renderToggle={ ( { isOpen, onToggle } ) => (
                        <Button
                            onClick={ onToggle }
                            aria-expanded={ isOpen }
                        >
                            <ColorIndicator colorValue={styles.prefix.color} />&nbsp;{ __( 'Select color', 'WLC' ) }
                        </Button>
                        ) }
                        renderContent={ () =>
                            <ColorPalette
                                value={ styles.prefix.color }
                                colors= { globalSettings.colors }
                                onChange={ value => setAttributes( {
                                    styles: { ...styles, prefix: { ...styles.prefix, color: value } }
                                } ) }
                                enableAlpha
                                clearable
                                defaultValue="#000"
                            />
                        }
                    />
                </PanelBody>
                <PanelBody title={ __('Value', 'WLC' ) } initialOpen={ false }>
                    <SettingsContainer el="value"/>
                    <Dropdown
                        popoverProps={ { placement: 'left-start' } }
                        renderToggle={ ( { isOpen, onToggle } ) => (
                        <Button
                            onClick={ onToggle }
                            aria-expanded={ isOpen }
                        >
                            <ColorIndicator colorValue={styles.value.color} />&nbsp;{ __( 'Select color', 'WLC' ) }
                        </Button>
                        ) }
                        renderContent={ () =>
                            <ColorPalette
                                value={ styles.value.color }
                                colors= { globalSettings.colors }
                                onChange={ value => setAttributes( {
                                    styles: { ...styles, value: { ...styles.value, color: value } }
                                } ) }
                                enableAlpha
                                clearable
                                defaultValue="#000"
                            />
                        }
                    />
                </PanelBody>
                <PanelBody title={ __('Value Suffix', 'WLC' ) } initialOpen={ false }>
                    <SettingsContainer el="suffix"/>
                    <Dropdown
                        popoverProps={ { placement: 'left-start' } }
                        renderToggle={ ( { isOpen, onToggle } ) => (
                        <Button
                            onClick={ onToggle }
                            aria-expanded={ isOpen }
                        >
                            <ColorIndicator colorValue={styles.suffix.color} />&nbsp;{ __( 'Select color', 'WLC' ) }
                        </Button>
                        ) }
                        renderContent={ () =>
                            <ColorPalette
                                value={ styles.suffix.color }
                                colors= { globalSettings.colors }
                                onChange={ value => setAttributes( {
                                    styles: { ...styles, suffix: { ...styles.suffix, color: value } }
                                } ) }
                                enableAlpha
                                clearable
                                defaultValue="#000"
                            />
                        }
                    />
                </PanelBody>
                <PanelBody title={ __('Text below value', 'WLC' ) } initialOpen={ false }>
                    <SettingsContainer el="textBelow"/>
                    <Dropdown
                        popoverProps={ { placement: 'left-start' } }
                        renderToggle={ ( { isOpen, onToggle } ) => (
                        <Button
                            onClick={ onToggle }
                            aria-expanded={ isOpen }
                        >
                            <ColorIndicator colorValue={styles.textBelow.color} />&nbsp;{ __( 'Select color', 'WLC' ) }
                        </Button>
                        ) }
                        renderContent={ () =>
                            <ColorPalette
                                value={ styles.textBelow.color }
                                colors= { globalSettings.colors }
                                onChange={ value => setAttributes( {
                                    styles: { ...styles, textBelow: { ...styles.textBelow, color: value } }
                                } ) }
                                enableAlpha
                                clearable
                                defaultValue="#000"
                            />
                        }
                    />
                </PanelBody>
            </InspectorControls>
            <BlockControls>
                <AlignmentControl
                    value={ textAlign }
                    onChange={ set({ textAlign }) }
                />
            </BlockControls>
            <div
                { ...blockProps }
            >
                <RichText
                    tagName="p"
                    className="text-above"
                    style={ { ...styles.textAbove, textAlign } }
                    value={ textAbove }
                    onChange={ set({ textAbove }) }
                    placeholder={ __('Text Above', 'WLC' ) }
                />
                <p style={ { textAlign } }>
                    <span className="prefix" style={ { ...styles.prefix, textAlign  } }>{ prefix }</span>
                    <span className="value" style={ {...styles.value} }>{ displayedValue }</span>
                    <span className="suffix" style={ { ...styles.suffix, textAlign } }>{ suffix }</span>
                </p>
                <RichText
                    tagName="p"
                    className="text-below"
                    style={ { ...styles.textBelow, textAlign  } }
                    value={ textBelow }
                    onChange={ set({ textBelow }) }
                    placeholder={ __('Text Below', 'WLC' ) }
                />
            </div>
        </>
    )
}

export default edit;

helpers.js

export const addSeparators = ( num, thousandsSep, decimalsSep ) => {
    const numStr = num.toString();
    const parts = numStr.split(".");
    const regex = /(\d)(?=(\d{3})+(?!\d))/g;
    
    parts[0] = parts[0].replace(regex, `$1${thousandsSep}`)
    
    return parts.join(decimalsSep);
}

const easings = {
    linear: time => time,
    easeIn: time => Math.pow(time,3),
    easeInOut: time => time < 0.5 ? 4 * Math.pow(time,3) : 1 - Math.pow(-2 * time + 2, 3) / 2,
    easeOut: time => 1 - Math.pow(1 - time, 3),
}

export const easing = type => easings[type];

index.js

import { registerBlockType } from "@wordpress/blocks";
import metadata from './block.json';
import save from "./save";
import edit from './edit';
import './style.css';

registerBlockType( metadata, {
    edit: edit,
    save: save,
});

save.js

import { useBlockProps, RichText } from '@wordpress/block-editor'
import { __ } from '@wordpress/i18n'
import { addSeparators } from './helpers'

const save = ( { attributes } ) => {
    
    const { 
        startValue, endValue,
        prefix, suffix,
        thousandsSeparator, decimalsSeparator,
        textAbove, textBelow,
        duration, easing,
        textAlign,
        styles
    } = attributes;
    const blockProps = useBlockProps.save({ className: 'wlc-number-counter'});

    const context = JSON.stringify( {
        start: parseFloat( startValue ),
        end: parseFloat( endValue ),
        thousandsSeparator,
        decimalsSeparator,
        duration: parseInt( duration ),
        easing,
        isAnimating: false,
    } );

    const displayedValue = addSeparators( endValue, thousandsSeparator, decimalsSeparator);

    return (
        <div
            { ...blockProps }
            data-wp-interactive="wlc/number-counter"
            data-wp-run="callbacks.animateNumber"
            data-wp-context={ context }
        >
            <RichText.Content tagName="p" className="text-above" value={ textAbove } style={ {...styles.textAbove, textAlign } }/>
            <p style={ { textAlign } }>
                <span className="prefix" style={ {...styles.prefix, textAlign } }>{ prefix }</span>
                <span className="value" data-raw-value={ endValue } style={ {...styles.value } }>{ displayedValue }</span>
                <span className="suffix" style={ {...styles.suffix, textAlign } }>{ suffix }</span>
            </p>
            <RichText.Content tagName="p" className="text-below" value={ textBelow }  style={ {...styles.textBelow, textAlign } }/>
        </div>
    )
}

export default save;

style.css

.wlc-number-counter {
    & > p {
        @apply m-0 p-0;
    }
}

view.js

import { getElement, getContext, store, useState, useEffect } from '@wordpress/interactivity'
import { addSeparators, easing } from './helpers';

const useInView = () => {
    const [ inView, setInView ] = useState(false);

    useEffect( () => {
        const { ref } = getElement();
        const observer = new IntersectionObserver( ( [ entry ] ) => {
                setInView( entry.isIntersecting );
        } );
        observer.observe( ref );
        return () => ref && observer.unobserve( ref );
    }, []);

    return inView;
}

const animate = () => {
    const context = getContext();
    context.isAnimating = true;

    const { ref } = getElement();
    
    const duration = context.duration;
    const start = performance.now();
    const range = context.end - context.start;

    const timing = easing(context.easing);

    const nextFrame = () => {
        const now = performance.now();
        const delta = Math.min((now - start) / duration, 1);
        const current = context.start + ( timing(delta) * range ) ;
        const value = addSeparators(current.toFixed(2), context.thousandsSeparator, context.decimalsSeparator );
        ref.querySelector('.value').textContent = value;
        if ( delta < 1 ) {
            window.requestAnimationFrame(nextFrame);
        }
    }
    window.requestAnimationFrame(nextFrame);
}

store( 'wlc/number-counter', {
    
    callbacks: {
        animateNumber: () => {
            const inView = useInView();
            useEffect( () => {
                const context = getContext();
                if ( inView && !context.isAnimating ) {
                    animate();
                }
            })
        }
    }
} )