公式编辑器 - 带提示下拉框功能 - 动态获取光标位置
引言
应开发需求,需要写一个公式编辑器插件,下面给大家讲一下实现过程。(擦汗,强作淡定,咳,开嗓~)
Html
真的只有一点点~
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>公式编辑器测试</title> <link rel="stylesheet" type="text/css" href="./css/global.css"> <script type="text/javascript" src="./js/jquery-1.8.3.min.js"></script> <script type="text/javascript" src="./js/editTips.js"></script> </head> <body> <textarea id="test" ></textarea> </body> </html>
css
一起给了吧~迟早要给:
/* 编辑器下拉框相关样式 */ table,tr,th,td{padding:0;margin:0;} ul,li,textarea,input{text-decoration:none;list-style:none;margin:0;padding:0;box-sizing: border-box;} input{ outline:none; } .editTips{ padding:5px 0; border-radius: 0!important; box-shadow: 0 2px 4px rgba(0,0,0,.2); max-height: auto; margin:0; z-index:9999; } .editTips li{ text-align:left; box-sizing:border-box; display:block; width:100%; line-height:1.42857143; margin:1px 0; padding:6px 11px; color:#333; cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:400; line-height:1.42857143; border-bottom:solid 1px #e5e5e5; background:#e5e5e5; } .editTips li:last-child{ border-bottom:none; } .active{ background:#fee188!important; } .editTips li.active{ background:#fee188!important; } textarea{ text-decoration: none; list-style: none; margin: 0; padding: 0; display: block; box-sizing: border-box; width: 500px; height: 300px; margin-top: 100px; margin-left: 100px; }
插件模板 - this 为输入框
(function ($) { $.fn.extend({ "editTips": function (options) { var opts = $.extend({}, defaults, options); //使用jQuery.extend 覆盖插件默认参数 return this.each(function () { //这里的this 就是 jQuery对象 }); } }); //默认参数 var defaults = { }; })(window.jQuery);
插入下拉框
这里 this 为输入框,在 html 中,不需要再去js插入,当然这不是问题,在这里我们将 输入框和下拉框都自定义一下,插入的 ul 中 editTips 类可以去掉,为了方便看效果暂时加上
我个人肯定是希望下拉框宽度是可以自定义的: 'width':opts.dropdownWidth, 这里我们定义了第一个参数: 下拉框宽度
1 var _this = this; 2 _this.entertext = $(this); 3 _this.dropdown = $('<ul class="editTips" style="display:none;"></ul>'); 5 $("body").after(_this.dropdown); 6 _this.dropdown.css({ 7 'width':opts.dropdownWidth, 8 'position':'absolute', 9 });
监听文本框
ps:既然是公式编辑器,那说明不会一直触发检索事件,只有在输入了指定字符的时候才会触发检索事件,比如:! @ # 之类,这里我们以 $ 为例,从这里我们可以想到,我们肯定
需要一个参数来动态改变检索事件触发条件,先记在心里
检索流程: 输入框输入值 - 输入特定字符触发检索,如 $ - 调用回调函数,我们这里为 callbacktips 匹配关键字 - 返回包含关键字部分匹配结果,以数组形式 - 遍历数组添加到下拉框 ul 中显示 - 选取当前项
因为其中还涉及到限制请求,方向键切换当前项,这里就不分开说了,直接给出整块代码如下:
// 监听输入框 _this.dropdown.parent().on("keyup",this,function(event){ var nowTime = window.sessionStorage.getItem('nowTime'); // 当前项索引 var n = _this.dropdown.find(".active").index(); // li 个数 var n_max = _this.dropdown.find("li").length; // 注意 event在 firefox 中不能兼容,在方法中带上event参数,如下声明实现兼容 EVT = event || window.event; if( EVT.keyCode == 38 ){ // 方向键控制 li 选项 if(n-1>=0){ _this.dropdown.find('li').eq(n-1).addClass("active").siblings().removeClass("active"); } if( n == 0){ _this.dropdown.find('li').eq(n_max-1).addClass("active").siblings().removeClass("active"); } return false; } // 禁止enter键换行 if( EVT.keyCode == 13 ){ return false; } if( EVT.keyCode == 40 ){ // 方向键控制 li 选项 if(n<n_max-1){ _this.dropdown.find('li').eq(n+1).addClass("active").siblings().removeClass("active"); } if( n+1 == n_max ){ _this.dropdown.find('li').eq(0).addClass("active").siblings().removeClass("active"); } return false; } // 限制请求,输入间隔不超过一秒时不触发检索事件发送请求 if( nowTime){ var oldTime = Date.now(); var nowTime = window.sessionStorage.getItem('nowTime'); var m = parseInt((oldTime - nowTime)/1000); if( m >= 1){ // 文本内容 var val = _this.entertext.val(); // 以空格切割文本返回数组 var arr_test = val.split(" "); // 获取数组最后一个元素 var temp = arr_test[arr_test.length-1]; // 切割最后一个元素 单字符元素 返回数组 var temp_type = temp.split(""); // 获取数字第一个字符 var temp_cha = temp_type[0]; // 最后一个元素长度 var temp_len = temp.length; var temp_dot = temp_type.lastIndexOf("."); // 定义回调函数 callbacktips var callbacktips = function(arr_json){ // 初始化 UL _this.dropdown.find("li").remove(); for( i=0;i<arr_json.length;i++ ){ _this.dropdown.append('<li>'+arr_json[i]+'</li>'); }; _this.dropdown.show(); _this.dropdown.find("li:first-child").addClass("active"); // 自定义样式 _this.dropdown.find("li").css({ 'width':'100%', }); }; // 最后一个元素为空值 if( temp_len == 0 ){ _this.dropdown.hide(); _this.dropdown.find('li').remove(); } // 为特定字符 符合触发条件 if( temp_cha == opts.triggerCharacter ){ if($.isFunction(opts.keyPressAction)){ opts.keyPressAction(temp, function(arr_json){ // 调用回调函数 callbacktips(arr_json); }); } }else{ _this.dropdown.hide(); _this.dropdown.find('li').remove(); } } } // 初始化第一次时间 window.sessionStorage.setItem('nowTime',Date.now()); });
ps:这里出现了我们第二个参数,也是至关重要的一个参数,触发检索事件字符: opts.triggerCharacter
鼠标切换当前项
下拉菜单显示之后(这里我们先不管下拉菜单显示的位置),从体验上来说我希望既可以用鼠标选取,也可以用方向键选取,方向键切换当前项整合在上面的代码中,这里鼠标切换当前项:
// 切换当前项 _this.dropdown.on('mouseenter','li',function(){ $(this).addClass("active").siblings().removeClass("active"); });
阻止键盘按键默认事件 - enter键选取当前项
如果不做处理,我们在用方向键切换当前项的时候,你会看到光标也在上下移动,这如何能忍?怎么办?屏蔽它~,在这里我们可以整合enter键选取当前项,看代码:
// 阻止输入过程中 方向键盘的默认事件 _this.entertext.on("keydown",_this.entertext,function(event){ EVT = event || window.event; if( EVT.keyCode == 38 ){ EVT.preventDefault(); } if( EVT.keyCode == 40 ){ EVT.preventDefault(); } // enter键选取对应的 li if( EVT.keyCode == 13 ){ EVT.preventDefault(); var txt = _this.dropdown.find(".active").html(); var test_txt = _this.entertext.val(); var arr_change = test_txt.split(" "); // 以空格切割文本返回数组 var arr_test = test_txt.split(" "); // 获取数组最后一个元素 var temp = arr_test[arr_test.length-1]; var temp_type = temp.split(""); var temp_dot = temp_type.lastIndexOf("."); var n ; temp_type.splice(temp_dot+1,temp_type.length-1-temp_dot,txt); n = temp_type.join(''); arr_change.splice(arr_change.length-1,1,""+n); _this.entertext.val(arr_change.join(" ")); _this.dropdown.hide(); _this.dropdown.find('li').remove(); } });
点击当前项 重组val
要不我们直接看代码吧,你可能已经饿了~好的。看代码:
// 点击替换 重组val $(document).on("click",'li',function(){ var txt = $(this).html(); var test_txt = _this.entertext.val(); var arr_change = test_txt.split(" "); // 以空格切割文本返回数组 var arr_test = test_txt.split(" "); // 获取数组最后一个元素 var temp = arr_test[arr_test.length-1];y var temp_type = temp.split(""); var temp_dot = temp_type.lastIndexOf("."); var n ; temp_type.splice(temp_dot+1,temp_type.length-1-temp_dot,txt); n = temp_type.join(''); arr_change.splice(arr_change.length-1,1,""+n); _this.entertext.val(arr_change.join(" ")); _this.dropdown.hide(); _this.dropdown.find('li').remove(); });
默认参数
到了这里抛开动态显示下拉框位置,我们的公式编辑器应该可以跑一跑了,这里我们给参数设置一下默认值:
//默认参数 var defaults = { triggerCharacter : '$', dropdownWidth:'150px' };
这里我们将特定字符设定为 $,下拉框默认默认宽度为150px,当然其中还有一些配角参数,不屑一提,看官们有兴趣也可以自行添加一些参数
测试之后发现是可以正常使用的,那么接下来就是下拉框显示位置的问题了,这个想象好像不是秒秒钟能解决的问题,怎么办?(吐槽:什么怎么办?问百度啊~不要问我搜不到怎么办,因为我也不知道~)
这种时候不得不安慰自己:身为菜鸟还是有好处的,毕竟有无数前辈在前进的道路上闪闪发光~
搜~
掌声,在这里附上获取光标像素左边的原文链接以示感谢:http://blog.csdn.net/kingwolfofsky/article/details/6586029
剩下的就是整合啦,当然对于整合过程中遇到的一点小问题不必讶异(都坐下,基本操作):
1,测试之后发现下拉框位置是动态出现了,可位置不对,偏移心里的理想位置有八百里。没办法,再去安静的阅读一下前辈的代码,看完代码,发现上文中 下拉框的位置是绝对定位的坐标,这意味着什么
呢?(我承认是后来才发现的~不测试不知道),这意味着下拉框的显示位置不是相对于输入框,而是相对于整个body,所以我们应该把下拉框插入在body里面:
$("body").after(_this.dropdown);
2,再测:发现位置大体上是对了,但还是有偏移,再测测测~原来下拉框显示位置还受输入框 margin-left 和 margin-top 的影响,测试得出以下代码:
// 调用 kingwolfofsky, 获取光标坐标 function show(elem) { var p = kingwolfofsky.getInputPositon(elem); var s = _this.dropdown.get(0); var ttop = parseInt(_this.entertext.css("marginTop")); var tleft = parseInt(_this.entertext.css("marginLeft")) console.log(ttop); s.style.top = p.bottom-ttop+10+'px'; s.style.left = p.left-tleft + 'px'; }
现在我们需要一次完整的测试,调用:
$("#test").editTips({ triggerCharacter : '$', dropdownWidth:'150px', keyPressAction:function(temp,callbacktips){ var arr_json; if( temp == "$" ){ arr_json = ["$a","$ab","$b","$bb"] } if(temp && temp.indexOf("$a")== 0){ arr_json = ["$a","$ab"]; } else if(temp && temp.indexOf("$b")== 0){ arr_json = ["$b","$bb"]; } callbacktips(arr_json); } });
当然我们这里只是模拟返回数组,如果公式库不是很大,可以在前端完成,比如说自己建一个 json文件啥的~测试结果如图:
全部 js 代码
效果很ok,那么接下来呢?看官息怒,我知道到了该出全部代码的时候到了:
/* *****公式编辑器***** * 调用 editTips()方法 * editTips({ * triggerCharacter: 触发匹配字符 默认为 "$" * textareaWidth: 输入框宽度 默认 auto * textareaHeight: 输入框高度 默认 auto * dropdownWidth: 下拉提示框宽度 默认150px * keyPressAction:function(temp,callbacktips){ * // 参数为temp 返回 arr_json 数组 调用回调函数 callbacktips(arr_json) * var arr_json; * callbacktips(arr_json); * } * }); * */ (function ($) { $.fn.extend({ "editTips": function (options) { var opts = $.extend({}, defaults, options); //使用jQuery.extend 覆盖插件默认参数 return this.each(function () { //这里的this 就是 jQuery对象 // 获取输入光标在页面中的坐标 返回left和top,bottom var kingwolfofsky = { getInputPositon: function (elem) { if (document.selection) { //IE Support elem.focus(); var Sel = document.selection.createRange(); return { left: Sel.boundingLeft, top: Sel.boundingTop, bottom: Sel.boundingTop + Sel.boundingHeight }; } else { var that = this; var cloneDiv = '{$clone_div}', cloneLeft = '{$cloneLeft}', cloneFocus = '{$cloneFocus}', cloneRight = '{$cloneRight}'; var none = '<span style="white-space:pre-wrap;"> </span>'; var div = elem[cloneDiv] || document.createElement('div'), focus = elem[cloneFocus] || document.createElement('span'); var text = elem[cloneLeft] || document.createElement('span'); var offset = that._offset(elem), index = this._getFocus(elem), focusOffset = { left: 0, top: 0 }; if (!elem[cloneDiv]) { elem[cloneDiv] = div, elem[cloneFocus] = focus; elem[cloneLeft] = text; div.appendChild(text); div.appendChild(focus); document.body.appendChild(div); focus.innerHTML = '|'; focus.style.cssText = 'display:inline-block;width:0px;overflow:hidden;z-index:-100;word-wrap:break-word;word-break:break-all;'; div.className = this._cloneStyle(elem); div.style.cssText = 'visibility:hidden;display:inline-block;position:absolute;z-index:-100;word-wrap:break-word;word-break:break-all;overflow:hidden;'; }; div.style.left = this._offset(elem).left + "px"; div.style.top = this._offset(elem).top + "px"; var strTmp = elem.value.substring(0, index).replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br/>').replace(/\s/g, none); text.innerHTML = strTmp; focus.style.display = 'inline-block'; try { focusOffset = this._offset(focus); } catch (e) { }; focus.style.display = 'none'; return { left: focusOffset.left, top: focusOffset.top, bottom: focusOffset.bottom }; } }, // 克隆元素样式并返回类 _cloneStyle: function (elem, cache) { if (!cache && elem['${cloneName}']) return elem['${cloneName}']; var className, name, rstyle = /^(number|string)$/; var rname = /^(content|outline|outlineWidth)$/; //Opera: content; IE8:outline && outlineWidth var cssText = [], sStyle = elem.style; for (name in sStyle) { if (!rname.test(name)) { val = this._getStyle(elem, name); if (val !== '' && rstyle.test(typeof val)) { // Firefox 4 name = name.replace(/([A-Z])/g, "-$1").toLowerCase(); cssText.push(name); cssText.push(':'); cssText.push(val); cssText.push(';'); }; }; }; cssText = cssText.join(''); elem['${cloneName}'] = className = 'clone' + (new Date).getTime(); this._addHeadStyle('.' + className + '{' + cssText + '}'); return className; }, // 向页头插入样式 _addHeadStyle: function (content) { var style = this._style[document]; if (!style) { style = this._style[document] = document.createElement('style'); document.getElementsByTagName('head')[0].appendChild(style); }; style.styleSheet && (style.styleSheet.cssText += content) || style.appendChild(document.createTextNode(content)); }, _style: {}, // 获取最终样式 _getStyle: 'getComputedStyle' in window ? function (elem, name) { return getComputedStyle(elem, null)[name]; } : function (elem, name) { return elem.currentStyle[name]; }, // 获取光标在文本框的位置 _getFocus: function (elem) { var index = 0; if (document.selection) {// IE Support elem.focus(); var Sel = document.selection.createRange(); if (elem.nodeName === 'TEXTAREA') {//textarea var Sel2 = Sel.duplicate(); Sel2.moveToElementText(elem); var index = -1; while (Sel2.inRange(Sel)) { Sel2.moveStart('character'); index++; }; } else if (elem.nodeName === 'INPUT') {// input Sel.moveStart('character', -elem.value.length); index = Sel.text.length; } } else if (elem.selectionStart || elem.selectionStart == '0') { // Firefox support index = elem.selectionStart; } return (index); }, // 获取元素在页面中位置 _offset: function (elem) { var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement; var clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0; var top = box.top + (self.pageYOffset || docElem.scrollTop) - clientTop, left = box.left + (self.pageXOffset || docElem.scrollLeft) - clientLeft; return { left: left, top: top, right: left + box.width, bottom: top + box.height }; } }; // 文本框监听事件 var _this = this; _this.entertext = $(this); _this.dropdown = $('<ul class="editTips" style="display:none;"></ul>'); // 获取到的弹出的下拉框的位置是绝对定位的坐标,所以得把弹出的层放到body里 $("body").after(_this.dropdown); _this.dropdown.css({ 'width':opts.dropdownWidth, 'position':'absolute', }); $(this).css({ 'position': 'relative', }); // 监听输入框 _this.dropdown.parent().on("keyup",this,function(event){ var nowTime = window.sessionStorage.getItem('nowTime'); // 当前项索引 var n = _this.dropdown.find(".active").index(); // li 个数 var n_max = _this.dropdown.find("li").length; // 注意 event在 firefox 中不能兼容,在方法中带上event参数,如下声明实现兼容 EVT = event || window.event; if( EVT.keyCode == 38 ){ // 方向键控制 li 选项 if(n-1>=0){ _this.dropdown.find('li').eq(n-1).addClass("active").siblings().removeClass("active"); } if( n == 0){ _this.dropdown.find('li').eq(n_max-1).addClass("active").siblings().removeClass("active"); } return false; } // 禁止enter键换行 if( EVT.keyCode == 13 ){ return false; } if( EVT.keyCode == 40 ){ // 方向键控制 li 选项 if(n<n_max-1){ _this.dropdown.find('li').eq(n+1).addClass("active").siblings().removeClass("active"); } if( n+1 == n_max ){ _this.dropdown.find('li').eq(0).addClass("active").siblings().removeClass("active"); } return false; } // 限制请求,输入间隔不超过一秒时不触发检索事件发送请求 if( nowTime){ var oldTime = Date.now(); var nowTime = window.sessionStorage.getItem('nowTime'); var m = parseInt((oldTime - nowTime)/1000); if( m >= 1){ // 文本内容 var val = _this.entertext.val(); // 以空格切割文本返回数组 var arr_test = val.split(" "); // 获取数组最后一个元素 var temp = arr_test[arr_test.length-1]; // 切割最后一个元素 单字符元素 返回数组 var temp_type = temp.split(""); // 获取数字第一个字符 var temp_cha = temp_type[0]; // 最后一个元素长度 var temp_len = temp.length; var temp_dot = temp_type.lastIndexOf("."); // 定义回调函数 callbacktips var callbacktips = function(arr_json){ // 初始化 UL _this.dropdown.find("li").remove(); for( i=0;i<arr_json.length;i++ ){ _this.dropdown.append('<li>'+arr_json[i]+'</li>'); }; _this.dropdown.show(); _this.dropdown.find("li:first-child").addClass("active"); // 自定义样式 _this.dropdown.find("li").css({ 'width':'100%', }); }; // 最后一个元素为空值 if( temp_len == 0 ){ _this.dropdown.hide(); _this.dropdown.find('li').remove(); } // 为特定字符 符合触发条件 if( temp_cha == opts.triggerCharacter ){ if($.isFunction(opts.keyPressAction)){ opts.keyPressAction(temp, function(arr_json){ // 调用回调函数 callbacktips(arr_json); }); } }else{ _this.dropdown.hide(); _this.dropdown.find('li').remove(); } } } // 初始化第一次时间 window.sessionStorage.setItem('nowTime',Date.now()); }); // 切换当前项 _this.dropdown.on('mouseenter','li',function(){ $(this).addClass("active").siblings().removeClass("active"); }); // 阻止输入过程中 方向键盘的默认事件 _this.entertext.on("keydown",_this.entertext,function(event){ EVT = event || window.event; if( EVT.keyCode == 38 ){ EVT.preventDefault(); } if( EVT.keyCode == 40 ){ EVT.preventDefault(); } // enter键选取对应的 li if( EVT.keyCode == 13 ){ EVT.preventDefault(); var txt = _this.dropdown.find(".active").html(); var test_txt = _this.entertext.val(); var arr_change = test_txt.split(" "); // 以空格切割文本返回数组 var arr_test = test_txt.split(" "); // 获取数组最后一个元素 var temp = arr_test[arr_test.length-1]; var temp_type = temp.split(""); var temp_dot = temp_type.lastIndexOf("."); var n ; temp_type.splice(temp_dot+1,temp_type.length-1-temp_dot,txt); n = temp_type.join(''); arr_change.splice(arr_change.length-1,1,""+n); _this.entertext.val(arr_change.join(" ")); _this.dropdown.hide(); _this.dropdown.find('li').remove(); } }); // 点击替换 重组val $(document).on("click",'li',function(){ var txt = $(this).html(); var test_txt = _this.entertext.val(); var arr_change = test_txt.split(" "); // 以空格切割文本返回数组 var arr_test = test_txt.split(" "); // 获取数组最后一个元素 var temp = arr_test[arr_test.length-1]; var temp_type = temp.split(""); var temp_dot = temp_type.lastIndexOf("."); var n ; temp_type.splice(temp_dot+1,temp_type.length-1-temp_dot,txt); n = temp_type.join(''); arr_change.splice(arr_change.length-1,1,""+n); _this.entertext.val(arr_change.join(" ")); _this.dropdown.hide(); _this.dropdown.find('li').remove(); }); // 调用获取坐标方法 show(elem) $(this).keyup(function(){ show(this); }); // 调用 kingwolfofsky, 获取光标坐标 function show(elem) { var p = kingwolfofsky.getInputPositon(elem); var s = _this.dropdown.get(0); var ttop = parseInt(_this.entertext.css("marginTop")); var tleft = parseInt(_this.entertext.css("marginLeft")) console.log(ttop); s.style.top = p.bottom-ttop+10+'px'; s.style.left = p.left-tleft + 'px'; } }); } }); //默认参数 var defaults = { triggerCharacter : '$', dropdownWidth:'150px' }; })(window.jQuery);
调用插件
对呀,还是上面的调用,没问题的吧~
$("#test").editTips({ triggerCharacter : '$', dropdownWidth:'150px', keyPressAction:function(temp,callbacktips){ var arr_json; if( temp == "$" ){ arr_json = ["$a","$ab","$b","$bb"] } if(temp && temp.indexOf("$a")== 0){ arr_json = ["$a","$ab"]; } else if(temp && temp.indexOf("$b")== 0){ arr_json = ["$b","$bb"]; } callbacktips(arr_json); } });
最后,复制粘贴上面的 html ,css ,js ,调用,它就是你的了~
这次分享就到这里了,欢迎亲们品鉴,有问题可以私信或者留言哦~(偷笑:虽然我不一定看~看了不一定回~回了不一定能解决问题~)