codevdesign 0.0.70 → 0.0.72

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.
@@ -1,326 +1,318 @@
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>
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="onBlur"
10
+ @keydown="onUserActivity"
11
+ @change="onUserActivity"
12
+ license-key="gpl"
13
+ />
14
+ </div>
15
+ </template>
16
+
17
+ <script>
18
+ import Editor from '@tinymce/tinymce-vue'
19
+ import tinymce from 'tinymce'
20
+
21
+ // Core/skins/thème
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
+
29
+ // Langue
30
+ import 'tinymce-i18n/langs7/fr_FR'
31
+
32
+ // Plugins
33
+ import 'tinymce/plugins/autoresize/plugin.min.js'
34
+ import 'tinymce/plugins/advlist/plugin.min.js'
35
+ import 'tinymce/plugins/lists/plugin.min.js'
36
+ import 'tinymce/plugins/link/plugin.min.js'
37
+ import 'tinymce/plugins/autolink/plugin.min.js'
38
+ import 'tinymce/plugins/fullscreen/plugin.min.js'
39
+ import 'tinymce/plugins/table/plugin.min.js'
40
+ import 'tinymce/plugins/image/plugin.min.js'
41
+ //import 'tinymce/plugins/emoticons/plugin.min.js'
42
+ //import 'tinymce/plugins/emoticons/js/emojis.min.js'
43
+ import 'tinymce/plugins/code/plugin.min.js'
44
+
45
+ const openLink = (url, target) => {
46
+ if (!url) return
47
+ if (import.meta.env.MODE !== 'development') {
48
+ if (target && target !== '_blank') {
49
+ document.location.href = url
50
+ return
51
+ }
52
+ window.open(url, target || '_blank', 'noopener,noreferrer')
53
+ }
54
+ }
55
+
56
+ export default {
57
+ name: 'EditeurForm',
58
+ components: { Editor },
59
+
60
+ props: {
61
+ modelValue: { type: String, default: '' },
62
+ langue: { type: String, default: 'fr_FR' },
63
+ interdireRedimension: { type: Boolean, default: false },
64
+ desactiver: { type: Boolean, default: false },
65
+ plugins: {
66
+ type: [String, Array],
67
+ default: 'autoresize table image link fullscreen advlist lists autolink code',
68
+ },
69
+ interdireImage: { type: Boolean, default: false },
70
+ imageTailleMaximale: { type: Number, default: 5 }, // Mo
71
+ cacherBarreMenu: { type: Boolean, default: false },
72
+ cacherBarreOutils: { type: Boolean, default: false },
73
+ toolbar: {
74
+ type: [String, Array],
75
+ default:
76
+ 'bold italic underline strikethrough | fontsizeselect | forecolor backcolor | ' +
77
+ 'alignleft aligncenter alignright alignjustify | emoticons | bullist numlist | ' +
78
+ 'image | link | outdent indent blockquote | undo redo | fullscreen | removeformat | table | code',
79
+ },
80
+ },
81
+
82
+ emits: ['update:modelValue', 'blur'],
83
+
84
+ data() {
85
+ return {
86
+ editorReady: true,
87
+ _editor: null,
88
+ editorValue: this.modelValue, // lié au v-model
89
+ _reinitPending: false,
90
+ _reinitLock: false,
91
+ }
92
+ },
93
+
94
+ computed: {
95
+ imageTailleMaxMo() {
96
+ const n = Number(this.imageTailleMaximale)
97
+ if (!Number.isFinite(n)) return 5
98
+ return Math.min(100, Math.max(0, n))
99
+ },
100
+
101
+ toolbarEffective() {
102
+ if (this.cacherBarreOutils) return false
103
+ const raw = Array.isArray(this.toolbar) ? this.toolbar.join(' | ') : this.toolbar
104
+ if (!this.interdireImage) return this._normalizeToolbar(raw)
105
+
106
+ const sansImage = raw
107
+ .replace(/\bimage\b/g, '')
108
+ .replace(/\s*\|\s*/g, ' | ')
109
+ .replace(/(\s*\|\s*){2,}/g, ' | ')
110
+ .replace(/^\s*\|\s*|\s*\|\s*$/g, '')
111
+ .replace(/\s{2,}/g, ' ')
112
+ .trim()
113
+
114
+ return this._normalizeToolbar(sansImage)
115
+ },
116
+
117
+ pluginsEffective() {
118
+ const list = (Array.isArray(this.plugins) ? this.plugins : this.plugins.split(/\s+/))
119
+ .map(p => p.trim())
120
+ .filter(Boolean)
121
+ const filtered = this.interdireImage ? list.filter(p => p !== 'image') : list
122
+ return filtered.join(' ')
123
+ },
124
+
125
+ initOptions() {
126
+ const opts = {
127
+ autolink: !this.desactiver,
128
+ autoresize: true,
129
+ automatic_uploads: !this.interdireImage,
130
+ branding: false,
131
+ browser_spellcheck: true,
132
+ content_css: 'default',
133
+ deprecation_warnings: false,
134
+ emoticons_database: 'emojis',
135
+ highlight_on_focus: false,
136
+ image_dimensions: !this.interdireImage,
137
+ language: this.langue === 'en' ? 'en_US' : 'fr_FR',
138
+ license_key: 'gpl',
139
+ menubar: !this.cacherBarreMenu,
140
+ object_resizing: !this.interdireImage,
141
+ paste_data_images: !this.interdireImage,
142
+ plugins: this.pluginsEffective,
143
+ promotion: false,
144
+ readonly: this.desactiver,
145
+ resize: !this.interdireRedimension,
146
+ resize_img_proportional: !this.interdireImage,
147
+ skin_url: 'default',
148
+ statusbar: this.cacherBarreOutils === false,
149
+ theme_advanced_resizing: true,
150
+ theme_advanced_resizing_use_cookie: false,
151
+ toolbar: this.toolbarEffective,
152
+
153
+ // --- File picker seulement si images permises ---
154
+ ...(this.interdireImage ? {} : { file_picker_types: 'image' }),
155
+
156
+ setup: editor => {
157
+ this._editor = editor
158
+
159
+ editor.on('init', () => {
160
+ // init ok
161
+ })
162
+
163
+ // Intercepter clics liens
164
+ editor.on('click', e => {
165
+ const a = e.target?.closest?.('a')
166
+ if (!a) return
167
+ const href = a.getAttribute('href')
168
+ const target = a.getAttribute('target')
169
+ if (href) {
170
+ e.preventDefault()
171
+ openLink(href, target)
172
+ }
173
+ })
174
+
175
+ // ENTER sur un lien
176
+ editor.on('keydown', e => {
177
+ if (e.key !== 'Enter') return
178
+ const node = editor.selection?.getNode?.()
179
+ const a = node?.closest?.('a')
180
+ if (a) {
181
+ e.preventDefault()
182
+ openLink(a.getAttribute('href'), a.getAttribute('target'))
183
+ }
184
+ })
185
+ },
186
+ }
187
+
188
+ if (!this.interdireImage) {
189
+ opts.file_picker_callback = cb => {
190
+ const input = document.createElement('input')
191
+ input.type = 'file'
192
+ input.accept = 'image/*'
193
+ input.onchange = () => {
194
+ const file = input.files?.[0]
195
+ if (!file) return
196
+
197
+ const reader = new FileReader()
198
+ reader.onload = () => {
199
+ const base64 = String(reader.result).split(',')[1]
200
+ const editor = this._editor
201
+ const blobCache = editor?.editorUpload?.blobCache
202
+ if (!blobCache) return
203
+
204
+ const id = 'blobid' + Date.now()
205
+ const blobInfo = blobCache.create(id, file, base64)
206
+ const imageSize = blobInfo.blob().size / 1_000_000
207
+ if (imageSize > this.imageTailleMaxMo) {
208
+ alert('Fichier trop volumineux > ' + this.imageTailleMaxMo + ' Mo')
209
+ return
210
+ }
211
+ blobCache.add(blobInfo)
212
+ cb(blobInfo.blobUri(), { title: file.name })
213
+ }
214
+ reader.readAsDataURL(file)
215
+ }
216
+ input.click()
217
+ }
218
+ }
219
+
220
+ return opts
221
+ },
222
+ },
223
+
224
+ watch: {
225
+ /* v-model (parent -> enfant) */
226
+ modelValue(v) {
227
+ this.editorValue = v
228
+ },
229
+
230
+ /* v-model (enfant -> parent) */
231
+ editorValue(v) {
232
+ this.$emit('update:modelValue', v)
233
+ },
234
+
235
+ // Re-montage propre sur changements des props
236
+ interdireImage() {
237
+ this._scheduleReinit()
238
+ },
239
+ interdireRedimension() {
240
+ this._scheduleReinit()
241
+ },
242
+ toolbar() {
243
+ this._scheduleReinit()
244
+ },
245
+ imageTailleMaximale() {
246
+ this._scheduleReinit()
247
+ },
248
+ plugins() {
249
+ this._scheduleReinit()
250
+ },
251
+ cacherBarreOutils() {
252
+ this._scheduleReinit()
253
+ },
254
+ cacherBarreMenu() {
255
+ this._scheduleReinit()
256
+ },
257
+ },
258
+
259
+ methods: {
260
+ onBlur() {
261
+ if (!this.desactiver) this.$emit('blur')
262
+ },
263
+ onUserActivity() {},
264
+
265
+ _normalizeToolbar(tb) {
266
+ const trimmed = (tb || '').trim()
267
+ return trimmed && trimmed !== '|' ? trimmed : 'undo redo'
268
+ },
269
+
270
+ _scheduleReinit() {
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
+ const ed = this._editor
284
+ if (ed && typeof ed.remove === 'function') {
285
+ try {
286
+ ed.remove()
287
+ } catch (e) {
288
+ console.warn('[Editeur] editor.remove erreur', e)
289
+ }
290
+ }
291
+ this._editor = null
292
+
293
+ // remount contrôlé
294
+ this.editorReady = false
295
+ await this.$nextTick()
296
+ this.editorReady = true
297
+ await this.$nextTick()
298
+ } finally {
299
+ this._reinitLock = false
300
+ }
301
+ },
302
+ },
303
+
304
+ beforeUnmount() {
305
+ try {
306
+ const ed = this._editor
307
+ if (ed && typeof ed.remove === 'function') ed.remove()
308
+ } catch {}
309
+ this._editor = null
310
+ },
311
+ }
312
+ </script>
313
+
314
+ <style scoped>
315
+ .editor a {
316
+ cursor: pointer !important;
317
+ }
318
+ </style>