Tinymce 如何嵌入vue 不说了
代码核心是对Tinymce 默认功能封装。 此篇文件 主要是针对富文本编辑器的样式改造和 一个拓展按钮功能实现
Tinymce 很强,他有丰富的APi和对用户开放的接口
我们可以通过对外开放接口 定制自己需要的功能。直接上代码
使用代码
引入
import Tinymce from '@comp/widgets/TinymceVue'
import { defaultStyles } from '@util/tinymceConfig'
html 片段
<tinymce id="tinymce-area" ref="tinymceArea" v-model="content" :width="942" style="width: 950px;margin: 0 auto;float: left;" @onChoose="isInsertModalShow=true"></tinymce>
tinymceConfig .js
//样式重构处理。
export const defaultStyles = `
.area {
position: relative;
display: inline-block;
text-indent: 0;
}
.area button {
position: relative;
margin-left: 1px;
top: -1px;
display:inline-block;
padding: 4px 30px;
border: none;
border-bottom: 1px solid #333;
width: auto;
min-width: 120px;
vertical-align: bottom;
background-color: #FFF3C8;
color: #F38F1B;
font-size: 14px;
text-align: center;
}
.area .remove {
display: none;
position: absolute;
right: -6px;
bottom: 20px;
width: 14px;
height: 14px;
background: url('/static/img/close.png') 0 0 no-repeat;
color: #fff;
cursor: pointer;
}
.area:hover .remove {
display: inline-block;
font-size: 12px;
}
`
// p {
// display: block;
// -webkit-margin-before: 1em;
// -webkit-margin-after: 1em;
// -webkit-margin-start: 0px;
// -webkit-margin-end: 0px;
// }
// .b1{white-space-collapsing:preserve;}
// .b2{margin: 0.7875in 1.0236111in 0.7875in 1.0236111in;}
// .p1{text-align:start;hyphenate:auto;font-family:Times New Roman;font-size:16pt;}
// .p2{text-indent:2.2326388in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:22pt;}
// .p3{text-align:center;hyphenate:auto;font-family:Times New Roman;font-size:22pt;}
// .p4{text-align:center;hyphenate:auto;font-family:宋体;font-size:12pt;}
// .p5{text-align:start;hyphenate:auto;font-family:宋体;font-size:16pt;}
// .p6{text-align:justify;hyphenate:auto;font-family:宋体;font-size:12pt;}
// .p7{text-indent:3.6006944in;text-align:start;hyphenate:auto;font-family:宋体;font-size:12pt;}
// .p8{text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p9{text-indent:0.29166666in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p10{text-indent:0.30208334in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p11{text-indent:0.29166666in;text-align:start;hyphenate:auto;font-family:Times New Roman;font-size:10pt;}
// .p12{text-indent:0.30208334in;text-align:justify;hyphenate:auto;font-family:Times New Roman;font-size:10pt;}
// .p13{text-indent:0.5833333in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p14{text-indent:0.29166666in;text-align:start;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p15{text-indent:0.40138888in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p16{text-indent:0.47430557in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p17{text-align:justify;hyphenate:auto;font-family:Times New Roman;font-size:10pt;}
// .p18{text-align:center;hyphenate:auto;font-family:Times New Roman;font-size:16pt;}
// .p19{text-align:start;hyphenate:auto;font-family:Times New Roman;font-size:10pt;}
// .p20{text-indent:0.29166666in;text-align:justify;hyphenate:auto;font-family:Times New Roman;font-size:10pt;}
// .p21{text-indent:0.29166666in;margin-left:0.29166666in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p22{text-indent:0.5833333in;margin-left:0.29166666in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p23{text-indent:0.875in;margin-left:0.29166666in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .p24{margin-right:-0.011805556in;text-align:justify;hyphenate:auto;font-family:宋体;font-size:10pt;}
// .s1{font-family:宋体;font-weight:bold;}
// .s2{color:red;text-decoration:underline;}
// .s3{font-weight:bold;}
// .s4{font-weight:bold;color:red;text-decoration:underline;}
// .s5{font-family:宋体;}
// .s6{text-decoration:underline;}
// .s7{color:black;}
// .s8{font-family:Times New Roman;color:red;text-decoration:underline;}
// .s9{color:black;text-decoration:underline;}
// .s10{color:red;}
// .s11{font-family:Times New Roman;color:black;}
// .s12{font-family:宋体;font-size:12pt;color:red;text-decoration:underline;}
// .s13{font-weight:bold;color:black;}
// .s14{font-family:宋体;color:red;text-decoration:underline;}
// .s15{font-family:宋体;color:black;}
// .td1{width:3.0194445in;padding-start:0.019444445in;padding-end:0.019444445in;border-bottom:thin solid black;border-left:thin solid black;border-right:thin solid black;border-top:thin solid black;}
// .td2{width:3.025in;padding-start:0.019444445in;padding-end:0.019444445in;border-bottom:thin solid black;border-left:thin solid black;border-right:thin solid black;border-top:thin solid black;}
// .r1{height:1.4631945in;keep-together:always;}
// .r2{height:1.0145833in;keep-together:always;}
// .t1{table-layout:fixed;border-collapse:collapse;border-spacing:0;}
export default {
width: '824',
height: '400',
'theme-advanced-resizing': false,
// content_css: 'ui/tinymce/myContent.css',
// content_style: styles,
// document_base_url: 'http://localhost:2333/',
// relative_urls: false,
skin: false,
statusbar: false,
// height: 300,
toolbar: 'undo redo | formatselect fontselect fontsizeselect | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | numlist bullist outdent indent | removeformat | table | InsertElements',
menu: {},
font_formats: '微软雅黑=微软雅黑,Microsoft YaHei;宋体=宋体, SimSun;仿宋=仿宋;仿宋_GB2312=FangSong_GB2312;新宋体=新宋体;黑体=黑体, SimHei;隶书=隶书, SimLi;楷体=楷体, SimKai;楷体_GB2312=KaiTi_GB2312;幼圆=幼圆;andale mono=andale mono;arial=arial, helvetica,sans-serif;arial black=arial black,avant garde;comic sans ms=comic sans ms;impact=impact,chicago;Arial=Arial;Verdana=Verdana;Georgia=Georgia;Times New Roman=Times New Roman;Trebuchet MS=Trebuchet MS;Courier New=Courier New;Impact=Impact;Comic Sans MS=Comic Sans MS;Calibri=Calibri',
fontsize_formats: '初号=42pt 小初=36pt 一号=26pt 小一=24pt 二号=22pt 小二=18pt 三号=16pt 小三=15pt 四号=14pt 小四=12pt 五号=10.5pt 小五=9pt 六号=7.5pt 小六=6.5pt 七号=5.5pt 八号=5pt 5pt 5.5pt 6.5pt 7.5pt 8pt 9pt 10pt 10.5pt 11pt 12pt 14pt 16pt 18pt 20pt 24pt 36pt 48pt 72pt',
block_formats: 'Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6'
}
代码如下
Tinymce.vue
<template>
<div style="overflow-y: auto;" :style="{height: height+'px'}">
<div :style="{width: width + 'px'}" style="margin: 0 auto;">
<textarea :id="id" />
</div>
</div>
</template>
<script>
// Import TinyMCE
import tinymce from 'tinymce/tinymce'
// A theme is also required
import 'tinymce/themes/modern/theme'
// Any plugins you want to use has to be imported
import 'tinymce/plugins/paste'
import 'tinymce/plugins/autoresize'
import 'tinymce/plugins/textcolor'
import 'tinymce/plugins/wordcount'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/table'
import '../../../static/langs/zh_CN'
import config from '@util/tinymceConfig'
import safeStr from '@util/xss'
let content = ''
export default {
name: 'Tinymce',
props: {
id: {
type: String,
required: true
},
// htmlClass: {
// default: '',
// type: String
// },
width: {
type: Number,
default: 1000
},
styles: {
type: String,
default: ''
},
value: {
type: String,
default: ''
},
plugins: {
default: () => {
return [
// 'advlist autolink lists link image charmap print preview hr anchor pagebreak',
// 'searchreplace wordcount visualblocks visualchars code fullscreen',
// 'insertdatetime media nonbreaking save table contextmenu directionality',
'textcolor wordcount fullscreen autoresize table'
]
},
type: Array
},
otherOptions: { default: () => {}, type: Object }
},
data () {
return {
// content: '',
editor: null,
checkerTimeout: null,
height: 500,
isTyping: false
}
},
watch: {
value: function (newValue) {
if (!this.isTyping) {
if (this.editor !== null) {
this.editor.setContent(newValue)
this.submitNewContent()
} else {
content = newValue
}
}
}
},
mounted () {
content = this.value
if (this.editor) {
this.editor.destroy()
}
this.height = this.$app.remote.getCurrentWindow().getSize()[1] - 200
// this.$app.remote.getCurrentWindow().on('resize', () => {
// this.height = this.$app.remote.getCurrentWindow().getSize()[1] - 200
// })
},
beforeDestroy () {
content = null
this.editor.destroy()
this.editor = null
},
methods: {
// 外部调用
insertElem ({ id, elementName, elementKey }) {
// contenteditable="false" 不可编辑
let dom = `<span class="area ${id}" contenteditable="false">
<button data-id="${id}" data-key="${elementKey}">
${safeStr(elementName)}
</button>
<span class="remove">.</span>
</span>`
// disabled="disabled" 去掉该button属性
this.editor.insertContent(dom)
this.saveRecentContent()
},
insertValueElem ({ id, elementName, elementKey, value }) {
// contenteditable="false" 不可编辑
let dom = `<span class="area valuearea ${id}" contenteditable="false">
<button data-id="${id}" data-key="${elementKey}">
${safeStr(value || '')}
</button>
<span class="remove">.</span>
</span>`
// disabled="disabled" 去掉该button属性
this.editor.insertContent(dom)
this.saveRecentContent()
},
init (styles = '') {
let options = {
selector: '#' + this.id,
width: this.width + '',
// height: this.height + '',
content_style: styles,
content_css: process.env === 'development' ? '/static/myTinymceSkin/content.min.css' : 'static/myTinymceSkin/content.min.css',
plugins: this.plugins,
autoresize_min_height: this.height - 57,
init_instance_callback: this.initEditor,
setup: this.setupEditor
}
tinymce.init(this._assign(config, options, this.otherOptions))
},
initEditor (editor) {
this.editor = editor
// editor.on('KeyUp', (e) => {
// this.submitNewContent()
// })
// editor.on('Change', (e) => {
// if (this.editor.getContent() !== this.value) {
// this.submitNewContent()
// }
// this.$emit('editorChange', e)
// })
editor.on('init', () => {
editor.setContent(content)
// this.$emit('input', this.content)
this.saveRecentContent()
})
editor.on('Click', (e) => {
if (e.target.className.split(' ').indexOf('remove') > -1) {
let removeNode = e.target.parentNode
if (!!window.ActiveXObject || 'ActiveXObject' in window || (/Trident\/7\./).test(navigator.userAgent)) { // IE或IE11
removeNode.parentNode.removeChild(removeNode)
} else {
removeNode.remove()
}
const id = removeNode.getElementsByTagName('button')[0].getAttribute('data-id')
this.$emit('remove', id)
this.submitNewContent()
}
})
editor.on('dblClick', (e) => {
let id = e.target.getAttribute('data-id') || ''
if (id) {
this.$emit('onDblclick', id)
}
})
this.$emit('editorInit', editor)
},
setupEditor (editor) {
this.editor = editor
editor.addButton('InsertElements', {
// icon: 'insertdatetime',
image: process.env.TARGET === 'web' ? '/static/image/insert.png' : './static/image/insert.png',
text: ' 插入要素',
tooltip: '插入要素',
onclick: () => {
this.$emit('onChoose')
}
})
},
submitNewContent () {
this.isTyping = true
if (this.checkerTimeout !== null) {
clearTimeout(this.checkerTimeout)
this.checkerTimeout = setTimeout(() => {
this.isTyping = false
}, 300)
}
// this.$emit('input', this.editor.getContent())
this.saveRecentContent()
},
setNewContent (newValue = '') {
this.editor.setContent(newValue)
},
saveRecentContent () {
this.$emit('input', this.editor.getContent())
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/*@import '~tinymce/skins/lightgray/skin.min.css';*/
@import '../../assets/myTinymceSkin/skin.min.css';
</style>
升级使用功能插入标签。
上述核心代码中有一句。
@onChoose="isInsertModalShow=true"
主要作用是对一个弹框的控制
我的代码引入如下
import InsertModal from '../Contract/Modals/Insert'
<insert-modal v-model="isInsertModalShow" @choiceElem="insertElem"></insert-modal>
// 插入要素事件 insertElem实现方法
insertElem (elem) {
this.$refs.tinymceArea.insertElem(elem)
},
.Insert.vue 代码如下(主要是我自己的业务代码,此处贴处理是为了告诉用户,我们可以通过这个实现可以做很多事情)
<template>
<Modal v-model="visible" title="插入要素" :mask-closable="false" width="450" class-name="vertical-center-modal">
<Row style="margin-bottom: 120px;">
<Col span="6" class="col-left">查找要素</Col>
<Col span="18">
<auto-comp class="insert-search" v-model.trim="elementName" placeholder="请输入关键字查找要素" style="width:240px"
@on-search="handleSearch" @on-focus="handleSearch">
<div class="empty" v-if="isEmpty">暂无搜索结果</div>
<Option v-else v-for="elem in elemsList" :value="elem.elementName" :key="elem.id" @click.native="handleSelect(elem)">{{ elem.elementName }}</Option>
</auto-comp>
</Col>
</Row>
<div slot="footer" align="middle">
<Button type="primary" @click="onSubmit">确定</Button>
<Button type="ghost" @click="onCancel">取消</Button>
</div>
</Modal>
</template>
<script>
import AutoComp from '@comp/widgets/AutoComp'
import ModalMixin from '@comp/mixins/ModalPage'
export default {
name: 'contract-insert-element',
mixins: [ModalMixin],
components: {
AutoComp
},
data () {
return {
setting: false, // 防重复提交
isEmpty: true,
elementName: '',
elem: {},
elemsList: []
}
},
methods: {
onShow () {
this.setting = false
},
handleSearch () {
let paramsData = {
escapeLoading: true,
pageNo: 1,
pageSize: 200,
sortField: 'createdTime',
sortType: 'desc',
elementName: this.elementName
}
this.$http.post('/contractElement/selectListByPage', paramsData).then(res => {
if (res.rtnCode === '000') {
console.log(res)
this.elemsList = res.data.rows || []
this.isEmpty = this.elemsList.length === 0
} else {
this.$Message.error(res.rtnMsg)
}
}).catch((err) => {
this.$debug(err)
})
},
handleSelect (elem) {
this.selected = true
this.elem = elem
},
onSubmit () {
// 防止重复提交
if (this.setting === false) {
if (this._isEmpty(this.elem) || this.elem.elementName !== this.elementName) {
this.$Message.warning('请点击选中要素')
} else {
this.setting = true
this.$emit('choiceElem', this.elem)
this.visible = false
}
}
},
onCancel () {
this.visible = false
}
}
}
</script>
<style lang='scss' scoped>
.col-left {
padding-right: 10px;
line-height: 30px;
font-size: 14px;
text-align: right;
color: #333;
&:after {
content: ':'
}
}
.empty {
height: 30px;
margin:10px 0 0 15px;
color: #999;
}
</style>