Skip to content

Instantly share code, notes, and snippets.

@homerjam
Created November 15, 2017 10:59
Show Gist options
  • Select an option

  • Save homerjam/10dd9fc56a5ed816f99d81a22f13d189 to your computer and use it in GitHub Desktop.

Select an option

Save homerjam/10dd9fc56a5ed816f99d81a22f13d189 to your computer and use it in GitHub Desktop.

Revisions

  1. homerjam created this gist Nov 15, 2017.
    331 changes: 331 additions & 0 deletions vue-scrollwizardry.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,331 @@
    /*
    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;