Multiple Widget Instances Fix

User
<style>
@font-face {
	src: url('https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2');
	font-family:'Compressa VF';
	font-style: normal;
}


#fit h1 {
	font-family:'Compressa VF';
	text-rendering: optimizeSpeed;
	color: #b6e925;
/*	width: 100%;*/
	user-select: none;
	line-height: 0.8em;
	margin: 0 auto;
	text-transform: uppercase;
	font-weight: 100;
	text-align: center;
/*	width: 100vw;*/
}
#fit h1 span {
	transform: translateY(-10px);
	user-select: none;
	font-family:'Compressa VF';

}
#fit h1.flex {
	display: flex;
	justify-content: space-between;
	
}
#fit h1.stroke span {
	position: relative;
	color: #211D26;
	line-height: inherit;
}
#fit h1.stroke span:after {
	content: attr(data-char);
	-webkit-text-stroke-width: 3px;
	-webkit-text-stroke-color: #FE6730;
	position: absolute;
	left: 0;
	line-height: inherit;
	color: transparent;
	z-index: -1;
}
</style>
<script>

</script>
<script>
var maxDist;
var mouse = { x: 0, y: 0 };
var cursor = {
    x: window.innerWidth,
    y: window.innerHeight
};

Math.dist = function(a, b) {
    var dx = b.x - a.x;
    var dy = b.y - a.y;
    return Math.sqrt(Math.pow(dx, 2), Math.pow(dy, 2));
}

window.addEventListener("mousemove", function(e) {
    cursor.x = e.clientX;
    cursor.y = e.clientY;
});

window.addEventListener("touchmove", function(e) {
    var t = e.touches[0];
    cursor.x = t.clientX;
    cursor.y = t.clientY;
}, {
    passive: false
});

var Char = function(container, char) {
    var span = document.createElement("span");
    span.setAttribute('data-char', char);
    span.innerText = char;
    container.appendChild(span);
    this.getDist = function() {
        this.pos = span.getBoundingClientRect();
        return Math.dist(mouse, {
            x: this.pos.x + (this.pos.width / 1.75),
            y: this.pos.y
        });
    }
    this.getAttr = function(dist, min, max) {
        var wght = max - Math.abs((max * dist / maxDist));
        return Math.max(min, wght + min);
    }
    this.update = function(args) {
        var dist = this.getDist();
        this.wdth = args.wdth ? ~~this.getAttr(dist, 5, 200) : 100;
        this.wght = args.wght ? ~~this.getAttr(dist, 100, 800) : 400;
        this.alpha = args.alpha ? this.getAttr(dist, 0, 1).toFixed(2) : 1;
        this.ital = args.ital ? this.getAttr(dist, 0, 1).toFixed(2) : 0;
        this.draw();
    }
    this.draw = function() {
        var style = "";
        style += "opacity: " + this.alpha + ";";
        style += "font-variation-settings: 'wght' " + this.wght + ", 'wdth' " + this.wdth + ", 'ital' " + this.ital + ";";
        span.style = style;
    }
    return this;
}

var VFont = function() {
    this.scale = false;
    this.flex = true;
    this.alpha = false;
    this.stroke = false;
    this.width = true;
    this.weight = true;
    this.italic = true;
    var title, str, chars = [];

    this.init = function() {
        title = document.getElementById("title");
        str = title.innerText;
        title.innerHTML = "";
        for (var i = 0; i < str.length; i++) {
            var _char = new Char(title, str[i]);
            chars.push(_char);
        }
        this.set();
        window.addEventListener("resize", this.setSize.bind(this));
    }

    this.set = function() {
        title.className = "";
        title.className += this.flex ? " flex" : "";
        title.className += this.stroke ? " stroke" : "";
        this.setSize();
    }

    this.setSize = function() {
        var mainDiv = document.getElementById("fit"); // assuming the outer div has an id "main"
        var fontSize = mainDiv.clientWidth / (str.length / 2);
        title.style = "font-size: " + fontSize + "px;";
        if (this.scale) {
            var scaleY = (window.innerHeight / title.getBoundingClientRect().height).toFixed(2);
            var lineHeight = scaleY * 0.8;
            title.style = "font-size: " + fontSize + "px; transform: scale(1," + scaleY + "); line-height: " + lineHeight + "em;"
        }
    }

    this.animate = function() {
        mouse.x += (cursor.x - mouse.x) / 20;
        mouse.y += (cursor.y - mouse.y) / 20;
        requestAnimationFrame(this.animate.bind(this));
        this.render();
    }

    this.render = function() {
        maxDist = title.getBoundingClientRect().width / 2;
        for (var i = 0; i < chars.length; i++) {
            chars[i].update({
                wght: this.weight,
                wdth: this.width,
                ital: this.italic,
                alpha: this.alpha
            });
        }
    }
    this.init();
    this.animate();
    return this;
}

var txt = new VFont();

// Start animation
txt.animate();
</script>
Assistant
You're right β€” the script and rendering logic you’re using in your `Elementor_Variable_Font_Widget` is **not scoped for multiple instances**. Here's why it fails on multiple widgets on the same page:

### πŸ” **Issue Breakdown**

1. **ID Conflicts**:
– The HTML uses static IDs: `id="fit"` and `id="title"` β€” which means **multiple widgets will reuse the same ID**, leading to unpredictable DOM behavior and broken animations.

2. **JavaScript is not modular**:
– The JS initializes the animation for a single `#title` inside `#fit`, and does not loop through multiple instances.

### βœ… **Solution Overview**

To make this work with **multiple widget instances on the same page**, you need to:

– Use **unique IDs** or **classes with `data-*` attributes**.
– Adjust the **JavaScript** to loop over all instances and apply the animation individually.

### βœ… **Updated Widget Render Method**

Here’s how you can update your `render()` function (and JS) to handle multiple instances correctly:

protected function render() {
    $settings = $this->get_settings_for_display();
    $text = $settings['content'];
    $effect_class = esc_attr($settings['font_effect']);

    $unique_id = 'variable-font-' . $this->get_id();

    echo '<div class="variable-font-wrapper ' . $effect_class . '" id="' . esc_attr($unique_id) . '" data-uid="' . esc_attr($unique_id) . '">';
    echo '  <h1 class="variable-font-title">' . esc_html($text) . '</h1>';
    echo '</div>';
    ?>

    <style>
        @font-face {
            src: url('https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2');
            font-family:'Compressa VF';
            font-style: normal;
        }

        .variable-font-title {
            font-family:'Compressa VF';
            text-rendering: optimizeSpeed;
            color: #b6e925;
            user-select: none;
            line-height: 0.8em;
            margin: 0 auto;
            text-transform: uppercase;
            font-weight: 100;
            text-align: center;
        }
        .variable-font-title span {
            transform: translateY(-10px);
            user-select: none;
            font-family:'Compressa VF';
        }
        .variable-font-title.flex {
            display: flex;
            justify-content: space-between;
        }
        .variable-font-title.stroke span {
            position: relative;
            color: #211D26;
            line-height: inherit;
        }
        .variable-font-title.stroke span:after {
            content: attr(data-char);
            -webkit-text-stroke-width: 3px;
            -webkit-text-stroke-color: #FE6730;
            position: absolute;
            left: 0;
            line-height: inherit;
            color: transparent;
            z-index: -1;
        }
    </style>

    <script>
    (function(){
        const wrapper = document.getElementById("<?php echo esc_js($unique_id); ?>");
        const title = wrapper.querySelector(".variable-font-title");
        const str = title.innerText;
        const chars = [];
        let mouse = { x: 0, y: 0 };
        let cursor = { x: window.innerWidth, y: window.innerHeight };
        let maxDist;

        title.innerHTML = '';
        [...str].forEach(char => {
            const span = document.createElement("span");
            span.setAttribute('data-char', char);
            span.innerText = char;
            title.appendChild(span);
            chars.push(span);
        });

        function getDist(span) {
            const rect = span.getBoundingClientRect();
            return Math.hypot((rect.left + rect.width / 2) - mouse.x, (rect.top + rect.height / 2) - mouse.y);
        }

        function getAttr(dist, min, max) {
            const val = max - Math.abs((max * dist / maxDist));
            return Math.max(min, val + min);
        }

        function updateChar(span) {
            const dist = getDist(span);
            const wdth = getAttr(dist, 5, 200);
            const wght = getAttr(dist, 100, 800);
            const ital = getAttr(dist, 0, 1).toFixed(2);
            span.style.fontVariationSettings = `'wght' ${~~wght}, 'wdth' ${~~wdth}, 'ital' ${ital}`;
        }

        function render() {
            maxDist = title.getBoundingClientRect().width / 2;
            chars.forEach(span => updateChar(span));
            requestAnimationFrame(render);
        }

        window.addEventListener("mousemove", function(e) {
            cursor.x = e.clientX;
            cursor.y = e.clientY;
        });

        window.addEventListener("touchmove", function(e) {
            const t = e.touches[0];
            cursor.x = t.clientX;
            cursor.y = t.clientY;
        }, { passive: false });

        function animateMouse() {
            mouse.x += (cursor.x - mouse.x) / 20;
            mouse.y += (cursor.y - mouse.y) / 20;
            requestAnimationFrame(animateMouse);
        }

        animateMouse();
        render();
    })();
    </script>
    <?php
}

### 🎯 Key Improvements

– Each widget instance gets a **unique `ID`** via `$this->get_id()`.
– The JS is **self-contained** in an IIFE (`(function(){…})()`) to avoid global variable conflicts.
– It loops over the characters of the specific widget's text only.
– It tracks mouse movement globally, but calculates distance per widget instance.

Let me know if you'd like to convert this to use an **external JS file** or hook into Elementor's lifecycle like `elementor/frontend/init`.

User
<style>
@font-face {
	src: url('https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2');
	font-family:'Compressa VF';
	font-style: normal;
}


#fit h1 {
	font-family:'Compressa VF';
	text-rendering: optimizeSpeed;
	color: #b6e925;
/*	width: 100%;*/
	user-select: none;
	line-height: 0.8em;
	margin: 0 auto;
	text-transform: uppercase;
	font-weight: 100;
	text-align: center;
/*	width: 100vw;*/
}
#fit h1 span {
	transform: translateY(-10px);
	user-select: none;
	font-family:'Compressa VF';

}
#fit h1.flex {
	display: flex;
	justify-content: space-between;
	
}
#fit h1.stroke span {
	position: relative;
	color: #211D26;
	line-height: inherit;
}
#fit h1.stroke span:after {
	content: attr(data-char);
	-webkit-text-stroke-width: 3px;
	-webkit-text-stroke-color: #FE6730;
	position: absolute;
	left: 0;
	line-height: inherit;
	color: transparent;
	z-index: -1;
}
</style>
<script>

</script>
<script>
var maxDist;
var mouse = { x: 0, y: 0 };
var cursor = {
    x: window.innerWidth,
    y: window.innerHeight
};

Math.dist = function(a, b) {
    var dx = b.x - a.x;
    var dy = b.y - a.y;
    return Math.sqrt(Math.pow(dx, 2), Math.pow(dy, 2));
}

window.addEventListener("mousemove", function(e) {
    cursor.x = e.clientX;
    cursor.y = e.clientY;
});

window.addEventListener("touchmove", function(e) {
    var t = e.touches[0];
    cursor.x = t.clientX;
    cursor.y = t.clientY;
}, {
    passive: false
});

var Char = function(container, char) {
    var span = document.createElement("span");
    span.setAttribute('data-char', char);
    span.innerText = char;
    container.appendChild(span);
    this.getDist = function() {
        this.pos = span.getBoundingClientRect();
        return Math.dist(mouse, {
            x: this.pos.x + (this.pos.width / 1.75),
            y: this.pos.y
        });
    }
    this.getAttr = function(dist, min, max) {
        var wght = max - Math.abs((max * dist / maxDist));
        return Math.max(min, wght + min);
    }
    this.update = function(args) {
        var dist = this.getDist();
        this.wdth = args.wdth ? ~~this.getAttr(dist, 5, 200) : 100;
        this.wght = args.wght ? ~~this.getAttr(dist, 100, 800) : 400;
        this.alpha = args.alpha ? this.getAttr(dist, 0, 1).toFixed(2) : 1;
        this.ital = args.ital ? this.getAttr(dist, 0, 1).toFixed(2) : 0;
        this.draw();
    }
    this.draw = function() {
        var style = "";
        style += "opacity: " + this.alpha + ";";
        style += "font-variation-settings: 'wght' " + this.wght + ", 'wdth' " + this.wdth + ", 'ital' " + this.ital + ";";
        span.style = style;
    }
    return this;
}

var VFont = function() {
    this.scale = false;
    this.flex = true;
    this.alpha = false;
    this.stroke = false;
    this.width = true;
    this.weight = true;
    this.italic = true;
    var title, str, chars = [];

    this.init = function() {
        title = document.getElementById("title");
        str = title.innerText;
        title.innerHTML = "";
        for (var i = 0; i < str.length; i++) {
            var _char = new Char(title, str[i]);
            chars.push(_char);
        }
        this.set();
        window.addEventListener("resize", this.setSize.bind(this));
    }

    this.set = function() {
        title.className = "";
        title.className += this.flex ? " flex" : "";
        title.className += this.stroke ? " stroke" : "";
        this.setSize();
    }

    this.setSize = function() {
        var mainDiv = document.getElementById("fit"); // assuming the outer div has an id "main"
        var fontSize = mainDiv.clientWidth / (str.length / 2);
        title.style = "font-size: " + fontSize + "px;";
        if (this.scale) {
            var scaleY = (window.innerHeight / title.getBoundingClientRect().height).toFixed(2);
            var lineHeight = scaleY * 0.8;
            title.style = "font-size: " + fontSize + "px; transform: scale(1," + scaleY + "); line-height: " + lineHeight + "em;"
        }
    }

    this.animate = function() {
        mouse.x += (cursor.x - mouse.x) / 20;
        mouse.y += (cursor.y - mouse.y) / 20;
        requestAnimationFrame(this.animate.bind(this));
        this.render();
    }

    this.render = function() {
        maxDist = title.getBoundingClientRect().width / 2;
        for (var i = 0; i < chars.length; i++) {
            chars[i].update({
                wght: this.weight,
                wdth: this.width,
                ital: this.italic,
                alpha: this.alpha
            });
        }
    }
    this.init();
    this.animate();
    return this;
}

var txt = new VFont();

// Start animation
txt.animate();
</script>
Assistant
Thanks for sharing the full code again. Your Elementor widget currently uses hardcoded `id`s (`#fit`, `#title`) and non-scoped JavaScript, which breaks if the widget is used multiple times on the same page.

### βœ… Here's the **corrected version** that:

– Avoids duplicate `id` conflicts
– Makes the JavaScript **instance-safe**
– Works with **multiple widget instances** on a page

### βœ… **Updated `render()` Function**
Replace your `render()` method with this:

protected function render() {
    $settings = $this->get_settings_for_display();
    $text = $settings['content'];
    $effect_class = esc_attr($settings['font_effect']);
    $unique_id = 'variable-font-' . esc_attr($this->get_id()); // Unique ID per widget

    echo '<style>
    @font-face {
        src: url("https://res.cloudinary.com/dr6lvwubh/raw/upload/v1529908256/CompressaPRO-GX.woff2");
        font-family: "Compressa VF";
        font-style: normal;
    }
    #' . $unique_id . ' h1 {
        font-family: "Compressa VF";
        text-rendering: optimizeSpeed;
        color: #b6e925;
        user-select: none;
        line-height: 0.8em;
        margin: 0 auto;
        text-transform: uppercase;
        font-weight: 100;
        text-align: center;
    }
    #' . $unique_id . ' h1 span {
        transform: translateY(-10px);
        user-select: none;
    }
    #' . $unique_id . ' h1.flex {
        display: flex;
        justify-content: space-between;
    }
    #' . $unique_id . ' h1.stroke span {
        position: relative;
        color: #211D26;
        line-height: inherit;
    }
    #' . $unique_id . ' h1.stroke span:after {
        content: attr(data-char);
        -webkit-text-stroke-width: 3px;
        -webkit-text-stroke-color: #FE6730;
        position: absolute;
        left: 0;
        line-height: inherit;
        color: transparent;
        z-index: -1;
    }
    </style>';

    echo "<div id='{$unique_id}' class='variable-font-wrapper {$effect_class}'>";
    echo "  <h1 class='variable-font-title'>{$text}</h1>";
    echo "</div>";

    // Inline script per widget instance
    echo "<script>
    (function(){
        const wrapper = document.getElementById('{$unique_id}');
        if (!wrapper) return;
        const title = wrapper.querySelector('.variable-font-title');
        const str = title.innerText;
        const chars = [];
        title.innerHTML = '';
        for (let i = 0; i < str.length; i++) {
            const span = document.createElement('span');
            span.setAttribute('data-char', str[i]);
            span.innerText = str[i];
            title.appendChild(span);
            chars.push(span);
        }

        let mouse = { x: 0, y: 0 };
        let cursor = { x: window.innerWidth, y: window.innerHeight };
        let maxDist = title.getBoundingClientRect().width / 2;

        window.addEventListener('mousemove', function(e) {
            cursor.x = e.clientX;
            cursor.y = e.clientY;
        });
        window.addEventListener('touchmove', function(e) {
            const t = e.touches[0];
            cursor.x = t.clientX;
            cursor.y = t.clientY;
        }, { passive: false });

        function getDist(span) {
            const rect = span.getBoundingClientRect();
            return Math.hypot((rect.left + rect.width / 2) - mouse.x, (rect.top + rect.height / 2) - mouse.y);
        }

        function getAttr(dist, min, max) {
            const val = max - Math.abs((max * dist / maxDist));
            return Math.max(min, val + min);
        }

        function updateChar(span) {
            const dist = getDist(span);
            const wdth = getAttr(dist, 5, 200);
            const wght = getAttr(dist, 100, 800);
            const ital = getAttr(dist, 0, 1).toFixed(2);
            span.style.fontVariationSettings = `'wght' ${~~wght}, 'wdth' ${~~wdth}, 'ital' ${ital}`;
        }

        function animateMouse() {
            mouse.x += (cursor.x - mouse.x) / 20;
            mouse.y += (cursor.y - mouse.y) / 20;
            requestAnimationFrame(animateMouse);
        }

        function render() {
            maxDist = title.getBoundingClientRect().width / 2;
            chars.forEach(span => updateChar(span));
            requestAnimationFrame(render);
        }

        // Resize handler to update font size
        function setFontSize() {
            const width = wrapper.clientWidth;
            const fontSize = width / (str.length / 2);
            title.style.fontSize = fontSize + 'px';
        }
        setFontSize();
        window.addEventListener('resize', setFontSize);

        // Start animations
        animateMouse();
        render();
    })();
    </script>";
}

### βœ… Final Notes

– **No more `id="fit"` or `id="title"` reuse** β€” everything is scoped by the widget's unique ID.
– JavaScript runs **independently** per instance.
– Fully safe for **multiple widget usage** on the same Elementor page.

Let me know if you want to load the script externally or conditionally enqueue only once per page.

User
unique each elementor widget id
Assistant
To give **each Elementor widget instance a unique ID**, the most reliable and Elementor-native way is to use:

$this->get_id();

This function returns a **unique ID** for each widget on the page, even if it's the same widget type used multiple times.

### βœ… How to Use in Your `render()` Function

Here’s how you assign a unique ID for each widget instance:

$unique_id = 'variable-font-' . esc_attr($this->get_id());

You can then apply this `$unique_id` to your wrapping `div`, like:

echo "<div id='{$unique_id}' class='variable-font-wrapper'>";

And reference it consistently in your “ and “ tags:

echo "<style>
    #{$unique_id} h1 {
        font-family: 'Compressa VF';
        ...
    }
</style>";

echo "<script>
    (function(){
        const wrapper = document.getElementById('{$unique_id}');
        ...
    })();
</script>";

### βœ… Where to Use `get_id()`:

– `id` attributes
– `querySelector`, `getElementById`, etc. in JS
– CSS selectors inside `style` tag or in JS `querySelector`
– Avoid hardcoded `#fit`, `#title` β€” use the unique ID instead

### Example Output in DOM:

<div id="variable-font-abc123" class="variable-font-wrapper">
  <h1 class="variable-font-title">My Animated Text</h1>
</div>

The ID `variable-font-abc123` is unique to this widget instance.

Let me know if you want this to be added as a `data-uid` attribute too for even cleaner JS handling or if you're planning to enqueue the script globally instead of inline.

User
<script>

</script>

include this only once in elementor widget

Assistant
Great question β€” including external scripts like `dat.gui.min.js` **only once** in an Elementor widget (even if it's used multiple times on the same page) requires checking if it has already been enqueued.

### βœ… Best Practice: Use `ElementorPlugin::instance()->frontend->add_render_attribute()`

But for simplicity and full control, here’s how to **conditionally load the script once per page** using a **static variable** inside your widget class:

### βœ… Modify Your Widget Class Like This

class Elementor_Variable_Font_Widget extends ElementorWidget_Base {

    // Static property to track if script was already printed
    protected static $script_loaded = false;

    ...

    protected function render() {
        $settings = $this->get_settings_for_display();
        $unique_id = 'variable-font-' . esc_attr($this->get_id());
        $text = esc_html($settings['content']);

        echo "<div id='{$unique_id}' class='variable-font-wrapper'>";
        echo "  <h1 class='variable-font-title'>{$text}</h1>";
        echo "</div>";

        // βœ… Print this only ONCE
        if ( ! self::$script_loaded ) {
            echo '<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.5/dat.gui.min.js"></script>';
            self::$script_loaded = true;
        }

        // Now continue with widget-specific script
        echo "<script>
        (function(){
            const wrapper = document.getElementById('{$unique_id}');
            if (!wrapper) return;
            const title = wrapper.querySelector('.variable-font-title');
            const str = title.innerText;
            const chars = [];
            title.innerHTML = '';
            for (let i = 0; i < str.length; i++) {
                const span = document.createElement('span');
                span.setAttribute('data-char', str[i]);
                span.innerText = str[i];
                title.appendChild(span);
                chars.push(span);
            }

            let mouse = { x: 0, y: 0 };
            let cursor = { x: window.innerWidth, y: window.innerHeight };
            let maxDist = title.getBoundingClientRect().width / 2;

            window.addEventListener('mousemove', function(e) {
                cursor.x = e.clientX;
                cursor.y = e.clientY;
            });

            window.addEventListener('touchmove', function(e) {
                const t = e.touches[0];
                cursor.x = t.clientX;
                cursor.y = t.clientY;
            }, { passive: false });

            function getDist(span) {
                const rect = span.getBoundingClientRect();
                return Math.hypot((rect.left + rect.width / 2) - mouse.x, (rect.top + rect.height / 2) - mouse.y);
            }

            function getAttr(dist, min, max) {
                const val = max - Math.abs((max * dist / maxDist));
                return Math.max(min, val + min);
            }

            function updateChar(span) {
                const dist = getDist(span);
                const wdth = getAttr(dist, 5, 200);
                const wght = getAttr(dist, 100, 800);
                const ital = getAttr(dist, 0, 1).toFixed(2);
                span.style.fontVariationSettings = `'wght' ${~~wght}, 'wdth' ${~~wdth}, 'ital' ${ital}`;
            }

            function animateMouse() {
                mouse.x += (cursor.x - mouse.x) / 20;
                mouse.y += (cursor.y - mouse.y) / 20;
                requestAnimationFrame(animateMouse);
            }

            function render() {
                maxDist = title.getBoundingClientRect().width / 2;
                chars.forEach(span => updateChar(span));
                requestAnimationFrame(render);
            }

            function setFontSize() {
                const width = wrapper.clientWidth;
                const fontSize = width / (str.length / 2);
                title.style.fontSize = fontSize + 'px';
            }
            setFontSize();
            window.addEventListener('resize', setFontSize);

            animateMouse();
            render();
        })();
        </script>";
    }

}

### πŸ” What's Happening:

– `static $script_loaded` is used to track whether the external script has been printed already.
– It **only prints “ once** β€” even if the widget is used 10 times.
– Elementor renders each widget instance separately, so this is a safe and performant way to manage third-party scripts inline.

Let me know if you’d prefer to register/enqueue it through WordPress properly instead of inline!

Leave a Comment

Your email address will not be published. Required fields are marked *