(I reserve the right to make *sensible* silent
updates or corrections in code and formatting for a day or two; until
I remove this line. No one wants a post that has “update: …” repeated
20 times and if I have to proof this for an hour right now, I won’t post it.) :P
This code will give the site a more modern layout and appearance.
You can thank tobyink for that part.
Fair warning
This code was just written for me. It’s not bombproof, it has no
automatic tests, and including external files is a security risk if
the source/host decides to exploit it or is itself hacked. I only
address problems as they arise so there may be edge cases galore that
I don’t know. I did not clean-up or check anything and I wrote 95% of
it like 2 years ago; generally speaking, I cannot remember code I
wrote last week.
I may not explain much of any of it,
As it stands, you will have to have a webserver. I am using
plackup with https support. If you have your own
host or something, you can definitely use that. Without supporting
https this stuff won’t work unless you unblock the sources with your
browser (a PITA and not a good idea).
This code does a skosh of dynamic handling of the Chatterbox.
Nodelet settings
In your Free Nodelet Settings you will need to add several lines.
<script src="//code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=
+"
crossorigin="anonymous"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.mi
+n.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.2/highlig
+ht.min.js"></script>
<script src="//buzzword.org.uk/2014/pm/pm2.js"></script>
<script src="//localhost:8282/pm/he.js"></script>
<script src="//localhost:8282/pm/pm.2.0.js"></script>
<script src="//localhost:8282/pm/pm.2.0.cb.js"></script>
<link rel="stylesheet" href="//localhost:8282/pm/pm.2.0.css" type="tex
+t/css" />
The jQuery and Bootstrap are from, I think, reliable CDNs. he.js is a library for
handling characters and HTML entities; hence “he.” The main formatting
script, pm2.js, is from tobyink’s buzzword.org.uk. The next JS and
CSS are mine and you’ll have to serve them yourself from somewhere.
Toby’s code needs Bootstrap. My code needs jQuery.
he.js
If you don’t have it installed already via node or something, just
take the he.js file to serve: https://github.com/mathiasbynens/he
pm.2.0.js
"use strict";
if ( typeof $().emulateTransitionEnd !== 'function')
{
throw("Bootstrap doesn’t seem to be available…");
}
function colorReps() {
$("div.reputation").each(function(){
$(this).text().match(/\s\d/) ?
$(this).css({color:"teal", opacity:0.66, fontSize:"85%"})
+:
$(this).css({color:"#DF3969", opcaity:0.66 , fontSize:"85%
+"});
});
}
function submitVote () {
// Need to unbind and/or include a spinner.
console.log("submitVote…");
let $clicked = $(this);
$clicked.off(); // Right?
$clicked.closest("div").animate({opacity:0},999)
.children().prop("disabled",true);
let $vote = $clicked.parent("label").find("button");
let $form = $vote.closest("form");
let voteName = $vote.attr("name");
let voteNodeId = voteName.replace(/vote__/,"");
let nodeId = $form.find("input[name='node_id']").val();
let formData = {
node_id: nodeId
,vc: $form.find("input[name='vc']").val()
,op: $form.find("input[name='op']").val()
,sexisgreat: $form.find("input[name='sexisgreat']").val()
};
formData[voteName] = $vote.val();
$.ajax({ method: $form.attr("method")
,url: $form.attr("action")
,data: formData
})
.fail(function(jqXHR, textStatus, errorThrown){
console.log(jqXHR);
if ( textStatus === "abort" ) return;
$clicked.closest("div").stop().animate({ opacity: 1 })
+;
$vote.closest("div.reputation").text([textStatus, erro
+rThrown || "unknown"].join(": "));
})
.done(function( html, textStatus, jqXHR ) {
console.log("here in top of done with node " + nodeId + "
+and vote " + voteNodeId);
console.log("div.reputation with node " + nodeId + " and v
+ote " + voteNodeId);
let $html = $($.parseHTML(html));
let $rep = $html.find("#rep-" + voteNodeId);
console.log("received html find " + $rep.html());
$rep.css({opacity:0});
$clicked.closest("div").replaceWith($rep);
$rep.animate({ opacity: 1 }, 666);
let $xp = $("h1:contains(XP Nodelet)")
.filter(function(){ return $(this).children().length =
+== 0 })
.parent()
.find("div");
// Can't use the same selector because the DOM isn't the s
+ame pre-transform.
let $newXp = $html.find("#XP_Nodelet td");
// Account for out of votes for day.
if ( $newXp.length )
{
$xp.fadeOut(100, function(){ $(this).html( $newXp.html
+() ).fadeIn(100) });
}
else
{
$xp.closest("section").fadeOut(100, function(){ $(this
+).remove() });
// Remove all vote buttons too. Untested, might work.
$("div.reputation").find("button").closest("div.reputa
+tion")
.fadeOut(100, function(){ $(this).remove() });
}
colorReps();
});
}
jQuery(function($) {
$("body").hide().fadeIn(100);
colorReps();
$("body").css({ color:"#00000a" }); // not being carried over anyw
+here...
let $stripe = $('<div style="font-size:1.5rem; text-align:left; po
+sition:fixed; bottom:16rem; left:-11rem; transform:rotate(-90deg); li
+ne-height:1rem; color:teal">Your Mother’s pm.2.0.js is loading…</div>
+');
$("body").append($stripe);
// Block bottom page/viewed controls.
$("#nodethreads-foot form").submit(function(){ return false });
// Block voting <input type="hidden" name="op" value="vote">
$("input[name='op'][value='vote']").closest("form").submit(functio
+n(){ return false });
$("input[name='node']").css({width:"100%"});
let $postBody = $("textarea[name='note_doctext']");
$postBody.css({width:"100%", fontFamily:"monospace"});
if ( $postBody.length )
{
let $stash = $('<div style="display:hidden"/>');
$postBody.parent().append($stash);
$stash.data( "post", $postBody.val().replace(/\s+/g,"") );
window.onbeforeunload = function() {
if ( $("submit['op']").val() == "op" ) return null;
if ( $("submit['op']").val() == "create" ) return null;
return $stash.data("post") == $postBody.val().replace(/\s+/g,"
+") ?
null : "Unsaved…";
};
}
let $voteForm = $("form").find('input[name="op"][value="vote"]').p
+arent("form");
$voteForm.find("input[name='sexisgreat']").remove(); // Ajax -> no
+ vote button.
// DOM elements.
let voteMap = { "1": '<button title="++" style="border:0; backgrou
+nd-color:inherit; font-size:1.75rem; padding:2px 0; color:teal; "><sp
+an class="glyphicon glyphicon-thumbs-up"></span></button>',
"-1": '<button title="--" style="border:0; border-r
+adius:100%; background-color:inherit; font-size:1.75rem; padding:2px
+0; color:#DF3969"><span class="glyphicon glyphicon-thumbs-down"></spa
+n></button>' };
$voteForm.find('input[type="radio"]').each(function (i, v) {
let $label = $(v).parent("label");
let $button = $(voteMap[$(v).val()]);
$button.attr("name",$(v).attr("name"));
$button.attr("value",$(v).val());
$label.html($button);
$button.on("click", submitVote);
});
let $viewedForm = $("input[name='pageloadtime']").closest("form");
$viewedForm.submit(function(evt){ evt.preventDefault() });
$viewedForm.find("input[type='submit']").on("click", function(evt)
+{
console.log(evt);
console.log($(evt.target).html());
console.log(evt.target.nodeName);
let $submit = $(evt.target);
let formData = $viewedForm.serializeArray();
formData.push({ name:$submit.attr("name"), value:$submit.val()
+ });
$("body").children().fadeOut(100);
console.log( $viewedForm.attr("method") + " " + $viewedForm.se
+rialize() );
console.log(formData);
$.ajax({
method: $viewedForm.attr("method")
,url: $viewedForm.attr("action")
,data: formData
})
.fail(function(jqXHR, textStatus, errorThrown){
if ( textStatus === "abort" ) return;
let err = [textStatus, errorThrown || "unknown"].join(": "
+);
let $err = $('<div style="position:absolute; top:20%; left
+:50%; margin-left:-25%;"></div>');
$err.text(err).fadeIn().delay(2000).fadeOut(); // Should r
+emove, wrote this once already…
$("body").children().fadeIn(100);
})
.done(function( html, textStatus, jqXHR ) {
let newDoc = document.open("text/html", "replace");
// html = html.replace(/<body /, '<body style="animatio
+n: fadein 2s" ');
html = html.replace(/<body /, '<body style="display:non
+e" ');
console.log(html);
newDoc.write(html);
newDoc.close();
});
return false;
});
$stripe.fadeOut(100, function(){ $(this).text("Your Mother is load
+ed with tobyink’s help").fadeIn() });
});
/*
Features
* Warning/confirm if leaving page with unsubmitted changes. Too aggr
+essive right now.
* Ajax voting on nodes.
* Ajax "Check Time" interception and reload to keep page GET.
To do
* Abort ajax on page leave; maybe https://stackoverflow.com/a/324482
+28/109483
*/
pm.2.0.cb.js
Code to try to make the Chatterbox more dynamic. It has bugs,
including an initial double display, that haven’t bugged me enough to
fix yet. If you use/edit it, please be careful not to put a
bunch of load on the server with bad timers, etc.
// What XML generators are currently available on PerlMonks? https://p
+erlmonks.org/?node_id=72241
// PerlMonks Related Scripts https://perlmonks.org/?node_id=26649
// REQUIRES he.js
"use strict";
// throw("Not ready to be incurring load from regular visits");
jQuery(function($) {
let $currentMsg = $(".cb_msg");
setTimeout(function(){ $currentMsg.fadeOut(function(){ $(this).remov
+e() }) }, 500000);
// <span class="cb_author"><a href="?node_id=248054" title="Posted :
+59:27 (1:19 before next)">Your Mother</a></span>
$("table.cb_table").css({margin:"1ex 0", fontSize:"1.4rem"}); // Dup
+.
let $CBform = $("input[type='submit'][name='message_send']").closest
+("form");
$CBform.find("input[name='message']").css({width:"100%",border:"1px
+solid gray",borderRadius:"2px"});
$CBform.find("input[name='message_send']").hide();
$CBform.parent().find("hr, input[type='checkbox']").remove();
$CBform.find("label, span.msg, span.cb_more_messages").delay(15000).
+slideUp(1000, function(){ $CBform.parent().find("br").remove() });
//span.cb_more_messages
$CBform.parent().css({padding:0});
// Needs some click animation-
$("input[type='submit']").css({border:"1px solid gray",borderRadius:
+"2px",margin:"2px 3px"});
$("i.cb_quiet").wrap('<div style="margin:1em 0 1ex 0"></div>');
let intID;
let chatURI = document.location.origin + "/bare/";
// .cb_table
// Timer, interval, like gmail; check frequently if there are a lot
+of current messages.
// Less and less frequently when there is none.
// getCB
// displayCB
function getCB ( arg ) {
if ( ! arg ) arg = { timeout: 5000 };
if ( arg.timeout < 500000 ) arg.timeout += 10000; // maybe 10% of
+current instead?
let queryData = { "node_id":207304 // hardcode node…
,"xmlstyle":"modern" } // Get the parsed->rendere
+d messages.
$.ajax({
method: "GET"
,url: chatURI
,data: queryData
,dataType: "xml"
})
.fail(function(jqXHR, textStatus, errorThrown){
console.log(jqXHR);
console.log([textStatus, errorThrown || "unknown"].join(":
+ "));
console.log('setTimeout( getCB, ' + arg.timeout + ', arg )
+;');
intID = setTimeout( getCB, arg.timeout, arg );
})
.done(function( xml, textStatus, jqXHR ) {
console.log("Received…", xml);
if ( ! xml ) {
console.log('setTimeout( getCB, ' + arg.timeout + ', a
+rg );');
intID = setTimeout( getCB, arg.timeout, arg );
return false;
}
// Getting null sometimes.
let messages = xml.getElementsByTagName("message");
if ( ! messages.length ) {
console.log('setTimeout( getCB, ' + arg.timeout + ', a
+rg );');
intID = setTimeout( getCB, arg.timeout, arg );
return false;
}
let $CBtable = $("table.cb_table");
if ( ! $CBtable.length ) { // No messages in the page curr
+ently…
$CBtable = $('<table class="cb_table" border="0" width
+="100%" cellspacing="0" cellpadding="2"></table>');
// <i class="cb_quiet">and all is quiet...</i>
// Always there by class? <i class="cb_quiet">and all
+is quiet...</i>
$("i.cb_quiet").replaceWith($CBtable);
}
$CBtable.css({margin:"1ex 0", fontSize:"1.4rem"}); // Dup…
for (var i = 0; i < messages.length; i++ )
{
let message = {
author: messages[i].getElementsByTagName("author")
+[0].textContent
,user: messages[i].getElementsByTagName("author_use
+r")[0].textContent
,epoch: messages[i].getElementsByTagName("createdep
+och")[0].textContent
,id: messages[i].getElementsByTagName("message_id")
+[0].textContent
,text: messages[i].getElementsByTagName("parsed")[0
+].textContent
};
// Skip duplicates!
let renderedMessage = `
<tr class="cb_msg msg_id_${message.id}">
<td><!-- class -> odd-row/even-row, but CSS is better -->
<span class="chat">
<span class="chatfrom_${message.user}">
<span class='cb_sq_br'>[</span><span class='cb_author'><a hr
+ef="?node_id=${message.user}">${message.author}</a></span><span class
+='cb_sq_br'>]</span><span class='cb_sep'>:</span>
<span class='content'>${message.text}</span>
</span>
</span>
</td>
</tr>`;
setTimeout(function(){ $(".msg_id_" + message.id).fade
+Out() }, 500000);
console.log(renderedMessage);
$CBtable.append(renderedMessage); // I think the direc
+tion is settable…
}
intID = setTimeout( getCB, 5000 );
});
}
getCB();
// console.log( "node_id from CB form -> " + $CBform.find("input[nam
+e='node_id']").val() );
$CBform.submit(function(evt){
evt.preventDefault();
sendMessage();
});
function sendMessage () {
let formData = {
node_id: $CBform.find("input[name='node_id']").val()
,op: $CBform.find("input[name='op']").val() // "message"
,message: he.encode( $CBform.find("input[name='message']").v
+al() )
,message_send: $CBform.find("input[name='message_send']").va
+l() // "talk"
};
console.log(formData);
$.ajax({
method: $CBform.attr("method")
,url: $CBform.attr("action")
,data: formData
})
.fail(function(jqXHR, textStatus, errorThrown){
console.log(jqXHR);
console.log(".fail -> " + [textStatus, errorThrown ||
+"unknown"].join(": "));
})
.done(function( html, textStatus, jqXHR ) {
$CBform.find("input[name='message']").val("");
clearTimeout(intID);
getCB({ timeout: 10000 });
});
}
$(document).on("visibilitychange", function(){
if ( document.hidden )
{
console.log("Tab is hidden, ceasing ajax CB");
clearTimeout(intID);
}
else
{
getCB({ timeout: 60000 });
console.log("Tab is visible, restarting ajax CB");
}
});
});
pm.2.0.css
The only purpose here is to do a sort of anti-spinner. When you
click I've checked all of these on the Recently Active
Threads page, it blanks the page so you know something happened
and shows it when the ajax has loaded it in the background. I probably
should have just done it in JS but I thought I’d be doing more CSS so started a file…
body {
opacity: 0 !important;
transition: opacity 0.5s;
}
If you don’t have a webserver already, this is how I run mine on
:8282; you’ll need Plack::App::Directory. Don’t ask me how
to do your own certs. I do it less than annually and it’s always a
RTFM situation.
plackup -p 8282 -MPlack::App::Directory -e \
'Plack::App::Directory->new({root => q{/path/to/the/files}})->to_app
+' \
--enable-ssl \
--ssl-key-file /path/to/certs/localhost.key \
--ssl-cert-file /path/to/certs/localhost.cert