codevdesign 1.0.38 → 1.0.40
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/composants/csqcAide.vue +55 -55
- package/composants/csqcAlerteErreur.vue +87 -87
- package/composants/csqcChaise/chaiseConteneur.vue +367 -367
- package/composants/csqcChaise/chaiseItem.vue +54 -54
- package/composants/csqcCodeBudgetaireGenerique.vue +336 -336
- package/composants/csqcConfirmation.vue +75 -75
- package/composants/csqcDate.vue +57 -57
- package/composants/csqcDialogue.vue +118 -118
- package/composants/csqcEditeurTexteRiche.vue +380 -380
- package/composants/csqcEntete.vue +163 -163
- package/composants/csqcImportCSV.vue +125 -125
- package/composants/csqcModaleSaisie.vue +97 -97
- package/composants/csqcOptionSwitch.vue +120 -120
- package/composants/csqcRecherche.vue +213 -213
- package/composants/csqcRechercheUtilisateur.vue +198 -197
- package/composants/csqcSnackbar.vue +88 -88
- package/composants/csqcTable/csqcTable.vue +383 -383
- package/composants/csqcTable/csqcTableModaleChoixColonnes.vue +586 -586
- package/composants/csqcTexteBilingue.vue +175 -175
- package/composants/csqcTiroir.vue +156 -156
- package/composants/gabarit/csqcMenu.vue +281 -281
- package/composants/gabarit/pivEntete.vue +205 -205
- package/index.ts +75 -75
- package/modeles/composants/csqcMenuModele.ts +18 -18
- package/modeles/composants/datatableColonne.ts +31 -31
- package/outils/appAxios.ts +116 -116
- package/outils/rafraichisseurToken.ts +187 -187
- package/package.json +1 -1
|
@@ -1,380 +1,380 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="editor">
|
|
3
|
-
<Editor
|
|
4
|
-
v-if="editorReady"
|
|
5
|
-
v-bind="$attrs"
|
|
6
|
-
v-model="editorValue"
|
|
7
|
-
:init="initOptions"
|
|
8
|
-
:disabled="desactiver"
|
|
9
|
-
license-key="gpl"
|
|
10
|
-
@blur="onBlur"
|
|
11
|
-
@keydown="onUserActivity"
|
|
12
|
-
@change="onUserActivity"
|
|
13
|
-
/>
|
|
14
|
-
</div>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
19
|
-
|
|
20
|
-
import { computed, ref, watch, nextTick } from 'vue'
|
|
21
|
-
import Editor from '@tinymce/tinymce-vue'
|
|
22
|
-
import type { Editor as TinyMCEEditor } from 'tinymce'
|
|
23
|
-
|
|
24
|
-
// Core / skins / thème
|
|
25
|
-
import 'tinymce/tinymce.js'
|
|
26
|
-
import 'tinymce/icons/default/icons.min.js'
|
|
27
|
-
import 'tinymce/themes/silver/theme.min.js'
|
|
28
|
-
import 'tinymce/models/dom/model.min.js'
|
|
29
|
-
import 'tinymce/skins/ui/oxide/skin.js'
|
|
30
|
-
import 'tinymce/skins/ui/oxide/content.js'
|
|
31
|
-
import 'tinymce/skins/content/default/content.js'
|
|
32
|
-
|
|
33
|
-
// Langue
|
|
34
|
-
import 'tinymce-i18n/langs7/fr_FR'
|
|
35
|
-
|
|
36
|
-
// Plugins
|
|
37
|
-
import 'tinymce/plugins/autoresize/plugin.min.js'
|
|
38
|
-
import 'tinymce/plugins/advlist/plugin.min.js'
|
|
39
|
-
import 'tinymce/plugins/lists/plugin.min.js'
|
|
40
|
-
import 'tinymce/plugins/link/plugin.min.js'
|
|
41
|
-
import 'tinymce/plugins/autolink/plugin.min.js'
|
|
42
|
-
import 'tinymce/plugins/fullscreen/plugin.min.js'
|
|
43
|
-
import 'tinymce/plugins/table/plugin.min.js'
|
|
44
|
-
import 'tinymce/plugins/image/plugin.min.js'
|
|
45
|
-
import 'tinymce/plugins/code/plugin.min.js'
|
|
46
|
-
|
|
47
|
-
const openLink = (url?: string | null, target?: string | null) => {
|
|
48
|
-
if (!url) return
|
|
49
|
-
if (import.meta.env.MODE !== 'development') {
|
|
50
|
-
if (target && target !== '_blank') {
|
|
51
|
-
document.location.href = url
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
window.open(url, target || '_blank', 'noopener,noreferrer')
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
type PluginsProp = string | string[]
|
|
59
|
-
type ToolbarProp = string | string[]
|
|
60
|
-
|
|
61
|
-
interface Props {
|
|
62
|
-
modelValue?: string
|
|
63
|
-
langue?: string
|
|
64
|
-
interdireRedimension?: boolean
|
|
65
|
-
desactiver?: boolean
|
|
66
|
-
plugins?: PluginsProp // optionnel
|
|
67
|
-
autoriserImage?: boolean
|
|
68
|
-
imageTailleMaximale?: number
|
|
69
|
-
cacherBarreMenu?: boolean
|
|
70
|
-
cacherBarreOutils?: boolean
|
|
71
|
-
toolbar?: ToolbarProp // optionnel
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
75
|
-
modelValue: '',
|
|
76
|
-
langue: 'fr_FR',
|
|
77
|
-
interdireRedimension: false,
|
|
78
|
-
desactiver: false,
|
|
79
|
-
plugins: 'autoresize table image link fullscreen advlist lists autolink code',
|
|
80
|
-
autoriserImage: false,
|
|
81
|
-
imageTailleMaximale: 5,
|
|
82
|
-
cacherBarreMenu: false,
|
|
83
|
-
cacherBarreOutils: false,
|
|
84
|
-
toolbar:
|
|
85
|
-
'print | bold italic underline strikethrough | fontsizeselect | forecolor backcolor | ' +
|
|
86
|
-
'alignleft aligncenter alignright alignjustify | bullist numlist | ' +
|
|
87
|
-
'image | link | outdent indent blockquote | undo redo | fullscreen | removeformat | table | code',
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
const emit = defineEmits<{
|
|
91
|
-
(e: 'update:modelValue', value: string): void
|
|
92
|
-
(e: 'blur'): void
|
|
93
|
-
}>()
|
|
94
|
-
|
|
95
|
-
const editorReady = ref(true)
|
|
96
|
-
const editorValue = ref<string>(props.modelValue)
|
|
97
|
-
const _editor = ref<TinyMCEEditor | null>(null)
|
|
98
|
-
const _reinitPending = ref(false)
|
|
99
|
-
const _reinitLock = ref(false)
|
|
100
|
-
|
|
101
|
-
const imageTailleMaxMo = computed<number>(() => {
|
|
102
|
-
const n = Number(props.imageTailleMaximale)
|
|
103
|
-
if (!Number.isFinite(n)) return 5
|
|
104
|
-
return Math.min(100, Math.max(0, n))
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
const normalizeToolbar = (tb: string): string => {
|
|
108
|
-
const trimmed = (tb || '').trim()
|
|
109
|
-
return trimmed && trimmed !== '|' ? trimmed : 'undo redo'
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const toolbarEffective = computed<string | false>(() => {
|
|
113
|
-
if (props.cacherBarreOutils) return false
|
|
114
|
-
|
|
115
|
-
const raw = Array.isArray(props.toolbar) ? props.toolbar.join(' | ') : props.toolbar
|
|
116
|
-
if (props.autoriserImage) return normalizeToolbar(raw)
|
|
117
|
-
|
|
118
|
-
const sansImage = raw
|
|
119
|
-
.replace(/\bimage\b/g, '')
|
|
120
|
-
.replace(/\s*\|\s*/g, ' | ')
|
|
121
|
-
.replace(/(\s*\|\s*){2,}/g, ' | ')
|
|
122
|
-
.replace(/^\s*\|\s*|\s*\|\s*$/g, '')
|
|
123
|
-
.replace(/\s{2,}/g, ' ')
|
|
124
|
-
.trim()
|
|
125
|
-
|
|
126
|
-
return normalizeToolbar(sansImage)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
const pluginsEffective = computed<string>(() => {
|
|
130
|
-
const list = (Array.isArray(props.plugins) ? props.plugins : props.plugins.split(/\s+/))
|
|
131
|
-
.map(p => p.trim())
|
|
132
|
-
.filter(Boolean)
|
|
133
|
-
|
|
134
|
-
const filtered = !props.autoriserImage ? list.filter(p => p !== 'image') : list
|
|
135
|
-
return filtered.join(' ')
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const initOptions = computed<any>(() => {
|
|
139
|
-
const opts: any = {
|
|
140
|
-
autolink: !props.desactiver,
|
|
141
|
-
autoresize: true,
|
|
142
|
-
automatic_uploads: props.autoriserImage,
|
|
143
|
-
branding: false,
|
|
144
|
-
browser_spellcheck: true,
|
|
145
|
-
content_css: 'default',
|
|
146
|
-
deprecation_warnings: false,
|
|
147
|
-
highlight_on_focus: false,
|
|
148
|
-
image_dimensions: true,
|
|
149
|
-
language: props.langue === 'en' ? 'en_US' : 'fr_FR',
|
|
150
|
-
license_key: 'gpl',
|
|
151
|
-
menubar: !props.cacherBarreMenu,
|
|
152
|
-
object_resizing: true,
|
|
153
|
-
paste_data_images: props.autoriserImage,
|
|
154
|
-
plugins: pluginsEffective.value,
|
|
155
|
-
promotion: false,
|
|
156
|
-
readonly: props.desactiver,
|
|
157
|
-
resize: !props.interdireRedimension,
|
|
158
|
-
resize_img_proportional: props.autoriserImage,
|
|
159
|
-
skin_url: 'default',
|
|
160
|
-
statusbar: props.cacherBarreOutils === false,
|
|
161
|
-
theme_advanced_resizing: true,
|
|
162
|
-
theme_advanced_resizing_use_cookie: false,
|
|
163
|
-
toolbar: toolbarEffective.value,
|
|
164
|
-
...(props.autoriserImage ? { file_picker_types: 'image' } : {}),
|
|
165
|
-
|
|
166
|
-
setup: (editor: TinyMCEEditor) => {
|
|
167
|
-
_editor.value = editor
|
|
168
|
-
|
|
169
|
-
editor.on('init', () => {
|
|
170
|
-
// init ok
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
// liens cliquables
|
|
174
|
-
editor.on('click', (e: any) => {
|
|
175
|
-
const a = e.target?.closest?.('a')
|
|
176
|
-
if (!a) return
|
|
177
|
-
const href = a.getAttribute('href')
|
|
178
|
-
const target = a.getAttribute('target')
|
|
179
|
-
if (href) {
|
|
180
|
-
e.preventDefault()
|
|
181
|
-
openLink(href, target)
|
|
182
|
-
}
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
editor.on('keydown', (e: KeyboardEvent) => {
|
|
186
|
-
if (e.key !== 'Enter') return
|
|
187
|
-
const node = editor.selection?.getNode?.()
|
|
188
|
-
const a = (node as any)?.closest?.('a')
|
|
189
|
-
if (a) {
|
|
190
|
-
e.preventDefault()
|
|
191
|
-
openLink(a.getAttribute('href'), a.getAttribute('target'))
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
// Si images interdites
|
|
196
|
-
if (!props.autoriserImage) {
|
|
197
|
-
const removeImagesFromDom = () => {
|
|
198
|
-
const body = editor.getBody()
|
|
199
|
-
if (!body) return
|
|
200
|
-
body.querySelectorAll('img').forEach(img => img.remove())
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// collage
|
|
204
|
-
editor.on('paste', (e: any) => {
|
|
205
|
-
const dt: DataTransfer | null = e.clipboardData || e.originalEvent?.clipboardData || null
|
|
206
|
-
if (!dt) return
|
|
207
|
-
|
|
208
|
-
const items = Array.from(dt.items || [])
|
|
209
|
-
const hasImage = items.some(item => item.kind === 'file' && item.type && item.type.startsWith('image/'))
|
|
210
|
-
|
|
211
|
-
if (hasImage) {
|
|
212
|
-
const text = dt.getData('text/plain')
|
|
213
|
-
if (!text) {
|
|
214
|
-
e.preventDefault()
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
e.preventDefault()
|
|
219
|
-
editor.insertContent(editor.dom.encode(text))
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
setTimeout(removeImagesFromDom, 0)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
// drag & drop
|
|
226
|
-
editor.on('drop', (e: DragEvent) => {
|
|
227
|
-
const dt = e.dataTransfer
|
|
228
|
-
if (!dt) return
|
|
229
|
-
const hasImageFile = Array.from(dt.items || []).some(
|
|
230
|
-
i => i.kind === 'file' && i.type && i.type.startsWith('image/'),
|
|
231
|
-
)
|
|
232
|
-
if (hasImageFile) {
|
|
233
|
-
e.preventDefault()
|
|
234
|
-
return
|
|
235
|
-
}
|
|
236
|
-
setTimeout(removeImagesFromDom, 0)
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
editor.on('input', removeImagesFromDom)
|
|
240
|
-
editor.on('SetContent', removeImagesFromDom)
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (!props.autoriserImage) {
|
|
246
|
-
opts.paste_data_images = false
|
|
247
|
-
opts.invalid_elements = 'img'
|
|
248
|
-
opts.images_upload_handler = (_blobInfo: any, _success: any, failure: (msg: string) => void) => {
|
|
249
|
-
failure('Les images sont désactivées dans cet éditeur.')
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (props.autoriserImage) {
|
|
254
|
-
opts.file_picker_callback = (cb: (url: string, meta?: any) => void) => {
|
|
255
|
-
const input = document.createElement('input')
|
|
256
|
-
input.type = 'file'
|
|
257
|
-
input.accept = 'image/*'
|
|
258
|
-
input.onchange = () => {
|
|
259
|
-
const file = input.files?.[0]
|
|
260
|
-
if (!file) return
|
|
261
|
-
|
|
262
|
-
const reader = new FileReader()
|
|
263
|
-
reader.onload = () => {
|
|
264
|
-
const base64 = String(reader.result).split(',')[1]
|
|
265
|
-
const editor = _editor.value as any
|
|
266
|
-
const blobCache = editor?.editorUpload?.blobCache
|
|
267
|
-
if (!blobCache) return
|
|
268
|
-
|
|
269
|
-
const id = 'blobid' + Date.now()
|
|
270
|
-
const blobInfo = blobCache.create(id, file, base64)
|
|
271
|
-
const imageSize = blobInfo.blob().size / 1_000_000
|
|
272
|
-
if (imageSize > imageTailleMaxMo.value) {
|
|
273
|
-
alert('Fichier trop volumineux > ' + imageTailleMaxMo.value + ' Mo')
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
blobCache.add(blobInfo)
|
|
277
|
-
cb(blobInfo.blobUri(), { title: file.name })
|
|
278
|
-
}
|
|
279
|
-
reader.readAsDataURL(file)
|
|
280
|
-
}
|
|
281
|
-
input.click()
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return opts
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
// --- v-model sync ---
|
|
289
|
-
watch(
|
|
290
|
-
() => props.modelValue,
|
|
291
|
-
v => {
|
|
292
|
-
if (v !== editorValue.value) {
|
|
293
|
-
editorValue.value = v
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
watch(
|
|
299
|
-
() => editorValue.value,
|
|
300
|
-
v => {
|
|
301
|
-
if (v !== props.modelValue) {
|
|
302
|
-
emit('update:modelValue', v)
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
// --- Re-montage TinyMCE si props importantes changent ---
|
|
308
|
-
const scheduleReinit = () => {
|
|
309
|
-
if (_reinitPending.value) return
|
|
310
|
-
_reinitPending.value = true
|
|
311
|
-
queueMicrotask(async () => {
|
|
312
|
-
_reinitPending.value = false
|
|
313
|
-
await reinitEditor()
|
|
314
|
-
})
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const reinitEditor = async () => {
|
|
318
|
-
if (_reinitLock.value) return
|
|
319
|
-
_reinitLock.value = true
|
|
320
|
-
try {
|
|
321
|
-
const ed = _editor.value as any
|
|
322
|
-
if (ed && typeof ed.remove === 'function') {
|
|
323
|
-
try {
|
|
324
|
-
ed.remove()
|
|
325
|
-
} catch (e) {
|
|
326
|
-
console.warn('[Editeur] editor.remove erreur', e)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
_editor.value = null
|
|
330
|
-
editorReady.value = false
|
|
331
|
-
await nextTick()
|
|
332
|
-
editorReady.value = true
|
|
333
|
-
await nextTick()
|
|
334
|
-
} finally {
|
|
335
|
-
_reinitLock.value = false
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
watch(
|
|
340
|
-
() => [
|
|
341
|
-
props.autoriserImage,
|
|
342
|
-
props.interdireRedimension,
|
|
343
|
-
props.toolbar,
|
|
344
|
-
props.imageTailleMaximale,
|
|
345
|
-
props.plugins,
|
|
346
|
-
props.cacherBarreOutils,
|
|
347
|
-
props.cacherBarreMenu,
|
|
348
|
-
],
|
|
349
|
-
() => {
|
|
350
|
-
scheduleReinit()
|
|
351
|
-
},
|
|
352
|
-
{ deep: true },
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
const onBlur = () => {
|
|
356
|
-
if (!props.desactiver) emit('blur')
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const onUserActivity = () => {
|
|
360
|
-
// hook pour autosave / "user is typing" si besoin
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const insererAuCurseur = (texte: string) => {
|
|
364
|
-
const ed = _editor.value
|
|
365
|
-
if (!ed) return
|
|
366
|
-
ed.focus()
|
|
367
|
-
;(ed as any).insertContent(texte)
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// (optionnel) expose si tu veux appeler `insererAuCurseur` depuis le parent
|
|
371
|
-
defineExpose({
|
|
372
|
-
insererAuCurseur,
|
|
373
|
-
})
|
|
374
|
-
</script>
|
|
375
|
-
|
|
376
|
-
<style scoped>
|
|
377
|
-
.editor a {
|
|
378
|
-
cursor: pointer !important;
|
|
379
|
-
}
|
|
380
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<div class="editor">
|
|
3
|
+
<Editor
|
|
4
|
+
v-if="editorReady"
|
|
5
|
+
v-bind="$attrs"
|
|
6
|
+
v-model="editorValue"
|
|
7
|
+
:init="initOptions"
|
|
8
|
+
:disabled="desactiver"
|
|
9
|
+
license-key="gpl"
|
|
10
|
+
@blur="onBlur"
|
|
11
|
+
@keydown="onUserActivity"
|
|
12
|
+
@change="onUserActivity"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
19
|
+
|
|
20
|
+
import { computed, ref, watch, nextTick } from 'vue'
|
|
21
|
+
import Editor from '@tinymce/tinymce-vue'
|
|
22
|
+
import type { Editor as TinyMCEEditor } from 'tinymce'
|
|
23
|
+
|
|
24
|
+
// Core / skins / thème
|
|
25
|
+
import 'tinymce/tinymce.js'
|
|
26
|
+
import 'tinymce/icons/default/icons.min.js'
|
|
27
|
+
import 'tinymce/themes/silver/theme.min.js'
|
|
28
|
+
import 'tinymce/models/dom/model.min.js'
|
|
29
|
+
import 'tinymce/skins/ui/oxide/skin.js'
|
|
30
|
+
import 'tinymce/skins/ui/oxide/content.js'
|
|
31
|
+
import 'tinymce/skins/content/default/content.js'
|
|
32
|
+
|
|
33
|
+
// Langue
|
|
34
|
+
import 'tinymce-i18n/langs7/fr_FR'
|
|
35
|
+
|
|
36
|
+
// Plugins
|
|
37
|
+
import 'tinymce/plugins/autoresize/plugin.min.js'
|
|
38
|
+
import 'tinymce/plugins/advlist/plugin.min.js'
|
|
39
|
+
import 'tinymce/plugins/lists/plugin.min.js'
|
|
40
|
+
import 'tinymce/plugins/link/plugin.min.js'
|
|
41
|
+
import 'tinymce/plugins/autolink/plugin.min.js'
|
|
42
|
+
import 'tinymce/plugins/fullscreen/plugin.min.js'
|
|
43
|
+
import 'tinymce/plugins/table/plugin.min.js'
|
|
44
|
+
import 'tinymce/plugins/image/plugin.min.js'
|
|
45
|
+
import 'tinymce/plugins/code/plugin.min.js'
|
|
46
|
+
|
|
47
|
+
const openLink = (url?: string | null, target?: string | null) => {
|
|
48
|
+
if (!url) return
|
|
49
|
+
if (import.meta.env.MODE !== 'development') {
|
|
50
|
+
if (target && target !== '_blank') {
|
|
51
|
+
document.location.href = url
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
window.open(url, target || '_blank', 'noopener,noreferrer')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type PluginsProp = string | string[]
|
|
59
|
+
type ToolbarProp = string | string[]
|
|
60
|
+
|
|
61
|
+
interface Props {
|
|
62
|
+
modelValue?: string
|
|
63
|
+
langue?: string
|
|
64
|
+
interdireRedimension?: boolean
|
|
65
|
+
desactiver?: boolean
|
|
66
|
+
plugins?: PluginsProp // optionnel
|
|
67
|
+
autoriserImage?: boolean
|
|
68
|
+
imageTailleMaximale?: number
|
|
69
|
+
cacherBarreMenu?: boolean
|
|
70
|
+
cacherBarreOutils?: boolean
|
|
71
|
+
toolbar?: ToolbarProp // optionnel
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
75
|
+
modelValue: '',
|
|
76
|
+
langue: 'fr_FR',
|
|
77
|
+
interdireRedimension: false,
|
|
78
|
+
desactiver: false,
|
|
79
|
+
plugins: 'autoresize table image link fullscreen advlist lists autolink code',
|
|
80
|
+
autoriserImage: false,
|
|
81
|
+
imageTailleMaximale: 5,
|
|
82
|
+
cacherBarreMenu: false,
|
|
83
|
+
cacherBarreOutils: false,
|
|
84
|
+
toolbar:
|
|
85
|
+
'print | bold italic underline strikethrough | fontsizeselect | forecolor backcolor | ' +
|
|
86
|
+
'alignleft aligncenter alignright alignjustify | bullist numlist | ' +
|
|
87
|
+
'image | link | outdent indent blockquote | undo redo | fullscreen | removeformat | table | code',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const emit = defineEmits<{
|
|
91
|
+
(e: 'update:modelValue', value: string): void
|
|
92
|
+
(e: 'blur'): void
|
|
93
|
+
}>()
|
|
94
|
+
|
|
95
|
+
const editorReady = ref(true)
|
|
96
|
+
const editorValue = ref<string>(props.modelValue)
|
|
97
|
+
const _editor = ref<TinyMCEEditor | null>(null)
|
|
98
|
+
const _reinitPending = ref(false)
|
|
99
|
+
const _reinitLock = ref(false)
|
|
100
|
+
|
|
101
|
+
const imageTailleMaxMo = computed<number>(() => {
|
|
102
|
+
const n = Number(props.imageTailleMaximale)
|
|
103
|
+
if (!Number.isFinite(n)) return 5
|
|
104
|
+
return Math.min(100, Math.max(0, n))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const normalizeToolbar = (tb: string): string => {
|
|
108
|
+
const trimmed = (tb || '').trim()
|
|
109
|
+
return trimmed && trimmed !== '|' ? trimmed : 'undo redo'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const toolbarEffective = computed<string | false>(() => {
|
|
113
|
+
if (props.cacherBarreOutils) return false
|
|
114
|
+
|
|
115
|
+
const raw = Array.isArray(props.toolbar) ? props.toolbar.join(' | ') : props.toolbar
|
|
116
|
+
if (props.autoriserImage) return normalizeToolbar(raw)
|
|
117
|
+
|
|
118
|
+
const sansImage = raw
|
|
119
|
+
.replace(/\bimage\b/g, '')
|
|
120
|
+
.replace(/\s*\|\s*/g, ' | ')
|
|
121
|
+
.replace(/(\s*\|\s*){2,}/g, ' | ')
|
|
122
|
+
.replace(/^\s*\|\s*|\s*\|\s*$/g, '')
|
|
123
|
+
.replace(/\s{2,}/g, ' ')
|
|
124
|
+
.trim()
|
|
125
|
+
|
|
126
|
+
return normalizeToolbar(sansImage)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const pluginsEffective = computed<string>(() => {
|
|
130
|
+
const list = (Array.isArray(props.plugins) ? props.plugins : props.plugins.split(/\s+/))
|
|
131
|
+
.map(p => p.trim())
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
|
|
134
|
+
const filtered = !props.autoriserImage ? list.filter(p => p !== 'image') : list
|
|
135
|
+
return filtered.join(' ')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const initOptions = computed<any>(() => {
|
|
139
|
+
const opts: any = {
|
|
140
|
+
autolink: !props.desactiver,
|
|
141
|
+
autoresize: true,
|
|
142
|
+
automatic_uploads: props.autoriserImage,
|
|
143
|
+
branding: false,
|
|
144
|
+
browser_spellcheck: true,
|
|
145
|
+
content_css: 'default',
|
|
146
|
+
deprecation_warnings: false,
|
|
147
|
+
highlight_on_focus: false,
|
|
148
|
+
image_dimensions: true,
|
|
149
|
+
language: props.langue === 'en' ? 'en_US' : 'fr_FR',
|
|
150
|
+
license_key: 'gpl',
|
|
151
|
+
menubar: !props.cacherBarreMenu,
|
|
152
|
+
object_resizing: true,
|
|
153
|
+
paste_data_images: props.autoriserImage,
|
|
154
|
+
plugins: pluginsEffective.value,
|
|
155
|
+
promotion: false,
|
|
156
|
+
readonly: props.desactiver,
|
|
157
|
+
resize: !props.interdireRedimension,
|
|
158
|
+
resize_img_proportional: props.autoriserImage,
|
|
159
|
+
skin_url: 'default',
|
|
160
|
+
statusbar: props.cacherBarreOutils === false,
|
|
161
|
+
theme_advanced_resizing: true,
|
|
162
|
+
theme_advanced_resizing_use_cookie: false,
|
|
163
|
+
toolbar: toolbarEffective.value,
|
|
164
|
+
...(props.autoriserImage ? { file_picker_types: 'image' } : {}),
|
|
165
|
+
|
|
166
|
+
setup: (editor: TinyMCEEditor) => {
|
|
167
|
+
_editor.value = editor
|
|
168
|
+
|
|
169
|
+
editor.on('init', () => {
|
|
170
|
+
// init ok
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// liens cliquables
|
|
174
|
+
editor.on('click', (e: any) => {
|
|
175
|
+
const a = e.target?.closest?.('a')
|
|
176
|
+
if (!a) return
|
|
177
|
+
const href = a.getAttribute('href')
|
|
178
|
+
const target = a.getAttribute('target')
|
|
179
|
+
if (href) {
|
|
180
|
+
e.preventDefault()
|
|
181
|
+
openLink(href, target)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
editor.on('keydown', (e: KeyboardEvent) => {
|
|
186
|
+
if (e.key !== 'Enter') return
|
|
187
|
+
const node = editor.selection?.getNode?.()
|
|
188
|
+
const a = (node as any)?.closest?.('a')
|
|
189
|
+
if (a) {
|
|
190
|
+
e.preventDefault()
|
|
191
|
+
openLink(a.getAttribute('href'), a.getAttribute('target'))
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Si images interdites
|
|
196
|
+
if (!props.autoriserImage) {
|
|
197
|
+
const removeImagesFromDom = () => {
|
|
198
|
+
const body = editor.getBody()
|
|
199
|
+
if (!body) return
|
|
200
|
+
body.querySelectorAll('img').forEach(img => img.remove())
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// collage
|
|
204
|
+
editor.on('paste', (e: any) => {
|
|
205
|
+
const dt: DataTransfer | null = e.clipboardData || e.originalEvent?.clipboardData || null
|
|
206
|
+
if (!dt) return
|
|
207
|
+
|
|
208
|
+
const items = Array.from(dt.items || [])
|
|
209
|
+
const hasImage = items.some(item => item.kind === 'file' && item.type && item.type.startsWith('image/'))
|
|
210
|
+
|
|
211
|
+
if (hasImage) {
|
|
212
|
+
const text = dt.getData('text/plain')
|
|
213
|
+
if (!text) {
|
|
214
|
+
e.preventDefault()
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
e.preventDefault()
|
|
219
|
+
editor.insertContent(editor.dom.encode(text))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
setTimeout(removeImagesFromDom, 0)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// drag & drop
|
|
226
|
+
editor.on('drop', (e: DragEvent) => {
|
|
227
|
+
const dt = e.dataTransfer
|
|
228
|
+
if (!dt) return
|
|
229
|
+
const hasImageFile = Array.from(dt.items || []).some(
|
|
230
|
+
i => i.kind === 'file' && i.type && i.type.startsWith('image/'),
|
|
231
|
+
)
|
|
232
|
+
if (hasImageFile) {
|
|
233
|
+
e.preventDefault()
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
setTimeout(removeImagesFromDom, 0)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
editor.on('input', removeImagesFromDom)
|
|
240
|
+
editor.on('SetContent', removeImagesFromDom)
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!props.autoriserImage) {
|
|
246
|
+
opts.paste_data_images = false
|
|
247
|
+
opts.invalid_elements = 'img'
|
|
248
|
+
opts.images_upload_handler = (_blobInfo: any, _success: any, failure: (msg: string) => void) => {
|
|
249
|
+
failure('Les images sont désactivées dans cet éditeur.')
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (props.autoriserImage) {
|
|
254
|
+
opts.file_picker_callback = (cb: (url: string, meta?: any) => void) => {
|
|
255
|
+
const input = document.createElement('input')
|
|
256
|
+
input.type = 'file'
|
|
257
|
+
input.accept = 'image/*'
|
|
258
|
+
input.onchange = () => {
|
|
259
|
+
const file = input.files?.[0]
|
|
260
|
+
if (!file) return
|
|
261
|
+
|
|
262
|
+
const reader = new FileReader()
|
|
263
|
+
reader.onload = () => {
|
|
264
|
+
const base64 = String(reader.result).split(',')[1]
|
|
265
|
+
const editor = _editor.value as any
|
|
266
|
+
const blobCache = editor?.editorUpload?.blobCache
|
|
267
|
+
if (!blobCache) return
|
|
268
|
+
|
|
269
|
+
const id = 'blobid' + Date.now()
|
|
270
|
+
const blobInfo = blobCache.create(id, file, base64)
|
|
271
|
+
const imageSize = blobInfo.blob().size / 1_000_000
|
|
272
|
+
if (imageSize > imageTailleMaxMo.value) {
|
|
273
|
+
alert('Fichier trop volumineux > ' + imageTailleMaxMo.value + ' Mo')
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
blobCache.add(blobInfo)
|
|
277
|
+
cb(blobInfo.blobUri(), { title: file.name })
|
|
278
|
+
}
|
|
279
|
+
reader.readAsDataURL(file)
|
|
280
|
+
}
|
|
281
|
+
input.click()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return opts
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// --- v-model sync ---
|
|
289
|
+
watch(
|
|
290
|
+
() => props.modelValue,
|
|
291
|
+
v => {
|
|
292
|
+
if (v !== editorValue.value) {
|
|
293
|
+
editorValue.value = v
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
watch(
|
|
299
|
+
() => editorValue.value,
|
|
300
|
+
v => {
|
|
301
|
+
if (v !== props.modelValue) {
|
|
302
|
+
emit('update:modelValue', v)
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
// --- Re-montage TinyMCE si props importantes changent ---
|
|
308
|
+
const scheduleReinit = () => {
|
|
309
|
+
if (_reinitPending.value) return
|
|
310
|
+
_reinitPending.value = true
|
|
311
|
+
queueMicrotask(async () => {
|
|
312
|
+
_reinitPending.value = false
|
|
313
|
+
await reinitEditor()
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const reinitEditor = async () => {
|
|
318
|
+
if (_reinitLock.value) return
|
|
319
|
+
_reinitLock.value = true
|
|
320
|
+
try {
|
|
321
|
+
const ed = _editor.value as any
|
|
322
|
+
if (ed && typeof ed.remove === 'function') {
|
|
323
|
+
try {
|
|
324
|
+
ed.remove()
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.warn('[Editeur] editor.remove erreur', e)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
_editor.value = null
|
|
330
|
+
editorReady.value = false
|
|
331
|
+
await nextTick()
|
|
332
|
+
editorReady.value = true
|
|
333
|
+
await nextTick()
|
|
334
|
+
} finally {
|
|
335
|
+
_reinitLock.value = false
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
watch(
|
|
340
|
+
() => [
|
|
341
|
+
props.autoriserImage,
|
|
342
|
+
props.interdireRedimension,
|
|
343
|
+
props.toolbar,
|
|
344
|
+
props.imageTailleMaximale,
|
|
345
|
+
props.plugins,
|
|
346
|
+
props.cacherBarreOutils,
|
|
347
|
+
props.cacherBarreMenu,
|
|
348
|
+
],
|
|
349
|
+
() => {
|
|
350
|
+
scheduleReinit()
|
|
351
|
+
},
|
|
352
|
+
{ deep: true },
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
const onBlur = () => {
|
|
356
|
+
if (!props.desactiver) emit('blur')
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const onUserActivity = () => {
|
|
360
|
+
// hook pour autosave / "user is typing" si besoin
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const insererAuCurseur = (texte: string) => {
|
|
364
|
+
const ed = _editor.value
|
|
365
|
+
if (!ed) return
|
|
366
|
+
ed.focus()
|
|
367
|
+
;(ed as any).insertContent(texte)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// (optionnel) expose si tu veux appeler `insererAuCurseur` depuis le parent
|
|
371
|
+
defineExpose({
|
|
372
|
+
insererAuCurseur,
|
|
373
|
+
})
|
|
374
|
+
</script>
|
|
375
|
+
|
|
376
|
+
<style scoped>
|
|
377
|
+
.editor a {
|
|
378
|
+
cursor: pointer !important;
|
|
379
|
+
}
|
|
380
|
+
</style>
|