富文本预览
- 使用 shadow dom 进行样式隔绝
- 使用 prismjs 进行 code 插件样式格式化
- 图片预览大图使用 Element UI 自带的图片组件
<template>
<div class="rt-view">
<rt-view v-if="contentHtml" :content="contentHtml" />
<el-image ref="rt_view_image" :src="previewImage" :preview-src-list="previewImageList" />
</div>
</template>
<script>
import Prism from 'prismjs'
// import { txt2Html } from '@/utils/index'
let vm = {}
class ViewElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
// html
const htmlText = document.createElement('div')
htmlText.setAttribute('id', 'rt_view')
htmlText.innerHTML = this.getAttribute('content')
// css
const cssLink = document.createElement('link')
cssLink.href = './prism.css' // prism.css 文件地址
cssLink.rel = 'stylesheet'
// style
const styleText = document.createElement('style')
styleText.textContent = 'img{max-width: 100%;} pre{overflow-x: auto;}'
// append
this.shadowRoot.appendChild(cssLink)
this.shadowRoot.appendChild(styleText)
this.shadowRoot.appendChild(htmlText)
// set code
const preDom = this.shadowRoot.querySelectorAll('pre')
preDom.forEach(item => {
const codeDom = item.querySelector('code')
if (codeDom && codeDom.textContent) {
const languages = item.getAttribute('class')?.split('language-')[1]
if (languages) {
if (Object.prototype.hasOwnProperty.call(Prism.languages, languages)) {
codeDom.innerHTML = Prism.highlight(item.textContent, Prism.languages[languages], languages)
} else {
codeDom.innerHTML = Prism.highlight(item.textContent, Prism.languages['text'], 'text')
}
}
}
})
// set table
this.shadowRoot.querySelectorAll('table').forEach(item => {
const tableParentDom = document.createElement('div')
tableParentDom.setAttribute('style', 'width: 100%; overflow: auto;')
item.parentNode.replaceChild(tableParentDom, item)
tableParentDom.appendChild(item)
})
// view image
this.shadowRoot.querySelectorAll('img').forEach(item => {
const currentSrc = item.getAttribute('src')
// 图片点击放大,带链接的图片除外
if (currentSrc && !item.closest('a')?.getAttribute('href')) {
const style = item.getAttribute('style')
item.setAttribute('style', 'cursor: zoom-in;' + style)
item.addEventListener('click', e => {
e.stopPropagation()
vm.handleView(currentSrc)
})
}
})
}
disconnectedCallback() {
this.shadowRoot.querySelectorAll('img').forEach(item => {
item.removeEventListener('click', null)
})
}
}
export default {
name: 'RbRichTextView',
props: {
content: {
type: String,
default: ''
}
},
watch: {
content: {
handler(e) {
this.contentHtml = ''
if (e) {
// if (/\.txt$/.test(e)) {
// txt2Html(e).then(html => {
// this.contentHtml = html
// })
// } else {
// this.contentHtml = e
// }
this.contentHtml = e
}
},
immediate: true
}
},
data() {
return {
contentHtml: '',
previewImage: '',
previewImageList: []
}
},
created() {
vm = this
if (!customElements.get('rt-view')) {
customElements.define('rt-view', ViewElement)
}
},
methods: {
handleView(e) {
this.previewImage = e
this.previewImageList = [e]
setTimeout(() => {
this.$refs['rt_view_image']?.clickHandler()
})
}
}
}
</script>
<style lang="less" scoped>
.rt-view {
word-break: break-all;
::v-deep {
.el-image__inner,
.el-image__error {
display: none;
}
}
}
</style>