Vue Scroll locked mixin

by ChangJoo Park

HTML

<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>

<div id="app">
  <div class="outer-container">
  
    <something></something>
    <something></something>
    <something></something>
    <div class="scroll-locked inner-container">
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
    </div>

    <div class="scroll-locked inner-container">
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
      <h1>Hello</h1>
    </div>

  </div>

</div>

<template id="something">
  <div class="something-outer scroll-locked">
    <div class="something-inner">
      <h1>Component</h1>
      <h1>Component</h1>
      <h1>Component</h1>
      <h1>Component</h1>
      <h1>Component</h1>
      <h1>Component</h1>
      <h1>Component</h1>
      <h1>Component</h1>
      <h1>Component</h1>
    </div>
  </div>
</template>

CSS

#app {
  background-color: tomato;
  overflow: scroll;
}

.outer-container {
  width: 100vw;
  height: 200vh;
}

.inner-container {
  width: 300px;
  height: 300px;
  margin: 0 auto;
  margin-top: 100px;
  background-color: white;
  overflow: auto;
}


.something-outer {
  width: 300px;
  height: 300px;
  background-color: white;
  overflow: scroll;
  display: inline-block;
}

JavaScript

var scrollLockMixin = {
  mounted: function() {
    var scrollables = this.$el.querySelectorAll('.scroll-locked');
    if (scrollables && scrollables.length > 0) {
      scrollables.forEach(function(scrollable) {
        scrollable.addEventListener('wheel', function(event) {
          var meetTop = scrollable.scrollTop === 0;
          var meetBottom = (scrollable.scrollHeight - scrollable.clientHeight) === scrollable.scrollTop;
          var isUpward = event.deltaY < 0;
          var isDownward = event.deltaY > 0;

          if (meetBottom && isDownward) {
            event.preventDefault();
            event.stopPropagation();
            return;
          }
          if (meetTop && isUpward) {
            event.preventDefault();
            event.stopPropagation();
            return;
          }
        });
      })
    }
  },
  beforeDestroyed: function() {
    var scrollables = this.$el.querySelectorAll('.scroll-lockable');
    if (scrollables && scrollables.length > 0) {
      scrollables.forEach(function(scrollable) {
        scrollable.removeEventListener('wheel');
      })
    }
  }
};

Vue.component('something', {
	template: '#something',
	mixins: [scrollLockMixin]
})

new Vue({
  el: '#app',
  mixins: [scrollLockMixin]
})