有偿请高手解决一个问题:添加 @user 回复的功能

2014-08-07 20:10:48 +08:00
 zhiyongyici
我使用wordpress+bbpress 搭建了一个小型社区网站(风格仿了v2ex),并且使用了 simditor 编辑器。现在想用js实现 @user 功能。请高手给予帮助。

作为回报,我可以付费,或者赠送本站半年的广告(自然志全站)。

实现功能:增加 @user 功能。
使用的程序:wordpress+bbpress插件。
地址: http://ziranzhi.com/bbs
测试账户:ceshi
密码:cc5228600

方便的话可以加QQ详谈。
11061-3846
6338 次点击
所在节点    问与答
59 条回复
spance
2014-08-07 20:19:06 +08:00
这个通常都是服务端来做的。

用类似这样的正则来提取 @(\w+)\b
提取到以后,拼到sql where里面检查是不是有这个人,查询输出的是存在的用户;
然后对这些用户的消息表里面写新消息,具体写什么就你自己定了,或者发到客户端。

这是一个精简的逻辑结构,你可以补充你的业务上去。
zhiyongyici
2014-08-07 20:21:08 +08:00
@spance 我不是很懂的,你可以帮忙实现吗?
spance
2014-08-07 20:26:19 +08:00
@zhiyongyici 不好意思,我不搞php的东西。
zts1993
2014-08-07 20:38:55 +08:00
text上js监听输入。检测到@ 之后ajax 加载用户,每输入一个字或者字母都查询一次,然后显示出来。。。
zhiyongyici
2014-08-07 20:48:12 +08:00
@zts1993 能帮忙实现一下吗?
zhiyongyici
2014-08-07 20:50:02 +08:00
@zts1993 其实就类似 v2ex 这样,点击回复 编辑窗口出现 @user 即可。你们说的我感觉有点复杂。
qq5775548
2014-08-07 21:07:26 +08:00
这个说起来简单 实际操作起来 要注意的问题挺多的:
方法同上面说的基本一样,就像微博@TA, 弹出输入框。
1: 控制textarea本身网上大把,记得要兼容ie 所以找个好点得小型库吧,insert,insertAfterStart, insertAfterSelect, del(删除x-y位置的字), getSelectText, getpos, select,selectAll,selectString,getCursorOffset 一些常用的函数还是要写得 juquery 貌似没有这些功能,至少我找不到。
2: 确定光标位置(getCursorOffset函数),因为具体pageX/Y 无法通过直接形式获得,常用方法建一个大小完全跟textarea一样的div 里面 的html跟 textarea 一样,记着要br来实现换行,然后在结尾添加一个自定义 span或其他标签 然后获取这个标签的位置就可以获取其相对于页面的位置鸟
3: 获取@ 的对象,这个形式是 "@mygirl" 当后面还有一大段的时候 就如: "@mygril 草泥马" 你应该确定我@的是mygril 而不是 mygril 草泥马。此外还需确定是当前光标对应前面的@ 而非后面或开始的@。这里还处理到点会之前的@, 修改@的人,要将@后面的人名字删除然后替换成新的人。
4还有几点应该都是小问题,努力尝试下做吧,主要注意就这个点
mahone3297
2014-08-07 21:13:45 +08:00
好奇这个功能多少钱。。。
不过这个要看效果做成如何。如果只是@还好一点,如果要输入人名的时候,自动提示,那又要麻烦一点。
Sunyanzi
2014-08-07 21:18:59 +08:00
提前占位吧 ... 如果到这周六还没人接的话这活我做了 ...

V2 做 php 的人不少 ... 有其他人想接的话只管接 ... 我只是提供个兜底而已 ...
zhiyongyici
2014-08-07 21:22:00 +08:00
@qq5775548 谢谢,虽然我不是很懂,但是可以给后面的人提供帮助。
skyshy
2014-08-07 21:22:40 +08:00
zhiyongyici
2014-08-07 21:24:30 +08:00
@mahone3297 不用自动提示,和 V2 一样,点击回复按钮,输入框出现 @user 即可。另外,我现在的问题可能更加简单,如果不用可视化编辑 ,点击回复按钮会出现 <a href="">@user</a> 的代码,如果使用了可视化编辑器就没有反应。
zhiyongyici
2014-08-07 21:25:15 +08:00
@skyshy 这个我看了,不是很喜欢这种方式。
greatdk
2014-08-07 21:33:18 +08:00
我以为我屏幕右下角钻了一直虫子、、、、
zhiyongyici
2014-08-07 21:33:42 +08:00
@greatdk 哈哈,都这么认为,还有人专门拿抹布去擦呢~~
qq5775548
2014-08-07 21:35:21 +08:00
我贴份2年前的代码吧 这里没有md 格式化样子不太好 可能要分开来看 回复两段代码吧,认为我倒米的可以吐槽。但烂代码没什么意义。希望对你有用,一直都没怎么理,找找代码,还是在一个旧项目中找到:一个jquery插件包括 emot @ 的实现 可以扩展更多的其他功能。
qq5775548
2014-08-07 21:35:38 +08:00
;(function() {
var ShareBox = function() {};
ShareBox.TextEdit = function() {};
ShareBox.Emotion = function() {};
ShareBox.SuggestBox = function() {};
ShareBox.SuggestBox = function() {};
ShareBox.SuggestBox.Box = function() {};

var KEY_CODE = {
32: [' '],

48: ['0', ')'],
49: ['1', '!'],
50: ['2', '@'],
51: ['3', '#'],
52: ['4', '$'],
53: ['5', '%'],
54: ['6', '^'],
55: ['7', '&'],
56: ['8', '*'],
57: ['9', '('],

65: ['a', 'A'],
66: ['b', 'B'],
67: ['c', 'C'],
68: ['d', 'D'],
69: ['e', 'E'],
70: ['f', 'F'],
71: ['g', 'G'],
72: ['h', 'H'],
73: ['i', 'I'],
74: ['j', 'J'],
75: ['k', 'K'],
76: ['l', 'L'],
77: ['m', 'M'],
78: ['n', 'N'],
79: ['o', 'O'],
80: ['p', 'P'],
81: ['q', 'Q'],
82: ['r', 'R'],
83: ['s', 'S'],
84: ['t', 'T'],
85: ['u', 'U'],
86: ['v', 'V'],
87: ['w', 'W'],
88: ['x', 'X'],
89: ['y', 'Y'],
90: ['z', 'Z'],

96: ['0'],
97: ['1'],
98: ['2'],
99: ['3'],
100: ['4'],
101: ['5'],
102: ['6'],
103: ['7'],
104: ['8'],
105: ['9'],

106: ['*'],
107: ['+'],
109: ['-'],
110: ['.'],
111: ['/'],

186: [';', ': '],
187: ['=', '+'],
188: [',', '<'],
189: ['-', '_'],
190: ['.', '>'],
191: ['/', '?'],
192: ['`', '~'],

219: ['[', '{'],
220: ['\'', '"'],
221: [']', '}'],
222: ['', '"']
};

var OPTIONS = {
ShareBox: {
label: "<li data-link='{datas}'><a>{view}</a></li>"
},
Emotion: {
tagPrefix: '[',
tagSuffix: ']',
perPage: 60,
autoHide: true,
autoReset: true,
defaultNav: '默认',
path: ('object' === typeof $.EmotionOptions ? $.EmotionOptions.path : false) || {'默认':{}},
emot: ('object' === typeof $.EmotionOptions ? $.EmotionOptions.emot : false) || {'默认':{}}
}
};

var trace = function(msg, type) {
if ('object' === typeof console) {
type = type || 'log';
console[type](msg);

} else if ('object' === typeof opera) {
opera.postError(msg);

} else if ('object' === typeof java && 'object' === typeof java.lang) {
java.lang.System.out.println(msg);
}
};

var toJSON = function(obj) {
switch (typeof(obj)) {
case 'object':
var ret = [];
if (obj instanceof Array) {
for (var i = 0, len = obj.length; i < len; i++) {
ret.push(toJSON(obj[i]));
}

return '[' + ret.join(',') + ']';

} else if (obj instanceof RegExp) {
return obj.toString();

} else {
for (var a in obj) {
ret.push("\"" + a + "\"" + ':' + toJSON(obj[a]));
}

return '{' + ret.join(',') + '}';
}
case 'function':
return 'function() {}';

case 'number':
return obj.toString();

case 'string':
return "\"" + obj.replace(/(\\|\")/g, "\\$1").replace(/\n|\r|\t/g, function(a) {
return("\n" == a) ? "\\n" : ("\r" == a) ? "\\r" : ("\t" == a) ? "\\t" : "";
}) + "\"";

case 'boolean':
return obj.toString();

default:
return obj.toString();
}
};

var getIndex = function(obj, index) {
var k = 0;
for (var i in obj) {
if (index === k ++) {
return i;
}
}
};
qq5775548
2014-08-07 21:36:11 +08:00
ShareBox.TextEdit.prototype = {
_constructor: function(field) {
this.field = $(field).get(0);
return this;
},
getLenInCh: function() {
var str = this.field.value;
var ch = str.match(/[\u4E00-\uFA29]/ig);
var en = str.match(/[^\u4E00-\uFA29]/ig);
var cl = ch ? ch.length * 2 : 0;
var el = en ? en.length : 0;

return Math.ceil((cl + el) / 2);
},
insert: function(value, type) {
var field = this.getField();
value = value.toString();

if (document.selection) { //IE
field.focus();
var seltext = this.getSelectText(field),
startPos = 'string' === typeof seltext ? field.value.length - seltext.length : field.value.length,
sel = document.selection.createRange();
sel.text = value;
sel.select();

if (type) {
var rng = field.createTextRange();

if (type == 'select') {
rng.moveStart('character', startPos);

} else if (type == 'start') {
rng.moveEnd('character', - value.length);
rng.moveStart('character', startPos);
}

rng.select();
}

} else if (field.selectionStart || field.selectionStart == '0') { //^IE
var startPos = field.selectionStart, endPos = field.selectionEnd,
restoreTop = field.scrollTop;

field.value = field.value.substring(0, startPos) + value + field.value.substring(endPos, field.value.length);
if (restoreTop > 0) field.scrollTop = restoreTop;

field.focus();
if (type == 'select') {
field.selectionStart = startPos;
field.selectionEnd = startPos + value.length;

} else if (type == 'start') {
field.selectionStart = field.selectionEnd = startPos;

} else {
field.selectionStart = field.selectionEnd = startPos + value.length;
}

} else{ //others
field.value += value;
field.focus();
}

return this;
},
insertAfterStart: function(value) {
this.insert(value, 'start');
return this;
},
insertAfterSelect: function(value) {
this.insert(value, 'select');
return this;
},
del: function(del_num) {
var field = this.getField();
var pos = this.getPos(field);
if (pos.startPos == 0) { //如果位置为0, 则会 自动在加上匹配内容;
return false;
}

var ft = field.scrollTop;
var val = field.value;
field.value = del_num > 0 ? val.slice(0, pos - del_num) + val.slice(pos): val.slice(0, pos) + val.slice(pos - num);
setPos(field, pos - (del_num < 0 ? 0 : del_num));

setTimeout(function() {
if (field.scrollTop != ft) field.scrollTop = ft;
}, 10);

return this;
},
getSelectText: function() {
var field = this.getField();
field.focus();

if (typeof document.selection != 'undefined') {
return document.selection.createRange().text;
}

if (field.selectionStart || field.selectionStart == '0') {
return field.value.substr(field.selectionStart, field.selectionEnd - field.selectionStart);
}
},
getPos: function() {
var field = this.getField();
if (document.selection) {
field.focus();

var rng = document.selection.createRange();
var tx_rng = document.body.createTextRange();
tx_rng.moveToElementText(field);

for (var startPos = 0; tx_rng.compareEndPoints('StartToStart' , rng) < 0; startPos ++) {
tx_rng.moveStart('character', 1);
}

for (var endPos = 0; tx_rng.compareEndPoints('StartToEnd' , rng) < 0; endPos ++) {
tx_rng.moveStart('character', 1);
}

return {
startPos: startPos,
endPos: endPos
};

} else if (field.selectionStart || field.selectionStart == '0') {
return {
startPos: field.selectionStart,
endPos: field.selectionEnd
};
}
},
select: function(start_pos, end_pos) {
var field = this.getField();
if (start_pos == undefined || end_pos == undefined || start_pos < 0 || end_pos > field.value.length) {
return false;
}

if (document.selection) { //IE
var rng = field.createTextRange();
rng.moveEnd('character', - field.value.length);
rng.moveEnd('character', end_pos);
rng.moveStart('character', start_pos);
rng.select();

} else { //^IE;
field.setSelectionRange(start_pos, end_pos);
field.focus();
}

return this;
},
setPos: function(pos) {
this.select(pos , pos);
return this;
},
selectAll:function() {
var field = this.getField();
this.select(0, field.value.length);
return this;
},
selectString: function(str) {
var field = this.getField();
var index = field.value.indexOf(str);
return index != -1
? this.select(field, index, index + str.length)
: false;
},
getCursorOffset: function(cursor_pos) {
var field = this.getField();
if (document.selection) {
var range = document.selection.createRange();
var $win = $(window);

return {
left: range.boundingLeft + $win.scrollLeft(),
top: range.boundingTop + $win.scrollTop() + range.boundingHeight
};
}

var $field = $(field),
$editor = $field.shareBox(),
w = $field.width(),
h = $field.height(),

pos = $field.offset(),
x = pos.left,
y = pos.top,

end_pos = $editor.getPos().endPos,
str = $field.val(),
start_str = str.substr(0, end_pos).replace(/[(^*\n*)|(^*\r*)]/g, '<br />'),
end_str = str.substr(end_pos, str.length).replace(/[(^*\n*)|(^*\r*)]/g, '<br />'),

fontSize = $field.css('fontSize'),
padding = $field.css('padding'),
lineHeight = $field.css('lineHeight'),
overflow = $field.css('overflow'),
scrollTop = $field.scrollTop();

if (! this.fake) this.fake = $('<div>').appendTo('body');

this.fake.html(start_str + '<span>x</span>' + end_str)
.css({
padding: padding,
width: w, height: h,
opacity: 0,
overflow: overflow,
position: 'absolute',
left: x, top: y, zIndex: -9999,
lineHeight: lineHeight,
wordWrap: 'break-word',
fontSize: fontSize
})
.scrollTop(scrollTop);

var marker = this.fake.children('span'),
height = marker.outerHeight(),
ofs = marker.offset(),
left = ofs.left,
top = ofs.top,
st = marker.scrollTop();

return {
left: left,
top: top + height - st
};
},
getField: function() {
return this.field;
}
};
qq5775548
2014-08-07 21:36:51 +08:00
ShareBox.Emotion.prototype = {
_constructor: function(options) {
this.nav_list = [];
this.page_list = [];
this.curNav = '';
this.emot_list = {};
this.active = false;
this.config = $.extend({}, OPTIONS.Emotion, options);

var $emot = $('<div class="shareEdit-emot">').bind('click', function(e) {
e.stopPropagation();
});

this.oEmot = $emot.get(0);
this.oNav = $('<ul class="emot-nav">').appendTo(this.oEmot).get(0);
this.oList = $('<ul class="emot-list">').appendTo(this.oEmot).get(0);
this.oPage = $('<ul class="emot-page">').appendTo(this.oEmot).get(0);

this.oEmot = $emot = $('<div class="shareEdit-emot-pure">')
.append(this.oEmot)
.appendTo('body')
.bind('click', function(e) {
e.stopPropagation();
});

//注册导航栏
var self = this;
for (var i in this.config.emot) {
var $emot = $('<li class="emotNav" data-nav="' + i + '"><a title="' + i + '">' + i + '</a></li>')
.appendTo(this.oNav)
.bind('click', function(e) {
e.stopPropagation();

self.nav($(this).attr('data-nav'));
});

this.nav_list.push($emot[0]);
}

//注册分页
for (var i = 1; i < 10; i ++) {
var $page = $('<li class="emotPage" data-page="' + i + '"><a title="' + i + '">' + i + '</a></li>')
.appendTo(this.oPage)
.bind('click', function(e) {
e.stopPropagation();

self.page($(this).attr('data-page'));
})

this.page_list.push($page[0]);
}

if (this.config.autoHide) {
$(document).bind('click', function() {
self.hide();
});
}

return this;
},
emot: function(nav) {
if (! this.config.emot[nav]) return;

var self = this, num = page = 0;
this.emot_list[nav] = [];
for (var i in this.config.emot[nav]) {
if (0 === num % this.config.perPage) {
var $list = $('<ul class="emotList"></ul>')
.appendTo(this.oList);

this.emot_list[nav][page ++] = $list[0];
}

$('<li class="ShareBoxEmotIcons" data-emot="' + i + '"><a title="' + i + '"><img src="' + self.config.path[nav] + self.config.emot[nav][i] + '" alt="' + i + '"/></a></li>')
.appendTo($list)
.bind('click', function(e) {
e.stopPropagation();

var emot = self.config.tagPrefix + $(this).attr('data-emot') + self.config.tagSuffix;
var $editor = $(self.field)
$editor.shareBox().insert(emot)
$editor.keyup();
/**
* 上面 autoHide 已在window 绑定 hide事件
* 同时更好处理 在window click warpper hide 出现问题
* 即 <div>...<emots dialog><div>
* $(document).bind('click', function(){$(div).hide()})
* $(div).live('click', function(){return false;})
* 此时 点击表情 div 不会消失 因此将 document 设置成 全局方法载体
*/
$(document).click();
});

num ++;
}

//补空
while (! (0 === num ++ % this.config.perPage)) {
$('<li class="ShareBoxEmotIcons"><a><img src="' + this.config.path[nav] + 'blank.gif" /></a></li>')
.appendTo($list)
.bind('click', function(e) {
e.stopPropagation();
});
}

return this;
},
showPage: function(nav) {
$(this.page_list).hide()
.filter(':lt(' + this.emot_list[nav].length + ')')
.show();

return this;
},
page: function(page) {
$(this.page_list)
.removeClass('active')
.filter('[data-page="' + page + '"]')
.addClass('active');

for (var i in this.emot_list) {
$(this.emot_list[i]).hide();
}

$(this.emot_list[this.curNav][page - 1]).show();

return this;
},
nav: function(nav) {
if (this.curNav === nav || 'string' !== typeof nav) return;

if (! this.config.emot[nav]) nav = getIndex(this.config.path, 0);

$(this.nav_list).removeClass('active')
.filter('[data-nav="' + nav + '"]')
.addClass('active');

if ('undefined' === typeof this.emot_list[nav]) this.emot(nav);

this.curNav = nav;
this.showPage(nav);
this.page(1);

return this;
},
show: function(field, reset, place) {
var self = this;
this.field = $(field)[0];

//重置或设置 导航
if (reset || this.config.autoReset) {
this.nav(getIndex(this.config.path, 0));
this.page(1);

} else {
this.nav(this.curNav);
}

//居中显示
$(this.oEmot).show();

if (place) this.to(place);
else this.toCenter();

this.active = true;
return this;
},
hide: function() {
$(this.oEmot).hide();

this.active = false;
return this;
},
to: function(place) {
var $win = $(window), $ct = $(this.oEmot), $place = $(place), ofs = $place.offset();
var ww = $win.width(), wh = $win.height(), wl = $win.scrollLeft(), wt = $win.scrollTop();
var cw = $ct.outerWidth(), ch = $ct.outerHeight(), cl = ct = 0;
var pw = $place.outerWidth(), ph = $place.outerHeight(), pl = ofs.left, pt = ofs.top;

if (pl + cw/2 >= wl + ww/1.5) cl = pl + pw - cw;
else cl = pl;

if (pt - ch/2 <= wt + wh/3) ct = pt + ph + 4;
else ct = pt - ch - 4;

$ct.css({left:cl, top:ct});
return this;
},
toCenter: function() {
var $win = $(window), $emot = $(this.oEmot),
wh = $win.height(), ww = $win.width(),
wl = $win.scrollLeft(), wt = $win.scrollTop(),
mh = $emot.outerHeight(), mw = $emot.outerWidth();

$emot.css({
left: (ww - mw)/2 + wl,
top: (wh - mh)/2 + wt
});

return this;
}
};
qq5775548
2014-08-07 21:36:57 +08:00
ShareBox.SuggestBox.prototype = {
label: '<li data-link="{datas}"><a>{view}</a></li>',
uid: 0,
queue: [],
// '@' suggest box
at: (function() {
var FORBID_CODE = [9, 13, 16 ,17 ,18, 27, 38, 40, 229];

//格式化数据
var format = function(datas) {
var s = [];
if ($.isArray(datas)) {
for (var i = 0; i < datas.length; i ++) {
s.push({view: datas[i], datas: {value: datas[i]}})
}

return s;
}

if ($.isPlainObject(datas)) {
for (var i in datas) {
s.push({view: datas[i], datas: {value: datas[i]}})
}

return s;
}

return [];
};

//get string of '@'
//获取 当前光标 对应的 '@xxx' 字符串 并返回其match string, start & end pos
var getAtObject = function(str, cursor_pos) {
var p = l = 0, m = '';
while(! (p <= cursor_pos - 1 && p + l > cursor_pos - 1)) {
p = str.search(/@[^\s\@\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}]*/g);
if (p == -1) {
return;
}

m = str.match(/@[^\s\@\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}]*/g)[0];
l = m.length;
str = str.replace('@', ' ');
}

return {
match: m,
startPos: p,
endPos: p + l
};
};

return function(options) {
var self = this;
var box = new ShareBox.SuggestBox.Box();
box._constructor();

var field = options.field, defaultList = options.defaultList,
url = options.url, name = options.name,
handle = options.ajaxGet;

//弹出自动完成窗口
var popHandle = function() {
var $this = $(this);
var $editor = $this.shareBox();
var v = $this.val(), pos = $editor.getPos();

//不能 match 到 @xxx 形式并定位则隐藏
var at = getAtObject(v, pos.startPos);
if (! ('object' === typeof at)) {
box.hide();
return;
};

var matchStr = at['match']
.replace('@', '')
.replace(/\s/g, '');

//获取 选中的字符串 若有选中字符串 则必须把 match到的字符串进行过滤
//这里 形式为 '@xxx_' 或 '@xxx' 则可以将 '_' 和 select 的字符串进行 /xx$/的替换
var sel = $editor.getSelectText();
if (sel.length > 0) {
var reg = sel.replace(/\s/g, '') + '$';
matchStr = matchStr.replace(new RegExp(reg), '');
}

if (matchStr != '' && 'string' === typeof url) {
//通过ajax 拿数据 source 获取数据后 再进行 生成列表
$.getJSON(url, [{
name: name,
value: matchStr

}], function(r) {
if ($.isFunction(handle)) options.source = handle.call(self, r, format);
else options.source = format(r);

box.at(options, {
startPos: at.startPos,
endPos: at.endPos
});
});

} else {
//刚打 '@' 时读取默认列表
setTimeout(function() {
//若没有 url 则 自己过滤默认数据
if (matchStr != '') {
for (var i = 0, l = defaultList, reg = new RegExp('^' + matchStr, 'im'), s = []; i < l.length; i ++ ) {
if (l[i].match(reg)) {
s.push(l[i]);
}
}
}

options.source = format(s || defaultList);
box.at(options, {
startPos: at.startPos,
endPos: at.endPos
});
}, 1);
}
};

//绑定 textarea 事件
$(field).bind('keydown', function(e) {
if (-1 != $.inArray(e.keyCode, [9])) return false;
})
.bind('keyup', function(e) {
//禁止组合键与功能键
if ('undefined' != typeof e.keyCode && -1 == $.inArray(e.keyCode, FORBID_CODE)) popHandle.apply(this, arguments);
if (KEY_CODE[e.keyCode]) box.hide();
})
.bind('click', function() {
popHandle.apply(this, arguments);
});

return box;
};
})(),
suggest: function(options) {
if (options.type) return this[options.type](options);
}
};

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/126651

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX