Preface
Recently embedded in project due to business requirements tinymce This rich text editor is designed to meet the business needs of the platform for users to edit various types of news content and what. It also takes a lot of time to experience and compare various open source editors on the market.
*Case demo version: vue-tinymce-demo
Online preview: vue-tinymce-demo.netlify.com/#/
Simple comparison of major WYSIWYG editors
1,UEditor
Because it is no longer maintained, a lot of source code needs to be modified. Many of the code written for rendering projects for servers such as jsp need to be deleted. Then the more deleted, the more afraid the more deleted, the less unusable. Depending on jquery, parse edits need to be deleted with js. Parse finished content may also pollute the global css and be compatible with old browsers.It's good, but we don't think much about compatibility with IE.So say goodbye.
2,wangEditor
Chinese documents, fast start, rely on jquery, less features to spend time writing plug-ins, need to write a separate interface for the picture upload function, the old project is busy online temporarily used, feel not suitable for the current business so heavy editing function has been abandoned.
3,Quill
api is friendly, has few features, needs specific css to parse text (which I don't like), ui looks good, and is suitable for use as forum posting function.
4,CKEditor
CKEditor is currently in the mainstream version of 4.x, but the documentation looks blindly and really can't be happy to configure it. It's just been abandoned after using it. The 5.x version just ended in beta and req ui res a dedicated node and npm version. Although the powerful configuration is flexible and beautiful, poor compatibility is unlikely to come upNow we have a public view.
5,KingEditor
Ugly, dislike, dislike
6,Draft-js
Knowing that the text editor you just changed was developed on the basis of draft, relying on react and discarding.
7,Medium-editor:
Although it feels cool to see, it doesn't fit our business scene, and the api is ugly and scary.
8,trix:
Well, another small and beautiful one, give up
9,Slate
react, give up
10,Bootstrap-wysiwyg:
bootstrap, jquery, abandon
11,vue-quill-editor
Lightweight, less toolbar configuration, IE10+ encapsulated according to quillJs.
Extended use: www.jianshu.com/p/dc2492160...
12,tinymce
Well documented, powerful, bug-free, no external dependency, we all say good, well, that's it.
As long as you can understand English and play with it, most of the problems encountered in the adapter can be solved by reading the documentation. Even if you can't solve the problem by reading the documentation, there are a lot of articles on the Internet that can tell you how to configure it.
Of course, the main thing I need to do here is to address some of the needs that others find too simple to think of themselves, such as:
- word document is pasted in to be formatted
- Compatible Mobile End
- word documents are pasted in to display properly and to be compatible with the mobile side
- The content pasted into the computer web page should be displayed properly and the layout should not be messy
- Content copied from computer web pages should also be compatible to mobile
Advanced usage
You may also want to use tinyMCE in some more advanced ways.
For example npm: npm install tinymce
bower: bowerinstall tinymce or
bower install tinymce-src=git://github.com/tinymce/tinymce
composer: php composer.phar require"tinymce/tinymce" ">= 4"
nuget: Install-PackageTinyMCE
sdk: You can download it here: www.tinymce.com/download/
JQuery: If you want a tinyMCE in the form of a jQuery plug-in, you can customize it here: www.tinymce.com/download/cu... .You can customize the functions you need according to your wish.In this way, you can get a tinyMCE that is as small as possible and applicable.
1. Plug-ins
TinyMCE has many plug-ins to use, such as code editing mode, highlight mode, picture upload, and so on.The plug-in extends or adds functionality to tinyMCE.Alternatively, you can customize some plug-ins.
There is too much content about the plug-in to be translated, and subsequent plug-ins are also shown as links to hang out of the official website.
2. Independent UI
1. Theme and skin
You can customize themes and skins and configure them with threm and spin.
2. Dimensions
These configurations help you customize the dimensions, width, height, min_width, max_width, min_height, max_height.
You may also need an adaptive size ( www.tinymce.com/docs/plugin... ) plug-ins to help you make size smarter.Alternatively, you can use resize configuration.
3. Style
content_css can be used to help you customize the style of the main area.
Stasbar is set to false to hide the status bar.
4. Source mode
www.tinymce.com/docs/get-st... .End of page.
5. Upload pictures
https://www.tinymce.com/docs/get-started/upload-images/
6. Spelling Check
www.tinymce.com/docs/get-st...
7. Content filtering
https://www.tinymce.com/docs/get-started/filter-content/
Compatibility
Mobile end:
PC side:
Initialization
Because the Plugins of tinymce are loaded on demand
To get started with this editor quickly
Just before vue-cli Insert an online cdn address by default in index.html of
<script src="https://cdn.bootcss.com/tinymce/4.7.4/tinymce.min.js"></script>Copy Code
Remember to download Language Pack To local,
Then it's introduced in the file
Import'. /zh_CN.js'Copy Code
There will be a chance to write down the separate packing later, as it is not a small one.
Insert vue component template
<template> <div> <textarea :id= "Id"></textarea> </div> </template>Copy Code
Make sure you wrap a div outside the textarea, or.. you'll see if you try it yourself.
Component Foundation Configuration
Mount tinymce into the component through the specified selector
<template> <div> <textarea :id= "Id"></textarea> </div> </template> <script> import './zh_CN.js' export default { data () { const Id = Date.now() return { Id: Id, Editor: null, DefaultConfig: {} } }, props: { value: { default: '', type: String }, config: { type: Object, default: () => { return { theme: 'modern', height: 300 } } } }, mounted () { this.init() }, beforeDestroy () { // Destroy TinyMCE this.$emit('on-destroy') window.tinymce.remove(`#${this.Id}`) }, methods: { init () { const self = this this.Editor = window.tinymce.init({ // Default configuration...this.DefaultConfig, // config passed in from prop...this.config, // mounted DOM object selector: `#${this.Id}`, setup: (editor) => { // Throw 'on-ready' event hook editor.on( 'init', () => { self.loading = false self.$emit('on-ready') editor.setContent(self.value) } ) // Throw 'input' Event hook, synchronization value data editor.on( 'input change undo redo', () => { self.$emit('input', editor.getContent()) } ) } }) } } } </script>Copy Code
Okay, the basic initialization of the component is complete, and then we'll officially start our pit walk
API
Specific Contents API for Official Web That's fine. I can also see demo in comparison with chrome translation when my English is not good. Of course, the main reason is that I am lazy.
I wrote a default configuration in the component's data based on my business needs
DefaultConfig: { // GLOBAL height: 500, theme: 'modern', menubar: false, toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat hr | paste code link | undo redo | fullscreen `, plugins: ` paste importcss image code table advlist fullscreen link media lists textcolor colorpicker hr preview `, // CONFIG forced_root_block: 'p', force_p_newlines: true, importcss_append: true, // CONFIG: ContentStyle is an important part of this basic style which should be written in the final rendered page to ensure consistency. The problem with `table` and `img` basically depends on this to fill in the content_style: ` * {padding:0; margin:0;} html, body {height:100%;} img{max-width:100%; display:block; height:auto;} a {text-decoration: none;} iframe {width: 100%;} P {line-height:1.6; margin: 0px;} table {word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; } .mce-object-iframe { width:100%; box-sizing:border-box; margin:0; padding:0; } ul,ol { list-style-position:inside; } `, insert_button_items: 'image link | inserttable', // CONFIG: Paste paste_retain_style_properties: 'all', paste_word_valid_elements: '*[*]', // word needs it paste_data_images: true, // Paste while automatically uploading pictures in the content, very powerful function paste_convert_word_fake_lists: false, // Inserting a word document requires this property paste_webkit_styles: 'all', paste_merge_formats: true, nonbreaking_force_tab: false, paste_auto_cleanup_on_paste: false, // CONFIG: Font fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px', // CONFIG: StyleSelect style_formats: [ { title: 'text-indent', block: 'p', styles: { 'text-indent': '2em' } }, { title: 'Row Height', items: [ {title: '1', styles: { 'line-height': '1' }, inline: 'span'}, {title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'}, {title: '2', styles: { 'line-height': '2' }, inline: 'span'}, {title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'}, {title: '3', styles: { 'line-height': '3' }, inline: 'span'} ] } ], // FontSelect font_formats: `Microsoft Yahei=Microsoft Yahei; Song Style=Song Style; Black Style=Black Style; Song Style=Song Style; Kai Style=Kai Style; Lishu=Lishu; Youyuan=Youyuan; Andale Mono=andale mono,times; Arial=arial, helvetica, sans-serif; Arial Black=arial black, avant garde; Book Antiqua=book antiqua,palatino; Comic Sans MS=comic sans ms,sans-serif; Courier New=courier new,courier; Georgia=georgia,palatino; Helvetica=helvetica; Impact=impact,chicago; Symbol=symbol; Tahoma=tahoma,arial,helvetica,sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms,geneva; Verdana=verdana,geneva; Webdings=webdings; Wingdings=wingdings,zapf dingbats`, // Tab tabfocus_elements: ':prev,:next', object_resizing: true, // Image imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions' }Copy Code
Because I am lazy, the code exported from the above configuration may run the risk of code injection. It is recommended to do injection filtering on both front and back ends when saving, but is the main data security issue on the server side?
The following picture uploads can be separated for a small configuration and written directly into props.
url: { default: '', type: String }, accept: { default: 'image/jpeg, image/png', type: String }, maxSize: { default: 2097152, type: Number }, withCredentials: { default: false, type: Boolean }Copy Code
Then plug this set into the init configuration
//Picture upload images_upload_handler: function (blobInfo, success, failure) {if (blobInfo.blob (). size > self.maxSize) {failure ('file size is too large')} if (self.accept.indexOf (blobInfo.blob (). type) >= 0) {uploadPic ()} else {failure ('picture format error')} function uploadPic () {const XHR = new XMLHttpRequest () const formData = new FormData () xhr.withCredentials = self.withCredentials xhr.open ('POST', self.url) xhr.onload = function () {if (xhr.status!= 200) {//throw'on-upload-fail'Hook self. $emit ('on-upload-fail') failure ('upload failure: '+ xhr.status' return} const JSON = JSON.parse (xhr.responseText) //throw'on-upload-success'hook self. $emit ('on-upload-complete', [json, success, failure)]) FormData.append ('file', blobInfo.blob ()) xhr.send (formData)}} Copy code
At this point, the packaging of a component is basically complete
Look at the first results
<template> <div> <textarea :id= "Id"></textarea> </div> </template> <script> import './zh_CN.js' export default { data () { const Id = Date.now() return { Id: Id, Editor: null, DefaultConfig: { // GLOBAL height: 500, theme: 'modern', menubar: false, toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat hr | paste code link | undo redo | fullscreen `, plugins: ` paste importcss image code table advlist fullscreen link media lists textcolor colorpicker hr preview `, // CONFIG forced_root_block: 'p', force_p_newlines: true, importcss_append: true, // CONFIG: ContentStyle is an important part of this basic style, which should be written on the final rendered page to ensure consistency. The problem with `table` and `img` basically depends on this to fill in the content_style: ` * {padding:0; margin:0;} html, body{height: 100%;} img {max-width: 100%; display: block; height: auto;} a {text-decoration: none;} iframe {width: 100%;} P {line-height: 1.6; margin: 0px;}Table {word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; } .mce-object-iframe { width:100%; box-sizing:border-box; margin:0; padding:0; } ul,ol { list-style-position:inside; } `, insert_button_items: 'image link | inserttable', // CONFIG: Paste paste_retain_style_properties: 'all', paste_word_valid_elements: '*[*]', // word needs it paste_data_images: true, // Paste while automatically uploading pictures in the content, very powerful function paste_convert_word_fake_lists: false, // Inserting a word document requires this property paste_webkit_styles: 'all', paste_merge_formats: true, nonbreaking_force_tab: false, paste_auto_cleanup_on_paste: false, // CONFIG: Font fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px', // CONFIG: StyleSelect style_formats: [ { title: 'text-indent', block: 'p', styles: { 'text-indent': '2em' } }, { title: 'Row Height', items: [ {title: '1', styles: { 'line-height': '1' }, inline: 'span'}, {title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'}, {title: '2', styles: { 'line-height': '2' }, inline: 'span'}, {title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'}, {title: '3', styles: { 'line-height': '3' }, inline: 'span'} ] } ], // FontSelect font_formats: `Microsoft Yahei=Microsoft Yahei; Song Style=Song Style; Black Style=Black Style; Song Style=Song Style; Kai Style=Kai Style; Lishu=Lishu; Youyuan=Youyuan; Andale Mono=andale mono,times; Arial=arial, helvetica, sans-serif; Arial Black=arial black, avant garde; Book Antiqua=book antiqua,palatino; Comic Sans MS=comic sans ms,sans-serif; Courier New=courier new,courier; Georgia=georgia,palatino; Helvetica=helvetica; Impact=impact,chicago; Symbol=symbol; Tahoma=tahoma,arial,helvetica,sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms,geneva; Verdana=verdana,geneva; Webdings=webdings; Wingdings=wingdings,zapf dingbats`, // Tab tabfocus_elements: ':prev,:next', object_resizing: true, // Image imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions' } } }, props: { value: { default: '', type: String }, config: { type: Object, default: () => { return { theme: 'modern', height: 300 } } }, url: { default: '', type: String }, accept: { default: 'image/jpeg, image/png', type: String }, maxSize: { default: 2097152, type: Number }, withCredentials: { default: false, type: Boolean } }, mounted () { this.init() }, beforeDestroy () { // Destroy TinyMCE this.$emit('on-destroy') window.tinymce.remove(`$#{this.Id}`) }, methods: { init () { const self = this this.Editor = window.tinymce.init({ // Default configuration...this.DefaultConfig, //Picture upload images_upload_handler: function (blobInfo, success, failure) { if (blobInfo.blob().size > self.maxSize) { failure('File size is too large') } if (self.accept.indexOf(blobInfo.blob().type) >= 0) { uploadPic() } else { failure('Picture format error') } function uploadPic () { const xhr = new XMLHttpRequest() const formData = new FormData() xhr.withCredentials = self.withCredentials xhr.open('POST', self.url) xhr.onload = function () { if (xhr.status !== 200) { // Throw 'on-upload-fail' hook self.$emit('on-upload-fail') failure('Upload failed: ' + xhr.status) return } const json = JSON.parse(xhr.responseText) // Throw 'on-upload-complete' hook self.$emit('on-upload-complete' , [ json, success, failure ]) } formData.append('file', blobInfo.blob()) xhr.send(formData) } }, // The config passed in from prop... This.config, //mounted DOM object selector: `#${this.Id}`, setup: (editor) => { // Throw 'on-ready' event hook editor.on( 'init', () => { self.loading = false self.$emit('on-ready') editor.setContent(self.value) } ) // Throw 'input' Event hook, synchronization value data editor.on( 'input change undo redo', () => { self.$emit('input', editor.getContent()) } ) } }) } } } </script>Copy Code
Just introduce component calls directly
<template> <mce-editor :config = "Config" v-model = "Value" :url = "Url" :max-size = "MaxSize" :accept = "Accept" :with-credentials = false @on-ready = "onEditorReady" @on-destroy = "onEditorDestroy" @on-upload-success= "onEditorUploadComplete" @on-upload-fail = "onEditorUploadFail" ></mce-editor> </template>Copy Code
But as a good programmer, how can this be enough?
Here's what packaging does
Plug into webpack
To speed up page loading, you need to solve the problem of loading too many files first. Most of the time, users do not need to load the editor's core file every time they open a page, but the editor itself needs to load content as needed. At first, you want to load each plugin as a separate component module on demand, but thisWhen it comes to modifying the source code of the editor itself, or deleting point attributes from window.tinymce, it can be cumbersome and risky, and it can have a big impact on subsequent code maintenance, so it's important to keep the cord intact.
Make changes at the back
Or vue-cli take as an example
Plug downloaded packages from the official website into the stataic folder
Then delete the cdn code from the index.html template.
Of course there are two options
Or make one Asynchronous Component , packaged separately, loaded on demand
Or bring it directly into main.js to make it a giant
So I chose the former,
First introduce the old rules into the editor body
Import'. /. /static/tinymce/tinymce.min.js'Copy Code
Then refresh the next page, it should be no surprise that Uncaught SyntaxError: Unexpected token <
Friends with sharp eyes should know what's wrong with theme.js:1
In the default configuration, the path to the theme loaded by tinymce is exactly this
Request URL:http://localhost:8080/themes/modern/theme.js
Then I ran to the official website and searched the api for only one name document_base_url 's api, but based on years of programmer intuitive experience, I've told me that it's not (well, I'm stuck here), and I've dumped all the literature on the Internet, but none of it.
Then what shall I do?
So I ran to see the source code...but 40,000 lines...That's it...
Then I printed the tinymce object in the console and found a string object called baseURL, well, hopefully.
Searched the source for the following baseURL
Jump out of this code.... There are lots of sections to calculate...
The general idea is to take a baseURL out of the current URI and just change it
Window.tinymce.baseURL ='/static/tinymce'Copy Code
If the address you need to load is another path such as your own company's cdn, you can just change to the full path
window.tinymce.baseURL = 'http://cdn.xxx.com/static/tinymce'Copy Code
Problem solving seemingly path
But new problems arise,
All plugins come with min, but all plugins loaded by default do not have min. It must be my source code that has not been carefully read.
Then I searched the code again
if (!baseURL && document.currentScript) { src = document.currentScript.src; if (src.indexOf('.min') != -1) { suffix = '.min'; } baseURL = src.substring(0, src.lastIndexOf('/')); }Copy Code
Hopefully right now, it looks like the business I'm loading is importing directly into the module, so a default value called suffix is empty, so I add another line of code:
Window.tinymce.suffix ='.min'copy code
Success!
Look, super simple is it? You don't need to change the source code at all. Don't trust what you say on the Internet and don't trust it. Most object-oriented things just change their default values.
Yes, remember the language pack in front of you.
Down to the / static/tinymce/langs folder
Then delete
Import'. /zh_CN.js'Copy Code
This line of code
Place a new configuration item in DefaultConfig
Language:'zh_CN'copy code
Okay, then there's module packaging.
Pack
One problem with the previous package is that the default configuration is to load the tinyMce ontology, which can cause the package to be approximately 500k in size. If this component is not handled as an asynchronous load, it can be a disaster for some businesses.Although this opens with only one file loaded, the business is more stable.
But I don't think it's elegant, so I finally carried it out alone.
Likewise, depending on the nature of the library itself, it is perfectly possible to unify all the necessary plugins into one package and load them directly as needed.In this way, we have an additional plugins package of several hundred kilograms.
The plugins and tinyMce body packages are then lazily loaded and cached in advance without blocking the page load for later use, while the component itself is a way to listen for window.tinymce global variables before mounting, and the cdn controls the expiration time of the following files.