沒有框架怎麼辦?原生 CSS + JS 實現一個標籤輸入框

XboxYan發表於2022-05-02
歡迎關注我的公眾號:前端偵探

最近在專案中需要做一個標籤輸入框,還挺實用的,演示效果如下

Kapture 2022-04-05 at 15.53.27

主要互動要求是這樣的

  1. 點選輸入框可以輸入內容
  2. 按回車可以生成標籤
  3. 按退格鍵可以刪除標籤
  4. 點選標籤上的關閉按鈕可以刪除標籤

習慣了各種 react 框架或者UI庫,大家有多久沒接觸沒有原生開發了呢?有時候頁面比較簡單,沒必要引入一個完整的框架,原生實現就完全滿足了,一起看看吧

一、自適應輸入框佈局

不管什麼元件,佈局都是最重要的。這個佈局分為標籤和輸入框兩個部分,假設 HTML 如下

<div class="tags-content">
  <tag>CSS<a class="tag-close"></a></tag>    
  <input class="tags-input" placeholder="新增標籤">
</div>

簡單修飾一下

.tags-content{
      display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 6px;
    width: 400px;
    box-sizing: border-box;
    padding: 8px 12px;
    border: 1px solid #D9D9D9;
    border-radius: 4px;
    font-size: 16px;
    line-height: 24px;
    color: #333;
    outline-color: #4F46E5;
    overflow: auto;
    cursor: text;
}
tag{
    display: flex;
    align-items: center;
    padding: 4px 0 4px 8px;
    font-size: 16px;
    line-height: 24px;
    background: #F5F5F5;
    color: rgba(0, 0, 0, 0.85);
    cursor: default;
}
tag-close{
    width: 18px;
    height: 18px;
    cursor: pointer;
    background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.578 5l2.93-3.493a.089.089 0 0 0-.068-.146h-.891a.182.182 0 0 0-.137.064l-2.417 2.88-2.416-2.88a.178.178 0 0 0-.137-.064h-.89a.089.089 0 0 0-.069.146L4.413 5l-2.93 3.493a.089.089 0 0 0 .068.146h.89a.182.182 0 0 0 .138-.064l2.416-2.88 2.417 2.88c.033.04.083.064.137.064h.89a.089.089 0 0 0 .069-.146l-2.93-3.493z' fill='%23000' fill-opacity='.45'/%3E%3C/svg%3E") center no-repeat;
}
.tags-input{
    flex: auto;
    border: 0;
    outline: 0;
    padding: 4px 0;
    line-height: 24px;
    font-size: 16px;
}
.tags-content:focus-within,
.tags-content:active{
    outline: auto #4F46E5;
}

注意幾點實現技巧:

  1. 標籤的間隔可以用 gap 實現
  2. 為了讓輸入框的區域鋪滿剩餘空間,這裡用到了flex: auto
  3. 為了讓父級處於聚焦狀態,這裡用到了:focus-within

效果如下

image-20220405140440965

但是這裡的輸入框用 input 還是有些問題的,如下所示

Kapture 2022-04-05 at 14.21.31

由於 input 輸入內容無法跟隨寬度自適應,所以有時候會出現文字被截斷的情況

image-20220405140844623

理想情況下,當輸入內容較多時,應該整體換行。如何實現呢?可以用普通的 div 來實現

<div class="tags-content">
  <tag>CSS<a class="tag-close"></a></tag>    
  <div class="tags-input" placeholder="新增標籤"></div>
</div>

可以通過新增contenteditable或者以下 CSS 來實現

.tags-input{
  -webkit-user-modify: read-write-plaintext-only;
}

這個屬性表示只允許輸入純文字,有興趣的可以參考張鑫旭的這篇文章: 小tip: 如何讓contenteditable元素只能輸入純文字

Kapture 2022-04-05 at 14.18.37

這樣可以自適應內容寬度了

二、輸入框佔位提示

由於輸入框已經從 input 換成了普通的 div 標籤,並沒有 placeholder 特性。不過,我們仍然可以通過其他 CSS 特性來實現佔位效果,當輸入框沒有內容時,就可以匹配到 :empty選擇器,然後通過偽元素::before動態生成 placeholder 內容,具體實現如下

.tags-input:empty::before{
    content: attr(placeholder);
    color: #828282;
}

效果如下

Kapture 2022-04-05 at 14.43.36

這樣就幾乎和 input 的佔位效果一致了

另外還有一種情況,如果需要僅在沒有任何標籤的情況下才顯示佔位,如何實現呢?可以想想,在沒有任何標籤的情況下,HTML 就變成了這樣

<div class="tags-content">
  <div class="tags-input" placeholder="新增標籤"></div>
</div>

這種情況,就僅剩輸入框唯一元素了,唯一元素可以通過only-child來匹配,所以實現如下

.tags-input:only-child:empty::before{
    content: attr(placeholder);
    color: #828282;
}

這樣新增一個偽類就解決了

image-20220405155046714

兩種需求都符合認知,看設計如何決定了

三、標籤的輸入與刪除

要實現標籤的輸入與刪除就需要 JS 出馬了,只需要監聽鍵盤的“回車”和“退格”兩個鍵值。需要注意的是,預設情況下,普通 contenteditable元素在回車時,會出現換行,如下

Kapture 2022-04-05 at 15.29.30

因此,在監聽鍵盤事件時需要阻止預設事件,然後動態建立標籤元素,通過 before新增到輸入框前面,具體實現如下

// TagInput是輸入框
TagInput.addEventListener('keydown', function(ev) {
  if (ev.key === 'Enter') {
    ev.preventDefault()
    if (this.innerText) { // 輸入框內容通過 innerText 獲取
      const tag = document.createElement('TAG');
      tag.innerHTML = this.innerText + '<a class="kalos-tag-close"></a>';
      this.before(tag);
      this.innerText = '';
    }
  }
})

這樣就能正常建立標籤了

Kapture 2022-04-05 at 15.34.23

然後是標籤的刪除。

這裡有兩種途徑,首先看鍵盤的刪除,具體邏輯是當輸入框內容為空時刪除標籤,很簡單,刪除的標籤就是輸入框的前面一個元素,通過previousElementSibling獲取,具體實現如下

TagInput.addEventListener('keydown', function(ev) {
  if (ev.key === 'Backspace' && !this.innerText) {
    this.previousElementSibling?.remove(); // 需要判斷前一個元素是否存在
  }
})

然後是點選刪除圖示的刪除。由於標籤是動態生成的,所以這裡需要用事件委託的方式來新增刪除事件

// TagContent是父級容器
TagContent.addEventListener('click', function(ev) {
  if (ev.target.className === 'tag-close') {
    ev.target.parentNode.remove();
  }
  TagInput.focus(); //點選任意地方輸入框都需要聚焦
})

這樣就實現了文章開頭的所示效果

Kapture 2022-04-05 at 15.53.27

完整程式碼可以訪問:input-tag

四、選擇框架還是原生?總結一下

整體實現並不算複雜,不少互動邏輯 CSS 也可以輕鬆實現,JS 也就 10 來行程式碼,這裡總結一下實現要點

  1. 普通 div 元素輸入純文字可以使用 -webkit-user-modify: read-write-plaintext-only
  2. 普通 div 元素輸入可以自適應內容寬度
  3. 普通 div 元素輸入框的 placeholder 佔位可以通過 :empty 結合偽元素實現
  4. 回車事件需要阻止預設事件,不然會換行
  5. 在一個元素的前面新增元素可以用 before 方法
  6. 刪除一個元素的前面一個元素,可以用 previousElementSibling.remove 方法
  7. 給動態生成的元素繫結事件可以用事件委託的方式

在各種框架大行其道的氛圍下,有些原生的屬性和方法可能都不太關注了,這也不失為是一種損失。當然,我本身也是各種框架都會用,特別是大型、複雜的互動頁面,一般比較小的互動,比如文章這個例子,在 ant design 中有相關的元件,也使用過,因為整體 UI 全是這種風格,設計也是按照這個設計的。後來需要單獨開發一個 chrome 外掛,也用到了這樣一個互動,但是僅僅用了這樣一個元件,引入整個框架就過於累贅了,所以還是選擇直接原生實現,簡單方便。

最後,如果覺得還不錯,對你有幫助的話,歡迎點贊、收藏、轉發❤❤❤

歡迎關注我的公眾號:前端偵探

相關文章