// ==UserScript==
// @name AO3 Floaty Comment Box (Responsive)
// @namespace http://tampermonkey.net/
// @version 1.10
// @description AO3 Floaty Comment Box (Responsive) is a userscript created to facilitate commenting on the fly while reading on archiveofourown - specifically for mobile browsing
// @author Schildpath
// @match http://archiveofourown.org/*
// @match https://archiveofourown.org/*
// @match http://www.archiveofourown.org/*
// @match https://www.archiveofourown.org/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
let floatyCreated = false;
function createFloaty(commentBox) {
// Prevent double initialization or initialization without cause
if (floatyCreated || !commentBox) return;
floatyCreated = true;
// Floaty toggle button
const floatyButton = document.createElement('button');
floatyButton.innerHTML = '✍';
floatyButton.id = 'floaty-toggle-button';
floatyButton.setAttribute('aria-label', 'Toggle Comment Box');
floatyButton.style.cssText = `
position: fixed;
inset: .5em .5em auto auto;
z-index: 9999;
padding: 0.1em 0.4em;
border-radius: 0.4em;
font-family: inherit;
font-size: 1.3em;
color: inherit;
opacity: .9;
cursor: pointer;
min-width: 20px;
min-height: 20px;
`;
floatyButton.onfocus = (e) => e.target.blur(); // Prevent AO3 focus style
document.body.appendChild(floatyButton);
// Floaty box container
const floatyContainer = document.createElement('div');
floatyContainer.id = 'floaty-container';
floatyContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
width: 100vw;
height: 30%;
z-index: 9997;
display: none;
flex-direction: column;
background: inherit;
border-top: 1px solid currentColor;
font-family: inherit;
font-size: 0.8em;
color: inherit;
box-shadow: 0 5px 10px rgba(0,0,0,0.5);
`;
document.body.appendChild(floatyContainer);
// Tips & about container
const bgContainer = document.createElement('div');
bgContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
width: 100vw;
height: 100vh;
display: none;
z-index: 9998
`;
const infoContainer = document.createElement('div');
infoContainer.style.cssText = `
position: fixed;
display: flex;
top: 50%;
left: 0;
transform: translate(0, -50%);
margin: 0 1em;
z-index: 9999;
display: none;
flex-direction: column;
padding: .9em;
gap: 0.5em;
font-family: inherit;
font-size: 0.8em;
background: inherit;
overflow-y: auto;
border-top: 1px solid inherit;
border-bottom: 1px solid inherit;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
`;
const aboutDiv = document.createElement('div');
aboutDiv.innerHTML = `<p><strong>About AO3 Floaty Comment Box (Responsive):</strong></p>` +
'<p>AO3 Floaty Comment Box (Responsive) was created to facilitate commenting while reading - to allow the user to copy paste favorite quotes and write down feelings & thoughts on the fly - specifically for mobile browsing.</p>' +
'<ul><li>🔛 <strong>Display:</strong> You can minimize the floaty comment box as you read and reopen it to continue to edit your review. You can also expand or collapse the box with the triangle buttons (▼▲) in the header bar.</li>' +
'<li>💬 <strong>Insert quotes:</strong> Select favorite quotes and use the « » button to insert them your comment: the selected text will be formatted in italics and put between quotation marks.</li>' +
'<li>👉 <strong>Navigate:</strong> Scroll down the real comment box with the downward arrow (⇓) and go back to your previous position with the upward arrow (⇑).</i>' +
'<li>🔄 <strong>Syncing:</strong> Everything that is typed in the floaty comment box will be automatically synced with the real comment box below the fic.</i>' +
'<li>💌 <strong>Submitting:</strong> Your comment will only be submitted once you submit it in the the real comment form below (scroll down with the ⇓ button).</li></ul>' +
`<br><p>📞 <strong>Contact:</strong> <a href="mailto:[email protected]">Email me</a> for questions or issues.</p>` +
`<br><p>© AO3 Floaty Comment Box (Responsive) was directly inspired (with permission) by an AO3 userscript originally developed by <a href="https://ravenel.tumblr.com/post/156555172141/i-saw-this-post-by-astropixie-about-how-itd-be">ravenel</a>. There is also a similar script by <a href="https://greasyfork.org/en/scripts/395902-ao3-floating-comment-box">ScriptMouse</a>.</p>`
;
const tipsDiv = document.createElement('div');
tipsDiv.innerHTML = `<p><strong>Suggestions for writing a comment:</strong></p>` +
'<ul><li>💬 Quotes you liked (select text and click « » to include)</li>'+
'<li>🎭 Scenes that you liked, or moved you, or surprised you</li>'+
'<li>😭 What is your feeling at the end of the chapter?</li>'+
'<li>👓 What are you most looking forward to next?</li>' +
'<li>🔮 Do you have any predictions for the next chapters you want to share?</li>'+
'<li>❓ Did this chapter give you any questions you can't wait to find out the answers for?</li>' +
'<li>✨ Is there something unique about the story that you like?</li>'+
'<li>🤹 Does the author have a style that really works for you?</li>' +
'<li>🎤 Did the author leave any comments in the notes that said what they wanted feedback on?</li>' +
'<li>🗣 Even if all you have are incoherent screams of delight, authors love to hear that as well.</li></ul>'
;
const closeAboutBtn = document.createElement('button');
const closeTipsBtn = document.createElement('button');
[closeAboutBtn, closeTipsBtn].forEach(btn => {
btn.innerHTML = 'x';
btn.style.cssText = `
position: absolute;
top: .9em;
right: .9em;
cursor: pointer;
`;
});
aboutDiv.appendChild(closeAboutBtn);
tipsDiv.appendChild(closeTipsBtn);
infoContainer.appendChild(aboutDiv);
infoContainer.appendChild(tipsDiv);
document.body.appendChild(bgContainer);
document.body.appendChild(infoContainer);
// Floaty container header
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: flex-start;
align-items: center;
align-content: stretch;
gap: 0.3em;
padding: .5em.9em;
font-size: 1em;
`;
const insertBtn = document.createElement('button');
insertBtn.innerHTML = '« quote »';
const tipsBtn = document.createElement('button');
tipsBtn.innerHTML = '💡';
const aboutBtn = document.createElement('button');
aboutBtn.innerHTML = '❓';
const downBtn = document.createElement('button');
downBtn.innerHTML = '⇓';
const upBtn = document.createElement('button');
upBtn.innerHTML = '⇑';
const expandBtn = document.createElement('button');
expandBtn.innerHTML = '▼';
const minimizeBtn = document.createElement('button');
minimizeBtn.innerHTML = '▲';
const collapseBtn = document.createElement('button');
collapseBtn.innerHTML = '▲';
const uncollapseBtn = document.createElement('button');
uncollapseBtn.innerHTML = '▼';
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '❯❯';
[insertBtn, aboutBtn, tipsBtn, downBtn, upBtn, expandBtn, minimizeBtn, uncollapseBtn, collapseBtn, closeBtn].forEach(btn => {
btn.style.cssText = `
font-family: inherit;
color: inherit;
cursor: pointer;
padding: 0.2em 0.4em;
font-size: 1em;
height: 17px;
min-width: 17px;
`;
});
closeBtn.style.fontSize = '.8em';
if (window.innerWidth > 768) {
closeBtn.style.marginRight = '1.5em';
}
minimizeBtn.style.display = 'none';
uncollapseBtn.style.display = 'none';
const rightButtons = document.createElement('div');
rightButtons.style.cssText = `
margin-left: auto;
display: flex;
gap: 0.3em;
justify-content: flex-start;
align-items: center;
align-content: stretch;
`;
rightButtons.append(collapseBtn, expandBtn, uncollapseBtn, minimizeBtn, closeBtn);
header.append(insertBtn, downBtn, upBtn, tipsBtn, aboutBtn, rightButtons);
floatyContainer.appendChild(header);
// Textarea
const floatyBox = document.createElement('textarea');
floatyBox.placeholder = 'Work-in-progress review...';
floatyBox.id = 'floaty-box';
floatyBox.style.cssText = `
padding: .9em;
font-size: 1em;
font-family: inherit;
color: inherit;
background: inherit;
border: none;
resize: none;
outline: none;
width: 100%;
height: 100%;
box-sizing: border-box;
`;
floatyContainer.appendChild(floatyBox);
// Character count
const charCount = document.createElement('span');
const updateCharCount = () => {
let count = 10000 - floatyBox.value.length;
charCount.textContent = `${count} characters left`;
charCount.style.color = count < 0 ? 'red' : 'inherit';
};
updateCharCount();
// Footer
const footer = document.createElement('div');
footer.id = 'floaty-footer';
footer.style.cssText = `
display: flex;
justify-content: flex-end;
align-items: center;
align-content: stretch;
gap: 0.5em;
padding: .5em .9em;
font-size: .8em;
opacity: 1;
`;
footer.appendChild(charCount);
floatyContainer.appendChild(footer);
// Actions
floatyButton.addEventListener('click', () => {
floatyContainer.style.display = 'flex';
floatyButton.style.display = 'none';
});
closeBtn.addEventListener('click', () => {
floatyContainer.style.display = 'none';
floatyButton.style.display = 'block';
aboutDiv.style.display = 'none';
tipsDiv.style.display = 'none';
});
[insertBtn].forEach(btn => {
btn.addEventListener('click', () => {
const selected = window.getSelection().toString().trim();
if (selected) {
floatyBox.focus();
floatyBox.setRangeText(`<blockquote><em>${selected}</em></blockquote>\n`, floatyBox.selectionStart, floatyBox.selectionEnd, 'end');
floatyBox.dispatchEvent(new Event('input')); // Sync with real box
}
});
});
aboutBtn.addEventListener('click', () => {
bgContainer.style.display = 'block';
infoContainer.style.display = 'flex';
aboutDiv.style.display = 'block';
tipsDiv.style.display = 'none';
});
closeAboutBtn.addEventListener('click', () => {
bgContainer.style.display = 'none';
infoContainer.style.display = 'none';
aboutDiv.style.display = 'none';
});
tipsBtn.addEventListener('click', () => {
bgContainer.style.display = 'block';
infoContainer.style.display = 'flex';
tipsDiv.style.display = 'block';
aboutDiv.style.display = 'none';
});
closeTipsBtn.addEventListener('click', () => {
bgContainer.style.display = 'none';
infoContainer.style.display = 'none';
tipsDiv.style.display = 'none';
});
bgContainer.addEventListener('click', () => {
bgContainer.style.display = 'none';
infoContainer.style.display = 'none';
aboutDiv.style.display = 'none';
tipsDiv.style.display = 'none';
});
infoContainer.addEventListener('click', (e) => {
e.stopPropagation();
});
expandBtn.addEventListener('click', () => {
expandBtn.style.display = 'none';
collapseBtn.style.display = 'none';
uncollapseBtn.style.display = 'none';
minimizeBtn.style.display = 'block';
floatyContainer.style.height = '100%';
});
minimizeBtn.addEventListener('click', () => {
minimizeBtn.style.display = 'none';
uncollapseBtn.style.display = 'none';
expandBtn.style.display = 'block';
collapseBtn.style.display = 'block';
floatyContainer.style.height = '30%';
});
collapseBtn.addEventListener('click', () => {
uncollapseBtn.style.display = 'block';
minimizeBtn.style.display = 'none';
expandBtn.style.display = 'none';
collapseBtn.style.display = 'none';
floatyBox.style.display = 'none';
footer.style.display = 'none';
floatyContainer.style.height = '36px';
});
uncollapseBtn.addEventListener('click', () => {
uncollapseBtn.style.display = 'none';
minimizeBtn.style.display = 'none';
expandBtn.style.display = 'block';
collapseBtn.style.display = 'block';
floatyBox.style.display = 'block';
footer.style.display = 'flex';
floatyContainer.style.height = '30%';
});
downBtn.addEventListener('click', () => {
// Save scroll position to local storage
localStorage.setItem("scrollY",window.scrollY);
// Jump to real comment box
document.querySelector('textarea[name="comment[comment_content]"]').scrollIntoView();
});
upBtn.addEventListener('click', () => {
// Go to previous scroll position
const prevScrollPosition = localStorage.getItem("scrollY");
window.scrollTo(0,parseInt(prevScrollPosition));
});
// Sync between floaty and real comment box
floatyBox.addEventListener('input', () => {
commentBox.value = floatyBox.value;
updateCharCount();
});
commentBox.addEventListener('input', () => {
if (commentBox.value !== floatyBox.value) {
floatyBox.value = commentBox.value;
updateCharCount();
}
});
}
function waitForCommentBox(attempts = 0) {
// This script only applies to story pages where you can comment, which we need to check for
const box = document.querySelector('textarea[name="comment[comment_content]"]');
if (box) {
createFloaty(box);
} else if (attempts < 20) {
setTimeout(() => waitForCommentBox(attempts + 1), 500);
} else {
console.log("Comment box not found after 20 attempts.");
}
}
function init() {
console.log("Init called");
waitForCommentBox();
}
window.addEventListener('load', init);
document.addEventListener('pjax:end', init);
setInterval(() => {
if (!floatyCreated) init();
}, 1500); // Safety net for mobile load quirks
})();