为了给ikuku.cn社区加上同样的功能,花了一点时间实现了QQ空间的评论框,有想学习编辑器的实现原理的朋友,可以认真阅读代码。 需要包含jQuery。
这个编辑框可以在任意位置输入@时,弹出@人名的补全内容,并且只有@正确的人名,才会给服务器端生成数据。删除时,也是一次删掉人名,不会发生@joson 退格一下变成 @joso 这种。 可以给用户不会出错的愉快体验。
var commentEditor = function(id){
this.editorContainer = $('#'+id);
this.editorPlaceholder = $('<div class="comment_editor_placeholder"></div>');
this.editor = $('<div class="comment_editor_main"></div>');
this.currentEl = null;
this.currentOffset = null;
this.offsetLeft = 0;
this.editorStatus = 0;
this.op = {
placeholder
: '在这里输入评论',
padding : 5
};
this.editorPlaceholder.css({
width : this.editorContainer.width() - this.op.padding*2,
height : this.editorContainer.height() - this.op.padding*2,
position:'relative'
});
this.editor.css({
overflow : 'auto',
width : this.editorContainer.width() - this.op.padding*2,
height : this.editorContainer.height() - this.op.padding*2,
position:'relative',
marginTop : -this.editorContainer.height()
});
this.editor.attr('contenteditable',true);
this.editorContainer.append(this.editorPlaceholder);
this.editorContainer.append(this.editor);
this.editorPlaceholder.text(this.op.placeholder);
this.editorContainer.addClass('comment_editor');
if( this.getData() != '' ){
this.editorPlaceholder.text('');
}
this.list = new commentEditor.downList(this);
this._init();
this.listenAt();
};
commentEditor.prototype = {
_init : function(){
var self = this;
this.listenChange();
$(document).bind('click',function(){
self.list.hide();
if( self.editorStatus == 1 ){
self.editorStatus = 0;
self.editor.trigger('editor.close');
}
});
this.editor.bind('click',function(e){
e.stopPropagation();
});
this.editor.bind('editor.change',function(){
self.listenCursor();
});
this.editor.bind('mouseup',function(){
self.listenCursor();
});
this.editor.bind('focus',function(){
self.editorPlaceholder.text('');
if( self.editorStatus == 0 ){
self.editorStatus = 1;
self.editor.trigger('editor.open');
}
});
this.editor.bind('blur',function(){
if( self.getData() == '' ){
self.editorPlaceholder.text(self.op.placeholder);
}
});
},
listenAt : function(){
var self = this,timeout=null,currentname=null;
this.editor.bind('editor.at',function(e,data){
self.offsetLeft = String(data).length + 1 ;
clearTimeout(timeout);
if( currentname == data ){
self.list.show();
return ;
}
currentname = data;
timeout = setTimeout(function(){
$.post('/test_comment_exe.php',{action:'at',name:data},function(data){
if( data != '' ){
self.list.show();
self.list.setData(data,function(data){
var op = $('<div><a href="javascript:">\
<div style="float:left;width:30px;height:30px;"><img src="/profile_image/'+data.ID+'/30" width="30" height="30" /></div>\
<div style="float:left; margin-left:10px;">'+data.user_nicename+'</div></a>\
</div>');
op.bind('click',function(){
self.insertHtml('<button contenteditable="false" class="comment_editor_element">@'+data.user_nicename+'</button>',true);
self.list.hide();
});
return op;
});
}else{
self.list.hide();
}
});
},100);
});
this.editor.bind('editor.at_close',function(){
clearTimeout(timeout);
self.list.hide();
});
},
listenChange : function(){
var self = this,oldText=this.editor.html();
this.editor.bind('keyup',function(e){
var newText = self.editor.html();
if( newText != oldText ){
oldText = newText;
self.editor.trigger('editor.change');
}
});
},
getSelectionObject : function(){
if (window.getSelection){ // mozilla FF
return window.getSelection();
}else if (document.getSelection){
return document.getSelection();
}else if (document.selection){ //IE
return document.selection.createRange().text;
}
},
getRangeObject : function(selectionObject) {
return selectionObject.getRangeAt(0);
},
listenCursor : function(){
var userSelection = this.getSelectionObject();
if( !userSelection.anchorNode.data || userSelection.type == 'None' || userSelection.type == 'Range' ){
this.editor.trigger('editor.at_close');
return ;
}
this.currentOffset = userSelection.anchorOffset;
this.currentEl = userSelection.anchorNode;
var prevText = userSelection.anchorNode.data.substring( 0, userSelection.anchorOffset );
var lastDot = prevText.lastIndexOf('@');
if( lastDot != -1 ){
prevText = prevText.substring(lastDot);
if( !prevText.match(/\s/) ){
this.editor.trigger('editor.at',[ prevText.substring(1)]);
return ;
}else if( prevText.match(/^[^\s]+\s$/) ){
this.editor.trigger('editor.at_apply',[prevText.substring(1,prevText.length-1)]);
}
}
this.editor.trigger('editor.at_close');
},
insertHtml : function(html,lastspace){
var offsetLeft = this.offsetLeft;
this.offsetLeft = 0;
if( this.currentEl != null && this.currentOffset != null ){
this.editor.focus();
var userSelection = this.getSelectionObject();
var range = this.getRangeObject(userSelection);
var el = $(html);
var el_dom = el.get(0);
if( !el_dom ){
el_dom = document.createTextNode(html);
}
range.setStart(this.currentEl,this.currentOffset - offsetLeft );
range.setEnd(this.currentEl,this.currentOffset );
range.deleteContents();
range.insertNode( el_dom );
range.setStartAfter( el_dom, 0 );
range.setEndAfter( el_dom , 0 );
if(lastspace){
var space = document.createTextNode('\u00A0');
range.insertNode( space );
range.setStartAfter( space, 0 );
range.setEndAfter( space , 0 );
}
if( navigator.userAgent.indexOf('Chrome') != -1 ){
if( el_dom.nextSibling == el_dom.parentNode.lastChild && el_dom.nextSibling.data==''){
range.insertNode($('<br/>').get(0));
}
}
if( userSelection.setPosition )
userSelection.setPosition(range.startContainer,range.startOffset);
else{
var nrange = this.getRangeObject(userSelection);
nrange.setStart(range.startContainer,range.startOffset);
nrange.setEnd(range.startContainer,range.startOffset);
userSelection.removeAllRanges();
userSelection.addRange(nrange);
}
this.currentOffset = range.startOffset;
this.currentEl = range.startContainer;
}else{
//console.log( $(html) );
this.editor.html( this.editor.html() + html );
this.editor.focus();
var userSelection = this.getSelectionObject();
var range = this.getRangeObject(userSelection);
range.setStartAfter( this.editor.get(0).lastChild, 0 );
range.setEndAfter( this.editor.get(0).lastChild, 0 );
if( userSelection.setPosition ){
userSelection.setPosition(range.startContainer,range.startOffset);
}else{
var nrange = this.getRangeObject(userSelection);
nrange.setStart(range.startContainer,range.startOffset);
nrange.setEnd(range.startContainer,range.startOffset);
userSelection.removeAllRanges();
userSelection.addRange(nrange);
}
this.currentOffset = range.startOffset;
this.currentEl = range.startContainer;
}
},
getData : function(){
return this.editor.html();
},
bind : function(eventName,callback){
this.editor.bind(eventName,callback);
}
};
commentEditor.downList = function(container){
this.container = container;
this.list = $('<div class="comment_editor_list"></div>');
this.list.css({
position:'absolute',
width: this.container.editor.width()+this.container.op.padding*2 - 2,
top:this.container.editor.offset().top + this.container.editor.height()+this.container.op.padding*2,
left:this.container.editor.offset().left
});
this.status = 'show';
this.list.hide();
$(document.body).append(this.list);
this.list.bind('click',function(e){
e.stopPropagation();
});
};
commentEditor.downList.prototype = {
show : function(){
this.status = 'show';
this.list.show();
},
hide : function(){
this.list.hide();
},
setData : function(data,callback){
var self = this;
this.list.find('*').remove();
var data = $.parseJSON(data);
if(data){
$.each(data,function(i){
if(callback)
self.list.append(callback(data[i]));
});
}
}
};