The version of vichan running on lainchan.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
11KB

  1. // author: joakimoa
  2. // keyboard navigation
  3. // v1.4
  4. $(document).on("ready", function() {
  5. // adding keyboard navigation to options menu
  6. if (window.Options && Options.get_tab('general')) {
  7. Options.extend_tab("general",
  8. "<fieldset><legend> Keyboard Navigation </legend>" +
  9. ("<label class='keyboardnav' id='keyboardnav' style='padding:0px;'><input type='checkbox' /> Enable Keyboard Navigation</label>") +
  10. "<table><tr><td>Action</td><td>Key (a-z)</td></tr>" +
  11. "<tr><td>Next Reply</td><td><input class='field' name='next-reply-input' spellcheck='false'></td></tr>" +
  12. "<tr><td>Previous Reply</td><td><input class='field' name='previous-reply-input' spellcheck='false'></td></tr>" +
  13. "<tr><td>Expand File</td><td><input class='field' name='expando-input' spellcheck='false'></td></tr>" +
  14. "<tr><td>Refresh Thread</td><td><input class='field' name='refresh-thread-input' spellcheck='false'></td></tr>" +
  15. "</table></fieldset>");
  16. }
  17. $('.keyboardnav').on('change', function(){
  18. var setting = $(this).attr('id');
  19. localStorage[setting] = $(this).children('input').is(':checked');
  20. });
  21. var nextReplyKeycode = 74; // j
  22. var previousReplyKeycode = 75; // k
  23. var expandoKeycode = 69; // e
  24. var refreshThreadKeycode = 82; // r
  25. if (!localStorage.keyboardnav) {
  26. localStorage.keyboardnav = 'false';
  27. }
  28. if (!localStorage["next.reply.key"]) {
  29. localStorage["next.reply.key"] = nextReplyKeycode;
  30. }
  31. if (!localStorage["previous.reply.key"]) {
  32. localStorage["previous.reply.key"] = previousReplyKeycode;
  33. }
  34. if (!localStorage["expando.key"]) {
  35. localStorage["expando.key"] = expandoKeycode;
  36. }
  37. if (!localStorage["refresh.thread.key"]) {
  38. localStorage["refresh.thread.key"] = refreshThreadKeycode;
  39. }
  40. // getting locally stored setting
  41. function getSetting(key) {
  42. return (localStorage[key] == 'true');
  43. }
  44. function isKeySet(key) {
  45. return (localStorage[key] !== false);
  46. }
  47. var nextReplyInput = document.getElementsByName("next-reply-input")[0];
  48. var previousReplyInput = document.getElementsByName("previous-reply-input")[0];
  49. var expandoInput = document.getElementsByName("expando-input")[0];
  50. var refreshThreadInput = document.getElementsByName("refresh-thread-input")[0];
  51. if (getSetting('keyboardnav')) $('#keyboardnav>input').prop('checked', 'checked');
  52. if (isKeySet('next.reply.key')) {
  53. nextReplyKeycode = localStorage["next.reply.key"];
  54. nextReplyInput.value = nextReplyKeycode;
  55. }
  56. if (isKeySet('previous.reply.key')) {
  57. previousReplyKeycode = localStorage["previous.reply.key"];
  58. previousReplyInput.value = previousReplyKeycode;
  59. }
  60. if (isKeySet('expando.key')) {
  61. expandoKeycode = localStorage["expando.key"];
  62. expandoInput.value = expandoKeycode;
  63. }
  64. if (isKeySet('refresh.thread.key')) {
  65. refreshThreadKeycode = localStorage["refresh.thread.key"];
  66. refreshThreadInput.value = refreshThreadKeycode;
  67. }
  68. nextReplyInput.value = String.fromCharCode(nextReplyKeycode);
  69. previousReplyInput.value = String.fromCharCode(previousReplyKeycode);
  70. expandoInput.value = String.fromCharCode(expandoKeycode);
  71. refreshThreadInput.value = String.fromCharCode(refreshThreadKeycode);
  72. nextReplyInput.addEventListener("keyup", changeNextReplyKey, false);
  73. previousReplyInput.addEventListener("keyup", changePreviousReplyKey, false);
  74. expandoInput.addEventListener("keyup", changeExpandoKey, false);
  75. refreshThreadInput.addEventListener("keyup", changeRefreshThreadKey, false);
  76. function changeNextReplyKey(e) {
  77. nextReplyInput.value = "";
  78. if (e.keyCode >= 65 && e.keyCode <= 90) {
  79. nextReplyInput.value = String.fromCharCode(e.keyCode);
  80. localStorage["next.reply.key"] = e.keyCode;
  81. }
  82. }
  83. function changePreviousReplyKey(e) {
  84. previousReplyInput.value = "";
  85. if (e.keyCode >= 65 && e.keyCode <= 90) {
  86. previousReplyInput.value = String.fromCharCode(e.keyCode);
  87. localStorage["previous.reply.key"] = e.keyCode;
  88. }
  89. }
  90. function changeExpandoKey(e) {
  91. expandoInput.value = "";
  92. if (e.keyCode >= 65 && e.keyCode <= 90) {
  93. expandoInput.value = String.fromCharCode(e.keyCode);
  94. localStorage["expando.key"] = e.keyCode;
  95. }
  96. }
  97. function changeRefreshThreadKey(e) {
  98. refreshThreadInput.value = "";
  99. if (e.keyCode >= 65 && e.keyCode <= 90) {
  100. refreshThreadInput.value = String.fromCharCode(e.keyCode);
  101. localStorage["refresh.thread.key"] = e.keyCode;
  102. }
  103. }
  104. // loads the main function
  105. function loadKeyboardNav() {
  106. var replies = document.getElementsByClassName("post reply");
  107. var current_file = null;
  108. var default_color = "#333";
  109. var highlight_color = "#555";
  110. // grabs base and highlight colors
  111. if (replies.length > 0) {
  112. if (replies[0].classList.contains("highlighted")) {
  113. replies[0].classList.remove('highlighted');
  114. default_color = window.getComputedStyle(replies[0], null).getPropertyValue("background-color");
  115. replies[0].classList.add('highlighted');
  116. highlight_color = window.getComputedStyle(replies[0], null).getPropertyValue("background-color");
  117. } else {
  118. default_color = window.getComputedStyle(replies[0], null).getPropertyValue("background-color");
  119. replies[0].classList.add('highlighted');
  120. highlight_color = window.getComputedStyle(replies[0], null).getPropertyValue("background-color");
  121. replies[0].classList.remove('highlighted');
  122. }
  123. }
  124. // check if user is in textareas where hotkeys needs to be disabled
  125. var text_input_focused = false;
  126. function checkFocused () {
  127. var el = document.activeElement;
  128. if (el && (el.tagName.toLowerCase() == 'input' && el.type == 'text' ||
  129. el.tagName.toLowerCase() == 'textarea')) {
  130. text_input_focused = true;
  131. } else {
  132. text_input_focused = false;
  133. }
  134. }
  135. document.addEventListener('focus',function(e){
  136. checkFocused();
  137. }, true);
  138. document.addEventListener('blur',function(e){
  139. text_input_focused = false;
  140. }, true);
  141. // strips out <a href="" class="file"> tags
  142. function getFileList(e) {
  143. var arr = [];
  144. var e = e.getElementsByClassName("file");
  145. if (e.length > 0) {
  146. for (i = 0; i < e.length; i++) {
  147. if (e[i].tagName === "DIV") {
  148. arr.push(e[i]);
  149. }
  150. }
  151. }
  152. return arr;
  153. }
  154. var reply_indexx = -1; // might change back to 0
  155. var image_indexx = -1;
  156. function focusNextReply() {
  157. if (reply_indexx < replies.length-1) {
  158. reply_indexx++;
  159. image_indexx = -1;
  160. var images = getFileList(replies[reply_indexx]);
  161. if (images.length !== 0) {
  162. focusNextImage();
  163. } else {
  164. scrollTo(replies[reply_indexx], true);
  165. }
  166. }
  167. }
  168. function focusNextImage() {
  169. var images = getFileList(replies[reply_indexx]);
  170. if (images.length === 0) {
  171. focusNextReply();
  172. } else {
  173. image_indexx++;
  174. if (image_indexx > images.length-1) {
  175. image_indexx = 0;
  176. focusNextReply();
  177. } else {
  178. var im = images[image_indexx].getElementsByClassName("full-image");
  179. if (im.length === 0) {
  180. im = images[image_indexx].getElementsByClassName("post-image");
  181. }
  182. scrollTo(im[0], true);
  183. }
  184. }
  185. }
  186. function focusPreviousReply() {
  187. if (reply_indexx > 0) {
  188. reply_indexx--;
  189. var images = getFileList(replies[reply_indexx]);
  190. image_indexx = images.length;
  191. if (images.length !== 0) {
  192. focusPreviousImage();
  193. } else {
  194. image_indexx = -1;
  195. scrollTo(replies[reply_indexx], false);
  196. }
  197. }
  198. }
  199. function focusPreviousImage() {
  200. var images = getFileList(replies[reply_indexx]);
  201. if (images.length === 0) {
  202. focusPreviousReply();
  203. } else {
  204. image_indexx--;
  205. if (image_indexx < 0) {
  206. image_indexx = 0;
  207. focusPreviousReply();
  208. } else {
  209. var im = images[image_indexx].getElementsByClassName("full-image");
  210. if (im.length === 0) {
  211. im = images[image_indexx].getElementsByClassName("post-image")
  212. }
  213. scrollTo(im[0], false);
  214. }
  215. }
  216. }
  217. // from https://gist.github.com/jjmu15/8646226
  218. function isInViewport(element) {
  219. var rect = element.getBoundingClientRect();
  220. var html = document.documentElement;
  221. return (
  222. rect.top >= 0 &&
  223. rect.left >= 0 &&
  224. rect.bottom <= (window.innerHeight || html.clientHeight) &&
  225. rect.right <= (window.innerWidth || html.clientWidth)
  226. );
  227. }
  228. function scrollTo(e, direction_down) {
  229. if (current_file !== null && !current_file.classList.contains("highlighted")) {
  230. current_file.style.backgroundColor = default_color;
  231. }
  232. current_file = e;
  233. if (!isInViewport(e)) {
  234. if (direction_down) {
  235. e.scrollIntoView(false);
  236. window.scrollBy(0, 30);
  237. } else {
  238. e.scrollIntoView();
  239. window.scrollBy(0, -30);
  240. }
  241. }
  242. e.style.backgroundColor = highlight_color;
  243. }
  244. function expandFile() {
  245. var imgg = replies[reply_indexx].getElementsByClassName("post-image");
  246. if (imgg.length > 0 && image_indexx > -1) {
  247. imgg[image_indexx].click();
  248. }
  249. }
  250. // input
  251. window.addEventListener("keydown", checkKeyPressed, false);
  252. function checkKeyPressed(e) {
  253. if (!text_input_focused) {
  254. replies = document.getElementsByClassName("post reply"); // if new ones via AJAX
  255. if (e.keyCode == nextReplyKeycode) {
  256. if (reply_indexx === -1) { // needed for initial condition
  257. focusNextReply();
  258. } else {
  259. focusNextImage();
  260. }
  261. } else if (e.keyCode == previousReplyKeycode) {
  262. focusPreviousImage();
  263. } else if (e.keyCode == expandoKeycode) {
  264. expandFile();
  265. } else if (e.keyCode == refreshThreadKeycode) {
  266. document.getElementById("update_thread").click();
  267. }
  268. }
  269. }
  270. }
  271. // loads main function if checkbox toggled and in a thread with replies
  272. if (getSetting('keyboardnav') && document.getElementsByClassName("thread").length === 1 && document.getElementsByClassName("post reply").length > 0) {
  273. loadKeyboardNav();
  274. }
  275. });