前回、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.