codevdesign 0.0.90 → 0.0.92

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,319 @@
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-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>