Skip to content

Instantly share code, notes, and snippets.

@homerjam
Created November 15, 2017 10:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save homerjam/10dd9fc56a5ed816f99d81a22f13d189 to your computer and use it in GitHub Desktop.
Save homerjam/10dd9fc56a5ed816f99d81a22f13d189 to your computer and use it in GitHub Desktop.
Vue plugin for scrollwizardy
/*
eslint no-underscore-dangle: 0, max-len: 0, prefer-destructuring: 0, no-nested-ternary: 0
*/
import TweenMax from 'gsap/TweenMax';
import TimelineMax from 'gsap/TimelineMax';
import * as ScrollWizardry from 'scrollwizardry';
// import * as ScrollWizardry from '../../scrollwizardry/src';
const { Controller, Scene } = ScrollWizardry;
const defaultOptions = {
addIndicators: false,
};
let sceneManager;
class SceneManager {
constructor(options) {
this.options = options;
this._scenes = {};
this._sceneObservers = {};
if (typeof window === 'undefined') {
return;
}
this.controller = new Controller(options);
}
static getTargetElement(element, targetElement) {
if (targetElement) {
if (typeof targetElement === 'string') {
if (/^(parent)$/i.test(targetElement)) {
return element.parentNode;
}
return document.querySelector(targetElement);
}
return targetElement;
}
return element;
}
static parseString(string, triggerElement) {
let number;
const vh = /([0-9]+)vh$/.exec(string);
if (vh) {
number = () => window.innerHeight * (parseFloat(vh[1]) / 100);
}
const percent = /([0-9]+)%$/.exec(string);
if (percent) {
number = () => triggerElement.clientHeight * (parseFloat(percent[1]) / 100);
}
return number;
}
static mounted(element) {
return new Promise((resolve) => {
const observer = new MutationObserver(resolve);
observer.observe(element.parentNode, { childList: true });
});
}
get addIndicators() {
return this.options.addIndicators;
}
_notifySceneObservers(sceneId, ...args) {
args.push(sceneId);
this._sceneObservers[sceneId].forEach((fn) => {
fn(...args);
});
}
addSceneObserver(sceneId, observer) {
if (this._scenes[sceneId]) {
observer(this._scenes[sceneId], sceneId);
return;
}
if (!this._sceneObservers[sceneId]) {
this._sceneObservers[sceneId] = [];
}
this._sceneObservers[sceneId].push(observer);
}
getSceneIds(sceneId) {
const defaultSceneId = Object.keys(this._scenes)[0] || 0;
if (Object.prototype.toString.call(sceneId) === '[object Array]') {
return sceneId;
}
return sceneId && sceneId.length ? [sceneId] : [defaultSceneId];
}
getScene(sceneId) {
sceneId = sceneId || this.getSceneIds(sceneId)[0];
return this._scenes[sceneId];
}
setScene(sceneId, scene) {
this._scenes[sceneId] = scene;
if (this._sceneObservers[sceneId]) {
this._notifySceneObservers(sceneId, scene);
}
}
destroyScene(sceneId) {
const scene = this._scenes[sceneId];
this.controller.removeScene(scene);
scene.destroy(true);
delete this._scenes[sceneId];
}
removeSceneObservers(sceneId, observers = []) {
const observerIds = observers.map(observer => observer.id);
if (this._sceneObservers[sceneId]) {
this._sceneObservers[sceneId] = this._sceneObservers[sceneId].filter(({ id }) => {
if (observerIds.includes(id)) {
return false;
}
return true;
});
}
}
}
const sceneDirective = {
inserted(el, binding) {
const options = binding.value;
const sceneId = sceneManager.getSceneIds(options.sceneId)[0];
if (sceneManager.getScene(sceneId)) {
sceneManager.destroyScene(sceneId);
}
let scene;
let offset = options.offset !== undefined ? options.offset : 0;
let duration = options.duration !== undefined ? options.duration : 0;
const triggerElement = options.triggerElement ? options.triggerElement : el;
const triggerHook = options.triggerHook !== undefined ? options.triggerHook : 0.5;
if (typeof offset === 'string') {
offset = SceneManager.parseString(offset, triggerElement);
}
if (typeof offset === 'function') {
offset = offset();
}
if (typeof duration === 'function') {
duration = duration.bind({
scene,
triggerElement,
offset,
triggerHook: options.triggerHook,
});
}
if (typeof duration === 'string') {
duration = SceneManager.parseString(duration, triggerElement);
}
scene = new Scene({
offset,
duration,
triggerElement,
triggerHook,
});
if (options.onEnter) {
scene.on('enter', options.onEnter.bind({
scene,
triggerElement,
props: options.props,
}));
}
if (options.onLeave) {
scene.on('leave', options.onLeave.bind({
scene,
triggerElement,
props: options.props,
}));
}
if (sceneManager.addIndicators) {
scene.addIndicators({
name: sceneId,
});
}
scene.addTo(sceneManager.controller);
sceneManager.setScene(sceneId, scene);
if (options.onInit) {
options.onInit({
scene,
triggerElement,
props: options.props,
});
}
},
unbind(el, binding) {
const options = binding.value;
const sceneId = sceneManager.getSceneIds(options.sceneId)[0];
sceneManager.destroyScene(sceneId);
},
};
const pinDirective = {
bind(el, binding) {
const options = binding.value;
el.$observers = [];
sceneManager.getSceneIds(options.sceneId).forEach((sceneId) => {
const observer = (scene) => {
scene.setPin(SceneManager.getTargetElement(el, options.targetElement));
};
observer.id = +new Date();
el.$observers.push(observer);
sceneManager.addSceneObserver(sceneId, observer);
});
},
unbind(el, binding) {
const options = binding.value;
sceneManager.removeSceneObservers(options.sceneId, el.$observers);
delete el.$observers;
},
};
const classToggleDirective = {
bind(el, binding) {
const options = binding.value;
el.$observers = [];
sceneManager.getSceneIds(options.sceneId).forEach((sceneId, i) => {
let className = options.class || options.classes;
if (Object.prototype.toString.call(className) === '[object Array]') {
className = className[i];
}
const observer = (scene) => {
scene.setClassToggle(SceneManager.getTargetElement(el, options.targetElement), className);
};
observer.id = +new Date();
el.$observers.push(observer);
sceneManager.addSceneObserver(sceneId, observer);
});
},
unbind(el, binding) {
const options = binding.value;
sceneManager.removeSceneObservers(options.sceneId, el.$observers);
delete el.$observers;
},
};
const tweenDirective = {
bind(el, binding) {
const options = binding.value;
el.$observers = [];
sceneManager.getSceneIds(options.sceneId).forEach((sceneId) => {
const duration = options.duration;
const fromVars = options.fromVars;
const toVars = options.toVars || options.vars;
const method = fromVars && toVars ? 'fromTo' : fromVars ? 'from' : 'to';
const observer = (scene) => {
if (!scene.timeline) {
scene.timeline = new TimelineMax();
}
const tween = TweenMax[method](SceneManager.getTargetElement(el, options.targetElement), duration || 1, fromVars || toVars, toVars);
scene.timeline.add([tween], 0, 'normal');
scene.setTween(scene.timeline);
};
observer.id = +new Date();
el.$observers.push(observer);
sceneManager.addSceneObserver(sceneId, observer);
});
},
unbind(el, binding) {
const options = binding.value;
sceneManager.removeSceneObservers(options.sceneId, el.$observers);
delete el.$observers;
},
};
const VueScrollWizardryPlugin = (_Vue, options = defaultOptions) => {
sceneManager = new SceneManager(options);
_Vue.directive('sw-scene', sceneDirective);
_Vue.directive('sw-pin', pinDirective);
_Vue.directive('sw-class-toggle', classToggleDirective);
_Vue.directive('sw-tween', tweenDirective);
};
export default VueScrollWizardryPlugin;
@NickBolles
Copy link

NickBolles commented Mar 24, 2018

Few Questions:
Do you have any examples of using this?
Do you have any plans of distributing this as an NPM module?
Also, what about typescript definitions for this and scrollwizardry? If not I'd do them for my project

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment