Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent removal of DOM elements on the Tumblr dashboard

I decided to keep the posts displayed on the dashboard by my extension inside the #posts element. Now my problem is that Tumblr automatically removes posts' content while scrolling.

Here is an example of a normal post before the removal:

<li class="post_container" data-pageable="post_110141415125">
    <div class="post post_full ..." [...]>
        <!-- ... -->
    </div>
</li>

And the same post, after the removal of its content:

<li class="post_container" data-pageable="post_110141415125" style="border-radius: 3px; height: 207px; width: 540px; background-color: rgb(255, 255, 255);">
</li>

Since that my extension re-positions the posts into a pinterest-like layout, the removal of posts' content messes up the entire layout. Is it possible to stop the Tumblr dashboard from removing the DOM elements inside each .post?

EDIT:

I added this injection code for the removeChild method in my content script:

function addScript(){
    var script = document.createElement('script');  
    script.id = 'newRemoveChildScript'; ///just to find is faster in the code
    script.src = chrome.extension.getURL("injectedCode.js");
    $($('script')[0]).before(script); ///Script is indeed present, I checked that
}

window.onload = addScript;

The next I did, was to create the injectedCode.js file with the second version, looking like this:

(function (obj, method) {
    function isPost(el) {
        return (/^post_\d+$/.test(el.id) && ~el.className.indexOf('post'));
    }
    obj[method] = (function (obj_method) {
        return function (child) {
            if (isPost(child)) return child;
            return obj_method.apply(this, arguments);
        }
    }(obj[method]));
}(Element.prototype, 'removeChild'));

When I load the Tumblr-Dashboard I can confirm that the Script-element is present in the head-element. The file is also properly linked, because the 'removeChild' function is called once. A script element is being removed there, it seems.

From that point on, the function is never called again and I can see those 'empty' posts again.

like image 410
Eru Iluvatar Avatar asked Dec 04 '22 04:12

Eru Iluvatar


1 Answers

Hijacking Tumblr dashboard behavior

Disclaimer: completely legal things going on here, don't panic!

What you're asking is something that is not really easy to do, but you can actually stop Tumblr from hiding the posts while scrolling when they get out of the viewport. More precisely, an advanced programmer could actually manipulate any Tumblr method to make it behave like they want, but, for now, let's focus on your question.

Why does Tumblr remove posts while scrolling?

Tumblr removes posts' content to make the page lighter and let you continue browsing without experiencing any lag and/or struggle scrolling because of the enormous amount of elements. Imagine scrolling down for an hour straight: your dash would hold thousands of posts, and your browser will probably freeze due to the excessive amount of things that it has got to move each time you scroll.

How does Tumblr accomplish this?

I didn't inspect and understand every single line of the Tumblr code, because it's all packed/minified, and it would be impossible to actually understand a single line of any method (unless you work at Tumblr, in which case congratulations), but, basically, I put some DOM breakpoints and I've inspected the part of code that removes the posts when they go out of the viewport.

In a nutshell, this is what it does:

  1. Tumblr creates some custom and crafty "scroll" events for the posts. These events will fire every time a post gets more than a certain distance out of the viewport.

  2. Now Tumblr attaches some listeners to these custom events, and, using a DOM breakpoint on a li.post_container element, you'll be able to see that these listeners reside in the dashboard.js JavaScript file.

    dashboard.js

    More precisely, the exact part of code that removes the posts' content looks like this:

    function j(t,v) { try{ t.removeChild(v) } catch(u){} }
    

    where t is the li.post_container and v is its children: div#post_[id].

    What does this part of code do? Nothing special: it literally removes the div#post_[id] element from its parent li.post_container.

  3. Now Tumblr has got to style the removed post's container in order to maintain the height of the #posts element, which holds all the posts, otherwise removing a post's content would make the page to scroll down H pixels, where H is the height of the post which got removed.

    This change takes place inside another script, which is index.js, and I didn't even take a closer look at it because it's just impossible to read:

    index.js

    By the way, this script is not important, since that we can use simple CSSs to prevent style changes.

  4. Now that the post's content has been "removed", it looks something like this:

    removed post

    *Just for clarity: I added the text and the tombstone, you don't actually see them lol

So, in the end, when a post gets out of the viewport, changes from this:

<li class="post_container" [...]>
    <div id="post_[id]" class="post post_full ..." [...]>
        <!-- post body... -->
    </div>
</li>

to this:

<li class="post_container" [...] style="border-radius: 3px; height: Hpx; width: Wpx; background-color: rgb(255, 255, 255);">
</li>

Prevent Tumblr code from removing posts (some little hack indeed)

Now that we know how Tumblr behaves when scrolling through the dashboard, we can prevent it from removing posts and styling them to look like empty white dead blocks with rounded borders.

Style first

Before caring about preventing the posts from being removed, we should style them not to be of fixed height and width, overriding the styles applied to them by Tumblr when they go out of the viewport.

We can do this injecting some simple CSSs in the page, making the rules !important to prevent inline styles from overriding them. Here's the CSS we'll add:

li.post_container {
    height: auto !important;
    width: 540px !important;
    /* 540px is the default width
     * change it to any amount that applies to your layout 
     */
}

Alternatively, using JavaScript, we can do:

var s = document.createElement('style');
s.textContent = 'li.post_container{height:auto!important;width:540px!important;}';
document.head.appendChild(s);

Prevent post removal: easy (silly) way

If we do not want to lose time thinking of a good solution, we could just implement the simplest one, and, In a few lines of code we could:

  1. Duplicate the removeChild method of the Element.prototype, and call it something like _removeChild.

  2. Replace the original one with a function that "filters" the calls made to it, deciding to remove anything except posts.

Here's the simple yet silly code to solve the issue:

Element.prototype._removeChild = Element.prototype.removeChild
Element.prototype.removeChild = function(child) {
    // Check if the element is a post (e.g. the id looks like "post_123456...")
    if (/^post_\d+$/.test(child.id)) {
        // If so, stop here by returning the post
        // This will make Tumblr believe the post has been removed
        return child;
    }
    
    // Otherwise the element isn't a post, so we can remove it
    return Element.prototype._removeChild.apply(this, arguments);
}

Code length without comments: 5 lines

You can check for more than one condition obviously: for example you can check if the post contains the classes post and post_full, or if it's parentElement has class post_container, and so on... it's up to you.

Prevent post removal: smart (harder) way

A more elegant, yet crafty, way to accomplish this manipulation is to "edit" the original Element.prototype.removeChild method, using some closures and more complicated logic. Here's what we're going to do:

  1. Create a closure in which we'll redefine the Element.prototype.removeChild method adding a "filter" to it for the posts.

  2. In this closure we will define our custom filter function, which we could call isPost(), to filter the elements that are going to be removed.

  3. Using a second closure, we'll redefine the Element.prototype.removeChild method to behave like this:

    1. Check the element to remove using our previously created filter function isPost().

    2. If the element to remove is a post, then stop here and do not remove it.

    3. Otherwise continue and call the original Element.prototype.removeChild method using the .apply method.

It may look difficult, but the code is not that hard to understand, and I fulfilled it with comments to make it more comprehensible, here it is:

(function (obj, method) {
    // This is all wrapped inside a closure
    // to make the new removeChild method remember the isPost function
    // and maintain all the work invisible to the rest of the code in the page

    function isPost(el) {
        // Check if the element is a post
        // This will return true if el is a post, otherwise false
        return (/^post_\d+$/.test(el.id) && ~el.className.indexOf('post'));
    }
    
    // Edit the Element.prototype.removeChild assigning to it our own function
    obj[method] = (function (obj_method) {
        return function (child) {
            // This is the function that will be assigned to removeChild
            // It is wrapped in a closure to make the check before removing the element
            
            // Check if the element that is going to be removed is a post
            if (isPost(child)) return child; 
            // If so, stop here by returning the post
            // This will make Tumblr believe the post has been removed

            // Otherwise continue and remove it
            return obj_method.apply(this, arguments);
        }
    }(obj[method]));

}(Element.prototype, 'removeChild'));

Code length without comments: 11 lines

Injecting the script in the page

Since that the content scripts work in an hidden, different window object, and therefore content script code cannot interact with page code, you'll have to inject the code directly in the page from your content script. To do this you'll need to create a <script> element and insert it in the <head> of the page. You can store the code you want to inject in a new script (let's call it injectedCode.js) and use chrome.runtime.getURL to get the absolute URL to use in your <script> src.

Important: you'll need to add the injectedCode.js file to your "web_accessible_resources" field in your manifest.json to make it accessible from Tumblr, like this:

"web_accessible_resources": ["/injectedCode.js"]

Then you can create a <script> setting the .src to your injectedCode.js like this:

/* In your content script: */

var s = document.createElement('script');
s.src = chrome.extension.getURL("/injectedCode.js");
document.head.appendChild(s);

End-notes

These are just two methods to accomplish what you want to do: prevent Tumblr from hiding posts while scrolling. There obviously are many other ways to do this, such as using MutationObservers to check for subtree modifications, remove all the posts and create a new container with new personal events and methods to emulate the Tumblr ones, interject the Tumblr code and edit it directly (almost impossible and not much legit), and so on.

I also want to underline the fact that this is only an answer to a question, and I'm not responsible for an incorrect use of these kind of manipulations to harm your extension's users.

like image 166
Marco Bonelli Avatar answered Dec 09 '22 14:12

Marco Bonelli