業務でドラッグスクロールしたいという要望がありライブラリを探してみたんですが、色々あって独自で作ることになりました。
せっかくなので、いろんな場面で使いまわせるようにディレクティブで作ってみたので、その際のメモを残しておきます。
ディレクティブとは
ディレクティブには2種類あります
今回は独自の属性ディレクティブを作って行きます
カスタム属性ディレクティブを作る
ディレクティブクラスの作成
-
@Directive
アノテーションを付与したディレクティブクラスを作成 -
selector
に指定した名称がhtmlで指定する属性になります-
[]
で囲うのを忘れないように!
-
drag-scroll.directive.ts
import { Directive, HostListener, ElementRef } from '@angular/core';
@Directive({
selector: '[dragScroll]'
})
export class DragScrollDirective {
private position: { top: number, left: number, x: number, y: number };
constructor(private el: ElementRef) { }
get element() { return this.el.nativeElement as HTMLElement; }
@HostListener('mousedown', ['$event']) onMouseDown(event: MouseEvent) {
if (!this.element) { return; }
// クリック時の位置を保持
this.position = {
top: this.element.scrollTop,
left: this.element.scrollLeft,
x: event.clientX,
y: event.clientY,
};
return false;
}
@HostListener('mousemove', ['$event']) onMouseMove(event: MouseEvent) {
if (this.position) {
if (event.clientX && event.clientY) {
// マウスの移動距離を算出
const moveX = event.clientX - this.position.x;
const moveY = event.clientY - this.position.y;
// 移動距離だけスクロール
this.element.scrollLeft = this.position.left - moveX;
this.element.scrollTop = this.position.top - moveY;
return false;
}
}
return true;
}
@HostListener('mouseup') onMouseUp() { this.position = null; }
@HostListener('mouseleave') onMouseLeave() { this.position = null; }
}
中身の処理は以下をやってるだけです。
- クリック時にクリック位置を保持
- 通常の
mousedown
イベントを伝搬しないようにfalseを返してます- これをしないとマウスを動かしたときに範囲選択されてしまって挙動がおかしくなることがあったので
- 通常の
- クリックしたままマウスを動かしたら、動かした分だけスクロール
- これもスクロールする場合はクリック時と同様にfalseを返してイベント伝搬しないようにしています
- クリック終了時およびカーソルがDOMの外に出た場合にクリック位置を破棄
モジュール化
- いろんな画面で使いまわせるようにモジュール化します
- ※別にモジュール化しなくても使えます
- 宣言は1か所でしかできないため、複数画面で使いたい場合はモジュール化が必須です。
- ※別にモジュール化しなくても使えます
drag-scroll.module.ts
import { NgModule } from '@angular/core';
import { DragScrollDirective } from './drag-scroll.directive';
@NgModule({
declarations: [DragScrollDirective],
exports: [DragScrollDirective],
})
export class DragScrollModule { }
カスタムディレクティブのインポート
- 作成したモジュールを使いたい場所のモジュールでインポート
- モジュール化していない場合は
declarations
で直接宣言
- モジュール化していない場合は
app.module.ts
・・・
import { DragScrollModule } from './shared/directives/drag-scroll/drag-scroll.module';
@NgModule({
・・・
imports: [
・・・
DragScrollModule,
],
・・・
})
export class AppModule { }
HTMLへの組み込み
- htmlで対象のタグにカスタムディレクティブを指定
app.component.html
<div dragScroll style="max-width: 300px; max-height: 300px; overflow: auto;">
<div style="width: 500px; height: 500px;">
test
</div>
</div>
ついでにドラッグスクロール要素内にドラッグ可能な要素を置いてみる
以下のようにdraggable="true"
の要素を置くだけだと、ドラッグスクロールが勝ってしまいます
app.component.html
<div dragScroll style="max-width: 300px; max-height: 300px; overflow: auto;">
<div style="width: 500px; height: 500px;">
test
<div draggable="true" style="width: 100px; height: 100px; background-color: red">
draggable
</div>
</div>
</div>
そのため、draggable要素のmousedownで一工夫してあげましょう
app.component.html
<div dragScroll style="max-width: 300px; max-height: 300px; overflow: auto;">
<div style="width: 500px; height: 500px;">
test
<div draggable="true" (mousedown)="onMouseDown($event)" style="width: 100px; height: 100px; background-color: red">
draggable
</div>
</div>
</div>
app.component.ts
onMouseDown(event: MouseEvent) {
// 親要素へのイベント伝搬を止める
event.stopImmediatePropagation();
}
このようにdraggable要素のmousedownにてstopImmediatePropagation
を呼ぶことで親要素へイベントが伝搬されないため、dragScrollのmousedownはハンドリングされなくなり、ちゃんとドラッグ出来るようになります。
最後に
割と簡単にカスタムディレクティブが作れました!
なお、今回作ったものはGitHubにアップしてます。