最近更新: 2025-02-26

網頁UI元件 - NumberInputElement,自訂增減值按鈕,以及用滑鼠滾輪修改數字

當設計師碰到限定輸入數值的輸入欄位時,可以用 NumberInputElement 網頁 UI 元件讓輸入框呈現更美觀的增加數值按鈕與減少數值按鈕。這是目前流行的數值輸入形式。

範例圖

NumberInputElement 元件具備以下特性:

  1. 只綁定型態為 numberinput 控制項 (input type=”number”)。
  2. 加入使用滑鼠滾輪改變數值的行為。上滾增值,下滾減值。
  3. 擴充 label 控制項的行為,使其具備對關聯控制項的增值行為或減值行為。 使用 type 屬性定義點擊 label 時的行為, inc 表示增值,dec 表示減值。
  4. 若不想用 label 改變 input 控制項的值 ,也可自行定義控制項的選擇器。 例如用 button 控制項處理增值或減值。
  5. 增值與減值行為都會參考 input 控制項的 max, min, step 三項標準屬性。

這個元件不會主動在 input 控制項旁邊增加控制按鈕,而是交給設計師決定。 如果設計師沒有放上代表增值或減值的按鈕/控制項,將只有滑鼠滾輪的擴充行為生效。

NumberInputElement 儲放在我的 non-jquery-ui 源碼庫: 「取得 NumberInputElement」。

沒想到時至今日還有機會寫這種網頁 UI 。我本來以為是我目前用的前端框架太輕量簡化,所以沒有這種 UI。但網路上搜了一遍,才發現功能更複雜的前端框架也經常忽視這種 UI 的使用需求。設計師往往要自己額外去找 UI 元件拼湊。

先說明一點,在手機或平板等觸控式操作環境中,瀏覽器已經針對數值型態的 input 控制項提供明顯特化的輸入介面,讓使用者輕鬆輸入數字。所以在這類環境下,不需要使用這個 UI 元件。但這個元件的擴充內容也不會干涉內建行為。

在 PC 環境下,瀏覽器似乎就沒有那麼用心了。像 Edge 或 Firefox 的電腦版碰到數值型態的 input 控制項,只是在輸入框邊緣加上兩個很小的上下箭頭 (如範例圖所示),讓只用滑鼠的使用者點擊箭頭調整數值。但這兩個小箭頭實在小到很難點擊,所以主流的設計方式就是自己加兩個更大的按鈕來做這件事。

喔,對了。針對鍵盤使用者,瀏覽器電腦版還為數值型態 input 控制項加了按上下方向鍵調整數值的操作行為。既然可以按方向鍵調整數值,那是不是也該讓滑鼠滾輪這麼做?

NumberInputElement 元件就要滿足上述兩項需求。

滑鼠滾輪調整輸入數值

首先對應觸控式操作環境使用上下滑動調整數值的操作習慣,必須加上滾動滑鼠滾輪調整數值的操作行為。這一點只需要傾聴代表滑鼠滾輪滾動的 wheel 事件,然後根據 deltaY 判斷滾輪是上滾或下滾調整數值。上滾增值,下滾減值。程式碼實作內容摘錄於下:

    document.querySelectorAll('input[type="number"]').forEach(elm => {
        elm.addEventListener('wheel', NumberInputElement.wheelHandler);
    });
    
    static wheelHandler(ev)
    {
        ev.preventDefault();
        const input = ev.target;
        const delta = ev.deltaY;
        if (delta > 0) {
            // console.log('wheel down');
            NumberInputElement.change(input, 'dec');
        } else {
            // console.log('wheel up');
            NumberInputElement.change(input, 'inc');
        }
    }

增加數值按鈕與減少數值按鈕

至於增加兩個大按鈕分別負責增值與減值的部份,我的設計考量是讓設計師決定按鈕外觀和位置,而不是 NumberInputElement 自己生成按鈕。設計師只要讓 NumberInputElement 知道是哪些按鈕,然後 NumberInputElement 負責綁定增值和減值的擴充行為。

為了讓 UI 行為一致,我決定用 label 作為增值按鈕與減值按鈕的基底,而不用 button。因為增值按鈕與減值按鈕要像內建的兩個小箭頭一樣關聯 input 控制項,而且點擊時要改變輸入焦點到 input 控制項(輸入框內),而不是把焦點放在按鈕上。label 本身就具有這些特性。

Label 的 for 標準屬性可以指定關聯控制項。我再另外添加 type 屬性指定 label 負責的調整行為。若 type 為 inc 表示增值,dec 表示減值。

具體來說,就是像下列的 HTML 碼:

<label for="input1">數值欄位: </label>
<label for="input1" type="inc"></label>
<input type="number" id="input1" max="10" min="-15">
<label for="input1" type="Dec"></label>

這個 HTML 就是範例圖呈現的第一組數值型態輸入元件。

範例圖

第一個 label 沒有添加 type 屬性,所以它只觸發原本的行為,亦即改變輸入焦點到輸入框。

第二個 label 添加了 type=”inc” 屬性,點擊時除了改變輸入焦點,還會增加數值一步。第三個 label 添加了 type=”dec” 屬性,所以點擊就減少數值一步。一步的量由 input 的標準屬性 step 決定,預設一步為 1 。 程式碼實作內容摘錄於下:

    document.querySelectorAll('input[type="number"]').forEach(elm => {
        const labels = document.querySelectorAll(`label[for="${elm.id}"]`);

        // 擴展 label 控制項的行為(若其具有 type 屬性)
        labels.forEach(elm => {
            if (!elm.getAttribute('type')) {
                return;
            }
            // label 的 click 事件觸發兩次 (在 mousedown 與 mouseup 後各一次)
            // 所以看 mouseup ,不看 click
            elm.addEventListener('mouseup', NumberInputElement.labelHandler);
        });
    });
    
    static labelHandler(ev)
    {
        const label = ev.target;
        const labelType = label.getAttribute('type');
        const input = label.control; // 只有 label 控制項有此屬性
        if (!input || !labelType) {
            return;
        }

        NumberInputElement.change(input, labelType);
        input.focus();
    }

不論是 label 的 for 還是 input 的 step, min, max 等屬性,都是 HTML 規範的標準屬性。我的設計思路都是按照 HTML 規範原則擴充操作行為,從而保持 UI 行為一致。

控制項的事件傳遞

在瀏覽器的原始環境下,操作者手動點擊輸入控制項旁的小箭頭改變數值的動作,將會觸發控制項的 input 事件。 前端設計師經常會傾聴這個事件,以便處理使用者的輸入內容。

然而透過程式手段改變數值的動作,亦即用 JavaScript 程式碼設置控制項的 value 屬性,則不會觸發控制項的 input 事件。 這使前端設計師佈置的 input 事件處理方法不起作用。 從擴展元件行為並保持行為一致性的設計原則來看,這算是 BUG 。 所以當 NumberInputElement 擴展的操作行為改變控制項的 value 屬性時,也應擲出該控制項的 input 事件,讓其他傾聽此控制項事件的人可以接著處理數值改變的事。

要做到這件事,首先需要產生一個 Event 實例,其事件值為 ‘input’。 然後將它交給控制項的 dispatchEvent() 方法,即可派出 input 事件,讓其他處理函數接著做事。 程式碼實作內容摘錄於下:

    static change(input, act)
    {

        input.value = computedValue;
        input.dispatchEvent(new Event('input', {bubbles: true}));
    }

開始使用

上述實作內容都已整合在 NumberInputElement。當設計師寫好 HTML 碼之後,只需載入 number-input-element.js ,然後呼叫 NumberInputElement.initial(); 就可生效。

<label for="input1" type="inc"></label>
<input id="input1"  type="number" max="10" min="-15">
<label for="input1" type="Dec"></label>

<script src="number-input-element.js"></script>

<script>
window.addEventListener('load', function(){
    NumberInputElement.initial();
}, false);
</script>

NumberInputElement 在 Firefox 和 MS Edge 瀏覽器電腦版內完成開發及測試工作。 本文範例沒有套用任何 CSS,所以 label, input 或 button 控制項都是預設外觀。設計師應自己套用 bootstrap 這些 CSS 工具,美化外觀。

基本上,我的設計目標到此就實現了。不過說到自訂點擊行為,大家通常都是用 button。所以我又為 NumberInputElement.initial() 加入 labelSelector 函數參數,讓設計師自己決定綁定增值行為與減值行為的控制項。

自訂選擇器

下例混用 label 和 button 設計增值按鈕與減值按鈕。為了突顯自訂性,我還特意讓兩組數值輸入控制項的按鈕位置不一樣。第一組把兩個按鈕分放兩邊,第二組則都放右側。

本例為了讓作為增值按鈕與減值按鈕的 button 控制項被選中,在呼叫 NumberInputElement.initial() 時,傳入了自訂選擇器的 callback 函數參數。

<div>
    <label for="input1">數值欄位: </label>
    <label for="input1" type="inc"></label>
    <input type="number" id="input1" max="10" min="-15">
    <label for="input1" type="Dec"></label>
</div>
<br/>
<div>
    <input type="number" id="input2">
    <button for="input2" type="dec"></button>
    <button for="input2" type="inc"></button>
</div>

<script src="number-input-element.js"></script>

<script>
window.addEventListener('load', function(){
    NumberInputElement.initial(id => {
        return document.querySelectorAll(`[for="${id}"]`);
    });
}, false);
</script>

如果設計師不想用 for 屬性指定關聯控制項,而想用別的屬性也行。 下例就是用 NumberInputElement.relatedAttribute 屬性指定用 target 屬性名稱。

<div>
    <button target="input3" type="dec"></button>
    <input type="number" id="input3">
    <button target="input3" type="inc"></button>
</div>

<script src="number-input-element.js"></script>

<script>
window.addEventListener('load', function(){
    NumberInputElement.relatedAttribute = 'target';
    NumberInputElement.initial(id => {
        return document.querySelectorAll(`button[target="${id}"]`);
    });
}, false);
</script>

整合前端框架

目前前端框架的主流設計方式,不是分別在控制項上綁處理函數,而是直接在 document 綁定處理函數,然後在處理函數內根據事件主體判斷分流。

如果設計師要把 NumberInputElement 整合到採用這種設計原則的框架內,則不要呼叫 NumberInputElement.initial(),而須分別使用 NumberInputElement.wheelHandler()NumberInputElement.labelHandler()

HTML 規範參考

NumberInputElement 儲放在我的 non-jquery-ui 源碼庫: 「取得 NumberInputElement」。