codevdesign 0.0.68 → 0.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,326 @@
1
+ <template>
2
+ <div class="editor">
3
+ <!-- Remount contrôlé pour TinyMCE -->
4
+ <Editor
5
+ v-if="editorReady"
6
+ v-model="editorValue"
7
+ :init="initOptions"
8
+ :disabled="desactiver"
9
+ @blur="surEvenementBlur"
10
+ @keydown="surEvenementBlur"
11
+ @input="surEvenementBlur"
12
+ @change="surEvenementBlur"
13
+ license-key="gpl"
14
+ />
15
+ </div>
16
+ </template>
17
+
18
+ <script>
19
+ /* Vue 3 - Options API */
20
+ import Editor from '@tinymce/tinymce-vue'
21
+ import * as tinymce from 'tinymce/tinymce.js'
22
+ import 'tinymce/icons/default/icons.min.js'
23
+ import 'tinymce/themes/silver/theme.min.js'
24
+ import 'tinymce/models/dom/model.min.js'
25
+ import 'tinymce/skins/ui/oxide/skin.js'
26
+ import 'tinymce/skins/ui/oxide/content.js'
27
+ import 'tinymce/skins/content/default/content.js'
28
+ import 'tinymce-i18n/langs7/fr_FR'
29
+
30
+ /* Plugins */
31
+ import 'tinymce/plugins/autoresize/plugin.min.js'
32
+ import 'tinymce/plugins/advlist/plugin.min.js'
33
+ import 'tinymce/plugins/lists/plugin.min.js'
34
+ import 'tinymce/plugins/link/plugin.min.js'
35
+ import 'tinymce/plugins/autolink/plugin.min.js'
36
+ import 'tinymce/plugins/fullscreen/plugin.min.js'
37
+ import 'tinymce/plugins/table/plugin.min.js'
38
+ import 'tinymce/plugins/image/plugin.min.js' // activé/désactivé via "pluginsEffective"
39
+ import 'tinymce/plugins/emoticons/plugin.min.js'
40
+ import 'tinymce/plugins/emoticons/js/emojis.min.js'
41
+ import 'tinymce/plugins/code/plugin.min.js'
42
+
43
+ const openLink = (url, target) => {
44
+ if (!url) return
45
+ if (import.meta.env.MODE !== 'development') {
46
+ if (target && target !== '_blank') {
47
+ document.location.href = url
48
+ return
49
+ }
50
+ window.open(url, target || '_blank', 'noopener,noreferrer')
51
+ }
52
+ }
53
+
54
+ export default {
55
+ name: 'EditeurForm',
56
+ components: { Editor },
57
+
58
+ props: {
59
+ langue: { type: String, default: 'fr_FR' },
60
+ value: { type: String, default: '' },
61
+ interdireRedimension: { type: Boolean, default: false },
62
+ desactiver: { type: Boolean, default: false },
63
+ plugins: {
64
+ type: [String, Array],
65
+ default: 'autoresize table image link fullscreen advlist lists emoticons autolink code',
66
+ },
67
+ interdireImage: { type: Boolean, default: false },
68
+ imageTailleMaximale: { type: Number, default: 5 }, // en Mo
69
+ cacherBarreMenu: { type: Boolean, default: false },
70
+ cacherBarreOutils: { type: Boolean, default: false },
71
+ toolbar: {
72
+ type: [String, Array],
73
+ default:
74
+ 'bold italic underline strikethrough | fontsizeselect | forecolor backcolor | ' +
75
+ 'alignleft aligncenter alignright alignjustify | emoticons | bullist numlist | ' +
76
+ 'image | link | outdent indent blockquote | undo redo | fullscreen | removeformat | table | code',
77
+ },
78
+ },
79
+
80
+ data() {
81
+ return {
82
+ editorReady: true,
83
+ _editor: null,
84
+ editorValue: this.value,
85
+ _reinitPending: false,
86
+ _reinitLock: false,
87
+ }
88
+ },
89
+
90
+ computed: {
91
+ // <= ICI on force un plafond d'image à 100 Mo (et on gère les valeurs non numériques)
92
+ imageTailleMaxMo() {
93
+ const n = Number(this.imageTailleMaximale)
94
+ if (!Number.isFinite(n)) return 5
95
+ return Math.min(100, Math.max(0, n))
96
+ },
97
+
98
+ // Nettoie la toolbar en retirant "image" et ses séparateurs orphelins
99
+ toolbarEffective() {
100
+ if (this.cacherBarreOutils) return false
101
+ const raw = Array.isArray(this.toolbar) ? this.toolbar.join(' | ') : this.toolbar
102
+ if (!this.interdireImage) return this._normalizeToolbar(raw)
103
+
104
+ const sansImage = raw
105
+ .replace(/\bimage\b/g, '') // retire le bouton
106
+ .replace(/\s*\|\s*/g, ' | ') // normalise les séparateurs
107
+ .replace(/(\s*\|\s*){2,}/g, ' | ') // évite les doubles |
108
+ .replace(/^\s*\|\s*|\s*\|\s*$/g, '') // retire | en début/fin
109
+ .replace(/\s{2,}/g, ' ') // espaces doublés
110
+ .trim()
111
+
112
+ return this._normalizeToolbar(sansImage)
113
+ },
114
+
115
+ // Plugins effectifs : retire "image" si interdireImage = true
116
+ pluginsEffective() {
117
+ const list = (Array.isArray(this.plugins) ? this.plugins : this.plugins.split(/\s+/))
118
+ .map(p => p.trim())
119
+ .filter(Boolean)
120
+
121
+ const filtered = this.interdireImage ? list.filter(p => p !== 'image') : list
122
+ return filtered.join(' ')
123
+ },
124
+
125
+ // Options TinyMCE réactives
126
+ initOptions() {
127
+ const opts = {
128
+ autolink: !this.desactiver,
129
+ autoresize: true,
130
+ branding: false,
131
+ browser_spellcheck: true,
132
+ content_css: 'default',
133
+ deprecation_warnings: false,
134
+ emoticons_database: 'emojis',
135
+ //height: this.height,
136
+ //min_heignt: 100,
137
+ //max_height: 800,
138
+ highlight_on_focus: false,
139
+ language: this.langue == 'en' ? 'en_US' : 'fr_FR',
140
+ license_key: 'gpl',
141
+ menubar: !this.cacherBarreMenu,
142
+ promotion: false,
143
+ resize: !this.interdireRedimension,
144
+ skin_url: 'default',
145
+ statusbar: this.cacherBarreOutils === false,
146
+ theme_advanced_resizing: true,
147
+ theme_advanced_resizing_use_cookie: false,
148
+
149
+ // Gestion images selon interdireImage
150
+ plugins: this.pluginsEffective,
151
+ toolbar: this.toolbarEffective,
152
+ automatic_uploads: !this.interdireImage,
153
+ object_resizing: !this.interdireImage,
154
+ paste_data_images: !this.interdireImage,
155
+ ...(this.interdireImage ? {} : { file_picker_types: 'image' }),
156
+
157
+ // Certaines options images n'ont effet que si plugin "image" est actif
158
+ image_dimensions: !this.interdireImage,
159
+ resize_img_proportional: !this.interdireImage,
160
+
161
+ readonly: this.desactiver,
162
+
163
+ setup: editor => {
164
+ this._editor = editor
165
+ editor.on('init', () => {
166
+ //console.log('[Editeur] TinyMCE INIT done')
167
+ })
168
+ // Délégation d’événements pour intercepter les clics de liens
169
+ editor.on('click', e => {
170
+ const a = e.target?.closest?.('a')
171
+ if (!a) return
172
+ const href = a.getAttribute('href')
173
+ const target = a.getAttribute('target')
174
+ if (href) {
175
+ e.preventDefault()
176
+ openLink(href, target)
177
+ }
178
+ })
179
+
180
+ // ENTER sur un lien
181
+ editor.on('keydown', e => {
182
+ if (e.key !== 'Enter') return
183
+ const node = editor.selection?.getNode?.()
184
+ const a = node?.closest?.('a')
185
+ if (a) {
186
+ e.preventDefault()
187
+ openLink(a.getAttribute('href'), a.getAttribute('target'))
188
+ }
189
+ })
190
+ },
191
+ }
192
+
193
+ // File picker uniquement si images permises
194
+ if (!this.interdireImage) {
195
+ opts.file_picker_callback = cb => {
196
+ const input = document.createElement('input')
197
+ input.type = 'file'
198
+ input.accept = 'image/*'
199
+ input.onchange = () => {
200
+ const file = input.files?.[0]
201
+ if (!file) return
202
+
203
+ const reader = new FileReader()
204
+ reader.onload = () => {
205
+ const base64 = String(reader.result).split(',')[1]
206
+ const editor = this._editor
207
+ const blobCache = editor?.editorUpload?.blobCache
208
+ if (!blobCache) return
209
+
210
+ const id = 'blobid' + Date.now()
211
+ const blobInfo = blobCache.create(id, file, base64)
212
+ const imageSize = blobInfo.blob().size / 1_000_000
213
+ if (imageSize > this.imageTailleMaxMo) {
214
+ alert('Fichier trop volumineux > ' + this.imageTailleMaxMo + ' Mo')
215
+ return
216
+ }
217
+ blobCache.add(blobInfo)
218
+ cb(blobInfo.blobUri(), { title: file.name })
219
+ }
220
+ reader.readAsDataURL(file)
221
+ }
222
+ input.click()
223
+ }
224
+ }
225
+
226
+ return opts
227
+ },
228
+ },
229
+
230
+ watch: {
231
+ // Re-montage propre de l’éditeur dès qu’on touche aux props
232
+ interdireImage() {
233
+ this._scheduleReinit()
234
+ },
235
+ interdireRedimension() {
236
+ this._scheduleReinit()
237
+ },
238
+ toolbar() {
239
+ this._scheduleReinit()
240
+ },
241
+ imageTailleMaximale() {
242
+ this._scheduleReinit()
243
+ },
244
+ plugins() {
245
+ this._scheduleReinit()
246
+ },
247
+ cacherBarreOutils() {
248
+ this._scheduleReinit()
249
+ },
250
+ cacherBarreMenu() {
251
+ this._scheduleReinit()
252
+ },
253
+
254
+ value(v) {
255
+ this.editorValue = v
256
+ },
257
+ editorValue(v) {
258
+ this.$emit('input', v)
259
+ },
260
+ },
261
+
262
+ methods: {
263
+ _normalizeToolbar(tb) {
264
+ // Si la toolbar devient vide après nettoyage, on met au moins "undo redo"
265
+ const trimmed = (tb || '').trim()
266
+ return trimmed && trimmed !== '|' ? trimmed : 'undo redo'
267
+ },
268
+
269
+ _scheduleReinit() {
270
+ // coalesce toutes les demandes de réinit dans la même micro-tâche
271
+ if (this._reinitPending) return
272
+ this._reinitPending = true
273
+ queueMicrotask(async () => {
274
+ this._reinitPending = false
275
+ await this._reinitEditor()
276
+ })
277
+ },
278
+
279
+ async _reinitEditor() {
280
+ if (this._reinitLock) return
281
+ this._reinitLock = true
282
+ try {
283
+ // détruire proprement l’instance via l’instance elle-même
284
+ const ed = this._editor
285
+ if (ed) {
286
+ try {
287
+ ed.remove && ed.remove()
288
+ } catch (e) {
289
+ console.warn('[Editeur] editor.remove erreur', e)
290
+ }
291
+ }
292
+ this._editor = null
293
+
294
+ // 🔁 remount contrôlé
295
+ this.editorReady = false
296
+ await this.$nextTick()
297
+ this.editorReady = true
298
+ await this.$nextTick()
299
+ } finally {
300
+ this._reinitLock = false
301
+ }
302
+ },
303
+
304
+ surEvenementBlur() {
305
+ if (!this.desactiver) {
306
+ this.$emit('blur')
307
+ this.$emit('input', this.editorValue)
308
+ }
309
+ },
310
+ },
311
+
312
+ beforeUnmount() {
313
+ // Cleanup sûr lors du démontage du composant parent
314
+ try {
315
+ if (this._editor) tinymce.remove(this._editor)
316
+ } catch {}
317
+ this._editor = null
318
+ },
319
+ }
320
+ </script>
321
+
322
+ <style scoped>
323
+ .editor a {
324
+ cursor: pointer !important;
325
+ }
326
+ </style>
package/index.ts CHANGED
@@ -1,70 +1,71 @@
1
- import csqcAlerteErreur from './composants/csqcAlerteErreur.vue'
2
- import csqcDialogue from './composants/csqcDialogue.vue'
3
- import csqcOptionSwitch from './composants/csqcOptionSwitch.vue'
4
- import csqcRecherche from './composants/csqcRecherche.vue'
5
- import csqcSnackbar from './composants/csqcSnackbar.vue'
6
- import csqcTiroir from './composants/csqcTiroir.vue'
7
- import pivEntete from './composants/gabarit/pivEntete.vue'
8
- import pivFooter from './composants/gabarit/pivPiedPage.vue'
9
- import csqcMenu from './composants/gabarit/csqcMenu.vue'
10
- import csqcConfirmation from './composants/csqcConfirmation.vue'
11
- import csqcTable from './composants/csqcTable/csqcTable.vue'
12
- import csqcCodeBudgetaire from './composants/codeBudgetaireGenerique.vue'
13
- //import csqcChaise from './composants/csqcChaise/chaiseConteneur.vue'
14
- import csqcAide from './composants/csqcAide.vue'
15
- import csqcEntete from './composants/csqcEntete.vue'
16
- import csqcTexteBilingue from './composants/csqcTexteBilingue.vue'
17
-
18
- //import validateurs from './validateurs'
19
-
20
- import Utilisateur from './modeles/utilisateur'
21
- import Unite from './modeles/unite'
22
- import Intervention from './modeles/intervention'
23
- import DroitIntervention from './modeles/droitIntervention'
24
- import Role from './modeles/role'
25
- import RoleMin from './modeles/roleMin'
26
- import GroupeCE from './modeles/groupeCE'
27
- import NotificationGabaritDefaut from './modeles/notificationGabaritDefaut'
28
- import modeleSnackbar from './modeles/composants/snackbar'
29
- import modeleDatatableColonne from './modeles/composants/datatableColonne'
30
- import apiReponse from './modeles/apiReponse'
31
- import data from './modeles/data'
32
- import response from './modeles/response'
33
-
34
- import csqcEn from './locales/en.json'
35
- import csqcFr from './locales/fr.json'
36
-
37
- export {
38
- csqcFr,
39
- csqcEn,
40
- csqcAlerteErreur,
41
- csqcDialogue,
42
- csqcConfirmation,
43
- csqcOptionSwitch,
44
- csqcRecherche,
45
- csqcSnackbar,
46
- csqcTable,
47
- csqcTiroir,
48
- csqcMenu,
49
- csqcCodeBudgetaire,
50
- //csqcChaise,
51
- pivFooter,
52
- pivEntete,
53
- csqcAide,
54
- csqcEntete,
55
- csqcTexteBilingue,
56
- //validateurs,
57
- Utilisateur,
58
- Unite,
59
- Intervention,
60
- modeleSnackbar,
61
- modeleDatatableColonne,
62
- apiReponse,
63
- data,
64
- response,
65
- NotificationGabaritDefaut,
66
- DroitIntervention,
67
- Role,
68
- RoleMin,
69
- GroupeCE,
70
- }
1
+ import csqcAlerteErreur from './composants/csqcAlerteErreur.vue'
2
+ import csqcDialogue from './composants/csqcDialogue.vue'
3
+ import csqcOptionSwitch from './composants/csqcOptionSwitch.vue'
4
+ import csqcRecherche from './composants/csqcRecherche.vue'
5
+ import csqcSnackbar from './composants/csqcSnackbar.vue'
6
+ import csqcTiroir from './composants/csqcTiroir.vue'
7
+ import pivEntete from './composants/gabarit/pivEntete.vue'
8
+ import pivFooter from './composants/gabarit/pivPiedPage.vue'
9
+ import csqcMenu from './composants/gabarit/csqcMenu.vue'
10
+ import csqcConfirmation from './composants/csqcConfirmation.vue'
11
+ import csqcTable from './composants/csqcTable/csqcTable.vue'
12
+ import csqcCodeBudgetaire from './composants/codeBudgetaireGenerique.vue'
13
+ //import csqcChaise from './composants/csqcChaise/chaiseConteneur.vue'
14
+ import csqcAide from './composants/csqcAide.vue'
15
+ import csqcEntete from './composants/csqcEntete.vue'
16
+ import csqcTexteBilingue from './composants/csqcTexteBilingue.vue'
17
+ import csqcEditeurTexteRiche from './composants/csqcEditeurTexteRiche.vue'
18
+ //import validateurs from './validateurs'
19
+
20
+ import Utilisateur from './modeles/utilisateur'
21
+ import Unite from './modeles/unite'
22
+ import Intervention from './modeles/intervention'
23
+ import DroitIntervention from './modeles/droitIntervention'
24
+ import Role from './modeles/role'
25
+ import RoleMin from './modeles/roleMin'
26
+ import GroupeCE from './modeles/groupeCE'
27
+ import NotificationGabaritDefaut from './modeles/notificationGabaritDefaut'
28
+ import modeleSnackbar from './modeles/composants/snackbar'
29
+ import modeleDatatableColonne from './modeles/composants/datatableColonne'
30
+ import apiReponse from './modeles/apiReponse'
31
+ import data from './modeles/data'
32
+ import response from './modeles/response'
33
+
34
+ import csqcEn from './locales/en.json'
35
+ import csqcFr from './locales/fr.json'
36
+
37
+ export {
38
+ csqcFr,
39
+ csqcEn,
40
+ csqcAlerteErreur,
41
+ csqcDialogue,
42
+ csqcConfirmation,
43
+ csqcOptionSwitch,
44
+ csqcRecherche,
45
+ csqcSnackbar,
46
+ csqcTable,
47
+ csqcTiroir,
48
+ csqcMenu,
49
+ csqcCodeBudgetaire,
50
+ //csqcChaise,
51
+ pivFooter,
52
+ pivEntete,
53
+ csqcAide,
54
+ csqcEntete,
55
+ csqcTexteBilingue,
56
+ csqcEditeurTexteRiche,
57
+ //validateurs,
58
+ Utilisateur,
59
+ Unite,
60
+ Intervention,
61
+ modeleSnackbar,
62
+ modeleDatatableColonne,
63
+ apiReponse,
64
+ data,
65
+ response,
66
+ NotificationGabaritDefaut,
67
+ DroitIntervention,
68
+ Role,
69
+ RoleMin,
70
+ GroupeCE,
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "0.0.68",
3
+ "version": "0.0.69",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",
@@ -13,7 +13,7 @@
13
13
  "build": "vite build"
14
14
  },
15
15
  "dependencies": {
16
- "vuetify": "^3.7.0",
16
+ "vuetify": "^3.8.0",
17
17
  "vue-i18n": "^11.0.0",
18
18
  "@e965/xlsx": "^0.20.3"
19
19
  },
@@ -21,7 +21,7 @@
21
21
  "@types/node": "^22.13.5",
22
22
  "@vitejs/plugin-vue": "^5.2.1",
23
23
  "typescript": "^5.7.3",
24
- "vite": "^6.2.0"
24
+ "vite": "^7.0.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "vue": "^3.5.0"