当前位置:

首页 > 编程开发 > Html 编辑器粘贴内容过滤技术详解

Html 编辑器粘贴内容过滤技术详解

本文目录

    针对HTML编辑器粘贴过滤需求,对比CKEditor与TinyMCE。CKEditor需手动设置Firefox权限,且无法保留Word格式;TinyMCE通过创建临时iframe(IE)或div(Firefox)绕过权限,保留格式,实现无感过滤。

    最近在解决数据粘贴方面取得了不少进展,这是HTML在线编辑器所必须具备的技术。这里详细给大家介绍整个过程,并提供实现参考。研究过程中确实走了不少弯路,尝试了多种方式——由于美国的PM始终觉得有些影响用户体验的东西无法接受,导致好几个提案被否掉,不过收获还是相当丰富的。

    现在写代码喜欢需求驱动,先来看看这项技术的主要需求:

    • 能够过滤用户贴进来的纯文本数据
    • 能够过滤用户贴进来的HTML数据(未经HTML编码)
    • 能够过滤用户贴进来的Word数据,并能把大部分Word格式保留下来
    • 在这一过程中尽量不要让用户知道我们在做过滤
    • 不要去提示用户是否启用某种权限

    本例所适用的场景为使用iframe实现的HTML编辑器,而不是文本框(textarea或type为text的input)。

    研究过程中,主要参考了TinyMCE和CKEditor,但最后选择了TinyMCE的实现方法,具体原因在看完下面这段文字后就会明白。

    CKEditor的实现方式是在onpaste事件触发时,从剪贴板取出数据,处理取出的文本,然后再把处理好的文本存入剪贴板。有人可能会问:能不能在onpaste中直接取消paste动作,然后自己把获得的内容放入iframe当中?当时也试过这条路,但结果出人意料——直接从剪贴板拿出的数据是不包括格式信息的文本,特别是从Word粘贴过来的数据,纯文本,颜色、布局等数据都不存在。这样一来,用户只能粘贴没有格式的数据过来,然后自己重新在HTML编辑器里面编辑。但如果让浏览器自己去做粘贴,格式信息都会保留,浏览器会自动把Word的粘贴数据转换为XML数据,放入DOM中。所以为了保留格式信息,恐怕只能通过浏览器的标准粘贴行为的帮助来实现这一点。

    另外,CKEditor在Firefox中有一个致命的弱点:如果你要从剪贴板读写数据,就必须提示用户自己去设置一个叫signed.applets.codebase_principal_support的权限,Ja vaScript脚本是没有权限去设置的。虽然从技术角度来看这是很正常的,但很多产品经理无法接受这一点——至少我遇到的PM是这么认为的。

    以下是CKEditor获取和设置剪贴板的代码,供参考。

    复制代码

    function setClipboard(maintext) {
        if (window.clipboardData) {
            return (window.clipboardData.setData("Text", maintext));
        }
        else if (window.netscape) {
            netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
            var clip = Components.classes['@mozilla.org/widget/clipboard;1'].createInstance(Components.interfaces.nsIClipboard);
            if (!clip) return;
            var trans = Components.classes['@mozilla.org/widget/transferable;1'].createInstance(Components.interfaces.nsITransferable);
            if (!trans) return;
            trans.addDataFla vor('text/unicode');
            var str = new Object();
            var len = new Object();
            var str = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString);
            var copytext=maintext;
            str.data=copytext;
            trans.setTransferData("text/unicode",str,copytext.length*2);
            var clipid=Components.interfaces.nsIClipboard;
            if (!clip) return false;
            clip.setData(trans,null,clipid.kGlobalClipboard);
            return true;
        }
        return false;
    }
    function getClipboard() {
        if (window.clipboardData) {
            return(window.clipboardData.getData('Text'));
        }
        else if (window.netscape) {
            netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
            var clip = Components.classes['@mozilla.org/widget/clipboard;1'].createInstance(Components.interfaces.nsIClipboard);
            if (!clip) return;
            var trans = Components.classes['@mozilla.org/widget/transferable;1'].createInstance(Components.interfaces.nsITransferable);
            if (!trans) return;
            trans.addDataFla vor('text/unicode');
            clip.getData(trans,clip.kGlobalClipboard);
            var str = new Object();
            var len = new Object();
            try {
                trans.getTransferData('text/unicode',str,len);
            }
            catch(error) {
                return null;
            }
            if (str) {
                if (Components.interfaces.nsISupportsWString) str=str.value.QueryInterface(Components.interfaces.nsISupportsWString);
                else if (Components.interfaces.nsISupportsString) str=str.value.QueryInterface(Components.interfaces.nsISupportsString);
                else str = null;
            }
            if (str) {
                return(str.data.substring(0,len.value / 2));
            }
        }
        return null;
    }
    

    以下是提示用户启用权限的代码:

    复制代码

    if (window.netscape)
    {
        try
        {
            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
        }
        catch (ex)
        {
            alert("If you want to do paste, please input 'about:config' in address bar, then input Enter.\n Set \"signed.applets.codebase_principal_support\" to \"true\"");
        }
    }
    

    于是转向了TinyMCE的实现方式。在看它的代码时,特别留意到它竟然不需要权限就能在Firefox下面搞定粘贴,并且还能保留Word格式,于是仔细阅读了其中的逻辑。TinyMCE的实现步骤在IE和Firefox下面是不同的:

    IE实现

    1. 在onpaste回调函数中创建一个临时的iframe,用于粘贴内容,这个iframe放在主窗口的body下面即可。
    2. 在当前光标位置创建一个Range,用来保存光标位置和选中信息。
    3. 让临时iframe获得焦点,执行粘贴命令,即document.execCommand(“paste”),内容会粘贴在临时的iframe中。
    4. 通过innerHTML获得临时iframe中的内容并进行过滤。
    5. 让HTML编辑器的iframe获得焦点,用之前创建的Range对象执行pasteHTML方法来粘贴过滤后的内容。
    6. 最后取消默认的paste动作。

    (临时iframe可以根据个人喜好从DOM中删除,但由于这个iframe可以在多个HTML编辑器之间共用,所以实现中仅仅改变了iframe的left、top来调整iframe的位置,而不是移除它。调整left和top的目的在于:焦点移到临时iframe的时候,如果HTML编辑器的iframe和临时iframe不在一个视图之内,屏幕会滚动,这样会导致屏幕没有原因的闪烁。)

    Firefox实现

    1. 在onpaste回调函数中创建一个临时的div,这个div放在HTML编辑器的iframe里面——这也是绕过权限问题的关键。
    2. 保存当前光标和焦点位置,然后将光标移到临时创建的div中。
    3. 通过window.setTimeout设置一个回调函数,在paste动作瞬间完成之后执行。
    4. 让paste动作执行(onpaste回调函数执行完毕)。
    5. 刚才设置的回调函数执行,在里面获得临时div的innerHTML并进行过滤。
    6. 恢复刚才保存的光标和焦点位置,并移除临时div。
    7. 通过inserthtml命令(execCommand(“inserthtml”))把过滤后的内容贴到HTML编辑器的iframe中。

    详细代码如下:

    复制代码

    function getSel(w)
    {
        return w.getSelection ? w.getSelection() : w.document.selection;
    }
    function setRange(sel,r)
    {
        sel.removeAllRanges();
        sel.addRange(r);
    }
    function filterPasteData(originalText)
    {
        var newText=originalText;
        //do something to filter unnecessary data
        return newText;
    }
    function block(e)
    {
        e.preventDefault();
    }
    var w,or,divTemp,originText;
    var newData;
    function pasteClipboardData(editorId,e)
    {
        var objEditor = document.getElementById(editorId);
        var edDoc=objEditor.contentWindow.document;
        if(isIE)
        {
            var orRange=objEditor.contentWindow.document.selection.createRange();
            var ifmTemp=document.getElementById("ifmTemp");
            if(!ifmTemp)
            {
                ifmTemp=document.createElement("IFRAME");
                ifmTemp.id="ifmTemp";
                ifmTemp.style.width="1px";
                ifmTemp.style.height="1px";
                ifmTemp.style.position="absolute";
                ifmTemp.style.border="none";
                ifmTemp.style.left="-10000px";
                ifmTemp.src="iframeblankpage.html";
                document.body.appendChild(ifmTemp);
                ifmTemp.contentWindow.document.designMode = "On";
                ifmTemp.contentWindow.document.open();
                ifmTemp.contentWindow.document.write("");
                ifmTemp.contentWindow.document.close();
            }else
            {
                ifmTemp.contentWindow.document.body.innerHTML="";
            }
            originText=objEditor.contentWindow.document.body.innerText;
            ifmTemp.contentWindow.focus();
            ifmTemp.contentWindow.document.execCommand("Paste",false,null);
            objEditor.contentWindow.focus();
            newData=ifmTemp.contentWindow.document.body.innerHTML;
            //filter the pasted data
            newData=filterPasteData(newData);
            ifmTemp.contentWindow.document.body.innerHTML=newData;
            //paste the data into the editor
            orRange.pasteHTML(newData);
            //block default paste
            if(e)
            {
                e.returnValue = false;
                if(e.preventDefault)
                    e.preventDefault();
            }
            return false;
        }else
        {
            enableKeyDown=false;
            //create the temporary html editor
            var divTemp=edDoc.createElement("DIV");
            divTemp.id='htmleditor_tempdiv';
            divTemp.innerHTML='\uFEFF';
            divTemp.style.left="-10000px"; //hide the div
            divTemp.style.height="1px";
            divTemp.style.width="1px";
            divTemp.style.position="absolute";
            divTemp.style.overflow="hidden";
            edDoc.body.appendChild(divTemp);
            //disable keyup,keypress, mousedown and keydown
            objEditor.contentWindow.document.addEventListener("mousedown",block,false);
            objEditor.contentWindow.document.addEventListener("keydown",block,false);
            enableKeyDown=false;
            //get current selection;
            w=objEditor.contentWindow;
            or=getSel(w).getRangeAt(0);
            //move the cursor to into the div
            var docBody=divTemp.firstChild;
            rng = edDoc.createRange();
            rng.setStart(docBody, 0);
            rng.setEnd(docBody, 1);
            setRange(getSel(w),rng);
            originText=objEditor.contentWindow.document.body.textContent;
            if(originText==='\uFEFF')
            {
                originText="";
            }
            window.setTimeout(function()
            {
                //get and filter the data after onpaste is done
                if(divTemp.innerHTML==='\uFEFF')
                {
                    newData="";
                    edDoc.body.removeChild(divTemp);
                    return;
                }
                newData=divTemp.innerHTML;
                // Restore the old selection
                if (or)
                {
                    setRange(getSel(w),or);
                }
                newData=filterPasteData(newData);
                divTemp.innerHTML=newData;
                //paste the new data to the editor
                objEditor.contentWindow.document.execCommand('inserthtml', false, newData );
                edDoc.body.removeChild(divTemp);
            },0);
            //enable keydown,keyup,keypress, mousedown;
            enableKeyDown=true;
            objEditor.contentWindow.document.removeEventListener("mousedown",block,false);
            objEditor.contentWindow.document.removeEventListener("keydown",block,false);
            return true;
        }
    }
    

    这里的pasteClipboardData是用作onpaste回调函数的。要使用它,可以通过下面的代码把它加到HTML编辑器的iframe的onpaste事件上。

    复制代码

    var ifrm=document.getElementById("editor")
    if(isIE)
    {
        ifrm.contentWindow.document.documentElement.attachEvent("onpaste", function(e){return pasteClipboardData(ifrm.id,e);});
    }
    else
    {
        ifrm.contentWindow.document.addEventListener("paste", function(e){return pasteClipboardData(ifrm.id,e);},false);
    }
    

    这里的filterPasteData函数就是专门用来做过滤的函数,具体要怎么去过滤纯文本、HTML及Word数据,将在下一篇讲解。

    本文内容来源于网友投稿,如有侵权请联系删除。
    作者最新文章
    编程开发
    相关文章 更多
    解决PHP递归报错:max_nesting_level限制与内存溢出处理
    解决PHP递归报错:max_nesting_level限制与内存溢出处理

    遇到PHP递归报错时,不要盲目调大max_nesting_level。本文教你区分Xdebug限制、内存耗尽和正则递归错误,提供代码级的终止条件优化与迭代替代方案,彻底解决栈溢出问题。

    PHP递归中static变量与引用传递的常见陷阱及调试
    PHP递归中static变量与引用传递的常见陷阱及调试

    本文分析PHP递归中static变量导致的状态污染及引用传递引发的共享数据修改问题。提供具体的代码复现、缓存键设计建议及调试打印技巧,帮助开发者避免隐蔽的逻辑错误。

    PHP递归性能优化技巧与迭代替代方案
    PHP递归性能优化技巧与迭代替代方案

    解析PHP递归函数在树形数据处理中的性能瓶颈,提供预加载数据消除I/O、使用显式栈替代深层递归的实战方案,帮助开发者在代码可读性与执行效率间做出合理取舍。

    Java测试中怎么使用Mockito模拟依赖对象
    Java测试中怎么使用Mockito模拟依赖对象

    详细讲解在Java单元测试中如何使用Mockito模拟依赖对象,包括引入依赖、创建Mock、打桩返回值、行为验证以及Mock与Spy的核心差异和常见陷阱排查。

    链表删除节点的时间复杂度是多少及其详细分析
    链表删除节点的时间复杂度是多少及其详细分析

    详细分析链表删除节点的时间复杂度,深入探讨单链表与双向链表在不同已知前提下的查找与删除开销,并结合完整代码与清晰图解进行对比总结。

    codex如何配置模型参数及文件设置教程
    codex如何配置模型参数及文件设置教程

    想知道如何让AI写出的代码更贴合你的习惯?本文手把手教你在VS Code中调整Codex相关模型参数,通过修改配置文件优化温度值和令牌限制,解决代码建议不准确或响应慢的问题。

    Claude Code AI编程工具实力揭秘与编程助手实测
    Claude Code AI编程工具实力揭秘与编程助手实测

    通过实测展示Claude Code在终端中如何理解自然语言指令、自动修改代码文件并处理复杂编程任务,帮助开发者评估其实际辅助能力。

    winforms教程自学入门与基础开发步骤详解
    winforms教程自学入门与基础开发步骤详解

    本教程详细讲解如何使用Visual Studio创建WinForms项目,通过添加按钮和标签控件并编写点击事件代码,实现一个基础的计数器功能,适合C#初学者快速上手Windows窗体应用开发。

    Cursor自动补全设置教程教你快速开启代码补全功能
    Cursor自动补全设置教程教你快速开启代码补全功能

    详解Cursor编辑器中自动补全功能的开启与优化设置,涵盖Tab触发机制、上下文窗口调整及模型切换,帮助开发者解决补全延迟、干扰大等问题,提升编码流畅度。

    pandas的数据格式怎么转换和设置方法教程
    pandas的数据格式怎么转换和设置方法教程

    详解Pandas中数据格式转换的核心方法,包括astype强制转换、to_numeric容错处理及日期解析技巧,解决常见类型错误并提升数据处理效率。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    macOS软件
    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    Mac软件 更多
    photoshop
    photoshop
    Windows、macOS 、 iPad

    Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

    Blender
    Blender
    Windows、macOS 和 Linux

    Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。

    灵活计算器
    灵活计算器
    macOS/iOS/Android

    灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    WINDOWS 更多
    3dmax(3ds max)
    3dmax(3ds max)
    Windows

    Autodesk 3ds Max 是一款专业的三维建模、动画与渲染软件,广泛应用于建筑可视化、游戏开发、影视动画、广告设计和产品展示等领域。

    photoshop
    photoshop
    Windows、macOS 、 iPad

    Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。

    Blender
    Blender
    Windows、macOS 和 Linux

    Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。