90intuition
90intuition

Reputation: 996

Chaning the order functions are loaded breaks the code unexpectedly

I have a script that looks like this:

function autoCorrect(searchString, replaceString) {
    $("body").on("keyup", "textarea", function() {
        // finds current cursor position
        var pos = $(this).prop("selectionStart");
        // this turns the textarea in a string
        var text = $(this).val();
        //only search for strings just typed
        var stringToSearch = text.substring(pos - searchString.length, pos);
        if (searchString == stringToSearch) {
            //if there is a match put the replaceString in the right place
            var newText = text.substring(0, pos - searchString.length) + replaceString + text.substring(pos);
            $(this).val(newText);
            //adjust the cursor position to the new text
            var newpos = pos - searchString.length + replaceString.length;
            this.setSelectionRange(newpos, newpos);
        }
    });
}
autoCorrect("=>", '⇒');
autoCorrect("->", "→");
autoCorrect("+-", "±");
autoCorrect("<=", "≤");
autoCorrect(">=", "≥");

Now, I want to change it a little bit, I want to change the order the functions are running. Like this:

$("body").on("keyup", "textarea", function() {
    function autoCorrect(searchString, replaceString) {
        // finds current cursor position
        var pos = $(this).prop("selectionStart");
        // this turns the textarea in a string
        var text = $(this).val();
        //only search for strings just typed
        var stringToSearch = text.substring(pos - searchString.length, pos);
        if (searchString == stringToSearch) {
            //if there is a match put the replaceString in the right place
            var newText = text.substring(0, pos - searchString.length) + replaceString + text.substring(pos);
            $(this).val(newText);
            //adjust the cursor position to the new text
            var newpos = pos - searchString.length + replaceString.length;
            this.setSelectionRange(newpos, newpos);
        }
    }
    autoCorrect("=>", '⇒');
    autoCorrect("->", "→");
    autoCorrect("+-", "±");
    autoCorrect("<=", "≤");
    autoCorrect(">=", "≥");
});

But this the script doesn't work anymore like this. I just don't understand why this breaks my code.

Here is my jsfiddle: http://jsfiddle.net/4SWy6/4/

Upvotes: 0

Views: 64

Answers (3)

adeneo
adeneo

Reputation: 318342

A new function creates a new scope :

$("body").on("keyup", "textarea", function () {
    autoCorrect("=>", '⇒', this);
    autoCorrect("->", "→", this);
    autoCorrect("+-", "±", this);
    autoCorrect("<=", "≤", this);
    autoCorrect(">=", "≥", this);
});

function autoCorrect(searchString, replaceString, elem) {
    var acList = {
        "=>": '⇒',
        "->": "→",
        "+-": "±",
        "<=": "≤",
        ">=": "≥"
    },
        pos = elem.selectionStart,
        text = elem.value,
        stringToSearch = text.substring(pos - searchString.length, pos);

    if (searchString == stringToSearch) {
        var newpos = pos - searchString.length + replaceString.length;
        elem.value = text.substring(0, pos - searchString.length) + replaceString + text.substring(pos);
        this.setSelectionRange(newpos, newpos);
    }
}

FIDDLE

EDIT:

based on the comment asking if you could just pass an object of key / value pairs to be replaced together with the element, I cooked up this, which should work with just about any object you pass it :

$("body").on("keyup", "textarea", function () {
    var acList = {
        "=>": '⇒',
        "->": "→",
        "+-": "±",
        "<=": "≤",
        ">=": "≥"
    };
    autoCorrect(acList, this);
});

function autoCorrect(acList, elem) {
    var regstr = Object.keys(acList).join("|").replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$]/g, "\\$&"),
        reg    = new RegExp('('+ regstr +')', "gi"),
        pos    = elem.selectionStart,
        diff   = 0;

    elem.value = elem.value.replace(reg, function(x) {
        diff += x.length - acList[x].length;
        return acList[x];
    });

    elem.setSelectionRange(pos-diff, pos-diff);
}

FIDDLE

As a sidenote, this isn't really cross browser, as selectionStart and setSelectionRange doesn't work in all browsers, and neither does Object.keys, so depending on what browsers you need to support, you might have to polyfill those methods.

Upvotes: 2

goto-bus-stop
goto-bus-stop

Reputation: 11824

In the inner function, $(this) is not actually the jQuery object you're looking for. That's because this is set to the window object. Observe:

function outer() {
  function inner() {
    return this;
  }
  console.log(this, inner());
}

outer.call({ this: "is an object" });

This will log:

{ this: "is an object" }
[Object Window]

JS's scoping behavior is weird. this is very magical and you can only really trust its value right at the place where it's set. Otherwise, it's probably window.

.call on a function calls the function with a this set to the first parameter, in this case { this: "is an object" }. This is called "binding". This is why in outer, this refers to our object. We say outer is bound to that object. In inner, we should distrust this, because it's not explicitly bound to anything -- so it's probably window.

Similarly to that example:

$('body').on('click', function clickCallback() {
  // `this` refers to the element that jQuery set it to,
  // as it called the `clickCallback` function with .call(element)
  $(this).css({ 'background-color': '#f00' });
  // We now have a bright red background!
  function changeColor() {
    // `this` refers to `window`!!!
    // We didn't bind it :(
    $(this).css({ color: '#0f0' });
    // Text color will not be changed!
  }
  changeColor();
});

Internally, jQuery uses the .call method to call the clickCallback, to bind the callback's this to the element that was clicked. So we're facing the exact same situation as with our outer and inner functions.

The solution to your problem is to either 1) Bind the inner function to the outer this or 2) Save the outer this for use in the inner function.

Usually you want to go for the latter. For the first, you'd need to either .call your function all the time (autoCorrect.call(this, "=>", '⇒');), which is ugly, or use .bind once (autoCorrect = autoCorrect.bind(this);), but that is not cool either, because not all browsers support the .bind method on functions, and it looks slower.

In your case, going for the latter option:

$("body").on("keyup", "textarea", function() {
    // Store `this` for use in the inner function
    var self = this;
    function autoCorrect(searchString, replaceString) {
        // `self` will now be the element we're looking for!
        // finds current cursor position
        var pos = $(self).prop("selectionStart");
        // this turns the textarea in a string
        var text = $(self).val();
        // [...]
    }
    autoCorrect("=>", '⇒');
    autoCorrect("->", "→");
    autoCorrect("+-", "±");
    autoCorrect("<=", "≤");
    autoCorrect(">=", "≥");
});

Upvotes: 1

sdespont
sdespont

Reputation: 14025

$("body").on("keyup", "textarea", function () {
    $(this).val($(this).val()
        .replace(/=>/g, "⇒")
        .replace(/=>/g, "⇒")
        .replace(/->/g, "→")
        .replace(/\+-/g, "±")
        .replace(/<=/g, "≥")
        .replace(/>=/g, "≥"));
});

DEMO

Upvotes: 0

Related Questions