Sean's Blog

An image showing avatar

Hi, I'm Sean

這裡記錄我學習網站開發的筆記
歡迎交流 (ゝ∀・)b

LinkedInGitHub

使用 vue-tribute 實作標記功能

這篇文章主要說明如何透過 vue-tribute 來實作網頁上的標記功能(@mention),先備知識必須要已經基本會使用 Vue。

開始使用 vue-tribute

vue-tribute @ GitHub

vue-tribute 是將 ES6 Native 的 tribute 經過 Vue.js 的寫法包裝後的套件,方便支援 Vue.js 框架的寫法。

安裝 vue-tribute

我個人是習慣使用 npm 安裝套件。

1npm install vue-tribute --save
2

使用 vue-tribute

我們可以在 vue-tribute 中使用 inputtextareacontenteditable 等 HTML 標籤,當我們輸入內容時,vue-tribute 會存取選項 tributeOptions 裡的屬性與方法,呈現出 @mention 後的畫面與功能。

1<vue-tribute :options="tributeOptions">
2  <input type="text" placeholder="@..." />
3</vue-tribute>
4

如果想要對輸入框做進一步的處理,像是讓文字變色、轉為連結等功能,可以使用 contenteditable 來完成。

1<vue-tribute :options="tributeOptions">
2  <div contenteditable="true" id="myElement" placeholder="輸入留言"></div>
3</vue-tribute>
4

參數選項設定

tributeOptions 是 vue-tribute 的參數選項,這個參數選項在 vue-tribute 與 tribute 套件中都是相同的,因為 vue-tribute 的參數設定就是沿用 tribute 的設定。

以下列出一些我自己常用的參數:

  • values:唯一的必填參數,就是你要用來搜尋的資料
  • trigger:觸發條件,可使用符號或字串
  • selectTemplate:選取後新增到輸入框裡面的內容,如果不是使用 contenteditable 元素,這邊就只能寫入純文字哩
  • menuItemTemplate:標記清單所呈現的組合樣板,以 Twitter 的 @mention 功能來說,就會呈現頭像、名稱、ID 等資料
  • lookup:通常放字串即可,但若要透過資料裡的多個屬性來搜尋,可以把 key 相加來做複合搜尋
  • requireLeadingSpace:觸發前要有空格
  • allowSpaces:@mention 的內容允許空格
  • menuItemLimit:清單最多呈現多少筆篩選結果
  • menuShowMinLength:觸發前至少要打幾個字
1tributeOptions: {
2  values: [],
3  trigger: '@',
4  selectTemplate(item) {
5    return `<span><a href="http://twitter.com/${item.original.id}">@${item.original.name}</a></span>`;
6  },
7  menuItemTemplate(item) {
8    return `<span>${item.original.name}</span>`;
9  },
10  lookup(item) {
11    return item.friend_name + item.friend_suuid;
12  },
13  requireLeadingSpace: true,
14  allowSpaces: false,
15  menuItemLimit: 10,
16  menuShowMinLength: 1,
17},
18

這是一個簡易的參數設定範例,若有其他需求可以參考 所有參數 的說明。

處理標記結果

接著,我們要來處理 @mention 得到的結果。
由於 vue-tribute 沒有提供方法來綁定與監聽選取到的項目,所以我們只好從選取後的文字框當中,將標記到的對象給篩選出來。

篩選出所有的標記對象

要擷取使用者這則留言總共標記了哪些人,我們可以直接用正規表達式篩選出 @ 後面的文字,並將結果以陣列傳回後端。

1// 計算留言 (content) 裡面標記了哪些人
2const str = this.content;
3const pattern = /\B@([a-z0-9_-]+)/gi; // 透過正規表達式查找符合規則的字段
4const arr = str.match(pattern); // ['@sean', '@sealman']
5let result = [];
6if (arr) {
7  result = arr.map((item) => item.substr(1)); // ['sean', 'sealman']
8}
9

將文字轉換成連結

透過正規表達式,我們也可以將標記文字轉為連結,像是將 @vuejs 這個標記,轉換為 Twitter 使用者的個人頁面連結,像是 http://twitter.com/vuejs

1// 將 @mention 轉為 twitter.com/mention 帳號的 Link
2const replaceContent = content.replace(
3  /\B@([a-z0-9_-]+)/gi, // 可位於開頭 or 左右有空格,可包含 _ 與 - 符號
4  '<a href="http://twitter.com/$1">@$1</a>',
5);
6

參考資料:Replace @mention with link - Stack Overflow

Troubleshooting

根據過往的開發經驗,如果需求比較複雜,或是想要實作出像是留言板等功能較完善的輸入框的話,輸入框可能就會選用 contenteditable 元素。不過 contenteditable 在使用時可能會遇到一些問題,以下就是我自己在實作中遇到過的幾個坑。

Contenteditable 滑鼠游標自動 Focus 至最後方

使用 contenteditable 時,如果有 Focus 游標的需求,無法直接透過一般的 focus() 完成,需要使用到 SelectionRange 屬性。

在 Stack Overflow 上 Nico Burnsjavascript - How to move cursor to end of contenteditable entity - Stack Overflow 這篇問題裡提到的解法,應該是目前可行的解法。

不過若 contenteditable 裡面有放其他元素,在 Node 節點上可能要再做判斷

1function setEndOfContenteditable(contentEditableElement) {
2  var range, selection;
3  if (document.createRange) {
4    range = document.createRange();
5    range.selectNodeContents(contentEditableElement);
6    range.collapse(false);
7    selection = window.getSelection();
8    selection.removeAllRanges();
9    selection.addRange(range);
10  } else if (document.selection) {
11    range = document.body.createTextRange();
12    range.moveToElementText(contentEditableElement);
13    range.collapse(false);
14    range.select();
15  }
16}
17

接著,我們透過 setEndOfContenteditable 觸發整個方法,完成將游標移動到最後面的功能。

1const elem = document.querySelector('.input-area');
2this.setEndOfContenteditable(elem); // 游標移到最後面
3

Contenteditable 複製貼上時限制為純文字

若要防止使用者貼上文字以外的內容,可以加上以下程式碼,防止使用者貼上任何內容。

Javascript trick for 'paste as plain text` in execCommand - Stack Overflow 這個問題串的最佳解答,算是比起另一個網路上的熱門答案更簡單、更好理解,且不容易出現 Bug 的寫法。

1editor.addEventListener('paste', function (e) {
2  e.preventDefault();
3  var text = (e.originalEvent || e).clipboardData.getData('text/plain');
4  document.execCommand('insertHTML', false, text);
5});
6

以上資源是我自己整理過後的筆記,若有錯誤歡迎隨時和我聯繫