AO3 Floating Comment Box

Floating comment box for AO3

  1. // ==UserScript==
  2. // @name AO3 Floating Comment Box
  3. // @description Floating comment box for AO3
  4. // @include *://archiveofourown.org/*works/*
  5. // @namespace https://greasyfork.org/en/scripts/395902-ao3-floating-comment-box
  6. // @version 0.9
  7. // @run-at document-end
  8. // @grant GM.getValue
  9. // @grant GM.setValue
  10. // @grant GM.deleteValue
  11. // ==/UserScript==
  12.  
  13.  
  14. 'use strict';
  15.  
  16. const primary = "#0275d8"
  17. const success = "#5cb85c"
  18. const danger = "#d9534f"
  19.  
  20. let curURL = document.URL
  21. if(curURL.includes("#")){
  22. curURL = document.URL.slice(0,document.URL.indexOf("#"))
  23. }
  24. let newURL = curURL
  25.  
  26. const addStyles = () => {
  27. const styles = document.createElement("style")
  28. styles.innerHTML = fullStyles() + "\n" + addMediaStyles()
  29. return styles
  30. }
  31.  
  32. const fullStyles = () => {
  33. let full = ""
  34. for(let [key, value] of Object.entries(allStyles)){
  35. let newStyle = key + " {"
  36. for(let [key2, val2] of Object.entries(value)){
  37. newStyle += "\n" + key2 + ": " + val2 + ";"
  38. }
  39. newStyle += "\n}\n"
  40. full += newStyle
  41. }
  42. return full
  43. }
  44.  
  45. const addMediaStyles = () => {
  46. let full = ""
  47. for(let [key, value] of Object.entries(mediaStyles)){
  48. let newStyle = key + "{"
  49. for(let [key2, val2] of Object.entries(value)){
  50. newStyle += "\n" + key2 + "{"
  51. for (let [key3, val3] of Object.entries(val2)){
  52. newStyle += "\n" + key3 + ": " + val3 + ";"
  53. }
  54. newStyle +="\n}\n"
  55. }
  56. newStyle += "\n}\n"
  57. full += newStyle
  58. }
  59. return full
  60. }
  61.  
  62. const mediaStyles = {
  63. "@media (min-width: 1375px)": {
  64. ".float-div": {
  65. "width": "80%",
  66. "max-width": "80%",
  67. "left": "10%"
  68. },
  69. ".float-cmt-btn": {
  70. "font-size": "1em"
  71. },
  72. "#openCmtBtn": {
  73. "font-size": "1.15em",
  74. "padding": "2px 4px"
  75. }
  76. },
  77. "@media (min-width: 1575px)": {
  78. ".float-div": {
  79. "width": "70%",
  80. "max-width": "70%",
  81. "left": "15%"
  82. },
  83. ".float-cmt-btn": {
  84. "font-size": "1em"
  85. },
  86. "#openCmtBtn": {
  87. "font-size": "1.3em",
  88. "padding": "4px 8px"
  89. }
  90. },
  91. "@media (min-width: 1850px)": {
  92. ".float-div": {
  93. "width": "60%",
  94. "max-width": "60%",
  95. "left": "20%"
  96. },
  97. ".float-cmt-btn": {
  98. "font-size": "1.1em"
  99. },
  100. "#openCmtBtn": {
  101. "font-size": "1.5em",
  102. "padding": "5px 10px"
  103. }
  104. }
  105. }
  106.  
  107. const allStyles = {
  108. ".float-div": {
  109. "display": "none",
  110. "position": "fixed",
  111. "z-index": "1",
  112. "bottom": ".5%",
  113. "width": "98%",
  114. "height": "30%",
  115. "background-color": "#ddd",
  116. "border-style": "double",
  117. "border-color": "grey",
  118. "padding": "5px",
  119. "resize": "both",
  120. "overflow": "auto",
  121. "border-radius": "25px",
  122. "border-width": "5px"
  123. },
  124. ".btn-div": {
  125. "display": "flex",
  126. "justify-content": "space-around",
  127. "top": "0px",
  128. "width": "100%",
  129. "max-width": "100%",
  130. "height": "15%"
  131. },
  132. ".char-count": {
  133. "font-size": ".8em"
  134. },
  135. ".float-box": {
  136. "min-height": "70%",
  137. "max-width": "98%",
  138. "background-color": "white"
  139. },
  140. ".float-cmt-btn": {
  141. "border": "none",
  142. "text-align": "center",
  143. "text-decoration": "none",
  144. "display": "inline-block",
  145. "font-size": ".8em",
  146. "padding": ".2% 3%",
  147. "top": "10%",
  148. "bottom": "10%",
  149. "height": "70%"
  150. },
  151. "#openCmtBtn": {
  152. "position": "fixed",
  153. "z-index": "1",
  154. "top": "0px",
  155. "left": "0px",
  156. "font-size": ".9em",
  157. "padding": "1px 2px",
  158. "border": "none",
  159. "text-align": "center",
  160. "text-decoration": "none",
  161. "display": "inline-block",
  162. "background": primary
  163. },
  164. "#addCmtBtn": {
  165. "background": primary
  166. },
  167. "#delCmtBtn": {
  168. "background": danger
  169. },
  170. "#insCmtBtn": {
  171. "background": primary
  172. },
  173. ".font-select": {
  174. "float": "right",
  175. "top": "10%",
  176. "bottom": "10%",
  177. "width": "10%",
  178. "height": "80%"
  179.  
  180. },
  181. ".btn-font": {
  182. "color": "white"
  183. }
  184.  
  185. }
  186.  
  187. const createBox = () => {
  188. const textBox = document.createElement("textarea")
  189. textBox.className = "float-box"
  190. textBox.addEventListener("keyup", async () => {
  191. await GM.setValue(newURL, textBox.value)
  192. const addBtn = document.querySelector("#addCmtBtn")
  193. const charCount = document.querySelector(".char-count")
  194. const newCount = 10000 - textBox.value.length
  195. charCount.textContent = `Characters left: ${newCount}`
  196. addBtn.style.background = primary
  197. addBtn.textContent = "Add to Comment Box"
  198. })
  199.  
  200. return textBox
  201. }
  202.  
  203. const createChangeFontSize = () => {
  204. const selectMenu = document.createElement("select")
  205. selectMenu.className = "font-select"
  206. const optNums = [".5em",".7em", ".85em", "1em", "1.25em", "1.5em"]
  207. for(let num of optNums){
  208. const opt = document.createElement("option")
  209. opt.value = num
  210. opt.className = "font-option"
  211. opt.style.fontSize = num
  212. opt.textContent = "Font size"
  213. selectMenu.appendChild(opt)
  214. }
  215. selectMenu.addEventListener("click", () => {
  216. const textBox = document.querySelector(".float-box")
  217. textBox.style.fontSize = selectMenu.value
  218. })
  219. return selectMenu
  220. }
  221.  
  222. const charCount = () => {
  223. const newDiv = document.createElement("div")
  224. newDiv.className = "char-count"
  225. newDiv.textContent = "Characters left: 10000"
  226. return newDiv
  227. }
  228.  
  229.  
  230. const createButton = () => {
  231. const newButton = document.createElement("button")
  232. newButton.className = "btn-font"
  233. newButton.id = "openCmtBtn"
  234. newButton.textContent = "O"
  235. newButton.addEventListener("click", () => {
  236. const div = document.querySelector(".float-div")
  237. if(div.style.display === "block"){
  238. div.style.display = "none"
  239. newButton.textContent = "O"
  240. newButton.style.background = primary
  241. } else {
  242. div.style.display = "block"
  243. newButton.textContent = "X"
  244. newButton.style.background = danger
  245. const textBox = document.querySelector(".float-box")
  246. textBox.scrollTop = textBox.scrollHeight
  247. }
  248.  
  249. })
  250. return newButton
  251. }
  252.  
  253. const createMainDiv = () => {
  254. const newDiv = document.createElement("div")
  255. newDiv.className = "float-div"
  256. const btnDiv = document.createElement("div")
  257. btnDiv.className = "btn-div"
  258. btnDiv.appendChild(insertButton())
  259. btnDiv.appendChild(addButton())
  260. btnDiv.appendChild(createDelete())
  261. btnDiv.appendChild(chapterRadio())
  262. btnDiv.appendChild(createChangeFontSize())
  263. newDiv.appendChild(btnDiv)
  264. newDiv.appendChild(createBox())
  265. newDiv.appendChild(charCount())
  266. return newDiv
  267. }
  268.  
  269. const createDelete = () => {
  270. const newButton = document.createElement("button")
  271. newButton.textContent = "Delete"
  272. newButton.className = "float-cmt-btn btn-font"
  273. newButton.id = "delCmtBtn"
  274. newButton.addEventListener("click", async () => {
  275. if(confirm("Are you sure you want to delete your comment?")){
  276. if((await GM.getValue(newURL, "noCmtHere")) !== "noCmtHere"){
  277. await GM.deleteValue(newURL)
  278. document.querySelector(".float-box").value = ""
  279. document.querySelector("textarea[id^='comment_content_for']").value = ""
  280. }
  281. }
  282. })
  283. return newButton
  284. }
  285.  
  286. const chapterRadio = () => {
  287. const radioDiv = document.createElement("div")
  288. radioDiv.className = "radio-div"
  289. const radioOne = document.createElement("input")
  290. const radioTwo = document.createElement("input")
  291. radioOne.type = "radio"
  292. radioTwo.type = "radio"
  293. radioOne.name = "chapters"
  294. radioTwo.name = "chapters"
  295. radioOne.className = "chapter-toggle"
  296. radioTwo.className = "chapter-toggle"
  297. radioOne.id = "entireCmt"
  298. radioTwo.id = "chapterCmt"
  299. const labelOne = document.createElement("label")
  300. const labelTwo = document.createElement("label")
  301. labelOne.setAttribute("for", "entireCmt")
  302. labelTwo.setAttribute("for","chapterCmt")
  303. labelOne.textContent = "Full Work"
  304. labelTwo.textContent = "By Chapter"
  305.  
  306. if(curURL.includes("chapters")){
  307. radioOne.checked = false
  308. radioTwo.checked = true
  309. } else {
  310. radioDiv.style.display = "none"
  311. radioOne.disabled = true
  312. radioTwo.disabled = true
  313. }
  314.  
  315. radioOne.addEventListener("click", () => {
  316. if(newURL.includes("chapters")){
  317. newURL = curURL.slice(0,curURL.indexOf("/chapters"))
  318. addStoredText()
  319. }
  320. })
  321. radioTwo.addEventListener("click", () => {
  322. if(!newURL.includes("chapters")){
  323. newURL = curURL
  324. addStoredText()
  325. }
  326.  
  327. })
  328. radioDiv.appendChild(radioOne)
  329. radioDiv.appendChild(labelOne)
  330. radioDiv.appendChild(radioTwo)
  331. radioDiv.appendChild(labelTwo)
  332. return radioDiv
  333. }
  334.  
  335. const addButton = () => {
  336. const newButton = document.createElement("button")
  337. newButton.textContent = "Add to Comment Box"
  338. newButton.className = "float-cmt-btn btn-font"
  339. newButton.id = "addCmtBtn"
  340. const realCmtBox = document.querySelector("textarea[id^='comment_content_for']")
  341. newButton.addEventListener("click", async () => {
  342. realCmtBox.value = document.querySelector(".float-box").value
  343. newButton.style.background = success
  344. newButton.textContent = "Added to Comment Box"
  345. })
  346. return newButton
  347. }
  348.  
  349. const insertButton = () => {
  350. const newButton = document.createElement("button")
  351. newButton.textContent = "Insert Selection"
  352. newButton.className = "float-cmt-btn btn-font"
  353. newButton.id = "insCmtBtn"
  354. newButton.addEventListener("click", async () => {
  355. const selection = `<i>${window.getSelection().toString().trim()}</i>`
  356. const textBox = document.querySelector(".float-box")
  357. const newText = `${textBox.value}${selection}\n`
  358. textBox.value = newText
  359. await GM.setValue(newURL, newText)
  360. })
  361. return newButton
  362. }
  363.  
  364. const addStoredText = async () => {
  365. const textBox = document.querySelector(".float-box")
  366. if(curURL.includes("full")){
  367. newURL = curURL.slice(0, curURL.indexOf("?"))
  368. }
  369. const storedText = await GM.getValue(newURL,"")
  370. textBox.value = storedText
  371. }
  372.  
  373.  
  374. const init = () => {
  375. const body = document.body
  376. body.appendChild(createButton())
  377. body.appendChild(addStyles())
  378. body.appendChild(createMainDiv())
  379. addStoredText()
  380. }
  381.  
  382. init()