提交 | 用户 | age
|
c0e4c9
|
1 |
<template> |
3ad1a4
|
2 |
<div :class="prefixCls" :style="{ width: containerWidth }"> |
V |
3 |
<ImgUpload |
39d629
|
4 |
:fullscreen="fullscreen" |
3ad1a4
|
5 |
@uploading="handleImageUploading" |
V |
6 |
@done="handleDone" |
|
7 |
v-if="showImageUpload" |
|
8 |
v-show="editorRef" |
966571
|
9 |
:disabled="disabled" |
3ad1a4
|
10 |
/> |
1093ec
|
11 |
<textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }"></textarea> |
c0e4c9
|
12 |
</div> |
J |
13 |
</template> |
|
14 |
|
|
15 |
<script lang="ts"> |
d73d43
|
16 |
import type { RawEditorSettings } from 'tinymce'; |
a81268
|
17 |
import tinymce from 'tinymce/tinymce'; |
V |
18 |
import 'tinymce/themes/silver'; |
|
19 |
import 'tinymce/icons/default/icons'; |
|
20 |
import 'tinymce/plugins/advlist'; |
|
21 |
import 'tinymce/plugins/anchor'; |
|
22 |
import 'tinymce/plugins/autolink'; |
|
23 |
import 'tinymce/plugins/autosave'; |
|
24 |
import 'tinymce/plugins/code'; |
|
25 |
import 'tinymce/plugins/codesample'; |
|
26 |
import 'tinymce/plugins/directionality'; |
|
27 |
import 'tinymce/plugins/fullscreen'; |
|
28 |
import 'tinymce/plugins/hr'; |
|
29 |
import 'tinymce/plugins/insertdatetime'; |
|
30 |
import 'tinymce/plugins/link'; |
|
31 |
import 'tinymce/plugins/lists'; |
|
32 |
import 'tinymce/plugins/media'; |
|
33 |
import 'tinymce/plugins/nonbreaking'; |
|
34 |
import 'tinymce/plugins/noneditable'; |
|
35 |
import 'tinymce/plugins/pagebreak'; |
|
36 |
import 'tinymce/plugins/paste'; |
|
37 |
import 'tinymce/plugins/preview'; |
|
38 |
import 'tinymce/plugins/print'; |
|
39 |
import 'tinymce/plugins/save'; |
|
40 |
import 'tinymce/plugins/searchreplace'; |
|
41 |
import 'tinymce/plugins/spellchecker'; |
|
42 |
import 'tinymce/plugins/tabfocus'; |
|
43 |
// import 'tinymce/plugins/table'; |
|
44 |
import 'tinymce/plugins/template'; |
|
45 |
import 'tinymce/plugins/textpattern'; |
|
46 |
import 'tinymce/plugins/visualblocks'; |
|
47 |
import 'tinymce/plugins/visualchars'; |
|
48 |
import 'tinymce/plugins/wordcount'; |
|
49 |
|
f75425
|
50 |
import { |
V |
51 |
defineComponent, |
|
52 |
computed, |
|
53 |
nextTick, |
|
54 |
ref, |
|
55 |
unref, |
|
56 |
watch, |
|
57 |
onUnmounted, |
|
58 |
onDeactivated, |
|
59 |
} from 'vue'; |
3576d0
|
60 |
import ImgUpload from './ImgUpload.vue'; |
a81268
|
61 |
import { toolbar, plugins } from './tinymce'; |
fcee7d
|
62 |
import { buildShortUUID } from '/@/utils/uuid'; |
f75425
|
63 |
import { bindHandlers } from './helper'; |
73c8e0
|
64 |
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated'; |
3ad1a4
|
65 |
import { useDesign } from '/@/hooks/web/useDesign'; |
a9462f
|
66 |
import { isNumber } from '/@/utils/is'; |
83c9cd
|
67 |
import { useLocale } from '/@/locales/useLocale'; |
无 |
68 |
import { useAppStore } from '/@/store/modules/app'; |
f75425
|
69 |
|
39d629
|
70 |
const tinymceProps = { |
V |
71 |
options: { |
d73d43
|
72 |
type: Object as PropType<Partial<RawEditorSettings>>, |
39d629
|
73 |
default: {}, |
V |
74 |
}, |
|
75 |
value: { |
|
76 |
type: String, |
|
77 |
}, |
|
78 |
|
|
79 |
toolbar: { |
|
80 |
type: Array as PropType<string[]>, |
|
81 |
default: toolbar, |
|
82 |
}, |
|
83 |
plugins: { |
|
84 |
type: Array as PropType<string[]>, |
|
85 |
default: plugins, |
|
86 |
}, |
|
87 |
modelValue: { |
|
88 |
type: String, |
|
89 |
}, |
|
90 |
height: { |
|
91 |
type: [Number, String] as PropType<string | number>, |
|
92 |
required: false, |
|
93 |
default: 400, |
|
94 |
}, |
|
95 |
width: { |
|
96 |
type: [Number, String] as PropType<string | number>, |
|
97 |
required: false, |
|
98 |
default: 'auto', |
|
99 |
}, |
|
100 |
showImageUpload: { |
|
101 |
type: Boolean, |
|
102 |
default: true, |
|
103 |
}, |
|
104 |
}; |
c0e4c9
|
105 |
|
J |
106 |
export default defineComponent({ |
|
107 |
name: 'Tinymce', |
9edc28
|
108 |
components: { ImgUpload }, |
144ab5
|
109 |
inheritAttrs: false, |
39d629
|
110 |
props: tinymceProps, |
f75425
|
111 |
emits: ['change', 'update:modelValue'], |
V |
112 |
setup(props, { emit, attrs }) { |
3576d0
|
113 |
const editorRef = ref(); |
39d629
|
114 |
const fullscreen = ref(false); |
fcee7d
|
115 |
const tinymceId = ref<string>(buildShortUUID('tiny-vue')); |
f75425
|
116 |
const elRef = ref<Nullable<HTMLElement>>(null); |
3ad1a4
|
117 |
|
V |
118 |
const { prefixCls } = useDesign('tinymce-container'); |
f75425
|
119 |
|
83c9cd
|
120 |
const appStore = useAppStore(); |
无 |
121 |
|
3576d0
|
122 |
const tinymceContent = computed(() => props.modelValue); |
f75425
|
123 |
|
c0e4c9
|
124 |
const containerWidth = computed(() => { |
J |
125 |
const width = props.width; |
a9462f
|
126 |
if (isNumber(width)) { |
c0e4c9
|
127 |
return `${width}px`; |
J |
128 |
} |
|
129 |
return width; |
83c9cd
|
130 |
}); |
无 |
131 |
|
|
132 |
const skinName = computed(() => { |
|
133 |
return appStore.getDarkMode === 'light' ? 'oxide' : 'oxide-dark'; |
|
134 |
}); |
|
135 |
|
|
136 |
const langName = computed(() => { |
|
137 |
const lang = useLocale().getLocale.value; |
|
138 |
return ['zh_CN', 'en'].includes(lang) ? lang : 'zh_CN'; |
c0e4c9
|
139 |
}); |
f75425
|
140 |
|
d73d43
|
141 |
const initOptions = computed((): RawEditorSettings => { |
39d629
|
142 |
const { height, options, toolbar, plugins } = props; |
a863ad
|
143 |
const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/'; |
c0e4c9
|
144 |
return { |
f75425
|
145 |
selector: `#${unref(tinymceId)}`, |
39d629
|
146 |
height, |
V |
147 |
toolbar, |
e034d1
|
148 |
menubar: 'file edit insert view format table', |
39d629
|
149 |
plugins, |
83c9cd
|
150 |
language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js', |
无 |
151 |
language: langName.value, |
39d629
|
152 |
branding: false, |
f75425
|
153 |
default_link_target: '_blank', |
V |
154 |
link_title: false, |
|
155 |
object_resizing: false, |
d73d43
|
156 |
auto_focus: true, |
83c9cd
|
157 |
skin: skinName.value, |
无 |
158 |
skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value, |
|
159 |
content_css: |
|
160 |
publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css', |
e034d1
|
161 |
...options, |
d73d43
|
162 |
setup: (editor) => { |
f75425
|
163 |
editorRef.value = editor; |
d73d43
|
164 |
editor.on('init', (e) => initSetup(e)); |
f75425
|
165 |
}, |
c0e4c9
|
166 |
}; |
f75425
|
167 |
}); |
V |
168 |
|
966571
|
169 |
const disabled = computed(() => { |
L |
170 |
const { options } = props; |
|
171 |
const getdDisabled = options && Reflect.get(options, 'readonly'); |
efce48
|
172 |
const editor = unref(editorRef); |
L |
173 |
if (editor) { |
|
174 |
editor.setMode(getdDisabled ? 'readonly' : 'design'); |
|
175 |
} |
966571
|
176 |
return getdDisabled ?? false; |
L |
177 |
}); |
|
178 |
|
f75425
|
179 |
watch( |
V |
180 |
() => attrs.disabled, |
|
181 |
() => { |
|
182 |
const editor = unref(editorRef); |
3576d0
|
183 |
if (!editor) { |
V |
184 |
return; |
|
185 |
} |
f75425
|
186 |
editor.setMode(attrs.disabled ? 'readonly' : 'design'); |
V |
187 |
} |
|
188 |
); |
39d629
|
189 |
|
73c8e0
|
190 |
onMountedOrActivated(() => { |
fcee7d
|
191 |
tinymceId.value = buildShortUUID('tiny-vue'); |
f75425
|
192 |
nextTick(() => { |
39d629
|
193 |
setTimeout(() => { |
V |
194 |
initEditor(); |
|
195 |
}, 30); |
f75425
|
196 |
}); |
V |
197 |
}); |
|
198 |
|
|
199 |
onUnmounted(() => { |
|
200 |
destory(); |
|
201 |
}); |
|
202 |
|
|
203 |
onDeactivated(() => { |
|
204 |
destory(); |
|
205 |
}); |
|
206 |
|
|
207 |
function destory() { |
39d629
|
208 |
if (tinymce !== null) { |
V |
209 |
tinymce?.remove?.(unref(editorRef)); |
f75425
|
210 |
} |
V |
211 |
} |
|
212 |
|
|
213 |
function initEditor() { |
1093ec
|
214 |
const el = unref(elRef); |
V |
215 |
if (el) { |
|
216 |
el.style.visibility = ''; |
|
217 |
} |
39d629
|
218 |
tinymce.init(unref(initOptions)); |
f75425
|
219 |
} |
V |
220 |
|
d73d43
|
221 |
function initSetup(e) { |
f75425
|
222 |
const editor = unref(editorRef); |
3576d0
|
223 |
if (!editor) { |
V |
224 |
return; |
|
225 |
} |
f75425
|
226 |
const value = props.modelValue || ''; |
V |
227 |
|
|
228 |
editor.setContent(value); |
|
229 |
bindModelHandlers(editor); |
|
230 |
bindHandlers(e, attrs, unref(editorRef)); |
|
231 |
} |
|
232 |
|
3ad1a4
|
233 |
function setValue(editor: Recordable, val: string, prevVal?: string) { |
58f988
|
234 |
if ( |
V |
235 |
editor && |
|
236 |
typeof val === 'string' && |
|
237 |
val !== prevVal && |
|
238 |
val !== editor.getContent({ format: attrs.outputFormat }) |
|
239 |
) { |
|
240 |
editor.setContent(val); |
|
241 |
} |
|
242 |
} |
|
243 |
|
f75425
|
244 |
function bindModelHandlers(editor: any) { |
V |
245 |
const modelEvents = attrs.modelEvents ? attrs.modelEvents : null; |
|
246 |
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents; |
09c9f8
|
247 |
|
f75425
|
248 |
watch( |
V |
249 |
() => props.modelValue, |
|
250 |
(val: string, prevVal: string) => { |
58f988
|
251 |
setValue(editor, val, prevVal); |
V |
252 |
} |
|
253 |
); |
|
254 |
|
|
255 |
watch( |
|
256 |
() => props.value, |
|
257 |
(val: string, prevVal: string) => { |
|
258 |
setValue(editor, val, prevVal); |
|
259 |
}, |
|
260 |
{ |
|
261 |
immediate: true, |
f75425
|
262 |
} |
V |
263 |
); |
|
264 |
|
|
265 |
editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => { |
58f988
|
266 |
const content = editor.getContent({ format: attrs.outputFormat }); |
V |
267 |
emit('update:modelValue', content); |
|
268 |
emit('change', content); |
f75425
|
269 |
}); |
39d629
|
270 |
|
V |
271 |
editor.on('FullscreenStateChanged', (e) => { |
|
272 |
fullscreen.value = e.state; |
|
273 |
}); |
f75425
|
274 |
} |
V |
275 |
|
3ad1a4
|
276 |
function handleImageUploading(name: string) { |
V |
277 |
const editor = unref(editorRef); |
3576d0
|
278 |
if (!editor) { |
V |
279 |
return; |
|
280 |
} |
3ad1a4
|
281 |
const content = editor?.getContent() ?? ''; |
3576d0
|
282 |
setValue(editor, `${content}\n${getUploadingImgName(name)}`); |
3ad1a4
|
283 |
} |
V |
284 |
|
|
285 |
function handleDone(name: string, url: string) { |
|
286 |
const editor = unref(editorRef); |
3576d0
|
287 |
if (!editor) { |
V |
288 |
return; |
|
289 |
} |
3ad1a4
|
290 |
const content = editor?.getContent() ?? ''; |
3576d0
|
291 |
const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? ''; |
3ad1a4
|
292 |
setValue(editor, val); |
V |
293 |
} |
|
294 |
|
3576d0
|
295 |
function getUploadingImgName(name: string) { |
3ad1a4
|
296 |
return `[uploading:${name}]`; |
V |
297 |
} |
|
298 |
|
f75425
|
299 |
return { |
3ad1a4
|
300 |
prefixCls, |
f75425
|
301 |
containerWidth, |
V |
302 |
initOptions, |
|
303 |
tinymceContent, |
|
304 |
elRef, |
|
305 |
tinymceId, |
3ad1a4
|
306 |
handleImageUploading, |
V |
307 |
handleDone, |
|
308 |
editorRef, |
39d629
|
309 |
fullscreen, |
966571
|
310 |
disabled, |
f75425
|
311 |
}; |
c0e4c9
|
312 |
}, |
J |
313 |
}); |
|
314 |
</script> |
|
315 |
|
3ad1a4
|
316 |
<style lang="less" scoped></style> |
V |
317 |
|
|
318 |
<style lang="less"> |
|
319 |
@prefix-cls: ~'@{namespace}-tinymce-container'; |
|
320 |
|
|
321 |
.@{prefix-cls} { |
c0e4c9
|
322 |
position: relative; |
J |
323 |
line-height: normal; |
|
324 |
|
3ad1a4
|
325 |
textarea { |
V |
326 |
z-index: -1; |
|
327 |
visibility: hidden; |
c0e4c9
|
328 |
} |
J |
329 |
} |
|
330 |
</style> |