@dnax/ui 0.0.27 → 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/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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QTiptapTableView — NodeView Vue du nœud « table » : rend un vrai <table>
|
|
3
|
+
// avec le contentDOM ProseMirror dans le <tbody>. Les rangées/cellules restent
|
|
4
|
+
// du contenu ProseMirror classique (sélection, merge/split, clavier).
|
|
5
|
+
// Ajouts maison : colgroup + poignées de redimensionnement de colonnes
|
|
6
|
+
// (drag), appliqué localement via des largeurs en % (non persistées dans le
|
|
7
|
+
// document — pour persister il faudrait écrire les attrs colwidth des cellules).
|
|
8
|
+
import { computed, onBeforeUnmount, ref, watch } from "vue"
|
|
9
|
+
import { nodeViewProps, NodeViewContent, NodeViewWrapper } from "@tiptap/vue-3"
|
|
10
|
+
|
|
11
|
+
const props = defineProps(nodeViewProps)
|
|
12
|
+
|
|
13
|
+
// Nombre de colonnes = largeur de la première rangée (les suivantes suivent
|
|
14
|
+
// les rowspans/colspans du schéma).
|
|
15
|
+
const cols = computed(() => {
|
|
16
|
+
const first = props.node?.firstChild
|
|
17
|
+
return first ? first.childCount : 0
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Largeurs en % (initialement égales) ; recalculees quand le nombre de
|
|
21
|
+
// colonnes change (ajout/suppression).
|
|
22
|
+
const widths = ref<number[]>([])
|
|
23
|
+
watch(
|
|
24
|
+
cols,
|
|
25
|
+
(count) => {
|
|
26
|
+
if (count === widths.value.length) return
|
|
27
|
+
const each = count > 0 ? 100 / count : 0
|
|
28
|
+
widths.value = Array.from({ length: count }, () => each)
|
|
29
|
+
},
|
|
30
|
+
{ immediate: true },
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// — Drag de redimensionnement (poignées entre colonnes) —
|
|
34
|
+
const tableEl = ref<HTMLElement | null>(null)
|
|
35
|
+
let dragging = false
|
|
36
|
+
let boundary = 0
|
|
37
|
+
let startX = 0
|
|
38
|
+
let leftStart = 0
|
|
39
|
+
let rightStart = 0
|
|
40
|
+
let tableWidth = 0
|
|
41
|
+
|
|
42
|
+
const onMove = (event: PointerEvent) => {
|
|
43
|
+
if (!dragging) return
|
|
44
|
+
const delta = event.clientX - startX
|
|
45
|
+
const minPx = 48
|
|
46
|
+
let leftPx = leftStart + delta
|
|
47
|
+
let rightPx = rightStart - delta
|
|
48
|
+
if (leftPx < minPx) {
|
|
49
|
+
rightPx -= minPx - leftPx
|
|
50
|
+
leftPx = minPx
|
|
51
|
+
}
|
|
52
|
+
if (rightPx < minPx) {
|
|
53
|
+
leftPx -= minPx - rightPx
|
|
54
|
+
rightPx = minPx
|
|
55
|
+
}
|
|
56
|
+
if (leftPx < minPx || rightPx < minPx) return
|
|
57
|
+
const total = tableWidth || 1
|
|
58
|
+
const next = [...widths.value]
|
|
59
|
+
next[boundary - 1] = (leftPx / total) * 100
|
|
60
|
+
next[boundary] = (rightPx / total) * 100
|
|
61
|
+
widths.value = next
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const onUp = () => {
|
|
65
|
+
dragging = false
|
|
66
|
+
window.removeEventListener("pointermove", onMove)
|
|
67
|
+
window.removeEventListener("pointerup", onUp)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const startResize = (event: PointerEvent, boundaryIndex: number) => {
|
|
71
|
+
if (!props.editor.isEditable) return
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
const table = tableEl.value
|
|
74
|
+
if (!table) return
|
|
75
|
+
const row = table.querySelector("tr")
|
|
76
|
+
if (!row) return
|
|
77
|
+
const leftCell = row.children[boundaryIndex - 1] as HTMLElement | undefined
|
|
78
|
+
const rightCell = row.children[boundaryIndex] as HTMLElement | undefined
|
|
79
|
+
if (!leftCell || !rightCell) return
|
|
80
|
+
tableWidth = table.getBoundingClientRect().width || 1
|
|
81
|
+
leftStart = leftCell.getBoundingClientRect().width
|
|
82
|
+
rightStart = rightCell.getBoundingClientRect().width
|
|
83
|
+
startX = event.clientX
|
|
84
|
+
boundary = boundaryIndex
|
|
85
|
+
dragging = true
|
|
86
|
+
window.addEventListener("pointermove", onMove)
|
|
87
|
+
window.addEventListener("pointerup", onUp)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onBeforeUnmount(() => {
|
|
91
|
+
window.removeEventListener("pointermove", onMove)
|
|
92
|
+
window.removeEventListener("pointerup", onUp)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Position d'une poignée (bord droit de la colonne index) en %
|
|
96
|
+
const boundaryLeft = (index: number) => {
|
|
97
|
+
const acc = widths.value.slice(0, index).reduce((sum, w) => sum + w, 0)
|
|
98
|
+
return Math.min(acc, 99.5)
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<node-view-wrapper as="div" class="q-tiptap__table-wrapper">
|
|
104
|
+
<table ref="tableEl" class="q-tiptap__table" :class="{ 'q-tiptap__table--resizable': editor.isEditable }">
|
|
105
|
+
<colgroup v-if="widths.length">
|
|
106
|
+
<col v-for="(width, i) in widths" :key="i" :style="{ width: width + '%' }" />
|
|
107
|
+
</colgroup>
|
|
108
|
+
<node-view-content as="tbody" />
|
|
109
|
+
</table>
|
|
110
|
+
|
|
111
|
+
<!-- Poignées de redimensionnement entre les colonnes (éditable seulement) -->
|
|
112
|
+
<span
|
|
113
|
+
v-for="index in Math.max(cols - 1, 0)"
|
|
114
|
+
:key="'handle-' + index"
|
|
115
|
+
class="q-tiptap__resize-handle"
|
|
116
|
+
:style="{ left: boundaryLeft(index) + '%' }"
|
|
117
|
+
role="separator"
|
|
118
|
+
aria-orientation="vertical"
|
|
119
|
+
:aria-label="'Resize column ' + index"
|
|
120
|
+
@pointerdown.prevent.stop="startResize($event, index)"
|
|
121
|
+
/>
|
|
122
|
+
</node-view-wrapper>
|
|
123
|
+
</template>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QTiptapTaskItemView — NodeView Vue du nœud « taskItem » : rend une vraie
|
|
3
|
+
// <q-checkbox> dnax.ui (au lieu du <input> natif du DOM par défaut).
|
|
4
|
+
// Le nœud est fourni par l'extension officielle TaskItem (schéma, keymap
|
|
5
|
+
// Enter/Tab, input rules "[ ]" / "[x]") dont on ne remplace que addNodeView.
|
|
6
|
+
//
|
|
7
|
+
// ⚠ NodeView Vue : la RACINE DOIT être <node-view-wrapper> (erreur Tiptap
|
|
8
|
+
// « Please use the NodeViewWrapper component » sinon). En v3 ce wrapper ne
|
|
9
|
+
// propage pas les attrs → data-checked/class cochée portés par le div interne
|
|
10
|
+
// qui contient <node-view-content> (le contentDOM peut être imbriqué).
|
|
11
|
+
import { nodeViewProps, NodeViewContent, NodeViewWrapper } from "@tiptap/vue-3"
|
|
12
|
+
import QCheckbox from "../QCheckbox.vue"
|
|
13
|
+
|
|
14
|
+
// nodeViewProps : descripteurs runtime officiels (editor, node, updateAttributes,
|
|
15
|
+
// getPos…) — pattern documenté pour les NodeViews Vue dans Tiptap.
|
|
16
|
+
const props = defineProps(nodeViewProps)
|
|
17
|
+
|
|
18
|
+
const checked = () => !!props.node.attrs.checked
|
|
19
|
+
const isEditable = () => props.editor.isEditable
|
|
20
|
+
|
|
21
|
+
const onToggle = (value: unknown) => {
|
|
22
|
+
if (!isEditable()) return
|
|
23
|
+
if (typeof props.getPos !== "function") return
|
|
24
|
+
props.editor.chain().focus().command(({ tr }) => {
|
|
25
|
+
const position = props.getPos()
|
|
26
|
+
if (typeof position !== "number") return false
|
|
27
|
+
const current = tr.doc.nodeAt(position)
|
|
28
|
+
tr.setNodeMarkup(position, undefined, { ...current?.attrs, checked: Boolean(value) })
|
|
29
|
+
return true
|
|
30
|
+
}).run()
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<node-view-wrapper as="li">
|
|
36
|
+
<label class="q-tiptap__task-check" contenteditable="false" @mousedown.prevent>
|
|
37
|
+
<q-checkbox
|
|
38
|
+
:model-value="checked()"
|
|
39
|
+
dense
|
|
40
|
+
:disable="!isEditable()"
|
|
41
|
+
aria-label="Task done"
|
|
42
|
+
@update:model-value="onToggle"
|
|
43
|
+
/>
|
|
44
|
+
</label>
|
|
45
|
+
<div class="q-tiptap__task-content" :class="{ 'q-tiptap__task-content--checked': checked() }">
|
|
46
|
+
<node-view-content />
|
|
47
|
+
</div>
|
|
48
|
+
</node-view-wrapper>
|
|
49
|
+
</template>
|
package/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export { default as QDialogFooter } from "./components/QDialogFooter.vue"
|
|
|
44
44
|
export { default as QDialogHeader } from "./components/QDialogHeader.vue"
|
|
45
45
|
export { default as QDialogProvider } from "./components/QDialogProvider.vue"
|
|
46
46
|
export { default as QDialogTrigger } from "./components/QDialogTrigger.vue"
|
|
47
|
+
export { default as QEditorJs } from "./components/QEditorJs.vue"
|
|
47
48
|
export { default as QFab } from "./components/QFab.vue"
|
|
48
49
|
export { default as QFabAction } from "./components/QFabAction.vue"
|
|
49
50
|
export { default as QFilePicker } from "./components/QFilePicker.vue"
|
|
@@ -122,6 +123,7 @@ export { default as QTable } from "./components/QTable.vue"
|
|
|
122
123
|
export { default as QTabs } from "./components/QTabs.vue"
|
|
123
124
|
export { default as QText } from "./components/QText.vue"
|
|
124
125
|
export { default as QTextCaption } from "./components/QTextCaption.vue"
|
|
126
|
+
export { default as QTiptap } from "./components/QTiptap.vue"
|
|
125
127
|
export { default as QToolbar } from "./components/QToolbar.vue"
|
|
126
128
|
export { default as QTooltip } from "./components/QTooltip.vue"
|
|
127
129
|
export { default as QUploader } from "./components/QUploader.vue"
|
|
@@ -144,6 +146,8 @@ export { screen } from "./lib/screen"
|
|
|
144
146
|
export type { QScreen } from "./lib/screen"
|
|
145
147
|
export { loading } from "./lib/loading"
|
|
146
148
|
export type { LoadingOptions, QLoadingPlugin } from "./lib/loading"
|
|
149
|
+
export { localStorage, sessionStorage } from "./lib/storage"
|
|
150
|
+
export type { QWebStorage } from "./lib/storage"
|
|
147
151
|
export {
|
|
148
152
|
registerOverlay,
|
|
149
153
|
unregisterOverlay,
|
|
@@ -154,3 +158,11 @@ export {
|
|
|
154
158
|
} from "./lib/overlayBack"
|
|
155
159
|
export type { OverlayHandle } from "./lib/overlayBack"
|
|
156
160
|
export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
|
|
161
|
+
export { vTouchPan } from "./lib/touchPan"
|
|
162
|
+
export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
|
|
163
|
+
export { vTouchHold } from "./lib/touchHold"
|
|
164
|
+
export type { TouchHoldDetails, TouchHoldHandler } from "./lib/touchHold"
|
|
165
|
+
export { vTouchSwipe } from "./lib/touchSwipe"
|
|
166
|
+
export type { TouchSwipeDetails, TouchSwipeHandler, SwipeDirection } from "./lib/touchSwipe"
|
|
167
|
+
export { vTouchRepeat } from "./lib/touchRepeat"
|
|
168
|
+
export type { TouchRepeatDetails, TouchRepeatHandler } from "./lib/touchRepeat"
|
package/lib/icons.ts
CHANGED
|
@@ -35,4 +35,30 @@ export const icons = {
|
|
|
35
35
|
search: "lucide:search",
|
|
36
36
|
star: "lucide:star",
|
|
37
37
|
x: "lucide:x",
|
|
38
|
+
// — éditeur de texte (QTiptap) —
|
|
39
|
+
undo2: "lucide:undo-2",
|
|
40
|
+
redo2: "lucide:redo-2",
|
|
41
|
+
pilcrow: "lucide:pilcrow",
|
|
42
|
+
heading1: "lucide:heading-1",
|
|
43
|
+
heading2: "lucide:heading-2",
|
|
44
|
+
heading3: "lucide:heading-3",
|
|
45
|
+
bold: "lucide:bold",
|
|
46
|
+
italic: "lucide:italic",
|
|
47
|
+
underline: "lucide:underline",
|
|
48
|
+
strikethrough: "lucide:strikethrough",
|
|
49
|
+
code: "lucide:code",
|
|
50
|
+
link: "lucide:link",
|
|
51
|
+
unlink: "lucide:unlink",
|
|
52
|
+
list: "lucide:list",
|
|
53
|
+
listOrdered: "lucide:list-ordered",
|
|
54
|
+
quote: "lucide:quote",
|
|
55
|
+
codeXml: "lucide:code-xml",
|
|
56
|
+
removeFormatting: "lucide:remove-formatting",
|
|
57
|
+
palette: "lucide:palette",
|
|
58
|
+
eraser: "lucide:eraser",
|
|
59
|
+
alignLeft: "lucide:align-left",
|
|
60
|
+
alignCenter: "lucide:align-center",
|
|
61
|
+
alignRight: "lucide:align-right",
|
|
62
|
+
listChecks: "lucide:list-checks",
|
|
63
|
+
table: "lucide:table",
|
|
38
64
|
} as const
|
package/lib/q.ts
CHANGED
|
@@ -14,6 +14,7 @@ import QNotifyToast from "../components/QNotifyToast.vue"
|
|
|
14
14
|
import { platform, qBreakpoints } from "./platform"
|
|
15
15
|
import { screen } from "./screen"
|
|
16
16
|
import { loading } from "./loading"
|
|
17
|
+
import { localStorage, sessionStorage } from "./storage"
|
|
17
18
|
|
|
18
19
|
export interface DialogOptions {
|
|
19
20
|
/** Composant de dialogue (SFC importé) ou nom de composant global */
|
|
@@ -323,8 +324,9 @@ function notify(opts: NotifyOptions): NotifyController {
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
// API publique : $q.dialog / $q.bottomSheet / $q.notify / $q.imagePreview / $q.platform / $q.breakpoints / $q.screen / $q.loading
|
|
327
|
+
// / $q.localStorage / $q.sessionStorage (stockage web typé)
|
|
326
328
|
// Conventions Quasar : .open() pour les piles (dialog, bottomSheet, imagePreview),
|
|
327
|
-
// .show() pour notify et loading.
|
|
329
|
+
// .show() pour notify et loading ; méthodes directes pour le stockage.
|
|
328
330
|
const notifyPlugin = notify as typeof notify & { show: typeof notify }
|
|
329
331
|
notifyPlugin.show = notify
|
|
330
332
|
|
|
@@ -343,6 +345,8 @@ export const $q = {
|
|
|
343
345
|
breakpoints: qBreakpoints,
|
|
344
346
|
screen,
|
|
345
347
|
loading,
|
|
348
|
+
localStorage,
|
|
349
|
+
sessionStorage,
|
|
346
350
|
}
|
|
347
351
|
|
|
348
352
|
/** Accès programmatique aux plugins ($q.dialog, $q.notify, $q.loading…) */
|
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
|
+
})
|