富文本编辑器

 代码演示

 

程序原理

文本编辑器具有的基本功能就是对编辑区选中的文字通过工具栏提供的功能对文字或选区进行操作.比如文字加粗,斜体,创建表格,插入链接和图片等一系列的操作.然后可以通过源码查看查看编辑后产生的源代码.

可以先通过Rich-Text_Editing大致了解下实现富文本编辑器的主要技术.这些功能就是我们代码设计的主线.

编辑区

我们需要一个iframe.并将其designMode属性设置为"on".这个iframe就具备了编辑功能.也是我们实现编辑器的基本容器.动态创建一个iframe代码如下

createIframe: function() {
    
var _this = this;
    
this.ifr = co.append(this.container, 'iframe', {'frameborder'0'style''border:0; vertical-align:bottom''class''econtent' });
    
this.doc =  this.ifr.contentDocument || this.ifr.contentWindow.document; // W3C || IE
    this.doc.designMode = 'on';
    
this.doc.open();
    
// margin为了消除iframe中的html上部的空白
    this.doc.write('<html><head><style>body{ margin:3px; word-wrap:break-word; word-break: break-all; }</style></head><body>GoodNessEditor</body></html>');
    
this.ifr.contentWindow.focus();
    
this.doc.close();
    
// 当iframe失去焦点.偷偷将代码存入textare中
    co.addEvent(this.ifr.contentWindow, 'blur'function() {
        _this.txtarea.value 
= _this.doc.body.innerHTML;
    });
},

这里我们创建了一个空白的iframe并将其document文档的designMode设置为"on".然后通过doc.write添加自定义的body和样式可以方便的设置编辑区的样式.然后记得要将焦点落到iframe中,焦点设置很重要,是为我们离开编辑区后获取源码做好准备.

这里有三个知识点:

1. 获取iframe的document文档.在W3C标准中用contentDocument(IE8开始支持.但IE6,7不支持),在IE6,7下用contentWindow.document获取.

2. 我们的焦点需要落在contentWindow即iframe的窗体中.

3. 我们绑定contentWindow的blur事件.当失去焦点的时候.就将代码存到textarea中.

    注意: (1) 这里绑定contentWindow事件.不可以用contentWindow.onblur的形式.因为firefox不支持.只能用addEventListener方法去绑定.

            (2) 将代码放入到textArea的原因就是利用了textArea可以显示HTML原生代码的这个优势.而使得源代码查看这个功能变得轻而易举.

命令工具栏

命令工具栏是我们发布命令的集结地.首先我们需要创建这个工具栏.为了让用户体验更好.避免让命令图片间断的出现.这里用css sprite技术将图片合并.通过position来控制图片显示.具体创建方法请参考源代码createToolBar方法.这里主要讲下命令的实现方式.比如我们点击字体加粗按钮.字体加粗是怎么实现的.

这里主要运用的技术就是execCommand命令函数

execCommand(String aCommandName, Boolean aShowDefaultUI, String aValueArgument)

Arguments
    String aCommandName
        the name of the command

    Boolean aShowDefaultUI
        whether the 
default user interface should be shown. This is not implemented in Mozilla.

    String aValueArgument
        some commands (such as insertimage) require an extra value argument (the image
's url). Pass an argument of null if no argument is needed.

这个方法提供了3个参数.中间的参数由于Mozilla没有实现我们就默认为false. 剩余两个参数就是我们实现命令的整个过程了.这些命令有的需要1个参数有的需要两个参数.我篇首给的链接里对于各个命令参数需求写的很清楚.比如我们实现字体加粗只需要ifrDoc.execCommand('bold' ,false, null)即可.

源码里我用静态变量存储了需要一个参数的命令:

editor.NO_ARG_COMMAND = {
    BOLD: 
'bold',                                      // 加粗
    ITALIC: 'italic',                                  // 斜体 
    UNDERLINE: 'underline',                            // 下划线
    CUT: 'cut',                                        // 剪切            
    COPY: 'copy',                                      // 复制
    JUSTIFYLEFT: 'justifyleft',                        // 靠左
    JUSTIFYRIGHT: 'justifyright',                      // 靠右
    JUSTIFYCENTER: 'justifycenter',                    // 居中
    INSERTUNORDEREDLIST: 'insertunorderedlist',        // 项目符号
    INSERTORDEREDLIST: 'insertorderedlist',            // 编号
    OUTDENT: 'outdent',                                // 减小缩进
    INDENT: 'indent',                                  // 增加缩进
    REMOVEFORMAT: 'removeformat'                       // 清除格式
};

除了这些剩下的几个方法就需要两个参数了,也是相对比较麻烦的几个功能.

插入链接.插入图片.插入表情.字体变色.插入表格.这几个功能首先都是使用了pop弹窗的形式实现的.

pop弹窗

首先取得所点击的图标的绝对位置

// 获取元素绝对位置
co.getPos = function(o) {
    
for(var _pos = {x: 0, y: 0}; o; o = o.offsetParent) {
        _pos.x 
+= o.offsetLeft;
        _pos.y 
+= o.offsetTop;
    }
    
return _pos;
};

然后将pop窗口的left和top定位到获取的x, y值即可

// 定位弹窗 
fixPop: function(fwin, tar) {
    co.setProperties(fwin, {
'style''top:' + (co.getPos(tar).y + tar.offsetHeight) + 'px; left:' + co.getPos(tar).x + 'px' });
},

单例工厂

由于我们每次点击弹窗的时候都会动态创建一个pop窗口.那么点击多少次就会创建多少个窗口.这样就会加大页面体积.最后导致崩溃.这里我们需要提供一个单例工厂模式.保证指定id窗口实例一经创建.就不需再次创建而重复利用.

var newFactory = function() {
    
var coll = {};
    
return {
        create: 
function(fn, cfg, content/* POP_Body*/) {
            
if(coll[cfg.id]) {
                
return coll[cfg.id];
            } 
else {
                
var win = fn(cfg, content); 
                coll[cfg.id] 
= win;
                
return win;
            }
        }
    }
}();

隐藏弹窗

隐藏弹窗是当我们点击弹窗以外的元素位置就将该窗口隐藏.所以我们需要监听iframe和页面document的onclick事件.但由于事件冒泡机制.当你点击POP窗口的时候也会冒泡至document而导致弹窗无法输入.所以要在pop窗口点击事件的时候禁止冒泡. 

var t = co.target(e);
if(title == '插入链接' || title == '插入图片' || title == '插入表格') { co.cancelBubble(e); } // 插入链接和图片禁止冒泡

清除选择

在IE下比如选择颜色以及点击图标等会导致selection丢失.所以要记住在相应的元素上设置unselectable = "on".

事件委托

在我们为工具栏的每个图片绑定事件以及为我们的颜色拾取控件的每个表格绑定事件的时候.如果循环所有的控件然后每个空间依次绑定事件.那么就会非常耗费资源.这里要得益于JS所具有的事件委托技术.这样只需要绑定父元素然后通过target去获取就可以了.

颜色拾取器

简易的颜色拾取器.就是通过['00', '33', '66', '99', 'CC', 'FF']6个色值循环组合.6 x 6的色盘. 具体细节可以参考源码.

插入表格

插入表格在firefox下提供了insertHTML这个命令用于复杂的HTML插入.但IE下却不支持.这个就为我们向指定光标处插入HTML增加了困难.因为你需要记住光标的位置.然后再将生成的HTML插入到光标的位置.那么怎么才能记住光标的位置呢.这里有个getBookmark方法

Saves the current TextRange object into a string (bookmark) that can be used for the moveToBookmark method to return to the original TextRange object.

 用这个就可以保存我们选择的光标位置.然后当插入HTML的时候用moveToBookmark移动到当前光标位置进行插入即可.

saveBookMark: function() {
    
var _this = this;
    co.addEvent(_this.ifr, 
'beforedeactivate'function() {
        
var rng = _this.doc.selection.createRange();
        
if(rng.getBookmark) {
            _this.bookmark 
= _this.doc.selection.createRange().getBookmark(); // 保存光标用selection下的createRange();
        }
    });
    co.addEvent(_this.ifr, 
'activate'function() {
        
if(_this.bookmark) {
            
// Moves the start and end points of the current TextRange object to the positions represented by the specified bookmark.
            // 将光标移动到 TextRange 所以需要用 body.createTextRange();
            var rng = _this.doc.body.createTextRange();                
            rng.moveToBookmark(_this.bookmark);
            rng.select();
            _this.bookmark 
= null;
        }
    });
},
    

这里用了beforedeactivate和activate方法来检测焦点的离开,一旦检测到离开就将当前光标选区存入到Bookmark中.用这个事件原因就是只有在光标移到外面的文档我们记录才有意义.具体见司徒正美的教程.

取到了光标位置.我们就可以运用IE下的pasteHTML方法.将HTML插入到光标处.

_this.doc.selection.createRange().pasteHTML(_html);

这样就完成了表格插入的操作.

源码显示区

上面的步骤都做完.编辑器的基本功能就快完成了.最后我们需要一个显示源码的功能.上面分析已经说过textArea与生俱来就具备显示HTML原生代码的条件.而且我们上面也实现了当iframe失去焦点的时候悄悄的将代码存入到了textArea中了.那么我们在这里只需要做iframe和textArea的切换和互相赋值就可以了.

这里还有一点就是Chrome和Safari浏览器的textArea是可以拖拽的.这里可以通过css的resize:none禁止其resize.

创建源码显示栏以及源码显示

createToolFoot: function() {
    
var _this = this;
    co.append(
this.container, 'div''efoot').innerHTML = '<input type="checkbox" id="showCode" /><label for="showCode">显示源码</label>';
    
// 绑定显示源码事件
    co.getId('showCode').onclick = function() { 
        
if(this.checked) {
            _this.layer.style.display 
= 'block';
            co.getId(
'bgcode').style.display = 'block';
            _this.ifr.style.display 
= 'none';
        } 
else {
            _this.layer.style.display 
= 'none';
            co.getId(
'bgcode').style.display = 'none';
            _this.doc.body.innerHTML 
= co.getId('bgcode').value;
            _this.ifr.style.display 
= 'block';                
        }
    };
},

程序源码

程序源码
// JS编辑器 
//
 @version beta 0.1
//
 @date 2010-03-21
//
 @author goodNess
//
 @blog http://www.cnblogs.com/goodness2010/
//
 @email goodNess2010@126.com
var co = co || {};
co.Root 
= 'http://images.cnblogs.com/cnblogs_com/goodness2010/238089/';  // 图片的根目录
//
 浏览器判断
co.browser = (function(ua) {
    
var b = {
        msie: 
/msie/.test(ua) && !/opera/.test(ua),
        opera: 
/opera/.test(ua),
        safari: 
/webkit/.test(ua) && !/chrome/.test(ua),
        firefox: 
/firefox/.test(ua),
        chrome: 
/chrome/.test(ua)
    };
    
var vMark = '';
    
for(var i in b) {
        
if(b[i]) { vMark = /(?:safari|opera)/.test(i) ? 'version' : i; break; }
    }
    b.version 
= vMark && RegExp('(?:'+ vMark +')[\\/: ]([\\d.]+)').test(ua) ? RegExp.$1 : 0;

    b.ie 
= b.msie;
    b.ie6 
= b.msie && parseInt(b.version) == 6;
    b.ie7 
= b.msie && parseInt(b.version) == 7;
    b.ie8 
= b.msie && parseInt(b.version) == 8;
    
return b;
})(window.navigator.userAgent.toLowerCase());

// ie6图片强制缓存
try {
    co.browser.ie6 
&& document.execCommand('BackgroundImageCache'truefalse);
catch(ex) {};

// 获取ID对象
co.getId = function(id) { return document.getElementById(id); };

// 获取对象
co.get = function(node) {
    
return typeof(node) == 'string' ? document.getElementById(node) : node;
};

// 创建DOM对象
co.append = function(parentNode, tag, attributes) {
    
var o = document.createElement(tag);
    
if(attributes && typeof(attributes) == 'string') {
        o.className 
= attributes;
    } 
else {
        co.setProperties(o, attributes);
    }
    co.get(parentNode).appendChild(o);
    
return o;
};

// 遍历数组
co.foreach = function(arr, callback) {
    
for(var i = 0, l = arr.length; i < l; i++) {
        arr[i] 
= callback(arr[i]);
    }
    
return arr;
};

// 设置属性
co.DIRECT_ATTRIBUTE_MAP_ = {
    
'cellpadding''cellPadding',
    
'cellspacing''cellSpacing',
    
'colspan''colSpan',
    
'rowspan''rowSpan',
    
'valign''vAlign',
    
'height''height',
    
'usemap''useMap',
    
'frameborder''frameBorder',
    
'type''type'
};

co.setProperties 
= function(element, properties) {
    
var val;
    
for(var key in properties) {
        val 
= properties[key];
        
if(key == 'style') {
            element.style.cssText 
= val;
        } 
else if(key == 'class') {
            element.className 
= val;
        } 
else if(key == 'for') {
            element.htmlFor 
= val;
        } 
else if(key in co.DIRECT_ATTRIBUTE_MAP_) {
            element.setAttribute(co.DIRECT_ATTRIBUTE_MAP_[key], val);
        } 
else {
            element[key] 
= val;
        }
    }
    
return element;
};

// 属性扩展
co.extend = function(destination, source) {
    
for(var property in source) {
        destination[property] 
= source[property];
    }
    
return destination;
};

// 获取元素绝对位置
co.getPos = function(o) {
    
for(var _pos = {x: 0, y: 0}; o; o = o.offsetParent) {
        _pos.x 
+= o.offsetLeft;
        _pos.y 
+= o.offsetTop;
    }
    
return _pos;
};

// 设置透明度
co.setOpacity = function(e, opac) {
    
if(co.browser.ie) {
        e.style.filter 
= "alpha(opacity=" + opac*100 + ")";
    } 
else {
        e.style.opacity 
= opac;
    }
}

// 事件绑定
co.addEvent = function(el, type, fn) {
    el.addEventListener 
? el.addEventListener(type, fn, false) : 
    el.attachEvent(
'on' + type, function() { fn.call(el); })
};

co.target 
= function(e) {
    
return e ? e.target : event.srcElement;
}

// 禁止冒泡
co.cancelBubble = function(e) {
    
if(e && e.stopPropagation) {
        e.stopPropagation();
    } 
else {
        event.cancelBubble 
= true;
    }
};

/**
 * 抽象单类工厂
 * @method create(cfg{必须有一个唯一的id标识})
 
*/
var newFactory = function() {
    
var coll = {};
    
return {
        create: 
function(fn, cfg, content/* POP_Body*/) {
            
if(coll[cfg.id]) {
                
return coll[cfg.id];
            } 
else {
                
var win = fn(cfg, content); 
                coll[cfg.id] 
= win;
                
return win;
            }
        }
    }
}();

/**
 *  ---------------------------------- PopUp窗口辅助类 -----------------------------
 *    config:
 *    id: 容器id
 *    title: 容器标题
 *  container: 容器class
 *    concss: 标题内容样式
 *    heacss: 标题外部样式
 *    bodcss: 容器内容样式
 *    chicss: 内容子列表样式
 *    content: 子列表内容
 *  @describe clicking on an element with the unselectable attribute set to on does not destroy the current selection if one exists.
 
*/
var popUp = {};

popUp.create 
= function(config, body) {
    
this.container = co.append(document.body, 'div', config['container']);
    
this.container.id = config.id;
    
var _head = '<div class="' + config.heacss + '"><span class="' + config.concss + '">' + config.title +'</span></div>';
    
var _body = '<div class="' + config.bodcss + '">';
    _body 
+= (body || '');
    _body 
+= '</div>';
    
this.container.innerHTML = _head + _body;
    
return this.container;
};

/*--------------------------------- ColorPicker辅助组件(单独提出.松耦合) -------------------------------------------*/
var ColorPicker = {
    create: 
function() {
        
// 定义变量
        var cl = ['00''33''66''99''CC''FF'], a, b, c, d, e, f, i, j, k, T;
        
// 创建整个外围容器
        this.win = co.append(document.body, 'div');
        
this.win.id = 'colorPicker';
        
// 创建head
        var h = '<div class="colorhead"><span class="colortitle">颜色选择</span></div>';
        
// 创建body [6 x 6的色盘]
        h += '<div class="colorbody"><table cellspacing="0" cellpadding="0"><tr>';
        
for(i = 0; i < 6++i) {
            h 
+= '<td><table class="colorpanel" cellspacing="0" cellpadding="0">';
            
for(j = 0, a = cl[i]; j < 6++j) {
                h 
+= '<tr>';
                
for(k = 0, c = cl[j]; k < 6++k) {
                    b 
= cl[k];
                    e 
= k == 5 && i != 2 && i != 5 ? ';border-right:none;' : '';
                    f 
= j == 5 && i < 3 ? ';border-bottom:none''';
                    d 
= '#' + a + b + c;
                    T 
= co.browser.ie ? '&nbsp;'''
                    h 
+= '<td unselectable="on" style="background: ' + d + e + f + '" title="' + d + '">' + T + '</td>'/* 切记设置unselectable='on'*/
                }
                h 
+= '</tr>';
            }
            h 
+= '</table></td>';
            
if(cl[i] == '66') h += '</tr><tr>';
        }
        h 
+= '</tr></table></div>';
        
this.win.innerHTML = h;
        
return this.win;
    }
};

/*--------------------------------- 编辑器基类 -----------------------------------------*/
var editor = function(id, bardata, options) {
    
this.container = co.getId(id);
    
this.bardata = bardata;
    
this.currActive = null;
    
this.bookmark = null;
    co.extend(
thisthis.setOptions(options));
    
// 创建框架结构
    this.createStruct();
    
// 创建快照书签
    co.browser.ie && this.saveBookMark();
};
// 静态变量https://developer.mozilla.org/en/Rich-Text_Editing_in_Mozilla
editor.NO_ARG_COMMAND = {
    BOLD: 
'bold',
    ITALIC: 
'italic',
    UNDERLINE: 
'underline',
    CUT: 
'cut',
    COPY: 
'copy',
    JUSTIFYLEFT: 
'justifyleft',
    JUSTIFYRIGHT: 
'justifyright',
    JUSTIFYCENTER: 
'justifycenter',
    INSERTUNORDEREDLIST: 
'insertunorderedlist',
    INSERTORDEREDLIST: 
'insertorderedlist',
    OUTDENT: 
'outdent',
    INDENT: 
'indent',
    REMOVEFORMAT: 
'removeformat'
};
// 原型扩展
editor.prototype = {
    setOptions: 
function(options) {
        
this.options = {
            emotion: [
                { 
'title''微笑''pos''-5px -5px',  'url': co.Root + 'o_220510752_p_r2_c2.gif' }, 
                { 
'title''大笑''pos''-32px -5px''url': co.Root + 'o_220510752_p_r2_c3.gif' },
                { 
'title''窃笑''pos''-59px -5px''url': co.Root + 'o_220510752_p_r2_c4.gif' },
                { 
'title''眨眼''pos''-86px -5px''url': co.Root + 'o_220510752_p_r2_c5.gif' },
                { 
'title''吐舌''pos''-113px -5px','url': co.Root + 'o_220510752_p_r2_c11.gif'},
                { 
'title''色色''pos''-140px -5px','url': co.Root + 'o_220510752_p_r2_c6.gif' },
                { 
'title''呲牙''pos''-168px -5px','url': co.Root + 'o_220510752_p_r2_c7.gif' },
                { 
'title''讨厌''pos''-194px -5px','url': co.Root + 'o_220510752_p_r2_c8.gif' }
            ],
            baroverOpc: 
0.7
        };
        
return co.extend(this.options, options || {});
    },
    
// 创建编辑器整个框架结构
    createStruct: function() {
        
// 创建工具条
        this.createToolBar();
        
// 创建隐藏textarea容器
        this.createTextArea();
        
// 创建iframe容器
        this.createIframe();
        
// 创建工具底栏
        this.createToolFoot();
        
// 创建工具条遮盖层
        this.createToolLayer();
    },
    
// 创建工具条
    createToolBar: function() {
        
var _this = this;
        
this.bar = co.append(this.container, 'div');
        
this.bar.id = 'ebar'this.bar.className = 'ebar';
        
for(var i = 0, l = this.bardata.length; i < l; i++) {
            
var sp = co.append(this.bar, 'span');
            co.setProperties(sp, 
this.bardata[i]);
        }
        
// 事件代理
        this.bar.onmousedown = function(e) {
            
var t = co.target(e), command = t['command'];
            
if(t.tagName == 'SPAN') {
                
if(!!command) {
                    _this.changeSty(t, 
'active'); // 切换样式
                    if(command.toUpperCase() in editor.NO_ARG_COMMAND) { // 不需要参数的命令
                        if(co.browser.firefox) { /* firefox暂不提供粘贴, 剪切, 复制功能 详见http://www.mozilla.org/editor/midasdemo/securityprefs.html*/
                            
if(command.toUpperCase() == 'CUT' || command.toUpperCase() == 'COPY') {
                                alert(
'为了信息安全FF暂不提供该功能');
                                
return false;
                            }
                        }
                        _this.doEditCommand(command);
                        _this.ifr.contentWindow.focus(); 
// 焦点要记住
                    } else {
                        
switch(command) {                                 // 代理分支 
                            case 'fontSize'// 字号
                            case 'fontName'// 字体
                            case 'createLink'// 创建连接
                            case 'insertImage'// 插入图片
                            case 'insertEmotion'// 插入表情
                            case 'insertHTML'// 插入表格
                                _this.setPopInit(command, t /*被点击的控件*/); /* 需要pop类弹窗的公用初始化方法 */
                                
break;
                            
case 'foreColor'// 颜色
                                _this.setFontColor(command, t);
                                
break;
                            
case 'autoLay'// 自动排版
                                _this.autoLay();
                                
break;
                            
default:
                                alert(
'没有提供此项功能');
                                
break;
                        }
                    }
                }
            }
        };
        
// 样式切换
        this.bar.onmouseup = function(e) { _this.changeSty(co.target(e), 'curr'); };
        
this.bar.onmouseover = function(e) { _this.changeSty(co.target(e), 'hover'); };
        
this.bar.onmouseout = function(e) { _this.changeSty(co.target(e), 'curr'); };
    },
    
// 样式切换
    changeSty: function(t, sign) {
        
if(t.tagName == 'SPAN') {
            
if(sign == 'curr') {
                t.className 
= this.bardata[t['index']]['class'];
                
this.currActive = null;
            } 
else {
                
if(!!this.currActive) {
                    
this.currActive.className =  this.bardata[this.currActive['index']]['class'];
                } 
                t.className 
= 'tag ' +  this.bardata[t['index']][sign];
                
this.currActive = t;                
            }
        }
    },

    
// 抽象需要弹窗功能的公用接口
    setPopInit: function(command, tar) {
        
var cfg = '', _body = '', _td = '', S = '';
        
if(command == 'fontSize') {
            cfg 
= {
                
'id''fscon',
                
'title''字号',
                
'container''fscon',
                
'concss''fsn',
                
'heacss''fshead',
                
'bodcss''fsbody',
                
'chicss': ['bas f1''bas f2''bas f3''bas f4''bas f5''bas f6''bas f7'],
                
'content': ['字号1''字号2''字号3''字号4''字号5''字号6''字号7']
            };    
            
for(var i = 0, l = cfg.content.length; i < l; i++) {
                _body 
+= '<a class="' + cfg.chicss[i] + '" href="javascript:void(0);">' + cfg.content[i] + '</a>';
            }
        }
        
if(command == 'fontName') {
            cfg 
= {
                
'id''ffcon',
                
'title''字体',
                
'container''ffcon',
                
'concss''fsn',
                
'heacss''fshead',
                
'bodcss''fsbody',
                
'chicss': ['bas''bas''bas''bas''bas''bas''bas''bas''bas'],
                
'content': ['宋体''黑体''楷体''隶书''幼圆''Arial''Georgia''Verdana''Helvetica']        
            };    
            
for(var i = 0, l = cfg.content.length; i < l; i++) {
                _body 
+= '<a class="' + cfg.chicss[i] + '" href="javascript:void(0);">' + cfg.content[i] + '</a>';
            }
        }
        
if(command == 'createLink' || command == 'insertImage' || command == 'insertEmotion') { // 创建链接 + 插入图片 + 插入表情形体类似. 只需要单独定制id和title即可
            cfg = {'container':'flcon''concss':'fsn''heacss':'fshead''bodcss':'fsbody'};
            
if(command == 'createLink') { cfg.title = '插入链接';    /*title*/cfg.id = 'fflink';/*容器id*/cfg.txtId = 'lurl';    /*文本框id*/cfg.cofbtnId = 'lkcof';/* 确认按钮*/cfg.celbtnId = 'lkcel';}/*撤销按钮*/
            
if(command == 'insertImage') { cfg.title = '插入图片';cfg.id = "ffimage";cfg.txtId = 'limg';cfg.cofbtnId = 'imcof';cfg.celbtnId = 'imcel';} 
            
if(command == 'insertEmotion') { cfg.title = '插入表情';cfg.id = "ffemotion";cfg.container="emotionCon"; }
            
if(command == 'createLink' || command == 'insertImage') {
                _body 
+= '<div style="padding:7px;background-color:#FFF;font-size:12px;"><span>链接地址</span>';
                _body 
+= '<input type="text" id="' + cfg.txtId + '" style="width:200px;" /></div>';
                _body 
+= '<div style="text-align:center;">'
                _body 
+= '<img id="' + cfg.cofbtnId + '" style="padding-right:10px;" src="'+co.Root+'o_220836549.p.gif" />';
                _body 
+= '<img id="' + cfg.celbtnId + '" src="'+co.Root+'o_220726721.p.gif" /></div>';
            }
            
if(command == 'insertEmotion') {
                
for(var i = 0, l = this.emotion.length; i < l; i++) {
                    S 
+= ';background-position:' + this.emotion[i]['pos'+ ';width:21px;height:21px;'
                    _td 
+= '<td unselectable="on" url="'+this.emotion[i].url+'" title='+this.emotion[i]['title']+' style="cursor:pointer;background-image:url('+co.Root+'o_220510752_p.gif)'+S+'"></td>';
                }
                _body 
+= '<table><tr>'+ _td +'</tr></table>';
            }
        }
        
if(command == 'insertHTML') {
            cfg 
= {
                        
'id':'fftable','title':'插入表格','container':'isbtlCon','concss':'fsn','heacss':'fshead',
                        
'bodcss':'fsbody','rowId':'rowtxt','cellId':'celltxt','cfmId':'tblcfm','celId':'tblcel',
                        
'tblwId':'tblwid','tblhId':'tblhid'    
                  };
            _body 
+= '<div class="tblCon">行数<input type="text" id="'+cfg.rowId+'" class="tblTxt" />列数<input type="text" id="'+cfg.cellId+'" class="tblTxt" /></div>';
            _body 
+= '<div class="tblCon">表格的宽度<input type="text" id="'+cfg.tblwId+'" class="tblTxt" />px</div>';
            _body 
+= '<div class="tblCon">表行的高度<input type="text" id="'+cfg.tblhId+'" class="tblTxt" />px<div class="tblbtn">';
            _body 
+= '<img id="'+cfg.cfmId+'" style="padding-right:6px;" src="'+co.Root+'o_220836549.p.gif" />';
            _body 
+= '<img id="'+cfg.celId+'" src="'+co.Root+'o_220726721.p.gif" /></div></div>';
        }
        
this.setPopRun(command, cfg, cfg.title, tar, _body);
    },
    
// 实现POP弹窗的所有功能
    setPopRun: function(command, cfg, title, tar/* 点击的控件 */, content/* POP弹窗的body内容 */) {
        
var _this = this;
        
var fwin = newFactory.create(popUp.create, cfg, content);
        _this.fixPop(fwin, tar);    
// 定位弹窗
        if(title == '插入链接' || title == '插入图片') { /* 插入链接和插入图片需要特殊定制 */
            co.getId(cfg.cofbtnId).onclick 
= function() { // 此处不用addEvent添加事件.避免多次绑定
                var _val = co.getId(cfg.txtId).value;
                
if(_val.length == 0) _val = ' '// IE下链接可以为空.但其他最起码有一个空格.否则报错
                _this.doEditCommand(command, _val);
                co.getId(cfg.id).style.display 
= "none"
            }; 
//确认
            co.getId(cfg.celbtnId).onclick = function() { co.getId(cfg.id).style.display = "none"; }
        }
        
if(title == '插入表格') {
            co.getId(cfg.cfmId).onclick 
= function() {
                
var _html = _this.createTblHtml(cfg);
                
if(!co.browser.ie) { // IE不支持insertHTML
                    _this.doEditCommand(command, _html);
                } 
else {
                    _this.ifr.contentWindow.focus(); 
// 注意IE下 focus问题
                    _this.doc.selection.createRange().pasteHTML(_html);
                }
                co.getId(cfg.id).style.display 
= 'none';
            };
            co.getId(cfg.celId).onclick 
= function() { co.getId(cfg.id).style.display = 'none'; }
        }
        _this.hidePop(fwin, title); 
// bind隐藏弹窗
        fwin.onclick = function(e) {
            
var t = co.target(e);
            
if(title == '插入链接' || title == '插入图片' || title == '插入表格') { co.cancelBubble(e); } // 插入链接和图片禁止冒泡
            if(t.tagName == 'A') { /* 字号和字体 */
                _this.doEditCommand(command, command 
== 'fontSize' ? t.innerHTML.slice(-1) : t.innerHTML);
            } 
else if(t.tagName == 'TD') { /* 表情 */
                _this.doEditCommand(
'insertImage', t.getAttribute('url'));
            }
        };    
    },
    
// 设置字体颜色 
    setFontColor: function(command, tar) {
        
var _this = this;
        
var fwin = newFactory.create(ColorPicker.create, {'id':'colorPicker'});
        _this.fixPop(fwin, tar);    
// 定位弹窗
        _this.hidePop(fwin, '文字颜色');
        co.addEvent(fwin, 
'click'function(e) {
            
var t = co.target(e);
            
if(!!t.title) {
                _this.doEditCommand(command, t.title);
            }
        });
    },
    
// 自动排版
    autoLay: function() {
        
var _child = this.doc.body.childNodes;
        
for(var i = 0, l = _child.length; i < l; i++){
            
if(_child[i].tagName == 'DIV' || _child[i].tagName == 'P') {
                _child[i].style.textIndent 
= _child[i].style.textIndent == '2em' ? '' : '2em'// text-indent属性
            }
        }
    },
    
// 生成Table的HTML
    createTblHtml: function(cfg) {
        
var _rownum = co.getId(cfg.rowId).value, _cellnum = co.getId(cfg.cellId).value,
            _tblwid 
= co.getId(cfg.tblwId).value, _tblhei = co.getId(cfg.tblhId).value;    
        
var _html = '<table border="1" width="'+_tblwid+'">';
        
for(var i = 0; i < parseInt(_rownum,10); i++) { // 行
            _html += '<tr height="'+_tblhei+'">';
            
for(var j = 0; j < parseInt(_cellnum,10); j++) { // 列
                _html += '<td></td>';
            }
            _html 
+= '</tr>';
        }    
        _html 
+='</table>';
        
return _html;
    },
    
// 保存快照用于IE定位
    saveBookMark: function() {
        
var _this = this;
        co.addEvent(_this.ifr, 
'beforedeactivate'function() {
            
var rng = _this.doc.selection.createRange();
            
if(rng.getBookmark) {
                _this.bookmark 
= _this.doc.selection.createRange().getBookmark(); // 保存光标用selection下的createRange();
            }
        });
        co.addEvent(_this.ifr, 
'activate'function() {
            
if(_this.bookmark) {
                
// Moves the start and end points of the current TextRange object to the positions represented by the specified bookmark.
                // 将光标移动到 TextRange 所以需要用 body.createTextRange();
                var rng = _this.doc.body.createTextRange();                
                rng.moveToBookmark(_this.bookmark);
                rng.select();
                _this.bookmark 
= null;
            }
        });
    },
    
// 定位弹窗 
    fixPop: function(fwin, tar) {
        co.setProperties(fwin, {
'style''top:' + (co.getPos(tar).y + tar.offsetHeight) + 'px; left:' + co.getPos(tar).x + 'px' });
    },
    
// 隐藏弹窗
    hidePop: function(fwin, title) {
        co.addEvent(document, 
'click'function(e) {
            
var t = co.target(e);
            fwin.style.display 
= t.title == title ? 'block' : 'none';
        });
        co.addEvent(
this.doc, 'click'function(e) { fwin.style.display = 'none'; }); /* 注意:绑定iframe事件句柄需要用W3C接口(addEventListener) */    
    },
    
// 执行命令
    doEditCommand: function(name, arg) {
        
try {
            
this.ifr.contentWindow.focus(); // 放置焦点要操作contentWindow
            this.doc.execCommand(name, false, arg);        
        } 
catch(e) {}
    },
    
// 创建隐藏文本域
    createTextArea: function() {
        
this.txtarea = co.append(this.container, 'textarea');
        
this.txtarea.id='bgcode'this.txtarea.style.display = 'none';
    },
    
// 创建空白iframe
    createIframe: function() {
        
var _this = this;
        
this.ifr = co.append(this.container, 'iframe', {'frameborder'0'style''border:0; vertical-align:bottom''class''econtent' });
        
this.doc =  this.ifr.contentDocument || this.ifr.contentWindow.document; // W3C || IE
        this.doc.designMode = 'on';
        
this.doc.open();
        
// margin为了消除iframe中的html上部的空白
        this.doc.write('<html><head><style>body{ margin:3px; word-wrap:break-word; word-break: break-all; }</style></head><body>GoodNessEditor</body></html>');
        
this.ifr.contentWindow.focus();
        
this.doc.close();
        
// 当iframe失去焦点.偷偷将代码存入textare中
        co.addEvent(this.ifr.contentWindow, 'blur'function() {
            _this.txtarea.value 
= _this.doc.body.innerHTML;
        });
    },
    
// 创建编辑器底部
    createToolFoot: function() {
        
var _this = this;
        co.append(
this.container, 'div''efoot').innerHTML = '<input type="checkbox" id="showCode" /><label for="showCode">显示源码</label>';
        
// 绑定显示源码事件
        co.getId('showCode').onclick = function() { 
            
if(this.checked) {
                _this.layer.style.display 
= 'block';
                co.getId(
'bgcode').style.display = 'block';
                _this.ifr.style.display 
= 'none';
            } 
else {
                _this.layer.style.display 
= 'none';
                co.getId(
'bgcode').style.display = 'none';
                _this.doc.body.innerHTML 
= co.getId('bgcode').value;
                _this.ifr.style.display 
= 'block';                
            }
        };
    },
    
// 创建工具栏遮盖层
    createToolLayer: function() {
        
this.layer = co.setProperties(
            co.append(document.body,
'div'), 
            {
                
'style':'width:'+this.bar.offsetWidth+'px;height:'+this.bar.offsetHeight+'px;background-color:#fff;position:absolute;display:none'
            }
        );
        co.setOpacity(
this.layer, this.baroverOpc);
        
this.layer.style.left = co.getPos(this.bar).x + 'px';
        
this.layer.style.top = co.getPos(this.bar).y + 'px';
    }
};

 

使用说明

实例化对象.传递容器ID和工具栏的具体定制

var bardata = [
    {
'index'0'title''加粗''class''tag curr0''hover''curr0_hover''active''curr0_active''unselectable''on''command''bold' },
    {
'index'1'title''斜体''class''tag curr1''hover''curr1_hover''active''curr1_active''unselectable''on''command''italic' },
    {
'index'2'title''下划线''class''tag curr2''hover''curr2_hover''active''curr2_active''unselectable''on''command''underline' },
    {
'index'3'title''字号''class''tag curr3''hover''curr3_hover''active''curr3_active''unselectable''on''command''fontSize' },
    {
'index'4'title''字体''class''tag curr4''hover''curr4_hover''active''curr4_active''unselectable''on''command''fontName' },
    {
'index'5'title''文字颜色''class''tag curr5''hover''curr5_hover''active''curr5_active''unselectable''on''command''foreColor' },
    {
'index'6'title''插入链接''class''tag curr6''hover''curr6_hover''active''curr6_active''unselectable''on''command''createLink'},
    {
'index'7'title''剪贴''class''tag curr7''hover''curr7_hover''active''curr7_active''unselectable''on''command''cut' },
    {
'index'8'title''复制''class''tag curr8''hover''curr8_hover''active''curr8_active''unselectable''on''command''copy'},
    {
'index'9'title''左对齐''class''tag curr9''hover''curr9_hover''active''curr9_active''unselectable''on''command''justifyLeft' },
    {
'index'10'title''居中对齐''class''tag curr10''hover''curr10_hover''active''curr10_active''unselectable''on''command''justifyCenter' },
    {
'index'11'title''右对齐''class''tag curr11''hover''curr11_hover''active''curr11_active''unselectable''on''command''justifyRight' },
    {
'index'12'title''项目符号''class''tag curr12''hover''curr12_hover''active''curr12_active''unselectable''on''command''insertUnorderedList' },
    {
'index'13'title''编号''class''tag curr13''hover''curr13_hover''active''curr13_active''unselectable''on''command''insertOrderedList' },
    {
'index'14'title''插入表格''class''tag curr14''hover''curr14_hover''active''curr14_active''unselectable''on''command''insertHTML'},
    {
'index'15'title''减少缩进''class''tag curr15''hover''curr15_hover''active''curr15_active''unselectable''on''command''outdent' },
    {
'index'16'title''增加缩进''class''tag curr16''hover''curr16_hover''active''curr16_active''unselectable''on''command''indent'},
    {
'index'17'title''清除样式''class''tag curr17''hover''curr17_hover''active''curr17_active''unselectable''on''command''removeFormat'},
    {
'index'18'title''插入图片''class''tag curr18''hover''curr18_hover''active''curr18_active''unselectable''on''command''insertImage'},
    {
'index'19'title''插入表情''class''tag curr19''hover''curr19_hover''active''curr19_active''unselectable''on''command''insertEmotion'},
    {
'index'20'title''自动排版''class''tag curr20''hover''curr20_hover''active''curr20_active''unselectable''on''command''autoLay' },
];

然后调用

new editor('container', bardata);

至此一个简易的富文本编辑器就编写完毕了.由于时间有限以及技术的不成熟.出现BUG再所难免.如有问题欢迎大家指出.

 

 

参考文档 

https://developer.mozilla.org/en/Rich-Text_Editing_in_Mozilla

http://help.dottoro.com/ljalggen.php

http://www.cnblogs.com/rubylouvre/archive/2009/08/04/1537164.html    部分代码参考

http://topic.csdn.net/u/20090720/09/164e8d7f-9818-4a77-9cc0-419378dab234.html?9951   样式参考

 

源码下载

posted @ 2010-03-25 13:52  GoodNess  阅读(18485)  评论(16编辑  收藏  举报