codevdesign 1.0.5 → 1.0.6

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