codevdesign 0.0.88 → 0.0.89

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,318 +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
- license-key="gpl"
10
- @blur="onBlur"
11
- @keydown="onUserActivity"
12
- @change="onUserActivity"
13
- />
14
- </div>
15
- </template>
16
-
17
- <script>
18
- import Editor from '@tinymce/tinymce-vue'
19
- import * as tinymce from 'tinymce/tinymce.js'
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>
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
+ license-key="gpl"
10
+ @blur="onBlur"
11
+ @keydown="onUserActivity"
12
+ @change="onUserActivity"
13
+ />
14
+ </div>
15
+ </template>
16
+
17
+ <script>
18
+ import Editor from '@tinymce/tinymce-vue'
19
+ import * as tinymce from 'tinymce/tinymce.js'
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>