@dnax/ui 0.0.27 → 0.0.29

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.
@@ -0,0 +1,175 @@
1
+ // Helpers purs du tableur QSpreadsheet — extraits du composant pour être
2
+ // testables unitairement (bun test). Aucune dépendance DOM / Vue.
3
+
4
+ /** Index de colonne (0-based) → lettres Excel : 0 → "A", 25 → "Z", 26 → "AA" */
5
+ export const colLetter = (i: number): string => {
6
+ let n = i
7
+ let s = ""
8
+ while (n >= 0) {
9
+ s = String.fromCharCode(65 + (n % 26)) + s
10
+ n = Math.floor(n / 26) - 1
11
+ }
12
+ return s
13
+ }
14
+
15
+ /** Lettres Excel → index de colonne 0-based : "A" → 0, "AA" → 26 */
16
+ export const colFromLetters = (s: string): number => {
17
+ let n = 0
18
+ for (const ch of s.toUpperCase()) n = n * 26 + (ch.charCodeAt(0) - 64)
19
+ return n - 1
20
+ }
21
+
22
+ /** Cellule « vide » : null, undefined ou chaîne vide */
23
+ export const isBlankValue = (v: any): boolean =>
24
+ v === null || v === undefined || v === ""
25
+
26
+ /**
27
+ * Décale les références A1 non absolues d'une formule de (dr, dc) —
28
+ * plages incluses, `$` préservés (copie / autofill).
29
+ */
30
+ export const shiftFormulaRefs = (src: string, dr: number, dc: number): string =>
31
+ src.replace(
32
+ /(\$?)([A-Za-z]{1,3})(\$?)(\d+)/g,
33
+ (_m: string, ca: string, L: string, ra: string, D: string) => {
34
+ const col = colFromLetters(L) + (ca === "$" ? 0 : dc)
35
+ const row = Number(D) - 1 + (ra === "$" ? 0 : dr)
36
+ if (col < 0 || row < 0) return _m
37
+ return ca + colLetter(col) + ra + (row + 1)
38
+ },
39
+ )
40
+
41
+ /** Date ISO AAAA-MM-JJ */
42
+ export const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
43
+
44
+ /** Ajoute des jours à une date ISO (UTC, évite le décalage de fuseau) */
45
+ export const isoAddDays = (iso: string, days: number): string => {
46
+ const d = new Date(iso + "T00:00:00Z")
47
+ d.setUTCDate(d.getUTCDate() + days)
48
+ return d.toISOString().slice(0, 10)
49
+ }
50
+
51
+ /** Échappe une valeur pour un CSV (quote si délimiteur / guillemet / newline) */
52
+ export const csvEscape = (s: unknown, delim: string): string => {
53
+ const str = s === null || s === undefined ? "" : String(s)
54
+ return str.includes(delim) || str.includes('"') || /[\n\r]/.test(str)
55
+ ? '"' + str.replace(/"/g, '""') + '"'
56
+ : str
57
+ }
58
+
59
+ /**
60
+ * Décode une ligne CSV (guillemets doublés, délimiteur multi-caractère géré
61
+ * comme un caractère unique ; retourne les cellules décodées).
62
+ */
63
+ export const csvSplitLine = (line: string, delim = ","): string[] => {
64
+ const out: string[] = []
65
+ let cur = ""
66
+ let inQ = false
67
+ let i = 0
68
+ const isDelim = (at: number) => line.startsWith(delim, at)
69
+ while (i < line.length) {
70
+ const ch = line[i]!
71
+ if (inQ) {
72
+ if (ch === '"') {
73
+ if (line[i + 1] === '"') {
74
+ cur += '"'
75
+ i += 2
76
+ continue
77
+ }
78
+ inQ = false
79
+ i++
80
+ continue
81
+ }
82
+ cur += ch
83
+ i++
84
+ continue
85
+ }
86
+ if (ch === '"') {
87
+ inQ = true
88
+ i++
89
+ continue
90
+ }
91
+ if (isDelim(i)) {
92
+ out.push(cur)
93
+ cur = ""
94
+ i += delim.length
95
+ continue
96
+ }
97
+ cur += ch
98
+ i++
99
+ }
100
+ out.push(cur)
101
+ return out
102
+ }
103
+
104
+ /**
105
+ * Parse un texte CSV complet (CRLF ou LF, guillemets) en lignes de cellules.
106
+ */
107
+ export const parseCsv = (text: string, delim = ","): string[][] => {
108
+ const lines: string[][] = []
109
+ const current: string[] = []
110
+ let cell = ""
111
+ let inQ = false
112
+ let sawDelim = false
113
+ let i = 0
114
+ const isDelim = (at: number) => text.startsWith(delim, at)
115
+ const pushCell = () => {
116
+ current.push(cell)
117
+ cell = ""
118
+ }
119
+ while (i < text.length) {
120
+ const ch = text[i]!
121
+ if (inQ) {
122
+ if (ch === '"') {
123
+ if (text[i + 1] === '"') {
124
+ cell += '"'
125
+ i += 2
126
+ continue
127
+ }
128
+ inQ = false
129
+ i++
130
+ continue
131
+ }
132
+ cell += ch
133
+ i++
134
+ continue
135
+ }
136
+ if (ch === '"') {
137
+ inQ = true
138
+ i++
139
+ continue
140
+ }
141
+ if (isDelim(i)) {
142
+ pushCell()
143
+ sawDelim = true
144
+ i += delim.length
145
+ continue
146
+ }
147
+ if (ch === "\n") {
148
+ pushCell()
149
+ lines.push(current.splice(0))
150
+ sawDelim = false
151
+ i++
152
+ continue
153
+ }
154
+ if (ch === "\r") {
155
+ // les retours \r\n sont déjà gérés par le \n
156
+ i++
157
+ continue
158
+ }
159
+ cell += ch
160
+ i++
161
+ }
162
+ pushCell()
163
+ const onlyTrailingNewline = current.length === 1 && current[0] === "" && !sawDelim
164
+ if (lines.length === 0 || !onlyTrailingNewline) lines.push(current)
165
+ return lines
166
+ }
167
+
168
+ /** Colonnes déduites des clés d'un jeu de lignes (type texte) */
169
+ export const deriveCols = (
170
+ rows: Record<string, any>[],
171
+ ): { name: string; type: "text" }[] => {
172
+ const keys = new Set<string>()
173
+ for (const r of rows) Object.keys(r).forEach((k) => keys.add(k))
174
+ return [...keys].map((k) => ({ name: k, type: "text" as const }))
175
+ }
package/lib/storage.ts ADDED
@@ -0,0 +1,175 @@
1
+ // $q.localStorage / $q.sessionStorage — stockage web façon Quasar
2
+ // (https://quasar.dev/quasar-plugins/web-storage).
3
+ //
4
+ // Les valeurs sont sérialisées avec leur type d'origine (Date, RegExp, number,
5
+ // boolean, string, objets/tableaux) grâce à un encodage préfixé — le format
6
+ // `__q_*` est volontairement IDENTIQUE à celui de Quasar : des clés écrites par
7
+ // une app Quasar restent lisibles par dnax.ui et inversement.
8
+ //
9
+ // Autres types (undefined, null, bigint, symbol, fonction) : stockés sous forme
10
+ // de string (comportement Quasar).
11
+ //
12
+ // SSR-safe : côté serveur (ou quand le Web Storage est indisponible — mode
13
+ // privé, iframe sandbox…), toutes les méthodes deviennent des no-op et
14
+ // getItem/getIndex/getKey renvoient null, getAll {}, getAllKeys [].
15
+
16
+ export interface QWebStorage {
17
+ /** true si la clé existe */
18
+ has(key: string): boolean
19
+ /** Alias de has() */
20
+ hasItem(key: string): boolean
21
+ /** Nombre de clés stockées */
22
+ getLength(): number
23
+ /** Valeur (typée) de la clé, ou null si absente */
24
+ getItem<T = unknown>(key: string): T | null
25
+ /** Valeur (typée) stockée à l'index donné (ordre du Web Storage), ou null */
26
+ getIndex<T = unknown>(index: number): T | null
27
+ /** Clé stockée à l'index donné, ou null */
28
+ getKey(index: number): string | null
29
+ /** Toutes les entrées : { clé: valeur typée } */
30
+ getAll(): Record<string, unknown>
31
+ /** Toutes les clés, dans l'ordre du Web Storage */
32
+ getAllKeys(): string[]
33
+ /** Stocke une valeur (sérialisation typée) — alias de setItem() */
34
+ set(key: string, value: unknown): void
35
+ /** Stocke une valeur (sérialisation typée) */
36
+ setItem(key: string, value: unknown): void
37
+ /** Supprime la clé — alias de removeItem() */
38
+ remove(key: string): void
39
+ /** Supprime la clé */
40
+ removeItem(key: string): void
41
+ /** Vide tout le stockage */
42
+ clear(): void
43
+ /** true si le stockage est vide */
44
+ isEmpty(): boolean
45
+ }
46
+
47
+ // — Sérialisation typée (format compatible Quasar) —
48
+ // Encode : `__q_<type>|<payload>` — tout type supporté ré-encode ses données
49
+ // pour que le Web Storage (qui ne stocke que des strings) restitue le type.
50
+
51
+ function encode(value: unknown): string {
52
+ if (value instanceof Date) return `__q_date|${value.getTime()}`
53
+ if (value instanceof RegExp) return `__q_expr|${value.source}`
54
+ switch (typeof value) {
55
+ case "number": return `__q_numb|${value}`
56
+ case "boolean": return `__q_bool|${value ? "1" : "0"}`
57
+ case "string": return `__q_strn|${value}`
58
+ case "function": return `__q_strn|${value.toString()}`
59
+ case "object":
60
+ if (value !== null) return `__q_objt|${JSON.stringify(value)}`
61
+ break
62
+ }
63
+ // Type non supporté (undefined, null, bigint, symbol…) : stocké tel quel
64
+ // (le Web Storage le convertit en string — même résultat côté lecture).
65
+ return String(value)
66
+ }
67
+
68
+ const NUMBER_RE = /^-?\d+$/
69
+
70
+ function decode(value: string): unknown {
71
+ // Valeur non encodée par nous (< préfixe, ou préfixe inconnu) : telle quelle
72
+ if (value.length < 9) return value
73
+ const type = value.slice(0, 8)
74
+ const source = value.slice(9)
75
+
76
+ switch (type) {
77
+ case "__q_date": {
78
+ // Horodatage numérique (format actuel) ou string ISO (anciens formats)
79
+ return new Date(NUMBER_RE.test(source) ? Number.parseInt(source, 10) : source)
80
+ }
81
+ case "__q_expr":
82
+ return new RegExp(source)
83
+ case "__q_numb":
84
+ return Number(source)
85
+ case "__q_bool":
86
+ return source === "1"
87
+ case "__q_strn":
88
+ return source
89
+ case "__q_objt": {
90
+ try {
91
+ return JSON.parse(source)
92
+ }
93
+ catch {
94
+ // Payload corrompu → valeur brute plutôt que de faire planter getItem()
95
+ return value
96
+ }
97
+ }
98
+ default:
99
+ return value
100
+ }
101
+ }
102
+
103
+ // Instance no-op pour SSR / Web Storage indisponible (miroir de Quasar :
104
+ // getEmptyStorage) — jamais de throw ni d'accès à window.
105
+ function emptyStorage(): QWebStorage {
106
+ const noop = () => {}
107
+ return {
108
+ has: () => false,
109
+ hasItem: () => false,
110
+ getLength: () => 0,
111
+ getItem: () => null,
112
+ getIndex: () => null,
113
+ getKey: () => null,
114
+ getAll: () => ({}),
115
+ getAllKeys: () => [],
116
+ set: noop,
117
+ setItem: noop,
118
+ remove: noop,
119
+ removeItem: noop,
120
+ clear: noop,
121
+ isEmpty: () => true,
122
+ }
123
+ }
124
+
125
+ function init(type: "local" | "session"): QWebStorage {
126
+ try {
127
+ if (typeof window === "undefined") return emptyStorage()
128
+ const ws = type === "local" ? window.localStorage : window.sessionStorage
129
+ if (!ws) return emptyStorage()
130
+
131
+ const get = (key: string): unknown => {
132
+ const item = ws.getItem(key)
133
+ return item === null ? null : decode(item)
134
+ }
135
+
136
+ return {
137
+ has: (key) => ws.getItem(key) !== null,
138
+ hasItem: (key) => ws.getItem(key) !== null,
139
+ getLength: () => ws.length,
140
+ getItem: <T = unknown>(key: string): T | null => get(key) as T | null,
141
+ getIndex: <T = unknown>(index: number): T | null =>
142
+ index < ws.length ? (get(ws.key(index) as string) as T | null) : null,
143
+ getKey: (index) => (index < ws.length ? ws.key(index) : null),
144
+ getAll: () => {
145
+ const result: Record<string, unknown> = {}
146
+ for (let i = 0; i < ws.length; i++) {
147
+ const key = ws.key(i) as string
148
+ result[key] = get(key)
149
+ }
150
+ return result
151
+ },
152
+ getAllKeys: () => {
153
+ const result: string[] = []
154
+ for (let i = 0; i < ws.length; i++) result.push(ws.key(i) as string)
155
+ return result
156
+ },
157
+ set: (key, value) => ws.setItem(key, encode(value)),
158
+ setItem: (key, value) => ws.setItem(key, encode(value)),
159
+ remove: (key) => ws.removeItem(key),
160
+ removeItem: (key) => ws.removeItem(key),
161
+ clear: () => ws.clear(),
162
+ isEmpty: () => ws.length === 0,
163
+ }
164
+ }
165
+ catch {
166
+ // Accès bloqué (mode privé, cookies désactivés…) → instance no-op
167
+ return emptyStorage()
168
+ }
169
+ }
170
+
171
+ /** $q.localStorage — stockage persistant (API Quasar Web Storage) */
172
+ export const localStorage: QWebStorage = init("local")
173
+
174
+ /** $q.sessionStorage — stockage par session (API Quasar Web Storage) */
175
+ export const sessionStorage: QWebStorage = init("session")
@@ -0,0 +1,19 @@
1
+ // Tables Tiptap pour QTiptap — le nœud « table » est rendu par une NodeView
2
+ // Vue (QTiptapTableView) au lieu du rendu DOM/colgroup par défaut. On garde
3
+ // les nœuds officiels TableRow/TableHeader/TableCell (schéma, keymaps,
4
+ // commandes) ; on ne remplace que le addNodeView de « table ».
5
+ import { Table, TableHeader, TableCell, TableRow } from "@tiptap/extension-table"
6
+ import { VueNodeViewRenderer } from "@tiptap/vue-3"
7
+ import type { NodeViewProps } from "@tiptap/vue-3"
8
+ import type { Component } from "vue"
9
+ import QTiptapTableView from "../components/internal/QTiptapTableView.vue"
10
+
11
+ export { TableHeader, TableCell, TableRow }
12
+
13
+ export const QTipTapTable = Table.extend({
14
+ addNodeView() {
15
+ // defineProps(nodeViewProps) ne fait pas remonter les types vers le renderer
16
+ // → cast vers Component<NodeViewProps>
17
+ return VueNodeViewRenderer(QTiptapTableView as Component<NodeViewProps>)
18
+ },
19
+ })
@@ -0,0 +1,16 @@
1
+ // Task list Tiptap pour QTiptap — l'élément de liste est rendu par une
2
+ // NodeView Vue (q-checkbox dnax.ui) : on garde l'extension officielle
3
+ // TaskItem (schéma, parse/render, keymap, input rules) et on ne remplace que
4
+ // son addNodeView par VueNodeViewRenderer(QTiptapTaskItemView).
5
+ import TaskList from "@tiptap/extension-task-list"
6
+ import TaskItem from "@tiptap/extension-task-item"
7
+ import { VueNodeViewRenderer } from "@tiptap/vue-3"
8
+ import QTiptapTaskItemView from "../components/internal/QTiptapTaskItemView.vue"
9
+
10
+ export { TaskList }
11
+
12
+ export const QTipTapTaskItem = TaskItem.extend({
13
+ addNodeView() {
14
+ return VueNodeViewRenderer(QTiptapTaskItemView)
15
+ },
16
+ })
@@ -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
+ }