Skip to main content

TinyMCE编辑器远程图片本地化

1、设置初始化结束后执行的回调参数

init_instance_callback : (editor) => {
// 这里边放置下面的js脚本方法
}

2、首先检测是否需要上传以及白名单域名

// 设置白名单域名,不需要转换的图片域名
const localDomains = ['cfyun.cc', 'cfyun.top'];

// 检测是否需要上传
let test = function test(url) {
    if (url.indexOf(location.host) !== -1 || /(^\.)|(^\/)/.test(url)) {
        return !0;
    }
    // 白名单
    if (localDomains) {
        for (let domain in localDomains) {
            if (localDomains.hasOwnProperty(domain) && url.indexOf(localDomains[domain]) !== -1) {
                return !0;
            }
        }
    }
    return !1;
};

2、文章内容变动时将文章中远程图片本地化

// 粘贴远程图片本地化
editor.on('Change', (e) => {
    let remoteImages = [];
    const doc = editor.getDoc();
    const items = doc.getElementsByTagName('img');

    if(items.length) {
        for (img of items) {
            var src = img.getAttribute('_src') || img.src || '';
            if (/^(https?|ftp):/img.test(src) && !test(src)) {
                remoteImages.push(src);
            }
        }
    }

    if (remoteImages.length) {
        // 需要上传的图片
        CatchRemoteImage(remoteImages, {
            success: (data) => {
                // 这部分代码本来是直接修改编辑器内的dom就可以实现效果的, 但是那种方式无解, 只能现在这样替换原有img的dom才能更新编辑器
                // 貌似编辑器diff验证了dom节点是否相同, 如果相同则没有更新编辑器的内容
                // 希望有大佬能有更好的解决方案
                // 现在的遗憾就是在上传完成后替换为本地的图片后, 有一段加载图片的时间, 这时候编辑器内图片会留白一段时间, 留白时间取决于网速
                let i, o, item, res, _src, __src, list = data.list;
                for (i = 0; item = items[i++];) {
                    _src = item.getAttribute('_src') || item.src || '';
                    for (o = 0; res = list[o++];) {
                        if (_src == res.source && res.state == 'success') {// 抓取失败时不做替换处理
                            __src = res.url;
                            // 无奈之举, 直接修改dom编辑器不更新内容, 但是替换dom可以更新
                            // 这里用到了jquery, 其他框架换个替换dom的方法
                            // $(``).replaceAll($(item));
                            // 下面给出一种从官方代码中参考来的方式
                            replaceImage(item, res); // 使用下面的新方法
                            break;
                        }
                    }
                }
                // 同步到textarea
                editor.save();
            },
            error: (data) => {
                // 上传出错回调
            }
        });
    }
});

3、从tinymce源码中参考的替换图片src的方式

此种方法, 不会造成图片上传之前的留白现象

const replaceImage = (image, data) => {
    const each = function (xs, f) {
        for (var i = 0, len = xs.length; i < len; i++) {
            var x = xs[i];
            f(x, i);
        }
    };
    const map = function (xs, f) {
        var len = xs.length;
        var r = new Array(len);
        for (var i = 0; i < len; i++) {
            var x = xs[i];
            r[i] = f(x, i);
        }
        return r;
    };
    const replaceString = function (content, search, replace) {
        let index = 0;
        do {
            index = content.indexOf(search, index);
            if (index !== -1) {
                content = content.substring(0, index) + replace + content.substr(index + search.length);
                index += replace.length - search.length + 1;
            }
        } while (index !== -1);
        return content;
    };
    const replaceImageUrl = function (content, targetUrl, replacementUrl) {
        let replacementString = 'src="' + replacementUrl + '"' + (replacementUrl === 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' ? ' data-placeholder="1"' : '');
        content = replaceString(content, 'src="' + targetUrl + '"', replacementString);
        return content;
    };
    const replaceUrlInUndoStack = function (targetUrl, replacementUrl) {
        each(editor.undoManager.data, function (level) {
            if (level.type === 'fragmented') {
                level.fragments = map(level.fragments, function (fragment) {
                    return replaceImageUrl(fragment, targetUrl, replacementUrl);
                });
            } else {
                level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
            }
        });
    };
    let src = editor.convertURL(data.url, 'src');
    let attr = {
        'src': data.url,
        'data-app': data.app,
        'data-aid': data.aid
    };
    if(data.db) {
        attr['data-db'] = data.db;
    } else {
        attr['data-width'] = data.width;
        attr['data-height'] = data.height;
    }
    replaceUrlInUndoStack(image.src, data.url);
    editor.$(image).attr(attr).removeAttr('alt').removeAttr('data-mce-src');
}

4、上传回调方法

/**
 * 远程图片本地化
 */
let CatchRemoteImage = (images, callback)  => {
    const _this = this;

    let data = new FormData();

    // 经过FormData处理后提交给后端的images数组是以,分割的字符串, 这个需要后端自己转为数组再处理
    data.append('urls', images);

    // Append add Form Data
    // formData是需要提交给后端的参数
    // $.each(formData, function(aKey, aVal) {
    //     if(typeof aVal == 'object') { // 如果是object则用jquery的val方法取得内容
    //         aVal = aVal.val();
    //     }
    //     data.append(aKey, aVal);
    // });
    // jquery的ajax上传可以替换为其他ajax方法, 或者使用原生XMLHttpRequest方法
    $.ajax({
      url: '/admin123/blog/imageUpload', // 接收远程图片抓去的后端地址
      method: 'post',
      data: data,
      cache: !1,
      contentType: !1,
      processData: !1,
      forceSync: !1,
      beforeSend: function(jqXHR, settings) {
        jqXHR.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
      }
    }).then((res) => {
        if(res.errno == 0) {
            callback.success(res.data);
            return !1;
        }
        callback.error(res.data);
    }).catch((error) => {
        callback.error('error');
    });
};

5、图片上传的接口返回要求

{
    "errno":0,
    "data":{
        "list":[
            {
                "state":"success",
                "url":"//img.cfyun.cc/apps/news/remote/Mon_2103/1616069544468119.png",
                "_url":"//img.cfyun.cc/apps/news/remote/Mon_2103/1616069544468119.png",
                "path":"apps/news/remote/Mon_2103/1616069544468119.png",
                "name":"1616069544468119.png",
                "original":"excalidraw.png",
                "size":84931,
                "ext":".png",
                "is_thumb":0,
                "is_image":1,
                "app":"news",
                "width":1280,
                "height":669,
                "db":52.265625,
                "aid":190,
                "source":"https://gitee.com/kevwan/static/raw/master/doc/images/excalidraw.png"
            }
        ]
    },
    "type":"right",
    "message":"success",
    "referer":"",
    "refresh":false
}

实例

在dcat admin 框架form表单中使用tinymce编辑器实例代码:

$form->editor('content_html')->disk('public')->imageDirectory(FunctionHelper::editorImgPath())->saving(function ($v) {
                return str_replace(doConfig('filesystem.original_domain'), doConfig('filesystem.cdn_url'), $v);
            })->options([
                'init_instance_callback' => JavaScript::make(
                    <<<JS
            function (editor) {
                // 设置不需要转换的图片域名
                const localDomains = ['localhost:8000', '127.0.0.1:8000'];
    
                // 检测是否需要上传
                let test = function test(url) {
                    if (url.indexOf(location.host) !== -1 || /(^\.)|(^\/)/.test(url)) {
                        return !0;
                    }
                    // 白名单
                    if (localDomains) {
                        for (let domain in localDomains) {
                            if (localDomains.hasOwnProperty(domain) && url.indexOf(localDomains[domain]) !== -1) {
                                return !0;
                            }
                        }
                    }
                    return !1;
                };

                // 粘贴远程图片本地化
                editor.on('Change', (e) => {
                    let remoteImages = [];
                    const doc = editor.getDoc();
                    const items = doc.getElementsByTagName('img');
    
                    if(items.length) {
                        for (img of items) {
                            var src = img.getAttribute('_src') || img.src || '';
                            if (/^(https?|ftp):/img.test(src) && !test(src)) {
                                remoteImages.push(src);
                            }
                        }
                    }
    
                    if (remoteImages.length) {
                        // 需要上传的图片
                        CatchRemoteImage(remoteImages, {
                            success: (data) => {
                                console.log(data);
                                // 这部分代码本来是直接修改编辑器内的dom就可以实现效果的, 但是那种方式无解, 只能现在这样替换原有img的dom才能更新编辑器
                                // 貌似编辑器diff验证了dom节点是否相同, 如果相同则没有更新编辑器的内容
                                // 希望有大佬能有更好的解决方案
                                // 现在的遗憾就是在上传完成后替换为本地的图片后, 有一段加载图片的时间, 这时候编辑器内图片会留白一段时间, 留白时间取决于网速
                                console.log(data.list);
                                let i, o, item, res, _src, __src, list = data.list;
                                for (i = 0; item = items[i++];) {
                                    _src = item.getAttribute('_src') || item.src || '';
                                    for (o = 0; res = list[o++];) {
                                        if (_src == res.source && res.state == 'success') {// 抓取失败时不做替换处理
                                            __src = res.url;
                                            // 无奈之举, 直接修改dom编辑器不更新内容, 但是替换dom可以更新
                                            // 这里用到了jquery, 其他框架换个替换dom的方法
                                            // $(``).replaceAll($(item));
                                            // 下面给出一种从官方代码中参考来的方式
                                            replaceImage(item, res); // 使用下面的新方法
                                            break;
                                        }
                                    }
                                }
                                // 同步到textarea
                                editor.save();
                            },
                            error: (data) => {
                            // 上传出错回调
                        }
                        });
                    }
                });

                const replaceImage = (image, data) => {
                    const each = function (xs, f) {
                        for (var i = 0, len = xs.length; i < len; i++) {
                            var x = xs[i];
                            f(x, i);
                        }
                    };
                    const map = function (xs, f) {
                        var len = xs.length;
                        var r = new Array(len);
                        for (var i = 0; i < len; i++) {
                            var x = xs[i];
                            r[i] = f(x, i);
                        }
                        return r;
                    };
                    const replaceString = function (content, search, replace) {
                        let index = 0;
                        do {
                            index = content.indexOf(search, index);
                            if (index !== -1) {
                                content = content.substring(0, index) + replace + content.substr(index + search.length);
                                index += replace.length - search.length + 1;
                            }
                        } while (index !== -1);
                        return content;
                    };
                    const replaceImageUrl = function (content, targetUrl, replacementUrl) {
                        let replacementString = 'src="' + replacementUrl + '"' + (replacementUrl === 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' ? ' data-placeholder="1"' : '');
                        content = replaceString(content, 'src="' + targetUrl + '"', replacementString);
                        return content;
                    };
                    const replaceUrlInUndoStack = function (targetUrl, replacementUrl) {
                        each(editor.undoManager.data, function (level) {
                            if (level.type === 'fragmented') {
                                level.fragments = map(level.fragments, function (fragment) {
                                    return replaceImageUrl(fragment, targetUrl, replacementUrl);
                                });
                            } else {
                                level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
                            }
                        });
                    };
                    let src = editor.convertURL(data.url, 'src');
                    let attr = {
                        'src': data.url,
                        'data-app': data.app,
                        'data-aid': data.aid
                    };
                    if(data.db) {
                        attr['data-db'] = data.db;
                    } else {
                        attr['data-width'] = data.width;
                        attr['data-height'] = data.height;
                    }
                    replaceUrlInUndoStack(image.src, data.url);
                    editor.$(image).attr(attr).removeAttr('alt').removeAttr('data-mce-src');
                }

                /**
                 * 远程图片本地化
                 */
                let CatchRemoteImage = (images, callback)  => {
                    const _this = this;
    
                    let data = new FormData();
    
                    // 经过FormData处理后提交给后端的images数组是以,分割的字符串, 这个需要后端自己转为数组再处理
                    data.append('urls', images);
    
                    // Append add Form Data
                    // formData是需要提交给后端的参数
                    // $.each(formData, function(aKey, aVal) {
                    //     if(typeof aVal == 'object') { // 如果是object则用jquery的val方法取得内容
                    //         aVal = aVal.val();
                    //     }
                    //     data.append(aKey, aVal);
                    // });
                    // jquery的ajax上传可以替换为其他ajax方法, 或者使用原生XMLHttpRequest方法
                    $.ajax({
                        url: '/admin123/blog/imageUpload', // 接收远程图片抓去的后端地址
                        method: 'post',
                        data: data,
                        cache: !1,
                        contentType: !1,
                        processData: !1,
                        forceSync: !1,
                        beforeSend: function(jqXHR, settings) {
                        jqXHR.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
                    }
                    }).then((res) => {
                        if(res.code == 200) {
                            callback.success(res.data);
                            return !1;
                        }
                        callback.error(res.data);
                    }).catch((error) => {
                        callback.error('error');
                    });
                };
            }
JS
                ),
            ]);