Lobster Keys

Keybindings for lobste.rs

  1. // ==UserScript==
  2. // @name Lobster Keys
  3. // @description Keybindings for lobste.rs
  4. // @namespace https://rodaine.com
  5. // @source https://github.com/rodaine/lobster-keys.git
  6. // @version 0.1.0
  7. // @author Chris Roche <github@rodaine.com>
  8. // @match https://lobste.rs/*
  9. // @grant none
  10. // @noframes
  11. // ==/UserScript==
  12.  
  13. /******/ (function(modules) { // webpackBootstrap
  14. /******/ // The module cache
  15. /******/ var installedModules = {};
  16. /******/
  17. /******/ // The require function
  18. /******/ function __webpack_require__(moduleId) {
  19. /******/
  20. /******/ // Check if module is in cache
  21. /******/ if(installedModules[moduleId]) {
  22. /******/ return installedModules[moduleId].exports;
  23. /******/ }
  24. /******/ // Create a new module (and put it into the cache)
  25. /******/ var module = installedModules[moduleId] = {
  26. /******/ i: moduleId,
  27. /******/ l: false,
  28. /******/ exports: {}
  29. /******/ };
  30. /******/
  31. /******/ // Execute the module function
  32. /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  33. /******/
  34. /******/ // Flag the module as loaded
  35. /******/ module.l = true;
  36. /******/
  37. /******/ // Return the exports of the module
  38. /******/ return module.exports;
  39. /******/ }
  40. /******/
  41. /******/
  42. /******/ // expose the modules object (__webpack_modules__)
  43. /******/ __webpack_require__.m = modules;
  44. /******/
  45. /******/ // expose the module cache
  46. /******/ __webpack_require__.c = installedModules;
  47. /******/
  48. /******/ // define getter function for harmony exports
  49. /******/ __webpack_require__.d = function(exports, name, getter) {
  50. /******/ if(!__webpack_require__.o(exports, name)) {
  51. /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
  52. /******/ }
  53. /******/ };
  54. /******/
  55. /******/ // define __esModule on exports
  56. /******/ __webpack_require__.r = function(exports) {
  57. /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
  58. /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  59. /******/ }
  60. /******/ Object.defineProperty(exports, '__esModule', { value: true });
  61. /******/ };
  62. /******/
  63. /******/ // create a fake namespace object
  64. /******/ // mode & 1: value is a module id, require it
  65. /******/ // mode & 2: merge all properties of value into the ns
  66. /******/ // mode & 4: return value when already ns object
  67. /******/ // mode & 8|1: behave like require
  68. /******/ __webpack_require__.t = function(value, mode) {
  69. /******/ if(mode & 1) value = __webpack_require__(value);
  70. /******/ if(mode & 8) return value;
  71. /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
  72. /******/ var ns = Object.create(null);
  73. /******/ __webpack_require__.r(ns);
  74. /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
  75. /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
  76. /******/ return ns;
  77. /******/ };
  78. /******/
  79. /******/ // getDefaultExport function for compatibility with non-harmony modules
  80. /******/ __webpack_require__.n = function(module) {
  81. /******/ var getter = module && module.__esModule ?
  82. /******/ function getDefault() { return module['default']; } :
  83. /******/ function getModuleExports() { return module; };
  84. /******/ __webpack_require__.d(getter, 'a', getter);
  85. /******/ return getter;
  86. /******/ };
  87. /******/
  88. /******/ // Object.prototype.hasOwnProperty.call
  89. /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
  90. /******/
  91. /******/ // __webpack_public_path__
  92. /******/ __webpack_require__.p = "";
  93. /******/
  94. /******/
  95. /******/ // Load entry module and return exports
  96. /******/ return __webpack_require__(__webpack_require__.s = 0);
  97. /******/ })
  98. /************************************************************************/
  99. /******/ ([
  100. /* 0 */
  101. /***/ (function(module, exports, __webpack_require__) {
  102.  
  103. "use strict";
  104.  
  105. /**
  106. * Story element on the document.
  107. */
  108. class Story {
  109. constructor(el) {
  110. this._el = el;
  111. }
  112. /**
  113. * Extracts all stories in-order from `d`.
  114. *
  115. * @param d Document
  116. */
  117. static FromDoc(d = window.document) {
  118. let els = d.getElementsByClassName('story');
  119. let out = [];
  120. for (let el of els) {
  121. out.push(new Story(el));
  122. }
  123. return out;
  124. }
  125. /**
  126. * Selects this story, making the keybindings apply to this Story. If a
  127. * different story was previously seelcted, that story should be
  128. * unfocused first.
  129. */
  130. focus() {
  131. this._el.classList.add(Story.focusCls);
  132. let url = this.getAnchor(".u-url" /* URL */);
  133. if (url) {
  134. url.focus();
  135. url.blur();
  136. }
  137. this.scrollIntoView();
  138. }
  139. /**
  140. * Deselects this story.
  141. */
  142. unfocus() {
  143. this._el.classList.remove(Story.focusCls);
  144. }
  145. /**
  146. * Opens the Story's URL
  147. */
  148. open() { this.click(".u-url" /* URL */); }
  149. /**
  150. * Opens the domain search page for the Story's hostname
  151. */
  152. domain() { this.click(".domain" /* Domain */); }
  153. /**
  154. * Opens the Story's author's profile page.
  155. */
  156. author() { this.click(".u-author" /* Author */); }
  157. /**
  158. * Opens the flag dropdown menu, and moves focus to the first option.
  159. */
  160. flag() {
  161. this.click(".flagger" /* Flag */);
  162. let opts = window.document.querySelector('#downvote_why a');
  163. opts && opts.focus();
  164. }
  165. /**
  166. * Toggles whether or not the Story is hidden.
  167. */
  168. hide() { this.click(".hider" /* Hide */); }
  169. /**
  170. * Toggles whether or not the Story is saved.
  171. */
  172. save() { this.click(".saver" /* Save */); }
  173. /**
  174. * Opens the Story's comments page.
  175. */
  176. comments() { this.click(".comments_label a" /* Comments */); }
  177. /**
  178. * Toggles the Story's upvote arrow.
  179. */
  180. upvote() { this.click(".upvoter" /* Upvote */); }
  181. getAnchor(a) {
  182. return this._el.querySelector(a);
  183. }
  184. click(a) {
  185. let anchor = this.getAnchor(a);
  186. anchor && anchor.click();
  187. }
  188. scrollIntoView() {
  189. let bound = this._el.getBoundingClientRect();
  190. if (bound.top < 0
  191. || bound.left < 0
  192. || bound.bottom > (window.innerHeight || document.documentElement.clientHeight)
  193. || bound.right > (window.innerWidth || document.documentElement.clientWidth)) {
  194. return this._el.scrollIntoView(Story.scrollOpts);
  195. }
  196. }
  197. }
  198. /**
  199. * The class name applied to the selected [[Story]]
  200. */
  201. Story.focusCls = 'lobster-keys-focus';
  202. Story.scrollOpts = { block: "nearest" };
  203. /**
  204. * Interaction controller for the keybindings
  205. */
  206. class LobstersKeyController {
  207. /**
  208. * Attaches a controller to `d`, listening for events and injecting styles
  209. * if a [[Story]] list is detected.
  210. *
  211. * @param d Document
  212. */
  213. constructor(d = window.document) {
  214. this.stories = Story.FromDoc(d);
  215. if (this.stories.length > 0) {
  216. document.addEventListener('keyup', (e) => { this.handleKeyUp(e); });
  217. this.attachStyles(d);
  218. }
  219. }
  220. get index() { return this._idx; }
  221. set index(i) {
  222. if (i === undefined) {
  223. this._idx = undefined;
  224. return;
  225. }
  226. if (i < 0) {
  227. i = 0;
  228. }
  229. else if (i >= this.stories.length) {
  230. i = this.stories.length - 1;
  231. }
  232. this._idx = i;
  233. }
  234. get story() {
  235. let i = this.index;
  236. return i === undefined ? undefined : this.stories[i];
  237. }
  238. changeStory(d) {
  239. if (this.index === undefined) {
  240. switch (d) {
  241. case 1 /* Next */:
  242. this.index = 0;
  243. break;
  244. case 0 /* Previous */:
  245. this.index = this.stories.length - 1;
  246. break;
  247. }
  248. }
  249. else {
  250. this.story && this.story.unfocus();
  251. switch (d) {
  252. case 1 /* Next */:
  253. this.index++;
  254. break;
  255. case 0 /* Previous */:
  256. this.index--;
  257. break;
  258. }
  259. }
  260. this.story && this.story.focus();
  261. }
  262. changePage(d) {
  263. switch (d) {
  264. case 0 /* Previous */:
  265. let prev = window.document.querySelector('.morelink a:first-child');
  266. prev && prev.innerText.indexOf('<<') > -1 && prev.click();
  267. break;
  268. case 1 /* Next */:
  269. let next = window.document.querySelector('.morelink a:last-child');
  270. next && next.innerText.indexOf('>>') > -1 && next.click();
  271. break;
  272. }
  273. }
  274. handleKeyUp(e) {
  275. switch (e.code) {
  276. case "KeyJ" /* J */:
  277. return this.changeStory(1 /* Next */);
  278. case "KeyK" /* K */:
  279. return this.changeStory(0 /* Previous */);
  280. case "BracketLeft" /* OpenBracket */:
  281. return this.changePage(0 /* Previous */);
  282. case "BracketRight" /* CloseBracket */:
  283. return this.changePage(1 /* Next */);
  284. }
  285. let story = this.story;
  286. if (story) {
  287. switch (e.code) {
  288. case "Enter" /* Enter */:
  289. return story.open();
  290. case "KeyA" /* A */:
  291. return story.author();
  292. case "KeyC" /* C */:
  293. return story.comments();
  294. case "KeyD" /* D */:
  295. return story.domain();
  296. case "KeyF" /* F */:
  297. return story.flag();
  298. case "KeyH" /* H */:
  299. return story.hide();
  300. case "KeyS" /* S */:
  301. return story.save();
  302. case "KeyU" /* U */:
  303. return story.upvote();
  304. }
  305. }
  306. }
  307. attachStyles(d) {
  308. let styles = d.createElement('style');
  309. styles.innerHTML = `.${Story.focusCls} { background-color: #fffcd799; }`;
  310. d.body.appendChild(styles);
  311. }
  312. }
  313. new LobstersKeyController(window.document);
  314.  
  315.  
  316. /***/ })
  317. /******/ ]);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址