The Return of the view raw button
-
@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.
-
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}";
-
-
@accalia ... I know. It refused to let me use
socket
unless I called it fromunsafeWindow
, andeval
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.
-
@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.
-
-
@accalia Well for starters, since it was inside the GreaseMonkey script, I got
ReferenceError: socket is not defined
. Then I checked thatunsafeWindow.socket
existed and tried calling that instead, and gotError: Permission denied to access property "apply"
.So I deduced that I needed to have code inside
unsafeWindow
callsocket
, 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 ofunsafeWindow
. 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
.
-
@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....
-
Does the return of the raw button mean people are going to start hiding shit in posts again?
-
@loopback0 Given how is escaped, probably not
-
@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 on you for you missing it, they're just an arsehole so you can ignore it
-
-
@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"
orstyle="visibility:hidden;"
, I meanhidden
...)
-
@anotherusername said:
if we can get @ben_lubar to whitelist the hidden attribute
-
@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.
-
@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.
-
@loopback0 bah, you're just high on 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 theli
element, and it adds thehidden
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); }); } }
-
@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).
-
@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 usedraw.appendChild(document.createTextNode(rawContent))
.Either one should work...
-
@abarker said:
@RaceProUK You know of a JS function to do that?
for simple display?
markup.relpace(/</g, '<');
should be sufficientif 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);
fixed
-
@accalia said:
create a detached DOM node, set the inner text,
grab the inner HTMLinsert it into the pageYou 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);
-
@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>
....and yes, it works on multi-line posts... that's what the
white-space: pre-wrap
is for...
-
Progress!
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...
-
@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...
-
-
@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?
-
@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...)
-
@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:
- 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.
- 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:
Yeah, styling is weird, that's fixable.
-
@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.
-
@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.