Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

Ajax voting and modern formatting on PerlMonks

by Your Mother (Archbishop)
on Jul 19, 2021 at 00:23 UTC ( #11135149=CUFP: print w/replies, xml ) Need Help??

(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

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: CUFP [id://11135149]
Front-paged by Discipulus
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others wandering the Monastery: (5)
As of 2022-01-24 20:50 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    In 2022, my preferred method to securely store passwords is:












    Results (65 votes). Check out past polls.

    Notices?