codevdesign 1.0.78 → 2.0.0

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