Search code examples
javascriptcssautocompletekeyboard-eventscapitalize

How to heuristically discover Javascript input autocomplete key* events


I recently had a requirement to enforce Proper-Case in a HTML input field. CSS Text-Transform: Capitalize was 90% there but didn't down-case multiple caps. So I wrote the Javascript below.

To my surprise you'll see that autocomplete input (user clicking one of the autocomplete options) results in keyup and keydown events that are bereft of any "key" properties :-( As you can also see in the code, I'm having to search for "key" in the event and if it's not there I assume it's an autocomplete.

Is there a better/documented way? Surely an autocomplete is more of a "Paste" event? Does someone have pointers to a specification(s)?

    <!DOCTYPE html>
    <html>
    <head>
    <script type="application/javascript">
        'use strict';
        
            document.addEventListener("DOMContentLoaded", isDOMIsGood);
    
            function isDOMIsGood() 
            {
                // Handle autocomplete
                document.body.addEventListener('keyup',
                    (e) => {
                        
                        if (e.target.tagName.toUpperCase()  != "INPUT"      || 
                            e.target.type.toUpperCase()     != "TEXT"       ||
                            getComputedStyle(e.target).textTransform.toUpperCase() 
                                                            != "CAPITALIZE" ||
                            (
                            "transform" in e.target.dataset                 &&
                            e.target.dataset.transform.toLowerCase()
                                                            == "standard"   
                            ))                                              
                            return true;
                            
                        if ("key" in e) // not autocomplete
                            return true;
    
                        e.target.value = e.target.value.toLowerCase();
                        
                        return true;
                    }
                );
                // Handle normal keyboard input         
                document.body.addEventListener('keydown',
                    (e) => {
                    
                        const WORD_DELIMS = ".,& -:";  // Needs work
                        
                        if (e.target.tagName.toUpperCase()  != "INPUT"      || 
                            e.target.type.toUpperCase()     != "TEXT")
                            return true;
                            
                        if (!("key" in e)) // autocomplete
                            return true;
                            
                        if (e.key.length                    >  1            || 
                            e.altKey                                        || 
                            e.ctrlKey                                       || 
                            e.isComposing                                   || 
                            e.metaKey                                       ||
                            e.target.value.length           ==  0           || 
                            e.target.selectionStart         ==  0           || 
                            e.key                           >  "Z"          || 
                            e.key                           <  "A"          ||
                            (
                            "transform" in e.target.dataset                 &&
                            e.target.dataset.transform.toLowerCase()
                                                            == "standard"   
                            )                                               ||
                            getComputedStyle(e.target).textTransform.toUpperCase() 
                                                            != "CAPITALIZE" ||
                            WORD_DELIMS.indexOf(e.target.value.substr(e.target.selectionStart - 1, 1)) != -1)           
                            return true;
                        
                        let   cursorPos         = e.target.selectionStart;
                        
                        e.target.value          = e.target.value.substring(0, cursorPos)        +
                                                  String.fromCharCode(e.key.charCodeAt(0) + 32) + 
                                                  e.target.value.substring(cursorPos);
                        e.target.selectionStart = ++cursorPos;
                        e.target.selectionEnd   = cursorPos;
                                        
                        e.preventDefault();
                        e.stopPropagation();
                        return false;
                    }
                );
                // Handle paste          
                document.body.addEventListener("paste", 
                    (e) => {
                        const WORD_DELIMS = ".,& -:";   // Needs work
                        
                        if (e.target.tagName.toUpperCase()  != "INPUT"      || 
                            e.target.type.toUpperCase()     != "TEXT"       ||
                            getComputedStyle(e.target).textTransform.toUpperCase() 
                                                            != "CAPITALIZE" ||
                            (
                            "transform" in e.target.dataset                 &&
                            e.target.dataset.transform.toLowerCase()
                                                            == "standard"   
                            ))                                              
                            return true;
    
                        let paste = (e.clipboardData || window.clipboardData).getData('text');
                        if (paste.length == 0)
                            return true;
                        
                        e.target.value = (e.target.value.substring(0, e.target.selectionStart) + 
                                            paste + e.target.value.substring(e.target.selectionStart)).toLowerCase();
    
                        e.preventDefault();
                        e.stopPropagation();
                        return true;
                    }
                    
                );
                // Allow the user, config file, etc set the enforcement level          
                document.getElementById("properRules").addEventListener("click", 
                    (e) => {
                        
                        if (e.target.tagName.toLowerCase() == "input" && 
                            e.target.type.toLowerCase()    == "radio" &&
                            e.target.name == "properCase")
                        {
                            var enhanced = document.getElementById("enhanced");
                            enhanced.dataset.transform = e.target.value;
                            
                            if (enhanced.dataset.transform == "none")
                                enhanced.style.textTransform = "none";
                            else
                                enhanced.style.textTransform = "capitalize";
                        }   
                        
                        return true;
                    }
                ); 
            }
    </script>
    <style type="text/css">
    input[type=radio]:checked {
        accent-color: lightblue;;
    }
    </style>
    </head>
    <body>
    <h1>Supplementing CSS Text-Transform: Capitalize</h1>
    <h2>How to enforce "Proper Case" on HTML input?</h2>
    
    <p>CSS Text-Transform will upcase each word if all lower case but not downcase multiple consecutive caps</p>
    <div id="outer">
        <div>
            <label for="enhanced">Enhanced Capitalize:</label>
            <input id="enhanced" type="text" style="text-transform: capitalize; background-color: lightblue;"
                autocomplete="new-password" autofocus spellcheck="false" >
            <br><br>
        </div>
        <div id="properRules">
          <p>"Proper Case" enforcement level:</p>
          <input type="radio" id="pc1" name="properCase" value="strict" checked=checked >
          <label for="pc1">Strict</label><br>
          <input type="radio" id="pc2" name="properCase" value="standard">
          <label for="pc2">Relaxed</label><br>
          <input type="radio" id="pc3" name="properCase" value="none">
          <label for="pc3">None</label><br>
        </div>
    </div>
    </body>
    </html>

Solution

  • Here is how you can do it without changing the html. But I would suggest a better way would be to have an input for the user to enter or paste their text and an output where you transform that text based on a radio button selection.



    HTML

    <h1>Supplementing CSS Text-Transform: Capitalize</h1>
    <h2>How to enforce "Proper Case" on HTML input?</h2>
    
    <p>CSS Text-Transform will upcase each word if all lower case but not downcase multiple consecutive caps</p>
    <div id="outer">
      <div>
        <label for="enhanced">Enhanced Capitalize:</label>
        <input id="enhanced" type="text" style="background-color: lightblue;" autocomplete="new-password" autofocus spellcheck="false">
        <button id="copy">Copy</button>
        <br><br>
      </div>
      <div id="properRules">
        <p>"Proper Case" enforcement level:</p>
        <input type="radio" id="pc1" name="properCase" value="strict" checked=checked>
        <label for="pc1">Strict</label><br>
        <input type="radio" id="pc2" name="properCase" value="standard">
        <label for="pc2">Relaxed</label><br>
        <input type="radio" id="pc3" name="properCase" value="none">
        <label for="pc3">None</label><br>
      </div>
    </div>
    

    CSS

    input[type=radio]:checked {
      accent-color: lightblue;
    }
    
    .capitalize {
      text-transform: capitalize;
    }
    

    JS

    const textCopy = document.getElementById("copy");
    const textInput = document.getElementById("enhanced");
    const radioStrict = document.getElementById("pc1");
    const radioRelaxed = document.getElementById("pc2");
    const radioNone = document.getElementById("pc3");
    
    textCopy.addEventListener('click', () => {
        textInput.select();
        const range = document.getSelection();
        const value = range.toString();
        navigator.clipboard.writeText( value );
        console.log( value );
        alert( value );
    });
    
    textInput.addEventListener('input', () => {
      if (radioStrict.checked) {
        textInput.value = textInput.value.toLowerCase();
        textInput.classList.add('capitalize');
        return true;
      }
    
      if (radioRelaxed.checked) {
        textInput.classList.add('capitalize');
        return true;
      }
    
      if (radioNone.checked) {
        textInput.classList.remove('capitalize');
        return true;
      }
    });
    
    radioStrict.addEventListener('input', () => {
      textInput.value = textInput.value.toLowerCase();
      textInput.classList.add('capitalize');
    });
    
    radioRelaxed.addEventListener('input', () => {
      textInput.classList.add('capitalize');
    });
    
    radioNone.addEventListener('input', () => {
      textInput.classList.remove('capitalize');
    });