javascriptjqueryvalidationkeydownime

Language Input Method Editor Software is interfering with my input validation


I have this javascript function for allowing numeric input only

window.addNumericInputValidation = function (elementId) {
    var $element = $("#" + elementId);

    // First remove any existing handlers to prevent stacking
    $element.off("keydown.numericValidation");
    $element.off("paste.numericValidation");

    // Handle keydown for typing
    $element.on("keydown.numericValidation", function (e) {
        console.log("key code", e.keyCode);
        // Allow: backspace, delete, tab, escape, enter, etc.
        if ($.inArray(e.keyCode, [46, 8, 9, 27, 13]) !== -1 ||
            // Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
            (e.keyCode === 65 && (e.ctrlKey || e.metaKey)) ||
            (e.keyCode === 67 && (e.ctrlKey || e.metaKey)) ||
            (e.keyCode === 86 && (e.ctrlKey || e.metaKey)) ||
            (e.keyCode === 88 && (e.ctrlKey || e.metaKey)) ||
            // Allow: home, end, left, right
            (e.keyCode >= 35 && e.keyCode <= 39)) {
            return;
        }

        // Ensure that it's a number and stop the keypress if not
        if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) &&
            (e.keyCode < 96 || e.keyCode > 105)) {
            e.preventDefault();
        }
    });

    // Handle paste events
    $element.on("paste.numericValidation", function (e) {
        // Get pasted data via clipboard API
        var clipboardData = e.originalEvent.clipboardData || window.clipboardData;
        var pastedData = clipboardData.getData('Text');

        // Check if pasted data contains only numbers
        if (!/^\d*$/.test(pastedData)) {
            e.preventDefault();
        }
    });
};

The problem happens when I turn on Unikey - which is a Vietnamese input editor for Vietnamese characters (For example the software will convert "dd" into the vietnamese "đ"). If Unikey is on, when I type numbers, it works normally but if i type "d" twice, it starts deleting the the number input.

After debugging, I found out that after you type "d" key twice, Unikey will insert 2 delete key and one "đ" key, to replace "dd" with "đ", and because my validation don't allow non-numeric characters, only the deletion happens.

This is the console after I input "d" twice

key code 68 (for 'd')
key code 231 (for 'đ')
key code 8 (for delete)
key code 8 (for delete)
key code 231 (for 'đ')

I can't really tell the customer to turn off their language editor software so what are my options here?

*Edit: I have refactor my code to this

window.addNumericInputValidation = function (elementId) {
    var $element = $("#" + elementId);
    // Remove existing handlers
    $element.off(".numericValidation");

    // Use isComposing flag from the event to detect IME activity
    var inComposition = false;

    // Track input value before composition starts
    var valueBeforeComposition = $element.val();

    // Handle composition events directly
    $element.on("compositionstart.numericValidation", function (e) {
        inComposition = true;
        valueBeforeComposition = $element.val();
    });

    $element.on("compositionend.numericValidation", function (e) {
        inComposition = false;

        // After composition ends, ensure only numbers remain
        setTimeout(function () {
            var currentValue = $element.val();
            var numericValue = currentValue.replace(/\D/g, '');
            if (currentValue !== numericValue) {
                $element.val(numericValue);
            }
        }, 0);
    });

    // Add input handler as fallback validation
    $element.on("input.numericValidation", function (e) {
        // If not in composition, validate immediately
        if (!inComposition) {
            var currentValue = $element.val();
            var numericValue = currentValue.replace(/\D/g, '');
            if (currentValue !== numericValue) {
                $element.val(numericValue);
            }
        }
    });

    // Handle paste events
    $element.on("paste.numericValidation", function (e) {
        // Get pasted data via clipboard API
        var clipboardData = e.originalEvent.clipboardData || window.clipboardData;
        var pastedData = clipboardData.getData('Text');

        // Check if pasted data contains only numbers
        if (!/^\d*$/.test(pastedData)) {
            e.preventDefault();
        }
    });
};

so it doesn't use keyCode and onKeyDown like @Yogi mentioned and also handle some IMEs, but the thing is Unikey doesn't trigger the composition event and the behaviour is still the same.

I really don't want to handle it this way but the requirement is that the field can only be input numeric characters. The only way i can think of right now is to negotiate the requirement :D

**Edit 2: This is my temporary fix for the problem

var unikeyBackspace = 0;
$element.on("keydown.numericValidation", function (e) {
    // unikey generate this key before generate 2 backspace key
    if (e.originalEvent.keyCode == 231 && e.originalEvent.key == "·") {
        unikeyBackspace = 2;
    }
    if (e.originalEvent.key == "Backspace" && unikeyBackspace > 0) {
        unikeyBackspace--;
        e.preventDefault();
    }
});

Solution

  • After some experimenting and researching, I found out the best way to prevent third party input is by checking typing speed (should've thought of this sooner), inputs generated by the software usually occur with intervals of less than 10 ms between keystrokes and I don't think there's any user can type that fast

    window.addNumericInputValidation = function (elementId) {
        var $element = $("#" + elementId);
        var lastKeyPressedTime = 0;
        var timeThreshold = 10; //ms
    
        ...
    
        $element.on("keydown.numericValidation", function (e) {
            var keyEvent = e.originalEvent;
            
            var currentTimestamp = keyEvent.timeStamp;
    
            // preventing key pressed winthin 10ms time window
            var timeDifference = currentTimestamp - lastKeyPressedTime;
            if (lastKeyPressedTime > 0 && timeDifference <= timeThreshold) {
                console.log("preventing", keyEvent.key);
                e.preventDefault();
            }
    
            lastKeyPressedTime = currentTimestamp;
        });
    
        ...
    };