一步步教你实现富文本编辑器(第三部分)
这部分我们把富文本编辑器的代码打包成一个类。至于如何实现没有什么好说的,就是那五种方案,我取用的是原型,那是最JS,也是最ruby的。我们的所有实现都在原型进行,最后new出来就是!构造函数有一个必选参数,就是那个textarea的id,其他都是动态生成的,包括其样式。关于样式,我已提供了一个很好用的addSheet函数了。那么开始吧,我们要尽快做出第二部分最后阶段的样式再说!
首先我为大家提供了一个模板,大家可以根据它自行完成我们讲过的部分。
var Class = { create: function () { return function () { this .initialize.apply( this , arguments); } } } var extend = function (destination, source) { for ( var property in source) { destination[property] = source[property]; } return destination; } var RichTextEditor = Class.create(); //我们的富文本编辑器类 RichTextEditor.prototype = { initialize: function (options){ this .setOptions(options); this .drawEditor( this .options.textarea_id); }, setOptions: function (options){ this .options = { //这里集中设置默认属性 id: 'jeditor_' + new Date().getTime(), textarea_id: null //用于textarea的ID,也就是我们的必选项 } extend( this .options, options || {}); //这里是用来重写默认属性 }, ID: function (id){ return document.getElementById(id) }, //getElementById的快捷方式 TN: function (tn){ return document.getElementsByTagName(tn) }, //getElementsByTagName的快捷方式 CE: function (s){ return document.createElement(s)}, //createElement的快捷方式 drawEditor: function (id){ var textarea = this .ID(id); textarea.style.display = "none" ; } } |
接着下来我们基本就是在drawEditor这个函数工作了,我们隐藏了原来的textarea后,然后在其下面生成一个div,当作富本文编辑器的工具栏,然后再在其下面生成iframe,这是我们富文本编辑器的工作区。工具栏的按钮很多,我们把这些按钮的名字以及隐藏在title的命令全部打包在一个对象,然后循环生成它们,并在循环中设置样式与绑定事件。这些和第二部分所讲的别无二致,我就不重复了,快手净脚搞出它们吧!
var buttons = { 'fontname' : { '宋体' : 'SimSun' , '隶书' : 'LiSu' , '楷体' : 'KaiTi_GB2312' , '幼圆' : 'YouYuan' , '黑体' : 'SimHei' , '雅黑' : 'Microsoft YaHei' , '仿宋' : 'FangSong' , 'Comic Sans MS' : 'Comic Sans MS' }, 'fontsize' : { '特小' : 1, '很小' : 2, '小' : 3, '中' : 4, '大' : 5, '很大' : 6, '特大' :7 }, 'removeformat' : '还原' , 'bold' : '加粗' , 'italic' : '斜体' , 'underline' : '下划线' , 'strikethrough' : '删除线' , 'justifyleft' : '居左' , 'justifycenter' : '居中' , 'justifyright' : '居右' , 'indent' : '缩进' , 'outdent' : '悬挂' , 'forecolor' : '前景色' , 'backcolor' : '背景色' , 'createlink' : '超链接' , 'insertimage' : '插图' , 'insertorderedlist' : '有序列表' , 'insertunorderedlist' : '无序列表' , 'html' : '查看' }; |
到这里,基本和第二部分差不多了。至于前景色与背景色,我们打算用我以前提供过的颜色选择器实现,现在我们的目标是那两个下拉选择框。我觉得那两个select太不人性化了,由于其级别很高,我们很难对它进行制定。作为可见即可得,我们要来在拉动那个select时,应该能给人们一个大概样子。因此select必须死。
我们修改buttons对象,把fontname与fontsize提取出来单独处理!
var fontFamilies = [ '宋体' , '经典中圆简' , '微软雅黑' , '黑体' , '楷体' , '隶书' , '幼圆' , 'Arial' , 'Arial Narrow' , 'Arial Black' , 'Comic Sans MS' , 'Courier New' , 'Georgia' , 'New Roman Times' , 'Verdana' ] var fontSizes= [[1, 'xx-small' , '最小' ], [2, 'x-small' , '特小' ], [3, 'small' , '小' ], [4, 'medium' , '中' ], [5, 'large' , '大' ], [6, 'x-large' , '特大' ], [7, 'xx-large' , '最大' ]]; |
但是这样一来,我们原来的事件绑定机制就遭到灭顶之灾!我们必须奠出我们的addEvent函数。addEvent要求我们传入三个参数(需要绑定的元素,事件类型与绑定事件),后两个很明确了,问题是第一个,我们怎么找到这些元素呢?不过一个个加id吧。不用,我们在最开始的循环就把这些元素加入一个数组就是!
for ( var i in buttons){ /*添加命令按钮的名字,样式*/ var button = buttonClone.cloneNode( "true" ); if (i == 'backcolor' ){ /*特殊处理背景色按钮*/ if (!+ "\v1" ){ button.setAttribute( "title" , "background" ) } else { button.setAttribute( "title" , "hilitecolor" ) } } button.setAttribute( "title" ,i); /*把execCommand的命令参数放到title*/ button.innerHTML = buttons[i]; button.setAttribute( "unselectable" , "on" ); /*防止焦点转移到点击的元素上,从而保证文本的选中状态*/ toolbar[i] = button; /*★★★★把元素放进一个数组,用于事件绑定!★★★★*/ fragment.appendChild(button); } toolbar.appendChild(fragment); } |
得益于javascript的事件机制,我们只对toolbar进行监听,就可以监听其所有子元素。另外,我们把格式化命令独立出来,简化我们的程序。
this .addEvent(toolbar, 'click' , function (){ var e = arguments[0] || window.event, target = e.srcElement ? e.srcElement : e.target, command = target.getAttribute( "title" ); switch (command){ case 'createlink' : case 'insertimage' : var value = prompt( '请输入网址:' , 'http://' ); _format(command,value); break ; case 'fontname' : //这几个特殊处理 case 'fontsize' : case 'forecolor' : case 'backcolor' : case 'html' : return ; default : _format(command, '' ); break ; } }); var _format = function (x,y){ //内部私有函数,处理富文本编辑器的格式化命令 iframeDocument.execCommand(x, false ,y); iframe.contentWindow.focus(); } |
至于字体与字码,我们可以用div模拟select了!然后为它们绑定两个事件,一个是用来显示隐藏select,一个是用来执行格式化命令。
var fontPicker = $.CE( 'div' ); fontPicker.setAttribute( 'unselectable' , 'on' ); fontPicker.className = "fontpicker" ; toolbar.appendChild(fontPicker); //字体选择器与字码选择器都是共用一个虚拟select $.addEvent(toolbar[ 'fontname' ], 'click' , function (){ //根据情况选择载入虚拟select的内容 }) $.addEvent(toolbar[ 'fontsize' ], 'click' , function (){ //根据情况选择载入虚拟select的内容 }) var bind_select_event = function (button,picker){ //显示或隐藏文字选择器 } /************************用于生成文字选择器的内容************************/ fontPickerHtml: function (type,array){ var builder = []; for ( var i = 0,l = array.length;i<l;i++){ builder.push( '<a unselectable="on" style="' ); if (type == 'fontname' ){ builder.push( 'font-family' ); builder.push( ':\'' ); builder.push(array[i]); /*呈现一行(一行就是一种字体)*/ builder.push( '\';" href="javascript:void(0)">' ); builder.push(array[i]); } else if (type == 'fontsize' ){ builder.push( 'font-size' ); /*呈现一行(一行就是一种字号)*/ builder.push( ':' ); builder.push(array[i][1]); builder.push( ';" sizevalue="' ); builder.push(array[i][0]); builder.push( '" href="javascript:void(0)">' ); //IE的a元素必须有href才有悬浮效果 builder.push(array[i][2]); } builder.push("</a>"); } return builder.join( '' ); } |
上面的代码其实有个问题,不过也可能是IE的问题,在IE中,当我们点击虚拟select的a元素时,execComman函数实际执行了两次,第一次确实是完成了格式化任务,第二次却因为参数为空而报错……囧!我不知道是哪里错了,不过我认为如果我们把事件直接绑定到a元素,而不是绑定到虚拟select的那个div元素,就肯定没问题。不过这样做代码非常复杂非常长,如何定位到这些a元素就要劳师动众一番。我是利用DOM2的事件传播机制缩短它的(嗯,写到这里,我好像明白了一些)。我用了一个很不值得推荐的方法,把execComman放到一个catch块中,吞掉这异常。
更好的办法,我想到了。execCommand之所以执行发两次,是因为IE并没有阻止onclick事件继续向上冒泡,之于为什么会冒泡呢?!这又是个谜了!这是新的代码:
$.addEvent(fontPicker, 'click' , function (){ /*****************略************/ _format(command,value); e.cancelBubble = true ; //重点 fontPicker.style.display = 'none' ; } }); var _format = function (x,y){ //内部私有函数,处理富文本编辑器的格式化命令 // try{ iframeDocument.execCommand(x, false ,y); iframe.contentWindow.focus(); // }catch(e){} } |
接着下来是背景色与前景色,以前我就做了一个颜色选择器,具体可参见这篇博文,我就不重复了!流程基本与文字选择器一样,我们这里得修改一下bind_select_event方法。
var bind_select_event = function (button,picker){ //显示或隐藏选择器 button.style.position = 'relative' ; var command = button.getAttribute( "title" ); if ( 'backcolor' == command){ command = !+ "\v1" ? 'backcolor' : 'hilitecolor' ; } /************略****************/ } |
紧接着是查看按钮,这个简单,这里我把封装一下,让它看起来不那么乱。
/********切换回代码界面*************/ var _doHTML = function () { iframe.style.display = "none" ; textarea.style.display = "block" ; textarea.value = iframeDocument.body.innerHTML; textarea.focus(); }; /********切换回富文本编辑器界面*************/ var _doRich = function () { iframe.style.display = "block" ; textarea.style.display = "none" ; iframeDocument.body.innerHTML = textarea.value; iframe.contentWindow.focus(); }; /********切换编辑模式的开关*************/ var switchEditMode = true ; $.addEvent(toolbar[ 'html' ], 'click' , function (){ if (switchEditMode){ _doHTML(); switchEditMode = false ; } else { _doRich(); switchEditMode = true ; } }); |
但这个不保证我们提交表单时textarea有东西,我们在iframe失去焦点时偷偷转移东西给textarea。这里的问题第二部分已详细提过,这里就不重复了
$.addEvent(iframe.contentWindow, "blur" , function (){ textarea.value = iframeDocument.body.innerHTML; }); |
“接着下来我们开始讲解复杂插入吧……”正想这样说,一看篇幅,改写成类比预期的费笔墨,今天就先开过头,下次再说。
我们先多添加一个按钮,用于插入表格,点击它将弹出一个层,上面要求我们填写将要生成的表格的参数。
var buttons = { //工具栏的按钮集合 /*********略************/ 'table' : '插入表格' , 'html' : '查看' }; |
var tableCreator = $.CE( 'div' ); tableCreator.className = 'tablecreator' ; toolbar.appendChild(tableCreator); tableCreator.innerHTML = $.tableHtml(); $.addEvent(toolbar[ 'table' ], 'click' , function (){ bind_select_event( this ,tableCreator); }); |
最后留个作业,希望各位博友们思考一下如何创建表格,并把插入到编辑光标之前。那么下一部分再见!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义