Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 74 additions & 47 deletions packages/frontend/src/components/MkPullToRefresh.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const isRefreshing = ref(false);
const pullDistance = ref(0);

let startScreenY: number | null = null;
let pullingPointerId: number | null = null;

const rootEl = useTemplateRef('rootEl');
let scrollEl: HTMLElement | null = null;
Expand All @@ -56,16 +57,32 @@ const emit = defineEmits<{
(ev: 'refresh'): void;
}>();

function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number {
if (('touches' in event) && event.touches[0] && event.touches[0].screenY != null) {
return event.touches[0].screenY;
} else if ('screenY' in event) {
return event.screenY;
} else {
return 0; // TSを黙らせるため
function detachPullListeners() {
if (rootEl.value == null) return;
rootEl.value.removeEventListener('pointermove', onPullPointerMove);
rootEl.value.removeEventListener('pointerup', onPullPointerUpOrCancel);
rootEl.value.removeEventListener('pointercancel', onPullPointerUpOrCancel);
}

function safeReleasePointerCapture(pointerId: number | null) {
if (rootEl.value == null) return;
if (pointerId == null) return;
try {
if (rootEl.value.hasPointerCapture(pointerId)) {
rootEl.value.releasePointerCapture(pointerId);
}
} catch {
// ignore
}
}

function cleanupPullInteraction() {
detachPullListeners();
safeReleasePointerCapture(pullingPointerId);
pullingPointerId = null;
startScreenY = null;
}

// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
function lockDownScroll() {
if (scrollEl == null) return;
Expand All @@ -79,9 +96,13 @@ function unlockDownScroll() {
scrollEl.style.overscrollBehavior = 'auto contain';
}

function moveStartByMouse(event: MouseEvent) {
if (event.button !== 1) return;
function moveStartByPointer(event: PointerEvent) {
if (isRefreshing.value) return;
if (isPulling.value) return;
if (!event.isPrimary) return;

// マウス操作は従来通り「中クリックドラッグ」でのみ開始
if (event.pointerType === 'mouse' && event.button !== 1) return;

const scrollPos = scrollEl!.scrollTop;
if (scrollPos !== 0) {
Expand All @@ -91,39 +112,26 @@ function moveStartByMouse(event: MouseEvent) {

lockDownScroll();

event.preventDefault(); // 中クリックによるスクロール、テキスト選択などを防ぐ
if (event.pointerType === 'mouse') {
// 中クリックによるスクロール、テキスト選択などを防ぐ
event.preventDefault();
}

isPulling.value = true;
startScreenY = getScreenY(event);
isPulledEnough.value = false;
startScreenY = event.screenY;
pullDistance.value = 0;
pullingPointerId = event.pointerId;

window.addEventListener('mousemove', moving, { passive: true });
window.addEventListener('mouseup', () => {
window.removeEventListener('mousemove', moving);
onPullRelease();
}, { passive: true, once: true });
}

function moveStartByTouch(event: TouchEvent) {
if (isRefreshing.value) return;

const scrollPos = scrollEl!.scrollTop;
if (scrollPos !== 0) {
unlockDownScroll();
return;
try {
rootEl.value?.setPointerCapture(event.pointerId);
} catch {
// ignore
}

lockDownScroll();

isPulling.value = true;
startScreenY = getScreenY(event);
pullDistance.value = 0;

window.addEventListener('touchmove', moving, { passive: true });
window.addEventListener('touchend', () => {
window.removeEventListener('touchmove', moving);
onPullRelease();
}, { passive: true, once: true });
rootEl.value?.addEventListener('pointermove', onPullPointerMove, { passive: true });
rootEl.value?.addEventListener('pointerup', onPullPointerUpOrCancel, { passive: true });
rootEl.value?.addEventListener('pointercancel', onPullPointerUpOrCancel, { passive: true });
}

function moveBySystem(to: number): Promise<void> {
Expand Down Expand Up @@ -163,7 +171,7 @@ async function closeContent() {
}

function onPullRelease() {
startScreenY = null;
cleanupPullInteraction();
if (isPulledEnough.value) {
isPulledEnough.value = false;
isRefreshing.value = true;
Expand All @@ -178,7 +186,7 @@ function onPullRelease() {
}
}

function toggleScrollLockOnTouchEnd() {
function toggleScrollLockOnPointerEnd() {
const scrollPos = scrollEl!.scrollTop;
if (scrollPos === 0) {
lockDownScroll();
Expand All @@ -187,7 +195,7 @@ function toggleScrollLockOnTouchEnd() {
}
}

function moving(event: MouseEvent | TouchEvent) {
function moving(event: PointerEvent) {
if ((scrollEl?.scrollTop ?? 0) > SCROLL_STOP + pullDistance.value || isHorizontalSwipeSwiping.value) {
pullDistance.value = 0;
isPulledEnough.value = false;
Expand All @@ -196,9 +204,9 @@ function moving(event: MouseEvent | TouchEvent) {
}

if (startScreenY === null) {
startScreenY = getScreenY(event);
startScreenY = event.screenY;
}
const moveScreenY = getScreenY(event);
const moveScreenY = event.screenY;

const moveHeight = moveScreenY - startScreenY!;
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
Expand All @@ -208,6 +216,24 @@ function moving(event: MouseEvent | TouchEvent) {
if (isPulledEnough.value) haptic();
}

function onPullPointerMove(event: PointerEvent) {
if (pullingPointerId == null) return;
if (event.pointerId !== pullingPointerId) return;
moving(event);
}

function onPullPointerUpOrCancel(event: PointerEvent) {
if (pullingPointerId == null) return;
if (event.pointerId !== pullingPointerId) return;
onPullRelease();
}

function onRootPointerUpOrCancel(event: PointerEvent) {
// 既存実装の touchend 相当: マウスは対象外
if (event.pointerType === 'mouse') return;
toggleScrollLockOnPointerEnd();
}

/**
* emit(refresh)が完了したことを知らせる関数
*
Expand All @@ -224,16 +250,17 @@ onMounted(() => {
if (rootEl.value == null) return;
scrollEl = getScrollContainer(rootEl.value);
lockDownScroll();
rootEl.value.addEventListener('mousedown', moveStartByMouse, { passive: false }); // preventDefaultするため
rootEl.value.addEventListener('touchstart', moveStartByTouch, { passive: true });
rootEl.value.addEventListener('touchend', toggleScrollLockOnTouchEnd, { passive: true });
rootEl.value.addEventListener('pointerdown', moveStartByPointer, { passive: false }); // preventDefaultする可能性があるため
rootEl.value.addEventListener('pointerup', onRootPointerUpOrCancel, { passive: true });
rootEl.value.addEventListener('pointercancel', onRootPointerUpOrCancel, { passive: true });
});

onUnmounted(() => {
unlockDownScroll();
if (rootEl.value) rootEl.value.removeEventListener('mousedown', moveStartByMouse);
if (rootEl.value) rootEl.value.removeEventListener('touchstart', moveStartByTouch);
if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
cleanupPullInteraction();
if (rootEl.value) rootEl.value.removeEventListener('pointerdown', moveStartByPointer);
if (rootEl.value) rootEl.value.removeEventListener('pointerup', onRootPointerUpOrCancel);
if (rootEl.value) rootEl.value.removeEventListener('pointercancel', onRootPointerUpOrCancel);
});
</script>

Expand Down
39 changes: 25 additions & 14 deletions packages/frontend/src/components/MkRange.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="thumb"
:style="{ left: thumbPosition + 'px' }"
@mouseenter.passive="onMouseenter"
@mousedown="onMousedown"
@touchstart="onMousedown"
@pointerdown="onPointerdown"
>
<div class="thumbInner"></div>
</div>
Expand Down Expand Up @@ -179,7 +178,7 @@ function onMouseenter() {

let lastClickTime: number | null = null;

function onMousedown(ev: MouseEvent | TouchEvent) {
function onPointerdown(ev: PointerEvent) {
if (props.disabled) return; // Prevent interaction if disabled

ev.preventDefault();
Expand All @@ -201,12 +200,19 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
window.document.head.appendChild(style);

const thumbWidth = getThumbWidth();
const draggingPointerId = ev.pointerId;
try {
thumbEl.value?.setPointerCapture(draggingPointerId);
} catch {
// ignore
}

const onDrag = (ev: MouseEvent | TouchEvent) => {
const onDrag = (ev: PointerEvent) => {
if (ev.pointerId !== draggingPointerId) return;
ev.preventDefault();
let beforeValue = finalValue.value;
const containerRect = containerEl.value!.getBoundingClientRect();
const pointerX = 'touches' in ev && ev.touches.length > 0 ? ev.touches[0].clientX : 'clientX' in ev ? ev.clientX : 0;
const pointerX = ev.clientX;
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2));
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth)));

Expand All @@ -217,13 +223,18 @@ function onMousedown(ev: MouseEvent | TouchEvent) {

let beforeValue = finalValue.value;

const onMouseup = () => {
const onPointerup = (ev: PointerEvent) => {
if (ev.pointerId !== draggingPointerId) return;
window.document.head.removeChild(style);
tooltipForDragShowing.value = false;
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('touchmove', onDrag);
window.removeEventListener('mouseup', onMouseup);
window.removeEventListener('touchend', onMouseup);
window.removeEventListener('pointermove', onDrag);
window.removeEventListener('pointerup', onPointerup);
window.removeEventListener('pointercancel', onPointerup);
try {
thumbEl.value?.releasePointerCapture(draggingPointerId);
} catch {
// ignore
}

// 値が変わってたら通知
if (beforeValue !== finalValue.value) {
Expand All @@ -232,10 +243,9 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
}
};

window.addEventListener('mousemove', onDrag);
window.addEventListener('touchmove', onDrag);
window.addEventListener('mouseup', onMouseup, { once: true });
window.addEventListener('touchend', onMouseup, { once: true });
window.addEventListener('pointermove', onDrag, { passive: false });
window.addEventListener('pointerup', onPointerup, { passive: true });
window.addEventListener('pointercancel', onPointerup, { passive: true });

if (lastClickTime == null) {
lastClickTime = Date.now();
Expand Down Expand Up @@ -374,6 +384,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
width: $thumbWidth;
height: $thumbHeight;
cursor: grab;
touch-action: none;

&:hover {
> .thumbInner {
Expand Down
6 changes: 4 additions & 2 deletions packages/frontend/src/components/global/MkPageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div v-if="(!thin_ && narrow && !hideTitle) || (actions && actions.length > 0)" :class="$style.buttons">
<template v-for="action in actions">
<button v-tooltip.noDelay="action.text" class="_button" :class="[$style.button, { [$style.highlighted]: action.highlighted }]" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
<button v-tooltip.noDelay="action.text" class="_button" :class="[$style.button, { [$style.highlighted]: action.highlighted }]" @click.stop="action.handler" @pointerdown="preventDrag"><i :class="action.icon"></i></button>
</template>
</div>
</div>
Expand Down Expand Up @@ -89,7 +89,9 @@ const show = computed(() => {
return !hideTitle.value || hasTabs.value || hasActions.value;
});

const preventDrag = (ev: TouchEvent) => {
const preventDrag = (ev: PointerEvent) => {
// マウス操作には影響させない
if (ev.pointerType === 'mouse') return;
ev.stopPropagation();
};

Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { Component, MaybeRef } from 'vue';
import type { Component, ComputedRef, MaybeRef } from 'vue';
import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
Expand Down Expand Up @@ -162,7 +162,7 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
}

// props に ref を許可するようにする
type PropsWithRefs<P> = { [K in keyof P]: MaybeRef<P[K]> };
type PropsWithRefs<P> = { [K in keyof P]: MaybeRef<P[K]> | ComputedRef<P[K]> };
type ComponentProps<T extends Component> = PropsWithRefs<CP<T>>;

// 関数の引数が any[] (もっとも広義なもの) かどうかを判定し、any[] の場合は排除 (never) するヘルパー
Expand Down
Loading
Loading