Advertisement
Kaanbaltlak

AO3 Sticky Comment Box - español

Jun 13th, 2024
450
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         AO3: Sticky Comment Box
  3. // @namespace    https://greasyfork.org/en/users/906106-escctrl
  4. // @version      2.3
  5. // @description  gives you a comment box that stays in view as you scroll and read the story
  6. // @author       escctrl
  7. // @license      MIT
  8. // @match        *://archiveofourown.org/works/*
  9. // @match        *://archiveofourown.org/collections/*/works/*
  10. // @exclude      *://archiveofourown.org/works/*/new
  11. // @exclude      *://archiveofourown.org/works/*/edit*
  12. // @exclude      *://archiveofourown.org/works/new*
  13. // @exclude      *://archiveofourown.org/works/search*
  14. // @require      https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js
  15. // @require      https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js
  16. // @require      https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js
  17. // @grant        none
  18. // @downloadURL https://update.greasyfork.org/scripts/489335/AO3%3A%20Sticky%20Comment%20Box.user.js
  19. // @updateURL https://update.greasyfork.org/scripts/489335/AO3%3A%20Sticky%20Comment%20Box.meta.js
  20. // ==/UserScript==
  21.  
  22. (function($) {
  23.     'use strict';
  24.  
  25.     // despite the @excludes, there are always ways that editing a work ends up with AO3's URL being just /works/xxxxx >:(
  26.     // so we can't rely on URLs, we gotta check for ourselves and stop if there's no fic to display
  27.     if ($('#main #chapters').length == 0) return;
  28.  
  29.     // select the work ID from the URL - we save cache with this, so it won't matter what the rest of the URL is (collections, chapters)
  30.     const workID = new URL(window.location.href).pathname.match(/\/works\/(\d+)/i)[1];
  31.  
  32.     // let's figure out if there are multiple chapters that could be commented on
  33.     const chapterIDs = $('#main ul.work.navigation ul#chapter_index').length > 0 ? $('#main ul.work.navigation ul#chapter_index select#selected_id option').toArray() // when in chapter-by-chapter view, there's a Chapter Index button
  34.                    : $('#main ul.work.navigation li.chapter.bychapter').length > 0 ? $('.chapter.preface h3.title a').toArray() // when in entire-work view, there's a Chapter By Chapter button
  35.                    : []; // and if neither exists, it's a work without chapters
  36.  
  37.     // if we're in entire-work view, we wanna give a hint to the user which chapter they're currently seeing
  38.     if ($('#main ul.work.navigation li.chapter.bychapter').length > 0) {
  39.         $(document).on('scrollend', () => { whatsInView(); }); // listen to scrolling for updates
  40.     }
  41.  
  42.     // gets called by scrolling events, and when dialog is first created
  43.     function whatsInView() {
  44.         // here we want to figure out which chapter is currently in view
  45.         $(chapterIDs).each((i) => {
  46.             let rect = $('#chapter-'+(i+1)).get(0).getBoundingClientRect();
  47.             if ((rect.top >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight)) || // top edge is visible
  48.                 (rect.bottom >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)) || // bottom edge is visible
  49.                 (rect.top < 0 && rect.bottom > (window.innerHeight || document.documentElement.clientHeight))) { // top is above and bottom is below viewport (we're seeing the middle of it)
  50.  
  51.                 // based on what's in view, we can update the selection
  52.                 $('#float_cmt_chap select option').eq(i+1).text(`Chapter ${(i+1)} (viewing)`);
  53.             }
  54.             // the others get reset
  55.             else $('#float_cmt_chap select option').eq(i+1).text(`Chapter ${(i+1)}`);
  56.         });
  57.     }
  58.  
  59.     // sticky button to open the comment box
  60.     let cmtButton = `<div id="float_cmt_toggle"><button>Caja de comentarios</button></div>`;
  61.     $('body').append(cmtButton);
  62.  
  63.     // listening to button click: open or close the dialog
  64.     $('#float_cmt_toggle').on('click', (e) => {
  65.         toggleCommentBox();
  66.     });
  67.  
  68.     // this is called by the button and also the keyboard shortcut
  69.     function toggleCommentBox() {
  70.         if ($(dlg+":hidden").length > 0) openCommentBox();
  71.         else if ($(dlg+":visible").length > 0) closeCommentBox();
  72.     }
  73.  
  74.     var dlg = "#float_cmt_dlg";
  75.  
  76.     let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "ui-darkness" : "base"; // if the background is dark, use the dark UI theme to match
  77.     let fontsize = $("#main #chapters .userstuff").css('font-size'); // enforce the reading font size for the dialog
  78.     $("head").append(`<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/${dialogtheme}/jquery-ui.css">`)
  79.     .append(`<style tyle="text/css">.ui-dialog ${dlg}, .ui-dialog .ui-dialog-titlebar, .ui-dialog .ui-dialog-buttonpane button { font-size: ${fontsize}; }
  80.     .ui-dialog .ui-dialog-buttonpane button { min-width: 2em; min-height: 2em; padding: 0 0.5em; }
  81.     .ui-dialog .ui-dialog-buttonpane { padding: 0; margin: 0; }
  82.     ${dlg} select { width: unset; min-width: unset; position: relative; bottom: 0.2em; }
  83.     ${dlg} input { width: 10em; min-width: unset; }
  84.     #float_cmt_counter, #float_cmt_settings_hint, #float_cmt_guest_hint { font-size: 80%; padding: 0.2em; margin: 0.2em 0; }
  85.     #float_cmt_toggle { position: fixed; bottom: 0.5em; right: 0.5em; z-index: 3; }
  86.     #float_cmt_toggle button { height: unset; font-size: ${fontsize}; }</style>`);
  87.  
  88.     // prepping the dialog (without opening it)
  89.     createCommentBox();
  90.  
  91.     // prepares the dialog and loads the cache into it
  92.     function createCommentBox() {
  93.         // designing the floating box
  94.         $("body").append(`<div id="float_cmt_dlg"></div>`);
  95.  
  96.         // optimizing the GUI in case it's a mobile device
  97.         let screen = parseInt($("body").css("width")); // parseInt ignores letters (px)
  98.         let buttonText = screen <= 500 ? false : true;
  99.         let dialogwidth = screen <= 500 ? screen * 0.9 : 500;
  100.         let resize = screen <= 500 ? false : true;
  101.  
  102.         $(dlg).dialog({
  103.             modal: false,
  104.             autoOpen: false,
  105.             resizable: resize,
  106.             draggable: true,
  107.             width: dialogwidth,
  108.             position: { my: "right bottom", at: "right bottom", of: "window" },
  109.             title: "Comentario",
  110.             buttons: [
  111.                 { text: "Config", icon: "ui-icon-gear", showLabel: buttonText, click: () => { toggleSettings(); } },
  112.                 { text: "Citar", icon: "ui-icon-caret-2-e-w", showLabel: buttonText, click: () => { grabHighlight(); } },
  113.                 { text: "Descartar", icon: "ui-icon-trash", showLabel: buttonText, click: () => { discardComment(); } },
  114.                 { text: "Publicar", icon: "ui-icon-comment", showLabel: buttonText, click: () => { submitComment(); } },
  115.                 { text: "Cerrar", icon: "ui-icon-close", showLabel: buttonText, click: () => { closeCommentBox(); } },
  116.             ],
  117.             // positioning stuff below is so that it SCROLLS WITH THE PAGE JFC https://stackoverflow.com/a/9242751/22187458
  118.             create: function(event, ui) {
  119.                 $(event.target).parent().css('position', 'fixed');
  120.                 // and also to put the dialog where it was last left across pageloads
  121.                 let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt')));
  122.                 if (cachemap.get('pos')) {
  123.                     let pos = JSON.parse(cachemap.get('pos'));
  124.                     pos.of = $(window);
  125.                     $(dlg).dialog('option','position', pos);
  126.                 }
  127.                 // issue: if you drag it around so far that the screen begins to scroll, the dialog disappears. need to refresh the page to get it back
  128.                 // workaround: force the dialog to stay within the visible screen - no dragging outside of viewport means it can't disappear
  129.                 $(dlg).dialog("widget").draggable("option","containment","window");
  130.             },
  131.             resizeStop: function(event, ui) {
  132.                 let position = [(Math.floor(ui.position.left) - $(window).scrollLeft()),
  133.                                  (Math.floor(ui.position.top) - $(window).scrollTop())];
  134.                 $(event.target).parent().css('position', 'fixed');
  135.                 $(dlg).dialog('option','position',position);
  136.             },
  137.             beforeClose: function() {
  138.                 // store the position of the dialog so we can reopen it there after page refresh
  139.                 let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt')));
  140.  
  141.                 let pos = $(dlg).dialog( "option", "position" );
  142.                 pos = { my: pos.my, at: pos.at }; // need to keep only the pieces we need - it's a cyclic object!
  143.                 cachemap.set('pos', JSON.stringify(pos));
  144.  
  145.                 // store the current settings and guest data (if exists) along with it
  146.                 cachemap.set('quotes', $('#float_cmt_quote').val());
  147.                 cachemap.set('kbd', $('#float_cmt_kbd').val());
  148.                 if ($('#float_cmt_name').length > 0) cachemap.set('name', $('#float_cmt_name').val());
  149.                 if ($('#float_cmt_email').length > 0) cachemap.set('email', $('#float_cmt_email').val());
  150.  
  151.                 bindShortcut($('#float_cmt_kbd').val()); // update the keyboard shortcut binding so it takes effect
  152.  
  153.                 localStorage.setItem('floatcmt', JSON.stringify( Array.from(cachemap.entries()) ));
  154.             },
  155.             close: function() {
  156.                 // remove formatting buttonbar event listener while dialog is closed so we don't stack listeners each time the dialog is reopened
  157.                 if ($(dlg).find('.comment-format').length > 0) $(dlg).find('.comment-format a').off("click.cmtfmt");
  158.             }
  159.         });
  160.  
  161.         // load cache: [0] = text, [1] = quotes, [2] = kbd, [3] = name, [4] = email
  162.         let cache = loadCache();
  163.  
  164.         $(dlg).html(`<div id="float_cmt_title" style="margin: 0 0 0.2em 0;">Comentar como <span id="float_cmt_as"></span> en <span id="float_cmt_chap"></span></div>
  165.                      <div id="float_cmt_userinput"><textarea style="min-height: 8em">${cache[0]}</textarea>
  166.                      <div id="float_cmt_counter"><span>10000</span> characters left</div>
  167.                      <div id="float_cmt_settings" style="display: none; margin: 0.5em 0 0 0;">
  168.                      Citas en <select id="float_cmt_quote"><option value="i" ${cache[1] == "i" ? "selected" : ""}>Itálicas</option>
  169.                      <option value="q" ${cache[1] == "q" ? "selected" : ""}>Bloque de cita</option></select>
  170.                          ${screen > 500 ? `Atajo de teclado: <input id="float_cmt_kbd" type="text" value="${cache[2]}">
  171.                          <div id="float_cmt_settings_hint" style="display: none;" class="ui-state-highlight ui-corner-all">
  172.                          Utiliza cualquier combinación de Ctrl/Alt/Mayús y una letra o número</div>` : `<input id="float_cmt_kbd" value="${cache[2]}" type="hidden">`}
  173.                      </div></div>`);
  174.  
  175.         // if we're logged in, add the pseud selection to the dialog so we know which one to submit with
  176.         if ($("#add_comment_placeholder [name='comment[pseud_id]']").length == 1) {
  177.             // clone the pseuds - either a hidden <input>, or a <select> - for our purposes
  178.             let pseud_id = $("#add_comment_placeholder [name='comment[pseud_id]']").eq(0).clone().attr('id', 'float_cmt_pseud_select');
  179.             $('#float_cmt_as').append(pseud_id); // add it to the dialog into the placeholder
  180.             // if there are no pseuds to select, display the username
  181.             if ($(pseud_id).prop('tagName') == "INPUT") $('#float_cmt_as').append($("#add_comment_placeholder span.byline").text());
  182.         }
  183.         // Guests need to enter a display name and email address (retrieved from cache)
  184.         else {
  185.             $('#float_cmt_as').append("&#9662;Guest").css('cursor', 'n-resize'); // show a generic "Guest" just so it's not blank
  186.             $('#float_cmt_title').after(`<div id="float_cmt_guest">
  187.             <span style="width: 3.5em; display: inline-block;">Nombre: </span><input type="text" id="float_cmt_name" value="${cache[3]}"><br />
  188.             <span style="width: 3.5em; display: inline-block;">Email: </span><input type="text" id="float_cmt_email" value="${cache[4]}"><small> (no se hará publico)</small></div>
  189.             <div id="float_cmt_guest_hint" class="ui-state-error ui-corner-all" style="display: none;">¡Ambas cosas son obligatorias!</div>`);
  190.             // show/hide the guest name & email to let user save space by clicking on "Guest" label
  191.             $('#float_cmt_as').on('click', () => {
  192.                 $('#float_cmt_guest').toggle("slow", function() {
  193.                     if ($(this).is(":visible")) $('#float_cmt_as').html("&#9662;Guest");
  194.                     else $('#float_cmt_as').html("&#9656;Guest");
  195.                 });
  196.             });
  197.         }
  198.  
  199.         // building a chapter selection list - we only use this to pick where to send the comment to
  200.         let select_chapter = `<option value="/works/${workID}/comments" selected="selected">Obra completa</option>`;
  201.         // build chapter options if there are any: <option value="/chapters/chapterID/comments">Chapter #</option>
  202.         if ($(chapterIDs[0]).prop('tagName') == "A") {
  203.             $(chapterIDs).each(function(i) {
  204.                 select_chapter += `<option value="${$(this).attr('href').match(/\/chapters\/\d+/i)[0]}/comments">${$(this).text()}</option>`;
  205.             });
  206.         }
  207.         else if ($(chapterIDs[0]).prop('tagName') == "OPTION") {
  208.             select_chapter = `<option value="/works/${workID}/comments">Obra completa</option>`; // reset to not-selected option
  209.             $(chapterIDs).each(function(i) {
  210.                 select_chapter += `<option value="/chapters/${$(this).val()}/comments" ${$(this).prop('selected') ? "selected='selected'" : ""}>
  211.                                    Capítulo ${i+1}${$(this).prop('selected') ? " (viewing)" : ""}</option>`;
  212.             });
  213.         }
  214.         select_chapter = "<select>" + select_chapter + "</select>";
  215.         $('#float_cmt_chap').append(select_chapter);
  216.  
  217.         // check what's visible on page load (might be a refresh halfway down the page)
  218.         if ($('#main ul.work.navigation li.chapter.bychapter').length > 0) whatsInView();
  219.  
  220.         // if there are no pseuds to select, and no chapters to select, save space and hide the whole part
  221.         if (($('#float_cmt_pseud_select').prop('tagName') == "INPUT") && $('#float_cmt_chap select option').length == 1) $('#float_cmt_title').hide();
  222.  
  223.         // listen to user typing so we can count characters and such
  224.         $('#float_cmt_userinput textarea').on('input', function(e) {
  225.             whenTextChanges(e.target);
  226.         });
  227.  
  228.         // set the current keyboard shortcut binding
  229.         bindShortcut(cache[2]);
  230.  
  231.         // in the settings field, let user set keyboard shortcut by pressing it
  232.         $('#float_cmt_kbd').on('keydown', function(e) {
  233.             e.preventDefault(); e.stopPropagation(); // this stops the browser from entering in the textfield or reacting for its own shortcuts
  234.  
  235.             // allow Backspace and Del key to reset to "" so shortcuts can be disabled
  236.             if (e.key == "Backspace" || e.key == "Delete") {
  237.                 $('#float_cmt_settings_hint').hide();
  238.                 $('#float_cmt_kbd').val("");
  239.             }
  240.             // is this something we consider a valid option?
  241.             if (e.key.length > 1 || e.key == " ") return; // only letters/numbers have a e.key string length of 1
  242.             if (!e.ctrlKey && !e.altKey) { // don't even try if it isn't a combo using Ctrl or Alt
  243.                 $('#float_cmt_settings_hint').show();
  244.                 return;
  245.             }
  246.  
  247.             // if it's good, build the text to show user what they selected
  248.             $('#float_cmt_settings_hint').hide();
  249.             let kbd = `${e.ctrlKey ? "Ctrl + " : ""}${e.altKey ? "Alt + " : ""}${e.shiftKey ? "Shift + " : ""}${e.key.toLowerCase()}`;
  250.             $('#float_cmt_kbd').val(kbd);
  251.         });
  252.     }
  253.  
  254.     // bind the keyboard shortcut for toggling the dialog
  255.     function bindShortcut(kbd) {
  256.         $(window).off('keydown.floatcmt'); // start fresh or we're binding multiple listeners
  257.         if (kbd == "") return; // if the shortcut was disabled, don't add any listeners
  258.         kbd = kbd.split(" + "); // setting text split into chunks for easier comparison
  259.  
  260.         // listen to keypress if our shortcut was called (we're using the .floatcmt namespace for controlled on/off())
  261.         $(window).on('keydown.floatcmt', function(e) {
  262.             if (e.key.length > 1) return; // only letters/numbers have a e.key string length of 1
  263.             if (!e.ctrlKey && !e.altKey) return; // don't even try if it isn't a combo using Ctrl or Alt
  264.             //console.log(`${e.ctrlKey ? "Ctrl + " : ""}${e.altKey ? "Alt + " : ""}${e.shiftKey ? "Shift + " : ""}${e.key.toLowerCase()}`);
  265.  
  266.             // was this our shortcut?
  267.             if (e.ctrlKey === kbd.includes("Ctrl") && e.altKey === kbd.includes("Alt") &&
  268.                 e.shiftKey === kbd.includes("Shift") && kbd.includes(e.key.toLowerCase())) {
  269.                 e.preventDefault(); e.stopPropagation(); // this stops the browser from reacting to its valid keyboard shortcuts (menu)
  270.                 toggleCommentBox();
  271.             }
  272.         });
  273.     }
  274.  
  275.     // counter and cache: triggered by event and other functions when text in the commentbox changes
  276.     function whenTextChanges(el) {
  277.         // calculate remaining characters
  278.         let cmt = $(el).val();
  279.         let rem = 10000 - (cmt.length + cmt.split("\n").length-1); // count like AO3 does: linebreak = 2 chars
  280.         $('#float_cmt_counter span').text(rem);
  281.  
  282.         // warning if we've exceeded allowed characters
  283.         if (rem<0) $('#float_cmt_counter').addClass('ui-state-error ui-corner-all');
  284.         else $('#float_cmt_counter').removeClass('ui-state-error ui-corner-all');
  285.  
  286.         storeCache();
  287.     }
  288.  
  289.     // shows the dialog
  290.     function openCommentBox() {
  291.         $(dlg).dialog('open');
  292.  
  293.         // check if dialog opened off viewport (browser window now smaller) https://stackoverflow.com/a/7557433/22187458
  294.         let rect = $(dlg).get(0).getBoundingClientRect();
  295.         if (!(rect.top >= 0 && rect.left >= 0 &&
  296.             rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  297.             rect.right <= (window.innerWidth || document.documentElement.clientWidth) )) {
  298.             // then we reset to the default bottom right
  299.             $(dlg).dialog('option','position', { my: "right bottom", at: "right bottom", of: window });
  300.         }
  301.  
  302.         // setting the cursor at the end of the available text
  303.         let area = $('#float_cmt_userinput textarea').get(0);
  304.         area.focus();
  305.         area.setSelectionRange(area.value.length, area.value.length);
  306.  
  307.         // update the char counter with any cached text
  308.         whenTextChanges(area);
  309.  
  310.         // Formatting Buttonbar support! trigger the value-has-changed event manually when a button is clicked, so the char counter updates
  311.         // adding this while opening dialog, when it's almost certain the comment formatting script has finished loading
  312.         if ($(dlg).find('.comment-format').length > 0) $(dlg).find('.comment-format a').on("click.cmtfmt", function() { $(area).trigger('input'); });
  313.     }
  314.  
  315.     // hides the dialog (more stuff is handled in the beforeClose and close dialog events)
  316.     function closeCommentBox() {
  317.         $(dlg).dialog('close');
  318.     }
  319.  
  320.     // display or hide a few setting options within the dialog (below the textarea)
  321.     function toggleSettings() {
  322.         $('#float_cmt_settings').toggle();
  323.     }
  324.  
  325.     // takes highlighted text and appends it to the comment
  326.     function grabHighlight() {
  327.         // copy highlighted text works only on summary, notes, and fic
  328.         if ($(window.getSelection().anchorNode).parents(".userstuff").length > 0) {
  329.             let area = $('#float_cmt_userinput textarea');
  330.             let highlighted = $('#float_cmt_quote').val() == "i" ?
  331.                 `<i>${window.getSelection().toString().trim()}</i>` :
  332.                 `<blockquote>${window.getSelection().toString().trim()}</blockquote>`;
  333.  
  334.             $(area).val($(area).val() + highlighted); // insert new text at the end
  335.  
  336.             whenTextChanges(area); // trigger an update for the counter
  337.         }
  338.     }
  339.  
  340.     // update the stored cache (called on any text change)
  341.     function storeCache() {
  342.         let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt')));
  343.  
  344.         // cache is stored per page: workID -> text, workID-date -> last update date
  345.         // update current values in Map() and localStorage immediately
  346.         cachemap.set(workID, $('#float_cmt_userinput textarea').val()).set(workID+"-date", Date.now());
  347.         localStorage.setItem('floatcmt', JSON.stringify( Array.from(cachemap.entries()) ));
  348.     }
  349.  
  350.     // on page load, retrieve previously stored cached text and settings
  351.     function loadCache() {
  352.         let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt')));
  353.  
  354.         // squeezing in here logic to select the correct quotes & kbd shortcut setting
  355.         let quotes = cachemap.get('quotes') || "";
  356.         let kbd = cachemap.get('kbd') || "";
  357.         let name = cachemap.get('name') || "";
  358.         let email = cachemap.get('email') || "";
  359.  
  360.         // any cache outdated? we keep it for max 1 month to avoid storage limit issues
  361.         let maxdate = createDate(0, -1, 0);
  362.         cachemap.forEach((v, k) => {
  363.             if (["quotes", "kbd", "pos"].includes(k)) return; // skip the non-comment parts
  364.             if (k.endsWith("-date")) {
  365.                 let cachedate = new Date(v);
  366.                 if (cachedate < maxdate) {
  367.                     cachemap.delete(k.slice(0, -5));
  368.                     cachemap.delete(k);
  369.                 }
  370.             }
  371.             // delete any possible leftovers that don't have an associated date
  372.             else if (cachemap.get(k+"-date") === undefined) cachemap.delete(k);
  373.         });
  374.  
  375.         // cache is stored per page: workID -> text, workID-date -> last update date
  376.         let cache = cachemap.get(workID) || ""; // blank if there's nothing stored yet for this workID
  377.  
  378.         return [cache, quotes, kbd, name, email];
  379.     }
  380.  
  381.     // clean up cache for this page
  382.     function deleteCache() {
  383.         let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt')));
  384.  
  385.         // cache is stored per page: workID -> text, workID-date -> last update date
  386.         cachemap.delete(workID);
  387.         cachemap.delete(workID+'-date');
  388.  
  389.         localStorage.setItem('floatcmt', JSON.stringify( Array.from(cachemap.entries()) ));
  390.     }
  391.  
  392.     // removes all traces of the comment for this page
  393.     function discardComment() {
  394.         $('#float_cmt_userinput textarea').val(""); // resets the textarea to blank
  395.         whenTextChanges($('#float_cmt_userinput textarea')); // updates the counter accordingly
  396.         deleteCache(); // deletes the cached data
  397.         closeCommentBox(); // and hides the dialog
  398.     }
  399.  
  400.     // assemble the form data needed to submit the comment
  401.     function submitComment() {
  402.         let action = $("#float_cmt_chap select").val(); // per selection, the work or chapter target for submitting a comment
  403.  
  404.         // consolidating the fields we need for submitting a comment
  405.         var fd = new FormData();
  406.         fd.set("authenticity_token", $("#add_comment_placeholder input[name='authenticity_token']").val());
  407.         fd.set("comment[comment_content]", $(dlg).find('textarea').val());
  408.  
  409.         // are we logged in?
  410.         if ($("#float_cmt_pseud_select").length > 0) {
  411.             // pick up the selected pseud (either hidden <input> or <select> option)
  412.             fd.set("comment[pseud_id]", $("#float_cmt_pseud_select").val());
  413.         }
  414.         else {
  415.             // check that everything's filled in that's needed
  416.             let guest_name = $("#float_cmt_name").val();
  417.             let guest_email = $("#float_cmt_email").val();
  418.             let checkemail = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
  419.             if (guest_name == "" || $("#float_cmt_email").val() == "" || !checkemail.test(guest_email)) {
  420.                 $('#float_cmt_guest_hint').show();
  421.                 return;
  422.             }
  423.             $('#float_cmt_guest_hint').hide();
  424.  
  425.             fd.set("comment[name]", guest_name);
  426.             fd.set("comment[email]", guest_email);
  427.         }
  428.  
  429.         // no clue what this is, but it's a hidden field in the standard comment form
  430.         if (action.startsWith('/chapters/')) fd.set("controller_name", "chapters");
  431.         else fd.set("controller_name", "works");
  432.  
  433.         // turn buttons into a loading indicator
  434.         let buttons = $(dlg).dialog( "option", "buttons" );
  435.         $(dlg).dialog( "option", "buttons", [{
  436.             text: "Posting Comment...",
  437.             click: function() { return false; }
  438.         }]);
  439.  
  440.         // post the comment and reload the page to show it
  441.         grabResponse(action, fd, buttons);
  442.     }
  443.  
  444.     // actually submit the comment in a POST request
  445.     async function grabResponse(action, fd, buttons) {
  446.         // post the comment! this uses the Fetch API to POST the form data
  447.         const response = await fetch(action, { method: "POST", body: fd });
  448.  
  449.         // response might be not OK in case of retry later (427)
  450.         if (!response.ok) {
  451.             // show an error to the user
  452.             $(dlg).dialog( "option", "buttons", [{
  453.                 text: "Error saving comment!",
  454.                 click: function() { return false; }
  455.             }]);
  456.             return false; // stop all processing (comment is still cached)
  457.         }
  458.  
  459.         discardComment(); // clean up since it's now posted
  460.  
  461.         // question: did we post to a single chapter while viewing the entire work? then we probably want to keep on reading.
  462.         // action tells us where we posted to, and if that was a /chapters/...
  463.         // we can still tell that we were viewing an entire work by the available "Chapter by Chapter" button
  464.         if (action.startsWith('/chapters/') && $('#main ul.work.navigation li.chapter.bychapter').length > 0) {
  465.             $(dlg).dialog( "option", "buttons", buttons ); // reset the buttons in the dialog (which just said "Posting Comment..." now)
  466.         }
  467.  
  468.         // otherwise we want to see the comment we just posted
  469.         // eff this, there's no way to get the original redirected location of the POST (which includes the new #comment_id at the end)
  470.         // so all we can do is look at the response page with comments shown (per the redirected GET)
  471.         else {
  472.             // puzzling together the reponse stream until we have a full HTML page (to avoid another background pageload)
  473.             let responseBody = "";
  474.             for await (const chunk of response.body) {
  475.                 let chunktext = new TextDecoder().decode(chunk); // turns it from uint8array to text
  476.                 responseBody += chunktext;
  477.             }
  478.  
  479.             // find out if there's multiple pages of comments now, based on the comment pagination (pick the last page)
  480.             let lastpage = $(responseBody).find('#comments_placeholder ol.pagination').first().children().eq(-2).find('a').attr('href');
  481.             // if there's no pagination, just use the redirect URL; either way scroll that to the footer
  482.             lastpage = (lastpage > "") ? lastpage.slice(0, -9)+'#footer' : response.url+'#footer';
  483.  
  484.             // redirect us to where we're hopefully seeing the comment we just posted
  485.             window.location.href = lastpage;
  486.         }
  487.     }
  488.  
  489. })(jQuery);
  490.  
  491. function createDate(days, months, years) {
  492.     var date = new Date();
  493.     date.setFullYear(date.getFullYear() + years);
  494.     date.setMonth(date.getMonth() + months);
  495.     date.setDate(date.getDate() + days);
  496.     return date;
  497. }
  498. // helper function to determine whether a color (the background in use) is light or dark
  499. // https://awik.io/determine-color-bright-dark-using-javascript/
  500. function lightOrDark(color) {
  501.     var r, g, b, hsp;
  502.     if (color.match(/^rgb/)) { color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
  503.         r = color[1]; g = color[2]; b = color[3]; }
  504.     else { color = +("0x" + color.slice(1).replace(color.length < 5 && /./g, '$&$&'));
  505.         r = color >> 16; g = color >> 8 & 255; b = color & 255; }
  506.     hsp = Math.sqrt( 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b) );
  507.     if (hsp>127.5) { return 'light'; } else { return 'dark'; }
  508. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement