最近爲實現一個新功能弄的焦頭爛額 @xxx
的實現,在實現後寫下些心得,供之後會跳入這坑的同志們參考。html
首先,當讓是考慮使用範圍,因爲項目僅僅須要考慮在 WEBKIT
環境下使用,因此能夠不用考慮 IE
這也使得代碼少了不少的 if(){}else{}
判斷。在Mozilla 開發者網絡上發現 selection
和 range
這兩個關於選區對象和光標對象,結合 Caret(一個用於判斷當前光標位置的JS插件)後,一個大體的雛形就浮現出來。node
大概就長這樣:git
先整理思路,捋一捋實現步驟。github
大體思路以下:web
@
後將選擇框顯示出來@xxx
@xxx
以後@xxx
時須要整個 @xxx
一塊兒刪除因爲項目使用了 angular
來構建,因此給的 demo
也是用 angular
來搭建的,可是不論用什麼框架,想法有了,那麼一切就好辦了。chrome
selection
和 range
對象的具體使用請參考 MDN
上的相關文章:網絡
主要涉及的幾個方法:app
<div class="demo-wrap" ng-controller="Controller"> <!-- 文本輸入框 --> <div class="demo" id="demo" contenteditable="true" ng-keydown="keyIn($event)"></div> <!-- 帶有輸入框的選人框 --> <div class="select-person" id="selectPerson" ng-show="showSelect" ng-style="sPersonPosi"> <input type="text" id="searchPersonInput" ng-model="personSearchText" ng-blur="missFocus()"> <ul class="person-wrap"> <li class="row" ng-click="sPersonDone({fullName:'全部人'})"> <div class="col-1"> <div class="img-wrap"> <portrait src="" text="'全部'"></portrait> </div> </div> <div class="col-2">全部人</div> </li> <li class="row" ng-click="sPersonDone(item)" ng-repeat="item in atList | filter :{fullName: personSearchText}"> <div class="col-1"> <div class="img-wrap"> <portrait src="item.img" text="item.fullName.slice(-2)"></portrait> </div> </div> <div class="col-2" ng-bind="item.fullName"></div> </li> </ul> </div> </div>
樣式相關的CSS
代碼就不放上來了,簡要分析下頁面結構,一個 contenteditable="true"
的輸入框和一個 id="selectPerson"
的選人框。框架
contenteditable="true"
主要是由於想在輸入框中插入標籤,將 @xxx
內容顯示出不一樣的顏色(這就須要將 @xxx
放在一個標籤中),綁定 keyIn
的鍵盤輸入事件,用於檢索用戶輸入 @
和 backspace
,並作出相應的動做;showSelect
來控制是否顯示,遍歷顯示須要顯示的選人,以及使用 input
中的內容來過濾選人。相關代碼以下:spa
$scope.keyIn = function(e) { var selection = getSelection(); var ele = $('#demo'); if (e.code == 'Digit2' && e.shiftKey) { $scope.showSelect = true; var offset = ele.caret('offset'); $scope.sPersonPosi = { left: offset.left - 10 + 'px', top: offset.top + 20 + 'px' }; // 讓選人框中的搜索框獲取焦點 $timeout(function(){ $('#searchPersonInput')[0].focus(); }) } }
實現起來挺簡單,代碼也不復雜,利用 caret
插件獲取到光標位置,將選人框在 @
符號的下方顯示出來,並同時實現了步驟中的第二步:將焦點放在搜索框中。
主要涉及步驟爲:三、四、5
。
當鼠標點擊備選項時須要按順序進行 三、四、5
步驟,因此需將 三、四、5
這 3
個步驟放在一塊兒。
相關代碼以下:
$scope.sPersonDone = function(person) { // 成功選人後,關閉選擇框,讓輸入框獲取焦點。 $scope.showSelect = false; var ele = $('#demo')[0]; ele.focus(); // 獲取以前保留先來的信息。 // 須要修改 keyIn 的代碼,保存選區以及光標信息,用於獲取在光標焦點離開前,光標的位置 var selection = lastSelection.selection; var range = lastSelection.range; var textNode = range.startContainer; // 刪除 @ 符號。 range.setStart(textNode, range.endOffset); range.setEnd(textNode, range.endOffset + 1); range.deleteContents(); // 生成須要顯示的內容,包括一個 span 和一個空格。 var spanNode1 = document.createElement('span'); var spanNode2 = document.createElement('span'); spanNode1.className = 'at-text'; spanNode1.innerHTML = '@' + person.fullName; spanNode2.innerHTML = ' '; // 將生成內容打包放在 Fragment 中,並獲取生成內容的最後一個節點,也就是空格。 var frag = document.createDocumentFragment(), node, lastNode; frag.appendChild(spanNode1); while ((node = spanNode2.firstChild)) { lastNode = frag.appendChild(node); } // 將 Fragment 中的內容放入 range 中,並將光標放在空格以後。 range.insertNode(frag); selection.extend(lastNode, 1); selection.collapseToEnd(); };
咱們須要的效果是在 @
選人後,將整理好的 @xxx
包裝成一個標籤,放在原先 @
的位置,因此咱們須要對原先的 $scope.keyIn
方法進行改造,保留原先的光標信息,方便在上面的方法中使用。
改造後的 $scope.keyIn
方法以下:
$scope.keyIn = function(e) { var selection = getSelection(); var ele = $('#demo'); if (e.code == 'Digit2' && e.shiftKey) { $scope.showSelect = true; // 保存光標信息 lastSelection = { range: selection.getRangeAt(0), offset: selection.focusOffset, selection: selection }; $scope.showSelect = true; // 設置彈出框位置 var offset = ele.caret('offset'); $scope.sPersonPosi = { left: offset.left - 10 + 'px', top: offset.top + 20 + 'px' }; $timeout(function(){ $('#searchPersonInput')[0].focus(); }) } }
這裏估計挺多人會有疑問,爲啥要在生成的標籤後面加一個空格,並且這個空格要經過
這樣的方式實現。
首先,先解釋第一個問題:爲啥要在標籤後加一個空格?
若是不加空格的話,以後在輸入文字會添加在咱們生成的標籤中,也就是說若是不加空格來隔斷咱們生成的標籤,咱們在文本框裏所作的操做就是在咱們生成的標籤中進行。而加了個空格就爲了不該問題的發生,使得文本編輯在正確的編輯框中進行。
第二個問題:爲啥不能直接加空格 ' '
,而是經過
,不得不說這是個過個悲傷的事實,仍是碰到了兼容性的問題,在 chrome
下運行好好的代碼,在 node-webkit
中就會各類報錯。緣由在不斷的 defug
後發現了: node-webkit
中,將一個 ' '
添加到 contenteditable="true"
的 div
中會沒有啊,坑爹啊有木有!!!呈上以前的代碼來祭奠下。
var spanNode1 = document.createElement('span'); var node = document.createTextNode(' '); spanNode1.className = 'at-text'; spanNode1.innerHTML = '@' + person.fullName; var frag = document.createDocumentFragment(); frag.appendChild(spanNode1); frag.appendChild(node); range.insertNode(frag); selection.extend(node, 1);
結果一上 node-webkit
環境各類報錯。真是坑了個大爹。緣由是光標定位不許,指定位置超出實際位置,可是 node-webkit
環境確實是能夠輸入空格的,一看原來是
而
不能經過 createTextNode
來建立,因此就有了以前的哪一個曲線救國的策略了。
終於捋到最後一個步驟了,刪除時,須要將一整個標籤一塊兒刪除。因爲須要監聽鍵盤的輸入,因此就可與以前 keyIn
的代碼寫在一塊兒。
最終的 keyIn
代碼爲:
$scope.keyIn = function(e) { var selection = getSelection(); var ele = document.getElementById('demo'); if (e.code == 'Digit2' && e.shiftKey) { // 保存光標信息 lastSelection = { range: selection.getRangeAt(0), offset: selection.focusOffset, selection: selection }; $scope.showSelect = true; // 設置彈出框位置 var offset = $(ele).caret('offset'); $scope.sPersonPosi = { left: offset.left + 'px', top: offset.top + 30 + 'px' }; $timeout(function(){ $('#searchPersonInput')[0].focus(); }) } else if (e.code == 'Backspace') { // 刪除邏輯 // 1 :因爲在建立時默認會在 @xxx 後添加一個空格, // 因此當得知光標位於 @xxx 以後的一個第一個字符後並按下刪除按鈕時, // 應該將光標前的 @xxx 給刪除 // 2 :當光標位於 @xxx 中間時,按下刪除按鈕時應該將整個 @xxx 給刪除。 var range = selection.getRangeAt(0); var removeNode = null; if (range.startOffset <= 1 && range.startContainer.parentElement.className != "at-text") removeNode = range.startContainer.previousElementSibling; if (range.startContainer.parentElement.className == "at-text") removeNode = range.startContainer.parentElement; if (removeNode) ele.removeChild(removeNode); } };
代碼的邏輯都寫在註釋裏了,這裏就很少說了。
這樣就完成 @
這一功能了。