The Return of the view raw button


  • BINNED

    @anotherusername Ok, makes sense... So, the template looks OK, the data is sent to the client... Y U NO RENDER?



  • @accalia said:

    huh... okay then.....

    $('.post-tools:not(:has(a.viewRaw))')

    that works.

    ...yeah, I'm not actually used to jQuery's CSS extensions, so I didn't even know that you could do that. As a matter of fact, .post-tools:not(a.viewRaw) doesn't even work in plain CSS (syntax error... .post-tools:not(a):not(.viewRaw) works, and should accomplish the same thing, I thinkbut I think it's going to be slightly different because it's ~a & ~b instead of ~(a & b)).



  • @FrostCat said:

    @accalia said:

    one cookie to anyone who wants to make it do the whole toggle

    Sigh. But then I'd have to learn more JS.

    0_1458925359845_aaaaandImInLoveWithYou.jpg



  • Okay, here's what I've got, and it appears to work:

    function addRawButtons() {
        function showRaw() {
            for (var e = this; e.getAttribute("component") != "post"; e = e.parentElement);
            var post = e.querySelector(".content:not(.raw-content)"), raw = e.querySelector(".raw-content");
            var pid = e.getAttribute("data-pid");
    
            if (raw) {
                if (post.classList.contains("hidden")) {
                    raw.classList.add("hidden");
                    post.classList.remove("hidden");
                } else {
                    post.classList.add("hidden");
                    raw.classList.remove("hidden");
                }
    
            } else {
                (unsafeWindow || window).eval("(" + (function (pid, post) {
                    socket.emit('posts.getRawPost', pid, function (err, rawContent) {
                        var showErrMsg = console && (console.error || console.log).bind(console) || alert;
                        if (err) {
                            showErrMsg(err);
                        } else {
                            var raw = document.createElement('div');
                            raw.setAttribute('class', 'content raw-content');
                            raw.innerText = rawContent;
    
                            post.parentElement.insertBefore(raw, post);
                            post.classList.add('hidden');
    
                            var o = new MutationObserver(function () {
                                var li = post.parentElement, raw = li.querySelector(".raw-content");
                                if (!raw || raw.classList.contains("hidden")) {
                                    li.contains(raw) && li.removeChild(raw);
                                    o.disconnect();
                                } else {
                                    socket.emit('posts.getRawPost', pid, function (err, rawContent) {
                                        if (err) {
                                            showErrMsg(err);
                                        } else {
                                            raw.innerText = rawContent;
                                        }
                                    });
                                }
                            });
                            o.observe(post, {childList: true, characterData: true, subtree: true});
                        }
                    });
                }) + ")")(pid, post);
            }
        }
        
        Array.apply(0, document.querySelectorAll('.post-tools')).forEach(e => {
            if (!e.querySelector(".view-raw")) {
                var viewRawButton = document.createElement("a");
                viewRawButton.appendChild(document.createTextNode("View Raw"));
                viewRawButton.setAttribute("class", "view-raw no-select");
                viewRawButton.setAttribute("href", "#");
                e.insertBefore(viewRawButton, e.firstChild);
                viewRawButton.addEventListener("click", showRaw);
            }
        });
    }
    
    var observer = new MutationObserver(addRawButtons);
    observer.observe(document.getElementById("content"), {childList: true, characterData: true, subtree: true});
    addRawButtons();
    
    document.head.appendChild(document.createElement("style")).innerHTML =
        "a.view-raw{background-color:#337ab7;color:white;padding:10px;margin-right:3px}.deleted a.view-raw{display:none}" +
        ".raw-content{font-family:monospace;font-size:10pt;white-space:pre-wrap;margin-bottom:15px}";
    

  • FoxDev

    @anotherusername said:

    (unsafeWindow || window).eval(

    really? REALLY?!



  • @accalia ... I know. It refused to let me use socket unless I called it from unsafeWindow, and eval is the easiest way to do that.

    I changed it to (unsafeWindow || window) in order that it shouldn't crash if someone tried to run it outside of GreaseMonkey.

    It's meant more to use as a roadmap than a copy-and-paste, though.


  • BINNED

    @anotherusername I may steal bits of that for the plugin if the NodeBB guys say there's no better way. With your permission of course ;)

    In either case, the socket abuse will be something that you'll be able to remove, since I currently have this working:

    "posts": [
            {
                "pid": 1,
                "uid": 1,
                "tid": 1,
                "content": "<h1>Welcome to your brand new NodeBB forum!</h1>\n<p>This is what a topic and post looks like. As an administrator, you can edit the post's title and content.<br />\nTo customise your forum, go to the <a href=\"../../admin\">Administrator Control Panel</a>. You can modify all aspects of your forum there, including installation of third-party plugins.</p>\n<h2>Additional Resources</h2>\n<ul>\n<li><a href=\"https://docs.nodebb.org\" rel=\"nofollow\">NodeBB Documentation</a></li>\n<li><a href=\"https://community.nodebb.org\" rel=\"nofollow\">Community Support Forum</a></li>\n<li><a href=\"https://github.com/nodebb/nodebb\" rel=\"nofollow\">Project repository</a></li>\n</ul>\n",
                "timestamp": 1458676374427,
                "reputation": 0,
                "votes": 0,
                "editor": null,
                "edited": 0,
                "deleted": false,
                "timestampISO": "2016-03-22T19:52:54.427Z",
                "editedISO": "",
                "raw": "# Welcome to your brand new NodeBB forum!\n\nThis is what a topic and post looks like. As an administrator, you can edit the post\\'s title and content.\nTo customise your forum, go to the [Administrator Control Panel](../../admin). You can modify all aspects of your forum there, including installation of third-party plugins.\n\n## Additional Resources\n\n* [NodeBB Documentation](https://docs.nodebb.org)\n* [Community Support Forum](https://community.nodebb.org)\n* [Project repository](https://github.com/nodebb/nodebb)",
                "index": 0,
    <snip>
    }]
    


  • @Onyx said:

    I may steal bits of that for the plugin if the NodeBB guys say there's no better way. With your permission of course ;)

    Absolutely. That's why I posted it.



  • @accalia said:

    really? REALLY?!

    Honestly, I thought the (function () { /* ... */ return arguments.callee; })() was an even uglier hack.


  • FoxDev

    @anotherusername said:

    It refused to let me use socket

    what was the error?



  • @accalia Well for starters, since it was inside the GreaseMonkey script, I got ReferenceError: socket is not defined. Then I checked that unsafeWindow.socket existed and tried calling that instead, and got Error: Permission denied to access property "apply".

    So I deduced that I needed to have code inside unsafeWindow call socket, and did that, and it worked. The function-string-mashing was mostly just because it let the code highlighter and indenter actually work correctly. (The idea is to pass "(function () {...})" to eval, and it returns the function within the scope of unsafeWindow. But writing the function as a giant string would've been difficult because the code indenter and highlighter wouldn't work inside the string. It likes "(" + (function () {...}) + ")" much better.)

    shrug

    If someone builds it into WTDWTF, they shouldn't run into the issue and they can just use socket.


  • FoxDev

    @anotherusername said:

    Well for starters, since it was inside the GreaseMonkey script,

    huh. dunno if tampermonkey is different but the script i posted at the top of thread is literally my tampermonkey script i used to start this whole thing.

    worked fine there....


  • Discourse touched me in a no-no place

    Does the return of the raw button mean people are going to start hiding shit in posts again?


  • FoxDev

    @loopback0 Given how is escaped, probably not


  • FoxDev

    @loopback0 said:

    oes the return of the raw button mean people are going to start hiding shit in posts again?

    only if we figure out a way to abuse the markdown parser nodebb uses.....

    so probably.

    good news though, if someone hides something in a post and then goes all :whoosh: on you for you missing it, they're just an arsehole so you can ignore it


  • Discourse touched me in a no-no place

    @RaceProUK said:

    probably not

    Good.


  • BINNED

    @loopback0 It's still useful for debugging. Also, personally, I'm taking this on as a learning experience on the whole plugin thing.



  • @accalia Well, if we can get @ben_lubar to whitelist the hidden attribute...

    (I don't mean class="hidden" or style="visibility:hidden;", I mean hidden...)
    0_1458930803011_Untitled.png


  • FoxDev

    @anotherusername said:

    if we can get @ben_lubar to whitelist the hidden attribute

    0_1458930903525_Do Not Want.png



  • @RaceProUK That one's easy, though... you just use a userscript to override it with [hidden] { display: unset !important; } and some other styles to make it stand out.

    I do agree, in Discourse the were annoying because there was no easy way to automatically find them (like there is when they're proper HTML comments) and hell if I'm going to try to view the raw for every individual post just in case someone's hidden something there. If it can't be done automatically, it's not worth the trouble.


  • Discourse touched me in a no-no place

    @Onyx said:

    It's still useful for debugging.

    I wasn't questioning the raw button itself, just one irritating use of it.

    I do remember when the content of a forum post didn't need debugging though.


  • BINNED

    @loopback0 bah, you're just high on :onion: fumes again, gramps!



  • ...okay, so apparently, when a post gets deleted, the "view raw" button doesn't go away...

    It adds the deleted class to the li element, and it adds the hidden class to all of the other .post-tools elements... but not to the "view raw" button.

    Adding a CSS rule to give it the display:none style when it's a descendant of .deleted fixes that.



  • I've come up with a version that only hides the baked content instead of replacing it with the raw as @AngleOSaxon's does. This should allow the baked post to update while you are viewing the raw. It also makes an attempt to escape some of the HTML in the raw in order to prevent some problems that I noticed when testing.

    // ==UserScript==
    // @name         View Raw Button
    // @match        https://what.thedailywtf.com/*
    // @grant        none
    // ==/UserScript==
    'use strict';
    
    // Define the base string for the View Raw button
    var viewRawButton = '<a class="viewRaw no-select" component="post/view-raw" href="#"><i class="fa fa-code"/> View Raw</a>';
    
    // append the a.viewRaw and prew.raw classes to the page
    $('<style type="text/css">a.viewRaw { background-color: #337ab7; color: white; padding: 10px; margin-right: 3px; } pre.raw { white-space: pre-wrap; word-wrap: break-word; }</style>').appendTo('head');
    
    // Load the button into the DOM
    $(window).on('action:posts.loaded', function(){
        $('.post-tools:not(:has(a.viewRaw))').prepend(viewRawButton);
    });
    $('.post-tools:not(:has(a.viewRaw))').prepend(viewRawButton);
    $(document).on('click', '.viewRaw', showRaw);
    
    
    function showRaw() {
        // Get the acting raw button, the corresponding post, and the post id
        var viewButton = $(this);
        var post = viewButton.closest('li[component=post]');
        var pid = post.data('pid');
        
        // Get the div containing the baked post content
        var contentDiv = post.find('div[component="post/content"]');
        
        // Check if the baked content is visible
        if (contentDiv.css('display') == 'none')
        {
            // Show baked content
            contentDiv.css('display', 'block');
            
            // Remove the raw content, if it exists
            var rawDiv = post.find('div[component="post/raw"]');
            if (rawDiv.length) { rawDiv.remove(); }
        }
        else
        {
            // Hide the baked content
            contentDiv.css('display', 'none');
            
            // Retrieve and display the raw post
            socket.emit('posts.getRawPost', pid, function(err,rawContent) {
                // Notify on error
                if (err) {
                    if (console && console.error) {
                        console.error(err);
                    }
                    else {
                        alert(err);
                    }
                }
            
                // Build a div for the raw content
                rawContent = $('<pre class="raw"></pre>').text(rawContent);
                rawContent = $('<div class="content" component="post/raw" itemprop="text"></div>').html(rawContent);
                
                contentDiv.before(rawContent);
            });
        }
    }
    

  • FoxDev

    @abarker said:

    It also makes an attempt to escape some of the HTML in the raw

    It should really escape all of it, just to be sure ;)



  • @RaceProUK You know of a JS function to do that? I was trying to throw something quick together, so I pulled it out into an easily extendable function at the bottom.



  • @abarker I created an actual DOM node and set its innerText property, which automatically forced it to escape everything.

    https://what.thedailywtf.com/topic/19403/the-return-of-the-view-raw-button/54

    I also got it to automatically update the post if it's edited. Since NodeBB takes care of the cooked post, I just watch that for changes, and if it does I either update or throw away the raw depending on whether it's currently visible (if it's not visible, I throw it away, and clicking the "view raw" button will fetch it again).


  • FoxDev

    @abarker JS? No, but jQuery has a way.

    Instead of

    rawContent = '<div class="content" component="post/raw" itemprop="text"><pre class="raw">' + rawContent + '</pre></div>';
    

    try

    rawContent = $('<pre class="raw"></pre>').text(rawContent); //This escapes the HTML
    rawContent = $('<div class="content" component="post/raw" itemprop="text"></div>').html(rawContent);
    

    @anotherusername said in The Return of the view raw button:

    I created an actual DOM node and set its innerText property, which automatically forced it to escape everything.

    That works too



  • @RaceProUK Yeah, I guess instead of raw.innerText = rawContent, I could've used raw.appendChild(document.createTextNode(rawContent)).

    Either one should work...


  • FoxDev

    @abarker said:

    @RaceProUK You know of a JS function to do that?

    for simple display?

    markup.relpace(/</g, '&lt;'); should be sufficient

    if you're doing more than just shoving it into a display then....

    @RaceProUK said:

    No, but jQuery has a way.

    oh! yeah there is that.

    create a detached DOM node, set the inner text, grab the inner HTML



  • @RaceProUK said:

    Instead of

    rawContent = '<div class="content" component="post/raw" itemprop="text"><pre class="raw">' + rawContent + '</pre></div>';
    

    try

    rawContent = $('<pre class="raw"></pre>').text(rawContent); //This escapes the HTML
    rawContent = $('<div class="content" component="post/raw" itemprop="text"></div>').html(rawContent);
    

    :facepalm:

    fixed



  • @accalia said:

    create a detached DOM node, set the inner text, grab the inner HTMLinsert it into the page

    You don't want the HTML. You'd just use it to create another DOM node exactly like the one you're throwing away.



  • @RaceProUK @abarker said:

    rawContent = $('<pre class="raw"></pre>').text(rawContent); //This escapes the HTML
    rawContent = $('<div class="content" component="post/raw" itemprop="text"></div>').html(rawContent);
    

    ...how's that different from this:

    rawContent = $('<div class="content" component="post/raw" itemprop="text"></div>').text(rawContent);

  • FoxDev

    @anotherusername Mine has the <pre> tag @abarker used :p



  • @RaceProUK yeah, I missed that... you don't need a <pre> tag, though. Just style the <div>.

    0_1458938934181_Untitled.png

    0_1458939008061_Untitled.png

    0_1458939043245_Untitled.png

    ...and yes, it works on multi-line posts... that's what the white-space: pre-wrap is for...

    0_1458939107055_Untitled.png


  • BINNED

    Progress!

    0_1458996197535_pasted_image_at_2016_03_26_13_29.png

    I can't add it next to the reply button, yet, requires changes in the core, intending to take a look at that but for now this should do. Note that this was done completely in the plugin, no weird clientside hacks, and should require no extra roundtrips to the server (the raw is already in the client and can be accessed). First version should be available for testing somewhere later today.



  • @Onyx said:

    should require no extra roundtrips to the server (the raw is already in the client and can be accessed)

    I wondered if it might already be somewhere, but lazy me took the socket code that was already posted and made it work...


  • BINNED

    @anotherusername There wasn't, I added it in the plugin ;)

    Anyway, trying to figure out how to bind actions to buttons ATM. It's all a bit reverse engineer-y at this point since I can't find it in the docs...


  • FoxDev

    @Onyx said:

    I can't add it next to the reply button

    Personally, I can live with that :)


  • Java Dev

    @Onyx said:

    There wasn't, I added it in the plugin

    How much extra bandwidth usage is that when you never click the raw button?


  • BINNED

    @PleegWat said:

    How much extra bandwidth usage is that when you never click the raw button?

    I don't really have a large dataset to work with here but, if you think about it, it's just text, no image data, no HTML or anything so... neglectable? I'm also considering making it an opt-in in user settings (I guess that should be doable, right?) so...

    Also, I'm stuck at binding actions to that button, need to poke through plugin list, see if there's something to steal code from, because I refuse to use the idiotic hacks.



  • @RaceProUK said:

    @Onyx said:

    I can't add it next to the reply button

    Personally, I can live with that :)

    I'll second this. There's not a lot of real estate on mobile, so I'd prefer it to stay behind the hamburger.



  • @NedFodder maybe it could depend on screen size?

    There would probably need to be 2 buttons, and the @media rules would need to hide one or the other depending on the screen width.



  • @Onyx it looks like the value of the component attribute is what the event handler uses to tell which button was clicked. Maybe if you can find wherever that's registered for one of the other buttons, you can do the same thing?

    (I'd try, but I'm on mobile...)


  • BINNED

    @anotherusername I don't think it is, not automatically anyway, and I can't find how they register them. Seems to be clientside, but I'm not sure. Found a way though:

    $(document).ready(function() {
        $(window).on('action:post.tools.load', function(e) {
            $('a[component="posts/viewraw"]').off('click').on('click', function(e){
                // code
        }
    });
    

    .off() is paranoia, mostly. I got it mostly working now.

    But I'd like to ask 2 serious questions now:

    1. Should I switch to getting the raw on click rather than shipping it with posts? It seems that I have to handle streaming posts in either case anyway (the data structure I'm getting it from doesn't get updated), and I have to handle edits anyway so it's not that much more work either way.
    2. Should I pad/shorten the <pre> element height to avoid any weird adjustments? Basically:

    http://i.imgur.com/3UfkHOE.png

    (yes, there's a cat, deal) Becomes:

    0_1459016757893_upload-29335521-d04a-401b-8e1c-df1000ed8f1a

    Yeah, styling is weird, that's fixable.


  • FoxDev

    @Onyx said:

    Should I switch to getting the raw on click rather than shipping it with posts?

    can't hurt. unless cooties exhibit should be nice and fast to ask the server for the raw, and sidesteps the edits issue.


  • ♿ (Parody)

    @anotherusername said in The Return of the view raw button:

    maybe it could depend on screen size?

    So we should add a 🔥 to this topic title?



  • @Onyx said in The Return of the view raw button:

    I don't think it is, not automatically anyway, and I can't find how they register them.

    It appears to be here... of course, it's minified...

    function l(t) {
      var e = n.get('topic');
      e.on('click', '[component="post/quote"]', function () {
        f($(this), t)
      });
      e.on('click', '[component="post/reply"]', function () {
        u($(this), t)
      });
      $('.topic').on('click', '[component="topic/reply"]', function () {
        u($(this), t)
      });
      $('.topic').on('click', '[component="topic/reply-as-topic"]', function () {
        i.translate('[[topic:link_back, ' + ajaxify.data.titleRaw + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function (t) {
          $(window).trigger('action:composer.topic.new', {
            cid: ajaxify.data.cid,
            body: t
          })
        })
      });
      e.on('click', '[component="post/favourite"]', function () {
        p($(this), m($(this), 'data-pid'))
      });
      e.on('click', '[component="post/upvote"]', function () {
        return d($(this), '.upvoted', 'posts.upvote')
      });
      e.on('click', '[component="post/downvote"]', function () {
        return d($(this), '.downvoted', 'posts.downvote')
      });
      e.on('click', '[component="post/vote-count"]', function () {
        h(m($(this), 'data-pid'))
      });
      e.on('click', '[component="post/flag"]', function () {
        var t = m($(this), 'data-pid');
        require(['forum/topic/flag'], function (e) {
          e.showFlagModal(t)
        })
      });
      e.on('click', '[component="post/edit"]', function () {
        var t = $(this);
        $(window).trigger('action:composer.post.edit', {
          pid: m(t, 'data-pid')
        })
      });
      e.on('click', '[component="post/delete"]', function () {
        v($(this), t)
      });
      e.on('click', '[component="post/restore"]', function () {
        v($(this), t)
      });
      e.on('click', '[component="post/purge"]', function () {
        y($(this), t)
      });
      e.on('click', '[component="post/move"]', function () {
        w($(this))
      });
      e.on('click', '[component="post/chat"]', function () {
        C($(this))
      })
    }
    

    @Onyx said in The Return of the view raw button:

    Should I switch to getting the raw on click rather than shipping it with posts?

    Probably.

    @Onyx said in The Return of the view raw button:

    I have to handle edits anyway

    I set a mutation observer to watch the post and either throw away the cached raw if it's hidden, or update it if it's not hidden.

    @Onyx said in The Return of the view raw button:

    Should I pad/shorten the <pre> element height to avoid any weird adjustments?

    No... I don't think so. Although in some cases it might make sense to scroll the top of the post back into view.


Log in to reply