Emily
Emily

Reputation: 10088

How to disable smart quotes for textarea fields in the browser?

I have a application that runs in the browser that compares strings. String compares with quotes fail using iOS11 because it is defaulting to smart quotes being on. can’t does not equal can't

I know smart quotes can be disabled under Setting for the whole device but I would like to handle it at the textarea level.

I thought I would be able to catch the smart quote on the keypress event, but iOS is making the switch later and under the covers.

textareaText is the text in my textarea field
39 is the character code for single quote
8216 is the character code for the left single smart quote
222 is the key code for the quote key
e is the event object passed to the keyboard event

keydown 
    e.keycode -> 222, e.key -> ', key.charCodeAt(0) -> 39 
    textareaText -> empty
keypress
    e.keycode -> 39, e.key -> ', key.charCodeAt(0) -> 39 
    textareaText -> empty
—> character is put in textarea here
keyup (iPad onscreen keyboard)
    e.keycode -> 222, e.key -> ', key.charCodeAt(0) -> 39 
    textareaText -> ’, textareaText.charCodeAt(0) -> 8216 
keyup (iPad external keyboard)
    e.keycode -> 0, e.key -> ', key.charCodeAt(0) -> 39 
    textareaText -> ’, textareaText.charCodeAt(0) -> 8216 

The keyup event thinks the character is the regular quote while the textarea contains the smart quote.

I tried:

textarea.innerHTML = textarea.innerHTML.replace(/’/,'\'')

but the cursor gets reset to the beginning of the string and I don't want to have to get and set the cursor position.

Is there any way to swap the smart quote before it is entered into the textarea? Or some way to disable smart quotes at the textarea level? Is any other solution to this problem?

My last resort is to replace the smart quotes in the string right before I compare it but I would prefer the browser to display the regular quotes to be consistent with what they entered, what they see, and what they are checked against.

Upvotes: 7

Views: 3751

Answers (3)

robotspacer
robotspacer

Reputation: 2762

I agree with the accepted answer that spellcheck="false" is the easiest way to handle this, but I wanted to find a solution that:

  • Doesn’t affect spell check or autocorrect if the user has them enabled.
  • Doesn’t affect other automatic substitutions like converting -- to .
  • Works in all browsers that insert curly quotes automatically. In addition to Safari on iOS, most Mac browsers do this as well. Safari for Mac behaves uniquely and must be handled differently.
  • Only affects automatic replacements. If the user explicitly enters a curly quote, pastes in curly quotes, etc. then those quotes should not be altered.

My solution is below. I’ve included lots of comments to explain each part of my approach. The DOMContentLoaded event listener at the end can be adjusted to determine which text areas it is applied to.

In my testing so far this seems to work very well. I’ve tested it in these browsers:

  • Safari on iPadOS 18.2.1
  • Safari 18.1.1 on macOS 15.1.1
  • Chrome 132.0.6834.111 on macOS 15.1.1
  • Microsoft Edge 132.0.2957.127 on macOS 15.1.1
  • Firefox 134.0.2on macOS 15.1.1 (which does not insert curly quotes)
"use strict";

(function(){

var keyPress = null;

function disableQuoteReplacement(element) {

    // This function disables curly quote replacement on `element`. The goals
    // of this are:
    //
    // - If a straight quote is entered by the user, and the browser tries to
    //   replace it with a curly quote, that should be prevented.
    // - If the user explicitly enters a curly quote, whether by typing it,
    //   pasting it, or any other method, that should not be prevented.
    // - Ideally we do not want to disable other features the user may have
    //   enabled, such as spell check or autocorrect.
    // - This should all be done as safely as possible (avoiding unexpected
    //   behavior in untested browsers) and as transparently as possible (so
    //   the user can always see what text has been entered).

    // This user agent detection is not ideal, but I couldn't find a proper
    // way to check whether a browser uses the `insertReplacementText` input
    // type. Checking `maxTouchPoints` is necessary because iPads use the Mac
    // user agent despite behaving very differently.

    const agent = navigator.userAgent;

    if (agent.includes('Safari/') &&
        agent.includes('Macintosh;') &&
        agent.includes('Chrome/') == false &&
        navigator.maxTouchPoints == 0) {

        // Safari for Mac uses a unique input type for replacements, it's easy
        // to prevent them with a low risk of anything unexpected happening.
        // It's important to note that in Safari for Mac, the replacement does
        // not always happen immediately after typing the character, so the
        // default approach below will not work.

        console.debug('Using Safari for Mac curly quote prevention.');

        element.addEventListener('beforeinput', function(e) {
            if (e.inputType == 'insertReplacementText') {
                if (straightenQuote(e.data)) {
                    console.debug('Ignoring replacement: '+e.data);
                    e.preventDefault();
                }
            }
        }, false);

    } else if (agent.includes('Macintosh;')) {

        // In other browsers, we need to use a different approach. Some
        // browsers (like Chromium) send a `beforeinput` event as they're
        // about to replace a straight quote with a curly quote. Other
        // browsers (like Safari on iOS) insert the curly quote directly.
        //
        // We can handle both of these by keeping track of the last key press,
        // and comparing it with the text that's about to be inserted. If they
        // don't match, and the text to be inserted is a curly quote, we
        // prevent the action, insert a straight quote instead, and move the
        // insertion point to the end of the selection range.

        console.debug('Using default curly quote prevention.');

        element.addEventListener('keypress', function(e) {
            keyPress = e.key;
        }, false);

        element.addEventListener('beforeinput', function(e) {
            if (e.inputType == 'insertText') {
                const input = e.data;
                if (keyPress && input != keyPress) {
                    const straight = straightenQuote(input);
                    if (straight) {
                        console.debug('Straightening replacement: '+e.data);
                        e.preventDefault();
                        const element = e.target;
                        const start = element.selectionStart;
                        const end = element.selectionEnd;
                        element.setRangeText(straight, start, end, 'end');
                    }
                }
            }
        }, false);
    
    }

}

function straightenQuote(text) {
    switch (text) {
        case '‘':
        case '’':
            return "'";
        case '“':
        case '”':
            return '"';
        default:
            return null;
    }
}

window.addEventListener("DOMContentLoaded", function() {
    const element = document.getElementById('txtIn');
    disableQuoteReplacement(element);
}, false);

})();

Upvotes: 0

Erik
Erik

Reputation: 392

The simplest way is to pass spellcheck="false" to the input element. This is based on how WebKit works: https://github.com/WebKit/WebKit/blob/596b3b01497f3bd3b3ea653ec79ffc69d6f68b30/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm#L6785

Upvotes: 13

Emily
Emily

Reputation: 10088

I ended up using the document.execCommand on the keypress event. This solution also works for contenteditable divs.

function checkInput(e) {
  if ((e.which == 39) || (e.which == 8216) || (e.which == 8217)) {
    e.preventDefault();
    document.execCommand('insertText', 0, "'");
  }
}   

var el = document.getElementById('txtIn');
el.addEventListener('keypress', checkInput, false);

Upvotes: 1

Related Questions