// ==UserScript== // @name AO3: Sticky Comment Box // @namespace https://greasyfork.org/en/users/906106-escctrl // @version 1.2 // @description gives you a comment box that stays in view as you scroll and read the story // @author escctrl // @license MIT // @match *://archiveofourown.org/works/* // @exclude *://archiveofourown.org/works/*/new // @exclude *://archiveofourown.org/works/*/edit* // @exclude *://archiveofourown.org/works/new* // @exclude *://archiveofourown.org/works/search* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js // @grant none // @downloadURL none // ==/UserScript== (function($) { 'use strict'; // despite the @excludes, there are always ways that editing a work ends up with AO3's URL being just /works/xxxxx >:( // so we can't rely on URLs, we gotta check for ourselves and stop if there's no fic to display if ($('#main.works-show #chapters').length == 0) return; // sticky button to open the comment box let cmtButton = `
`; $('body').append(cmtButton); // listening to button click: open or close the dialog $('#float_cmt_toggle').on('click', (e) => { toggleCommentBox(); }); // this is called by the button and also the keyboard shortcut function toggleCommentBox() { if ($(dlg+":hidden").length > 0) openCommentBox(); else if ($(dlg+":visible").length > 0) closeCommentBox(); } var dlg = "#float_cmt_dlg"; let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "ui-darkness" : "base"; // if the background is dark, use the dark UI theme to match let fontsize = $("#main #chapters .userstuff").css('font-size'); // enforce the reading font size for the dialog $("head").append(``) .append(``); // prepping the dialog (without opening it) createCommentBox(); var scrollPOS; // prepares the dialog and loads the cache into it function createCommentBox() { // designing the floating box $("body").append(`
`); // optimizing the GUI in case it's a mobile device let screen = parseInt($("body").css("width")); // parseInt ignores letters (px) let buttonText = screen <= 500 ? false : true; let dialogwidth = screen <= 500 ? screen * 0.9 : 500; let resize = screen <= 500 ? false : true; $(dlg).dialog({ modal: false, autoOpen: false, resizable: resize, draggable: true, width: dialogwidth, position: { my: "right bottom", at: "right bottom", of: "window" }, title: "Comment", buttons: [ { text: "Settings", icon: "ui-icon-gear", showLabel: buttonText, click: () => { toggleSettings(); } }, { text: "Quote", icon: "ui-icon-caret-2-e-w", showLabel: buttonText, click: () => { grabHighlight(); } }, { text: "Discard", icon: "ui-icon-trash", showLabel: buttonText, click: () => { discardComment(); } }, { text: "Post", icon: "ui-icon-comment", showLabel: buttonText, click: () => { submitComment(); } }, { text: "Close", icon: "ui-icon-close", showLabel: buttonText, click: () => { closeCommentBox(); } }, ], // positioning stuff below is so that it SCROLLS WITH THE PAGE JFC https://stackoverflow.com/a/9242751/22187458 create: function(event, ui) { $(event.target).parent().css('position', 'fixed'); // and also to put the dialog where it was last left across pageloads let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt'))); if (cachemap.get('pos')) { let pos = JSON.parse(cachemap.get('pos')); pos.of = $(window); $(dlg).dialog('option','position', pos); } // 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 // workaround: force the dialog to stay within the visible screen - no dragging outside of viewport means it can't disappear $(dlg).dialog("widget").draggable("option","containment","window"); // issue: to fix the return-to-top scrolling, the standard close button would need hookins to the beforeClose and close events // workaround: simply not display that x in the title, there's anyways the Close button at the bottom //$(dlg).parent().find(".ui-dialog-titlebar-close").hide(); }, resizeStop: function(event, ui) { let position = [(Math.floor(ui.position.left) - $(window).scrollLeft()), (Math.floor(ui.position.top) - $(window).scrollTop())]; $(event.target).parent().css('position', 'fixed'); $(dlg).dialog('option','position',position); }, beforeClose: function() { // store the position of the dialog so we can reopen it there after page refresh let cachemap = new Map(JSON.parse(localStorage.getItem('floatcmt'))); let pos = $(dlg).dialog( "option", "position" ); pos = { my: pos.my, at: pos.at }; // need to keep only the pieces we need - it's a cyclic object! cachemap.set('pos', JSON.stringify(pos)); // store the current settings along with it cachemap.set('quotes', $('#float_cmt_quote').val()); cachemap.set('kbd', $('#float_cmt_kbd').val()); bindShortcut($('#float_cmt_kbd').val()); // update the keyboard shortcut binding so it takes effect localStorage.setItem('floatcmt', JSON.stringify( Array.from(cachemap.entries()) )); // issue: when closing the dialog, the opening button is scrolled back into focus - intended behavior (: // workaround: remember the scroll position before closing and return there after scrollPOS = window.scrollY; // get current scroll position }, close: function() { window.scroll({ top: scrollPOS, left: 0, behavior: "instant" }); // scroll page back to previous scroll position } }); // load cache: [0] = text, [1] = quotes, [2] = kbd let cache = loadCache(); $(dlg).html(`
Comment as
10000 characters left
`); // add the pseud selection to the dialog so we know which one to submit with let pseud_id = $("#add_comment_placeholder [name='comment[pseud_id]']").get(0); // available pseuds - either a hidden , or a or