119 lines
3.9 KiB
JavaScript
119 lines
3.9 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const MODES = Object.freeze(["auto", "camera", "tool", "family"]);
|
|
const STORAGE_KEY = "tarinai_mobile_input_mode_v1";
|
|
|
|
class InputModeManager {
|
|
constructor() {
|
|
this.storageKey = STORAGE_KEY;
|
|
this.currentMode = this.loadMode();
|
|
this.gestureState = {
|
|
active: false,
|
|
kind: "",
|
|
startedAt: 0,
|
|
pointerCount: 0,
|
|
};
|
|
this.classification = this.classify();
|
|
}
|
|
|
|
loadMode() {
|
|
try {
|
|
const mode = global.localStorage?.getItem(STORAGE_KEY) || "auto";
|
|
return MODES.includes(mode) ? mode : "auto";
|
|
} catch (_) {
|
|
return "auto";
|
|
}
|
|
}
|
|
|
|
setMode(mode = "auto") {
|
|
this.currentMode = MODES.includes(mode) ? mode : "auto";
|
|
try { global.localStorage?.setItem(STORAGE_KEY, this.currentMode); } catch (_) {}
|
|
this.emit("input:mode", this.snapshot());
|
|
return this.currentMode;
|
|
}
|
|
|
|
classify() {
|
|
const nav = global.navigator || {};
|
|
const ua = String(nav.userAgent || "");
|
|
const mm = typeof global.matchMedia === "function" ? global.matchMedia.bind(global) : null;
|
|
const width = Number(global.innerWidth || 9999);
|
|
const height = Number(global.innerHeight || 9999);
|
|
const shortSide = Math.min(width, height);
|
|
const longSide = Math.max(width, height);
|
|
const pointerCoarse = Boolean(mm?.("(pointer: coarse)")?.matches);
|
|
const hasHover = Boolean(mm?.("(hover: hover)")?.matches);
|
|
const noHover = Boolean(mm?.("(hover: none)")?.matches);
|
|
const isTouchDevice = Number(nav.maxTouchPoints || 0) > 0 || "ontouchstart" in global;
|
|
const isCompactViewport = shortSide <= 720 && longSide <= 1100;
|
|
const userAgentMobile = /Android|iPhone|iPod|iPad|Mobile|Silk|Tablet/i.test(ua);
|
|
const touchFirst = Boolean(pointerCoarse && noHover && !hasHover && isTouchDevice && (isCompactViewport || userAgentMobile));
|
|
this.classification = Object.freeze({
|
|
isTouchDevice,
|
|
isCompactViewport,
|
|
hasHover,
|
|
pointerCoarse,
|
|
userAgentMobile,
|
|
touchFirst,
|
|
width,
|
|
height,
|
|
});
|
|
return this.classification;
|
|
}
|
|
|
|
refresh() {
|
|
const previous = this.classification;
|
|
const next = this.classify();
|
|
if (JSON.stringify(previous) !== JSON.stringify(next)) this.emit("input:classification", this.snapshot());
|
|
return next;
|
|
}
|
|
|
|
beginGesture(kind = "", pointerCount = 1) {
|
|
this.gestureState = {
|
|
active: true,
|
|
kind: String(kind || ""),
|
|
startedAt: performance?.now?.() ?? Date.now(),
|
|
pointerCount: Math.max(0, Number(pointerCount) || 0),
|
|
};
|
|
return this.gestureState;
|
|
}
|
|
|
|
endGesture() {
|
|
this.gestureState = { ...this.gestureState, active: false, pointerCount: 0 };
|
|
return this.gestureState;
|
|
}
|
|
|
|
canPanCanvas() {
|
|
if (!this.classification?.touchFirst) return true;
|
|
return this.currentMode === "auto" || this.currentMode === "camera";
|
|
}
|
|
|
|
canPlaceTools() {
|
|
if (!this.classification?.touchFirst) return true;
|
|
return this.currentMode === "auto" || this.currentMode === "tool";
|
|
}
|
|
|
|
canFamilyTreeCaptureTouch() {
|
|
if (!this.classification?.touchFirst) return true;
|
|
return this.currentMode === "auto" || this.currentMode === "family";
|
|
}
|
|
|
|
snapshot() {
|
|
return Object.freeze({
|
|
...this.classification,
|
|
currentMode: this.currentMode,
|
|
gestureState: { ...this.gestureState },
|
|
canPanCanvas: this.canPanCanvas(),
|
|
canPlaceTools: this.canPlaceTools(),
|
|
canFamilyTreeCaptureTouch: this.canFamilyTreeCaptureTouch(),
|
|
});
|
|
}
|
|
|
|
emit(type, detail) {
|
|
global.TarinaiEvents?.emit?.(type, detail);
|
|
}
|
|
}
|
|
|
|
global.InputModeManager = InputModeManager;
|
|
global.TarinaiInputMode = global.TarinaiInputMode || new InputModeManager();
|
|
})(typeof window !== "undefined" ? window : globalThis);
|