Vue's Rich Text

Keywords: Javascript JSON Vue JQuery Mobile

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:


  1. word document is pasted in to be formatted
  2. Compatible Mobile End
  3. word documents are pasted in to display properly and to be compatible with the mobile side
  4. The content pasted into the computer web page should be displayed properly and the layout should not be messy
  5. 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.

Posted by d0rr on Fri, 20 Sep 2019 19:42:33 -0700