lemmy-keyboard-navigation

Easily navigate Lemmy with your keyboard

当前为 2023-08-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name lemmy-keyboard-navigation
  3. // @match https://*/*
  4. // @grant none
  5. // @version 2.1
  6. // @author vmavromatis
  7. // @author howdy@thesimplecorner.org
  8. // @author InfinibyteF4
  9. // @author aglidden
  10. // @license GPL3
  11. // @icon https://raw.githubusercontent.com/vmavromatis/Lemmy-keyboard-navigation/main/icon.png?inline=true
  12. // @homepageURL https://github.com/vmavromatis/Lemmy-keyboard-navigation
  13. // @namespace https://github.com/vmavromatis/Lemmy-keyboard-navigation
  14. // @description Easily navigate Lemmy with your keyboard
  15. // @run-at document-end
  16. // ==/UserScript==
  17.  
  18. /*global window,console,localStorage,sessionStorage,document,GM_addStyle,PRO_addStyle,addStyle,MutationObserver,location*/
  19.  
  20. //isLemmySite
  21. if (document.querySelectorAll('.lemmy-site').length >= 1) {
  22.  
  23. // DEBUGGING (ignore me!)
  24. //localStorage.clear();
  25. //sessionStorage.clear();
  26.  
  27. //settings page (this is from lemmyTools)
  28. const optionsKey = "lemmy-keyboard-navigation-Options";
  29.  
  30. function getSettingsFromLocalStorage() {
  31. try {
  32. return JSON.parse(localStorage.getItem(optionsKey) || "{}");
  33. } catch (_) {
  34. return {};
  35. }
  36. }
  37.  
  38. function checkedIfTrue(val) {
  39. return val ? "checked" : "";
  40. }
  41.  
  42. function options(open) {
  43. const odiv = document.getElementById("lkOptions");
  44. let userOptions = {};
  45. if (open === "open") {
  46. odiv.style.display = "block";
  47. } else if (open === "set") {
  48. //First run set defaults or pull from localstorage.
  49. userOptions = Object.assign({}, {
  50. pageOffset: 5,
  51. vimKeyNavigation: true,
  52. smoothScroll: false,
  53. scrollPosition: "middle",
  54. backgroundHex: "#373737",
  55. kb_expand: "KeyX",
  56. kb_comments: "KeyC",
  57. kb_openLink: "Enter",
  58. kb_parent: "KeyP",
  59. kb_upvote: "KeyA",
  60. kb_downvote: "KeyZ",
  61. kb_replyComm: "KeyR",
  62. kb_save: "KeyS",
  63. kb_context: "KeyQ",
  64. kb_smallerImg: "Minus",
  65. kb_largerImg: "Equal",
  66. kb_user: "KeyU",
  67. kb_edit: "KeyE",
  68. kb_top: "KeyT",
  69. m_dialog: "KeyG",
  70. m_posts: "KeyP",
  71. m_comments: "KeyC",
  72. m_subscribed: "Digit1",
  73. m_local: "Digit2",
  74. m_all: "Digit3",
  75. m_frontpage: "KeyF",
  76. m_saved: "KeyS",
  77. m_userpage: "KeyU",
  78. m_inbox: "KeyI",
  79. m_options: "KeyO"
  80. },
  81. getSettingsFromLocalStorage()
  82. );
  83. localStorage.setItem(optionsKey, JSON.stringify(userOptions));
  84. } else if (open === "save") {
  85. //save button
  86. odiv.style.display = "none";
  87. //general
  88. userOptions.vimKeyNavigation =
  89. document.getElementById("option_vimKeyNavigation").checked;
  90.  
  91. userOptions.smoothScroll =
  92. document.getElementById("option_smoothScroll").checked;
  93.  
  94. let offset = parseFloat(
  95. document.getElementById("option_pageOffset").value
  96. );
  97. if (isNaN(offset) || offset < 0 || offset > 100) {
  98. userOptions.pageOffset = 0;
  99. } else {
  100. userOptions.pageOffset = offset;
  101. }
  102.  
  103. userOptions.backgroundHex =
  104. document.getElementById("option_backgroundHex").value;
  105.  
  106. userOptions.scrollPosition =
  107. document.getElementById("option_scrollPosition").value;
  108. //keybinds
  109. userOptions.kb_expand =
  110. document.getElementById("option_kb_expand").value;
  111.  
  112. userOptions.kb_comments =
  113. document.getElementById("option_kb_comments").value;
  114.  
  115. userOptions.kb_openLink =
  116. document.getElementById("option_kb_openLink").value;
  117.  
  118. userOptions.kb_parent =
  119. document.getElementById("option_kb_parent").value;
  120.  
  121. userOptions.kb_upvote =
  122. document.getElementById("option_kb_upvote").value;
  123.  
  124. userOptions.kb_downvote =
  125. document.getElementById("option_kb_downvote").value;
  126.  
  127. userOptions.kb_replyComm =
  128. document.getElementById("option_kb_replyComm").value;
  129.  
  130. userOptions.kb_save =
  131. document.getElementById("option_kb_save").value;
  132.  
  133. userOptions.kb_context =
  134. document.getElementById("option_kb_context").value;
  135.  
  136. userOptions.kb_smallerImg =
  137. document.getElementById("option_kb_smallerImg").value;
  138.  
  139. userOptions.kb_largerImg =
  140. document.getElementById("option_kb_largerImg").value;
  141.  
  142. userOptions.kb_user =
  143. document.getElementById("option_kb_user").value;
  144.  
  145. userOptions.kb_edit =
  146. document.getElementById("option_kb_edit").value;
  147.  
  148. userOptions.kb_top =
  149. document.getElementById("option_kb_top").value;
  150. //dialog keybinds
  151. userOptions.m_dialog =
  152. document.getElementById("option_m_dialog").value;
  153.  
  154. userOptions.m_posts =
  155. document.getElementById("option_m_posts").value;
  156.  
  157. userOptions.m_comments =
  158. document.getElementById("option_m_comments").value;
  159.  
  160. userOptions.m_subscribed =
  161. document.getElementById("option_m_subscribed").value;
  162.  
  163. userOptions.m_local =
  164. document.getElementById("option_m_local").value;
  165.  
  166. userOptions.m_all =
  167. document.getElementById("option_m_all").value;
  168.  
  169. userOptions.m_frontpage =
  170. document.getElementById("option_m_frontpage").value;
  171.  
  172. userOptions.m_saved =
  173. document.getElementById("option_m_saved").value;
  174.  
  175. userOptions.m_userpage =
  176. document.getElementById("option_m_userpage").value;
  177.  
  178. userOptions.m_inbox =
  179. document.getElementById("option_m_inbox").value;
  180.  
  181. userOptions.m_options =
  182. document.getElementById("option_m_options").value;
  183.  
  184. localStorage.setItem(optionsKey, JSON.stringify(userOptions));
  185. window.location.reload();
  186. }
  187.  
  188. userOptions = getSettingsFromLocalStorage();
  189. return userOptions;
  190. }
  191.  
  192. let settings = options("set");
  193. let vimKeyNavigation = checkedIfTrue(settings.vimKeyNavigation);
  194. let smoothScroll = checkedIfTrue(settings.smoothScroll);
  195. let pageOffset = window.innerHeight * settings.pageOffset / 100;
  196. let scrollPosition = settings.scrollPosition;
  197. let backgroundHex = settings.backgroundHex;
  198.  
  199. // Set selected entry colors
  200. const backgroundColor = `${backgroundHex}`;
  201. const textColor = 'white';
  202.  
  203. // Set navigation keys with keycodes here: https://www.toptal.com/developers/keycode
  204. let nextKey = 'ArrowDown';
  205. let prevKey = 'ArrowUp';
  206. let nextPageKey = 'ArrowRight';
  207. let prevPageKey = 'ArrowLeft';
  208.  
  209. if (vimKeyNavigation) {
  210. nextKey = 'KeyJ';
  211. prevKey = 'KeyK';
  212. nextPageKey = 'KeyL';
  213. prevPageKey = 'KeyH';
  214. }
  215.  
  216. const expandKey = `${settings.kb_expand}`;
  217. const openCommentsKey = `${settings.kb_comments}`;
  218. const openLinkAndCollapseKey = `${settings.kb_openLink}`;
  219. const parentCommentKey = `${settings.kb_parent}`;
  220. const upvoteKey = `${settings.kb_upvote}`;
  221. const downvoteKey = `${settings.kb_downvote}`;
  222. const replyCommKey = `${settings.kb_replyComm}`;
  223. const saveKey = `${settings.kb_save}`;
  224. const contextKey = `${settings.kb_context}`;
  225. const smallerImgKey = `${settings.kb_smallerImg}`;
  226. const biggerImgKey = `${settings.kb_largerImg}`;
  227. const userKey = `${settings.kb_user}`;
  228. const editKey = `${settings.kb_edit}`;
  229. const topKey = `${settings.kb_top}`;
  230. const linkOneKey = 'Digit1';
  231. const linkTwoKey = 'Digit2';
  232. const linkThreeKey = 'Digit3';
  233. const linkFourKey = 'Digit4';
  234. const linkFiveKey = 'Digit5';
  235. const linkSixKey = 'Digit6';
  236. const linkSevenKey = 'Digit7';
  237. const linkEightKey = 'Digit8';
  238. const linkNineKey = 'Digit9';
  239. const linkZeroKey = 'Digit0';
  240.  
  241. const modalPopupKey = `${settings.m_dialog}`;
  242. const modalPostsKey = `${settings.m_posts}`;
  243. const modalCommentsKey = `${settings.m_comments}`;
  244. const modalSubscribedKey = `${settings.m_subscribed}`;
  245. const modalLocalKey = `${settings.m_local}`;
  246. const modalAllKey = `${settings.m_all}`;
  247. const modalFrontpageKey = `${settings.m_frontpage}`;
  248. const modalSavedKey = `${settings.m_saved}`;
  249. const modalProfileKey = `${settings.m_userpage}`;
  250. const modalInboxKey = `${settings.m_inbox}`;
  251. const modalOptionsKey = `${settings.m_options}`;
  252.  
  253. const escapeKey = 'Escape';
  254. let modalMode = 0;
  255. console.log(`modalMode: ${modalMode}`);
  256.  
  257. // Stop arrows from moving the page if not using Vim navigation
  258. window.addEventListener("keydown", function(e) {
  259. if (["ArrowUp", "ArrowDown"].indexOf(e.code) > -1 && !vimKeyNavigation) {
  260. e.preventDefault();
  261. }
  262. }, false);
  263.  
  264. // Remove scroll animations
  265. document.documentElement.style = "scroll-behavior: auto";
  266.  
  267. // Set CSS for selected entry
  268. const css = `
  269. .selected {
  270. background-color: ${backgroundColor} !important;
  271. color: ${textColor};
  272. }`;
  273.  
  274. // dialog box
  275. let myDialog = document.createElement("dialog");
  276. document.body.appendChild(myDialog);
  277. let para = document.createElement("p");
  278. para.innerHTML = `
  279. <h3><b>Frontpage Sort</b></h3>
  280. <p>${modalPostsKey} = Posts</br>
  281. ${modalCommentsKey} = Comments</br>
  282. ${modalSubscribedKey} = Subscribed</br>
  283. ${modalLocalKey} = Local</br>
  284. ${modalAllKey} = all</p>
  285. <h3><b>Go To Page</b></h3>
  286. <p>${modalFrontpageKey} = Frontpage</br>
  287. ${modalSavedKey} = Saved</br>
  288. ${modalProfileKey} = User Profile Page</br>
  289. ${modalInboxKey} = Inbox</br></p>
  290. <h6>${modalOptionsKey} = Options Page</br></br></h6>
  291. `;
  292. myDialog.appendChild(para);
  293. let button = document.createElement("button");
  294. button.classList.add('CLOSEBUTTON1');
  295. button.innerHTML = `Press ESC or ${modalPopupKey} to Close`;
  296. myDialog.appendChild(button);
  297.  
  298. //draw settings page
  299. const odiv = document.createElement("div");
  300. odiv.setAttribute("id", "lkOptions");
  301. odiv.classList.add("lkoptions", "border-secondary", "card");
  302. odiv.innerHTML = `
  303. <h4>Lemmy-keyboard-navigation Options</h4>
  304. </hr>
  305. <div class='table-responsive'>
  306. <table class='table'>
  307. <thead class='pointer'>
  308. <td><b>Save and close settings</b></td>
  309. <td><button id='LKsaveoptionsTop'>Save and Close</button></td>
  310. </tr>
  311. <tr>
  312. <th><h3><b>General</b></h3></th><td><td/>
  313. </thead>
  314. </tr>
  315. <tbody>
  316. <tr>
  317. <td><b>Use Vim key navigation</b><br/>Also known as HJKL navigation.<br/>Uncheck to use arrow keys instead.</td>
  318. <td><input type='checkbox' id='option_vimKeyNavigation' ${vimKeyNavigation} /></td>
  319. </tr>
  320. <tr>
  321. <td><b>Smooth scrolling</b><br/>Scroll smoothly to the current selection.</td>
  322. <td><input type='checkbox' id='option_smoothScroll' ${smoothScroll} /></td>
  323. </tr>
  324. <tr>
  325. <td><b>Page Offset</b><br/>Percent of page to offset selected entry when scrolling.<br/>0-20% recommended<br/>Default: 5</td>
  326. <td><textarea id='option_pageOffset'>${settings.pageOffset}</textarea>%</td>
  327. </tr>
  328. <tr>
  329. <td><b>Scrolling position</b><br/>middle: only scroll the page if selection is near the bottom.<br/>top: always scroll to keep the selection near the top.</td>
  330. <td><select id="option_scrollPosition">
  331. <option value='${settings.scrollPosition}'>${settings.scrollPosition}</option>
  332. <option value='middle'>middle</option>
  333. <option value='top'>top</option>
  334. </select></td>
  335. </tr>
  336. <tr>
  337. <td><b>Selected Hex Code</b><br/>The background color of selected posts/comments.<br/>Default: #373737</td>
  338. <td><textarea id='option_backgroundHex'>${settings.backgroundHex}</textarea></td>
  339. </tr>
  340. <tr>
  341. <td><h3><b>Rebind Keys</b></h3>Set keybinds with keycodes here:<br/><a href='https://www.toptal.com/developers/keycode'>https://www.toptal.com/developers/keycode</a></td><td><td/>
  342. </tr>
  343. <tr>
  344. <tr>
  345. <td><b>Expand/Collapse</b><br/>Expand/collapse both post and comment content.<br/>Default: KeyX</td>
  346. <td><textarea id='option_kb_expand'>${settings.kb_expand}</textarea></td>
  347. </tr>
  348. <tr>
  349. <td><b>Open Comments</b><br/>Go to the comments of a post.<br/>Default: KeyC</td>
  350. <td><textarea id='option_kb_comments'>${settings.kb_comments}</textarea></td>
  351. </tr>
  352. <tr>
  353. <td><b>Open Links</b><br/>Open Links on a post. (can also be used to collapse comments!)<br/>Default: Enter</td>
  354. <td><textarea id='option_kb_openLink'>${settings.kb_openLink}</textarea></td>
  355. </tr>
  356. <tr>
  357. <td><b>Go to Parent Comment</b><br/>Goes one level up the comment chain.<br/>Default: KeyP</td>
  358. <td><textarea id='option_kb_parent'>${settings.kb_parent}</textarea></td>
  359. </tr>
  360. <tr>
  361. <td><b>Upvote</b><br/>:\)<br/>Default: KeyA</td>
  362. <td><textarea id='option_kb_upvote'>${settings.kb_upvote}</textarea></td>
  363. </tr>
  364. <tr>
  365. <td><b>Downvote</b><br/>:\(<br/>Default: KeyZ</td>
  366. <td><textarea id='option_kb_downvote'>${settings.kb_downvote}</textarea></td>
  367. </tr>
  368. <tr>
  369. <td><b>Reply/Go to community</b><br/>Posts: goes to the post's community<br/>Comments: replies to the selected comment<br/>Default: KeyR</td>
  370. <td><textarea id='option_kb_replyComm'>${settings.kb_replyComm}</textarea></td>
  371. </tr>
  372. <tr>
  373. <td><b>Save post/comment</b><br/>Saves the selected post/comment.<br/>Default: KeyS</td>
  374. <td><textarea id='option_kb_save'>${settings.kb_save}</textarea></td>
  375. </tr>
  376. <tr>
  377. <td><b>Get context of comment</b><br/>Goes to the context of the selected comment.<br/>Default: KeyQ</td>
  378. <td><textarea id='option_kb_context'>${settings.kb_context}</textarea></td>
  379. </tr>
  380. <tr>
  381. <td><b>Shrink expanded image</b><br/>Make an expanded image smaller.<br/>Default: Minus</td>
  382. <td><textarea id='option_kb_smallerImg'>${settings.kb_smallerImg}</textarea></td>
  383. </tr>
  384. <tr>
  385. <td><b>Grow expanded image</b><br/>Make an expanded image larger.<br/>Default: Equal</td>
  386. <td><textarea id='option_kb_largerImg'>${settings.kb_largerImg}</textarea></td>
  387. </tr>
  388. <tr>
  389. <td><b>Go to poster's profile</b><br/>Go to the profile of whoever posted the selected post/comment.<br/>Default: KeyU</td>
  390. <td><textarea id='option_kb_user'>${settings.kb_user}</textarea></td>
  391. </tr>
  392. <tr>
  393. <td><b>Edit the selected post/comment</b><br/>It only works on your own posts!<br/>Default: KeyE</td>
  394. <td><textarea id='option_kb_edit'>${settings.kb_edit}</textarea></td>
  395. </tr>
  396. <tr>
  397. <td><b>Scroll to top</b><br/>Scroll to the top of the page.<br/>Default: KeyT</td>
  398. <td><textarea id='option_kb_top'>${settings.kb_top}</textarea></td>
  399. </tr>
  400. <tr>
  401. <td><h3><b>Rebind Dialog Keys</b></h3></td><td><td/>
  402. </tr>
  403. <tr>
  404. <td><b>Open/Close Dialog</b><br/>Default: KeyG</td>
  405. <td><textarea id='option_m_dialog'>${settings.m_dialog}</textarea></td>
  406. </tr>
  407. <tr>
  408. <tr>
  409. <td><h4><b>Frontpage Sort</b></h4></td><td><td/>
  410. </tr>
  411. <td><b>Sort by Posts</b><br/>Default: KeyP</td>
  412. <td><textarea id='option_m_posts'>${settings.m_posts}</textarea></td>
  413. </tr>
  414. <tr>
  415. <td><b>Sort by Comments</b><br/>Default: KeyC</td>
  416. <td><textarea id='option_m_comments'>${settings.m_comments}</textarea></td>
  417. </tr>
  418. <tr>
  419. <td><b>Subscribed Feed</b><br/>Default: Digit1</td>
  420. <td><textarea id='option_m_subscribed'>${settings.m_subscribed}</textarea></td>
  421. </tr>
  422. <tr>
  423. <td><b>Local Feed</b><br/>Default: Digit2</td>
  424. <td><textarea id='option_m_local'>${settings.m_local}</textarea></td>
  425. </tr>
  426. <tr>
  427. <td><b>All Feed</b><br/>Default: Digit3</td>
  428. <td><textarea id='option_m_all'>${settings.m_all}</textarea></td>
  429. </tr>
  430. <tr>
  431. <td><h4><b>Go to page</b></h4></td><td><td/>
  432. </tr>
  433. <tr>
  434. <td><b>Go to Frontpage</b><br/>Default: KeyF</td>
  435. <td><textarea id='option_m_frontpage'>${settings.m_frontpage}</textarea></td>
  436. </tr>
  437. <tr>
  438. <td><b>Go to Saved posts/comments</b><br/>Default: KeyS</td>
  439. <td><textarea id='option_m_saved'>${settings.m_saved}</textarea></td>
  440. </tr>
  441. <tr>
  442. <td><b>Go to Current User's Profile</b><br/>Default: KeyU</td>
  443. <td><textarea id='option_m_userpage'>${settings.m_userpage}</textarea></td>
  444. </tr>
  445. <tr>
  446. <td><b>Go to Inbox</b><br/>Default: KeyI</td>
  447. <td><textarea id='option_m_inbox'>${settings.m_inbox}</textarea></td>
  448. </tr>
  449. <tr>
  450. <td><b>Open options page</b><br/>Default: KeyO</td>
  451. <td><textarea id='option_m_options'>${settings.m_options}</textarea></td>
  452. </tr>
  453. <tr>
  454. <td><b>Save and close settings</b></td>
  455. <td><button id='LKsaveoptions'>Save and Close</button></td>
  456. </tr>
  457. <tr>
  458. <td><b style='color:red;'>WARNING:<br/>The button bellow will reset all your settings to default.<br/>This cannot be undone.</b></td><td></td>
  459. </tr>
  460. <tr>
  461. <td><button id='LKresetoptions'>Reset All Settings</button></td><td></td>
  462. </tr>
  463. </tbody>
  464. </table>
  465. </div>
  466. <hr />
  467. <p>lemmy-keyboard-navigation links:</p>
  468. <a
  469. href='https://github.com/vmavromatis/Lemmy-keyboard-navigation'>Github</a><br/><a
  470. href='https://greasyfork.org/en/scripts/470498-lemmy-keyboard-navigation'>GreasyFork</a><br/><a
  471. href='https://chrome.google.com/webstore/detail/lemmy-keyboard-navigator/lamoeoaekeeklbcekclbceaeafjkdhbi'>Chrome Extension</a><br/></p>
  472. <p>This settings page was taken from the <a href='https://github.com/howdy-tsc/LemmyTools'>LemmyTools</a> Userscript.</p>
  473. `;
  474.  
  475. let styleString = `
  476. .lkoptions {
  477. position: fixed;
  478. min-width: auto;
  479. min-height: auto;
  480. width: auto;
  481. height: 100%;
  482. top: 0;
  483. display: none;
  484. left: 0;
  485. overflow: scroll;
  486. z-index: 1000;
  487. padding: 0.5%;
  488. margin-top:35px;
  489. }
  490. `;
  491. document.head.appendChild(document.createElement("style")).innerHTML = styleString;
  492. document.body.appendChild(odiv); //options
  493.  
  494. document.getElementById("LKsaveoptions").addEventListener("click", (e) => {
  495. e.preventDefault();
  496. options("save");
  497. });
  498. document.getElementById("LKsaveoptionsTop").addEventListener("click", (e) => {
  499. e.preventDefault();
  500. options("save");
  501. });
  502. document.getElementById("LKresetoptions").addEventListener("click", (e) => {
  503. e.preventDefault();
  504. localStorage.clear();
  505. window.location.reload();
  506. });
  507.  
  508. // Global variables
  509. let currentEntry;
  510. let commentBlock;
  511. let entries = [];
  512. let previousUrl = "";
  513. let expand = false;
  514.  
  515. if (typeof GM_addStyle !== "undefined") {
  516. GM_addStyle(css);
  517. } else if (typeof PRO_addStyle !== "undefined") {
  518. PRO_addStyle(css);
  519. } else if (typeof addStyle !== "undefined") {
  520. addStyle(css);
  521. } else {
  522. let node = document.createElement("style");
  523. node.type = "text/css";
  524. node.appendChild(document.createTextNode(css));
  525. let heads = document.getElementsByTagName("head");
  526. if (heads.length > 0) {
  527. heads[0].appendChild(node);
  528. } else {
  529. // no head yet, stick it whereever
  530. document.documentElement.appendChild(node);
  531. }
  532. }
  533.  
  534. const selectedClass = "selected";
  535.  
  536. const targetNode = document.documentElement;
  537. const config = {
  538. childList: true,
  539. subtree: true
  540. };
  541.  
  542. const observer = new MutationObserver(() => {
  543. entries = document.querySelectorAll(".post-listing, .comment-node");
  544.  
  545. if (entries.length > 0) {
  546. if (location.href !== previousUrl) {
  547. previousUrl = location.href;
  548. currentEntry = null;
  549. }
  550. init();
  551. }
  552. });
  553.  
  554. observer.observe(targetNode, config);
  555.  
  556. function init() {
  557. // If jumping to comments
  558. if (window.location.search.includes("scrollToComments=true") &&
  559. entries.length > 1 &&
  560. (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0)
  561. ) {
  562. selectEntry(entries[1], true);
  563. }
  564. // If jumping to comment from anchor link
  565. else if (window.location.pathname.includes("/comment/") &&
  566. (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0)
  567. ) {
  568. const commentId = window.location.pathname.replace("/comment/", "");
  569. const anchoredEntry = document.getElementById("comment-" + commentId);
  570.  
  571. if (anchoredEntry) {
  572. selectEntry(anchoredEntry, true);
  573. }
  574. }
  575. // If no entries yet selected, default to last selected
  576. else if (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0) {
  577. if (sessionStorage.getItem('currentselection') === null) {
  578. selectEntry(entries[0]);
  579. } else {
  580. sessionCurrentEntry("restore");
  581. }
  582. }
  583.  
  584. Array.from(entries).forEach(entry => {
  585. entry.removeEventListener("click", clickEntry, true);
  586. entry.addEventListener('click', clickEntry, true);
  587. });
  588.  
  589. document.removeEventListener("keydown", handleKeyPress, true);
  590. document.addEventListener("keydown", handleKeyPress, true);
  591. }
  592.  
  593. function handleKeyPress(event) {
  594. if (["TEXTAREA", "INPUT"].indexOf(event.target.tagName) > -1 || event.metaKey) {
  595. return;
  596. }
  597.  
  598. switch (modalMode) {
  599. case modalMode = 0:
  600. switch (event.code) {
  601. case nextKey:
  602. case prevKey:
  603. previousKey(event);
  604. break;
  605. case upvoteKey:
  606. upVote();
  607. break;
  608. case downvoteKey:
  609. downVote();
  610. break;
  611. case expandKey:
  612. toggleExpand();
  613. expand = isExpanded() ? true : false;
  614. break;
  615. case smallerImgKey:
  616. imgResize("smaller");
  617. break;
  618. case biggerImgKey:
  619. imgResize("larger");
  620. break;
  621. case saveKey:
  622. save();
  623. break;
  624. case editKey:
  625. edit();
  626. break;
  627. case openCommentsKey:
  628. comments(event);
  629. break;
  630. case modalPopupKey:
  631. goToDialog("open");
  632. break;
  633. case contextKey:
  634. getContext(event);
  635. break;
  636. case replyCommKey:
  637. // allow refresh with Ctrl + R
  638. if (!event.ctrlKey) {
  639. if (window.location.pathname.includes("/post/")) {
  640. reply(event);
  641. } else {
  642. community(event);
  643. }
  644. }
  645. break;
  646. case userKey:
  647. visitUser(event);
  648. break;
  649. case openLinkAndCollapseKey:
  650. if (window.location.pathname.includes("/post/")) {
  651. toggleExpand();
  652. } else {
  653. const linkElement = currentEntry.querySelector(".col.flex-grow-1>p>a");
  654. if (linkElement) {
  655. if (event.shiftKey) {
  656. window.open(linkElement.href);
  657. } else {
  658. linkElement.click();
  659. }
  660. } else {
  661. comments(event);
  662. }
  663. }
  664. break;
  665. case parentCommentKey: {
  666. let targetBlock;
  667. if (currentEntry.classList.contains("ms-1")) {
  668. targetBlock = getPrevEntry(currentEntry);
  669. } else if (currentEntry.parentElement.parentElement.parentElement.nodeName === "LI") {
  670. targetBlock = currentEntry.parentElement.parentElement.parentElement.getElementsByTagName("article")[0];
  671. }
  672. if (targetBlock) {
  673. if (expand) {
  674. collapseEntry();
  675. }
  676. selectEntry(targetBlock, true);
  677. if (expand) {
  678. expandEntry();
  679. }
  680. }
  681. }
  682. break;
  683. case topKey:
  684. window.scrollTo(0, 0);
  685. sessionStorage.setItem('currentselection', 0);
  686. sessionCurrentEntry("restore");
  687. break;
  688. case linkOneKey:
  689. clickLink(1);
  690. break;
  691. case linkTwoKey:
  692. clickLink(2);
  693. break;
  694. case linkThreeKey:
  695. clickLink(3);
  696. break;
  697. case linkFourKey:
  698. clickLink(4);
  699. break;
  700. case linkFiveKey:
  701. clickLink(5);
  702. break;
  703. case linkSixKey:
  704. clickLink(6);
  705. break;
  706. case linkSevenKey:
  707. clickLink(7);
  708. break;
  709. case linkEightKey:
  710. clickLink(8);
  711. break;
  712. case linkNineKey:
  713. clickLink(9);
  714. break;
  715. case linkZeroKey:
  716. clickLink(0);
  717. break;
  718. case nextPageKey:
  719. case prevPageKey: {
  720. const pageButtons = Array.from(document.querySelectorAll(".paginator>button"));
  721.  
  722. if (pageButtons && (document.getElementsByClassName('paginator').length > 0)) {
  723. const buttonText = event.code === nextPageKey ? "Next" : "Prev";
  724. pageButtons.find(btn => btn.innerHTML === buttonText).click();
  725. }
  726. // Jump next block of comments
  727. if (event.code === nextPageKey) {
  728. commentBlock = getNextEntrySameLevel(currentEntry);
  729. }
  730. // Jump previous block of comments
  731. if (event.code === prevPageKey) {
  732. commentBlock = getPrevEntrySameLevel(currentEntry);
  733. }
  734. if (commentBlock) {
  735. if (expand) {
  736. collapseEntry();
  737. }
  738. selectEntry(commentBlock, true);
  739. if (expand) {
  740. expandEntry();
  741. }
  742. }
  743. }
  744. sessionStorage.setItem('currentselection', 0); //reset the selection back to the first post when switching pages
  745. }
  746. break;
  747. case modalMode = 1:
  748. switch (event.code) {
  749. case escapeKey:
  750. modalMode = 0;
  751. console.log(`modalMode: ${modalMode}`);
  752. break;
  753. case modalPopupKey:
  754. goToDialog("close");
  755. break;
  756. case modalSubscribedKey:
  757. let subelement = document.querySelectorAll('[title="Shows the communities you\'ve subscribed to"]')[0];
  758. sessionStorage.setItem('currentselection', 0); //reset the selection to the first post when switching filters
  759. subelement.click();
  760. goToDialog("close");
  761. break;
  762. case modalLocalKey:
  763. let localelement = document.querySelectorAll('[title="Shows only local communities"]')[0];
  764. sessionStorage.setItem('currentselection', 0);
  765. localelement.click();
  766. goToDialog("close");
  767. break;
  768. case modalAllKey:
  769. let allelement = document.querySelectorAll('[title="Shows all communities, including federated ones"]')[0];
  770. sessionStorage.setItem('currentselection', 0);
  771. allelement.click();
  772. goToDialog("close");
  773. break;
  774. case modalSavedKey:
  775. if (window.location.pathname.includes("/u/")) {
  776. let savedelement = document.getElementsByClassName("btn btn-outline-secondary pointer")[3];
  777. if (savedelement) {
  778. savedelement.click();
  779. goToDialog("close");
  780. }
  781. } else {
  782. instanceAndUser("saved");
  783. }
  784. break;
  785. case modalFrontpageKey:
  786. frontpage();
  787. break;
  788. case modalProfileKey:
  789. let profileelement = document.querySelectorAll('[title="Profile"]')[0];
  790. if (profileelement) {
  791. profileelement.click();
  792. goToDialog("close");
  793. } else {
  794. instanceAndUser("profile");
  795. }
  796. break;
  797. case modalInboxKey:
  798. let notifelement = document.getElementsByClassName("nav-link d-inline-flex align-items-center d-md-inline-block")[2];
  799. if (notifelement) {
  800. notifelement.click();
  801. goToDialog("close");
  802. } else {
  803. window.location.replace(window.location.origin + "/login");
  804. }
  805. break;
  806. case modalCommentsKey:
  807. let commentsbutton = document.getElementsByClassName("pointer btn btn-outline-secondary")[1];
  808. sessionStorage.setItem('currentselection', 0);
  809. commentsbutton.click();
  810. goToDialog("close");
  811. break;
  812. case modalPostsKey:
  813. let postsbutton = document.getElementsByClassName("pointer btn btn-outline-secondary")[0];
  814. sessionStorage.setItem('currentselection', 0);
  815. postsbutton.click();
  816. goToDialog("close");
  817. break;
  818. case modalOptionsKey:
  819. options("open");
  820. goToDialog("close");
  821. break;
  822. }
  823. }
  824. }
  825.  
  826. function getNextEntry(e) {
  827. const currentEntryIndex = Array.from(entries).indexOf(e);
  828.  
  829. if (currentEntryIndex + 1 >= entries.length) {
  830. return e;
  831. }
  832. return entries[currentEntryIndex + 1];
  833. }
  834.  
  835. function getPrevEntry(e) {
  836. const currentEntryIndex = Array.from(entries).indexOf(e);
  837.  
  838. if (currentEntryIndex - 1 < 0) {
  839. return e;
  840. }
  841. return entries[currentEntryIndex - 1];
  842. }
  843.  
  844. function getNextEntrySameLevel(e) {
  845. const nextSibling = e.parentElement.nextElementSibling;
  846.  
  847. if (!nextSibling || nextSibling.getElementsByTagName("article").length < 1) {
  848. return getNextEntry(e);
  849. }
  850.  
  851. return nextSibling.getElementsByTagName("article")[0];
  852. }
  853.  
  854. function getPrevEntrySameLevel(e) {
  855. const prevSibling = e.parentElement.previousElementSibling;
  856.  
  857. if (!prevSibling || prevSibling.getElementsByTagName("article").length < 1) {
  858. return getPrevEntry(e);
  859. }
  860.  
  861. return prevSibling.getElementsByTagName("article")[0];
  862. }
  863.  
  864. function clickEntry(event) {
  865. const e = event.currentTarget;
  866. const target = event.target;
  867.  
  868. // Deselect if already selected, also ignore if clicking on any link/button
  869. if (e === currentEntry && e.classList.contains(selectedClass) &&
  870. !(
  871. target.tagName.toLowerCase() === "button" || target.tagName.toLowerCase() === "a" ||
  872. target.parentElement.tagName.toLowerCase() === "button" ||
  873. target.parentElement.tagName.toLowerCase() === "a" ||
  874. target.parentElement.parentElement.tagName.toLowerCase() === "button" ||
  875. target.parentElement.parentElement.tagName.toLowerCase() === "a"
  876. )
  877. ) {
  878. e.classList.remove(selectedClass);
  879. } else {
  880. selectEntry(e);
  881. }
  882. }
  883.  
  884. function selectEntry(e, scrollIntoView = false) {
  885. if (currentEntry) {
  886. currentEntry.classList.remove(selectedClass);
  887. let linkNumber = currentEntry.querySelectorAll(".linkNumber");
  888. if (linkNumber) {
  889. for (const link of linkNumber) {
  890. link.remove();
  891. }
  892. }
  893. }
  894. currentEntry = e;
  895. currentEntry.classList.add(selectedClass);
  896. sessionCurrentEntry("save");
  897. let links = currentEntry.getElementsByClassName("md-div")[0];
  898. if (links) {
  899. let alink = links.querySelectorAll('a');
  900. if (alink.length > 0) {
  901. alink.forEach(function(value, i) {
  902. let linkNumber = document.createElement("span");
  903. linkNumber.classList.add("linkNumber");
  904. linkNumber.style.fontSize = "9px";
  905. linkNumber.style.lineHeight = 0;
  906. linkNumber.style.verticalAlign = "super";
  907. linkNumber.setAttribute("data-text", `[${i+1}]`);
  908. linkNumber.innerText = `[${i+1}]`;
  909. linkNumber.title = `Press ${i+1} to open link`;
  910. if (i <= 9) {
  911. value.appendChild(linkNumber);
  912. }
  913. });
  914. }
  915. }
  916.  
  917. if (scrollIntoView) {
  918. scrollIntoViewWithOffset(e, pageOffset);
  919. }
  920. }
  921.  
  922. function sessionCurrentEntry(n) {
  923. const sessionEntry = sessionStorage.getItem('currentselection');
  924. const currentEntryIndex = Array.from(entries).indexOf(currentEntry);
  925.  
  926. if (n === "save") {
  927. if (document.querySelector(".home")) {
  928. sessionStorage.setItem('currentselection', currentEntryIndex);
  929. }
  930. } else if (n === "restore") {
  931. selectEntry(entries[sessionEntry]);
  932. console.log(`Set to entry ${sessionEntry}`);
  933. }
  934. }
  935.  
  936. function clickLink(n) {
  937. let links = currentEntry.getElementsByClassName("md-div")[0];
  938. let alink = links.querySelectorAll('a');
  939. if (n === 1) {
  940. window.open(
  941. alink[0].href
  942. );
  943. } else if (n === 2) {
  944. window.open(
  945. alink[1].href
  946. );
  947. } else if (n === 3) {
  948. window.open(
  949. alink[2].href
  950. );
  951. } else if (n === 4) {
  952. window.open(
  953. alink[3].href
  954. );
  955. } else if (n === 5) {
  956. window.open(
  957. alink[4].href
  958. );
  959. } else if (n === 6) {
  960. window.open(
  961. alink[5].href
  962. );
  963. } else if (n === 7) {
  964. window.open(
  965. alink[6].href
  966. );
  967. } else if (n === 8) {
  968. window.open(
  969. alink[7].href
  970. );
  971. } else if (n === 9) {
  972. window.open(
  973. alink[8].href
  974. );
  975. } else if (n === 0) {
  976. window.open(
  977. alink[9].href
  978. );
  979. }
  980. }
  981.  
  982. function isExpanded() {
  983. if (
  984. currentEntry.querySelector("a.d-inline-block:not(.thumbnail)") ||
  985. currentEntry.querySelector("#postContent") ||
  986. currentEntry.querySelector(".card-body")
  987. ) {
  988. return true;
  989. }
  990.  
  991. return false;
  992. }
  993.  
  994. function previousKey(event) {
  995. let selectedEntry;
  996. // Next button
  997. if (event.code === nextKey) {
  998. if (event.shiftKey && vimKeyNavigation) {
  999. selectedEntry = getNextEntrySameLevel(currentEntry);
  1000.  
  1001. } else {
  1002. selectedEntry = getNextEntry(currentEntry);
  1003. }
  1004. }
  1005. // Previous button
  1006. if (event.code === prevKey) {
  1007. if (event.shiftKey && vimKeyNavigation) {
  1008. selectedEntry = getPrevEntrySameLevel(currentEntry);
  1009.  
  1010. } else {
  1011. selectedEntry = getPrevEntry(currentEntry);
  1012. }
  1013. }
  1014. if (selectedEntry) {
  1015. if (expand) {
  1016. collapseEntry();
  1017. }
  1018. selectEntry(selectedEntry, true);
  1019. if (expand) {
  1020. expandEntry();
  1021. }
  1022. }
  1023. }
  1024.  
  1025. function upVote() {
  1026. const upvoteButton = currentEntry.querySelector("button[aria-label='Upvote']");
  1027.  
  1028. if (upvoteButton) {
  1029. upvoteButton.click();
  1030. }
  1031. }
  1032.  
  1033. function downVote() {
  1034. const downvoteButton = currentEntry.querySelector("button[aria-label='Downvote']");
  1035.  
  1036. if (downvoteButton) {
  1037. downvoteButton.click();
  1038. }
  1039. }
  1040.  
  1041. function goToDialog(n) {
  1042.  
  1043. const closeButton = document.getElementsByClassName("CLOSEBUTTON1")[0];
  1044. closeButton.addEventListener("click", () => {
  1045. myDialog.close();
  1046. modalMode = 0;
  1047. console.log(`modalMode: ${modalMode}`);
  1048. });
  1049. if (n === "open") {
  1050. myDialog.showModal();
  1051. modalMode = 1;
  1052. console.log(`modalMode: ${modalMode}`);
  1053. }
  1054.  
  1055. if (n === "close") {
  1056. myDialog.close();
  1057. modalMode = 0;
  1058. console.log(`modalMode: ${modalMode}`);
  1059. }
  1060. }
  1061.  
  1062. function instanceAndUser(n) {
  1063. let currentInstance = window.location.origin;
  1064. let dropdownUser = document.getElementsByClassName("btn dropdown-toggle")[0];
  1065. let username;
  1066. if (dropdownUser) {
  1067. username = dropdownUser.textContent;
  1068. }
  1069. if (n === "profile") {
  1070. if (username) {
  1071. let userlink = currentInstance + "/u/" + username;
  1072. window.location.replace(userlink);
  1073. } else {
  1074. window.location.replace(currentInstance + "/login");
  1075. }
  1076. }
  1077. if (n === "saved") {
  1078. if (username) {
  1079. let savedlink = currentInstance + "/u/" + username + "?page=1&sort=New&view=Saved";
  1080. window.location.replace(savedlink);
  1081. } else {
  1082. window.location.replace(currentInstance + "/login");
  1083. }
  1084. }
  1085. }
  1086.  
  1087. function frontpage() {
  1088. let homeElement = document.getElementsByClassName("d-flex align-items-center navbar-brand me-md-3 active")[0];
  1089. if (homeElement) {
  1090. homeElement.click();
  1091. goToDialog("close");
  1092. } else {
  1093. window.location.replace(window.location.origin);
  1094. }
  1095. }
  1096.  
  1097. function reply(event) {
  1098. const replyButton = currentEntry.querySelector("button[data-tippy-content='reply']");
  1099.  
  1100. if (replyButton) {
  1101. event.preventDefault();
  1102. replyButton.click();
  1103. }
  1104. }
  1105.  
  1106. function community(event) {
  1107. if (event.shiftKey) {
  1108. window.open(
  1109. currentEntry.querySelector("a.community-link").href
  1110. );
  1111. } else {
  1112. currentEntry.querySelector("a.community-link").click();
  1113. }
  1114. }
  1115.  
  1116. function visitUser(event) {
  1117. if (event.shiftKey) {
  1118. window.open(
  1119. currentEntry.getElementsByClassName("person-listing d-inline-flex align-items-baseline text-info")[0].href
  1120. );
  1121. } else {
  1122. currentEntry.getElementsByClassName("person-listing d-inline-flex align-items-baseline text-info")[0].click();
  1123. }
  1124. }
  1125.  
  1126. function comments(event) {
  1127. if (event.shiftKey) {
  1128. window.open(
  1129. currentEntry.querySelector("a.btn[title*='Comment']").href
  1130. );
  1131. } else {
  1132. currentEntry.querySelector("a.btn[title*='Comment']").click();
  1133. }
  1134. }
  1135.  
  1136. function getContext(event) {
  1137. if (event.shiftKey) {
  1138. window.open(
  1139. currentEntry.getElementsByClassName("btn btn-link btn-animate text-muted btn-sm")[0].href
  1140. );
  1141. } else {
  1142. currentEntry.getElementsByClassName("btn btn-link btn-animate text-muted btn-sm")[0].click();
  1143. }
  1144. }
  1145.  
  1146. let maxSize = 0;
  1147.  
  1148. function imgResize(n) {
  1149. let expandedImg = currentEntry.getElementsByClassName("overflow-hidden pictrs-image img-fluid img-expanded slight-radius")[0];
  1150. let expandedHeight = expandedImg.height;
  1151. let expandedWidth = expandedImg.width;
  1152. let expandedHeightbefore = expandedHeight;
  1153. let expandedWidthbefore = expandedWidth;
  1154.  
  1155. if (n === "smaller") {
  1156. expandedHeight = expandedHeight / 1.15;
  1157. expandedWidth = expandedWidth / 1.15;
  1158. expandedImg.style.height = expandedHeight + 'px';
  1159. expandedImg.style.width = expandedWidth + 'px';
  1160. maxSize = 0;
  1161. console.log(`maxSize: ${maxSize}`);
  1162. }
  1163.  
  1164. if (n === "larger") {
  1165. expandedHeight = expandedHeight * 1.15;
  1166. expandedWidth = expandedWidth * 1.15;
  1167. expandedImg.style.width = expandedWidth + 'px';
  1168. expandedImg.style.height = expandedHeight + 'px';
  1169.  
  1170. if (maxSize === 1) {
  1171. expandedImg.style.width = expandedWidthbefore + 'px';
  1172. expandedImg.style.height = expandedHeightbefore + 'px';
  1173. }
  1174. if (expandedImg.width !== Math.round(expandedWidth) || expandedImg.height !== Math.round(expandedHeight)) {
  1175. maxSize = 1;
  1176. console.log(`maxSize: ${maxSize}`);
  1177. }
  1178. }
  1179. }
  1180.  
  1181. function save() {
  1182. const saveButton = currentEntry.querySelector("button[aria-label='save']");
  1183. const unsaveButton = currentEntry.querySelector("button[aria-label='unsave']");
  1184. const moreButton = currentEntry.querySelector("button[aria-label='more']");
  1185. if (saveButton) {
  1186. saveButton.click();
  1187. } else if (unsaveButton) {
  1188. unsaveButton.click();
  1189. } else {
  1190. moreButton.click();
  1191. if (saveButton) {
  1192. saveButton.click();
  1193. } else if (unsaveButton) {
  1194. unsaveButton.click();
  1195. }
  1196. }
  1197. }
  1198.  
  1199. function edit() {
  1200. let editButton = currentEntry.querySelector("button[aria-label='Edit']");
  1201. let moreButton = currentEntry.querySelector("button[aria-label='more']");
  1202.  
  1203. if (editButton) {
  1204. editButton.click();
  1205. } else {
  1206. moreButton.click();
  1207. }
  1208. }
  1209.  
  1210. function toggleExpand() {
  1211. const expandButton = currentEntry.querySelector("button[aria-label='Expand here']");
  1212. const textExpandButton = currentEntry.querySelector(".post-title>button");
  1213. const commentExpandButton = currentEntry.querySelector(".ms-2>div>button");
  1214. const moreExpandButton = currentEntry.querySelector(".ms-1>button");
  1215.  
  1216. if (expandButton) {
  1217. expandButton.click();
  1218.  
  1219. // Scroll into view if picture/text preview cut off
  1220. const imgContainer = currentEntry.querySelector("a.d-inline-block");
  1221.  
  1222. if (imgContainer) {
  1223. // Check container positions once image is loaded
  1224. imgContainer.querySelector("img").addEventListener("load", function() {
  1225. scrollIntoViewWithOffset(imgContainer, pageOffset);
  1226. }, true);
  1227. currentEntry.getElementsByClassName("offset-sm-3 my-2 d-none d-sm-block")[0].className = "my-2 d-none d-sm-block";
  1228. }
  1229. }
  1230.  
  1231. if (textExpandButton) {
  1232. textExpandButton.click();
  1233.  
  1234. const textContainers = [currentEntry.querySelector("#postContent"), currentEntry.querySelector(".card-body")];
  1235. textContainers.forEach(container => {
  1236. if (container) {
  1237. scrollIntoViewWithOffset(container, pageOffset);
  1238. }
  1239. });
  1240. }
  1241.  
  1242. if (commentExpandButton) {
  1243. commentExpandButton.click();
  1244. }
  1245.  
  1246. if (moreExpandButton) {
  1247. moreExpandButton.click();
  1248. selectEntry(getPrevEntry(currentEntry), true);
  1249. }
  1250. }
  1251.  
  1252. function expandEntry() {
  1253. if (!isExpanded()) {
  1254. toggleExpand();
  1255. }
  1256. }
  1257.  
  1258. function collapseEntry() {
  1259. if (isExpanded()) {
  1260. toggleExpand();
  1261. }
  1262. }
  1263.  
  1264. function scrollIntoViewWithOffset(e, offset) {
  1265. const y = e.getBoundingClientRect().top + window.scrollY - offset;
  1266. if (scrollPosition === "middle") {
  1267. if (e.getBoundingClientRect().top < 0 ||
  1268. e.getBoundingClientRect().bottom > window.innerHeight
  1269. ) {
  1270. scrollPage(y);
  1271. }
  1272. } else if (scrollPosition === "top") {
  1273. scrollPage(y);
  1274. }
  1275. }
  1276.  
  1277. function scrollPage(y) {
  1278. if (smoothScroll) {
  1279. window.scrollTo({
  1280. top: y,
  1281. behavior: "smooth"
  1282. });
  1283. } else {
  1284. window.scrollTo({
  1285. top: y
  1286. });
  1287. }
  1288. }
  1289.  
  1290. }