富文本编辑器-按需加载版

作者:root



/** console.trace()*/
var log=console.log;
var OakEditor = ((editor,path)=>{return{init(cfg){
    return new editor(cfg,path);
}}})(class {
    constructor(cfg,PATH){
        this.tool=['heading','|','bold','italic','createlink','insertunorderedlist','insertorderedlist','createimg','blockquote','table','undo','redo','code'],
        this.balloon=!1,
        this.br=this.creatNode('<p></br>'),
        this.savenode=null,
        this.editnode=null,
        this.area=document.createElement('textarea'),
        this.lastRange=null,
        this.sel=null,
        this.domPath=null,
        this._Event=new WeakMap(),
        this.flag={},
        this.queue=[],
        this.path=PATH,
        this.btnExample=new Map(),
        this.MOD={},
        this.CFG=cfg,
        this._toRender=e=>this.MOD.res?this.renderer(e.currentTarget,e):this.render(e.currentTarget,e),
        this.CSS=[],
        this.hooks={
            all:{},
            add:(names, callback) =>{
                let hooks = this.hooks.all;
                Array.isArray(names)||(names=[names]);
                names.filter(i=>{
                    hooks[i] = hooks[i]||[];
                    hooks[i].push(callback);
                })
            },
            run:(name, env) =>{
                let callbacks =this.hooks.all[name];
                if (!callbacks || !callbacks.length)return;
                callbacks.filter(f=>f(env))
            }
        }
        this.selectorCode=this.CFG.selectorCode||'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'
    }
    async loadMod(mod,dir='./mod'){
        if(!this.MOD[mod])await import(`./${dir}/${mod}.js`).then(res=>Object.assign(this.MOD,res)).catch(err=>console.error(err))
    }
    _getCss(css){
        if(this.CSS.includes(css)) return;else{
            this.CSS.push(css);
            let cssPath=this.path+css,o = document.querySelector('head')||document.querySelector('body');
            o&&o.appendChild(this.creatNode('<link charset="utf-8" rel="stylesheet"  href="'+cssPath+'">'))
        }
    }
    _setExtraCss(s){
        if(this.CSS.includes(s)) return;else{
            this.CSS.push(s);
            let css,h=document.querySelector('head')||document.querySelector('body');
            if(h){
                css= document.createElement('style');
                css.innerHTML=s;
                h.appendChild(css);
            }
        }
    }
    creatNode(s){ 
        let n = document.createElement("div"); 
        n.innerHTML = s;
        return n.childNodes[0]; 
    }
    start(str){
        let ns=document.querySelectorAll(str);
        this.editList=Array.from(ns),
        this.editList.filter(i=>(i.addEventListener('click',this._toRender),this.deCode(i)))
    }
    clear(){
        this._removeRendr(),
        this.editList.filter(i=>(i.removeEventListener('click',this._toRender),this.deCode(i))),
        this.savenode=null 
    }
    _removeRendr(){
        this.hooks.run('removeRendr',this),
        this.removeCursor(this.editnode),
        this.editnode&&(
            this.savenode.innerHTML=this.editnode.innerHTML,
            this.savenode.style.display='block',
            this.toolPanle.remove()
        )
    }
    /** 对代码里的HTML标签删除和转为换行 */
    linefeed(s){
        return s.replace(/(<[^>]+>)/g,(m)=>{
            if(m.startsWith("</div")||m.startsWith("</p")||m.startsWith("<br")||m.startsWith("</li")) return'\n';else return'';
        })
    }
    /** 对编辑区里所有的代码里的HTML标签删除和转为换行 */
    deCode(n){this.rmNodeTag(Array.from(n.querySelectorAll(this.selectorCode)))}
    rmNodeTag(ary){ary.filter(i=>i.innerHTML=this.linefeed(i.innerHTML)),ary==null}
    /** 节点是否在编辑器内 */
    inRoot(n,p=!1){return p=p||this.editnode,p.compareDocumentPosition(n)&16}
    /** 保存 range 及节点路径 与 编辑内容到 area*/
    saveRange(){
        this.flag.codeNodeArray&&this.rmNodeTag(this.flag.codeNodeArray);
        this.area.textContent =this.editnode.innerHTML;
        this.sel=getSelection();
        this.domPath=this.getdomPath(this._getChangeNode());
        this.sel.rangeCount&&(this.lastRange=this.sel.getRangeAt(0));
    }
    bind(i,o){Object.defineProperty(this.flag,i,{set:(v)=>{o.setAttribute(i,v),v?o.innerHTML=this.lang[v]||v:o.innerHTML=this.lang[i]},get(){return o.getAttribute(i)},configurable: true})}
    /**
     * 根据模板生成节点
     * @param {模板} tp 
     * @param {与模板合并的参数对象} o 
     * @param {节点生成后调用的函数} f 
     */ 
    UI(tp,o={},f=null){
        let t={};
        Object.assign(t,tp,o);
        let n = document.createElement(t.tag),e = t.attributes||{};
        n.innerHTML=t.innerHTML||'';
        t.class&&n.classList.add(...t.class);
        for (let [k,v] of Object.entries(e)){n.setAttribute(k,v)}
        t.listen&&this.DOClisten(n,t.listen);
        t.listenTo&&this.listenTo(n,t.listenTo);
        (t=t.children) && (Array.isArray(t)?t.filter(i=>i&&n.appendChild(i.nodeName?i:Array.isArray(i)?this.UI(...i):this.UI(i))):n.appendChild(t.nodeName?t:this.UI(t)));
        f&&f.call(this,n);
        return n;
    }
    /**
     * 设置节点属性,
     * @param {css选择器} c
     * @param {属性} s 
     * @param {值,false 会删除属性} v  
     * @param {.class 数组} l
     */
    setAttrALL(c,s,v,l){this.editnode&&this.editnode.querySelectorAll(c).forEach(n=>{s&&v?n.setAttribute(s,v):n.removeAttribute(s),l&&n.classList.add(...l)})}
    /**
     * 异步加载按钮模块
     * @param {*} n 
     * @param {*} e 
     */
    async render(n,e){
        let promiseArr = [];
        this._getCss('oakeditor.css');
        this.btnExample.clear();
        this.tool=this.CFG.toolBar||this.tool;
        await import(`${this.path}translations/${this.CFG.lang||'zh-cn'}.js`).then(i=>{this.lang=i.lang});
        await import(`${this.path}mod/resources.js`).then(res=>{
            Object.assign(this.MOD,res);
            this.B=res.btnCalss
        }).then(()=>{this.tool.filter(n=>{
            if(n!='|'&&!this.MOD[n]){
                let p = new Promise((resolve, reject) => {
                    import(`${this.path}mod/${n}.js`).then(res=>resolve(Object.assign(this.MOD,res))).catch(err=>reject(console.error(err)))
                })
                promiseArr.push(p)
            }})
            Promise.all(promiseArr).then(() => {
                this.tool.filter(n=>this.btnExample.set(n,new this.B(this.MOD[n],n,this)))
            }).then(()=>this._setExtraCss(this.CFG.extracss||this.MOD.res.extracss)).then(()=>this.renderer(n,e)) 
        })
    }
    renderer(n,e){
        if(typeof(n) == "string") n= document.querySelector(n);
        this.flag.currentCursor=this.saveCursor(n);
        e&&this._isElement(e.target,'img')&&(this.flag.currentCursor=null)
        if(this._isElement(n, 'textarea')){
            this.area=n;
            n.parentNode.insertBefore(this._rendertool(),n);
            n.style.display='none';
            this.editnode=this.toolPanle.appendChild(document.createElement('div'));
            this.editnode.appendChild(this.br);
        }else{  
            if(n==this.savenode)return;
            else{
                /** 清除上一次的编辑环境 */
                this.savenode&&this._removeRendr(),
                this.savenode=n,
                n.style.display= 'none',
                n.parentNode.insertBefore(this._rendertool(),n),
                this.editnode=this.toolPanle.appendChild(n.cloneNode(true))
            }
        }
        this.popbody=this.toolPanle.appendChild(this.creatNode('<div class="ck ck-reset_all ck-body ck-rounded-corners"></div>')),
        this.editnode.classList.add('ck','oak_block','ck-editor__editable'),
        this.editnode.style.cssText='height:auto!important;';
        this.editnode.style.minHeight=this.CFG.height;
        this.DOClisten(this.editnode,{click:this.pop_body,input:this.saveRange,mouseup:this.saveRange,keyup:null,cut:null,paste:this._paste});
        this.toolbar.hasChildNodes()||this.DOClisten(document,{keydown:this._hotkey,click:()=>this.editnode.classList.remove('ck-focused')});
        this.tool.filter(s=>{this.btnExample.get(s).getButton&&this.toolbar.appendChild(this.btnExample.get(s).getButton)});
        this.editnode.setAttribute('contenteditable',true);
        document.execCommand("defaultParagraphSeparator", false, "p");
        this.flag.currentCursor&&this.inRoot(e.target,this.savenode)&&this.reCursor(this.flag.currentCursor);
        this.hooks.run('render',this),
        /**表格图片使用 */
        this.saveRange();
    }
    /** 热键处理函数,及禁止在<figcaption>标签内回车 */
    _hotkey(n,e){
        if(e.ctrlKey){
            if(e.key.toUpperCase()=='A'){
               e.preventDefault(),document.execCommand('selectAll',!1,!1),this.saveRange();
            }else
                for (let v of this.btnExample.values())e.key.toUpperCase()==v.hotkey&&(e.preventDefault(),v.command(n,e)) 
        }    
        e.key=='Enter'&&this._isElement(e.target,'figcaption')&&e.preventDefault();
        e.key=='Backspace'&&this.editnode.querySelectorAll('figure').forEach(i=>{i.classList.contains('ck-widget_selected')&&(e.preventDefault(),i.remove(),this.balloon.remove())});
    }
    /** 粘体回调函数 */
    _paste(n,e){
        let d=e.clipboardData;
        for (let i=0;i<d.items.length;i++){
            let item=d.items[i];
            if(item.kind==="string"&&item.type==="text/html"){
                e.preventDefault(0);
                let imgTag = ['<img','<a','</a'],
                attr=['src','href'],
                retain = ['<div','</div','<p','</p','<table','</table','<th','</th','<tr','</tr','<td','</td'],
                f =(s,a,t=!1)=>{return a.filter(i=>t|=s.startsWith(i,0)),t};
                item.getAsString((str)=>{
                    str = str.replace(/(<[^>]+>)/g, (match)=>{
                        let isImg = f(match,imgTag),
                        attrsRe=/\s+([a-z]*|data-.*?)\s*=\s*".*?"/ig;
                        if(isImg){
                            return match.replace(attrsRe,(match)=>{
                                return f(match.replace(/(^\s*)/g,''),attr)?match:'';
                            });
                        }else if(f(match,retain)){
                            return match.replace(attrsRe,'')
                        }else return '';
                    })
                    this.insertHTML(n,str)
                })
            }
        }
        setTimeout(()=>{this.insertQueue()},100)
    }
    
    /** 把未上传的远程图片加入队列 */
    insertQueue(){
        let nodes=this.editnode.querySelectorAll('img');
        for(let i=0;i<nodes.length;i++){
            let src=nodes[i].src;
            src.includes(window.location.host)||this.queue.unshift(nodes[i]);
        }
        setTimeout(()=>{this.startUp()},100)
    }
    /** 开始上传 并更新图片地址,firefox 支持本地图片粘贴上传*/
    startUp(){
        if(this.queue.length&&this.CFG.uploadURL){
            let n=this.queue.pop(),data = new FormData(),postvar=this.CFG.postVarsPerFile,
            init={method:"POST",credentials:"include",cache:"no-cache"};
            if(n.src.match('\^data:image'))
                data.append('Filedata',this.base64toblob(n.src),Date.now()+'.jpg');
            else data.append('url',n.src);
            n=this.addfigure(n);
            n.src=this.path+'translations/timg.gif';
            for(let [k,v] of Object.entries(postvar))data.append(k,v);
            init.body=data;
            fetch(this.CFG.uploadURL,init).then((res)=>{return res.json()}).then((o)=>{
                let s='';
                for(let [k,v] of Object.entries(postvar)){s+=k+'='+v+'&';}
                n.src=this.CFG.downURL+'?type=resizes&'+s+'filename='+o.filename;
                this.startUp();
            }).catch(e =>{n.remove(),console.error(e)}); 
        }
    }
    /** base64转 blob */
    base64toblob(base64String){
        let bytes = window.atob(base64String.split(',')[1]),array = []; 
        for(let i = 0; i < bytes.length; i++){ array.push(bytes.charCodeAt(i)); }
        return new Blob([new Uint8Array(array)], {type: 'image/jpeg'});
    }
    /**
     * 为了插入图片的格式统一,与图片管理插件共用
     * @param {替换的节点或新的节点} n 
     * @param {true 返回新节点的 outerHTML,false 替换页面的n节点} html 
     */
    addfigure(n,html=false){
        let figure =this.UI(this.MOD.res.TP.figure,{children:this.MOD.res.TP.figcaption});
        figure.classList.add('image');figure.setAttribute('contenteditable',false);
        if(html){
            figure.insertBefore(n,figure.firstChild);
            return figure.outerHTML 
        }else{
            let t=n.cloneNode(true);
            figure.insertBefore(t,figure.firstChild);
            n.parentNode.replaceChild(figure,n);
            return t
        }
    }
    _isElement(el, tag) {
        if(el && el.tagName && (el.tagName.toLowerCase() == tag)){
          return true;
        }
        if(el && el.getAttribute && (el.getAttribute('tag') == tag)){
          return true;
        }
        return false;
    }
     /** 返回焦点节点*/
    _getChangeNode(){
        let sel = getSelection(),range,n=null;
        if(!sel.rangeCount) return false;
        range=sel.getRangeAt(0);
        if (sel.anchorNode && (sel.anchorNode.nodeType == 3)) {
            if (sel.anchorNode.parentNode) { /* next check parentNode */
                n = sel.anchorNode.parentNode;
            }
            if (sel.anchorNode.nextSibling != sel.focusNode.nextSibling) {
                n = sel.anchorNode.nextSibling;
            }
        }
        if (this._isElement(n, 'br')) {n = null;}
        if (!n) {
            n = range.commonAncestorContainer; /* startContainer和endContainer共同的祖先节点在文档树中位置最深的那个 */
            if (!range.collapsed) {
                if (range.startContainer == range.endContainer) { /* 开始节点与结束节点是同一节点 */
                    if (range.startOffset - range.endOffset < 2) {
                        if (range.startContainer.hasChildNodes()) {
                            n = range.startContainer.childNodes[range.startOffset];
                        }
                    }
                }
            }
        }
        return n;
    }
    /** 得到n节点 至 编辑器根节点 的路径 */
    getdomPath(n) {
        let domPath = [];
        while(n&&(this.editnode.compareDocumentPosition(n)&16)){
            if (n.nodeName && n.nodeType && (n.nodeType === 1)){
                domPath.push(n);
            }
            n=n.parentNode;
        }
        if(domPath.length === 0){
            domPath[0] = this.editnode;
        }
        return domPath.reverse();
    }
    /** 把节点路径转换为 以小写tagName 为键的map */
    domPathMap(n=!1){
        let m = new Map(),p;
        n?p=this.getdomPath(n):p=this.domPath;
        return p.filter(n=>m.set(n.tagName.toLowerCase(),n)),m
    }
    _rendertool() {
        this.toolbar=this.UI(this.MOD.res.TP.toolbar);
        this.MOD.res.TP.toolPanle.children.children=this.toolbar;
        return this.toolPanle=this.UI(this.MOD.res.TP.toolPanle)
    }
    on(s,f,c=null,i=!1){
        let o={},E=this._Event;
        E.has(Object)||E.set(Object,{});
        E.get(Object)[s]||(E.get(Object)[s]=[]);
        o.callback=f;
        o.cfg=c;
        o.one =i;
        E.get(Object)[s].push(o);
    }
    del(s){delete this._Event.get(Object)[s]}
    /**
     * 在事件map里面建立以节点为健的对象,
     *  this._Event[node]:
     * {
     *      _domNode:node,
     *      _domListeners:{
     *          click:{
     *              callback:f
     *              remove:i.removeListener
     *          }
     *      }
     *  }
     * 给节点绑定事件,事件函数指向 this.fire
     * @param {node 注册事件的节点} n 
     * @param {object {系统事件:函数,}} l 
     * @param { bool 可选。指定事件是否在捕获或冒泡阶段执行。} b 
     */
    DOClisten(n,l,b=!1){
        this._Event.has(n)||this._Event.set(n,{_domNode:n})
        let E=this._Event.get(n);
        for(let [a,f] of Object.entries(l)){
            if (E._domListeners && E._domListeners[a])return;
            const i = this._createDomListener(E,a,b);
            E._domNode.addEventListener(a, i, b),
            E._domListeners || (E._domListeners = {}),
            E._domListeners[a] = {callback:f,remove:i.removeListener}
        }
    }
   /** 
    *  [{from:发出事件的节点,listen:绑定的事件, to:执行的函数,cfg:参数[未使用]}]
    * @param {node 接收事件的节点} n 
    * @param {object 或 Array 模板对象里的listenTo属性[{},{from:发出事件的节点,listen:系统事件名称,to:回调函数}]} o 
    */
    listenTo(n,l){
        let E;
        Array.isArray(l)||(l=[l])
        l.filter(o=>{
            E = this._Event.get(o.from);
            E._events||(E._events={})
            Array.isArray(o.listen)||(o.listen=[o.listen])
            o.listen.filter(i=>{E._events[i]||(E._events[i]=new Map()),E._events[i].has(n)||E._events[i].set(n,[]),E._events[i].get(n).push(o.to)})
        })
    }
    /**
     * 创建监听函数
     * 给 fire 增加删除事件绑定的函数,
     * @param {object:{_Event:n,}} E 
     * @param {string 系统事件类型} a 
     * @param {bool 可选。指定事件是否在捕获或冒泡阶段执行。} b 
     */
    _createDomListener(E,a,b){
        const t = e=>{this.fire(E._domNode,e)};
        return t.removeListener = (()=>{
            E._domNode.removeEventListener(a, t, b),
            delete E._domListeners[a],
            delete E._events[a]
        }),t
    }
    /**
     * 事件监听函数,事件发生后,遍历在此节点注册的事件类型下的函数并执行。单独使用的时与on及 once函数匹配。
     * @param {发出事件的节点。单独使用时为自定义事件名称} n 
     * @param {事件对象。单独使用时为传递给on或once函数的第二个参数||也可是事件字符串} e 
     */
    fire(n,e){
        if('string' === typeof n){
            let E=this._Event.get(Object),a;E&&E.hasOwnProperty(n)&&(a=E[n]);
            if(a)for(let [i, v] of a.entries()){v&&v.callback&&(v.callback(v.cfg,e),v.one&&a.splice(i,1))}
        }else{
            let E=this._Event.get(n),b,h,t=e;E._events&&(h=E._events),
            'string' === typeof e || (t=e.type),
            b=E._domListeners[t],
            b&&b.callback&&b.callback.call(this,n,e);
            if(h&&h[t])for(const [k,v] of h[t])v.filter(f=>f.call(this,k,e));
        }
    }
     /**
     * 用Range设置选择区
     * @param {Range} r 
     */
    setRange(r){let s=getSelection();return s.removeAllRanges(),this.lastRange=r,s.addRange(r)}
    /**
     * 改变选择区为 光标所在节点
     * @param {开关,光标在文本尾部时是否改变选择区,为真时改变} t 
     */
    SetRangeToBlock(t=1){
        let s=this.sel,a=s.anchorNode;
        if(s.isCollapsed &&a.nodeType===3){
            if(t>1) this.reSetRange(a,0,a,a.length)
            else (s.anchorOffset<a.length)&&this.reSetRange(a,0,a,a.length)
        }
    }
    /**
     * 用节点及其偏移设置选择区
     * @param {node,startContainer} a 
     * @param {startOffset} s 
     * @param {node,endContainer} f 
     * @param {endOffset} e 
     */
    reSetRange(a,s,f,e){
        let r= new Range();
        return r.setStart(a,s),r.setEnd(f,e),this.setRange(r)
    }
    /**
     * 默认恢复页面
     * @param {保存的光标} t 
     * @param {false 仅恢复光标,不恢复修改前的内容} b 
     */
    reCursor(t,b=!0){
        b&&(this.editnode.innerHTML=t.html||'<p></br></p>');
        let s=this.editnode.querySelector('.cursor-start'),
        e=this.editnode.querySelector('.cursor-end');
        s||(s=this.editnode),
        e||(e=this.editnode);
        if(t.atype==3)s=s.childNodes[t.aindex];
        if(t.ftype==3)e=e.childNodes[t.findex];
        return this.reSetRange(s,t.start,e,t.end)
    }
    /** 删除光标 */
    removeCursor(root){
        root&&(root.querySelectorAll('.cursor-start').forEach(n =>n.classList.remove('cursor-start')),
            root.querySelectorAll('.cursor-end').forEach(n =>n.classList.remove('cursor-end')))
        
    }
    /** 
     * 保存光标 支持 edge,chrome,firefox.
     * @param {要保存光标的节点,为了在切换页面时恢复光标新加} root 
     */
    saveCursor(root){
        this.removeCursor(root);
        let s=getSelection();
        if(!s.rangeCount)return !1;
        let o={},r=s.getRangeAt(0),an=r.startContainer,fn=r.endContainer,
        /** 得到n节点在父节点里的索引 */
        index=(n)=>{
            let ns=n.parentNode.childNodes;
            for(let i=0;i<ns.length;i++){if(ns[i].isSameNode(n)) return i}
            return -1
        },
        /** 计算文本节点合并后的光标所在节点与偏移位置*/
        f=(node)=>{
            let i=0,n=node;
            while(n&&n.nodeType&&n.nodeType==3){
                i+=n.length,node=n;
                n=n.previousSibling;
            }return{i,node}
        };
        o.start=r.startOffset,
        o.end=r.endOffset;
        if(an.nodeType===3){
            let t=f(an);o.start+=t.i-an.length,an=t.node
            o.aindex=index(an),
            an=an.parentNode,o.atype=3
        }else{
            let n=an.childNodes[o.start];
            if(n&&n.nodeType===3){
                let t=f(n);o.start=t.i-n.length,
                an=t.node,
                o.aindex=index(an),
                an=an.parentNode,o.atype=3
            }
        }
        if(fn.nodeType===3){
            let t=f(fn);o.end+=t.i-fn.length,fn=t.node
            o.findex=index(fn),
            fn=fn.parentNode,o.ftype=3
        }else{
            let n=fn.childNodes[o.end-1]
            if(n&&n.nodeType===3){
                let t=f(n);o.end=t.i,fn=t.node,
                o.findex=index(fn),
                fn=fn.parentNode,o.ftype=3
            }
        }
        root.compareDocumentPosition(an)&16&&(an.classList.add('cursor-start'),fn.classList.add('cursor-end'));
        return o.html=root.innerHTML,o
    }
    wrap(tag,n=!1){
        let ary=[],b=document.createElement(tag),ns,r,f,a,s,e;
        n?(r=new Range(),r.selectNode(n)):r=this.lastRange;
        f=r.endContainer,a=r.startContainer;
        if(a.isSameNode(f)){
            r.surroundContents(b)
        }else{
            ns=r.commonAncestorContainer.childNodes;
            for (let i=0; i<ns.length; i++){
                if(ns[i].compareDocumentPosition(a)&16||ns[i].isSameNode(a))s=i;
                if(ns[i].compareDocumentPosition(f)&16||ns[i].isSameNode(f))e=i;
            }
            for (let i=s; i<=e; i++)ary.push(ns[i]);
            ary.filter(n=>b.appendChild(n)),
            r.insertNode(b);
        }
        return b
    }
    /**
     * 拆包
     * @param {被拆掉的节点} n 
     */
    unwrap(n){
        let l,r=new Range(),t=this.lastRange;
        r.selectNodeContents(n),
        l = r.extractContents(),
        t.selectNode(n),
        t.deleteContents(),
        t.insertNode(l);
        /** edge 专用 */
        this.setRange(t)
    }
    pop_body(n,e){
        this.editnode.classList.add('ck-focused');
        e&&(e.stopPropagation(),this.domPath.includes(e.target)||this.domPath.push(e.target))
        this.pop_remove();
        this.domPath.filter(i=>{
            let t='pop_'+i.tagName.toLowerCase();
            this.MOD[t]&&this.MOD[t]().flip&&this.MOD[t](this).flip(i);
        });
        e&&this.figcaption(e)
    }
    /**
     * 把气泡移动到 n 节点附近
     * @param {气泡内节点} t 
     * @param {光标所在节点} n
     * @param {给光标所在节点添加的css} css1
     * @param {给balloon节点添加的css,用于this.execCommand,避免删除pop} css2
     */
    pop_move(t,n,css=!1){
        t&&(this.pop_remove(),
            this.balloon=this.popbody.appendChild(this.UI(this.MOD.res.TP.balloon)),
            this.balloon.appendChild(t))
        let r,fcN=this._getChangeNode().firstChild;
        n?r=n.getBoundingClientRect():r=this.lastRange.getBoundingClientRect();
        r.top==0&&r.left==0&&(fcN&&(r=fcN.getBoundingClientRect()));
        let HalfH=window.screen.height/2,pc=this.balloon.offsetWidth/2,
            HalfW=window.screen.width/3,
            pl=this.balloon.offsetLeft,
            ph=this.balloon.offsetHeight,
            c='n',s='w',x=window.scrollX,y=window.scrollY,
            l=r.left+(r.right-r.left)/2+x,
            b=r.bottom+10+y;
        if(n){
            let nc=(r.left+r.right)/2;
            nc>HalfW?(s='',l=nc-pc-pl):l=nc-25,
            n.offsetTop>HalfH&&(c='s',b=n.offsetTop-ph-10),
            css&&n.classList.add(css)
        }else{
            if(r.left>HalfW){s='',l=l-pc-pl}else l=l-25;
            if(r.top>HalfH){c='s',b=r.top-ph-10+y}
        }
        this.balloon.classList.add(`ck-balloon-panel_arrow_${c+s}`),//css2 会出现 null 
        this.balloon.style.left=l+'px',
        this.balloon.style.top=b+'px'
    }
    /** 气泡删除 */
    pop_remove(){//
        this.balloon&&this.balloon.remove();
        this.MOD.res.remove_css&&this.MOD.res.remove_css.filter(s=>{this.editnode.querySelectorAll('.'+s).forEach(n=>{n.classList.remove(s)})})
    }
    figcaption(e){
        this.editnode.querySelectorAll('figcaption').forEach(n=>(n.parentNode.compareDocumentPosition(e.target)&16)||n.innerHTML.length&&(!new RegExp(/<br\s*\/*>/i).test(n.innerHTML))||n.classList.add('ck-hidden'))
    }
    renametag(n,v){
        let t=document.createElement(v),ns=n.attributes,r=this.saveCursor(this.editnode);
        t.innerHTML=n.innerHTML;
        for(let i=0;i<ns.length;i++){
            ns[i].value&&t.setAttribute(ns[i].name,ns[i].value)
        }
        n.parentNode.replaceChild(t,n);
        return this.reCursor(r,!1),t
    }
    insertHTML(n,v){//document.execCommand('insertHTML', !1, v),
        let r=new Range();
        n= document.createElement('div'),
        n.innerHTML=v,
        r.selectNodeContents(n),
        n = r.extractContents(),
        this.lastRange.insertNode(n)
        this.fire('savechange')
    }
},document.scripts[document.scripts.length - 1].src.substring(0, document.scripts[document.scripts.length - 1].src.lastIndexOf("/") + 1));