1

I have this userscript (written with a great help from Stack Overflow) for the site metal-archives.com.

Its structure is like this:

function appendColumn(...) {
    // code for appending column
    // code for making the table sortable   
}

waitForKeyElements ("table", appendColumn);        

The script works okay except for a visual glitch/delay as you switch sub-tabs (tables).

When switching, the extra (6th) column is initially shown as expected. But then, the table is displayed momentarily in its original form, and then finally with the 6th column as it should be.

To see this, install the script, visit this typical target page, and then switch the sub-tabs (Between Complete Discography, Main, Lives, Demos, Misc, etc.).
It looks like this:

Screen shot of table "bounce"


I've tried to make it so that the initial table does not appear by adding:

GM_addStyle(".display.discog {display: none;} ");

to the beginning of appendColumn() and:

GM_addStyle(".display.discog {display: inline !important;} "); 

to the end of appendColumn().
But it didn't make any difference.


I used Firefox Network Monitor on that page and it seems that when you switch tabs:

  • The code immediately modifies the table (which is loaded from cache?? -- because there's no entry in Network Monitor).
  • Then the table (the relevant HTML file) is loaded from the server.
  • Then the table is modified one last time.

How can I change the code (while using waitForKeyElements) to prevent the key element from being displayed, and only display it after it's modified by my code?

Or how do I speed up the response?

Thank you.

Brock Adams
  • 90,639
  • 22
  • 233
  • 295
darkred
  • 591
  • 5
  • 28

2 Answers2

7

I loaded your script, added timing lines, and tested it. The time elapsed from AJAX-complete until the table was fixed and finished was only 400 to 500 milliseconds! That's plenty fast for most people and situations.

But, for those times when you absolutely want to squeeze out the milliseconds, you can switch to MutationObservers. These are finicky, brittle, and less cross-browser portable, but they are fast.
In this case, MutationObservers took the AJAX-to-fixed-table time down to the 20 to 40 millisecond range.

I recommend using a library like Mutation Summary to take some of the pain out of the process.

To convert from a simple waitForKeyElements() implementation to Mutation Summary:

  1. Add

    @require https://raw.githubusercontent.com/rafaelw/mutation-summary/master/src/mutation-summary.js
    

    to your metadata block.

  2. Plug your waitForKeyElements callback and simple selector into this structure:

    var muteObserver = new MutationSummary ( {
        callback: handleDiscographyChanges,
        rootNode: $( {ANY-JQUERY-SELECTOR} )[0],
        queries: [ {element: {A-SIMPLE-SELECTOR}} ]
    } );
    
    function handleDiscographyChanges (muteSummaries) {
        var mSummary    = muteSummaries[0];
        if (mSummary.added.length) {
            {YOUR-CALLBACK} ( $(mSummary.added[0]) );
        }
    }
    

For example in this case, change:

waitForKeyElements (".display.discog", appendColumn);

To:

var muteObserver = new MutationSummary ( {
    callback: handleDiscographyChanges,
    rootNode: $("#band_disco")[0],
    queries: [ {element: ".discog"} ]
} );

function handleDiscographyChanges (muteSummaries) {
    var mSummary    = muteSummaries[0];
    if (mSummary.added.length) {
        appendColumn ( $(mSummary.added[0]) );
    }
}

Where the rootNode was determined by examining the page structure.




For reference, a complete script with 3 optional approaches and timing logging is below. It was only tested on Firefox, but should work with Tampermonkey too (maybe).

See the //OPTION n lines just above each line to optionally comment out.

// ==UserScript==
// @name        Metal Archives (discography pages) - Reviews column split and sortable tables
// @include     http://www.metal-archives.com/bands/*
// @include     http://www.metal-archives.com/band/*
// @grant       none
// @require     http://code.jquery.com/ui/1.9.1/jquery-ui.min.js
// @require     https://greasyfork.org/scripts/2199-waitforkeyelements/code/waitForKeyElements.js?version=6349
// @require     https://greasyfork.org/scripts/5844-tablesorter/code/TableSorter.js?version=21758
// @require     https://raw.githubusercontent.com/rafaelw/mutation-summary/master/src/mutation-summary.js
// ==/UserScript==

function appendColumn(jNode) {
    logTime ("Table fixed");

    // STEP 1+2: SPLIT THE 'REVIEWS' COLUMN INTO A 'REVIEWS' COLUMN AND A 'RATINGS' COLUMN
    var tbl = jNode[0];     // table reference

    // If the current sub-table has no data, then stop the execution of the function
    if (tbl.rows[1].cells[0].innerHTML == '<em>Nothing entered yet. Please add the releases, if applicable. </em>') {
        return;
    }

    var newCell, newText;

    const cols = tbl.rows[0].cells.length - 1;

    var tr = tbl.tHead.children[0],
    th = document.createElement('th');

    th.innerHTML = "Ratings";
    th.className = "ratingsCol";
    tr.appendChild(th);

    for (i = 1; i < tbl.rows.length; i++) {
        k = tbl.rows[i].cells[cols].innerHTML;    // Retrieve the content of the current cell of the Review column and store it to variable k


        re1 = /<a [^>]*>[^(]*[(]([^)]+)/ ;        // (RegEx which matches the 'Ratings' percentage(incl.the % symbol)
        l = re1.exec(k);                          // (Execute the RegEx and store it to variable l)

        newCell = tbl.rows[i].insertCell(-1);     // Add a new cell (for the new 'Ratings' column ) -for each row-

        if (re1.test(k) != 0){                    // If the RegEx has matches, (only) then create new cells with...

            re0 = /(<a [^>]*>)[0-9]*[^(]/ ;       // (RegEx which matches the reviews URL)
            url = re0.exec(k);                    // (Execute the RegEx and store it to variable url)

            newCell.innerHTML = url[1] + l[1] + '</url>'; // ...the Ratings percentage (which is also a link to the Reviews)...


            re2 = /<a [^>]*>([0-9]*)[^(]/ ;       // (RegEx which matches the 'Reviews' number)
            m = re2.exec(k);                      // (Execute the RegEx and store it to variable m)

            newCell = tbl.rows[i].cells[cols];    //
            newCell.innerHTML = url[1] + m[1] + '</url>'; // ...and the Reviews number (which is also a link to the Reviews)
        }
    }

    //  STEP 3: MAKE THE DISCOGRAPHY TABLE SORTABLE  (using the jQuery plugin "tablesorter")
    $(tbl).tablesorter ( {
        cssAsc: 'up',
        cssDesc: 'down',
        headers: {
              0: {sorter: false}
        }
    } );
}

//OPTION 1
//waitForKeyElements (".display.discog", appendColumn);

$(document).ajaxComplete (function (e, xhr, config){
    logTime ("Ajax complete");
    //OPTION 2
    return; //-- For compare test

    if (config.url.indexOf ('/tab/') != -1){
        $(".display.discog").each ( function () {
            appendColumn ( $(this) );
        } );
    }
} );

$("#band_disco > ul > li").on ("click", "a.ui-tabs-anchor", function (zEvent) {
    logTime (zEvent.target.textContent + " tab was clicked.");
} );

function logTime (lableTxt) {
    var tNow    = new Date ();
    console.log (tNow.toLocaleFormat ('%H:%M:%S') + "." + tNow.getMilliseconds (), " <== " + lableTxt);
}

//OPTION 3
//*--- Remove leading slash, from this line, to comment out block, below.
var muteObserver = new MutationSummary ( {
    callback: handleDiscographyChanges,
    rootNode: $("#band_disco")[0],
    queries: [ {element: ".discog"} ]
} );
//*/ -- Tail end of optional comment block

function handleDiscographyChanges (muteSummaries) {
    var mSummary    = muteSummaries[0];
    if (mSummary.added.length) {
        appendColumn ( $(mSummary.added[0]) );
    }
}

Note that styling code, and some original comments, were omitted from this example.

Brock Adams
  • 90,639
  • 22
  • 233
  • 295
  • Though I had accepted and upvoted this answer the day you posted it, I just wanted to express my gratitude for this great, so detailed answer! It was far beyond my expectations! Thank you! I'm grateful – darkred Oct 26 '15 at 14:36
0

waitForKeyElements is a slow way to insert your content. This is one reason you're seeing the rendering behavior that you are seeing.

The reason the tab is rendered appropriately when you first switch is because it is hidden and correctly adjusted while hidden. When the tab is shown the content is updated to reflect the latest from the server, which has not been adjusted. waitForKeyElements then notices the change and fires again to correct.

Using ajaxComplete you can hide the panel when the content is first loaded.

Hiding the panel when data first returns:

$(document).ajaxComplete(function(e, xhr, config){
   if(config.url.indexOf('/tab/') != -1){                     // Because the tables URLs are like `www.metal-archives.com/band/discography/id/xxx/tab/lives`
       $('.ui-tabs-panel').css('visibility', 'hidden');
   }
});

Showing the panel in your appendColumn function:

function appendColumn(...) {
    // code for appending column
    // code for making the table sortable   
    $('.ui-tabs-panel').css('visibility', 'visible');
}

Happy coding!

darkred
  • 591
  • 5
  • 28
N-ate
  • 6,051
  • 2
  • 40
  • 48
  • I don't mind changing waitForKeyElements. But it was practical because it was passing the node of the key element(jNode) -i.e. the relevant table- to the callback function (`appendColumn`). Passing the nod is not possible via `ajaxComplete`. – darkred Aug 27 '15 at 11:49
  • I've updated the code with a better approach. In this case a hybrid approach. – N-ate Aug 27 '15 at 14:13
  • Thanks for trying to help, but even with your new approach, I still need an equivalent of waitForKeyElements's `jNode` i.e. a way to pass the node of the key element to `appendColumn`. (I edited your answer, adding the correct url to the 1st code part) – darkred Aug 27 '15 at 16:00
  • My updated answer doesn't change your use of waitForKeyElements and therefore should give you access to the node as it always had. – N-ate Aug 28 '15 at 17:05
  • 1
    You would make this answer almost work by using code like `OPTION 2` in my answer. However, it doesn't help much as when the AJAX completes, the new table still doesn't exist yet. There is additional delay while the table is inserted. ... Also, this answer's approach is brittle and subject to break if the `@grant` mode is changed. – Brock Adams Aug 28 '15 at 19:00
  • @BrockAdams it isn't any more subject to breakage than his original answer. The ajax complete only hides the area that shows prior to waitForKeyElement firing. After waitForKeyElement fires it is made visible. – N-ate Aug 30 '15 at 02:47
  • The OP's original script could easily be made to work in any grant mode, and across several platforms. But AJAX intercepts of target-page operations, MUST operate in the page scope. Making this type of answer break or get much more complicated -- depending on the grant mode and also on the platform. – Brock Adams Aug 30 '15 at 03:08
  • I understand now. You're using break in a way I've not seen it used. You mean that under certain conditions the intercept may fail and the dom will not be hidden. You're not saying that it actually breaks anything about the page, functionality, or user experience. It only might fail to hide the mis-rendered portion in an exotic environment. – N-ate Aug 31 '15 at 15:35