前篇文章提到有個 blur() + alert() IE Only 網頁在 Edge/Chrome 會發生 alert 無窮迴圈的悲劇,讓我心生寫個簡單通用程式庫解決這類需求的念頭。規格如下:

  • 希望搭配原有檢核邏輯,程式不要大改,換掉原本 alert 訊息顯示就好
  • 要能明確指出檢核失敗欄位所在位置
  • 支援將焦點移回無效欄位,強迫輸入正確才可離開的模式
  • 力求簡便易用,希望單一 .js 搞定,不需額外載入 .css
  • 能支援 IE11 更佳

這樣的需求用純 JavaScript 理論上也寫得出來,但我決定寫成 jQuery 插件,節省程式碼跟腦細胞。

直接看結果,車牌欄位採強迫校正,會加上遮罩阻止操作,並將焦點移回車牌欄位;其餘則是在欄位前方標註訊息,點擊可移除。標註位置以欄位左上為原則,則欄位位於頁頂空間不足,則標於左下角。

呼叫方式如下:

    <table>
        <tr>
            <td>車牌1</td><td><input id="c1" /></td>
            <td>欄位X</td><td><input id="x1" /></td>
        </tr>
        <tr>
            <td>說明</td>
            <td colspan="3">
                <textarea id="t1" style="width: 100%" rows="4"></textarea>
            </td>
        </tr>
        <tr>
            <td>欄位A</td><td><input id="f1" /></td>
            <td>欄位B</td><td><input id="f2" /></td>
        </tr>
    </table>
    <button onclick="validate()">測試檢核</button>
    <script>
        $('#c1').blur(function () {
            var inp = $(this);
            if (!/^[-0-9A-Z]{5,7}$/.exec(inp.val())) {
                inp.showInvalidMessageTag("車牌格式有誤,請更正!", { forceChange: true });
            }
        });
        function validate() {
            $('input,textarea').not('#c1').each(function () {
                var inp = $(this);
                if (!inp.val()) 
                    inp.showInvalidMessageTag("欄位不可空白");
            });
        }
    </script>   

為擴大相容性,用 IE11 嘛也通。

[2022-06-29 更新]

採納讀者 78 建議,加上 10 秒訊息自動消失,預設開啟,但可透過參數 inp.showInvalidMessageTag("檢核有誤!", { autoHide: false }) 停用,仿效 NOTY 顯示倒數進度,滑鼠滑過時會停止倒數,回歸點選關閉:

jQuery 插件完整程式碼如下,也有線上展示,但這個版本仍屬雛型,還沒上戰爭歷練過,歡迎大家試玩並提供建議:

        jQuery.fn.showInvalidMessageTag = function (msg, options) {
            //ver 1.1 add auto clear
            options = $.extend({ autoHide: true, forceChange: false }, options);
            var forceChange = options.forceChange;
            if (forceChange) options.autoHide = false;
            //add style block
            var styleBlock = $('#invld-msg-styles');
            if (!styleBlock.length) {
                $("\
<style id='invld-msg-styles'>\
.ivmt-tag { font-size:10pt; position: absolute; cursor: pointer; z-index: 99999; opacity: 0.85 }\
.ivmt-tag .arw { color: #e53939; font-size: 12px; padding-left: 2px }\
.ivmt-tag .msg { background-color: #e53939; color: white; padding: 4px 12px; width: auto; margin-top: -4px; border-radius: 3px}\
.ivmt-tag .prog { width: 100%; background-color: yellow; height: 2px; opacity: 0.5; }\
.ivmt-mask-layer { position: absolute; z-index: 65535; opacity: 0.3; background-color: #888; top: 0; left: 0; width: 100vw }\
</style>").prependTo('body');
            }
            var elem = $(this);
            var maskLayer = $('.ivmt-mask-layer');
            if (forceChange && maskLayer.length) return;
            var pos = elem.offset();
            var bgc = '#e53939';
            var tag = $('<div class="ivmt-tag" style="visibility:hidden;"><div class="arw">▲</div><div class="msg"><div class="text"></div></div>');
            tag.find('.msg .text').text(msg);
            if (options.autoHide) tag.find('.msg').append('<div class="prog"></div>');
            tag.appendTo('body');
            var tagPos = pos.top < tag.height() ? 'bottom' : 'top';
            var top = pos.top + elem.height();
            if (tagPos == 'top') {
                top = pos.top - tag.height() + 4;
                var msgBlock = tag.find('.msg');
                msgBlock.css('marginTop', '0');
                tag.find('.arw').text('▼').css('marginTop', '-5px').before(msgBlock);
            }
            if (forceChange) {
                maskLayer = $('<div class=ivmt-mask-layer></div>');
                maskLayer.css({ height: Math.max(window.innerHeight, document.body.scrollHeight) + 'px' }).appendTo('body');
            }
            if (!forceChange && options.autoHide) {
                var count = 100;
                var hnd = setInterval(function() {
                    count--;
                    tag.find('.prog').css('width', count + '%');
                    if (count <= 0) {
                        clearInterval(hnd);
                        tag.remove();
                    }
                }, 100);
                tag.hover(function() { 
                    clearInterval(hnd);
                    tag.find('.prog').remove();
                });
            }

            tag.css({ visibility: 'visible', top: top + 'px', left: pos.left + 'px' })
            .add(maskLayer).click(function () {
                tag.remove();
                if (forceChange) {
                    elem.focus();
                    maskLayer.remove();
                }
            });
        }

A simple jQuery plugin to show invalid message on the input fields.


Comments

# by 78

無能者一點小建議 覺得讓警示過段時間消失比較好 一個個點警示有點麻煩

# by Jeffrey

to 78,好建議! 改了一個版本,加入倒數自動關閉功能。

# by Toolman

黑大好 關於 css 的部分,想請問 1. css 獨立成一份 css file 會不會比較好呢? 2. progress 倒數計時使用 setInterval 那邊,用 window.requestAnimationFrame 會不會比較好? 理論上動畫會比較順暢一點

# by Jeffrey

to Toolman, 獨立 css 檔是較正統做法,但缺點是每次應用時要多部署一個檔,網頁要多加一行 <link href=...>,值不值得因人而異。有人不介意多費點力氣去維持嚴謹,我則偏好「程式庫用起來愈方便愈好」,大家取捨的點不同。如果要想自訂 css,options 可再增加一個 custCssUrl 動態載入。 requestAnimationFrame() 我不太熟,要怎麼應用還需要研究一下。

# by Toolman

其實用法和 setInterval 差不多,都是丟個 callback 進去,只是 requestAnimationFrame 的 callback 還要記得再呼叫 requestAnimationFrame 情境上,只要是想用 js 做動畫的話,requestAnimationFrame 會幫忙處理跟瀏覽器 FPS 相關的東西 如果用 setInterval,很容易發生 fps 不夠高,導致動畫看起來卡卡的 這幾篇 stackover 上的討論還不錯 Why is requestAnimationFrame better than setInterval or setTimeout https://stackoverflow.com/questions/38709923/why-is-requestanimationframe-better-than-setinterval-or-settimeout requestAnimationFrame loop not correct FPS https://stackoverflow.com/questions/43379640/requestanimationframe-loop-not-correct-fps/43381828#43381828

# by Jeffrey

to Toolman, 感謝分享。

Post a comment