milvinae

web & product design.

独自スクロールバーをjavascript+scssだけで実装してみる2

前回、ScrollbarをJavascript+scssで自作しました。
しかし、あのやり方では1つのページに複数のスクロール領域があるとうまくいきません。
というのも、querySelectorで1つのscroll-areaにしかアクセスしていませんし対応する変数が1つしかないからです。

もし複数のスクロール領域を対象にするなら、それぞれに対して位置や長さなどの変数が必要になります。
さぁて、これからどうしていきましょうか…

1つ目の方法は、querySelectorAllで全部のframe-area/scroll-areaを拾って、前述の通りその分だけ変数を準備する方法。
理屈的にはできそうですが、今どのスクロールエリアがスクロールされているのかを総当たりでチェックするような未来が見えます。
あまりスマートなやり方ではなさそうな感じがしますね。

2つ目の方法はスクロールクラスを作って、スクロール領域1つ1つに対してクラス実体を持たせる方法。
クラスに変数を持たせることでスクロール領域ごとの情報を独立保持することができます。
処理も独立しているのでなんとなくうまくいきそうな気がします。
2つ目の方法で進めてみましょう。


// props
var thumbThickness = 8;
var trackLengRatio = 0.8;
var thumbMinLength = 20;
var trackDistanceFromEdge = 5;

class customScroller{
  // element
  frameArea ;
  scrollArea ;
  trackH ;
  thumbH ;
  trackV ;
  thumbV ;
  // variable
  frameRect;        // current frame rect info.
  scrollRect;       // current scroll rect info.
  thumbVTopEdge;    // client Y about vertical thumb when mouse down.  
  thumbHLeftEdge;   // client X about horizontal thumb when mouse down.
  movableLengH;     // length that scroll-area can move in horizontal.  
  movableLengV;     // length that scroll-area can move in vertical.
  movableBarH;      // length that horizontal thumb on horizontal track.
  movableBarV;      // length that vertical thumb on vertical track.
  // flags
  activeH;          // is horizontal thumb draggin?
  activeV;          // is vertical thumb draggin?

  constructor(el){
    this.frameArea  = el;
    this.scrollArea = el.querySelector(".scroll-area");

    this.createHTML();
    this.initialize();
    this.updateTrack();
  }

  // initializer
  initialize(classThis){
 
    this.trackH      = this.frameArea.querySelector('.scroll-track-h');
    this.thumbH      = this.frameArea.querySelector('.scroll-thumb-h');
    this.trackV      = this.frameArea.querySelector('.scroll-track-v');
    this.thumbV      = this.frameArea.querySelector('.scroll-thumb-v');
    
    this.thumbH.addEventListener("mousedown",   this.holdThumbH, { passive: true });
    this.thumbV.addEventListener("mousedown",   this.holdThumbV, { passive: true });
    this.frameArea.addEventListener("mousemove", this.moveThumb, { passive: false }); 
    // "passive: false" means callback has preventDefault function
    this.frameArea.addEventListener("mouseup",   this.releaseThumb);
    this.scrollArea.addEventListener("selectstart",   function(e) {
      if(this.activeH || this.activeV) 
        e.preventDefault();
    });
    
    this.frameArea.addEventListener("scroll",  this.scrollingArea);
    this.frameArea.addEventListener("transitionend", this.updateTrack);
    this.trackH.addEventListener("click",       this.clickTrackH, { passive: false });
    this.trackV.addEventListener("click",       this.clickTrackV, { passive: false });
    
    window.addEventListener('resize', this.updateTrack);
    this.frameResizingObserver(this);
    
    this.thumbHLeftEdge = 0;
    this.thumbVTopEdge  = 0;
    this.movableLengH   = 0;
    this.movableLengV   = 0;
    this.movableBarH    = 0;
    this.movableBarV    = 0;
    this.activeH        = false;
    this.activeV        = false;
  }
  
  frameResizingObserver(){
    const observer = new MutationObserver(this.updateTrack);
    observer.observe(this.frameArea, {attributes: true, attributeFilter: ["style"]});
  }

  // update variable 
  // recalled by window resize or frame resize
  updateTrack(){
    // if(this.frameArea.style.opacity < 1) 
    //   return ;

    this.frameRect  = this.frameArea.getBoundingClientRect();
    this.scrollRect = this.scrollArea.getBoundingClientRect();

    var goHorizon  = false;
    var goVertical = false;
    if(this.frameRect.width >= this.scrollRect.width){
      this.trackH.style.visibility = "hidden";
      this.thumbH.style.visibility = "hidden";
    }else{
      this.trackH.style.visibility = "visible";
      this.thumbH.style.visibility = "visible";
      goHorizon = true;
    }
    
    if(this.frameRect.height >= this.scrollRect.height){
      this.trackV.style.visibility = "hidden"; 
      this.thumbV.style.visibility = "hidden"; 
    }else{
      this.trackV.style.visibility = "visible";
      this.thumbV.style.visibility = "visible";
      goVertical = true;
    }
      
    var widRatio = (this.frameRect.width * trackLengRatio)  / this.scrollRect.width;
    var hgtRatio = (this.frameRect.height * trackLengRatio) / this.scrollRect.height;

    // horizontal scrollbar 
    if(goHorizon == true){
      this.trackH.style.width  = (this.frameRect.width * trackLengRatio) + "px";
      this.trackH.style.height = thumbThickness + "px";
      this.trackH.style.left   = (this.frameRect.x + this.frameRect.width * (1 - trackLengRatio) / 2)  + "px";
      this.trackH.style.bottom = (window.innerHeight - this.frameRect.bottom + thumbThickness + trackDistanceFromEdge) + "px";
      
      var trackH_Rect = this.trackH.getBoundingClientRect();
      var thumbHWidth = trackH_Rect.width  * widRatio;
      if(thumbHWidth < thumbMinLength) thumbHWidth = thumbMinLength;
      this.thumbH.style.width  = thumbHWidth + "px";
      this.thumbH.style.height = trackH_Rect.height + "px";
      this.thumbH.style.top = "0px";
      this.thumbH.style.borderRadius = thumbThickness / 2 + "px";
      
      this.thumbLeftEdge = this.thumbH.getBoundingClientRect().x; 
      this.movableLengH  = this.scrollRect.width - this.frameRect.width;
      this.movableBarH   = (this.frameRect.width * trackLengRatio) - thumbHWidth;
    }

    // vertical scrollbar
    if(goVertical){
      this.trackV.style.width  = thumbThickness + "px";
      this.trackV.style.height = (this.frameRect.height * trackLengRatio) + "px";
      this.trackV.style.right  = (window.innerWidth - this.frameRect.right + (thumbThickness + trackDistanceFromEdge))  + "px";
      this.trackV.style.top    = (this.frameRect.y + this.frameRect.height * (1 - trackLengRatio) / 2) + "px";
      
      var trackV_Rect = this.trackV.getBoundingClientRect();
      this.thumbV.style.width  = trackV_Rect.width  + "px";
      var thumbVHeight = trackV_Rect.height * hgtRatio;
      if(thumbVHeight < thumbMinLength) thumbVHeight = thumbMinLength;
      this.thumbV.style.height = thumbVHeight + "px";
      this.thumbV.style.top = "0px";
      this.thumbV.style.borderRadius = thumbThickness / 2 + "px";
      
      this.thumbTopEdge = this.thumbV.getBoundingClientRect().y; 
      this.movableLengV = this.scrollRect.height - this.frameRect.height;
      this.movableBarV  = (this.frameRect.height * trackLengRatio) - thumbVHeight;
    }
  }

  createHTML(){
    // Horizontal
    const horizontalTrack = document.createElement("div");
    horizontalTrack.classList.add("scroll-track-h");
    const horizontalThumb = document.createElement("div");
    horizontalThumb.classList.add("scroll-thumb-h");
    horizontalTrack.appendChild(horizontalThumb);
    this.frameArea.appendChild(horizontalTrack);
    
    // vertical
    const verticalTrack = document.createElement("div");
    verticalTrack.classList.add("scroll-track-v");
    const verticalThumb = document.createElement("div");
    verticalThumb.classList.add("scroll-thumb-v");
    verticalTrack.appendChild(verticalThumb);
    this.frameArea.appendChild(verticalTrack);
  };

  // event 
  // mouse down on horizontal thumb 
  holdThumbH(e){
    console.log("mouseDown");
    this.activeH = true;
    this.thumbHLeftEdge = e.offsetX;
  } 

  // mouse down on vertical thumb 
  holdThumbV(e){
    console.log("mouseDown");
    this.activeV = true;
    this.thumbVTopEdge  = e.offsetY;
  }

  // mouse move on document
  // because dont move only on thumb 
  moveThumb(e){ 
    if(!this.activeH && !this.activeV){ 
      return; }
    e.preventDefault();
    
    if(this.activeH){
      e = e.clientX;
      console.log("horizontal: " + e);
      // it use css style "translate".
      // because need diff length from element initial pos.
      var curThumbLeftX = e - this.trackH.getBoundingClientRect().x - this.thumbHLeftEdge;
      var trackRatio = curThumbLeftX / this.movableBarH;
      var actualMoveX = this.movableLengH * trackRatio;
      if(curThumbLeftX < 0) 
        curThumbLeftX = 0;
      else if(curThumbLeftX > this.movableBarH) 
        curThumbLeftX = this.movableBarH;
      this.thumbH.style.transform = "translateX(" + curThumbLeftX + "px)";
      this.frameArea.scrollLeft = actualMoveX;
    }
    if(this.activeV){
      e = e.clientY;
      console.log("vertical: " + e);
      var curThumbTopY = e - this.trackV.getBoundingClientRect().y - this.thumbVTopEdge;
      var trackRatio = curThumbTopY / this.movableBarV;
      var actualMoveY = this.movableLengV * trackRatio;
      if(curThumbTopY < 0) 
        curThumbTopY = 0;
      else if(curThumbTopY > this.movableBarV) 
        curThumbTopY = this.movableBarV;
      this.thumbV.style.transform = "translateY(" + curThumbTopY + "px)";
      this.frameArea.scrollTop = actualMoveY;
    }
  }

  releaseThumb(e){
    console.log("mouseUp");

    // wait for move sequence.
    requestAnimationFrame(function() {
      document.body.style.overflow = "";
      e.stopPropagation();
      this.activeV = false;
      this.activeH = false;
    }, 0);
  }

  scrollingArea(){
    console.log("scrolled");
    if(this.activeH || this.activeH) {
      return;  
    }
    var scrolltop  = this.frameArea.scrollTop;
    var scrollleft = this.frameArea.scrollLeft;
    var scrollRatioH = scrollleft / this.movableLengH;
    var scrollRatioV = scrolltop  / this.movableLengV;
    var curThumbTopY = this.movableBarV * scrollRatioV;
    var curThumbLeftX= this.movableBarH * scrollRatioH;
    
    this.thumbV.style.transform = "translateY(" + curThumbTopY + "px)";
    this.thumbH.style.transform = "translateX(" + curThumbLeftX + "px)";
    
  }

  // click in trackbar
  clickTrackH(e){
    if(this.activeH) {return;} // reject mousemove and release
    
    console.log("clicked Horizontal");
    e.preventDefault();
    
    var thumbNewX = e.offsetX - this.thumbH.scrollWidth / 2;
    if(thumbNewX < 0)
      thumbNewX = 0;
    else if(thumbNewX > this.movableBarH)
      thumbNewX = this.movableBarH;
    this.thumbH.style.transform = "translateX(" + thumbNewX + "px)";
    
    var trackRatio = thumbNewX / this.movableBarH;
    var actualMoveX = this.movableLengH * trackRatio;
    this.frameArea.scrollLeft = actualMoveX; 
  }

  clickTrackV(e){
    if(this.activeV) { return ;}
    console.log("clicked vertical");
    e.preventDefault();
    
    var thumbNewY = e.offsetY - this.thumbV.scrollHeight / 2;
      if(thumbNewY < 0)
      thumbNewY = 0;
    else if(thumbNewY > this.movableBarV)
      thumbNewY = this.movableBarV;
    this.thumbV.style.transform = "translateY(" + thumbNewY + "px)";
    
    var trackRatio = thumbNewY / this.movableBarV;
    var actualMoveY = this.movableLengV * trackRatio;
    this.frameArea.scrollTop = actualMoveY;
  }

}

// called after Dom loaded
document.addEventListener("DOMContentLoaded", (e) => {
  var frames = document.querySelectorAll(".frame-area");
  for( i = 0; i < frames.length; i++){
    var scroller = new customScroller(frames[i]);
  }
});

class化しました。
変数と関数群をClassの中に入れて、DomContentLoadedでリッスンしてたのをコンストラクタに移しました。

理屈で行けば行けそうな気がします。






うまくいきませんね。
デバッグで追跡すると、this.frameAreaが見つからない(undefined)とのことでした。
私の実行環境ではthisがMutitationObserverになっていました。

重要なポイントなのですが、javascriptのthisとCのthisは挙動が違います。
C経験者的にはthisはクラス内部にアクセスするものだと思ってしまいますが、javascriptのthisは呼び出された元の内部にアクセスする仕様があります。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this

今回の実装でにはMutitationObserverでリサイズを監視しています。
リサイズが検知されてObserverが登録された関数を実行すると、関数内のthisが呼び出したObserverとして処理されてしまうというわけです。

ふーむ…。
これは困りましたね。
どうしたらいいでしょうか?

順当に考えれば、クラス実体を変数にして保持しておけば、thisを使わずにクラス実体にアクセスできるはずです。
クラスに実体を保持する変数を追加し、コンストラクタで自身を代入してみます。

this._this_ = this;






…ダメでしたね。
そもそも変数で保持してもそれを呼び出すにはまたthisが必要になるからです。


ならば、イベントリスナーに登録するときに実体も一緒に渡してはどうでしょうか。
つまりこういうことです。
this.XXXXX.addEventListener("some-event",   function(e){this.someFunction(e, this) });

・・・・

結局、呼び出したときthisが処理されるので、呼び出し元にsomeFunctionが無くNGでした。

では一度変数に入れなおしてから呼び出してみましょう。
var _this_ = this;
this.XXXXX.addEventListener("some-event",   function(e){_this_.someFunction(e, _this_) });


うまくいきましたね!
_this_に実体を入れて確定しているため、望ましく処理が実行されました。
イベントで呼び出される処理のthisを書き換えていきます。

では書き直したコードを下に記載します。


// props
var thumbThickness = 8;
var trackLengRatio = 0.8;
var thumbMinLength = 20;
var trackDistanceFromEdge = 5;

class customScroller{
  // element
  frameArea ;
  scrollArea ;
  trackH ;
  thumbH ;
  trackV ;
  thumbV ;
  // variable
  frameRect;        // current frame rect info.
  scrollRect;       // current scroll rect info.
  thumbVTopEdge;    // client Y about vertical thumb when mouse down.  
  thumbHLeftEdge;   // client X about horizontal thumb when mouse down.
  movableLengH;     // length that scroll-area can move in horizontal.  
  movableLengV;     // length that scroll-area can move in vertical.
  movableBarH;      // length that horizontal thumb on horizontal track.
  movableBarV;      // length that vertical thumb on vertical track.
  // flags
  activeH;          // is horizontal thumb draggin?
  activeV;          // is vertical thumb draggin?

  constructor(el){
    this.frameArea  = el;
    this.scrollArea = el.querySelector(".scroll-area");

    this.createHTML();
    this.initialize(this);
    this.updateTrack(this);
  }

  // initializer
  initialize(classThis){
 
    this.trackH      = this.frameArea.querySelector('.scroll-track-h');
    this.thumbH      = this.frameArea.querySelector('.scroll-thumb-h');
    this.trackV      = this.frameArea.querySelector('.scroll-track-v');
    this.thumbV      = this.frameArea.querySelector('.scroll-thumb-v');
    
    this.thumbH.addEventListener("mousedown",   function(e){classThis.holdThumbH(e, classThis) }, { passive: true });
    this.thumbV.addEventListener("mousedown",   function(e){classThis.holdThumbV(e, classThis) }, { passive: true });
    this.frameArea.addEventListener("mousemove", function(e){classThis.moveThumb(e, classThis) }, { passive: false }); 
    // "passive: false" means callback has preventDefault function
    this.frameArea.addEventListener("mouseup",   function(e){classThis.releaseThumb(e, classThis) });
    this.scrollArea.addEventListener("selectstart",   function(e) {
      if(classThis.activeH || classThis.activeV) 
        e.preventDefault();
    });
    
    this.frameArea.addEventListener("scroll",  function(){classThis.scrollingArea(classThis)});
    this.frameArea.addEventListener("transitionend", function(){classThis.updateTrack(classThis)});
    this.trackH.addEventListener("click",       function(e){classThis.clickTrackH(e, classThis) }, { passive: false });
    this.trackV.addEventListener("click",       function(e){classThis.clickTrackV(e, classThis) }, { passive: false });
    
    window.addEventListener('resize', function(e){classThis.updateTrack(classThis) });
    this.frameResizingObserver(this);
    
    this.thumbHLeftEdge = 0;
    this.thumbVTopEdge  = 0;
    this.movableLengH   = 0;
    this.movableLengV   = 0;
    this.movableBarH    = 0;
    this.movableBarV    = 0;
    this.activeH        = false;
    this.activeV        = false;
  }
  
  frameResizingObserver(classThis){
    const observer = new MutationObserver(function(){classThis.updateTrack(classThis)});
    observer.observe(this.frameArea, {attributes: true, attributeFilter: ["style"]});
  }

  // update variable 
  // recalled by window resize or frame resize
  updateTrack(_this_){
    // if(_this_.frameArea.style.opacity < 1) 
    //   return ;

    _this_.frameRect  = _this_.frameArea.getBoundingClientRect();
    _this_.scrollRect = _this_.scrollArea.getBoundingClientRect();

    var goHorizon  = false;
    var goVertical = false;
    if(_this_.frameRect.width >= _this_.scrollRect.width){
      _this_.trackH.style.visibility = "hidden";
      _this_.thumbH.style.visibility = "hidden";
    }else{
      _this_.trackH.style.visibility = "visible";
      _this_.thumbH.style.visibility = "visible";
      goHorizon = true;
    }
    
    if(_this_.frameRect.height >= _this_.scrollRect.height){
      _this_.trackV.style.visibility = "hidden"; 
      _this_.thumbV.style.visibility = "hidden"; 
    }else{
      _this_.trackV.style.visibility = "visible";
      _this_.thumbV.style.visibility = "visible";
      goVertical = true;
    }
      
    var widRatio = (_this_.frameRect.width * trackLengRatio)  / _this_.scrollRect.width;
    var hgtRatio = (_this_.frameRect.height * trackLengRatio) / _this_.scrollRect.height;

    // horizontal scrollbar 
    if(goHorizon == true){
      _this_.trackH.style.width  = (_this_.frameRect.width * trackLengRatio) + "px";
      _this_.trackH.style.height = thumbThickness + "px";
      _this_.trackH.style.left   = (_this_.frameRect.x + _this_.frameRect.width * (1 - trackLengRatio) / 2)  + "px";
      _this_.trackH.style.bottom = (window.innerHeight - _this_.frameRect.bottom + thumbThickness + trackDistanceFromEdge) + "px";
      
      var trackH_Rect = _this_.trackH.getBoundingClientRect();
      var thumbHWidth = trackH_Rect.width  * widRatio;
      if(thumbHWidth < thumbMinLength) thumbHWidth = thumbMinLength;
      _this_.thumbH.style.width  = thumbHWidth + "px";
      _this_.thumbH.style.height = trackH_Rect.height + "px";
      _this_.thumbH.style.top = "0px";
      _this_.thumbH.style.borderRadius = thumbThickness / 2 + "px";
      
      _this_.thumbLeftEdge = _this_.thumbH.getBoundingClientRect().x; 
      _this_.movableLengH  = _this_.scrollRect.width - _this_.frameRect.width;
      _this_.movableBarH   = (_this_.frameRect.width * trackLengRatio) - thumbHWidth;
    }

    // vertical scrollbar
    if(goVertical){
      _this_.trackV.style.width  = thumbThickness + "px";
      _this_.trackV.style.height = (_this_.frameRect.height * trackLengRatio) + "px";
      _this_.trackV.style.right  = (window.innerWidth - _this_.frameRect.right + (thumbThickness + trackDistanceFromEdge))  + "px";
      _this_.trackV.style.top    = (_this_.frameRect.y + _this_.frameRect.height * (1 - trackLengRatio) / 2) + "px";
      
      var trackV_Rect = _this_.trackV.getBoundingClientRect();
      _this_.thumbV.style.width  = trackV_Rect.width  + "px";
      var thumbVHeight = trackV_Rect.height * hgtRatio;
      if(thumbVHeight < thumbMinLength) thumbVHeight = thumbMinLength;
      _this_.thumbV.style.height = thumbVHeight + "px";
      _this_.thumbV.style.top = "0px";
      _this_.thumbV.style.borderRadius = thumbThickness / 2 + "px";
      
      _this_.thumbTopEdge = _this_.thumbV.getBoundingClientRect().y; 
      _this_.movableLengV = _this_.scrollRect.height - _this_.frameRect.height;
      _this_.movableBarV  = (_this_.frameRect.height * trackLengRatio) - thumbVHeight;
    }
  }

  createHTML(){
    // Horizontal
    const horizontalTrack = document.createElement("div");
    horizontalTrack.classList.add("scroll-track-h");
    const horizontalThumb = document.createElement("div");
    horizontalThumb.classList.add("scroll-thumb-h");
    horizontalTrack.appendChild(horizontalThumb);
    this.frameArea.appendChild(horizontalTrack);
    
    // vertical
    const verticalTrack = document.createElement("div");
    verticalTrack.classList.add("scroll-track-v");
    const verticalThumb = document.createElement("div");
    verticalThumb.classList.add("scroll-thumb-v");
    verticalTrack.appendChild(verticalThumb);
    this.frameArea.appendChild(verticalTrack);
  };

  // event 
  // mouse down on horizontal thumb 
  holdThumbH(e, _this_){
    console.log("mouseDown");
    _this_.activeH = true;
    _this_.thumbHLeftEdge = e.offsetX;
  } 

  // mouse down on vertical thumb 
  holdThumbV(e, _this_){
    console.log("mouseDown");
    _this_.activeV = true;
    _this_.thumbVTopEdge  = e.offsetY;
  }

  // mouse move on document
  // because dont move only on thumb 
  moveThumb(e, _this_){ 
    if(!_this_.activeH && !_this_.activeV){ 
      return; }
    e.preventDefault();
    
    if(_this_.activeH){
      e = e.clientX;
      console.log("horizontal: " + e);
      // it use css style "translate".
      // because need diff length from element initial pos.
      var curThumbLeftX = e - _this_.trackH.getBoundingClientRect().x - _this_.thumbHLeftEdge;
      var trackRatio = curThumbLeftX / _this_.movableBarH;
      var actualMoveX = _this_.movableLengH * trackRatio;
      if(curThumbLeftX < 0) 
        curThumbLeftX = 0;
      else if(curThumbLeftX > _this_.movableBarH) 
        curThumbLeftX = _this_.movableBarH;
      _this_.thumbH.style.transform = "translateX(" + curThumbLeftX + "px)";
      _this_.frameArea.scrollLeft = actualMoveX;
    }
    if(_this_.activeV){
      e = e.clientY;
      console.log("vertical: " + e);
      var curThumbTopY = e - _this_.trackV.getBoundingClientRect().y - _this_.thumbVTopEdge;
      var trackRatio = curThumbTopY / _this_.movableBarV;
      var actualMoveY = _this_.movableLengV * trackRatio;
      if(curThumbTopY < 0) 
        curThumbTopY = 0;
      else if(curThumbTopY > _this_.movableBarV) 
        curThumbTopY = _this_.movableBarV;
      _this_.thumbV.style.transform = "translateY(" + curThumbTopY + "px)";
      _this_.frameArea.scrollTop = actualMoveY;
    }
  }

  releaseThumb(e, _this_){
    console.log("mouseUp");

    // wait for move sequence.
    requestAnimationFrame(function() {
      document.body.style.overflow = "";
      e.stopPropagation();
      _this_.activeV = false;
      _this_.activeH = false;
    }, 0);
  }

  scrollingArea(_this_){
    console.log("scrolled");
    if(_this_.activeH || _this_.activeH) {
      return;  
    }
    var scrolltop  = _this_.frameArea.scrollTop;
    var scrollleft = _this_.frameArea.scrollLeft;
    var scrollRatioH = scrollleft / _this_.movableLengH;
    var scrollRatioV = scrolltop  / _this_.movableLengV;
    var curThumbTopY = _this_.movableBarV * scrollRatioV;
    var curThumbLeftX= _this_.movableBarH * scrollRatioH;
    
    _this_.thumbV.style.transform = "translateY(" + curThumbTopY + "px)";
    _this_.thumbH.style.transform = "translateX(" + curThumbLeftX + "px)";
    
  }

  // click in trackbar
  clickTrackH(e, _this_){
    if(_this_.activeH) {return;} // reject mousemove and release
    
    console.log("clicked Horizontal");
    e.preventDefault();
    
    var thumbNewX = e.offsetX - _this_.thumbH.scrollWidth / 2;
    if(thumbNewX < 0)
      thumbNewX = 0;
    else if(thumbNewX > _this_.movableBarH)
      thumbNewX = _this_.movableBarH;
    _this_.thumbH.style.transform = "translateX(" + thumbNewX + "px)";
    
    var trackRatio = thumbNewX / _this_.movableBarH;
    var actualMoveX = _this_.movableLengH * trackRatio;
    _this_.frameArea.scrollLeft = actualMoveX; 
  }

  clickTrackV(e, _this_){
    if(_this_.activeV) { return ;}
    console.log("clicked vertical");
    e.preventDefault();
    
    var thumbNewY = e.offsetY - _this_.thumbV.scrollHeight / 2;
      if(thumbNewY < 0)
      thumbNewY = 0;
    else if(thumbNewY > _this_.movableBarV)
      thumbNewY = _this_.movableBarV;
    _this_.thumbV.style.transform = "translateY(" + thumbNewY + "px)";
    
    var trackRatio = thumbNewY / _this_.movableBarV;
    var actualMoveY = _this_.movableLengV * trackRatio;
    _this_.frameArea.scrollTop = actualMoveY;
  }

}

// called after Dom loaded
document.addEventListener("DOMContentLoaded", (e) => {
  var frames = document.querySelectorAll(".frame-area");
  for( i = 0; i < frames.length; i++){
    var scroller = new customScroller(frames[i]);
  }
});

codepenはこちらから。
気に入っていただけたらLove、Pinお願いします。

See the Pen custom scrollbar class by keiyashi (@keiyashi) on CodePen.