@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.
- package/components/QBar.vue +42 -0
- package/components/QBoard.vue +285 -0
- package/components/QConfigProvider.vue +9 -5
- package/components/QDialog.vue +26 -12
- package/components/QEditorJs.vue +146 -0
- package/components/QFab.vue +19 -2
- package/components/QInteract.vue +387 -0
- package/components/QSpreadsheet.vue +4662 -0
- 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 +18 -0
- package/lib/config.ts +6 -0
- package/lib/formula.test.ts +206 -0
- package/lib/formula.ts +292 -0
- package/lib/icons.ts +43 -0
- package/lib/intersection.ts +118 -0
- package/lib/q.ts +5 -1
- package/lib/spreadsheet.test.ts +92 -0
- package/lib/spreadsheet.ts +175 -0
- 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/lib/useComponentProps.ts +7 -0
- package/module.ts +10 -0
- package/package.json +27 -1
- package/scripts/generate-exports.ts +12 -0
- package/styles/main.css +2217 -32
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QBar — barre fine façon Quasar : ligne compacte en haut d'une fenêtre /
|
|
3
|
+
// d'une app (contrôles fenêtre, menus, statut…). Utile pour Electron
|
|
4
|
+
// frameless, entêtes de dialogs desktop, etc. API : <q-bar dense dark>…
|
|
5
|
+
import type { Component } from "vue"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Compact (hauteur réduite) */
|
|
9
|
+
dense?: boolean
|
|
10
|
+
/** Force le style sombre (fond foncé, texte clair) */
|
|
11
|
+
dark?: boolean
|
|
12
|
+
/** Balise rendue (défaut : div) */
|
|
13
|
+
as?: Component | string
|
|
14
|
+
/** Rôle ARIA (défaut : toolbar) — passer "" pour aucun */
|
|
15
|
+
role?: string
|
|
16
|
+
/** aria-label du toolbar */
|
|
17
|
+
label?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
dense: false,
|
|
22
|
+
dark: false,
|
|
23
|
+
as: "div",
|
|
24
|
+
role: "toolbar",
|
|
25
|
+
label: "Bar",
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<component
|
|
31
|
+
:is="props.as"
|
|
32
|
+
class="q-bar"
|
|
33
|
+
:class="{
|
|
34
|
+
'q-bar--dense': dense,
|
|
35
|
+
'q-bar--dark': dark,
|
|
36
|
+
}"
|
|
37
|
+
:role="role || undefined"
|
|
38
|
+
:aria-label="role === 'toolbar' ? label : undefined"
|
|
39
|
+
>
|
|
40
|
+
<slot />
|
|
41
|
+
</component>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// QBoard — tableau de bord type Power BI : une grille de tuiles en arrière-plan
|
|
3
|
+
// (colonnes × rangées) et, À L'INTÉRIEUR, QInteract qui gère le drag & resize
|
|
4
|
+
// des widgets. Les items sont exprimés en TUILLES :
|
|
5
|
+
// { id, column, row, spanColumns, spanRows, … }
|
|
6
|
+
// QBoard traduit ces tuiles en pixels pour QInteract et reconvertit les
|
|
7
|
+
// positions déplacées / redimensionnées en tuiles (v-model:items).
|
|
8
|
+
export interface QBoardItem {
|
|
9
|
+
/** Identifiant unique */
|
|
10
|
+
id: string | number
|
|
11
|
+
/** Colonne de départ (0-based) */
|
|
12
|
+
column: number
|
|
13
|
+
/** Rangée de départ (0-based) */
|
|
14
|
+
row: number
|
|
15
|
+
/** Nombre de colonnes occupées */
|
|
16
|
+
spanColumns: number
|
|
17
|
+
/** Nombre de rangées occupées */
|
|
18
|
+
spanRows: number
|
|
19
|
+
/** Données métier libres (title, type de widget, …) */
|
|
20
|
+
[key: string]: any
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
26
|
+
import QInteract from "./QInteract.vue"
|
|
27
|
+
import type { QInteractItem } from "./QInteract.vue"
|
|
28
|
+
import { cn } from "../lib/utils"
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
/** Tuiles (v-model:items) : [{ id, column, row, spanColumns, spanRows, … }] */
|
|
32
|
+
items?: QBoardItem[]
|
|
33
|
+
/** Tuile sélectionnée (v-model:selected) */
|
|
34
|
+
selected?: string | number | null
|
|
35
|
+
/** Nombre de colonnes de la grille (défaut 12) */
|
|
36
|
+
columns?: number
|
|
37
|
+
/** Hauteur d'une rangée en px (défaut 64) */
|
|
38
|
+
rowHeight?: number
|
|
39
|
+
/** Nombre de rangées visibles (défaut : calculé depuis les tuiles) */
|
|
40
|
+
rows?: number
|
|
41
|
+
/** Hauteur du conteneur (px ou CSS) — sinon auto depuis rows */
|
|
42
|
+
height?: string | number
|
|
43
|
+
/** Marge intérieure (px) */
|
|
44
|
+
padding?: number
|
|
45
|
+
/** Espacement entre les tuiles (px, défaut 6) */
|
|
46
|
+
gap?: number
|
|
47
|
+
/** Autorise drag & resize (défaut true) */
|
|
48
|
+
interactive?: boolean
|
|
49
|
+
/** Lecture seule : drag & resize désactivés, poignées masquées */
|
|
50
|
+
readonly?: boolean
|
|
51
|
+
/** Drag limité à la poignée haute */
|
|
52
|
+
handle?: boolean
|
|
53
|
+
/** Empêche le chevauchement des tuiles (défaut true) */
|
|
54
|
+
collision?: boolean
|
|
55
|
+
/** Affiche la numérotation des tuiles de fond */
|
|
56
|
+
showLabels?: boolean
|
|
57
|
+
dense?: boolean
|
|
58
|
+
dark?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
62
|
+
items: () => [],
|
|
63
|
+
selected: null,
|
|
64
|
+
columns: 12,
|
|
65
|
+
rowHeight: 64,
|
|
66
|
+
rows: 0,
|
|
67
|
+
height: undefined,
|
|
68
|
+
padding: 10,
|
|
69
|
+
gap: 6,
|
|
70
|
+
interactive: true,
|
|
71
|
+
readonly: false,
|
|
72
|
+
handle: false,
|
|
73
|
+
collision: true,
|
|
74
|
+
showLabels: false,
|
|
75
|
+
dense: false,
|
|
76
|
+
dark: false,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const emit = defineEmits<{
|
|
80
|
+
"update:items": [value: QBoardItem[]]
|
|
81
|
+
"update:selected": [value: string | number | null]
|
|
82
|
+
select: [item: QBoardItem]
|
|
83
|
+
"drag-end": [item: QBoardItem]
|
|
84
|
+
"resize-end": [item: QBoardItem]
|
|
85
|
+
}>()
|
|
86
|
+
|
|
87
|
+
const grid = ref<QBoardItem[]>([])
|
|
88
|
+
watch(
|
|
89
|
+
() => props.items,
|
|
90
|
+
(v) => {
|
|
91
|
+
if (v && v !== grid.value) grid.value = v.map((it) => ({ ...it }))
|
|
92
|
+
},
|
|
93
|
+
{ immediate: true },
|
|
94
|
+
)
|
|
95
|
+
const push = (next: QBoardItem[]) => {
|
|
96
|
+
grid.value = next
|
|
97
|
+
emit("update:items", next)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const sel = ref<string | number | null>(props.selected)
|
|
101
|
+
watch(
|
|
102
|
+
() => props.selected,
|
|
103
|
+
(v) => {
|
|
104
|
+
sel.value = v
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// ─── Largeur réelle (Responsive : la cellule s'adapte à la largeur) ───
|
|
109
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
110
|
+
const widthPx = ref(0)
|
|
111
|
+
let ro: ResizeObserver | null = null
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
if (rootEl.value) widthPx.value = rootEl.value.clientWidth
|
|
114
|
+
if (typeof ResizeObserver !== "undefined" && rootEl.value) {
|
|
115
|
+
ro = new ResizeObserver((entries) => {
|
|
116
|
+
const w = entries[0]?.contentRect.width ?? 0
|
|
117
|
+
if (w !== widthPx.value) widthPx.value = w
|
|
118
|
+
})
|
|
119
|
+
ro.observe(rootEl.value)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
onBeforeUnmount(() => ro?.disconnect())
|
|
123
|
+
|
|
124
|
+
const innerWidth = computed(() => Math.max(1, widthPx.value - props.padding * 2))
|
|
125
|
+
const cellW = computed(() => innerWidth.value / Math.max(1, props.columns))
|
|
126
|
+
|
|
127
|
+
// ─── Hauteur ───
|
|
128
|
+
const usedRows = computed(() => {
|
|
129
|
+
if (props.rows > 0) return props.rows
|
|
130
|
+
let max = 1
|
|
131
|
+
for (const it of grid.value) max = Math.max(max, it.row + (it.spanRows || 1))
|
|
132
|
+
return max
|
|
133
|
+
})
|
|
134
|
+
const heightStyle = computed<Record<string, string>>(() =>
|
|
135
|
+
props.height !== undefined
|
|
136
|
+
? { height: typeof props.height === "number" ? props.height + "px" : props.height }
|
|
137
|
+
: { height: usedRows.value * props.rowHeight + props.padding * 2 + "px" },
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// ─── Tuiles → pixels (pour QInteract) ───
|
|
141
|
+
const pxItems = ref<QInteractItem[]>([])
|
|
142
|
+
const clampCol = (c: number) => Math.max(0, Math.min(props.columns - 1, c))
|
|
143
|
+
const buildPx = () => {
|
|
144
|
+
pxItems.value = grid.value.map((it) => {
|
|
145
|
+
const col = Math.min(it.column, props.columns - 1)
|
|
146
|
+
return {
|
|
147
|
+
...it,
|
|
148
|
+
x: Math.round(col * cellW.value) + props.padding,
|
|
149
|
+
y: Math.round(it.row * props.rowHeight) + props.padding,
|
|
150
|
+
w: Math.round((it.spanColumns || 1) * cellW.value),
|
|
151
|
+
h: Math.round((it.spanRows || 1) * props.rowHeight),
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
watch([grid, cellW, () => props.rowHeight, () => props.padding], buildPx, { immediate: true })
|
|
156
|
+
watch(innerWidth, () => nextTickPx())
|
|
157
|
+
|
|
158
|
+
let pending = false
|
|
159
|
+
const nextTickPx = () => {
|
|
160
|
+
if (pending) return
|
|
161
|
+
pending = true
|
|
162
|
+
nextTick(() => {
|
|
163
|
+
pending = false
|
|
164
|
+
buildPx()
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── QInteract (pixels) → tuiles ───
|
|
169
|
+
const toGridItem = (px: QInteractItem): QBoardItem => {
|
|
170
|
+
const cw = cellW.value || 1
|
|
171
|
+
const column = clampCol(Math.round((px.x - props.padding) / cw))
|
|
172
|
+
const spanColumns = Math.max(1, Math.round(px.w / cw))
|
|
173
|
+
const row = Math.max(0, Math.round((px.y - props.padding) / props.rowHeight))
|
|
174
|
+
const spanRows = Math.max(1, Math.round(px.h / props.rowHeight))
|
|
175
|
+
return { ...px, column, row, spanColumns, spanRows } as QBoardItem
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const onInteractItems = (pxList: QInteractItem[]) => {
|
|
179
|
+
const next = pxList.map(toGridItem)
|
|
180
|
+
// recentre les tuiles si elles débordent de la grille
|
|
181
|
+
for (const it of next) {
|
|
182
|
+
it.column = clampCol(Math.min(it.column, props.columns - (it.spanColumns || 1)))
|
|
183
|
+
}
|
|
184
|
+
push(next)
|
|
185
|
+
buildPx()
|
|
186
|
+
}
|
|
187
|
+
const onSelect = (px: QInteractItem) => {
|
|
188
|
+
const g = toGridItem(px)
|
|
189
|
+
sel.value = g.id
|
|
190
|
+
emit("update:selected", g.id)
|
|
191
|
+
emit("select", g)
|
|
192
|
+
}
|
|
193
|
+
const forwardDragEnd = (px: QInteractItem) => {
|
|
194
|
+
emit("drag-end", toGridItem(px))
|
|
195
|
+
}
|
|
196
|
+
const forwardResizeEnd = (px: QInteractItem) => {
|
|
197
|
+
emit("resize-end", toGridItem(px))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const gridToUser = (g: QBoardItem) => g
|
|
201
|
+
const userOf = (px: QInteractItem) => toGridItem(px)
|
|
202
|
+
|
|
203
|
+
const gridStyle = computed<Record<string, string>>(() => ({
|
|
204
|
+
backgroundSize: Math.max(1, cellW.value) + "px " + props.rowHeight + "px",
|
|
205
|
+
}))
|
|
206
|
+
|
|
207
|
+
const rootClasses = computed(() =>
|
|
208
|
+
cn("q-board", props.dark && "q-board--dark", props.dense && "q-board--dense"),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// ─── API ───
|
|
212
|
+
const addItem = (item: QBoardItem) => {
|
|
213
|
+
push([...grid.value, { ...item }])
|
|
214
|
+
sel.value = item.id
|
|
215
|
+
emit("update:selected", item.id)
|
|
216
|
+
}
|
|
217
|
+
const removeItem = (id: string | number) => {
|
|
218
|
+
push(grid.value.filter((it) => String(it.id) !== String(id)))
|
|
219
|
+
if (String(sel.value) === String(id)) sel.value = null
|
|
220
|
+
}
|
|
221
|
+
const clear = () => push([])
|
|
222
|
+
const layout = () => grid.value.map((it) => ({ ...it }))
|
|
223
|
+
|
|
224
|
+
defineExpose({
|
|
225
|
+
addItem,
|
|
226
|
+
removeItem,
|
|
227
|
+
clear,
|
|
228
|
+
layout,
|
|
229
|
+
getItems: () => grid.value.map((it) => ({ ...it })),
|
|
230
|
+
columns: () => props.columns,
|
|
231
|
+
rows: () => usedRows.value,
|
|
232
|
+
})
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
<template>
|
|
236
|
+
<div ref="rootEl" class="q-board" :class="rootClasses" :style="heightStyle">
|
|
237
|
+
<!-- ═══════ Grille de tuiles en arrière-plan ═══════ -->
|
|
238
|
+
<div class="q-board__grid" :style="gridStyle" aria-hidden="true">
|
|
239
|
+
<template v-if="showLabels">
|
|
240
|
+
<span
|
|
241
|
+
v-for="c in columns"
|
|
242
|
+
:key="'c' + c"
|
|
243
|
+
class="q-board__cell-label q-board__cell-label--col"
|
|
244
|
+
:style="{ left: props.padding + (c - 1) * cellW + 'px', width: cellW + 'px' }"
|
|
245
|
+
>{{ c }}</span>
|
|
246
|
+
<span
|
|
247
|
+
v-for="r in usedRows"
|
|
248
|
+
:key="'r' + r"
|
|
249
|
+
class="q-board__cell-label q-board__cell-label--row"
|
|
250
|
+
:style="{ top: props.padding + (r - 1) * rowHeight + 'px', height: rowHeight + 'px' }"
|
|
251
|
+
>{{ r }}</span>
|
|
252
|
+
</template>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<!-- ═══════ QInteract à l'intérieur (drag & resize) ═══════ -->
|
|
256
|
+
<QInteract
|
|
257
|
+
class="q-board__interact"
|
|
258
|
+
:items="pxItems"
|
|
259
|
+
:selected="sel"
|
|
260
|
+
:interactive="interactive"
|
|
261
|
+
:readonly="readonly"
|
|
262
|
+
:handle="handle"
|
|
263
|
+
:snap="1"
|
|
264
|
+
:collision="collision"
|
|
265
|
+
:padding="props.padding"
|
|
266
|
+
:gap="gap"
|
|
267
|
+
:min-width="cellW"
|
|
268
|
+
:min-height="rowHeight"
|
|
269
|
+
:dense="dense"
|
|
270
|
+
@update:items="onInteractItems"
|
|
271
|
+
@update:selected="(v) => (sel = v)"
|
|
272
|
+
@select="onSelect"
|
|
273
|
+
@drag-end="forwardDragEnd"
|
|
274
|
+
@resize-end="forwardResizeEnd"
|
|
275
|
+
>
|
|
276
|
+
<template #item="{ item }">
|
|
277
|
+
<slot name="item" :item="userOf(item)" />
|
|
278
|
+
</template>
|
|
279
|
+
<template #handle="{ item }">
|
|
280
|
+
<slot name="handle" :item="userOf(item)" />
|
|
281
|
+
</template>
|
|
282
|
+
<slot name="empty" />
|
|
283
|
+
</QInteract>
|
|
284
|
+
</div>
|
|
285
|
+
</template>
|
|
@@ -12,7 +12,7 @@ import QLoadingProvider from "./QLoadingProvider.vue"
|
|
|
12
12
|
import QBottomSheetProvider from "./QBottomSheetProvider.vue"
|
|
13
13
|
import QImagePreviewProvider from "./QImagePreviewProvider.vue"
|
|
14
14
|
import { qConfigKey, qProvidersKey } from "../lib/config"
|
|
15
|
-
import type { QConfigContext, QTheme, ThemeMode } from "../lib/config"
|
|
15
|
+
import type { QAppLang, QConfigContext, QTheme, ThemeMode } from "../lib/config"
|
|
16
16
|
import { isRadiusScale, RADIUS_VALUES } from "../lib/useComponentProps"
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
@@ -23,6 +23,8 @@ interface Props {
|
|
|
23
23
|
* :theme="{ mode: 'dark', colors: { primary: '#ff0000' }, componentProps: { QBtn: { radius: 'md' } } }"
|
|
24
24
|
*/
|
|
25
25
|
theme?: QTheme | ThemeMode
|
|
26
|
+
/** Langue appliquée aux composants qui la supportent ("en" | "fr") */
|
|
27
|
+
lang?: QAppLang
|
|
26
28
|
/** Rend un div conteneur ; sinon fournit le thème sans élément DOM */
|
|
27
29
|
render?: boolean
|
|
28
30
|
/** Classe(s) additionnelle(s) sur le conteneur */
|
|
@@ -33,6 +35,7 @@ interface Props {
|
|
|
33
35
|
|
|
34
36
|
const props = withDefaults(defineProps<Props>(), {
|
|
35
37
|
theme: () => ({}),
|
|
38
|
+
lang: undefined,
|
|
36
39
|
render: true,
|
|
37
40
|
class: "",
|
|
38
41
|
style: undefined,
|
|
@@ -67,9 +70,13 @@ const mergedTheme = computed<QTheme>(() => {
|
|
|
67
70
|
mode: self.mode ?? parentTheme?.mode ?? "system",
|
|
68
71
|
colors: { ...parentTheme?.colors, ...self.colors },
|
|
69
72
|
componentProps: mergeComponentProps(parentTheme?.componentProps, self.componentProps),
|
|
73
|
+
lang: props.lang ?? self.lang ?? parentTheme?.lang ?? "en",
|
|
70
74
|
}
|
|
71
75
|
})
|
|
72
76
|
|
|
77
|
+
/** Langue effective : prop > theme.lang > parent */
|
|
78
|
+
const lang = computed<QAppLang>(() => mergedTheme.value.lang ?? "en")
|
|
79
|
+
|
|
73
80
|
// — Mode clair/sombre —
|
|
74
81
|
const systemDark = ref(false)
|
|
75
82
|
let mql: MediaQueryList | null = null
|
|
@@ -100,9 +107,6 @@ const isDark = computed<boolean>(() => {
|
|
|
100
107
|
if (mode === "light") return false
|
|
101
108
|
return systemDark.value
|
|
102
109
|
})
|
|
103
|
-
|
|
104
|
-
// Applique .dark sur le conteneur ET sur <html> (les overlays téléportés au body
|
|
105
|
-
// — dialog, sheets — doivent aussi passer en sombre)
|
|
106
110
|
watch(
|
|
107
111
|
isDark,
|
|
108
112
|
(dark) => {
|
|
@@ -113,7 +117,7 @@ watch(
|
|
|
113
117
|
{ immediate: true },
|
|
114
118
|
)
|
|
115
119
|
|
|
116
|
-
provide<QConfigContext>(qConfigKey, { theme: mergedTheme, isDark })
|
|
120
|
+
provide<QConfigContext>(qConfigKey, { theme: mergedTheme, isDark, lang })
|
|
117
121
|
|
|
118
122
|
// Providers intégrés ($q.dialog + $q.notify) : rendus UNE fois par le
|
|
119
123
|
// QConfigProvider le plus externe (les imbriqués ne re-rendent pas → pas de doublons)
|
package/components/QDialog.vue
CHANGED
|
@@ -41,10 +41,18 @@ interface Props {
|
|
|
41
41
|
contentStyle?: StyleValue
|
|
42
42
|
noBackdropDismiss?: boolean
|
|
43
43
|
noEscDismiss?: boolean
|
|
44
|
-
/** Animation : fade | zoom | slide-up | slide-down |
|
|
45
|
-
|
|
44
|
+
/** Animation : fade | zoom | slide-up | slide-down | sheet-up | sheet-down
|
|
45
|
+
* (même glissement que slide, ouverture qui décélère en fin de course) |
|
|
46
|
+
* slide-left | slide-right | swipe-left | swipe-right (sinon basée sur position) */
|
|
47
|
+
transition?: "fade" | "zoom" | "slide-up" | "slide-down" | "sheet-up" | "sheet-down" | "slide-left" | "slide-right" | "swipe-left" | "swipe-right"
|
|
46
48
|
/** Durée des transitions d'entrée/sortie en ms (défaut CSS : ~200ms entrée, ~150ms sortie) */
|
|
47
49
|
transitionDuration?: number
|
|
50
|
+
/** Courbe d'easing de l'OUVERTURE — valeur CSS ("cubic-bezier(0.32, 0.72, 0, 1)",
|
|
51
|
+
* "ease-out", "linear"…) — défaut : courbe propre à chaque transition
|
|
52
|
+
* (sheet-* = décélération en fin de course, slide-* = ease…) */
|
|
53
|
+
transitionEasingEnter?: string
|
|
54
|
+
/** Courbe d'easing de la FERMETURE — même principe que transition-easing-enter */
|
|
55
|
+
transitionEasingLeave?: string
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -60,6 +68,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
60
68
|
noBackdropDismiss: false,
|
|
61
69
|
noEscDismiss: false,
|
|
62
70
|
transitionDuration: undefined,
|
|
71
|
+
transitionEasingEnter: "",
|
|
72
|
+
transitionEasingLeave: "",
|
|
63
73
|
})
|
|
64
74
|
|
|
65
75
|
const emit = defineEmits<{
|
|
@@ -159,15 +169,19 @@ const transitionName = computed(() =>
|
|
|
159
169
|
props.transition ? `q-dialog-${props.transition}` : "q-dialog",
|
|
160
170
|
)
|
|
161
171
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
// Réglages des transitions (durée + courbes d'easing) exposés en variables CSS
|
|
173
|
+
// sur l'overlay : --q-dialog-duration-enter/leave et
|
|
174
|
+
// --q-dialog-easing-enter/leave (consommées par styles/main.css).
|
|
175
|
+
const transitionStyle = computed<StyleValue>(() => {
|
|
176
|
+
const style: Record<string, string> = {}
|
|
177
|
+
if (props.transitionDuration) {
|
|
178
|
+
style["--q-dialog-duration-enter"] = `${props.transitionDuration}ms`
|
|
179
|
+
style["--q-dialog-duration-leave"] = `${props.transitionDuration}ms`
|
|
180
|
+
}
|
|
181
|
+
if (props.transitionEasingEnter) style["--q-dialog-easing-enter"] = props.transitionEasingEnter
|
|
182
|
+
if (props.transitionEasingLeave) style["--q-dialog-easing-leave"] = props.transitionEasingLeave
|
|
183
|
+
return style
|
|
184
|
+
})
|
|
171
185
|
</script>
|
|
172
186
|
|
|
173
187
|
<template>
|
|
@@ -178,7 +192,7 @@ const durationStyle = computed<StyleValue>(() =>
|
|
|
178
192
|
:ref="markOverlay"
|
|
179
193
|
class="q-dialog__overlay"
|
|
180
194
|
:class="overlayClasses"
|
|
181
|
-
:style="
|
|
195
|
+
:style="transitionStyle"
|
|
182
196
|
@mousedown.self="!persistent && !noBackdropDismiss && (open = false)"
|
|
183
197
|
>
|
|
184
198
|
<div
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QEditorJs — éditeur par blocs basé sur Editor.js (https://editorjs.io).
|
|
3
|
+
// v-model = données JSON : { time?, blocks: [{ type, data }], version? }.
|
|
4
|
+
// Tools par défaut : paragraph, header (H1-H3), list (bullet/ordered),
|
|
5
|
+
// checklist, quote, code, delimiter. Extensible via la prop `tools`.
|
|
6
|
+
// SSR-safe : l'instance Editor.js n'est créée qu'au montage (client).
|
|
7
|
+
// Note : Editor.js v2 injecte lui-même ses styles à l'init (aucun import CSS
|
|
8
|
+
// nécessaire) ; les surcharges de thème vivent dans styles/main.css.
|
|
9
|
+
import { onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
10
|
+
|
|
11
|
+
interface QEditorJsBlock {
|
|
12
|
+
type: string
|
|
13
|
+
data: Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface QEditorJsData {
|
|
17
|
+
time?: number
|
|
18
|
+
blocks: QEditorJsBlock[]
|
|
19
|
+
version?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** Données du document (v-model) — { blocks: [{ type, data }] } */
|
|
24
|
+
data?: QEditorJsData | null
|
|
25
|
+
/** Placeholder du bloc par défaut */
|
|
26
|
+
placeholder?: string
|
|
27
|
+
/** Hauteur minimale de la zone d'édition (défaut : 220px) */
|
|
28
|
+
minHeight?: string
|
|
29
|
+
/** Lecture seule */
|
|
30
|
+
readonly?: boolean
|
|
31
|
+
/** Désactivé (lecture seule + style grisé) */
|
|
32
|
+
disable?: boolean
|
|
33
|
+
/** Tools supplémentaires (défaut : paragraph, header, list, checklist, quote, code, delimiter) */
|
|
34
|
+
tools?: Record<string, any>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
38
|
+
data: null,
|
|
39
|
+
placeholder: "Write something…",
|
|
40
|
+
minHeight: "220px",
|
|
41
|
+
readonly: false,
|
|
42
|
+
disable: false,
|
|
43
|
+
tools: () => ({}),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits<{
|
|
47
|
+
"update:modelValue": [data: QEditorJsData]
|
|
48
|
+
ready: [editor: unknown]
|
|
49
|
+
}>()
|
|
50
|
+
|
|
51
|
+
const holderEl = ref<HTMLElement | null>(null)
|
|
52
|
+
let editor: any = null
|
|
53
|
+
let lastJson = ""
|
|
54
|
+
let internal = false
|
|
55
|
+
|
|
56
|
+
const isEditable = () => !props.readonly && !props.disable
|
|
57
|
+
|
|
58
|
+
const emitData = async () => {
|
|
59
|
+
if (!editor) return
|
|
60
|
+
try {
|
|
61
|
+
const saved: QEditorJsData = (await editor.save()) as QEditorJsData
|
|
62
|
+
internal = true
|
|
63
|
+
lastJson = JSON.stringify(saved)
|
|
64
|
+
emit("update:modelValue", saved)
|
|
65
|
+
internal = false
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* document invalide — ignoré */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onMounted(async () => {
|
|
73
|
+
const mod = await import("@editorjs/editorjs")
|
|
74
|
+
const EditorJS = mod.default
|
|
75
|
+
// Les packages tools Editor.js n'embarquent pas tous des types TS
|
|
76
|
+
// @ts-ignore
|
|
77
|
+
const Header = (await import("@editorjs/header")).default
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
const Paragraph = (await import("@editorjs/paragraph")).default
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
const List = (await import("@editorjs/list")).default
|
|
82
|
+
// @ts-ignore
|
|
83
|
+
const Checklist = (await import("@editorjs/checklist")).default
|
|
84
|
+
// @ts-ignore
|
|
85
|
+
const Quote = (await import("@editorjs/quote")).default
|
|
86
|
+
// @ts-ignore
|
|
87
|
+
const Code = (await import("@editorjs/code")).default
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
const Delimiter = (await import("@editorjs/delimiter")).default
|
|
90
|
+
|
|
91
|
+
const defaults: Record<string, any> = {
|
|
92
|
+
paragraph: { class: Paragraph },
|
|
93
|
+
header: { class: Header, inlineToolbar: true, config: { levels: [1, 2, 3] } },
|
|
94
|
+
list: { class: List, inlineToolbar: true },
|
|
95
|
+
checklist: { class: Checklist, inlineToolbar: true },
|
|
96
|
+
quote: { class: Quote, inlineToolbar: true },
|
|
97
|
+
code: { class: Code },
|
|
98
|
+
delimiter: { class: Delimiter },
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
editor = new EditorJS({
|
|
102
|
+
holder: holderEl.value as HTMLElement,
|
|
103
|
+
tools: { ...defaults, ...props.tools },
|
|
104
|
+
data: props.data ?? undefined,
|
|
105
|
+
placeholder: props.placeholder,
|
|
106
|
+
readOnly: !isEditable(),
|
|
107
|
+
onChange: () => { emitData() },
|
|
108
|
+
onReady: () => { emit("ready", editor) },
|
|
109
|
+
})
|
|
110
|
+
emit("ready", editor)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Changement externe → render (sans boucle : on compare au dernier JSON émis)
|
|
114
|
+
watch(
|
|
115
|
+
() => props.data,
|
|
116
|
+
(value) => {
|
|
117
|
+
if (internal || !editor) return
|
|
118
|
+
const json = value ? JSON.stringify(value) : ""
|
|
119
|
+
if (json === lastJson) return
|
|
120
|
+
editor.render(value ?? { time: Date.now(), blocks: [], version: "2.31.6" })
|
|
121
|
+
.then(() => { lastJson = json })
|
|
122
|
+
.catch(() => { /* ignore */ })
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
onBeforeUnmount(() => {
|
|
127
|
+
if (editor && typeof editor.destroy === "function") editor.destroy()
|
|
128
|
+
editor = null
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// — Méthodes exposées —
|
|
132
|
+
const save = () => editor?.save() as Promise<QEditorJsData> | undefined
|
|
133
|
+
const clear = () => editor?.clear()
|
|
134
|
+
const getEditor = () => editor
|
|
135
|
+
|
|
136
|
+
defineExpose({ save, clear, getEditor })
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<template>
|
|
140
|
+
<div
|
|
141
|
+
class="q-editor-js"
|
|
142
|
+
:class="{ 'q-editor-js--readonly': readonly || disable, 'q-editor-js--disable': disable }"
|
|
143
|
+
>
|
|
144
|
+
<div ref="holderEl" class="q-editor-js__holder" :style="{ minHeight }" />
|
|
145
|
+
</div>
|
|
146
|
+
</template>
|
package/components/QFab.vue
CHANGED
|
@@ -10,7 +10,7 @@ export const qFabKey: InjectionKey<QFabContext> = Symbol("q-fab")
|
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<script setup lang="ts">
|
|
13
|
-
import { computed, provide, ref } from "vue"
|
|
13
|
+
import { computed, onBeforeUnmount, onMounted, provide, ref } from "vue"
|
|
14
14
|
import { Icon } from "@iconify/vue"
|
|
15
15
|
import { icons } from "../lib/icons"
|
|
16
16
|
import { colorValue, foregroundFor } from "../lib/colors"
|
|
@@ -48,6 +48,23 @@ const toggle = () => {
|
|
|
48
48
|
if (!props.disable) open.value = !open.value
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Clic à l'extérieur du FAB → fermeture (actions repliées)
|
|
52
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
53
|
+
const onDocPointerDown = (e: PointerEvent) => {
|
|
54
|
+
if (!props.modelValue || !rootEl.value) return
|
|
55
|
+
if (!rootEl.value.contains(e.target as Node)) open.value = false
|
|
56
|
+
}
|
|
57
|
+
onMounted(() => {
|
|
58
|
+
if (typeof document !== "undefined") {
|
|
59
|
+
document.addEventListener("pointerdown", onDocPointerDown)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
onBeforeUnmount(() => {
|
|
63
|
+
if (typeof document !== "undefined") {
|
|
64
|
+
document.removeEventListener("pointerdown", onDocPointerDown)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
51
68
|
provide<QFabContext>(qFabKey, {
|
|
52
69
|
close: () => {
|
|
53
70
|
open.value = false
|
|
@@ -61,7 +78,7 @@ const mainStyle = computed<Record<string, string>>(() => ({
|
|
|
61
78
|
</script>
|
|
62
79
|
|
|
63
80
|
<template>
|
|
64
|
-
<div class="q-fab" :class="[`q-fab--${position}`, open && 'q-fab--open']">
|
|
81
|
+
<div ref="rootEl" class="q-fab" :class="[`q-fab--${position}`, open && 'q-fab--open']">
|
|
65
82
|
<div class="q-fab__actions" role="menu">
|
|
66
83
|
<slot />
|
|
67
84
|
</div>
|