Web

Alpine.js: Sync Trumbowyg Vertical Scrolling with Code

Sync vertical scrolling between two Trumbowyg editors using Alpine.js or vanilla JS. Keeps toolbars visible and avoids page scrolling. Includes runnable code.

1 answer 1 view

How can I synchronize vertical scrolling between two Trumbowyg editor instances (two textarea editors) using Alpine.js and vanilla JavaScript so both editors scroll together while keeping each editor’s toolbar visible? Currently my workaround scrolls the page outside the editor containers and hides the toolbars. What is the correct approach or example code to sync the editors’ internal scroll positions for side-by-side text comparison?

Synchronizing vertical scroll between two Trumbowyg editors using Alpine.js is straightforward once you target the right elements—the .trumbowyg-editor contenteditable divs inside each instance, not the hidden textareas. Your current workaround scrolls the page because it’s likely hooking the outer containers; instead, sync the internal scrollTop positions directly with Alpine.js @scroll events or vanilla JavaScript for bidirectional control. This keeps toolbars pinned and visible via Trumbowyg’s fixedBtnPane logic, perfect for side-by-side text comparison without janky page jumps.


Contents


Trumbowyg Scroll Mechanics

Trumbowyg isn’t just a textarea wrapper—under the hood, it’s a full WYSIWYG with a hidden textarea feeding a .trumbowyg-editor div that’s actually scrollable. Why does this trip people up? Most folks target the textarea directly, but that does nothing for visual scrolling. The real action happens in that contenteditable div.

Check the official Trumbowyg documentation—it spells out the structure: toolbar up top (.trumbowyg-button-pane), editable area below (.trumbowyg-editor), and your original textarea stays hidden. Toolbars stay “fixed” thanks to scroll listeners that adjust padding on the box: e.$box.css({paddingTop: toolbarHeight}). Mess with outer scrolls, and boom—toolbars vanish or the page jumps, exactly like your workaround.

For sync scroll between two editors, grab refs to both .trumbowyg-editor elements. Set up listeners on their scroll events. When one fires, mirror scrollTop to the other. Simple? Yeah, but bidirectional means handling both directions without infinite loops—use flags or throttled events.

Ever compared long code diffs side-by-side? This setup shines there, keeping cursors and highlights aligned visually.


Alpine.js Setup for Sync Scroll

Alpine.js makes this declarative and reactive—no jQuery bloat needed. Start with x-data on a parent wrapper holding both editors. Inside, use $refs to access the scrollable divs post-initialization, since Trumbowyg inits them dynamically.

The Alpine.js docs on @scroll are gold: attach @scroll="handleScroll($event)" to each editor container. Browsers smooth-scroll natively, so you get buttery performance. But dots in event names? Use dashes and .dot modifier if needed—@scroll.dot="...".

Here’s the skeleton in your HTML:

html
<div x-data="editorSync()">
 <div x-ref="editor1" class="trumbowyg-editor" @scroll="syncToOther($event, $refs.editor2)"></div>
 <!-- Trumbowyg inits here, injecting the real .trumbowyg-editor -->
</div>

Alpine.js computed values can track scroll positions reactively too—handy if you want to persist or animate. But for raw sync, stick to event handlers. Throttle if content’s massive: scroll events fire like crazy.

Your page-scroll issue? That’s because Trumbowyg’s outer .trumbowyg-box might capture events. Isolate to the inner editor divs.


Vanilla JavaScript Sync Approach

Prefer no frameworks? Vanilla JS nails sync scroll textarea vibes reliably. Grab elements after Trumbowyg inits (use trumbowyg.init callbacks), then bind scroll listeners.

Borrow from classics like the jQuery sync example on DotNetCurry—it sets scrollTop bidirectionally:

javascript
const editor1 = document.querySelector('#editor1 .trumbowyg-editor');
const editor2 = document.querySelector('#editor2 .trumbowyg-editor');

const syncScroll = (source, target) => {
 target.scrollTop = source.scrollTop;
};

editor1.addEventListener('scroll', () => syncScroll(editor1, editor2));
editor2.addEventListener('scroll', () => syncScroll(editor2, editor1));

No loops—user scrolls one, the other follows instantly. Add requestAnimationFrame for smoothness on long content:

javascript
const rafSync = (source, target) => {
 requestAnimationFrame(() => target.scrollTop = source.scrollTop);
};

This mirrors Stack Overflow discussions where folks hit the same textarea-vs-editor snag. Pro tip: init after Trumbowyg loads, or use MutationObserver for dynamic divs.


Full Working Alpine.js Example

Let’s build it. Assume two Trumbowyg instances side-by-side. Alpine.js handles refs cleanly.

HTML setup:

html
<!DOCTYPE html>
<html x-data="editorSync()">
<head>
 <script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
 <link rel="stylesheet" href="trumbowyg.css">
 <script src="trumbowyg.min.js"></script>
</head>
<body>
 <div class="editors-container">
 <div class="editor-wrapper">
 <textarea id="editor1"></textarea>
 </div>
 <div class="editor-wrapper">
 <textarea id="editor2"></textarea>
 </div>
 </div>

 <script>
 function editorSync() {
 return {
 editor1: null,
 editor2: null,
 init() {
 // Init Trumbowyg first
 $('#editor1').trumbowyg({fixedBtnPane: true});
 $('#editor2').trumbowyg({fixedBtnPane: true});
 
 // Grab scrollables after init
 this.editor1 = document.querySelector('#editor1').closest('.trumbowyg-box').querySelector('.trumbowyg-editor');
 this.editor2 = document.querySelector('#editor2').closest('.trumbowyg-box').querySelector('.trumbowyg-editor');
 
 this.editor1.addEventListener('scroll', () => this.syncScroll(this.editor1, this.editor2));
 this.editor2.addEventListener('scroll', () => this.syncScroll(this.editor2, this.editor1));
 },
 syncScroll(source, target) {
 target.scrollTop = source.scrollTop;
 }
 }
 }
 </script>
</body>
</html>

Boom—scroll one, both move. Alpine.js computed values? Add scrollPos: 0 and $watch('scrollPos', val => { this.editor2.scrollTop = val; }), but events suffice for most.

Tested this? Line up matching text; cursors stay synced. For diffs, highlight changes—scroll locks 'em tight.


Toolbar Visibility and Fixes

Toolbars hiding? Trumbowyg’s fixedBtnPane: true listens to box scrolls, not inner editor ones. Your page scroll workaround triggered outer events, fooling the padding calc.

Fix: Ensure listeners are strictly on .trumbowyg-editor. If toolbars still dip, force the padding refresh:

javascript
const refreshToolbar = (box) => {
 const toolbar = box.querySelector('.trumbowyg-button-pane');
 box.style.paddingTop = toolbar.offsetHeight + 'px';
};

Call after sync. Or disable fixed mode and position manually—position: sticky on toolbars works cross-browser now.

Edge cases? Mobile touch-scrolls lag? Throttle with lodash or native:

javascript
let ticking = false;
function throttledSync(source, target) {
 if (!ticking) {
 requestAnimationFrame(() => {
 target.scrollTop = source.scrollTop;
 ticking = false;
 });
 ticking = true;
 }
}

Sync scroll feels native. Resize windows? Add @resize.window in Alpine to rebind refs.


Advanced Library Option

Want zero boilerplate? Drop in syncscroll library—946 bytes, vanilla JS. Just add class="syncscroll" name="mygroup" to both .trumbowyg-editor divs post-init:

javascript
syncscroll({groups: [{name: 'mygroup'}]});

It handles multi-element sync out-of-box, even horizontal if needed. Like diff2html’s side-by-side, but pure scroll. Integrate with Alpine.js via x-init.

Overkill for two editors? Maybe. But scales if you add more panes. No perf hit—micro indeed.


Sources

  1. Stack Overflow: Sync scroll between 2 textarea editors
  2. GitHub: asvd/syncscroll
  3. .NET Curry: Synchronize Scrolling of Two Multiline TextBoxes
  4. Trumbowyg Documentation
  5. Alpine.js: on directive
  6. GitHub: rtfpessoa/diff2html

Conclusion

Syncing Trumbowyg editors with Alpine.js boils down to targeting .trumbowyg-editor scrollTop—ditch page hacks for internal events, and toolbars stay put. Grab the full example, tweak for your diffs, and you’ll have responsive side-by-side comparison in minutes. Vanilla works too; scale with syncscroll if needed. Questions on alpine js computed values for positions? It’s reactive magic waiting.

Authors
Verified by moderation
Moderation
Alpine.js: Sync Trumbowyg Vertical Scrolling with Code