@dnax/ui 0.0.26 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/QConfigProvider.vue +25 -0
- package/components/QDialog.vue +26 -12
- package/components/QEditorJs.vue +146 -0
- package/components/QFab.vue +19 -2
- package/components/QSyntax.vue +22 -9
- package/components/QTiptap.vue +993 -0
- package/components/internal/QTiptapTableView.vue +123 -0
- package/components/internal/QTiptapTaskItemView.vue +49 -0
- package/index.ts +12 -0
- package/lib/icons.ts +26 -0
- package/lib/q.ts +5 -1
- package/lib/storage.ts +175 -0
- package/lib/tiptap-table.ts +19 -0
- package/lib/tiptap-task-list.ts +16 -0
- package/lib/touchHold.ts +221 -0
- package/lib/touchPan.ts +237 -0
- package/lib/touchRepeat.ts +148 -0
- package/lib/touchSwipe.ts +182 -0
- package/module.ts +8 -0
- package/package.json +25 -1
- package/scripts/generate-exports.ts +10 -0
- package/styles/main.css +873 -32
package/lib/touchHold.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// v-touch-hold — directive : détecte l'appui long (hold) sur un élément,
|
|
2
|
+
// tactile ET souris (modifier .mouse), équivalent Quasar
|
|
3
|
+
// https://quasar.dev/vue-directives/touch-hold
|
|
4
|
+
//
|
|
5
|
+
// Usage :
|
|
6
|
+
// <div v-touch-hold="handler" /> → 600ms, sensibilité 5/7px
|
|
7
|
+
// <div v-touch-hold.mouse="{ time: 400, sensitivity: 6 }" />
|
|
8
|
+
// <div v-touch-hold="'400:8:10'" /> → 400ms, 8px tactile, 10px souris
|
|
9
|
+
// <div v-touch-hold="350" />
|
|
10
|
+
// modifiers : mouse (inclut la souris), capture / mouseCapture, stop, prevent,
|
|
11
|
+
// passive
|
|
12
|
+
//
|
|
13
|
+
// Détails du handler (compatible Quasar) :
|
|
14
|
+
// { evt, position: { left, top }, duration }
|
|
15
|
+
// Après déclenchement, le geste est « consommé » : le click/touchend qui suit
|
|
16
|
+
// est supprimé (un appui long ne déclenche pas aussi un clic).
|
|
17
|
+
|
|
18
|
+
export interface TouchHoldDetails {
|
|
19
|
+
evt: Event
|
|
20
|
+
position: { left: number; top: number }
|
|
21
|
+
duration: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type TouchHoldHandler = (details: TouchHoldDetails) => void
|
|
25
|
+
|
|
26
|
+
const DEFAULTS = { time: 600, touchSensitivity: 5, mouseSensitivity: 7 }
|
|
27
|
+
|
|
28
|
+
interface HoldOptions {
|
|
29
|
+
handler: TouchHoldHandler
|
|
30
|
+
time: number
|
|
31
|
+
touchSensitivity: number
|
|
32
|
+
mouseSensitivity: number
|
|
33
|
+
mouse: boolean
|
|
34
|
+
stop: boolean
|
|
35
|
+
prevent: boolean
|
|
36
|
+
capture: boolean
|
|
37
|
+
passive: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type QElement = HTMLElement & { __qTouchHoldCleanup?: () => void }
|
|
41
|
+
|
|
42
|
+
function parseValue(value: unknown): Partial<typeof DEFAULTS> {
|
|
43
|
+
if (typeof value === "number") return { time: value }
|
|
44
|
+
if (typeof value === "string") {
|
|
45
|
+
const [t, touch, mouse] = value.split(":")
|
|
46
|
+
const out: Partial<typeof DEFAULTS> = {}
|
|
47
|
+
const num = Number(t)
|
|
48
|
+
if (Number.isFinite(num)) out.time = num
|
|
49
|
+
if (touch !== undefined) {
|
|
50
|
+
const n = Number(touch)
|
|
51
|
+
if (Number.isFinite(n)) out.touchSensitivity = n
|
|
52
|
+
}
|
|
53
|
+
if (mouse !== undefined) {
|
|
54
|
+
const n = Number(mouse)
|
|
55
|
+
if (Number.isFinite(n)) out.mouseSensitivity = n
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
const out: Partial<typeof DEFAULTS> = {}
|
|
61
|
+
const t = Number(value[0])
|
|
62
|
+
if (Number.isFinite(t)) out.time = t
|
|
63
|
+
const touch = Number(value[1])
|
|
64
|
+
if (Number.isFinite(touch)) out.touchSensitivity = touch
|
|
65
|
+
const mouse = Number(value[2])
|
|
66
|
+
if (Number.isFinite(mouse)) out.mouseSensitivity = mouse
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
if (value && typeof value === "object") {
|
|
70
|
+
const o = value as Record<string, unknown>
|
|
71
|
+
const out: Partial<typeof DEFAULTS> = {}
|
|
72
|
+
const t = Number(o.time ?? o.duration)
|
|
73
|
+
if (Number.isFinite(t)) out.time = t
|
|
74
|
+
const touch = Number(o.sensitivity ?? o.touchSensitivity)
|
|
75
|
+
if (Number.isFinite(touch)) out.touchSensitivity = touch
|
|
76
|
+
const mouse = Number(o.mouseSensitivity)
|
|
77
|
+
if (Number.isFinite(mouse)) out.mouseSensitivity = mouse
|
|
78
|
+
return out
|
|
79
|
+
}
|
|
80
|
+
return {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const vTouchHold = {
|
|
84
|
+
mounted(el: HTMLElement, binding: { value?: unknown; modifiers?: Record<string, boolean> }) {
|
|
85
|
+
const handler = binding.value
|
|
86
|
+
if (typeof handler !== "function") {
|
|
87
|
+
console.warn("[v-touch-hold] value doit être une fonction (handler)")
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
const m = binding.modifiers ?? {}
|
|
91
|
+
const parsed = parseValue(binding.value)
|
|
92
|
+
const options: HoldOptions = {
|
|
93
|
+
handler: handler as TouchHoldHandler,
|
|
94
|
+
time: parsed.time ?? DEFAULTS.time,
|
|
95
|
+
touchSensitivity: parsed.touchSensitivity ?? DEFAULTS.touchSensitivity,
|
|
96
|
+
mouseSensitivity: parsed.mouseSensitivity ?? DEFAULTS.mouseSensitivity,
|
|
97
|
+
mouse: !!m.mouse || !!m.mouseCapture,
|
|
98
|
+
stop: !!m.stop,
|
|
99
|
+
prevent: !!m.prevent,
|
|
100
|
+
capture: !!m.capture || !!m.mouseCapture,
|
|
101
|
+
passive: !!m.passive,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Éviter scroll / context menu / sélection pendant l'appui
|
|
105
|
+
el.style.touchAction = "none"
|
|
106
|
+
el.style.userSelect = "none"
|
|
107
|
+
;(el as HTMLElement).style.webkitUserSelect = "none"
|
|
108
|
+
|
|
109
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
110
|
+
let pending = false
|
|
111
|
+
let consumed = false
|
|
112
|
+
let startX = 0
|
|
113
|
+
let startY = 0
|
|
114
|
+
let lastEvt: PointerEvent | null = null
|
|
115
|
+
let startTime = 0
|
|
116
|
+
|
|
117
|
+
const clearTimer = () => {
|
|
118
|
+
if (timer) clearTimeout(timer)
|
|
119
|
+
timer = undefined
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cancel = () => {
|
|
123
|
+
clearTimer()
|
|
124
|
+
pending = false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const finish = () => {
|
|
128
|
+
clearTimer()
|
|
129
|
+
if (!pending) return
|
|
130
|
+
pending = false
|
|
131
|
+
consumed = true
|
|
132
|
+
const duration = Math.round(performance.now() - startTime)
|
|
133
|
+
const evt = lastEvt
|
|
134
|
+
if (evt) {
|
|
135
|
+
options.handler({
|
|
136
|
+
evt,
|
|
137
|
+
position: { left: evt.clientX, top: evt.clientY },
|
|
138
|
+
duration: options.time || duration,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
// Consomme la fin du geste : pas de click/touchend après un hold
|
|
142
|
+
const onUp = (e: Event) => {
|
|
143
|
+
if (options.prevent) e.preventDefault()
|
|
144
|
+
if (options.stop) e.stopPropagation()
|
|
145
|
+
window.removeEventListener("pointerup", onUp, true)
|
|
146
|
+
window.removeEventListener("pointercancel", onCancelAfter, true)
|
|
147
|
+
window.removeEventListener("click", onClick, true)
|
|
148
|
+
window.removeEventListener("touchend", onUp, true)
|
|
149
|
+
}
|
|
150
|
+
const onClick = (e: Event) => {
|
|
151
|
+
e.preventDefault()
|
|
152
|
+
e.stopPropagation()
|
|
153
|
+
window.removeEventListener("click", onClick, true)
|
|
154
|
+
window.removeEventListener("pointerup", onUp, true)
|
|
155
|
+
window.removeEventListener("touchend", onUp, true)
|
|
156
|
+
}
|
|
157
|
+
const onCancelAfter = () => {
|
|
158
|
+
window.removeEventListener("pointerup", onUp, true)
|
|
159
|
+
window.removeEventListener("pointercancel", onCancelAfter, true)
|
|
160
|
+
window.removeEventListener("click", onClick, true)
|
|
161
|
+
}
|
|
162
|
+
window.addEventListener("pointerup", onUp, true)
|
|
163
|
+
window.addEventListener("pointercancel", onCancelAfter, true)
|
|
164
|
+
window.addEventListener("touchend", onUp, true)
|
|
165
|
+
window.addEventListener("click", onClick, true)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
169
|
+
if (pending || consumed) return
|
|
170
|
+
if (e.pointerType === "mouse" && !options.mouse) return
|
|
171
|
+
if (options.prevent && !options.passive) e.preventDefault()
|
|
172
|
+
pending = true
|
|
173
|
+
startX = e.clientX
|
|
174
|
+
startY = e.clientY
|
|
175
|
+
startTime = performance.now()
|
|
176
|
+
lastEvt = e
|
|
177
|
+
const sensitivity = e.pointerType === "mouse" ? options.mouseSensitivity : options.touchSensitivity
|
|
178
|
+
timer = setTimeout(finish, options.time)
|
|
179
|
+
|
|
180
|
+
const onMove = (ev: PointerEvent) => {
|
|
181
|
+
if (!pending) return
|
|
182
|
+
if (Math.abs(ev.clientX - startX) > sensitivity || Math.abs(ev.clientY - startY) > sensitivity) {
|
|
183
|
+
cancel()
|
|
184
|
+
cleanup()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const onUpEarly = () => cancel()
|
|
188
|
+
const cleanup = () => {
|
|
189
|
+
el.removeEventListener("pointermove", onMove)
|
|
190
|
+
el.removeEventListener("pointerup", onUpEarly)
|
|
191
|
+
el.removeEventListener("pointercancel", onUpEarly)
|
|
192
|
+
}
|
|
193
|
+
el.addEventListener("pointermove", onMove, { passive: options.passive })
|
|
194
|
+
el.addEventListener("pointerup", onUpEarly)
|
|
195
|
+
el.addEventListener("pointercancel", onUpEarly)
|
|
196
|
+
;(el as any).__qHoldCleanupEarly = cleanup
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const onContext = (e: Event) => {
|
|
200
|
+
if (pending || consumed) e.preventDefault()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const capture = options.capture
|
|
204
|
+
el.addEventListener("pointerdown", onPointerDown, { capture, passive: options.passive })
|
|
205
|
+
el.addEventListener("contextmenu", onContext, { capture })
|
|
206
|
+
|
|
207
|
+
;(el as QElement).__qTouchHoldCleanup = () => {
|
|
208
|
+
el.removeEventListener("pointerdown", onPointerDown, { capture } as any)
|
|
209
|
+
el.removeEventListener("contextmenu", onContext, { capture } as any)
|
|
210
|
+
;(el as any).__qHoldCleanupEarly?.()
|
|
211
|
+
clearTimer()
|
|
212
|
+
el.style.touchAction = ""
|
|
213
|
+
el.style.userSelect = ""
|
|
214
|
+
el.style.webkitUserSelect = ""
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
unmounted(el: HTMLElement) {
|
|
218
|
+
;(el as QElement).__qTouchHoldCleanup?.()
|
|
219
|
+
delete (el as QElement).__qTouchHoldCleanup
|
|
220
|
+
},
|
|
221
|
+
}
|
package/lib/touchPan.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// v-touch-pan — directive : détecte les gestes « pan » (glisser) sur un
|
|
2
|
+
// élément, tactile ET souris (modifier .mouse), équivalent Quasar
|
|
3
|
+
// https://quasar.dev/vue-directives/touch-pan
|
|
4
|
+
//
|
|
5
|
+
// Usage : <div v-touch-pan.horizontal.mouse.prevent="onPan" />
|
|
6
|
+
// modifiers : horizontal | vertical | up | down | left | right (filtres de
|
|
7
|
+
// direction), mouse (inclut la souris), prevent (bloque le scroll natif),
|
|
8
|
+
// stop, capture, passive, mouseCapture
|
|
9
|
+
//
|
|
10
|
+
// Détails passés au handler (compatible Quasar) :
|
|
11
|
+
// { evt, position: { top, left }, direction, delta: { x, y },
|
|
12
|
+
// distance: { x, y }, duration, speed: { x, y },
|
|
13
|
+
// isFirst, isFinal, isVertical, isHorizontal }
|
|
14
|
+
|
|
15
|
+
export interface PanPosition {
|
|
16
|
+
top: number
|
|
17
|
+
left: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PanDelta {
|
|
21
|
+
x: number
|
|
22
|
+
y: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PanDirection = "up" | "down" | "left" | "right"
|
|
26
|
+
|
|
27
|
+
export interface PanDetails {
|
|
28
|
+
evt: PointerEvent
|
|
29
|
+
position: PanPosition
|
|
30
|
+
direction: PanDirection
|
|
31
|
+
delta: PanDelta
|
|
32
|
+
distance: PanDelta
|
|
33
|
+
duration: number
|
|
34
|
+
speed: PanDelta
|
|
35
|
+
isFirst: boolean
|
|
36
|
+
isFinal: boolean
|
|
37
|
+
isVertical: boolean
|
|
38
|
+
isHorizontal: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type TouchPanHandler = (details: PanDetails) => void
|
|
42
|
+
|
|
43
|
+
interface PanState {
|
|
44
|
+
pointerId: number
|
|
45
|
+
startX: number
|
|
46
|
+
startY: number
|
|
47
|
+
lastX: number
|
|
48
|
+
lastY: number
|
|
49
|
+
startTime: number
|
|
50
|
+
lastTime: number
|
|
51
|
+
axis: "horizontal" | "vertical" | null
|
|
52
|
+
panning: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PAN_MIN = 4 // seuil avant de déclarer le pan (px)
|
|
56
|
+
|
|
57
|
+
interface TouchPanOptions {
|
|
58
|
+
handler: TouchPanHandler
|
|
59
|
+
allowHorizontal: boolean
|
|
60
|
+
allowVertical: boolean
|
|
61
|
+
allowUp: boolean
|
|
62
|
+
allowDown: boolean
|
|
63
|
+
allowLeft: boolean
|
|
64
|
+
allowRight: boolean
|
|
65
|
+
mouse: boolean
|
|
66
|
+
prevent: boolean
|
|
67
|
+
stop: boolean
|
|
68
|
+
capture: boolean
|
|
69
|
+
passive: boolean
|
|
70
|
+
mouseCapture: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type QElement = HTMLElement & { __qTouchPanCleanup?: () => void }
|
|
74
|
+
|
|
75
|
+
function axisAllowed(options: TouchPanOptions, direction: PanDirection) {
|
|
76
|
+
if (options.allowHorizontal) return direction === "left" || direction === "right"
|
|
77
|
+
if (options.allowVertical) return direction === "up" || direction === "down"
|
|
78
|
+
switch (direction) {
|
|
79
|
+
case "up": return options.allowUp
|
|
80
|
+
case "down": return options.allowDown
|
|
81
|
+
case "left": return options.allowLeft
|
|
82
|
+
case "right": return options.allowRight
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseDirection(dx: number, dy: number): PanDirection {
|
|
87
|
+
return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? "right" : "left") : (dy > 0 ? "down" : "up")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function applyPrevent(e: Event, options: TouchPanOptions, active: boolean) {
|
|
91
|
+
if (options.stop) e.stopPropagation()
|
|
92
|
+
if (options.prevent && active) e.preventDefault()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const vTouchPan = {
|
|
96
|
+
mounted(el: HTMLElement, binding: { value?: unknown; modifiers?: Record<string, boolean> }) {
|
|
97
|
+
const handler = binding.value
|
|
98
|
+
if (typeof handler !== "function") {
|
|
99
|
+
console.warn("[v-touch-pan] value doit être une fonction (handler)")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
const m = binding.modifiers ?? {}
|
|
103
|
+
const options: TouchPanOptions = {
|
|
104
|
+
handler: handler as TouchPanHandler,
|
|
105
|
+
allowHorizontal: !!m.horizontal,
|
|
106
|
+
allowVertical: !!m.vertical,
|
|
107
|
+
allowUp: !!m.up,
|
|
108
|
+
allowDown: !!m.down,
|
|
109
|
+
allowLeft: !!m.left,
|
|
110
|
+
allowRight: !!m.right,
|
|
111
|
+
mouse: !!m.mouse || !!m.mouseCapture,
|
|
112
|
+
prevent: !!m.prevent,
|
|
113
|
+
stop: !!m.stop,
|
|
114
|
+
capture: !!m.capture || !!m.mouseCapture,
|
|
115
|
+
passive: !!m.passive,
|
|
116
|
+
mouseCapture: false,
|
|
117
|
+
}
|
|
118
|
+
options.mouseCapture = !!m.mouseCapture
|
|
119
|
+
|
|
120
|
+
let state: PanState | null = null
|
|
121
|
+
|
|
122
|
+
// touch-action : laisser le scroll natif quand on ne capture qu'un axe
|
|
123
|
+
if (options.prevent) el.style.touchAction = "none"
|
|
124
|
+
else if (options.allowHorizontal && !options.allowVertical) el.style.touchAction = "pan-y"
|
|
125
|
+
else if (options.allowVertical && !options.allowHorizontal) el.style.touchAction = "pan-x"
|
|
126
|
+
else el.style.touchAction = options.allowUp || options.allowDown || options.allowLeft || options.allowRight ? "pan-y pan-x" : "auto"
|
|
127
|
+
|
|
128
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
129
|
+
if (state) return
|
|
130
|
+
if (e.pointerType === "mouse" && !options.mouse) return
|
|
131
|
+
applyPrevent(e, options, false)
|
|
132
|
+
state = {
|
|
133
|
+
pointerId: e.pointerId,
|
|
134
|
+
startX: e.clientX,
|
|
135
|
+
startY: e.clientY,
|
|
136
|
+
lastX: e.clientX,
|
|
137
|
+
lastY: e.clientY,
|
|
138
|
+
startTime: performance.now(),
|
|
139
|
+
lastTime: performance.now(),
|
|
140
|
+
axis: null,
|
|
141
|
+
panning: false,
|
|
142
|
+
}
|
|
143
|
+
try { (e.currentTarget as Element).setPointerCapture?.(e.pointerId) } catch { /* ignore */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
147
|
+
if (!state || state.pointerId !== e.pointerId) return
|
|
148
|
+
const dx = e.clientX - state.startX
|
|
149
|
+
const dy = e.clientY - state.startY
|
|
150
|
+
const deltaX = e.clientX - state.lastX
|
|
151
|
+
const deltaY = e.clientY - state.lastY
|
|
152
|
+
const now = performance.now()
|
|
153
|
+
const duration = now - state.startTime
|
|
154
|
+
|
|
155
|
+
if (!state.panning) {
|
|
156
|
+
if (Math.abs(dx) < PAN_MIN && Math.abs(dy) < PAN_MIN) return
|
|
157
|
+
const direction = parseDirection(dx, dy)
|
|
158
|
+
if (!axisAllowed(options, direction)) return
|
|
159
|
+
state.axis = direction === "left" || direction === "right" ? "horizontal" : "vertical"
|
|
160
|
+
state.panning = true
|
|
161
|
+
applyPrevent(e, options, true)
|
|
162
|
+
state.lastX = e.clientX
|
|
163
|
+
state.lastY = e.clientY
|
|
164
|
+
state.lastTime = now
|
|
165
|
+
emit(state, e, direction, { x: 0, y: 0 }, { x: dx, y: dy }, duration, true)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const direction = state.axis === "horizontal" ? (dx > 0 ? "right" : "left") : (dy > 0 ? "down" : "up")
|
|
170
|
+
applyPrevent(e, options, true)
|
|
171
|
+
state.lastX = e.clientX
|
|
172
|
+
state.lastY = e.clientY
|
|
173
|
+
state.lastTime = now
|
|
174
|
+
emit(state, e, direction, { x: deltaX, y: deltaY }, { x: dx, y: dy }, duration, false)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const onPointerEnd = (e: PointerEvent) => {
|
|
178
|
+
if (!state || state.pointerId !== e.pointerId) return
|
|
179
|
+
const dx = e.clientX - state.startX
|
|
180
|
+
const dy = e.clientY - state.startY
|
|
181
|
+
const duration = performance.now() - state.startTime
|
|
182
|
+
if (state.panning) {
|
|
183
|
+
const direction = state.axis === "horizontal" ? (dx > 0 ? "right" : "left") : (dy > 0 ? "down" : "up")
|
|
184
|
+
emit(state, e, direction, { x: 0, y: 0 }, { x: dx, y: dy }, duration, false, true)
|
|
185
|
+
}
|
|
186
|
+
state = null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const onPointerCancel = (e: PointerEvent) => {
|
|
190
|
+
if (state && state.pointerId === e.pointerId) state = null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const emit = (
|
|
194
|
+
s: PanState,
|
|
195
|
+
evt: PointerEvent,
|
|
196
|
+
direction: PanDirection,
|
|
197
|
+
delta: PanDelta,
|
|
198
|
+
distance: PanDelta,
|
|
199
|
+
duration: number,
|
|
200
|
+
isFirst: boolean,
|
|
201
|
+
isFinal = false,
|
|
202
|
+
) => {
|
|
203
|
+
const elapsed = Math.max(duration, 1)
|
|
204
|
+
options.handler({
|
|
205
|
+
evt,
|
|
206
|
+
position: { top: evt.clientY, left: evt.clientX },
|
|
207
|
+
direction,
|
|
208
|
+
delta,
|
|
209
|
+
distance,
|
|
210
|
+
duration,
|
|
211
|
+
speed: { x: Math.abs(distance.x) / elapsed * 1000, y: Math.abs(distance.y) / elapsed * 1000 },
|
|
212
|
+
isFirst,
|
|
213
|
+
isFinal,
|
|
214
|
+
isVertical: s.axis === "vertical",
|
|
215
|
+
isHorizontal: s.axis === "horizontal",
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const capture = options.capture
|
|
220
|
+
el.addEventListener("pointerdown", onPointerDown, { capture, passive: options.passive })
|
|
221
|
+
el.addEventListener("pointermove", onPointerMove, { capture, passive: options.passive })
|
|
222
|
+
el.addEventListener("pointerup", onPointerEnd, { capture })
|
|
223
|
+
el.addEventListener("pointercancel", onPointerCancel, { capture })
|
|
224
|
+
|
|
225
|
+
;(el as QElement).__qTouchPanCleanup = () => {
|
|
226
|
+
el.removeEventListener("pointerdown", onPointerDown, { capture } as any)
|
|
227
|
+
el.removeEventListener("pointermove", onPointerMove, { capture } as any)
|
|
228
|
+
el.removeEventListener("pointerup", onPointerEnd, { capture } as any)
|
|
229
|
+
el.removeEventListener("pointercancel", onPointerCancel, { capture } as any)
|
|
230
|
+
el.style.touchAction = ""
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
unmounted(el: HTMLElement) {
|
|
234
|
+
;(el as QElement).__qTouchPanCleanup?.()
|
|
235
|
+
delete (el as QElement).__qTouchPanCleanup
|
|
236
|
+
},
|
|
237
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// v-touch-repeat — directive : déclenche un handler en boucle tant que
|
|
2
|
+
// l'élément est pressé (appui long répété), équivalent Quasar
|
|
3
|
+
// https://quasar.dev/vue-directives/touch-repeat
|
|
4
|
+
//
|
|
5
|
+
// Usage :
|
|
6
|
+
// <button v-touch-repeat.mouse="onRepeat">…</button>
|
|
7
|
+
// <button v-touch-repeat.mouse="{ delay: 500, interval: 100 }">…</button>
|
|
8
|
+
// <button v-touch-repeat.mouse="'500:100'">…</button>
|
|
9
|
+
// modifiers : mouse (inclut la souris), capture / mouseCapture, stop, prevent,
|
|
10
|
+
// passive
|
|
11
|
+
//
|
|
12
|
+
// Détails du handler : { evt, count, elapsed }
|
|
13
|
+
|
|
14
|
+
export interface TouchRepeatDetails {
|
|
15
|
+
evt: Event
|
|
16
|
+
/** Nombre d'appels (1 = premier, après le délai) */
|
|
17
|
+
count: number
|
|
18
|
+
/** Temps écoulé depuis le pointerdown (ms) */
|
|
19
|
+
elapsed: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TouchRepeatHandler = (details: TouchRepeatDetails) => void
|
|
23
|
+
|
|
24
|
+
const DEFAULTS = { delay: 600, interval: 150 }
|
|
25
|
+
|
|
26
|
+
interface RepeatOptions {
|
|
27
|
+
handler: TouchRepeatHandler
|
|
28
|
+
delay: number
|
|
29
|
+
interval: number
|
|
30
|
+
mouse: boolean
|
|
31
|
+
stop: boolean
|
|
32
|
+
prevent: boolean
|
|
33
|
+
capture: boolean
|
|
34
|
+
passive: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type QElement = HTMLElement & { __qTouchRepeatCleanup?: () => void }
|
|
38
|
+
|
|
39
|
+
function parseValue(value: unknown): { delay?: number; interval?: number } {
|
|
40
|
+
if (typeof value === "number") return { interval: value }
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
const [delay, interval] = value.split(":")
|
|
43
|
+
const out: { delay?: number; interval?: number } = {}
|
|
44
|
+
const d = Number(delay)
|
|
45
|
+
if (Number.isFinite(d)) out.delay = d
|
|
46
|
+
const i = Number(interval)
|
|
47
|
+
if (Number.isFinite(i)) out.interval = i
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
if (value && typeof value === "object") {
|
|
51
|
+
const o = value as Record<string, unknown>
|
|
52
|
+
const out: { delay?: number; interval?: number } = {}
|
|
53
|
+
const d = Number(o.delay)
|
|
54
|
+
if (Number.isFinite(d)) out.delay = d
|
|
55
|
+
const i = Number(o.interval)
|
|
56
|
+
if (Number.isFinite(i)) out.interval = i
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
return {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const vTouchRepeat = {
|
|
63
|
+
mounted(el: HTMLElement, binding: { value?: unknown; modifiers?: Record<string, boolean> }) {
|
|
64
|
+
const handler = binding.value
|
|
65
|
+
if (typeof handler !== "function") {
|
|
66
|
+
console.warn("[v-touch-repeat] value doit être une fonction (handler)")
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const m = binding.modifiers ?? {}
|
|
70
|
+
const parsed = parseValue(binding.value)
|
|
71
|
+
const options: RepeatOptions = {
|
|
72
|
+
handler: handler as TouchRepeatHandler,
|
|
73
|
+
delay: parsed.delay ?? DEFAULTS.delay,
|
|
74
|
+
interval: parsed.interval ?? DEFAULTS.interval,
|
|
75
|
+
mouse: !!m.mouse || !!m.mouseCapture,
|
|
76
|
+
stop: !!m.stop,
|
|
77
|
+
prevent: !!m.prevent,
|
|
78
|
+
capture: !!m.capture || !!m.mouseCapture,
|
|
79
|
+
passive: !!m.passive,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
el.style.touchAction = "none"
|
|
83
|
+
|
|
84
|
+
let active = false
|
|
85
|
+
let count = 0
|
|
86
|
+
let startTime = 0
|
|
87
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
88
|
+
let intervalTimer: ReturnType<typeof setInterval> | undefined
|
|
89
|
+
|
|
90
|
+
const stop = () => {
|
|
91
|
+
active = false
|
|
92
|
+
if (timer) clearTimeout(timer)
|
|
93
|
+
if (intervalTimer) clearInterval(intervalTimer)
|
|
94
|
+
timer = undefined
|
|
95
|
+
intervalTimer = undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const tick = () => {
|
|
99
|
+
count++
|
|
100
|
+
options.handler({
|
|
101
|
+
evt: lastEvt ?? new Event("touch-repeat"),
|
|
102
|
+
count,
|
|
103
|
+
elapsed: Math.round(performance.now() - startTime),
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let lastEvt: Event | null = null
|
|
108
|
+
|
|
109
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
110
|
+
if (active) return
|
|
111
|
+
if (e.pointerType === "mouse" && !options.mouse) return
|
|
112
|
+
if (options.prevent && !options.passive) e.preventDefault()
|
|
113
|
+
if (options.stop) e.stopPropagation()
|
|
114
|
+
active = true
|
|
115
|
+
count = 0
|
|
116
|
+
startTime = performance.now()
|
|
117
|
+
lastEvt = e
|
|
118
|
+
timer = setTimeout(() => {
|
|
119
|
+
tick()
|
|
120
|
+
intervalTimer = setInterval(tick, options.interval)
|
|
121
|
+
}, options.delay)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
125
|
+
lastEvt = e
|
|
126
|
+
stop()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const capture = options.capture
|
|
130
|
+
el.addEventListener("pointerdown", onPointerDown, { capture, passive: options.passive })
|
|
131
|
+
el.addEventListener("pointerup", onPointerUp, { capture })
|
|
132
|
+
el.addEventListener("pointercancel", onPointerUp, { capture })
|
|
133
|
+
el.addEventListener("pointerleave", onPointerUp, { capture })
|
|
134
|
+
|
|
135
|
+
;(el as QElement).__qTouchRepeatCleanup = () => {
|
|
136
|
+
el.removeEventListener("pointerdown", onPointerDown, { capture } as any)
|
|
137
|
+
el.removeEventListener("pointerup", onPointerUp, { capture } as any)
|
|
138
|
+
el.removeEventListener("pointercancel", onPointerUp, { capture } as any)
|
|
139
|
+
el.removeEventListener("pointerleave", onPointerUp, { capture } as any)
|
|
140
|
+
stop()
|
|
141
|
+
el.style.touchAction = ""
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
unmounted(el: HTMLElement) {
|
|
145
|
+
;(el as QElement).__qTouchRepeatCleanup?.()
|
|
146
|
+
delete (el as QElement).__qTouchRepeatCleanup
|
|
147
|
+
},
|
|
148
|
+
}
|