mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Improved scrollbar control
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020-2025 - https://www.igorski.nl
|
||||
* Igor Zinken 2020-2026 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -23,25 +23,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="canScrollHorizontally"
|
||||
ref="horScroll"
|
||||
class="scroll scroll--horizontal"
|
||||
@mousedown="handlePointerDown"
|
||||
@touchstart="handlePointerDown"
|
||||
>
|
||||
<div
|
||||
v-if="canScrollHorizontally"
|
||||
class="scroll__handle"
|
||||
:style="horHandleStyle"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="canScrollVertically"
|
||||
ref="verScroll"
|
||||
class="scroll scroll--vertical"
|
||||
@mousedown="handlePointerDown"
|
||||
@touchstart="handlePointerDown"
|
||||
>
|
||||
<div
|
||||
v-if="canScrollVertically"
|
||||
class="scroll__handle"
|
||||
:style="verHandleStyle"
|
||||
></div>
|
||||
@@ -51,10 +51,38 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { mapState } from "vuex";
|
||||
import { type Coordinate } from "zcanvas";
|
||||
|
||||
const MOVE_EVENTS = [ "mousemove", "touchmove", "wheel" ];
|
||||
const UP_EVENTS = [ "mouseup", "touchend", "touchcancel" ];
|
||||
|
||||
/**
|
||||
* Wrapper to get uniform coordinates from a pointer / touch event
|
||||
* regardless whether it was initiated by mouse pointer or on a touchscreen/pad.
|
||||
* By use of preventDefault a side effect can be introduced to stop the default
|
||||
* behaviour on touch events (as in the context of scrollbar panning it could
|
||||
* trigger a page bounce effect)
|
||||
*/
|
||||
function getCoordinate( e: PointerEvent | TouchEvent, preventDefault = true ): Coordinate {
|
||||
let x = e.clientX;
|
||||
let y = e.clientY;
|
||||
|
||||
// touch event handling
|
||||
const touches = e.touches || e.changedTouches;
|
||||
if ( touches && touches.length ) {
|
||||
x = touches[ 0 ].pageX;
|
||||
y = touches[ 0 ].pageY;
|
||||
if ( preventDefault ) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function clamp( value: number ) {
|
||||
return Math.min( 1, Math.max( 0, value ));
|
||||
}
|
||||
|
||||
export default {
|
||||
emits: [ "input" ],
|
||||
props: {
|
||||
@@ -86,8 +114,8 @@ export default {
|
||||
horHandleSize: 0,
|
||||
verHandlePos: 0,
|
||||
verHandleSize: 0,
|
||||
x: 0, // the "scrollLeft" position
|
||||
y: 0, // the "scrollTop" position
|
||||
x: 0.5, // the "scrollLeft" position, normalized 0 - 1 range
|
||||
y: 0.5, // the "scrollTop" position, normalized 0 - 1 range
|
||||
}),
|
||||
computed: {
|
||||
...mapState([
|
||||
@@ -110,12 +138,26 @@ export default {
|
||||
},
|
||||
canScrollVertically(): boolean {
|
||||
return this.scrollHeight > 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
windowSize(): void {
|
||||
this.calcDimensions();
|
||||
},
|
||||
canScrollHorizontally( value: boolean ): void {
|
||||
if ( value ) {
|
||||
this.$nextTick(() => {
|
||||
this.calcDimensions();
|
||||
});
|
||||
}
|
||||
},
|
||||
canScrollVertically( value: boolean ): void {
|
||||
if ( value ) {
|
||||
this.$nextTick(() => {
|
||||
this.calcDimensions();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted(): void {
|
||||
this.upHandler = this.handlePointerUp.bind( this );
|
||||
@@ -128,19 +170,29 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
calcDimensions(): void {
|
||||
this.trackWidth = this.$refs.horScroll.offsetWidth;
|
||||
this.trackHeight = this.$refs.verScroll.offsetHeight;
|
||||
this.trackWidth = this.$refs.horScroll?.offsetWidth ?? this.viewportWidth;
|
||||
this.trackHeight = this.$refs.verScroll?.offsetHeight ?? this.viewportHeight;
|
||||
|
||||
this.scrollWidth = this.contentWidth - this.viewportWidth;
|
||||
this.scrollHeight = this.contentHeight - this.viewportHeight;
|
||||
|
||||
this.horHandleSize = ( this.viewportWidth / this.contentWidth ) * this.trackWidth;
|
||||
this.verHandleSize = ( this.viewportHeight / this.contentHeight ) * this.trackHeight;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.positionHorizontalHandle();
|
||||
this.positionVerticalHandle();
|
||||
});
|
||||
},
|
||||
handlePointerDown( e: PointerEvent ): void {
|
||||
this.hasHorizontalScroll = e.target === this.$refs.horScroll;
|
||||
this.hasVerticalScroll = e.target === this.$refs.verScroll;
|
||||
|
||||
this.startOffset = getCoordinate( e );
|
||||
this.xAtStart = Math.max( 0.05, this.x );
|
||||
this.yAtStart = Math.max( 0.05, this.y );
|
||||
this.hasScrolled = false;
|
||||
|
||||
UP_EVENTS.forEach( type => {
|
||||
window.addEventListener( type, this.upHandler );
|
||||
});
|
||||
@@ -148,48 +200,57 @@ export default {
|
||||
window.addEventListener( type, this.moveHandler, { passive: false });
|
||||
});
|
||||
},
|
||||
handlePointerUp(): void {
|
||||
this.hasHorizontalScroll = false;
|
||||
this.hasVerticalScroll = false;
|
||||
|
||||
handlePointerUp( e: PointerEvent | TouchEvent ): void {
|
||||
UP_EVENTS.forEach( type => {
|
||||
window.removeEventListener( type, this.upHandler );
|
||||
});
|
||||
MOVE_EVENTS.forEach( type => {
|
||||
window.removeEventListener( type, this.moveHandler );
|
||||
});
|
||||
|
||||
if ( !this.hasScrolled && !( e instanceof TouchEvent )) {
|
||||
if ( this.hasHorizontalScroll ) {
|
||||
this.x = clamp( e.offsetX / this.trackWidth );
|
||||
this.positionHorizontalHandle();
|
||||
}
|
||||
if ( this.hasVerticalScroll ) {
|
||||
this.y = clamp( e.offsetY / this.trackHeight );
|
||||
this.positionVerticalHandle();
|
||||
}
|
||||
this.$emit( "input", { left: this.x, top: this.y });
|
||||
}
|
||||
},
|
||||
handleScroll( e: PointerEvent | TouchEvent ): void {
|
||||
if ( e.target !== this.$refs.horScroll && e.target !== this.$refs.verScroll ) {
|
||||
return;
|
||||
}
|
||||
let pointerX = e.offsetX;
|
||||
let pointerY = e.offsetY;
|
||||
const { x, y } = getCoordinate( e );
|
||||
|
||||
// touch event handling
|
||||
const touches = e.touches || e.changedTouches;
|
||||
if ( touches && touches.length ) {
|
||||
pointerX = touches[ 0 ].pageX;
|
||||
pointerY = touches[ 0 ].pageY;
|
||||
e.preventDefault(); // prevent page bounce
|
||||
}
|
||||
// the coordinate values here are in normalized 0 - 1 range
|
||||
|
||||
let targetX = this.xAtStart / this.startOffset.x * x;
|
||||
let targetY = this.yAtStart / this.startOffset.y * y;
|
||||
|
||||
if ( this.hasHorizontalScroll ) {
|
||||
this.x = Math.max( 0, pointerX / this.trackWidth ); // is % of total
|
||||
this.x = clamp( targetX );
|
||||
this.positionHorizontalHandle();
|
||||
}
|
||||
if ( this.hasVerticalScroll ) {
|
||||
this.y = Math.max( 0, pointerY / this.trackHeight ); // is % of total
|
||||
this.y = clamp( targetY );
|
||||
this.positionVerticalHandle();
|
||||
}
|
||||
this.hasScrolled = true;
|
||||
this.$emit( "input", { left: this.x, top: this.y });
|
||||
},
|
||||
update( left: number, top: number ): void {
|
||||
this.x = isNaN( left ) ? 0 : left;
|
||||
this.y = isNaN( top ) ? 0 : top;
|
||||
update( left: number, top: number, emit = false ): void {
|
||||
this.x = isNaN( left ) ? 0 : clamp( left );
|
||||
this.y = isNaN( top ) ? 0 : clamp( top );
|
||||
|
||||
this.positionHorizontalHandle();
|
||||
this.positionVerticalHandle();
|
||||
this.$nextTick(() => {
|
||||
this.positionHorizontalHandle();
|
||||
this.positionVerticalHandle();
|
||||
|
||||
if ( emit ) {
|
||||
this.$emit( "input", { left: this.x, top: this.y });
|
||||
}
|
||||
});
|
||||
},
|
||||
positionHorizontalHandle(): void {
|
||||
this.horHandlePos = ( this.trackWidth - this.horHandleSize ) * this.x;
|
||||
|
||||
Reference in New Issue
Block a user