Use Tinymce in Vue project to solve image upload/paste

Preface
Recently, because the backend management side of the company’s project needs to implement an editor function, on the one hand, it can meet the needs of editing various article content, and on the other hand, it needs to edit some course-related introductions by itself, so I spent some time comparing and experiencing some existing open source tools. editor.

Simple comparison between editors
UEditor: Basically meets various needs. It relies on jquery but it is no longer maintained. To upload images, etc., you need to modify the source code. The interface is not very beautiful. It is compatible with old browsers, but I use VueJS for development here, so give up

wangEditor: It is relatively lightweight. The most important thing is that it has Chinese documents and is quick to use. The UI is also relatively beautiful, and it is also made in China. For those who have little demand for editor functions, I can consider it, but considering that my project is relatively heavy-duty. , so I had to give up

Bootstrap-wysiwyg: simple and beautiful, relies on Bootstrap, jquery, and discards the selected Element-ui

TinyMCE: Supports online image processing, has many plug-ins and powerful functions. Well, just choose it (although the document is in English, Google Translate is also good?)

The requirements that our project needs to solve are not complicated, but they are very annoying, such as:

1. Implement image upload (basic function)

2. Simulate mobile phone preview function (basic function)

3. The edited content needs to be adapted when displayed in the app

4. The content copied from 135 Editor, Xiumi and other editors must be displayed normally and the layout must be maintained. These third-party images must also be uploaded to your own service (for fear that the third party will remove the images)

Import and initialize
Import tinymace files
The project is built using [email protected], download TinyMCE in the same directory as index.html, and introduce TinyMCE in index.html

<script src=./tinymce4.7.5/tinymce.min.js></script>

initialization
After introducing the file, initialize TinyMCE on the html element. Since TinyMCE allows replaceable elements to be identified through CSS selectors, we only need to pass the object containing the selector to TinyMCE.init(). The code is as follows:

<template>
<div class="tinymce-container editor-container">
<textarea :id="tinymceId" class="tinymce-textarea" />
</div>
</template>
<script>
export default {<!-- -->
    name: 'Tinymce',
    data() {<!-- -->
        return {<!-- -->
            tinymceId: this.id
        }
     },
    mounted(){<!-- -->
        this.initTinymce()
    },
    methods: {<!-- -->
        initTinymce() {<!-- -->
             window.tinymce.init({<!-- -->
                selector: `#${<!-- -->this.tinymceId}`
                        })
                }
        }}
</script>

In this way, the textarea is replaced with the TinyMCE editor instance, and the simplest initialization is completed.

Configuration items
The next step is to add configuration items to enrich the functions of the TinyMCE editor.

Basic configuration
Regarding the basic configuration, I will not introduce it one by one. There are detailed instructions in the document. If your English is as weak as mine, you can use Chrome’s translation and you can probably understand it.

The following is the script content of the encapsulated component. Some configurations are explained directly in the code:

import plugins from '@/components/Tinymce/plugins'
import toolbar from '@/components/Tinymce/toolbar'
import {<!-- -->
    uploadFile
} from '@/api/file/upload'
export default {<!-- -->
    name: 'Tinymce',
    props: {<!-- -->
        tid: {<!-- -->
            type: String,
            default: 'my-tinymce-' + new Date().getTime() + parseInt(Math.random(100))
        },
        content: {<!-- -->
            type: String,
            default: ''
        },
        menubar: {<!-- --> // menu bar
            type: String,
            default: 'file edit insert view format table'
        },
        toolbar: {<!-- --> // Toolbar
            type: Array,
            required: false,
            default () {<!-- -->
                return []
            }
        },
        height: {<!-- -->
            type: Number,
            required: false,
            default: 360
        }
    },
    data() {<!-- -->
        return {<!-- -->
            tinymceId: this.tid,
            finishInit: false,
            hasChanged: false,
            config: {<!-- -->}
        }
    },
    mounted() {<!-- -->
        this.initTinymce()
    },
    methods: {<!-- -->
        initTinymce() {<!-- -->
            window.tinymce.init({<!-- -->
                selector: `#${<!-- -->this.tinymceId}`,
                ...this.config,
                content_style: 'img {max-width:100% !important }', // Initialization assignment
                init_instance_callback: editor => {<!-- -->
                    if (this.content) {<!-- -->
                        editor.setContent(this.content)
                    }
                    this.finishInit = true
                    editor.on('NodeChange Change SetContent KeyUp', () => {<!-- -->
                        this.hasChanged = true
                    })
                }, // upload image
                images_upload_handler: (blobInfo, success, failure) => {<!-- -->
                    const formData = new FormData();
                    formData.append('file', blobInfo.blob());
                    uploadFile(formData).then(res => {<!-- -->
                        if (res.data.code == 0) {<!-- -->
                            let file = res.data.data;
                            success(file.filePath);
                            return
                        }
                        failure('Upload failed')
                    }).catch(() => {<!-- -->
                        failure('Upload error')
                    })
                }
            })
        }
    }
}

The component initialization is completed, and the editing box is as shown in the figure:

config content

In order to facilitate reading, the config content is extracted here and displayed separately. I also annotated some configuration items to facilitate understanding:

config: {<!-- -->
    language: 'zh_CN',
    height: this.height,
    menubar: this.menubar, //Menu: Specify which menus should appear
    toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar, // Group toolbar controls
    plugins: plugins, // Plugins (for example: advlist | link | image | preview, etc.)
    object_resizing: false, // Whether to disable table image resizing
    end_container_on_empty_block: true, // enter key block
    powerpaste_word_import: 'merge', // Whether to retain the word paste style clean | merge
    code_dialog_height: 450, // Code box height and width
    code_dialog_width: 1000,
    advlist_bullet_styles: 'square' // Unordered list Ordered list
}

toolbar.js

The toolbar.js file introduced in the component stores the toolbar controls loaded when TinyMCE is initialized. The order of settings represents the order in which they appear on the editor toolbar.

const toolbar = ["searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample", "hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen" ];
export default toolbar;

plugin.js

The plugin.js file introduced in the component is to set which plugins are loaded when TinyMCE is initialized. By default, TinyMCE will not load any plugins:

const plugins = ["advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools importcss insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount"];
export default plugins;

upload image
TinyMCE provides the image upload processing function images_upload_handler. This function has three parameters: blobInfo, success callback, failure callback, which are the image content, a successful callback function and a failure callback function. The specific code for uploading images has been written above, here I won’t go into details; it should be noted that after uploading the image to the background, we need to call the success function to replace the src attribute of the tag with the server address.

succuss(service image address);

I originally thought that uploading the image would be done, but the product partner said: “Can’t you copy and paste this image directly? It’s better to click upload every time!!” Then continue to add the copy and paste function. !

Drag/paste images
In fact, it is not difficult to paste pictures. The paste plug-in has been loaded before. Next, you only need to insert the configuration items in the initialization:

 paste_data_images: true, // Setting to "true" will allow pasting of images, while setting it to "false" will not allow pasting of images.

But I spent an hour doing this, because I couldn’t paste it, so I have to mention this pitfall: because I use Chrome for development, the Chrome browser cannot paste the image directly in the file. , but it can be pasted from the WeChat input box or other places, or dragged in. I have not yet further made the compatibility of pasting in the Chrome browser, and I will have time to do it later.

Image processing comes to an end~

About preview
TinyMCE is configured with the preview plug-in preview, which was also added in plugin.js earlier. However, our need is to achieve preview in mobile mode, so we also need to set the width and height of the preview content.

 plugin_preview_width: 375, // Preview width plugin_preview_height: 668,

After setting the preview, I found that the image was larger than the preview width, and scrolling occurred, so I found a content_style attribute. You can set the css style, inject the style into the editor, and set the following attributes in the initialization:

content_style: ` * {<!-- --> padding:0; margin:0; } img {<!-- -->max-width:100% !important }`,

So the simulated mobile phone preview was completed, but after the content was submitted, the image viewed on the mobile phone was still very large. The reason was that I ignored what the official document said: these styles will not be saved together with the content.

So I spliced this style string to the content when submitting the code

content + = `<style>* {<!-- --> padding:0; margin:0; } img {<!-- -->max-width:100% !important }</style> `

Third-party editor content copy
I also mentioned above the need for content copying from third-party editors. The content must be copied so that the layout remains unchanged, and the image content must be uploaded to our own server.

  1. For 135 editor

The 135 editor supports copying html code, which can keep the layout style unchanged by directly pasting it into the code. The idea for processing image addresses is as follows:

1. Set up a whitelist for your own server

2. Pass the image link addresses that are not in the whitelist on the page to the backend, and let the backend put these images on its own server and return me the new image link.

3. Then I will update the corresponding image link;

This mainly involves:

Find all image links

Update the corresponding image link

Originally, I planned to use regular expressions to find pictures, obtain the content returned by the server, and then use regular expression matching to replace it. Later I found that TinyMCE provides urlconverter_callback to handle url replacement. It has four parameters: url, node, an_save, name, which are mainly used. is the url address to be replaced, and this method returns the replaced url address;

This is how I do it:

urlconverter_callback: (url, node, on_save, name) => {<!-- --> //Set the whitelist
    const assignUrl = ['http://192.168.1.49', 'https://lms0-1251107588']
    let isInnerUrl = false //The default is not an internal link
    try {<!-- -->
        assignUrl.forEach(item => {<!-- -->
            if (url.indexOf(item) > -1) {<!-- -->
                isInnerUrl = true
                throw new Error('EndIterate')
            }
        })
    } catch (e) {<!-- -->
        if (e.message != 'EndIterate') throw e
    }
    if (!isInnerUrl) {<!-- -->
        replaceUrl(url).then(result => {<!-- -->
            if (result.data.code == 0) {<!-- -->
                this.newImgUrl.push(result.data.data)
            }
        })
    }
    return url
},

This step only implements sending the image address outside the whitelist to the server, receiving and saving the returned address. You may be curious why not directly use the return value to replace the image address here?

Since this function does not provide a callback function, when a new address is retrieved from the server asynchronously, the URL returned by renturn will not wait for anyone. I tried using await to solve the problem, but found that it does not support asynchronous processing, so I had to give up. Use this method to change the direction, allowing the user to match and replace the content when they click save.

if (!this.newImgUrl.length) return this.content // Match and replace the src image path in img
this.content = this.content.replace(/<img [^>]*src=['"]([^'"] + )[^>]*>/gi, (mactch, capture) => {<!-- -->
    let current = ''
    this.newImgUrl.forEach(item => {<!-- -->
        if (capture == item.oriUrl) {<!-- -->
            current = item.filePath
        }
    })
    current = current ? current : capture
    return mactch.replace(/src=[\'"]?([^\'"]*)[\'"]?/i, 'src =' + current)
}) // Match and replace the url path in any html element
this.content = this.content.replace(/url\(['"](. + )['"]\)/gi, (mactch, capture) => {<!-- - ->
    let current = ''
    this.newImgUrl.forEach(item => {<!-- -->
        if (capture == item.oriUrl) {<!-- -->
            current = item.filePath
        }
    })
    current = current ? current : capture;
    return mactch.replace(/url\((['"])(. + )(['"])\)/i, `url($1${<!-- -->current }$3) `)
})
return content

Finally, the replaced content is sent to the backend. The use of the TinyMce editor comes to an end here. Thank you for reading carefully. I hope it will be helpful to you. I will update you if there are new functions or new content in the future. updated.

Reference article: http://blog.ncmem.com/wordpress/2023/10/17/Use tinymce in vue project to solve image upload-paste/
Welcome to join the group to discuss