22
\$\begingroup\$

I came up with an idea for a userscript to simplify writing answers on Stack Exchange while writing a previous answer.

This userscript adds "Review"-links on the top of each code segment. When clicked, it adds checkboxes in front of every code line. You select the checkboxes for the lines you want to comment about, click the link again - which has now changed to "Add to answer", and voilà - the code will be copied to your answer.

using userscript to select checkboxes

resulting answer template after using script

I believe this userscript can significantly reduce the scrolling up and down where you consistently copy some code from the question, paste it in the answer, scroll up again, copy code, scroll down, paste, scroll up copy, etc... Now you can just go through the code from top to bottom and select the lines you want to comment on.

I have tested this userscript using Google Chrome + Tampermonkey, but I believe it should also work with Firefox + Greasemonkey, and with other browser/extension combinations that supports userscripts.

The code is also available on GitHub.

If you are able to include a userscript from a link, use this link.

// ==UserScript==
// @name          Auto-Review
// @author        Simon Forsberg
// @namespace     zomis
// @homepage      https://www.github.com/Zomis/Auto-Review
// @description   Adds checkboxes for copying code in a post to an answer.
// @include       http://stackoverflow.com/*
// @include       http://meta.stackoverflow.com/*
// @include       http://superuser.com/*
// @include       http://serverfault.com/*
// @include       http://meta.superuser.com/*
// @include       http://meta.serverfault.com/*
// @include       http://stackapps.com/*
// @include       http://askubuntu.com/*
// @include       http://*.stackexchange.com/*
// @exclude       http://chat.stackexchange.com/*
// ==/UserScript==

function embedFunction(name, theFunction) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.textContent = theFunction.toString().replace(/function ?/, 'function ' + name);
    document.getElementsByTagName('head')[0].appendChild(script);
}

embedFunction('showAutoreviewButtons', function(clickedObject) {

    var i;
    if ($(clickedObject).data('review')) {
        var answer = $("#wmd-input");
        var answer_text = answer.val();
        var added_lines = 0;
        var added_blocks = 0;

        // loop through checkboxes and prepare answer
        var checkboxes = $("input.autoreview");
        var block = [];
        for (i = 0; i < checkboxes.length; i++) {
            if (!$(checkboxes[i]).prop('checked')) {
                continue;
            }

            var checkbox = $(checkboxes[i]);
            var line_data = (checkbox).data('line');
            block.push(line_data);
            if ((i === checkboxes.length - 1) || !$(checkboxes[i + 1]).prop('checked')) {
                // add block
                var block_line;
                var cut_count = 1000;
                for (block_line = 0; block_line < block.length; block_line++) {
                    var cut_this = block[block_line].indexOf(block[block_line].trim());
                    if (cut_count > cut_this) {
                        cut_count = cut_this;
                    }
                }
                for (block_line = 0; block_line < block.length; block_line++) {
                    answer_text = answer_text + "\n    " + block[block_line].substr(cut_count);
                }
                answer_text += "\n\n---\n";
                added_lines += block.length;
                added_blocks++;
                block = [];
            }
        }

        answer.val(answer_text);
        alert(added_lines + " lines in " + added_blocks + " blocks added to answer.");

        return;
    }
    $(clickedObject).data('review', true);
    $(clickedObject).text("Add to answer");

    var spans = $("code span", $(clickedObject).next());
    console.log(spans.length);

    var count = spans.length;
    var line = "";
    var first = null;
    for (i = 0; i < count; i++) {
        var element = $(spans[i]);

        if (first === null) {
            first = element;
        }
        if (element.text().indexOf("\n") !== -1) {
            console.log(i + " line: " + line);

            var lines = element.text().split("\n");
            element.text("");
            for (var line_index = 1; line_index < lines.length; line_index++) {
                var current_line = lines[line_index];
                var prev_line = lines[line_index - 1];

                var span;
                // Add the last part of the previous line
                if (line_index == 1) {
                    line += prev_line;
                    span = $('<span class="pln zomis before">' + prev_line + '\n</span>');
                    element.after(span);
                    element = span;
                }

                // Add the checkbox for the previous line
                if (line.length > 0) {
                    var dataProperty = 'data-line="' + line + '" ';
                    var checkbox = $('<input type="checkbox" ' + dataProperty + ' class="autoreview"></input>');
                    first.before(checkbox);
                    first = null;
                }

                // Add the beginning <span> element for the current line
                if (line_index < lines.length - 1) {
                    current_line += "\n";
                }
                span = $('<span class="pln zomis after">' + current_line + '</span>');
                element.after(span);
                first = span;
                element = span;
                line = current_line;
            }
        }
        else {
            line += element.text().replace(/\\/g, '\\\\').replace(/"/g, '\\"');
        }
    }
    if (line.length > 0) {
        dataProperty = 'data-line="' + line + '" ';
        checkbox = $('<input type="checkbox" ' + dataProperty + ' class="autoreview"></input>');
        first.before(checkbox);
    }
});

$('pre code').parent().before('<a href="javascript:void(0);" onclick="showAutoreviewButtons($(this))">(Review)</a>');

Primary questions:

  • Am I following JavaScript conventions? I have seen people.coding( "like this" ); with extra spaces around each parameter, and I've also seen people declaring all variables at the beginning of a method, is this some official convention? Am I break any official convention here? I feel that I am using Java conventions in my JavaScript code.
  • Can the code be cleaned up, and utility methods and stuff be used, while still keeping browser compatibility? For example, in the code to figure out the indentation for a block I was thinking of how I would do it in Java, where I would use block.stream().mapToInt(str -> str.indexOf(str.trim())).min(), can something similar be done here?
  • Any other comments welcome!

I bet there are many things that can be improved. The fact that it's a userscript made me feel a bit restricted on what I could do, but perhaps that's just because I am not aware of how to do them for userscripts.

I plan on posting this userscript to http://www.stackapps.com when I'm happy enough with it, any feature-requests / UI suggestions can be added as issues on github, or you can ping me in The 2nd Monitor.

Auto-upvote to anyone who is using the script while writing their answer!

\$\endgroup\$
  • \$\begingroup\$ I don't like that you have to click a checkbox per line. Currently I just hit edit with middle mouse and copy the relevant parts. How is your tool faster when I have to deal with 12+ lines of code? \$\endgroup\$ – Pimgd Jan 27 '15 at 9:14
  • 2
    \$\begingroup\$ @Pimgd currently it might not be, but I am looking for ways to improve that. Such as holding shift and clicking a checkbox to select all lines from the previously selected checkbox? \$\endgroup\$ – Simon Forsberg Jan 27 '15 at 9:22
  • \$\begingroup\$ Yes, do that. Would help tons. \$\endgroup\$ – Pimgd Jan 27 '15 at 9:25
11
\$\begingroup\$

Naming

I realize naming can be hard, but 'Auto-Review' is a name that conflicts a bit with the exiting UserScript on StackExchange called AutoReviewComments

Protocols

There are a number of UserScript conventions you are failing to adhere to.

First up, for Firefox, you need to wrap the header section in to a "preserved" comment block:

/** @preserve
// ==UserScript==
.....
// ==/UserScript==
*/

The preserve is required to keep the comment block after the javascript is processed. This allows the 'compiled' version to keep the references, and for FireFox to know what it is about still.

Other browsers may not have the same requirements.

Additionally, you need to specify @grant permissions too for GreaseMonkey to be happy. In your case, none is appropriate:

// @grant   none

Once I made those modifications the userscript loaded up nicely in my FireFox.

Usability

There are four user-experience enhancements that I would suggest:

  1. NO POPUPS - popups are a horrible distraction
  2. Scroll-to-inserted-content - after inserting the code blocks, scroll to the edit point and make it visible on the screen
  3. Trigger a changed-event on the answer entry box - this will update the 'preview' of the answer. As it is currently, you have to manually change something in the answer box for the preview to update.
  4. You should clear the checkboxes after processing them. Having to un-check each box is a PITA if you need to copy different blocks later.

Review

  • double-array-dereferencing is a micro-optimization that's unnecessary. You have the following code (copied here using your userscript):

    for (i = 0; i < checkboxes.length; i++) {
        if (!$(checkboxes[i]).prop('checked')) {
            continue;
        }
    
        var checkbox = $(checkboxes[i]);
        var line_data = (checkbox).data('line');
    

    That code double-dereferences $(checkboxes[i]). I presume this is because you don't want to carry the overhead of the variable if it is not checked. This is an early-optimization. The code would be simpler as:

    for (i = 0; i < checkboxes.length; i++) {
        var checkbox = $(checkboxes[i]);
        if (!checkbox.prop('checked')) {
            continue;
        }
    
        var line_data = (checkbox).data('line');
    
  • var declarations. JavaScript 'hoists' all variable declarations to the top of the function that contains it. Unlike other languages, JavaScript should not be coded with 'block-local' declarations. It is best practice to move all variable declarations to be the first items in the function. This Stack Overflow answer does a better job of explaining it than I would.

\$\endgroup\$
  • 1
    \$\begingroup\$ While all var declarations are hoisted, I am still of the opinion that avoiding monster blocks of var declarations at the top of the function is the lesser of two evils. I'd rather accidentally declare an identifier as var twice than forget to declare it at all due to the separation between declaration and definition. \$\endgroup\$ – 200_success Jan 27 '15 at 5:42
10
\$\begingroup\$

Coding style

I'm not a JavaScript guru, but I'm pretty sure the coding style guide is much less strict compared to Java, for example. The way used spacing around operators, and the placement of braces like in Java seem very common, and I've never heard anybody object to that. There are some other styles like the ones you mentioned, but they are rare. Some people also prefer indentation using 2 spaces instead of 4, but it seems to be largely a matter of taste.

For reference, PyCharm is a fantastic IDE for Python + Web development by JetBrains (same breed as IntelliJ), it has good support for JavaScript, and it doesn't object to your code in terms of style. I'd take that as a good sign. For code like people.coding( "like this" );, the auto-format function removes the spaces inside the parens.

Bad practices

First of all, paste the code in http://jshint.com/ and see for yourself:

  • One duplicate variable definition: checkbox
  • A few variables used out of scope

In addition, I find it odd that you don't take full advantage of jQuery in embedFunction. Since you already use jQuery quite a bit elsewhere in the script, for example instead of:

   document.getElementsByTagName('head')[0].appendChild(script);

You could simplify as:

$('head').append(script);

UPDATE: It seems (from your comment) that this suggestion doesn't work for you "as-is". I'll try to figure out why. In the meantime, my objection still stands: it's a bit odd to have a mixture of classic JavaScript and jQuery within the same script, as if it was done by two different people or the same person at different times. I think it's better to be consistent and write either classic JavaScript without depending on jQuery, or fully embrace jQuery.

Cache the result of $(...) lookups

DOM lookups are not exactly cheap. So whenever you repeat a lookup, consider caching in a variable instead, for example here:

if (!$(checkboxes[i]).prop('checked')) {
    continue;
}
var checkbox = $(checkboxes[i]);

The above is better if you first assign to checkbox, and then do the if. It also reduces the duplication of checkboxes[i].

Later in the code I see $(clickedObject) many times. It would be good to cache that too, of course.

Strange elements

On this line:

var line_data = (checkbox).data('line');

The parens around checkbox are... curious... You don't need them, and might confuse the reader that it was intended as jQuery with mistakenly omitted $ (which it is not).

Augmented assignment

This assignment can be replaced with += augmented assignment:

answer_text = answer_text + "\n    " + block[block_line].substr(cut_count);
\$\endgroup\$
  • 1
    \$\begingroup\$ $('head').append(script); didn't work, I got the excellent error message "undefined is not a function". Seems like JQuery is undefined there for some reason. (I guess it hasn't been loaded yet) \$\endgroup\$ – Simon Forsberg Jan 27 '15 at 11:35
  • \$\begingroup\$ That surprises me, because if jQuery wasn't loaded, then the $('pre code') you have right after the embedFunction(...) call should have given the same error in the first place. I'll try to play with your script a bit more tonight. I did a minor update to this point in my review now. \$\endgroup\$ – janos Jan 27 '15 at 12:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.