vue2-bbl-editor 1.3.0
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.
- package/CHANGELOG.md +659 -0
- package/COMPONENT_USAGE_GUIDE.md +2590 -0
- package/CONTENT_WRAPPER_GUIDE.md +385 -0
- package/EXTERNAL_HTML_RENDERING.md +266 -0
- package/EXTERNAL_INTEGRATION_GUIDE.md +833 -0
- package/INSTALLATION.md +282 -0
- package/LICENSE +21 -0
- package/PACKAGE_DOCUMENTATION.md +1386 -0
- package/QUICK_SETUP.md +99 -0
- package/README.md +1694 -0
- package/dist/demo.html +10 -0
- package/dist/vue2-bbl-editor.common.js +24486 -0
- package/dist/vue2-bbl-editor.common.js.map +1 -0
- package/dist/vue2-bbl-editor.css +1 -0
- package/dist/vue2-bbl-editor.umd.js +24497 -0
- package/dist/vue2-bbl-editor.umd.js.map +1 -0
- package/dist/vue2-bbl-editor.umd.min.js +6 -0
- package/dist/vue2-bbl-editor.umd.min.js.map +1 -0
- package/package.json +172 -0
- package/types/index.d.ts +266 -0
|
@@ -0,0 +1,2590 @@
|
|
|
1
|
+
# Complete Component Usage Guide
|
|
2
|
+
|
|
3
|
+
This guide provides comprehensive examples of how to use all components included in the Vue2 Premium BBL Editor package.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Main Editor Components](#main-editor-components)
|
|
8
|
+
2. [Toolbar Components](#toolbar-components)
|
|
9
|
+
3. [Menu Components](#menu-components)
|
|
10
|
+
4. [Modal Components](#modal-components)
|
|
11
|
+
5. [Upload System Components](#upload-system-components)
|
|
12
|
+
6. [Development Tools](#development-tools)
|
|
13
|
+
7. [Composables](#composables)
|
|
14
|
+
8. [Advanced Usage Patterns](#advanced-usage-patterns)
|
|
15
|
+
|
|
16
|
+
## Main Editor Components
|
|
17
|
+
|
|
18
|
+
### PremiumBblEditor (Composition API)
|
|
19
|
+
|
|
20
|
+
The main editor component using Vue 3 Composition API (compatible with Vue 2.6+ via @vue/composition-api).
|
|
21
|
+
|
|
22
|
+
**Import:**
|
|
23
|
+
```javascript
|
|
24
|
+
import { PremiumBblEditor } from 'vue2-premium-bbl-editor'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Basic Usage:**
|
|
28
|
+
```vue
|
|
29
|
+
<template>
|
|
30
|
+
<PremiumBblEditor
|
|
31
|
+
v-model="content"
|
|
32
|
+
placeholder="Start writing..."
|
|
33
|
+
@ready="handleReady"
|
|
34
|
+
/>
|
|
35
|
+
</template>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### PremiumBblEditorOptionsAPI (Options API)
|
|
39
|
+
|
|
40
|
+
The same editor component using pure Vue 2 Options API (no Composition API dependency required).
|
|
41
|
+
|
|
42
|
+
**Import:**
|
|
43
|
+
```javascript
|
|
44
|
+
import { PremiumBblEditorOptionsAPI } from 'vue2-premium-bbl-editor'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Basic Usage:**
|
|
48
|
+
```vue
|
|
49
|
+
<template>
|
|
50
|
+
<PremiumBblEditorOptionsAPI
|
|
51
|
+
v-model="content"
|
|
52
|
+
placeholder="Start writing with Options API..."
|
|
53
|
+
@ready="handleReady"
|
|
54
|
+
/>
|
|
55
|
+
</template>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Complete Options API Example
|
|
59
|
+
|
|
60
|
+
```vue
|
|
61
|
+
<template>
|
|
62
|
+
<PremiumBblEditorOptionsAPI
|
|
63
|
+
v-model="content"
|
|
64
|
+
:placeholder="placeholder"
|
|
65
|
+
:editable="editable"
|
|
66
|
+
:theme="theme"
|
|
67
|
+
:toolbar-config="toolbarConfig"
|
|
68
|
+
:extension-config="extensionConfig"
|
|
69
|
+
:upload-handler="uploadHandler"
|
|
70
|
+
:auto-save="autoSave"
|
|
71
|
+
:auto-save-interval="autoSaveInterval"
|
|
72
|
+
:max-height="maxHeight"
|
|
73
|
+
:min-height="minHeight"
|
|
74
|
+
:font-families="fontFamilies"
|
|
75
|
+
:font-sizes="fontSizes"
|
|
76
|
+
:line-heights="lineHeights"
|
|
77
|
+
:allowed-headings="allowedHeadings"
|
|
78
|
+
:max-file-size="maxFileSize"
|
|
79
|
+
:allowed-image-types="allowedImageTypes"
|
|
80
|
+
:allowed-video-types="allowedVideoTypes"
|
|
81
|
+
:show-toolbar="showToolbar"
|
|
82
|
+
:show-bubble-menu="showBubbleMenu"
|
|
83
|
+
:show-table-bubble-menu="showTableBubbleMenu"
|
|
84
|
+
:show-image-bubble-menu="showImageBubbleMenu"
|
|
85
|
+
:show-video-bubble-menu="showVideoBubbleMenu"
|
|
86
|
+
:editor-class="editorClass"
|
|
87
|
+
:toolbar-class="toolbarClass"
|
|
88
|
+
:content-class="contentClass"
|
|
89
|
+
@input="handleInput"
|
|
90
|
+
@update="handleUpdate"
|
|
91
|
+
@ready="handleReady"
|
|
92
|
+
@created="handleCreated"
|
|
93
|
+
@focus="handleFocus"
|
|
94
|
+
@blur="handleBlur"
|
|
95
|
+
@destroyed="handleDestroyed"
|
|
96
|
+
@auto-save="handleAutoSave"
|
|
97
|
+
@content-limit-exceeded="handleLimitExceeded"
|
|
98
|
+
@error="handleError"
|
|
99
|
+
/>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
import { PremiumBblEditorOptionsAPI } from 'vue2-premium-bbl-editor'
|
|
104
|
+
|
|
105
|
+
export default {
|
|
106
|
+
components: {
|
|
107
|
+
PremiumBblEditorOptionsAPI
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
data() {
|
|
111
|
+
return {
|
|
112
|
+
content: '<p>Start writing with Options API...</p>',
|
|
113
|
+
placeholder: 'Enter your content here...',
|
|
114
|
+
editable: true,
|
|
115
|
+
theme: 'default', // 'default', 'minimal', 'dark'
|
|
116
|
+
autoSave: true,
|
|
117
|
+
autoSaveInterval: 30000, // 30 seconds
|
|
118
|
+
maxHeight: 500,
|
|
119
|
+
minHeight: 200,
|
|
120
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
121
|
+
showToolbar: true,
|
|
122
|
+
showBubbleMenu: true,
|
|
123
|
+
showTableBubbleMenu: true,
|
|
124
|
+
showImageBubbleMenu: true,
|
|
125
|
+
showVideoBubbleMenu: true,
|
|
126
|
+
|
|
127
|
+
// Toolbar configuration
|
|
128
|
+
toolbarConfig: {
|
|
129
|
+
bold: true,
|
|
130
|
+
italic: true,
|
|
131
|
+
underline: true,
|
|
132
|
+
strike: true,
|
|
133
|
+
code: true,
|
|
134
|
+
textColor: true,
|
|
135
|
+
highlight: true,
|
|
136
|
+
fontFamily: true,
|
|
137
|
+
fontSize: true,
|
|
138
|
+
textAlign: true,
|
|
139
|
+
lineHeight: true,
|
|
140
|
+
headings: true,
|
|
141
|
+
lists: true,
|
|
142
|
+
blockquote: true,
|
|
143
|
+
codeBlock: true,
|
|
144
|
+
horizontalRule: true,
|
|
145
|
+
link: true,
|
|
146
|
+
image: true,
|
|
147
|
+
video: true,
|
|
148
|
+
table: true,
|
|
149
|
+
clearFormatting: true,
|
|
150
|
+
sourceCode: true,
|
|
151
|
+
undo: true,
|
|
152
|
+
redo: true
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// Extension configuration
|
|
156
|
+
extensionConfig: {
|
|
157
|
+
image: {
|
|
158
|
+
allowResize: true,
|
|
159
|
+
allowAlignment: true,
|
|
160
|
+
allowDelete: true,
|
|
161
|
+
maxWidth: '100%',
|
|
162
|
+
quality: 0.8
|
|
163
|
+
},
|
|
164
|
+
video: {
|
|
165
|
+
allowResize: true,
|
|
166
|
+
allowAlignment: true,
|
|
167
|
+
allowDelete: true,
|
|
168
|
+
maxWidth: '100%',
|
|
169
|
+
autoplay: false
|
|
170
|
+
},
|
|
171
|
+
table: {
|
|
172
|
+
resizable: true,
|
|
173
|
+
allowRowControls: true,
|
|
174
|
+
allowColumnControls: true,
|
|
175
|
+
cellSelection: true
|
|
176
|
+
},
|
|
177
|
+
link: {
|
|
178
|
+
openOnClick: false,
|
|
179
|
+
autolink: true,
|
|
180
|
+
linkOnPaste: true
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// Styling options
|
|
185
|
+
fontFamilies: [
|
|
186
|
+
'Inter',
|
|
187
|
+
'Arial',
|
|
188
|
+
'Helvetica',
|
|
189
|
+
'Times New Roman',
|
|
190
|
+
'Georgia',
|
|
191
|
+
'Courier New'
|
|
192
|
+
],
|
|
193
|
+
fontSizes: [
|
|
194
|
+
'12px', '14px', '16px', '18px', '20px', '24px', '28px', '32px'
|
|
195
|
+
],
|
|
196
|
+
lineHeights: [
|
|
197
|
+
'1', '1.2', '1.4', '1.6', '1.8', '2', '2.5', '3'
|
|
198
|
+
],
|
|
199
|
+
allowedHeadings: [1, 2, 3, 4, 5, 6],
|
|
200
|
+
allowedImageTypes: [
|
|
201
|
+
'image/jpeg',
|
|
202
|
+
'image/png',
|
|
203
|
+
'image/gif',
|
|
204
|
+
'image/webp'
|
|
205
|
+
],
|
|
206
|
+
allowedVideoTypes: [
|
|
207
|
+
'video/mp4',
|
|
208
|
+
'video/webm',
|
|
209
|
+
'video/ogg'
|
|
210
|
+
],
|
|
211
|
+
|
|
212
|
+
// Local file base64 management
|
|
213
|
+
useBase64Upload: true,
|
|
214
|
+
base64Quality: 0.8,
|
|
215
|
+
base64MaxWidth: 1920,
|
|
216
|
+
base64MaxHeight: 1080,
|
|
217
|
+
enableImageCompression: true,
|
|
218
|
+
preserveOriginalFileName: true,
|
|
219
|
+
base64Prefix: 'data:',
|
|
220
|
+
|
|
221
|
+
// CSS classes
|
|
222
|
+
editorClass: 'my-custom-editor-options',
|
|
223
|
+
toolbarClass: 'my-custom-toolbar-options',
|
|
224
|
+
contentClass: 'my-custom-content-options'
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
methods: {
|
|
229
|
+
// Event handlers
|
|
230
|
+
handleInput(content) {
|
|
231
|
+
console.log('Options API - Content changed:', content)
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
handleUpdate(content) {
|
|
235
|
+
console.log('Options API - Content updated:', content)
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
handleReady(editor) {
|
|
239
|
+
console.log('Options API - Editor ready:', editor)
|
|
240
|
+
// Store editor reference for later use
|
|
241
|
+
this.editorInstance = editor
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
handleCreated(editor) {
|
|
245
|
+
console.log('Options API - Editor created:', editor)
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
handleFocus() {
|
|
249
|
+
console.log('Options API - Editor focused')
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
handleBlur() {
|
|
253
|
+
console.log('Options API - Editor blurred')
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
handleDestroyed() {
|
|
257
|
+
console.log('Options API - Editor destroyed')
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
handleAutoSave(content) {
|
|
261
|
+
console.log('Options API - Auto-saving content...')
|
|
262
|
+
// Save to your backend
|
|
263
|
+
this.saveToServer(content)
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
handleLimitExceeded({ current, max }) {
|
|
267
|
+
console.log(`Options API - Content limit exceeded: ${current}/${max}`)
|
|
268
|
+
alert(`Content too long: ${current}/${max} characters`)
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
handleError(error) {
|
|
272
|
+
console.error('Options API - Editor error:', error)
|
|
273
|
+
// Handle different error types
|
|
274
|
+
switch (error.type) {
|
|
275
|
+
case 'missing-dependencies':
|
|
276
|
+
this.showDependencyError(error.dependencies)
|
|
277
|
+
break
|
|
278
|
+
case 'initialization-failed':
|
|
279
|
+
this.showInitializationError(error.message)
|
|
280
|
+
break
|
|
281
|
+
default:
|
|
282
|
+
this.showGenericError(error.message)
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
handleImageCompressed(compressionData) {
|
|
287
|
+
console.log('Options API - Image compressed:', compressionData)
|
|
288
|
+
|
|
289
|
+
// Calculate savings percentage
|
|
290
|
+
const savings = Math.round(((compressionData.originalSize - compressionData.compressedSize) / compressionData.originalSize) * 100)
|
|
291
|
+
|
|
292
|
+
// Show compression statistics
|
|
293
|
+
console.log(`Compression stats:
|
|
294
|
+
Original: ${this.formatFileSize(compressionData.originalSize)}
|
|
295
|
+
Compressed: ${this.formatFileSize(compressionData.compressedSize)}
|
|
296
|
+
Savings: ${savings}%
|
|
297
|
+
Ratio: ${compressionData.compressionRatio.toFixed(2)}x
|
|
298
|
+
Original dimensions: ${compressionData.originalDimensions.width}×${compressionData.originalDimensions.height}
|
|
299
|
+
Compressed dimensions: ${compressionData.compressedDimensions.width}×${compressionData.compressedDimensions.height}
|
|
300
|
+
`)
|
|
301
|
+
|
|
302
|
+
// You could emit this data to parent component or store it
|
|
303
|
+
this.$emit('image-compression-stats', compressionData)
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
// Custom upload handler
|
|
307
|
+
async uploadHandler(file) {
|
|
308
|
+
try {
|
|
309
|
+
const formData = new FormData()
|
|
310
|
+
formData.append('file', file)
|
|
311
|
+
|
|
312
|
+
const response = await fetch('/api/upload', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: {
|
|
315
|
+
'Authorization': `Bearer ${this.getAuthToken()}`
|
|
316
|
+
},
|
|
317
|
+
body: formData
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
throw new Error(`Upload failed: ${response.statusText}`)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const data = await response.json()
|
|
325
|
+
return {
|
|
326
|
+
src: data.url,
|
|
327
|
+
alt: file.name
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error('Options API - Upload error:', error)
|
|
331
|
+
throw error
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// Helper methods
|
|
336
|
+
async saveToServer(content) {
|
|
337
|
+
try {
|
|
338
|
+
await fetch('/api/save', {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
headers: {
|
|
341
|
+
'Content-Type': 'application/json',
|
|
342
|
+
'Authorization': `Bearer ${this.getAuthToken()}`
|
|
343
|
+
},
|
|
344
|
+
body: JSON.stringify({ content })
|
|
345
|
+
})
|
|
346
|
+
console.log('Options API - Content saved successfully')
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error('Options API - Save failed:', error)
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
getAuthToken() {
|
|
353
|
+
return localStorage.getItem('authToken') || ''
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
formatFileSize(bytes) {
|
|
357
|
+
if (bytes === 0) return '0 Bytes'
|
|
358
|
+
const k = 1024
|
|
359
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
360
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
361
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
showDependencyError(dependencies) {
|
|
365
|
+
const message = `Missing dependencies: ${dependencies.join(', ')}`
|
|
366
|
+
alert(message)
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
showInitializationError(message) {
|
|
370
|
+
alert(`Editor initialization failed: ${message}`)
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
showGenericError(message) {
|
|
374
|
+
alert(`Editor error: ${message}`)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
</script>
|
|
379
|
+
|
|
380
|
+
<style scoped>
|
|
381
|
+
.my-custom-editor-options {
|
|
382
|
+
border: 2px solid #e2e8f0;
|
|
383
|
+
border-radius: 8px;
|
|
384
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.my-custom-toolbar-options {
|
|
388
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
389
|
+
border-bottom: 1px solid #e2e8f0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.my-custom-content-options {
|
|
393
|
+
font-family: 'Georgia', serif;
|
|
394
|
+
line-height: 1.6;
|
|
395
|
+
}
|
|
396
|
+
</style>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Component Comparison
|
|
400
|
+
|
|
401
|
+
| Feature | PremiumBblEditor (Composition API) | PremiumBblEditorOptionsAPI (Options API) |
|
|
402
|
+
|---------|-----------------------------------|------------------------------------------|
|
|
403
|
+
| **Vue Version** | Vue 2.6+ with @vue/composition-api | Pure Vue 2.x |
|
|
404
|
+
| **Dependencies** | Requires @vue/composition-api | No additional dependencies |
|
|
405
|
+
| **Performance** | Slightly better reactivity | Standard Vue 2 reactivity |
|
|
406
|
+
| **Bundle Size** | Slightly larger (composition-api) | Smaller bundle |
|
|
407
|
+
| **API Style** | Modern Composition API patterns | Traditional Options API |
|
|
408
|
+
| **Props** | Identical | Identical |
|
|
409
|
+
| **Events** | Identical | Identical |
|
|
410
|
+
| **Methods** | Identical | Identical |
|
|
411
|
+
| **Features** | All features available | All features available |
|
|
412
|
+
|
|
413
|
+
### When to Use Which Version
|
|
414
|
+
|
|
415
|
+
**Use PremiumBblEditor (Composition API) when:**
|
|
416
|
+
- You're already using @vue/composition-api in your project
|
|
417
|
+
- You prefer modern Vue 3 style code patterns
|
|
418
|
+
- You want the latest reactivity improvements
|
|
419
|
+
- You're planning to migrate to Vue 3 in the future
|
|
420
|
+
|
|
421
|
+
**Use PremiumBblEditorOptionsAPI (Options API) when:**
|
|
422
|
+
- You want to minimize dependencies
|
|
423
|
+
- Your project uses traditional Vue 2 patterns
|
|
424
|
+
- You want the smallest possible bundle size
|
|
425
|
+
- You're working with legacy Vue 2 projects
|
|
426
|
+
- You prefer the familiar Options API syntax
|
|
427
|
+
|
|
428
|
+
### Shared Props, Events, and Methods
|
|
429
|
+
|
|
430
|
+
Both components share the exact same interface:
|
|
431
|
+
|
|
432
|
+
```vue
|
|
433
|
+
<template>
|
|
434
|
+
<PremiumBblEditor
|
|
435
|
+
v-model="content"
|
|
436
|
+
:placeholder="placeholder"
|
|
437
|
+
:editable="editable"
|
|
438
|
+
:theme="theme"
|
|
439
|
+
:toolbar-config="toolbarConfig"
|
|
440
|
+
:extension-config="extensionConfig"
|
|
441
|
+
:upload-handler="uploadHandler"
|
|
442
|
+
:auto-save="autoSave"
|
|
443
|
+
:auto-save-interval="autoSaveInterval"
|
|
444
|
+
:max-height="maxHeight"
|
|
445
|
+
:min-height="minHeight"
|
|
446
|
+
:font-families="fontFamilies"
|
|
447
|
+
:font-sizes="fontSizes"
|
|
448
|
+
:line-heights="lineHeights"
|
|
449
|
+
:allowed-headings="allowedHeadings"
|
|
450
|
+
:max-file-size="maxFileSize"
|
|
451
|
+
:allowed-image-types="allowedImageTypes"
|
|
452
|
+
:allowed-video-types="allowedVideoTypes"
|
|
453
|
+
:show-toolbar="showToolbar"
|
|
454
|
+
:show-bubble-menu="showBubbleMenu"
|
|
455
|
+
:show-table-bubble-menu="showTableBubbleMenu"
|
|
456
|
+
:show-image-bubble-menu="showImageBubbleMenu"
|
|
457
|
+
:show-video-bubble-menu="showVideoBubbleMenu"
|
|
458
|
+
:editor-class="editorClass"
|
|
459
|
+
:toolbar-class="toolbarClass"
|
|
460
|
+
:content-class="contentClass"
|
|
461
|
+
@input="handleInput"
|
|
462
|
+
@update="handleUpdate"
|
|
463
|
+
@ready="handleReady"
|
|
464
|
+
@created="handleCreated"
|
|
465
|
+
@focus="handleFocus"
|
|
466
|
+
@blur="handleBlur"
|
|
467
|
+
@destroyed="handleDestroyed"
|
|
468
|
+
@auto-save="handleAutoSave"
|
|
469
|
+
@content-limit-exceeded="handleLimitExceeded"
|
|
470
|
+
@error="handleError"
|
|
471
|
+
/>
|
|
472
|
+
</template>
|
|
473
|
+
|
|
474
|
+
<script>
|
|
475
|
+
import { PremiumBblEditor } from 'vue2-premium-bbl-editor'
|
|
476
|
+
|
|
477
|
+
export default {
|
|
478
|
+
components: {
|
|
479
|
+
PremiumBblEditor
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
data() {
|
|
483
|
+
return {
|
|
484
|
+
content: '<p>Start writing...</p>',
|
|
485
|
+
placeholder: 'Enter your content here...',
|
|
486
|
+
editable: true,
|
|
487
|
+
theme: 'default', // 'default', 'minimal', 'dark'
|
|
488
|
+
autoSave: true,
|
|
489
|
+
autoSaveInterval: 30000, // 30 seconds
|
|
490
|
+
maxHeight: 500,
|
|
491
|
+
minHeight: 200,
|
|
492
|
+
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
493
|
+
showToolbar: true,
|
|
494
|
+
showBubbleMenu: true,
|
|
495
|
+
showTableBubbleMenu: true,
|
|
496
|
+
showImageBubbleMenu: true,
|
|
497
|
+
showVideoBubbleMenu: true,
|
|
498
|
+
|
|
499
|
+
// Toolbar configuration
|
|
500
|
+
toolbarConfig: {
|
|
501
|
+
bold: true,
|
|
502
|
+
italic: true,
|
|
503
|
+
underline: true,
|
|
504
|
+
strike: true,
|
|
505
|
+
code: true,
|
|
506
|
+
textColor: true,
|
|
507
|
+
highlight: true,
|
|
508
|
+
fontFamily: true,
|
|
509
|
+
fontSize: true,
|
|
510
|
+
textAlign: true,
|
|
511
|
+
lineHeight: true,
|
|
512
|
+
headings: true,
|
|
513
|
+
lists: true,
|
|
514
|
+
blockquote: true,
|
|
515
|
+
codeBlock: true,
|
|
516
|
+
horizontalRule: true,
|
|
517
|
+
link: true,
|
|
518
|
+
image: true,
|
|
519
|
+
video: true,
|
|
520
|
+
table: true,
|
|
521
|
+
clearFormatting: true,
|
|
522
|
+
sourceCode: true,
|
|
523
|
+
undo: true,
|
|
524
|
+
redo: true
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
// Extension configuration
|
|
528
|
+
extensionConfig: {
|
|
529
|
+
image: {
|
|
530
|
+
allowResize: true,
|
|
531
|
+
allowAlignment: true,
|
|
532
|
+
allowDelete: true,
|
|
533
|
+
maxWidth: '100%',
|
|
534
|
+
quality: 0.8
|
|
535
|
+
},
|
|
536
|
+
video: {
|
|
537
|
+
allowResize: true,
|
|
538
|
+
allowAlignment: true,
|
|
539
|
+
allowDelete: true,
|
|
540
|
+
maxWidth: '100%',
|
|
541
|
+
autoplay: false
|
|
542
|
+
},
|
|
543
|
+
table: {
|
|
544
|
+
resizable: true,
|
|
545
|
+
allowRowControls: true,
|
|
546
|
+
allowColumnControls: true,
|
|
547
|
+
cellSelection: true
|
|
548
|
+
},
|
|
549
|
+
link: {
|
|
550
|
+
openOnClick: false,
|
|
551
|
+
autolink: true,
|
|
552
|
+
linkOnPaste: true
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
// Styling options
|
|
557
|
+
fontFamilies: [
|
|
558
|
+
'Inter',
|
|
559
|
+
'Arial',
|
|
560
|
+
'Helvetica',
|
|
561
|
+
'Times New Roman',
|
|
562
|
+
'Georgia',
|
|
563
|
+
'Courier New'
|
|
564
|
+
],
|
|
565
|
+
fontSizes: [
|
|
566
|
+
'12px', '14px', '16px', '18px', '20px', '24px', '28px', '32px'
|
|
567
|
+
],
|
|
568
|
+
lineHeights: [
|
|
569
|
+
'1', '1.2', '1.4', '1.6', '1.8', '2', '2.5', '3'
|
|
570
|
+
],
|
|
571
|
+
allowedHeadings: [1, 2, 3, 4, 5, 6],
|
|
572
|
+
allowedImageTypes: [
|
|
573
|
+
'image/jpeg',
|
|
574
|
+
'image/png',
|
|
575
|
+
'image/gif',
|
|
576
|
+
'image/webp'
|
|
577
|
+
],
|
|
578
|
+
allowedVideoTypes: [
|
|
579
|
+
'video/mp4',
|
|
580
|
+
'video/webm',
|
|
581
|
+
'video/ogg'
|
|
582
|
+
],
|
|
583
|
+
|
|
584
|
+
// CSS classes
|
|
585
|
+
editorClass: 'my-custom-editor',
|
|
586
|
+
toolbarClass: 'my-custom-toolbar',
|
|
587
|
+
contentClass: 'my-custom-content'
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
methods: {
|
|
592
|
+
// Event handlers
|
|
593
|
+
handleInput(content) {
|
|
594
|
+
console.log('Content changed:', content)
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
handleUpdate(content) {
|
|
598
|
+
console.log('Content updated:', content)
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
handleReady(editor) {
|
|
602
|
+
console.log('Editor ready:', editor)
|
|
603
|
+
// Editor is ready, you can now use editor methods
|
|
604
|
+
},
|
|
605
|
+
|
|
606
|
+
handleCreated(editor) {
|
|
607
|
+
console.log('Editor created:', editor)
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
handleFocus() {
|
|
611
|
+
console.log('Editor focused')
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
handleBlur() {
|
|
615
|
+
console.log('Editor blurred')
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
handleDestroyed() {
|
|
619
|
+
console.log('Editor destroyed')
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
handleAutoSave(content) {
|
|
623
|
+
console.log('Auto-saving content...')
|
|
624
|
+
// Save to your backend
|
|
625
|
+
this.saveToServer(content)
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
handleLimitExceeded({ current, max }) {
|
|
629
|
+
console.log(`Content limit exceeded: ${current}/${max}`)
|
|
630
|
+
alert(`Content too long: ${current}/${max} characters`)
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
handleError(error) {
|
|
634
|
+
console.error('Editor error:', error)
|
|
635
|
+
// Handle different error types
|
|
636
|
+
switch (error.type) {
|
|
637
|
+
case 'missing-dependencies':
|
|
638
|
+
this.showDependencyError(error.dependencies)
|
|
639
|
+
break
|
|
640
|
+
case 'initialization-failed':
|
|
641
|
+
this.showInitializationError(error.message)
|
|
642
|
+
break
|
|
643
|
+
default:
|
|
644
|
+
this.showGenericError(error.message)
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
|
|
648
|
+
// Custom upload handler
|
|
649
|
+
async uploadHandler(file) {
|
|
650
|
+
try {
|
|
651
|
+
const formData = new FormData()
|
|
652
|
+
formData.append('file', file)
|
|
653
|
+
|
|
654
|
+
const response = await fetch('/api/upload', {
|
|
655
|
+
method: 'POST',
|
|
656
|
+
headers: {
|
|
657
|
+
'Authorization': `Bearer ${this.authToken}`
|
|
658
|
+
},
|
|
659
|
+
body: formData
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
if (!response.ok) {
|
|
663
|
+
throw new Error(`Upload failed: ${response.statusText}`)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const data = await response.json()
|
|
667
|
+
return {
|
|
668
|
+
src: data.url,
|
|
669
|
+
alt: file.name
|
|
670
|
+
}
|
|
671
|
+
} catch (error) {
|
|
672
|
+
console.error('Upload error:', error)
|
|
673
|
+
throw error
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
// Helper methods
|
|
678
|
+
async saveToServer(content) {
|
|
679
|
+
try {
|
|
680
|
+
await fetch('/api/save', {
|
|
681
|
+
method: 'POST',
|
|
682
|
+
headers: {
|
|
683
|
+
'Content-Type': 'application/json',
|
|
684
|
+
'Authorization': `Bearer ${this.authToken}`
|
|
685
|
+
},
|
|
686
|
+
body: JSON.stringify({ content })
|
|
687
|
+
})
|
|
688
|
+
console.log('Content saved successfully')
|
|
689
|
+
} catch (error) {
|
|
690
|
+
console.error('Save failed:', error)
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
showDependencyError(dependencies) {
|
|
695
|
+
const message = `Missing dependencies: ${dependencies.join(', ')}`
|
|
696
|
+
alert(message)
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
showInitializationError(message) {
|
|
700
|
+
alert(`Editor initialization failed: ${message}`)
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
showGenericError(message) {
|
|
704
|
+
alert(`Editor error: ${message}`)
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
</script>
|
|
709
|
+
|
|
710
|
+
<style scoped>
|
|
711
|
+
.my-custom-editor {
|
|
712
|
+
border: 2px solid #e2e8f0;
|
|
713
|
+
border-radius: 8px;
|
|
714
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.my-custom-toolbar {
|
|
718
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
719
|
+
border-bottom: 1px solid #e2e8f0;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.my-custom-content {
|
|
723
|
+
font-family: 'Georgia', serif;
|
|
724
|
+
line-height: 1.6;
|
|
725
|
+
}
|
|
726
|
+
</style>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### PremiumBblEditorOptionsAPI
|
|
730
|
+
|
|
731
|
+
The same editor component using Vue 2 Options API (for projects not using Composition API).
|
|
732
|
+
|
|
733
|
+
```vue
|
|
734
|
+
<template>
|
|
735
|
+
<PremiumBblEditorOptionsAPI
|
|
736
|
+
v-model="content"
|
|
737
|
+
:placeholder="placeholder"
|
|
738
|
+
:toolbar-config="toolbarConfig"
|
|
739
|
+
@ready="handleReady"
|
|
740
|
+
/>
|
|
741
|
+
</template>
|
|
742
|
+
|
|
743
|
+
<script>
|
|
744
|
+
import { PremiumBblEditorOptionsAPI } from 'vue2-premium-bbl-editor'
|
|
745
|
+
|
|
746
|
+
export default {
|
|
747
|
+
components: {
|
|
748
|
+
PremiumBblEditorOptionsAPI
|
|
749
|
+
},
|
|
750
|
+
|
|
751
|
+
data() {
|
|
752
|
+
return {
|
|
753
|
+
content: '<p>Options API Editor</p>',
|
|
754
|
+
placeholder: 'Start writing with Options API...',
|
|
755
|
+
toolbarConfig: {
|
|
756
|
+
bold: true,
|
|
757
|
+
italic: true,
|
|
758
|
+
underline: true,
|
|
759
|
+
link: true,
|
|
760
|
+
image: true
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
methods: {
|
|
766
|
+
handleReady(editor) {
|
|
767
|
+
console.log('Options API Editor ready:', editor)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
</script>
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
## Toolbar Components
|
|
775
|
+
|
|
776
|
+
### ToolbarMain
|
|
777
|
+
|
|
778
|
+
Complete toolbar with all formatting options.
|
|
779
|
+
|
|
780
|
+
```vue
|
|
781
|
+
<template>
|
|
782
|
+
<div class="custom-editor">
|
|
783
|
+
<ToolbarMain
|
|
784
|
+
v-if="editor"
|
|
785
|
+
:editor="editor"
|
|
786
|
+
:config="toolbarConfig"
|
|
787
|
+
:font-families="fontFamilies"
|
|
788
|
+
:font-sizes="fontSizes"
|
|
789
|
+
:line-heights="lineHeights"
|
|
790
|
+
:allowed-headings="allowedHeadings"
|
|
791
|
+
:source-code-mode="isSourceMode"
|
|
792
|
+
@execute-command="executeCommand"
|
|
793
|
+
@insert-link="openLinkModal"
|
|
794
|
+
@insert-image="openImageModal"
|
|
795
|
+
@insert-video="openVideoModal"
|
|
796
|
+
@clear-formatting="clearFormatting"
|
|
797
|
+
@toggle-source-code="toggleSourceCode"
|
|
798
|
+
/>
|
|
799
|
+
|
|
800
|
+
<div class="editor-content">
|
|
801
|
+
<editor-content :editor="editor" />
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
</template>
|
|
805
|
+
|
|
806
|
+
<script>
|
|
807
|
+
import { EditorContent } from '@tiptap/vue-2'
|
|
808
|
+
import { ToolbarMain, useEditor } from 'vue2-premium-bbl-editor'
|
|
809
|
+
|
|
810
|
+
export default {
|
|
811
|
+
components: {
|
|
812
|
+
EditorContent,
|
|
813
|
+
ToolbarMain
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
setup() {
|
|
817
|
+
const { editor, executeCommand } = useEditor({
|
|
818
|
+
content: '<p>Custom toolbar implementation</p>'
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
editor,
|
|
823
|
+
executeCommand
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
|
|
827
|
+
data() {
|
|
828
|
+
return {
|
|
829
|
+
isSourceMode: false,
|
|
830
|
+
toolbarConfig: {
|
|
831
|
+
bold: true,
|
|
832
|
+
italic: true,
|
|
833
|
+
underline: true,
|
|
834
|
+
textColor: true,
|
|
835
|
+
highlight: true,
|
|
836
|
+
fontFamily: true,
|
|
837
|
+
fontSize: true,
|
|
838
|
+
headings: true,
|
|
839
|
+
lists: true,
|
|
840
|
+
link: true,
|
|
841
|
+
image: true,
|
|
842
|
+
video: true,
|
|
843
|
+
table: true
|
|
844
|
+
},
|
|
845
|
+
fontFamilies: ['Inter', 'Arial', 'Georgia'],
|
|
846
|
+
fontSizes: ['14px', '16px', '18px', '20px'],
|
|
847
|
+
lineHeights: ['1.2', '1.4', '1.6', '1.8'],
|
|
848
|
+
allowedHeadings: [1, 2, 3, 4]
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
|
|
852
|
+
methods: {
|
|
853
|
+
openLinkModal() {
|
|
854
|
+
console.log('Open link modal')
|
|
855
|
+
// Your link modal logic
|
|
856
|
+
},
|
|
857
|
+
|
|
858
|
+
openImageModal() {
|
|
859
|
+
console.log('Open image modal')
|
|
860
|
+
// Your image modal logic
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
openVideoModal() {
|
|
864
|
+
console.log('Open video modal')
|
|
865
|
+
// Your video modal logic
|
|
866
|
+
},
|
|
867
|
+
|
|
868
|
+
clearFormatting() {
|
|
869
|
+
this.executeCommand('unsetAllMarks')
|
|
870
|
+
this.executeCommand('clearNodes')
|
|
871
|
+
},
|
|
872
|
+
|
|
873
|
+
toggleSourceCode() {
|
|
874
|
+
this.isSourceMode = !this.isSourceMode
|
|
875
|
+
// Toggle between visual and source code mode
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
</script>
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### ToolbarGroup
|
|
883
|
+
|
|
884
|
+
Group related toolbar buttons together.
|
|
885
|
+
|
|
886
|
+
```vue
|
|
887
|
+
<template>
|
|
888
|
+
<div class="custom-toolbar">
|
|
889
|
+
<ToolbarGroup label="Text Formatting">
|
|
890
|
+
<ToolbarButton
|
|
891
|
+
:editor="editor"
|
|
892
|
+
command="toggleBold"
|
|
893
|
+
icon="B"
|
|
894
|
+
tooltip="Bold"
|
|
895
|
+
:is-active="editor && editor.isActive('bold')"
|
|
896
|
+
/>
|
|
897
|
+
<ToolbarButton
|
|
898
|
+
:editor="editor"
|
|
899
|
+
command="toggleItalic"
|
|
900
|
+
icon="I"
|
|
901
|
+
tooltip="Italic"
|
|
902
|
+
:is-active="editor && editor.isActive('italic')"
|
|
903
|
+
/>
|
|
904
|
+
<ToolbarButton
|
|
905
|
+
:editor="editor"
|
|
906
|
+
command="toggleUnderline"
|
|
907
|
+
icon="U"
|
|
908
|
+
tooltip="Underline"
|
|
909
|
+
:is-active="editor && editor.isActive('underline')"
|
|
910
|
+
/>
|
|
911
|
+
</ToolbarGroup>
|
|
912
|
+
|
|
913
|
+
<ToolbarGroup label="Structure">
|
|
914
|
+
<ToolbarButton
|
|
915
|
+
:editor="editor"
|
|
916
|
+
command="toggleBulletList"
|
|
917
|
+
icon="•"
|
|
918
|
+
tooltip="Bullet List"
|
|
919
|
+
:is-active="editor && editor.isActive('bulletList')"
|
|
920
|
+
/>
|
|
921
|
+
<ToolbarButton
|
|
922
|
+
:editor="editor"
|
|
923
|
+
command="toggleOrderedList"
|
|
924
|
+
icon="1."
|
|
925
|
+
tooltip="Numbered List"
|
|
926
|
+
:is-active="editor && editor.isActive('orderedList')"
|
|
927
|
+
/>
|
|
928
|
+
</ToolbarGroup>
|
|
929
|
+
</div>
|
|
930
|
+
</template>
|
|
931
|
+
|
|
932
|
+
<script>
|
|
933
|
+
import { ToolbarGroup, ToolbarButton } from 'vue2-premium-bbl-editor'
|
|
934
|
+
|
|
935
|
+
export default {
|
|
936
|
+
components: {
|
|
937
|
+
ToolbarGroup,
|
|
938
|
+
ToolbarButton
|
|
939
|
+
},
|
|
940
|
+
|
|
941
|
+
props: {
|
|
942
|
+
editor: {
|
|
943
|
+
type: Object,
|
|
944
|
+
required: true
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
</script>
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
### ToolbarButton
|
|
952
|
+
|
|
953
|
+
Individual toolbar button component.
|
|
954
|
+
|
|
955
|
+
```vue
|
|
956
|
+
<template>
|
|
957
|
+
<div class="toolbar-buttons">
|
|
958
|
+
<!-- Basic button -->
|
|
959
|
+
<ToolbarButton
|
|
960
|
+
:editor="editor"
|
|
961
|
+
command="toggleBold"
|
|
962
|
+
icon="B"
|
|
963
|
+
tooltip="Bold (Ctrl+B)"
|
|
964
|
+
:is-active="editor && editor.isActive('bold')"
|
|
965
|
+
@click="handleBoldClick"
|
|
966
|
+
/>
|
|
967
|
+
|
|
968
|
+
<!-- Button with custom icon -->
|
|
969
|
+
<ToolbarButton
|
|
970
|
+
:editor="editor"
|
|
971
|
+
command="toggleItalic"
|
|
972
|
+
tooltip="Italic (Ctrl+I)"
|
|
973
|
+
:is-active="editor && editor.isActive('italic')"
|
|
974
|
+
>
|
|
975
|
+
<template #icon>
|
|
976
|
+
<svg width="16" height="16" viewBox="0 0 24 24">
|
|
977
|
+
<path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4h-8z"/>
|
|
978
|
+
</svg>
|
|
979
|
+
</template>
|
|
980
|
+
</ToolbarButton>
|
|
981
|
+
|
|
982
|
+
<!-- Dropdown button -->
|
|
983
|
+
<ToolbarButton
|
|
984
|
+
:editor="editor"
|
|
985
|
+
:is-dropdown="true"
|
|
986
|
+
tooltip="Heading"
|
|
987
|
+
@click="toggleHeadingDropdown"
|
|
988
|
+
>
|
|
989
|
+
<template #icon>H</template>
|
|
990
|
+
<template #dropdown v-if="showHeadingDropdown">
|
|
991
|
+
<div class="heading-dropdown">
|
|
992
|
+
<div @click="setHeading(1)">Heading 1</div>
|
|
993
|
+
<div @click="setHeading(2)">Heading 2</div>
|
|
994
|
+
<div @click="setHeading(3)">Heading 3</div>
|
|
995
|
+
<div @click="setParagraph">Paragraph</div>
|
|
996
|
+
</div>
|
|
997
|
+
</template>
|
|
998
|
+
</ToolbarButton>
|
|
999
|
+
</div>
|
|
1000
|
+
</template>
|
|
1001
|
+
|
|
1002
|
+
<script>
|
|
1003
|
+
import { ToolbarButton } from 'vue2-premium-bbl-editor'
|
|
1004
|
+
|
|
1005
|
+
export default {
|
|
1006
|
+
components: {
|
|
1007
|
+
ToolbarButton
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
props: {
|
|
1011
|
+
editor: {
|
|
1012
|
+
type: Object,
|
|
1013
|
+
required: true
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
|
|
1017
|
+
data() {
|
|
1018
|
+
return {
|
|
1019
|
+
showHeadingDropdown: false
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
|
|
1023
|
+
methods: {
|
|
1024
|
+
handleBoldClick() {
|
|
1025
|
+
console.log('Bold button clicked')
|
|
1026
|
+
// Custom logic before/after bold toggle
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
toggleHeadingDropdown() {
|
|
1030
|
+
this.showHeadingDropdown = !this.showHeadingDropdown
|
|
1031
|
+
},
|
|
1032
|
+
|
|
1033
|
+
setHeading(level) {
|
|
1034
|
+
this.editor.chain().focus().toggleHeading({ level }).run()
|
|
1035
|
+
this.showHeadingDropdown = false
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
setParagraph() {
|
|
1039
|
+
this.editor.chain().focus().setParagraph().run()
|
|
1040
|
+
this.showHeadingDropdown = false
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
</script>
|
|
1045
|
+
|
|
1046
|
+
<style scoped>
|
|
1047
|
+
.toolbar-buttons {
|
|
1048
|
+
display: flex;
|
|
1049
|
+
gap: 4px;
|
|
1050
|
+
align-items: center;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.heading-dropdown {
|
|
1054
|
+
position: absolute;
|
|
1055
|
+
top: 100%;
|
|
1056
|
+
left: 0;
|
|
1057
|
+
background: white;
|
|
1058
|
+
border: 1px solid #e2e8f0;
|
|
1059
|
+
border-radius: 4px;
|
|
1060
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
1061
|
+
z-index: 10;
|
|
1062
|
+
min-width: 120px;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.heading-dropdown div {
|
|
1066
|
+
padding: 8px 12px;
|
|
1067
|
+
cursor: pointer;
|
|
1068
|
+
border-bottom: 1px solid #f1f5f9;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.heading-dropdown div:hover {
|
|
1072
|
+
background: #f8fafc;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.heading-dropdown div:last-child {
|
|
1076
|
+
border-bottom: none;
|
|
1077
|
+
}
|
|
1078
|
+
</style>
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
## Menu Components
|
|
1082
|
+
|
|
1083
|
+
### EditorBubbleMenu
|
|
1084
|
+
|
|
1085
|
+
Text selection bubble menu that appears when text is selected.
|
|
1086
|
+
|
|
1087
|
+
```vue
|
|
1088
|
+
<template>
|
|
1089
|
+
<div class="editor-with-bubble">
|
|
1090
|
+
<editor-content :editor="editor" />
|
|
1091
|
+
|
|
1092
|
+
<EditorBubbleMenu
|
|
1093
|
+
v-if="editor"
|
|
1094
|
+
:editor="editor"
|
|
1095
|
+
:config="bubbleConfig"
|
|
1096
|
+
:show-link-options="true"
|
|
1097
|
+
:show-text-formatting="true"
|
|
1098
|
+
:show-text-styling="true"
|
|
1099
|
+
@execute-command="executeCommand"
|
|
1100
|
+
@open-link-modal="openLinkModal"
|
|
1101
|
+
/>
|
|
1102
|
+
</div>
|
|
1103
|
+
</template>
|
|
1104
|
+
|
|
1105
|
+
<script>
|
|
1106
|
+
import { EditorContent } from '@tiptap/vue-2'
|
|
1107
|
+
import { EditorBubbleMenu, useEditor } from 'vue2-premium-bbl-editor'
|
|
1108
|
+
|
|
1109
|
+
export default {
|
|
1110
|
+
components: {
|
|
1111
|
+
EditorContent,
|
|
1112
|
+
EditorBubbleMenu
|
|
1113
|
+
},
|
|
1114
|
+
|
|
1115
|
+
setup() {
|
|
1116
|
+
const { editor, executeCommand } = useEditor({
|
|
1117
|
+
content: '<p>Select text to see the bubble menu</p>'
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
return {
|
|
1121
|
+
editor,
|
|
1122
|
+
executeCommand
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
data() {
|
|
1127
|
+
return {
|
|
1128
|
+
bubbleConfig: {
|
|
1129
|
+
bold: true,
|
|
1130
|
+
italic: true,
|
|
1131
|
+
underline: true,
|
|
1132
|
+
strike: true,
|
|
1133
|
+
code: true,
|
|
1134
|
+
textColor: true,
|
|
1135
|
+
highlight: true,
|
|
1136
|
+
link: true
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
|
|
1141
|
+
methods: {
|
|
1142
|
+
openLinkModal() {
|
|
1143
|
+
console.log('Open link modal from bubble menu')
|
|
1144
|
+
// Your link modal logic
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
</script>
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
### TableBubbleMenu
|
|
1152
|
+
|
|
1153
|
+
Table-specific bubble menu for table operations.
|
|
1154
|
+
|
|
1155
|
+
```vue
|
|
1156
|
+
<template>
|
|
1157
|
+
<div class="editor-with-table-menu">
|
|
1158
|
+
<editor-content :editor="editor" />
|
|
1159
|
+
|
|
1160
|
+
<TableBubbleMenu
|
|
1161
|
+
v-if="editor"
|
|
1162
|
+
:editor="editor"
|
|
1163
|
+
:show-row-controls="true"
|
|
1164
|
+
:show-column-controls="true"
|
|
1165
|
+
:show-cell-controls="true"
|
|
1166
|
+
:show-table-controls="true"
|
|
1167
|
+
@add-row-before="addRowBefore"
|
|
1168
|
+
@add-row-after="addRowAfter"
|
|
1169
|
+
@delete-row="deleteRow"
|
|
1170
|
+
@add-column-before="addColumnBefore"
|
|
1171
|
+
@add-column-after="addColumnAfter"
|
|
1172
|
+
@delete-column="deleteColumn"
|
|
1173
|
+
@merge-cells="mergeCells"
|
|
1174
|
+
@split-cell="splitCell"
|
|
1175
|
+
@delete-table="deleteTable"
|
|
1176
|
+
/>
|
|
1177
|
+
</div>
|
|
1178
|
+
</template>
|
|
1179
|
+
|
|
1180
|
+
<script>
|
|
1181
|
+
import { EditorContent } from '@tiptap/vue-2'
|
|
1182
|
+
import { TableBubbleMenu, useEditor } from 'vue2-premium-bbl-editor'
|
|
1183
|
+
|
|
1184
|
+
export default {
|
|
1185
|
+
components: {
|
|
1186
|
+
EditorContent,
|
|
1187
|
+
TableBubbleMenu
|
|
1188
|
+
},
|
|
1189
|
+
|
|
1190
|
+
setup() {
|
|
1191
|
+
const { editor } = useEditor({
|
|
1192
|
+
content: `
|
|
1193
|
+
<table>
|
|
1194
|
+
<tr><th>Header 1</th><th>Header 2</th></tr>
|
|
1195
|
+
<tr><td>Cell 1</td><td>Cell 2</td></tr>
|
|
1196
|
+
</table>
|
|
1197
|
+
`
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
return { editor }
|
|
1201
|
+
},
|
|
1202
|
+
|
|
1203
|
+
methods: {
|
|
1204
|
+
addRowBefore() {
|
|
1205
|
+
this.editor.chain().focus().addRowBefore().run()
|
|
1206
|
+
},
|
|
1207
|
+
|
|
1208
|
+
addRowAfter() {
|
|
1209
|
+
this.editor.chain().focus().addRowAfter().run()
|
|
1210
|
+
},
|
|
1211
|
+
|
|
1212
|
+
deleteRow() {
|
|
1213
|
+
this.editor.chain().focus().deleteRow().run()
|
|
1214
|
+
},
|
|
1215
|
+
|
|
1216
|
+
addColumnBefore() {
|
|
1217
|
+
this.editor.chain().focus().addColumnBefore().run()
|
|
1218
|
+
},
|
|
1219
|
+
|
|
1220
|
+
addColumnAfter() {
|
|
1221
|
+
this.editor.chain().focus().addColumnAfter().run()
|
|
1222
|
+
},
|
|
1223
|
+
|
|
1224
|
+
deleteColumn() {
|
|
1225
|
+
this.editor.chain().focus().deleteColumn().run()
|
|
1226
|
+
},
|
|
1227
|
+
|
|
1228
|
+
mergeCells() {
|
|
1229
|
+
this.editor.chain().focus().mergeCells().run()
|
|
1230
|
+
},
|
|
1231
|
+
|
|
1232
|
+
splitCell() {
|
|
1233
|
+
this.editor.chain().focus().splitCell().run()
|
|
1234
|
+
},
|
|
1235
|
+
|
|
1236
|
+
deleteTable() {
|
|
1237
|
+
this.editor.chain().focus().deleteTable().run()
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
</script>
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
### ImageBubbleMenu
|
|
1245
|
+
|
|
1246
|
+
Image-specific bubble menu for image operations.
|
|
1247
|
+
|
|
1248
|
+
```vue
|
|
1249
|
+
<template>
|
|
1250
|
+
<div class="editor-with-image-menu">
|
|
1251
|
+
<editor-content :editor="editor" />
|
|
1252
|
+
|
|
1253
|
+
<ImageBubbleMenu
|
|
1254
|
+
v-if="editor"
|
|
1255
|
+
:editor="editor"
|
|
1256
|
+
:show-alignment-controls="true"
|
|
1257
|
+
:show-resize-controls="true"
|
|
1258
|
+
:show-delete-control="true"
|
|
1259
|
+
:show-alt-text-control="true"
|
|
1260
|
+
@align-left="alignImageLeft"
|
|
1261
|
+
@align-center="alignImageCenter"
|
|
1262
|
+
@align-right="alignImageRight"
|
|
1263
|
+
@resize-image="resizeImage"
|
|
1264
|
+
@delete-image="deleteImage"
|
|
1265
|
+
@edit-alt-text="editAltText"
|
|
1266
|
+
/>
|
|
1267
|
+
</div>
|
|
1268
|
+
</template>
|
|
1269
|
+
|
|
1270
|
+
<script>
|
|
1271
|
+
import { EditorContent } from '@tiptap/vue-2'
|
|
1272
|
+
import { ImageBubbleMenu, useEditor } from 'vue2-premium-bbl-editor'
|
|
1273
|
+
|
|
1274
|
+
export default {
|
|
1275
|
+
components: {
|
|
1276
|
+
EditorContent,
|
|
1277
|
+
ImageBubbleMenu
|
|
1278
|
+
},
|
|
1279
|
+
|
|
1280
|
+
setup() {
|
|
1281
|
+
const { editor } = useEditor({
|
|
1282
|
+
content: '<p>Click on an image to see the image menu</p>'
|
|
1283
|
+
})
|
|
1284
|
+
|
|
1285
|
+
return { editor }
|
|
1286
|
+
},
|
|
1287
|
+
|
|
1288
|
+
methods: {
|
|
1289
|
+
alignImageLeft() {
|
|
1290
|
+
// Custom image alignment logic
|
|
1291
|
+
console.log('Align image left')
|
|
1292
|
+
},
|
|
1293
|
+
|
|
1294
|
+
alignImageCenter() {
|
|
1295
|
+
console.log('Align image center')
|
|
1296
|
+
},
|
|
1297
|
+
|
|
1298
|
+
alignImageRight() {
|
|
1299
|
+
console.log('Align image right')
|
|
1300
|
+
},
|
|
1301
|
+
|
|
1302
|
+
resizeImage(size) {
|
|
1303
|
+
console.log('Resize image to:', size)
|
|
1304
|
+
},
|
|
1305
|
+
|
|
1306
|
+
deleteImage() {
|
|
1307
|
+
console.log('Delete image')
|
|
1308
|
+
},
|
|
1309
|
+
|
|
1310
|
+
editAltText() {
|
|
1311
|
+
console.log('Edit alt text')
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
</script>
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
### VideoBubbleMenu
|
|
1319
|
+
|
|
1320
|
+
Video-specific bubble menu for video operations.
|
|
1321
|
+
|
|
1322
|
+
```vue
|
|
1323
|
+
<template>
|
|
1324
|
+
<div class="editor-with-video-menu">
|
|
1325
|
+
<editor-content :editor="editor" />
|
|
1326
|
+
|
|
1327
|
+
<VideoBubbleMenu
|
|
1328
|
+
v-if="editor"
|
|
1329
|
+
:editor="editor"
|
|
1330
|
+
:show-alignment-controls="true"
|
|
1331
|
+
:show-resize-controls="true"
|
|
1332
|
+
:show-playback-controls="true"
|
|
1333
|
+
:show-delete-control="true"
|
|
1334
|
+
@align-left="alignVideoLeft"
|
|
1335
|
+
@align-center="alignVideoCenter"
|
|
1336
|
+
@align-right="alignVideoRight"
|
|
1337
|
+
@resize-video="resizeVideo"
|
|
1338
|
+
@toggle-autoplay="toggleAutoplay"
|
|
1339
|
+
@toggle-controls="toggleControls"
|
|
1340
|
+
@delete-video="deleteVideo"
|
|
1341
|
+
/>
|
|
1342
|
+
</div>
|
|
1343
|
+
</template>
|
|
1344
|
+
|
|
1345
|
+
<script>
|
|
1346
|
+
import { EditorContent } from '@tiptap/vue-2'
|
|
1347
|
+
import { VideoBubbleMenu, useEditor } from 'vue2-premium-bbl-editor'
|
|
1348
|
+
|
|
1349
|
+
export default {
|
|
1350
|
+
components: {
|
|
1351
|
+
EditorContent,
|
|
1352
|
+
VideoBubbleMenu
|
|
1353
|
+
},
|
|
1354
|
+
|
|
1355
|
+
setup() {
|
|
1356
|
+
const { editor } = useEditor({
|
|
1357
|
+
content: '<p>Add a video to see the video menu</p>'
|
|
1358
|
+
})
|
|
1359
|
+
|
|
1360
|
+
return { editor }
|
|
1361
|
+
},
|
|
1362
|
+
|
|
1363
|
+
methods: {
|
|
1364
|
+
alignVideoLeft() {
|
|
1365
|
+
console.log('Align video left')
|
|
1366
|
+
},
|
|
1367
|
+
|
|
1368
|
+
alignVideoCenter() {
|
|
1369
|
+
console.log('Align video center')
|
|
1370
|
+
},
|
|
1371
|
+
|
|
1372
|
+
alignVideoRight() {
|
|
1373
|
+
console.log('Align video right')
|
|
1374
|
+
},
|
|
1375
|
+
|
|
1376
|
+
resizeVideo(size) {
|
|
1377
|
+
console.log('Resize video to:', size)
|
|
1378
|
+
},
|
|
1379
|
+
|
|
1380
|
+
toggleAutoplay() {
|
|
1381
|
+
console.log('Toggle video autoplay')
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1384
|
+
toggleControls() {
|
|
1385
|
+
console.log('Toggle video controls')
|
|
1386
|
+
},
|
|
1387
|
+
|
|
1388
|
+
deleteVideo() {
|
|
1389
|
+
console.log('Delete video')
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
</script>
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
## Modal Components
|
|
1397
|
+
|
|
1398
|
+
### ImageModal
|
|
1399
|
+
|
|
1400
|
+
Modal for uploading and configuring images.
|
|
1401
|
+
|
|
1402
|
+
```vue
|
|
1403
|
+
<template>
|
|
1404
|
+
<div>
|
|
1405
|
+
<button @click="showImageModal = true">Insert Image</button>
|
|
1406
|
+
|
|
1407
|
+
<ImageModal
|
|
1408
|
+
:visible="showImageModal"
|
|
1409
|
+
:upload-handler="uploadHandler"
|
|
1410
|
+
:max-file-size="maxFileSize"
|
|
1411
|
+
:allowed-types="allowedImageTypes"
|
|
1412
|
+
:show-url-input="true"
|
|
1413
|
+
:show-alt-text-input="true"
|
|
1414
|
+
:show-alignment-options="true"
|
|
1415
|
+
:show-size-options="true"
|
|
1416
|
+
@close="showImageModal = false"
|
|
1417
|
+
@insert="insertImage"
|
|
1418
|
+
@upload-progress="handleUploadProgress"
|
|
1419
|
+
@upload-error="handleUploadError"
|
|
1420
|
+
/>
|
|
1421
|
+
</div>
|
|
1422
|
+
</template>
|
|
1423
|
+
|
|
1424
|
+
<script>
|
|
1425
|
+
import { ImageModal } from 'vue2-premium-bbl-editor'
|
|
1426
|
+
|
|
1427
|
+
export default {
|
|
1428
|
+
components: {
|
|
1429
|
+
ImageModal
|
|
1430
|
+
},
|
|
1431
|
+
|
|
1432
|
+
data() {
|
|
1433
|
+
return {
|
|
1434
|
+
showImageModal: false,
|
|
1435
|
+
maxFileSize: 5 * 1024 * 1024, // 5MB
|
|
1436
|
+
allowedImageTypes: [
|
|
1437
|
+
'image/jpeg',
|
|
1438
|
+
'image/png',
|
|
1439
|
+
'image/gif',
|
|
1440
|
+
'image/webp'
|
|
1441
|
+
]
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
|
|
1445
|
+
methods: {
|
|
1446
|
+
async uploadHandler(file) {
|
|
1447
|
+
try {
|
|
1448
|
+
const formData = new FormData()
|
|
1449
|
+
formData.append('image', file)
|
|
1450
|
+
|
|
1451
|
+
const response = await fetch('/api/upload/image', {
|
|
1452
|
+
method: 'POST',
|
|
1453
|
+
body: formData
|
|
1454
|
+
})
|
|
1455
|
+
|
|
1456
|
+
if (!response.ok) {
|
|
1457
|
+
throw new Error(`Upload failed: ${response.statusText}`)
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const data = await response.json()
|
|
1461
|
+
return {
|
|
1462
|
+
src: data.url,
|
|
1463
|
+
alt: file.name,
|
|
1464
|
+
width: data.width,
|
|
1465
|
+
height: data.height
|
|
1466
|
+
}
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
console.error('Image upload error:', error)
|
|
1469
|
+
throw error
|
|
1470
|
+
}
|
|
1471
|
+
},
|
|
1472
|
+
|
|
1473
|
+
insertImage(imageData) {
|
|
1474
|
+
console.log('Insert image:', imageData)
|
|
1475
|
+
// Insert image into editor
|
|
1476
|
+
// this.editor.chain().focus().setImage(imageData).run()
|
|
1477
|
+
},
|
|
1478
|
+
|
|
1479
|
+
handleUploadProgress(progress) {
|
|
1480
|
+
console.log('Upload progress:', progress)
|
|
1481
|
+
},
|
|
1482
|
+
|
|
1483
|
+
handleUploadError(error) {
|
|
1484
|
+
console.error('Upload error:', error)
|
|
1485
|
+
alert(`Upload failed: ${error.message}`)
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
</script>
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
### VideoModal
|
|
1493
|
+
|
|
1494
|
+
Modal for uploading and configuring videos.
|
|
1495
|
+
|
|
1496
|
+
```vue
|
|
1497
|
+
<template>
|
|
1498
|
+
<div>
|
|
1499
|
+
<button @click="showVideoModal = true">Insert Video</button>
|
|
1500
|
+
|
|
1501
|
+
<VideoModal
|
|
1502
|
+
:visible="showVideoModal"
|
|
1503
|
+
:upload-handler="uploadHandler"
|
|
1504
|
+
:max-file-size="maxFileSize"
|
|
1505
|
+
:allowed-types="allowedVideoTypes"
|
|
1506
|
+
:show-url-input="true"
|
|
1507
|
+
:show-embed-input="true"
|
|
1508
|
+
:show-playback-options="true"
|
|
1509
|
+
:show-size-options="true"
|
|
1510
|
+
@close="showVideoModal = false"
|
|
1511
|
+
@insert="insertVideo"
|
|
1512
|
+
@upload-progress="handleUploadProgress"
|
|
1513
|
+
@upload-error="handleUploadError"
|
|
1514
|
+
/>
|
|
1515
|
+
</div>
|
|
1516
|
+
</template>
|
|
1517
|
+
|
|
1518
|
+
<script>
|
|
1519
|
+
import { VideoModal } from 'vue2-premium-bbl-editor'
|
|
1520
|
+
|
|
1521
|
+
export default {
|
|
1522
|
+
components: {
|
|
1523
|
+
VideoModal
|
|
1524
|
+
},
|
|
1525
|
+
|
|
1526
|
+
data() {
|
|
1527
|
+
return {
|
|
1528
|
+
showVideoModal: false,
|
|
1529
|
+
maxFileSize: 50 * 1024 * 1024, // 50MB
|
|
1530
|
+
allowedVideoTypes: [
|
|
1531
|
+
'video/mp4',
|
|
1532
|
+
'video/webm',
|
|
1533
|
+
'video/ogg'
|
|
1534
|
+
]
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
|
|
1538
|
+
methods: {
|
|
1539
|
+
async uploadHandler(file) {
|
|
1540
|
+
try {
|
|
1541
|
+
const formData = new FormData()
|
|
1542
|
+
formData.append('video', file)
|
|
1543
|
+
|
|
1544
|
+
const response = await fetch('/api/upload/video', {
|
|
1545
|
+
method: 'POST',
|
|
1546
|
+
body: formData
|
|
1547
|
+
})
|
|
1548
|
+
|
|
1549
|
+
if (!response.ok) {
|
|
1550
|
+
throw new Error(`Upload failed: ${response.statusText}`)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const data = await response.json()
|
|
1554
|
+
return {
|
|
1555
|
+
src: data.url,
|
|
1556
|
+
poster: data.poster,
|
|
1557
|
+
width: data.width,
|
|
1558
|
+
height: data.height
|
|
1559
|
+
}
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
console.error('Video upload error:', error)
|
|
1562
|
+
throw error
|
|
1563
|
+
}
|
|
1564
|
+
},
|
|
1565
|
+
|
|
1566
|
+
insertVideo(videoData) {
|
|
1567
|
+
console.log('Insert video:', videoData)
|
|
1568
|
+
// Insert video into editor
|
|
1569
|
+
// this.editor.chain().focus().setVideo(videoData).run()
|
|
1570
|
+
},
|
|
1571
|
+
|
|
1572
|
+
handleUploadProgress(progress) {
|
|
1573
|
+
console.log('Upload progress:', progress)
|
|
1574
|
+
},
|
|
1575
|
+
|
|
1576
|
+
handleUploadError(error) {
|
|
1577
|
+
console.error('Upload error:', error)
|
|
1578
|
+
alert(`Upload failed: ${error.message}`)
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
</script>
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
### LinkModal
|
|
1586
|
+
|
|
1587
|
+
Modal for creating and editing links.
|
|
1588
|
+
|
|
1589
|
+
```vue
|
|
1590
|
+
<template>
|
|
1591
|
+
<div>
|
|
1592
|
+
<button @click="showLinkModal = true">Insert Link</button>
|
|
1593
|
+
|
|
1594
|
+
<LinkModal
|
|
1595
|
+
:visible="showLinkModal"
|
|
1596
|
+
:initial-text="linkData.text"
|
|
1597
|
+
:initial-url="linkData.url"
|
|
1598
|
+
:initial-target="linkData.target"
|
|
1599
|
+
:show-text-input="true"
|
|
1600
|
+
:show-target-options="true"
|
|
1601
|
+
:show-title-input="true"
|
|
1602
|
+
:validate-url="true"
|
|
1603
|
+
@close="showLinkModal = false"
|
|
1604
|
+
@insert="insertLink"
|
|
1605
|
+
@update="updateLink"
|
|
1606
|
+
@remove="removeLink"
|
|
1607
|
+
/>
|
|
1608
|
+
</div>
|
|
1609
|
+
</template>
|
|
1610
|
+
|
|
1611
|
+
<script>
|
|
1612
|
+
import { LinkModal } from 'vue2-premium-bbl-editor'
|
|
1613
|
+
|
|
1614
|
+
export default {
|
|
1615
|
+
components: {
|
|
1616
|
+
LinkModal
|
|
1617
|
+
},
|
|
1618
|
+
|
|
1619
|
+
data() {
|
|
1620
|
+
return {
|
|
1621
|
+
showLinkModal: false,
|
|
1622
|
+
linkData: {
|
|
1623
|
+
text: '',
|
|
1624
|
+
url: '',
|
|
1625
|
+
target: '_blank'
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
|
|
1630
|
+
methods: {
|
|
1631
|
+
insertLink(linkData) {
|
|
1632
|
+
console.log('Insert link:', linkData)
|
|
1633
|
+
// Insert link into editor
|
|
1634
|
+
// this.editor.chain().focus().setLink(linkData).run()
|
|
1635
|
+
},
|
|
1636
|
+
|
|
1637
|
+
updateLink(linkData) {
|
|
1638
|
+
console.log('Update link:', linkData)
|
|
1639
|
+
// Update existing link
|
|
1640
|
+
},
|
|
1641
|
+
|
|
1642
|
+
removeLink() {
|
|
1643
|
+
console.log('Remove link')
|
|
1644
|
+
// Remove link from editor
|
|
1645
|
+
// this.editor.chain().focus().unsetLink().run()
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
</script>
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
## Upload System Components
|
|
1653
|
+
|
|
1654
|
+
### UploadManager Integration
|
|
1655
|
+
|
|
1656
|
+
```vue
|
|
1657
|
+
<template>
|
|
1658
|
+
<div class="upload-system-demo">
|
|
1659
|
+
<PremiumBblEditor
|
|
1660
|
+
v-model="content"
|
|
1661
|
+
@ready="onEditorReady"
|
|
1662
|
+
/>
|
|
1663
|
+
|
|
1664
|
+
<div class="upload-status" v-if="uploadStatus">
|
|
1665
|
+
<div class="progress-bar">
|
|
1666
|
+
<div
|
|
1667
|
+
class="progress-fill"
|
|
1668
|
+
:style="{ width: uploadProgress + '%' }"
|
|
1669
|
+
></div>
|
|
1670
|
+
</div>
|
|
1671
|
+
<p>{{ uploadStatus }} - {{ uploadProgress }}%</p>
|
|
1672
|
+
</div>
|
|
1673
|
+
</div>
|
|
1674
|
+
</template>
|
|
1675
|
+
|
|
1676
|
+
<script>
|
|
1677
|
+
import {
|
|
1678
|
+
PremiumBblEditor,
|
|
1679
|
+
UploadManager,
|
|
1680
|
+
LocalStorageAdapter,
|
|
1681
|
+
integrateTipTapUpload,
|
|
1682
|
+
createUploadConfig
|
|
1683
|
+
} from 'vue2-premium-bbl-editor'
|
|
1684
|
+
|
|
1685
|
+
export default {
|
|
1686
|
+
components: {
|
|
1687
|
+
PremiumBblEditor
|
|
1688
|
+
},
|
|
1689
|
+
|
|
1690
|
+
data() {
|
|
1691
|
+
return {
|
|
1692
|
+
content: '<p>Upload system integration example</p>',
|
|
1693
|
+
uploadManager: null,
|
|
1694
|
+
uploadStatus: null,
|
|
1695
|
+
uploadProgress: 0
|
|
1696
|
+
}
|
|
1697
|
+
},
|
|
1698
|
+
|
|
1699
|
+
async created() {
|
|
1700
|
+
await this.initializeUploadSystem()
|
|
1701
|
+
},
|
|
1702
|
+
|
|
1703
|
+
methods: {
|
|
1704
|
+
async initializeUploadSystem() {
|
|
1705
|
+
try {
|
|
1706
|
+
// Create upload configuration
|
|
1707
|
+
const config = createUploadConfig('localStorage', {
|
|
1708
|
+
validation: {
|
|
1709
|
+
maxSize: 10 * 1024 * 1024, // 10MB
|
|
1710
|
+
allowedTypes: [
|
|
1711
|
+
'image/jpeg', 'image/png', 'image/gif',
|
|
1712
|
+
'video/mp4', 'video/webm'
|
|
1713
|
+
]
|
|
1714
|
+
},
|
|
1715
|
+
retry: {
|
|
1716
|
+
maxAttempts: 3,
|
|
1717
|
+
baseDelay: 1000
|
|
1718
|
+
}
|
|
1719
|
+
})
|
|
1720
|
+
|
|
1721
|
+
// Initialize upload manager
|
|
1722
|
+
this.uploadManager = new UploadManager(config)
|
|
1723
|
+
|
|
1724
|
+
// Register adapters
|
|
1725
|
+
const localAdapter = new LocalStorageAdapter({
|
|
1726
|
+
generateObjectUrl: true
|
|
1727
|
+
})
|
|
1728
|
+
this.uploadManager.registerAdapter(localAdapter)
|
|
1729
|
+
this.uploadManager.setDefaultAdapter('local-storage')
|
|
1730
|
+
|
|
1731
|
+
// Set up event listeners
|
|
1732
|
+
this.setupUploadEvents()
|
|
1733
|
+
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
console.error('Upload system initialization failed:', error)
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
|
|
1739
|
+
setupUploadEvents() {
|
|
1740
|
+
// Progress tracking
|
|
1741
|
+
this.uploadManager.on('progress', (progress) => {
|
|
1742
|
+
this.uploadProgress = Math.round(progress.percentage)
|
|
1743
|
+
this.uploadStatus = 'Uploading...'
|
|
1744
|
+
})
|
|
1745
|
+
|
|
1746
|
+
// Upload completed
|
|
1747
|
+
this.uploadManager.on('uploadCompleted', (event) => {
|
|
1748
|
+
this.uploadStatus = 'Upload completed!'
|
|
1749
|
+
this.uploadProgress = 100
|
|
1750
|
+
|
|
1751
|
+
setTimeout(() => {
|
|
1752
|
+
this.uploadStatus = null
|
|
1753
|
+
this.uploadProgress = 0
|
|
1754
|
+
}, 2000)
|
|
1755
|
+
})
|
|
1756
|
+
|
|
1757
|
+
// Upload failed
|
|
1758
|
+
this.uploadManager.on('uploadFailed', (event) => {
|
|
1759
|
+
this.uploadStatus = `Upload failed: ${event.error.message}`
|
|
1760
|
+
this.uploadProgress = 0
|
|
1761
|
+
|
|
1762
|
+
setTimeout(() => {
|
|
1763
|
+
this.uploadStatus = null
|
|
1764
|
+
}, 3000)
|
|
1765
|
+
})
|
|
1766
|
+
},
|
|
1767
|
+
|
|
1768
|
+
onEditorReady(editor) {
|
|
1769
|
+
// Integrate upload system with editor
|
|
1770
|
+
integrateTipTapUpload(editor, this.uploadManager, {
|
|
1771
|
+
onStart: () => {
|
|
1772
|
+
this.uploadStatus = 'Starting upload...'
|
|
1773
|
+
this.uploadProgress = 0
|
|
1774
|
+
},
|
|
1775
|
+
onEnd: () => {
|
|
1776
|
+
// Upload process ended (success or failure)
|
|
1777
|
+
}
|
|
1778
|
+
})
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
|
|
1782
|
+
beforeDestroy() {
|
|
1783
|
+
if (this.uploadManager) {
|
|
1784
|
+
this.uploadManager.destroy()
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
</script>
|
|
1789
|
+
|
|
1790
|
+
<style scoped>
|
|
1791
|
+
.upload-status {
|
|
1792
|
+
margin-top: 20px;
|
|
1793
|
+
padding: 15px;
|
|
1794
|
+
background: #f8f9fa;
|
|
1795
|
+
border-radius: 6px;
|
|
1796
|
+
border: 1px solid #e9ecef;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
.progress-bar {
|
|
1800
|
+
width: 100%;
|
|
1801
|
+
height: 8px;
|
|
1802
|
+
background: #e9ecef;
|
|
1803
|
+
border-radius: 4px;
|
|
1804
|
+
overflow: hidden;
|
|
1805
|
+
margin-bottom: 10px;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
.progress-fill {
|
|
1809
|
+
height: 100%;
|
|
1810
|
+
background: #007bff;
|
|
1811
|
+
transition: width 0.3s ease;
|
|
1812
|
+
}
|
|
1813
|
+
</style>
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
## Development Tools
|
|
1817
|
+
|
|
1818
|
+
### DiagnosticTool
|
|
1819
|
+
|
|
1820
|
+
Built-in diagnostic tool for troubleshooting.
|
|
1821
|
+
|
|
1822
|
+
```vue
|
|
1823
|
+
<template>
|
|
1824
|
+
<div class="development-tools">
|
|
1825
|
+
<h3>Development Tools</h3>
|
|
1826
|
+
|
|
1827
|
+
<!-- Main Editor -->
|
|
1828
|
+
<PremiumBblEditor
|
|
1829
|
+
v-model="content"
|
|
1830
|
+
@ready="handleReady"
|
|
1831
|
+
@error="handleError"
|
|
1832
|
+
/>
|
|
1833
|
+
|
|
1834
|
+
<!-- Diagnostic Tool -->
|
|
1835
|
+
<div class="diagnostic-section">
|
|
1836
|
+
<h4>Diagnostic Information</h4>
|
|
1837
|
+
<DiagnosticTool
|
|
1838
|
+
:show-dependency-check="true"
|
|
1839
|
+
:show-browser-info="true"
|
|
1840
|
+
:show-editor-state="true"
|
|
1841
|
+
:show-performance-metrics="true"
|
|
1842
|
+
:editor="editor"
|
|
1843
|
+
@diagnostic-complete="handleDiagnostic"
|
|
1844
|
+
/>
|
|
1845
|
+
</div>
|
|
1846
|
+
</div>
|
|
1847
|
+
</template>
|
|
1848
|
+
|
|
1849
|
+
<script>
|
|
1850
|
+
import { PremiumBblEditor, DiagnosticTool } from 'vue2-premium-bbl-editor'
|
|
1851
|
+
|
|
1852
|
+
export default {
|
|
1853
|
+
components: {
|
|
1854
|
+
PremiumBblEditor,
|
|
1855
|
+
DiagnosticTool
|
|
1856
|
+
},
|
|
1857
|
+
|
|
1858
|
+
data() {
|
|
1859
|
+
return {
|
|
1860
|
+
content: '<p>Editor with diagnostic tools</p>',
|
|
1861
|
+
editor: null
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
|
|
1865
|
+
methods: {
|
|
1866
|
+
handleReady(editor) {
|
|
1867
|
+
this.editor = editor
|
|
1868
|
+
console.log('Editor ready for diagnostics')
|
|
1869
|
+
},
|
|
1870
|
+
|
|
1871
|
+
handleError(error) {
|
|
1872
|
+
console.error('Editor error detected:', error)
|
|
1873
|
+
},
|
|
1874
|
+
|
|
1875
|
+
handleDiagnostic(diagnosticData) {
|
|
1876
|
+
console.log('Diagnostic complete:', diagnosticData)
|
|
1877
|
+
|
|
1878
|
+
// Check for issues
|
|
1879
|
+
if (diagnosticData.issues.length > 0) {
|
|
1880
|
+
console.warn('Issues found:', diagnosticData.issues)
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// Performance metrics
|
|
1884
|
+
if (diagnosticData.performance) {
|
|
1885
|
+
console.log('Performance metrics:', diagnosticData.performance)
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
</script>
|
|
1891
|
+
|
|
1892
|
+
<style scoped>
|
|
1893
|
+
.development-tools {
|
|
1894
|
+
max-width: 1000px;
|
|
1895
|
+
margin: 0 auto;
|
|
1896
|
+
padding: 20px;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
.diagnostic-section {
|
|
1900
|
+
margin-top: 30px;
|
|
1901
|
+
padding: 20px;
|
|
1902
|
+
background: #f8f9fa;
|
|
1903
|
+
border-radius: 8px;
|
|
1904
|
+
border: 1px solid #e9ecef;
|
|
1905
|
+
}
|
|
1906
|
+
</style>
|
|
1907
|
+
```
|
|
1908
|
+
|
|
1909
|
+
### DebugHelper
|
|
1910
|
+
|
|
1911
|
+
Development debugging helper component.
|
|
1912
|
+
|
|
1913
|
+
```vue
|
|
1914
|
+
<template>
|
|
1915
|
+
<div class="debug-environment" v-if="isDevelopment">
|
|
1916
|
+
<h3>Debug Environment</h3>
|
|
1917
|
+
|
|
1918
|
+
<!-- Main Editor -->
|
|
1919
|
+
<PremiumBblEditor
|
|
1920
|
+
v-model="content"
|
|
1921
|
+
@ready="handleReady"
|
|
1922
|
+
@input="handleInput"
|
|
1923
|
+
@focus="handleFocus"
|
|
1924
|
+
@blur="handleBlur"
|
|
1925
|
+
/>
|
|
1926
|
+
|
|
1927
|
+
<!-- Debug Helper -->
|
|
1928
|
+
<div class="debug-section">
|
|
1929
|
+
<h4>Debug Information</h4>
|
|
1930
|
+
<DebugHelper
|
|
1931
|
+
:editor="editor"
|
|
1932
|
+
:show-editor-state="true"
|
|
1933
|
+
:show-content-analysis="true"
|
|
1934
|
+
:show-event-log="true"
|
|
1935
|
+
:show-performance-monitor="true"
|
|
1936
|
+
:auto-refresh="true"
|
|
1937
|
+
:refresh-interval="1000"
|
|
1938
|
+
@state-change="handleStateChange"
|
|
1939
|
+
@performance-update="handlePerformanceUpdate"
|
|
1940
|
+
/>
|
|
1941
|
+
</div>
|
|
1942
|
+
</div>
|
|
1943
|
+
</template>
|
|
1944
|
+
|
|
1945
|
+
<script>
|
|
1946
|
+
import { PremiumBblEditor, DebugHelper } from 'vue2-premium-bbl-editor'
|
|
1947
|
+
|
|
1948
|
+
export default {
|
|
1949
|
+
components: {
|
|
1950
|
+
PremiumBblEditor,
|
|
1951
|
+
DebugHelper
|
|
1952
|
+
},
|
|
1953
|
+
|
|
1954
|
+
data() {
|
|
1955
|
+
return {
|
|
1956
|
+
content: '<p>Editor with debug helper</p>',
|
|
1957
|
+
editor: null
|
|
1958
|
+
}
|
|
1959
|
+
},
|
|
1960
|
+
|
|
1961
|
+
computed: {
|
|
1962
|
+
isDevelopment() {
|
|
1963
|
+
return process.env.NODE_ENV === 'development'
|
|
1964
|
+
}
|
|
1965
|
+
},
|
|
1966
|
+
|
|
1967
|
+
methods: {
|
|
1968
|
+
handleReady(editor) {
|
|
1969
|
+
this.editor = editor
|
|
1970
|
+
console.log('Editor ready for debugging')
|
|
1971
|
+
},
|
|
1972
|
+
|
|
1973
|
+
handleInput(content) {
|
|
1974
|
+
console.log('Content changed:', content.length, 'characters')
|
|
1975
|
+
},
|
|
1976
|
+
|
|
1977
|
+
handleFocus() {
|
|
1978
|
+
console.log('Editor focused')
|
|
1979
|
+
},
|
|
1980
|
+
|
|
1981
|
+
handleBlur() {
|
|
1982
|
+
console.log('Editor blurred')
|
|
1983
|
+
},
|
|
1984
|
+
|
|
1985
|
+
handleStateChange(state) {
|
|
1986
|
+
console.log('Editor state changed:', state)
|
|
1987
|
+
},
|
|
1988
|
+
|
|
1989
|
+
handlePerformanceUpdate(metrics) {
|
|
1990
|
+
console.log('Performance metrics:', metrics)
|
|
1991
|
+
|
|
1992
|
+
// Alert if performance is poor
|
|
1993
|
+
if (metrics.renderTime > 100) {
|
|
1994
|
+
console.warn('Slow render time detected:', metrics.renderTime, 'ms')
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
</script>
|
|
2000
|
+
|
|
2001
|
+
<style scoped>
|
|
2002
|
+
.debug-environment {
|
|
2003
|
+
max-width: 1200px;
|
|
2004
|
+
margin: 0 auto;
|
|
2005
|
+
padding: 20px;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
.debug-section {
|
|
2009
|
+
margin-top: 30px;
|
|
2010
|
+
padding: 20px;
|
|
2011
|
+
background: #1e1e1e;
|
|
2012
|
+
color: #d4d4d4;
|
|
2013
|
+
border-radius: 8px;
|
|
2014
|
+
font-family: 'Courier New', monospace;
|
|
2015
|
+
}
|
|
2016
|
+
</style>
|
|
2017
|
+
```
|
|
2018
|
+
|
|
2019
|
+
## Composables
|
|
2020
|
+
|
|
2021
|
+
### useEditor
|
|
2022
|
+
|
|
2023
|
+
Core editor composable for custom implementations.
|
|
2024
|
+
|
|
2025
|
+
```vue
|
|
2026
|
+
<template>
|
|
2027
|
+
<div class="custom-editor-implementation">
|
|
2028
|
+
<div class="editor-toolbar" v-if="editor">
|
|
2029
|
+
<button
|
|
2030
|
+
@click="executeCommand('toggleBold')"
|
|
2031
|
+
:class="{ active: isActive('bold') }"
|
|
2032
|
+
>
|
|
2033
|
+
Bold
|
|
2034
|
+
</button>
|
|
2035
|
+
<button
|
|
2036
|
+
@click="executeCommand('toggleItalic')"
|
|
2037
|
+
:class="{ active: isActive('italic') }"
|
|
2038
|
+
>
|
|
2039
|
+
Italic
|
|
2040
|
+
</button>
|
|
2041
|
+
<button @click="focus">Focus</button>
|
|
2042
|
+
<button @click="getContentExample">Get Content</button>
|
|
2043
|
+
</div>
|
|
2044
|
+
|
|
2045
|
+
<div class="editor-content">
|
|
2046
|
+
<editor-content :editor="editor" />
|
|
2047
|
+
</div>
|
|
2048
|
+
|
|
2049
|
+
<div class="editor-status">
|
|
2050
|
+
<p>Ready: {{ isEditorReady }}</p>
|
|
2051
|
+
<p>Content Length: {{ currentContent.length }}</p>
|
|
2052
|
+
<p v-if="editorError" class="error">Error: {{ editorError }}</p>
|
|
2053
|
+
</div>
|
|
2054
|
+
</div>
|
|
2055
|
+
</template>
|
|
2056
|
+
|
|
2057
|
+
<script>
|
|
2058
|
+
import { EditorContent } from '@tiptap/vue-2'
|
|
2059
|
+
import { useEditor } from 'vue2-premium-bbl-editor'
|
|
2060
|
+
|
|
2061
|
+
export default {
|
|
2062
|
+
components: {
|
|
2063
|
+
EditorContent
|
|
2064
|
+
},
|
|
2065
|
+
|
|
2066
|
+
setup(props, { emit }) {
|
|
2067
|
+
// Use the editor composable
|
|
2068
|
+
const {
|
|
2069
|
+
editor,
|
|
2070
|
+
isEditorReady,
|
|
2071
|
+
currentContent,
|
|
2072
|
+
editorError,
|
|
2073
|
+
missingDependencies,
|
|
2074
|
+
executeCommand,
|
|
2075
|
+
isActive,
|
|
2076
|
+
getContent,
|
|
2077
|
+
setContent,
|
|
2078
|
+
focus,
|
|
2079
|
+
blur
|
|
2080
|
+
} = useEditor({
|
|
2081
|
+
value: '<p>Custom editor using useEditor composable</p>',
|
|
2082
|
+
placeholder: 'Start typing...',
|
|
2083
|
+
editable: true,
|
|
2084
|
+
autofocus: false,
|
|
2085
|
+
toolbarConfig: {
|
|
2086
|
+
bold: true,
|
|
2087
|
+
italic: true,
|
|
2088
|
+
underline: true
|
|
2089
|
+
},
|
|
2090
|
+
extensionConfig: {
|
|
2091
|
+
image: {
|
|
2092
|
+
allowResize: true
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}, emit)
|
|
2096
|
+
|
|
2097
|
+
return {
|
|
2098
|
+
editor,
|
|
2099
|
+
isEditorReady,
|
|
2100
|
+
currentContent,
|
|
2101
|
+
editorError,
|
|
2102
|
+
missingDependencies,
|
|
2103
|
+
executeCommand,
|
|
2104
|
+
isActive,
|
|
2105
|
+
getContent,
|
|
2106
|
+
setContent,
|
|
2107
|
+
focus,
|
|
2108
|
+
blur
|
|
2109
|
+
}
|
|
2110
|
+
},
|
|
2111
|
+
|
|
2112
|
+
methods: {
|
|
2113
|
+
getContentExample() {
|
|
2114
|
+
const htmlContent = this.getContent('html')
|
|
2115
|
+
const jsonContent = this.getContent('json')
|
|
2116
|
+
|
|
2117
|
+
console.log('HTML Content:', htmlContent)
|
|
2118
|
+
console.log('JSON Content:', jsonContent)
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
</script>
|
|
2123
|
+
|
|
2124
|
+
<style scoped>
|
|
2125
|
+
.custom-editor-implementation {
|
|
2126
|
+
max-width: 800px;
|
|
2127
|
+
margin: 0 auto;
|
|
2128
|
+
padding: 20px;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
.editor-toolbar {
|
|
2132
|
+
display: flex;
|
|
2133
|
+
gap: 8px;
|
|
2134
|
+
margin-bottom: 16px;
|
|
2135
|
+
padding: 12px;
|
|
2136
|
+
background: #f8f9fa;
|
|
2137
|
+
border-radius: 6px;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
.editor-toolbar button {
|
|
2141
|
+
padding: 6px 12px;
|
|
2142
|
+
border: 1px solid #dee2e6;
|
|
2143
|
+
background: white;
|
|
2144
|
+
border-radius: 4px;
|
|
2145
|
+
cursor: pointer;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
.editor-toolbar button:hover {
|
|
2149
|
+
background: #e9ecef;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
.editor-toolbar button.active {
|
|
2153
|
+
background: #007bff;
|
|
2154
|
+
color: white;
|
|
2155
|
+
border-color: #007bff;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
.editor-content {
|
|
2159
|
+
border: 1px solid #dee2e6;
|
|
2160
|
+
border-radius: 6px;
|
|
2161
|
+
min-height: 200px;
|
|
2162
|
+
padding: 16px;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
.editor-status {
|
|
2166
|
+
margin-top: 16px;
|
|
2167
|
+
padding: 12px;
|
|
2168
|
+
background: #f8f9fa;
|
|
2169
|
+
border-radius: 6px;
|
|
2170
|
+
font-size: 14px;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
.error {
|
|
2174
|
+
color: #dc3545;
|
|
2175
|
+
font-weight: bold;
|
|
2176
|
+
}
|
|
2177
|
+
</style>
|
|
2178
|
+
```
|
|
2179
|
+
|
|
2180
|
+
## Advanced Usage Patterns
|
|
2181
|
+
|
|
2182
|
+
### Multi-Editor Setup
|
|
2183
|
+
|
|
2184
|
+
```vue
|
|
2185
|
+
<template>
|
|
2186
|
+
<div class="multi-editor-setup">
|
|
2187
|
+
<h2>Multi-Editor Setup</h2>
|
|
2188
|
+
|
|
2189
|
+
<div class="editor-tabs">
|
|
2190
|
+
<button
|
|
2191
|
+
v-for="(tab, index) in tabs"
|
|
2192
|
+
:key="index"
|
|
2193
|
+
@click="activeTab = index"
|
|
2194
|
+
:class="{ active: activeTab === index }"
|
|
2195
|
+
>
|
|
2196
|
+
{{ tab.title }}
|
|
2197
|
+
</button>
|
|
2198
|
+
</div>
|
|
2199
|
+
|
|
2200
|
+
<div class="editor-content">
|
|
2201
|
+
<PremiumBblEditor
|
|
2202
|
+
v-for="(tab, index) in tabs"
|
|
2203
|
+
:key="index"
|
|
2204
|
+
v-show="activeTab === index"
|
|
2205
|
+
v-model="tab.content"
|
|
2206
|
+
:placeholder="tab.placeholder"
|
|
2207
|
+
:toolbar-config="tab.toolbarConfig"
|
|
2208
|
+
@ready="handleEditorReady(index, $event)"
|
|
2209
|
+
/>
|
|
2210
|
+
</div>
|
|
2211
|
+
</div>
|
|
2212
|
+
</template>
|
|
2213
|
+
|
|
2214
|
+
<script>
|
|
2215
|
+
import { PremiumBblEditor } from 'vue2-premium-bbl-editor'
|
|
2216
|
+
|
|
2217
|
+
export default {
|
|
2218
|
+
components: {
|
|
2219
|
+
PremiumBblEditor
|
|
2220
|
+
},
|
|
2221
|
+
|
|
2222
|
+
data() {
|
|
2223
|
+
return {
|
|
2224
|
+
activeTab: 0,
|
|
2225
|
+
tabs: [
|
|
2226
|
+
{
|
|
2227
|
+
title: 'Article Content',
|
|
2228
|
+
content: '<h2>Article Title</h2><p>Article content...</p>',
|
|
2229
|
+
placeholder: 'Write your article...',
|
|
2230
|
+
toolbarConfig: {
|
|
2231
|
+
bold: true,
|
|
2232
|
+
italic: true,
|
|
2233
|
+
underline: true,
|
|
2234
|
+
headings: true,
|
|
2235
|
+
lists: true,
|
|
2236
|
+
link: true,
|
|
2237
|
+
image: true
|
|
2238
|
+
}
|
|
2239
|
+
},
|
|
2240
|
+
{
|
|
2241
|
+
title: 'Summary',
|
|
2242
|
+
content: '<p>Article summary...</p>',
|
|
2243
|
+
placeholder: 'Write a summary...',
|
|
2244
|
+
toolbarConfig: {
|
|
2245
|
+
bold: true,
|
|
2246
|
+
italic: true,
|
|
2247
|
+
lists: true
|
|
2248
|
+
}
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
title: 'Notes',
|
|
2252
|
+
content: '<p>Internal notes...</p>',
|
|
2253
|
+
placeholder: 'Add notes...',
|
|
2254
|
+
toolbarConfig: {
|
|
2255
|
+
bold: true,
|
|
2256
|
+
italic: true,
|
|
2257
|
+
code: true,
|
|
2258
|
+
lists: true
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
],
|
|
2262
|
+
editors: []
|
|
2263
|
+
}
|
|
2264
|
+
},
|
|
2265
|
+
|
|
2266
|
+
methods: {
|
|
2267
|
+
handleEditorReady(index, editor) {
|
|
2268
|
+
this.editors[index] = editor
|
|
2269
|
+
console.log(`Editor ${index} ready:`, editor)
|
|
2270
|
+
},
|
|
2271
|
+
|
|
2272
|
+
getAllContent() {
|
|
2273
|
+
return this.tabs.map((tab, index) => ({
|
|
2274
|
+
title: tab.title,
|
|
2275
|
+
content: tab.content,
|
|
2276
|
+
editor: this.editors[index]
|
|
2277
|
+
}))
|
|
2278
|
+
},
|
|
2279
|
+
|
|
2280
|
+
saveAllContent() {
|
|
2281
|
+
const allContent = this.getAllContent()
|
|
2282
|
+
console.log('Saving all content:', allContent)
|
|
2283
|
+
// Save to backend
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
</script>
|
|
2288
|
+
|
|
2289
|
+
<style scoped>
|
|
2290
|
+
.multi-editor-setup {
|
|
2291
|
+
max-width: 1000px;
|
|
2292
|
+
margin: 0 auto;
|
|
2293
|
+
padding: 20px;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
.editor-tabs {
|
|
2297
|
+
display: flex;
|
|
2298
|
+
gap: 4px;
|
|
2299
|
+
margin-bottom: 20px;
|
|
2300
|
+
border-bottom: 1px solid #dee2e6;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
.editor-tabs button {
|
|
2304
|
+
padding: 12px 20px;
|
|
2305
|
+
border: none;
|
|
2306
|
+
background: transparent;
|
|
2307
|
+
cursor: pointer;
|
|
2308
|
+
border-bottom: 2px solid transparent;
|
|
2309
|
+
font-weight: 500;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
.editor-tabs button:hover {
|
|
2313
|
+
background: #f8f9fa;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
.editor-tabs button.active {
|
|
2317
|
+
border-bottom-color: #007bff;
|
|
2318
|
+
color: #007bff;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
.editor-content {
|
|
2322
|
+
min-height: 400px;
|
|
2323
|
+
}
|
|
2324
|
+
</style>
|
|
2325
|
+
```
|
|
2326
|
+
|
|
2327
|
+
### Form Integration
|
|
2328
|
+
|
|
2329
|
+
```vue
|
|
2330
|
+
<template>
|
|
2331
|
+
<form @submit.prevent="submitForm" class="editor-form">
|
|
2332
|
+
<h2>Article Form with Editor</h2>
|
|
2333
|
+
|
|
2334
|
+
<div class="bbl-form-group">
|
|
2335
|
+
<label for="title">Title:</label>
|
|
2336
|
+
<input
|
|
2337
|
+
id="title"
|
|
2338
|
+
v-model="form.title"
|
|
2339
|
+
type="text"
|
|
2340
|
+
required
|
|
2341
|
+
class="form-control"
|
|
2342
|
+
/>
|
|
2343
|
+
</div>
|
|
2344
|
+
|
|
2345
|
+
<div class="bbl-form-group">
|
|
2346
|
+
<label for="category">Category:</label>
|
|
2347
|
+
<select id="category" v-model="form.category" class="form-control">
|
|
2348
|
+
<option value="news">News</option>
|
|
2349
|
+
<option value="tutorial">Tutorial</option>
|
|
2350
|
+
<option value="review">Review</option>
|
|
2351
|
+
</select>
|
|
2352
|
+
</div>
|
|
2353
|
+
|
|
2354
|
+
<div class="bbl-form-group">
|
|
2355
|
+
<label>Content:</label>
|
|
2356
|
+
<PremiumBblEditor
|
|
2357
|
+
v-model="form.content"
|
|
2358
|
+
placeholder="Write your article content..."
|
|
2359
|
+
:auto-save="true"
|
|
2360
|
+
:auto-save-interval="30000"
|
|
2361
|
+
:max-content-length="10000"
|
|
2362
|
+
@auto-save="handleAutoSave"
|
|
2363
|
+
@content-limit-exceeded="handleLimitExceeded"
|
|
2364
|
+
@ready="handleEditorReady"
|
|
2365
|
+
/>
|
|
2366
|
+
</div>
|
|
2367
|
+
|
|
2368
|
+
<div class="bbl-form-group">
|
|
2369
|
+
<label for="tags">Tags:</label>
|
|
2370
|
+
<input
|
|
2371
|
+
id="tags"
|
|
2372
|
+
v-model="form.tags"
|
|
2373
|
+
type="text"
|
|
2374
|
+
placeholder="Comma-separated tags"
|
|
2375
|
+
class="form-control"
|
|
2376
|
+
/>
|
|
2377
|
+
</div>
|
|
2378
|
+
|
|
2379
|
+
<div class="form-actions">
|
|
2380
|
+
<button type="button" @click="saveDraft" class="btn btn-secondary">
|
|
2381
|
+
Save Draft
|
|
2382
|
+
</button>
|
|
2383
|
+
<button type="submit" class="btn btn-primary">
|
|
2384
|
+
Publish Article
|
|
2385
|
+
</button>
|
|
2386
|
+
</div>
|
|
2387
|
+
|
|
2388
|
+
<div class="form-status" v-if="status">
|
|
2389
|
+
<p :class="statusClass">{{ status }}</p>
|
|
2390
|
+
</div>
|
|
2391
|
+
</form>
|
|
2392
|
+
</template>
|
|
2393
|
+
|
|
2394
|
+
<script>
|
|
2395
|
+
import { PremiumBblEditor } from 'vue2-premium-bbl-editor'
|
|
2396
|
+
|
|
2397
|
+
export default {
|
|
2398
|
+
components: {
|
|
2399
|
+
PremiumBblEditor
|
|
2400
|
+
},
|
|
2401
|
+
|
|
2402
|
+
data() {
|
|
2403
|
+
return {
|
|
2404
|
+
form: {
|
|
2405
|
+
title: '',
|
|
2406
|
+
category: 'news',
|
|
2407
|
+
content: '<p>Start writing your article...</p>',
|
|
2408
|
+
tags: ''
|
|
2409
|
+
},
|
|
2410
|
+
editor: null,
|
|
2411
|
+
status: '',
|
|
2412
|
+
statusClass: ''
|
|
2413
|
+
}
|
|
2414
|
+
},
|
|
2415
|
+
|
|
2416
|
+
methods: {
|
|
2417
|
+
handleEditorReady(editor) {
|
|
2418
|
+
this.editor = editor
|
|
2419
|
+
console.log('Form editor ready')
|
|
2420
|
+
},
|
|
2421
|
+
|
|
2422
|
+
handleAutoSave(content) {
|
|
2423
|
+
this.form.content = content
|
|
2424
|
+
this.saveDraft(true) // Auto-save as draft
|
|
2425
|
+
},
|
|
2426
|
+
|
|
2427
|
+
handleLimitExceeded({ current, max }) {
|
|
2428
|
+
this.showStatus(`Content too long: ${current}/${max} characters`, 'error')
|
|
2429
|
+
},
|
|
2430
|
+
|
|
2431
|
+
async saveDraft(isAutoSave = false) {
|
|
2432
|
+
try {
|
|
2433
|
+
const response = await fetch('/api/articles/draft', {
|
|
2434
|
+
method: 'POST',
|
|
2435
|
+
headers: {
|
|
2436
|
+
'Content-Type': 'application/json'
|
|
2437
|
+
},
|
|
2438
|
+
body: JSON.stringify({
|
|
2439
|
+
...this.form,
|
|
2440
|
+
isDraft: true
|
|
2441
|
+
})
|
|
2442
|
+
})
|
|
2443
|
+
|
|
2444
|
+
if (response.ok) {
|
|
2445
|
+
const message = isAutoSave ? 'Auto-saved' : 'Draft saved'
|
|
2446
|
+
this.showStatus(message, 'success')
|
|
2447
|
+
} else {
|
|
2448
|
+
throw new Error('Save failed')
|
|
2449
|
+
}
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
this.showStatus('Save failed', 'error')
|
|
2452
|
+
}
|
|
2453
|
+
},
|
|
2454
|
+
|
|
2455
|
+
async submitForm() {
|
|
2456
|
+
try {
|
|
2457
|
+
// Validate form
|
|
2458
|
+
if (!this.form.title.trim()) {
|
|
2459
|
+
this.showStatus('Title is required', 'error')
|
|
2460
|
+
return
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
if (!this.form.content.trim() || this.form.content === '<p></p>') {
|
|
2464
|
+
this.showStatus('Content is required', 'error')
|
|
2465
|
+
return
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// Submit form
|
|
2469
|
+
const response = await fetch('/api/articles', {
|
|
2470
|
+
method: 'POST',
|
|
2471
|
+
headers: {
|
|
2472
|
+
'Content-Type': 'application/json'
|
|
2473
|
+
},
|
|
2474
|
+
body: JSON.stringify({
|
|
2475
|
+
...this.form,
|
|
2476
|
+
isDraft: false,
|
|
2477
|
+
publishedAt: new Date().toISOString()
|
|
2478
|
+
})
|
|
2479
|
+
})
|
|
2480
|
+
|
|
2481
|
+
if (response.ok) {
|
|
2482
|
+
this.showStatus('Article published successfully!', 'success')
|
|
2483
|
+
// Reset form or redirect
|
|
2484
|
+
} else {
|
|
2485
|
+
throw new Error('Publish failed')
|
|
2486
|
+
}
|
|
2487
|
+
} catch (error) {
|
|
2488
|
+
this.showStatus('Publish failed', 'error')
|
|
2489
|
+
}
|
|
2490
|
+
},
|
|
2491
|
+
|
|
2492
|
+
showStatus(message, type) {
|
|
2493
|
+
this.status = message
|
|
2494
|
+
this.statusClass = `status-${type}`
|
|
2495
|
+
|
|
2496
|
+
setTimeout(() => {
|
|
2497
|
+
this.status = ''
|
|
2498
|
+
this.statusClass = ''
|
|
2499
|
+
}, 3000)
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
</script>
|
|
2504
|
+
|
|
2505
|
+
<style scoped>
|
|
2506
|
+
.editor-form {
|
|
2507
|
+
max-width: 800px;
|
|
2508
|
+
margin: 0 auto;
|
|
2509
|
+
padding: 20px;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
.bbl-form-group {
|
|
2513
|
+
margin-bottom: 20px;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
.bbl-form-group label {
|
|
2517
|
+
display: block;
|
|
2518
|
+
margin-bottom: 8px;
|
|
2519
|
+
font-weight: 500;
|
|
2520
|
+
color: #374151;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
.form-control {
|
|
2524
|
+
width: 100%;
|
|
2525
|
+
padding: 10px 12px;
|
|
2526
|
+
border: 1px solid #d1d5db;
|
|
2527
|
+
border-radius: 6px;
|
|
2528
|
+
font-size: 14px;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
.form-control:focus {
|
|
2532
|
+
outline: none;
|
|
2533
|
+
border-color: #3b82f6;
|
|
2534
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
.form-actions {
|
|
2538
|
+
display: flex;
|
|
2539
|
+
gap: 12px;
|
|
2540
|
+
margin-top: 30px;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
.btn {
|
|
2544
|
+
padding: 10px 20px;
|
|
2545
|
+
border: none;
|
|
2546
|
+
border-radius: 6px;
|
|
2547
|
+
font-weight: 500;
|
|
2548
|
+
cursor: pointer;
|
|
2549
|
+
transition: background-color 0.2s;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
.btn-primary {
|
|
2553
|
+
background: #3b82f6;
|
|
2554
|
+
color: white;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
.btn-primary:hover {
|
|
2558
|
+
background: #2563eb;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
.btn-secondary {
|
|
2562
|
+
background: #6b7280;
|
|
2563
|
+
color: white;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
.btn-secondary:hover {
|
|
2567
|
+
background: #4b5563;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
.form-status {
|
|
2571
|
+
margin-top: 20px;
|
|
2572
|
+
padding: 12px;
|
|
2573
|
+
border-radius: 6px;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
.status-success {
|
|
2577
|
+
background: #d1fae5;
|
|
2578
|
+
color: #065f46;
|
|
2579
|
+
border: 1px solid #a7f3d0;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
.status-error {
|
|
2583
|
+
background: #fee2e2;
|
|
2584
|
+
color: #991b1b;
|
|
2585
|
+
border: 1px solid #fca5a5;
|
|
2586
|
+
}
|
|
2587
|
+
</style>
|
|
2588
|
+
```
|
|
2589
|
+
|
|
2590
|
+
This comprehensive guide covers all the components and usage patterns available in the Vue2 Premium BBL Editor package. Each example includes complete implementation details, event handling, and styling to help you integrate the components effectively in your projects.
|