Skip to content

Instantly share code, notes, and snippets.

@Scribblerockerz
Last active March 20, 2022 14:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Scribblerockerz/87e92e2c01a0510f48402771cc263367 to your computer and use it in GitHub Desktop.
Save Scribblerockerz/87e92e2c01a0510f48402771cc263367 to your computer and use it in GitHub Desktop.
Scroll Sync Directive for vue3
<template>
<!-- Sync only vertically -->
<div class="scrollable-area" v-scroll-sync:timeline.y />
<!-- Sync only horizontally -->
<div class="scrollable-area" v-scroll-sync:timeline.x />
<!-- Sync in all directions -->
<div class="scrollable-area" v-scroll-sync:timeline />
<!-- Declare 3 different channels to sync to -->
<div class="scrollable-area" v-scroll-sync:sidebar />
<div class="scrollable-area" v-scroll-sync:paneA />
<div class="scrollable-area" v-scroll-sync:paneB />
<!-- Default channel = 'default' to sync to -->
<div class="scrollable-area" v-scroll-sync />
<div class="scrollable-area" v-scroll-sync:default />
</template>
import { ObjectDirective, VNode } from 'vue';
export interface ScrollSyncEvent extends Event {
detail: {
source: number;
channel: string;
original: Event;
};
}
declare global {
interface WindowEventMap {
'scroll-sync': ScrollSyncEvent;
}
}
type ScrollUpdate = {
top: number | null;
left: number | null;
};
function calculatePositions(el: HTMLElement) {
const scrollDistanceTop = el.scrollHeight - el.clientHeight;
const percentTop = scrollDistanceTop === 0 ? 0 : (el.scrollTop * 100) / scrollDistanceTop;
const scrollDistanceLeft = el.scrollWidth - el.clientWidth;
const percentLeft = scrollDistanceLeft === 0 ? 0 : (el.scrollLeft * 100) / scrollDistanceLeft;
return {
left: percentLeft,
top: percentTop,
distanceTop: scrollDistanceTop,
distanceLeft: scrollDistanceLeft,
};
}
function applyScrollUpdate(el: HTMLElement, update: ScrollUpdate) {
const { distanceTop, distanceLeft } = calculatePositions(el);
if (update.top !== null) {
el.scrollTop = distanceTop * (update.top / 100);
}
if (update.left !== null) {
el.scrollLeft = distanceLeft * (update.left / 100);
}
}
// Instance trackers
let ssid = 0;
const channelUpdates: {
[key: string]: number;
} = {};
const latestChannelUpdates: {
[key: string]: {
top: number;
left: number;
};
} = {};
interface ScrollSyncVNode extends VNode {
__channelUpdates: number;
}
/**
* Scroll Sync Directive
*/
export const scrollSync: ObjectDirective<any, boolean> = {
mounted(el: HTMLElement, binding, vnode) {
const channel = binding.arg || 'default';
const id = ssid++;
const syncY = binding.modifiers.y || !binding.modifiers.x;
const syncX = binding.modifiers.x || !binding.modifiers.y;
if (!channelUpdates[channel]) {
channelUpdates[channel] = 0;
}
// Keep track of the last applied update counter
(vnode as ScrollSyncVNode).__channelUpdates = channelUpdates[channel];
// Restore position for channels with events
const lastUpdate = latestChannelUpdates[channel];
if (lastUpdate) {
applyScrollUpdate(el, {
top: syncY ? lastUpdate.top : null,
left: syncX ? lastUpdate.left : null,
});
}
el.addEventListener('scroll', (e: Event) => {
const isBehindUpdates = channelUpdates[channel] !== (vnode as ScrollSyncVNode).__channelUpdates;
// Scroll event was initialized by current instance
if (!isBehindUpdates) {
// increase update count
channelUpdates[channel]++;
}
// Reset channel counter, to prevent overflow (╯°□°)╯︵ ┻━┻
if (channelUpdates[channel] > 100) {
channelUpdates[channel] = 0;
}
(vnode as ScrollSyncVNode).__channelUpdates = channelUpdates[channel];
// This scroll event is a sideffect of our change, exit
if (isBehindUpdates) {
return;
}
// Update the latest channel update, for future instances
const { top, left } = calculatePositions(el);
latestChannelUpdates[channel] = {
top,
left,
};
window.dispatchEvent(
new CustomEvent('scroll-sync', {
detail: {
source: id,
channel: channel,
original: e,
},
})
);
});
window.addEventListener('scroll-sync', (e: ScrollSyncEvent) => {
// we don't care about our own events, or from other channels
if (channel !== e.detail.channel || e.detail.source === id) {
return;
}
const { left, top } = calculatePositions(e.detail.original.target as HTMLElement);
const scrollUpdate: ScrollUpdate = {
top: syncY ? top : null,
left: syncX ? left : null,
};
window.requestAnimationFrame(() => {
applyScrollUpdate(el, scrollUpdate);
});
});
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment