上週產品經理提出一個需求:
在編輯框中,使用者雙擊或直接選中左括號(右引號)或左引號(有引號),對其包含的所有字元(含括號或引號字元在內)進行高亮;
聽到這個需求的時候,我的心理活動是這樣的:“臥槽,這什麼需求,從來沒見過。前端做得了嗎?”。 臉上笑嘻嘻,心裡媽賣批,假裝淡定的對產品經理說,沒那麼快做,排到3月份的計劃去吧~
雙手撓頭,開始頭腦風暴
首先想到的是,搜尋時,在搜尋結果中對關鍵詞的進行高亮。這個思路很簡單,就是在搜尋結果的文字中匹配關鍵詞,用個span標籤包裹,並給span標籤加個樣式即可。這個,好像跟需求相去甚遠啊!
百度了半天也沒找到相關的案例,沒事,我們還有時間,可以自己來造個輪子。
正確思路:
選擇(select)事件
- 從事件入手。高亮是文字被選中時觸發,文字框有個DOM事件——select,表示文字框文字被選中時觸發的事件。
如果取得選中的文字:
- 兩個屬性:selectionStart、selectionEnd。 這兩個屬性中儲存的是基於 0 的數值,表示所選擇 文字的範圍(即文字選區開頭和結尾的偏移量)。
- 要獲得選中的文字,可以使用如下程式碼:
target.value.substring(selectionStart, selectionEnd); // target是文字物件
複製程式碼
- IE9+、Firefox、Safari、Chrome 和 Opera 都支援這兩個屬性。IE8 及之前版本不支援這兩個屬性, 而是提供了另一種方案,這裡不做介紹
如何選擇部分文字
- 所有文字框都有一個 setSelectionRange()方法。這個方法接收兩個引數:要選擇的第一個字元的索引和要選擇的最後一個字元之後的字元的索引。
實現
知道了怎麼獲取選中的文字和如何選中部分文字,接下來就是去實現了。
- 括號跟引號的匹配邏輯是不同的,括號要考慮巢狀問題,而引號不用,因為引號的巢狀沒有語義。
- 左括號跟右括號的匹配邏輯也是不同的,如果使用者選中的是左括號,則要從前向後匹配。如果是右括號,則要從後往前匹配。
- 引號類似,只是不需要考慮巢狀關係
有了思路,實現就不難了,程式碼很簡單,相信大家應該看一下就懂了,我就不在囉嗦了。
原始碼:
/**
* 選中括號、引號,對其包含的所有字元進行高亮顯示
* @param target 目標物件
*/
function keywordHighlight(target) {
var selectionStart = target.selectionStart, // 選中的文字的起始位置
selectionEnd = target.selectionEnd, // 選中的文字的結束位置
text = target.value, // 輸入框中的文字內容
selectValue = target.value.substring(selectionStart, selectionEnd); // 選中的文字
var tmpValue, // 待匹配的文字
tmpArr = [], // 用於存放切割後的待匹配文字的陣列
stack = [], // 棧
endIndex; // 結束位置在tmpArr中的下標
// selectValue === '(',從前向後匹配
if (selectValue === '(') {
tmpValue = text.slice(selectionStart);
tmpArr = tmpValue.split("");
for (var i = 0, len = tmpArr.length; i < len; i++) {
if (tmpArr[i] === '(') {
stack.push(tmpArr[i]);
} else if (tmpArr[i] === ')') {
stack.pop();
if (stack.length === 0) {
endIndex = i + 1;
break;
}
}
}
if (endIndex) {
target.setSelectionRange(selectionStart, selectionStart + endIndex);
}
}
// selectValue === ')',從後向前匹配
if (selectValue.trim() === ')') {
tmpValue = text.slice(0, selectionEnd);
tmpArr = tmpValue.split("");
for (var j = tmpArr.length - 1; j >= 0; j--) {
if (tmpArr[j] === ')') {
stack.push(tmpArr[j]);
} else if (tmpArr[j] === '(') {
stack.pop();
if (stack.length === 0) {
endIndex = j;
break;
}
}
}
if (endIndex || endIndex === 0) {
target.setSelectionRange(endIndex, selectionEnd);
}
}
if (selectValue.trim() === '"') {
// 獲取引號出現次數
var count = (text.slice(0, selectionEnd).split('"')).length-1;
if (count % 2 !== 0) { // 引號出現次數為奇數,從前向後匹配
// slice獲取匹配文字時length+1,獲取匹配下標時length+1,故最後 +1+1
endIndex = text.slice(selectionStart + 1,text.length).indexOf('"') + 1 + 1;
if (endIndex) {
target.setSelectionRange(selectionStart, selectionStart + endIndex);
}
} else { // 引號出現次數為偶數,從後向前匹配
tmpValue = text.slice(0, selectionEnd).trim();
endIndex = tmpValue.slice(0,tmpValue.length-1).lastIndexOf('"');
if (endIndex || endIndex === 0) {
target.setSelectionRange(endIndex, selectionEnd);
}
}
}
};
複製程式碼
參考文獻:《javascript高階程式設計》第三版