app.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. /*jshint esversion: 6 */
  2. (function() {
  3. 'use strict';
  4. class DomHelper {
  5. static isVisible(element) {
  6. return element.offsetParent !== null;
  7. }
  8. static openNewTab(url) {
  9. let win = window.open("");
  10. win.opener = null;
  11. win.location = url;
  12. win.focus();
  13. }
  14. static scrollPageTo(element) {
  15. let windowScrollPosition = window.pageYOffset;
  16. let windowHeight = document.documentElement.clientHeight;
  17. let viewportPosition = windowScrollPosition + windowHeight;
  18. let itemBottomPosition = element.offsetTop + element.offsetHeight;
  19. if (viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) {
  20. window.scrollTo(0, element.offsetTop - 10);
  21. }
  22. }
  23. static getVisibleElements(selector) {
  24. let elements = document.querySelectorAll(selector);
  25. let result = [];
  26. for (let i = 0; i < elements.length; i++) {
  27. if (this.isVisible(elements[i])) {
  28. result.push(elements[i]);
  29. }
  30. }
  31. return result;
  32. }
  33. static findParent(element, selector) {
  34. for (; element && element !== document; element = element.parentNode) {
  35. if (element.classList.contains(selector)) {
  36. return element;
  37. }
  38. }
  39. return null;
  40. }
  41. }
  42. class TouchHandler {
  43. constructor() {
  44. this.reset();
  45. }
  46. reset() {
  47. this.touch = {
  48. start: {x: -1, y: -1},
  49. move: {x: -1, y: -1},
  50. element: null
  51. };
  52. }
  53. calculateDistance() {
  54. if (this.touch.start.x >= -1 && this.touch.move.x >= -1) {
  55. let horizontalDistance = Math.abs(this.touch.move.x - this.touch.start.x);
  56. let verticalDistance = Math.abs(this.touch.move.y - this.touch.start.y);
  57. if (horizontalDistance > 30 && verticalDistance < 70) {
  58. return this.touch.move.x - this.touch.start.x;
  59. }
  60. }
  61. return 0;
  62. }
  63. findElement(element) {
  64. if (element.classList.contains("touch-item")) {
  65. return element;
  66. }
  67. return DomHelper.findParent(element, "touch-item");
  68. }
  69. onTouchStart(event) {
  70. if (event.touches === undefined || event.touches.length !== 1) {
  71. return;
  72. }
  73. this.reset();
  74. this.touch.start.x = event.touches[0].clientX;
  75. this.touch.start.y = event.touches[0].clientY;
  76. this.touch.element = this.findElement(event.touches[0].target);
  77. }
  78. onTouchMove(event) {
  79. if (event.touches === undefined || event.touches.length !== 1 || this.element === null) {
  80. return;
  81. }
  82. this.touch.move.x = event.touches[0].clientX;
  83. this.touch.move.y = event.touches[0].clientY;
  84. let distance = this.calculateDistance();
  85. let absDistance = Math.abs(distance);
  86. if (absDistance > 0) {
  87. let opacity = 1 - (absDistance > 75 ? 0.9 : absDistance / 75 * 0.9);
  88. let tx = distance > 75 ? 75 : (distance < -75 ? -75 : distance);
  89. this.touch.element.style.opacity = opacity;
  90. this.touch.element.style.transform = "translateX(" + tx + "px)";
  91. }
  92. }
  93. onTouchEnd(event) {
  94. if (event.touches === undefined) {
  95. return;
  96. }
  97. if (this.touch.element !== null) {
  98. let distance = Math.abs(this.calculateDistance());
  99. if (distance > 75) {
  100. EntryHandler.toggleEntryStatus(this.touch.element);
  101. }
  102. this.touch.element.style.opacity = 1;
  103. this.touch.element.style.transform = "none";
  104. }
  105. this.reset();
  106. }
  107. listen() {
  108. let elements = document.querySelectorAll(".touch-item");
  109. elements.forEach((element) => {
  110. element.addEventListener("touchstart", (e) => this.onTouchStart(e), false);
  111. element.addEventListener("touchmove", (e) => this.onTouchMove(e), false);
  112. element.addEventListener("touchend", (e) => this.onTouchEnd(e), false);
  113. element.addEventListener("touchcancel", () => this.reset(), false);
  114. });
  115. }
  116. }
  117. class KeyboardHandler {
  118. constructor() {
  119. this.queue = [];
  120. this.shortcuts = {};
  121. }
  122. on(combination, callback) {
  123. this.shortcuts[combination] = callback;
  124. }
  125. listen() {
  126. document.onkeydown = (event) => {
  127. if (this.isEventIgnored(event)) {
  128. return;
  129. }
  130. let key = this.getKey(event);
  131. this.queue.push(key);
  132. for (let combination in this.shortcuts) {
  133. let keys = combination.split(" ");
  134. if (keys.every((value, index) => value === this.queue[index])) {
  135. this.queue = [];
  136. this.shortcuts[combination]();
  137. return;
  138. }
  139. if (keys.length === 1 && key === keys[0]) {
  140. this.queue = [];
  141. this.shortcuts[combination]();
  142. return;
  143. }
  144. }
  145. if (this.queue.length >= 2) {
  146. this.queue = [];
  147. }
  148. };
  149. }
  150. isEventIgnored(event) {
  151. return event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA";
  152. }
  153. getKey(event) {
  154. const mapping = {
  155. 'Esc': 'Escape',
  156. 'Up': 'ArrowUp',
  157. 'Down': 'ArrowDown',
  158. 'Left': 'ArrowLeft',
  159. 'Right': 'ArrowRight'
  160. };
  161. for (let key in mapping) {
  162. if (mapping.hasOwnProperty(key) && key === event.key) {
  163. return mapping[key];
  164. }
  165. }
  166. return event.key;
  167. }
  168. }
  169. class FormHandler {
  170. static handleSubmitButtons() {
  171. let elements = document.querySelectorAll("form");
  172. elements.forEach((element) => {
  173. element.onsubmit = () => {
  174. let button = document.querySelector("button");
  175. if (button) {
  176. button.innerHTML = button.dataset.labelLoading;
  177. button.disabled = true;
  178. }
  179. };
  180. });
  181. }
  182. }
  183. class MouseHandler {
  184. onClick(selector, callback) {
  185. let elements = document.querySelectorAll(selector);
  186. elements.forEach((element) => {
  187. element.onclick = (event) => {
  188. event.preventDefault();
  189. callback(event);
  190. };
  191. });
  192. }
  193. }
  194. class RequestBuilder {
  195. constructor(url) {
  196. this.callback = null;
  197. this.url = url;
  198. this.options = {
  199. method: "POST",
  200. cache: "no-cache",
  201. credentials: "include",
  202. body: null,
  203. headers: new Headers({
  204. "Content-Type": "application/json",
  205. "X-Csrf-Token": this.getCsrfToken()
  206. })
  207. };
  208. }
  209. withBody(body) {
  210. this.options.body = JSON.stringify(body);
  211. return this;
  212. }
  213. withCallback(callback) {
  214. this.callback = callback;
  215. return this;
  216. }
  217. getCsrfToken() {
  218. let element = document.querySelector("meta[name=X-CSRF-Token]");
  219. if (element !== null) {
  220. return element.getAttribute("value");
  221. }
  222. return "";
  223. }
  224. execute() {
  225. fetch(new Request(this.url, this.options)).then((response) => {
  226. if (this.callback) {
  227. this.callback(response);
  228. }
  229. });
  230. }
  231. }
  232. class UnreadCounterHandler {
  233. static decrement(n) {
  234. this.updateValue((current) => {
  235. return current - n;
  236. });
  237. }
  238. static increment(n) {
  239. this.updateValue((current) => {
  240. return current + n;
  241. });
  242. }
  243. static updateValue(callback) {
  244. let counterElements = document.querySelectorAll("span.unread-counter");
  245. counterElements.forEach((element) => {
  246. let oldValue = parseInt(element.textContent, 10);
  247. element.innerHTML = callback(oldValue);
  248. });
  249. // The titlebar must be updated only on the "Unread" page.
  250. if (window.location.href.endsWith('/unread')) {
  251. // The following 3 lines ensure that the unread count in the titlebar
  252. // is updated correctly when users presses "v".
  253. let oldValue = parseInt(document.title.split('(')[1], 10);
  254. let newValue = callback(oldValue);
  255. // Notes:
  256. // - This will only be executed in the /unread page. Therefore, it
  257. // will not affect titles on other pages.
  258. // - When there are no unread items, user cannot press "v".
  259. // Therefore, we need not handle the case where title is
  260. // "Unread Items - Miniflux". This applies to other cases as well.
  261. // i.e.: if there are no unread items, user cannot decrement or
  262. // increment anything.
  263. document.title = document.title.replace(
  264. /(.*?)\(\d+\)(.*?)/,
  265. function (match, prefix, suffix, offset, string) {
  266. return prefix + '(' + newValue + ')' + suffix;
  267. }
  268. );
  269. }
  270. }
  271. }
  272. class EntryHandler {
  273. static updateEntriesStatus(entryIDs, status, callback) {
  274. let url = document.body.dataset.entriesStatusUrl;
  275. let request = new RequestBuilder(url);
  276. request.withBody({entry_ids: entryIDs, status: status});
  277. request.withCallback(callback);
  278. request.execute();
  279. // The following 5 lines ensure that the unread count in the menu is
  280. // updated correctly when users presses "v".
  281. if (status === "read") {
  282. UnreadCounterHandler.decrement(1);
  283. } else {
  284. UnreadCounterHandler.increment(1);
  285. }
  286. }
  287. static toggleEntryStatus(element) {
  288. let entryID = parseInt(element.dataset.id, 10);
  289. let statuses = {read: "unread", unread: "read"};
  290. for (let currentStatus in statuses) {
  291. let newStatus = statuses[currentStatus];
  292. if (element.classList.contains("item-status-" + currentStatus)) {
  293. element.classList.remove("item-status-" + currentStatus);
  294. element.classList.add("item-status-" + newStatus);
  295. this.updateEntriesStatus([entryID], newStatus);
  296. let link = element.querySelector("a[data-toggle-status]");
  297. if (link) {
  298. this.toggleLinkStatus(link);
  299. }
  300. break;
  301. }
  302. }
  303. }
  304. static toggleLinkStatus(link) {
  305. if (link.dataset.value === "read") {
  306. link.innerHTML = link.dataset.labelRead;
  307. link.dataset.value = "unread";
  308. } else {
  309. link.innerHTML = link.dataset.labelUnread;
  310. link.dataset.value = "read";
  311. }
  312. }
  313. static toggleBookmark(element) {
  314. element.innerHTML = element.dataset.labelLoading;
  315. let request = new RequestBuilder(element.dataset.bookmarkUrl);
  316. request.withCallback(() => {
  317. if (element.dataset.value === "star") {
  318. element.innerHTML = element.dataset.labelStar;
  319. element.dataset.value = "unstar";
  320. } else {
  321. element.innerHTML = element.dataset.labelUnstar;
  322. element.dataset.value = "star";
  323. }
  324. });
  325. request.execute();
  326. }
  327. static markEntryAsRead(element) {
  328. if (element.classList.contains("item-status-unread")) {
  329. element.classList.remove("item-status-unread");
  330. element.classList.add("item-status-read");
  331. let entryID = parseInt(element.dataset.id, 10);
  332. this.updateEntriesStatus([entryID], "read");
  333. }
  334. }
  335. static saveEntry(element) {
  336. if (element.dataset.completed) {
  337. return;
  338. }
  339. element.innerHTML = element.dataset.labelLoading;
  340. let request = new RequestBuilder(element.dataset.saveUrl);
  341. request.withCallback(() => {
  342. element.innerHTML = element.dataset.labelDone;
  343. element.dataset.completed = true;
  344. });
  345. request.execute();
  346. }
  347. static fetchOriginalContent(element) {
  348. if (element.dataset.completed) {
  349. return;
  350. }
  351. element.innerHTML = element.dataset.labelLoading;
  352. let request = new RequestBuilder(element.dataset.fetchContentUrl);
  353. request.withCallback((response) => {
  354. element.innerHTML = element.dataset.labelDone;
  355. element.dataset.completed = true;
  356. response.json().then((data) => {
  357. if (data.hasOwnProperty("content")) {
  358. document.querySelector(".entry-content").innerHTML = data.content;
  359. }
  360. });
  361. });
  362. request.execute();
  363. }
  364. }
  365. class ConfirmHandler {
  366. remove(url) {
  367. let request = new RequestBuilder(url);
  368. request.withCallback(() => window.location.reload());
  369. request.execute();
  370. }
  371. handle(event) {
  372. let questionElement = document.createElement("span");
  373. let linkElement = event.target;
  374. let containerElement = linkElement.parentNode;
  375. linkElement.style.display = "none";
  376. let yesElement = document.createElement("a");
  377. yesElement.href = "#";
  378. yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes));
  379. yesElement.onclick = (event) => {
  380. event.preventDefault();
  381. let loadingElement = document.createElement("span");
  382. loadingElement.className = "loading";
  383. loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));
  384. questionElement.remove();
  385. containerElement.appendChild(loadingElement);
  386. this.remove(linkElement.dataset.url);
  387. };
  388. let noElement = document.createElement("a");
  389. noElement.href = "#";
  390. noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo));
  391. noElement.onclick = (event) => {
  392. event.preventDefault();
  393. linkElement.style.display = "inline";
  394. questionElement.remove();
  395. };
  396. questionElement.className = "confirm";
  397. questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion + " "));
  398. questionElement.appendChild(yesElement);
  399. questionElement.appendChild(document.createTextNode(", "));
  400. questionElement.appendChild(noElement);
  401. containerElement.appendChild(questionElement);
  402. }
  403. }
  404. class MenuHandler {
  405. clickMenuListItem(event) {
  406. let element = event.target;
  407. if (element.tagName === "A") {
  408. window.location.href = element.getAttribute("href");
  409. } else {
  410. window.location.href = element.querySelector("a").getAttribute("href");
  411. }
  412. }
  413. toggleMainMenu() {
  414. let menu = document.querySelector(".header nav ul");
  415. if (DomHelper.isVisible(menu)) {
  416. menu.style.display = "none";
  417. } else {
  418. menu.style.display = "block";
  419. }
  420. }
  421. }
  422. class ModalHandler {
  423. static exists() {
  424. return document.getElementById("modal-container") !== null;
  425. }
  426. static open(fragment) {
  427. if (ModalHandler.exists()) {
  428. return;
  429. }
  430. let container = document.createElement("div");
  431. container.id = "modal-container";
  432. container.appendChild(document.importNode(fragment, true));
  433. document.body.appendChild(container);
  434. let closeButton = document.querySelector("a.btn-close-modal");
  435. if (closeButton !== null) {
  436. closeButton.onclick = (event) => {
  437. event.preventDefault();
  438. ModalHandler.close();
  439. };
  440. }
  441. }
  442. static close() {
  443. let container = document.getElementById("modal-container");
  444. if (container !== null) {
  445. container.parentNode.removeChild(container);
  446. }
  447. }
  448. }
  449. class NavHandler {
  450. showKeyboardShortcuts() {
  451. let template = document.getElementById("keyboard-shortcuts");
  452. if (template !== null) {
  453. ModalHandler.open(template.content);
  454. }
  455. }
  456. markPageAsRead() {
  457. let items = DomHelper.getVisibleElements(".items .item");
  458. let entryIDs = [];
  459. items.forEach((element) => {
  460. element.classList.add("item-status-read");
  461. entryIDs.push(parseInt(element.dataset.id, 10));
  462. });
  463. if (entryIDs.length > 0) {
  464. EntryHandler.updateEntriesStatus(entryIDs, "read", () => {
  465. // This callback make sure the Ajax request reach the server before we reload the page.
  466. this.goToPage("next", true);
  467. });
  468. }
  469. }
  470. saveEntry() {
  471. if (this.isListView()) {
  472. let currentItem = document.querySelector(".current-item");
  473. if (currentItem !== null) {
  474. let saveLink = currentItem.querySelector("a[data-save-entry]");
  475. if (saveLink) {
  476. EntryHandler.saveEntry(saveLink);
  477. }
  478. }
  479. } else {
  480. let saveLink = document.querySelector("a[data-save-entry]");
  481. if (saveLink) {
  482. EntryHandler.saveEntry(saveLink);
  483. }
  484. }
  485. }
  486. fetchOriginalContent() {
  487. if (! this.isListView()){
  488. let link = document.querySelector("a[data-fetch-content-entry]");
  489. if (link) {
  490. EntryHandler.fetchOriginalContent(link);
  491. }
  492. }
  493. }
  494. toggleEntryStatus() {
  495. let currentItem = document.querySelector(".current-item");
  496. if (currentItem !== null) {
  497. // The order is important here,
  498. // On the unread page, the read item will be hidden.
  499. this.goToNextListItem();
  500. EntryHandler.toggleEntryStatus(currentItem);
  501. }
  502. }
  503. toggleBookmark() {
  504. if (! this.isListView()) {
  505. this.toggleBookmarkLink(document.querySelector(".entry"));
  506. return;
  507. }
  508. let currentItem = document.querySelector(".current-item");
  509. if (currentItem !== null) {
  510. this.toggleBookmarkLink(currentItem);
  511. }
  512. }
  513. toggleBookmarkLink(parent) {
  514. let bookmarkLink = parent.querySelector("a[data-toggle-bookmark]");
  515. if (bookmarkLink) {
  516. EntryHandler.toggleBookmark(bookmarkLink);
  517. }
  518. }
  519. openOriginalLink() {
  520. let entryLink = document.querySelector(".entry h1 a");
  521. if (entryLink !== null) {
  522. DomHelper.openNewTab(entryLink.getAttribute("href"));
  523. return;
  524. }
  525. let currentItemOriginalLink = document.querySelector(".current-item a[data-original-link]");
  526. if (currentItemOriginalLink !== null) {
  527. DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));
  528. // Move to the next item and if we are on the unread page mark this item as read.
  529. let currentItem = document.querySelector(".current-item");
  530. this.goToNextListItem();
  531. EntryHandler.markEntryAsRead(currentItem);
  532. }
  533. }
  534. openSelectedItem() {
  535. let currentItemLink = document.querySelector(".current-item .item-title a");
  536. if (currentItemLink !== null) {
  537. // The following 4 lines ensure that the unread count in the menu is
  538. // updated correctly when users presses "o".
  539. let currentItemOriginalLink = document.querySelector(".current-item a[data-original-link]");
  540. if (currentItemOriginalLink !== null) {
  541. let currentItem = document.querySelector(".current-item");
  542. EntryHandler.markEntryAsRead(currentItem);
  543. }
  544. window.location.href = currentItemLink.getAttribute("href");
  545. }
  546. }
  547. /**
  548. * @param {string} page Page to redirect to.
  549. * @param {boolean} fallbackSelf Refresh actual page if the page is not found.
  550. */
  551. goToPage(page, fallbackSelf) {
  552. let element = document.querySelector("a[data-page=" + page + "]");
  553. if (element) {
  554. document.location.href = element.href;
  555. } else if (fallbackSelf) {
  556. window.location.reload();
  557. }
  558. }
  559. goToPrevious() {
  560. if (this.isListView()) {
  561. this.goToPreviousListItem();
  562. } else {
  563. this.goToPage("previous");
  564. }
  565. }
  566. goToNext() {
  567. if (this.isListView()) {
  568. this.goToNextListItem();
  569. } else {
  570. this.goToPage("next");
  571. }
  572. }
  573. goToPreviousListItem() {
  574. let items = DomHelper.getVisibleElements(".items .item");
  575. if (items.length === 0) {
  576. return;
  577. }
  578. if (document.querySelector(".current-item") === null) {
  579. items[0].classList.add("current-item");
  580. return;
  581. }
  582. for (let i = 0; i < items.length; i++) {
  583. if (items[i].classList.contains("current-item")) {
  584. items[i].classList.remove("current-item");
  585. if (i - 1 >= 0) {
  586. items[i - 1].classList.add("current-item");
  587. DomHelper.scrollPageTo(items[i - 1]);
  588. }
  589. break;
  590. }
  591. }
  592. }
  593. goToNextListItem() {
  594. let currentItem = document.querySelector(".current-item");
  595. let items = DomHelper.getVisibleElements(".items .item");
  596. if (items.length === 0) {
  597. return;
  598. }
  599. if (currentItem === null) {
  600. items[0].classList.add("current-item");
  601. return;
  602. }
  603. for (let i = 0; i < items.length; i++) {
  604. if (items[i].classList.contains("current-item")) {
  605. items[i].classList.remove("current-item");
  606. if (i + 1 < items.length) {
  607. items[i + 1].classList.add("current-item");
  608. DomHelper.scrollPageTo(items[i + 1]);
  609. }
  610. break;
  611. }
  612. }
  613. }
  614. isListView() {
  615. return document.querySelector(".items") !== null;
  616. }
  617. }
  618. document.addEventListener("DOMContentLoaded", function() {
  619. FormHandler.handleSubmitButtons();
  620. let touchHandler = new TouchHandler();
  621. touchHandler.listen();
  622. let navHandler = new NavHandler();
  623. let keyboardHandler = new KeyboardHandler();
  624. keyboardHandler.on("g u", () => navHandler.goToPage("unread"));
  625. keyboardHandler.on("g b", () => navHandler.goToPage("starred"));
  626. keyboardHandler.on("g h", () => navHandler.goToPage("history"));
  627. keyboardHandler.on("g f", () => navHandler.goToPage("feeds"));
  628. keyboardHandler.on("g c", () => navHandler.goToPage("categories"));
  629. keyboardHandler.on("g s", () => navHandler.goToPage("settings"));
  630. keyboardHandler.on("ArrowLeft", () => navHandler.goToPrevious());
  631. keyboardHandler.on("ArrowRight", () => navHandler.goToNext());
  632. keyboardHandler.on("j", () => navHandler.goToPrevious());
  633. keyboardHandler.on("p", () => navHandler.goToPrevious());
  634. keyboardHandler.on("k", () => navHandler.goToNext());
  635. keyboardHandler.on("n", () => navHandler.goToNext());
  636. keyboardHandler.on("h", () => navHandler.goToPage("previous"));
  637. keyboardHandler.on("l", () => navHandler.goToPage("next"));
  638. keyboardHandler.on("o", () => navHandler.openSelectedItem());
  639. keyboardHandler.on("v", () => navHandler.openOriginalLink());
  640. keyboardHandler.on("m", () => navHandler.toggleEntryStatus());
  641. keyboardHandler.on("A", () => navHandler.markPageAsRead());
  642. keyboardHandler.on("s", () => navHandler.saveEntry());
  643. keyboardHandler.on("d", () => navHandler.fetchOriginalContent());
  644. keyboardHandler.on("f", () => navHandler.toggleBookmark());
  645. keyboardHandler.on("?", () => navHandler.showKeyboardShortcuts());
  646. keyboardHandler.on("Escape", () => ModalHandler.close());
  647. keyboardHandler.listen();
  648. let mouseHandler = new MouseHandler();
  649. mouseHandler.onClick("a[data-save-entry]", (event) => {
  650. event.preventDefault();
  651. EntryHandler.saveEntry(event.target);
  652. });
  653. mouseHandler.onClick("a[data-toggle-bookmark]", (event) => {
  654. event.preventDefault();
  655. EntryHandler.toggleBookmark(event.target);
  656. });
  657. mouseHandler.onClick("a[data-toggle-status]", (event) => {
  658. event.preventDefault();
  659. let currentItem = DomHelper.findParent(event.target, "item");
  660. if (currentItem) {
  661. EntryHandler.toggleEntryStatus(currentItem);
  662. }
  663. });
  664. mouseHandler.onClick("a[data-fetch-content-entry]", (event) => {
  665. event.preventDefault();
  666. EntryHandler.fetchOriginalContent(event.target);
  667. });
  668. mouseHandler.onClick("a[data-on-click=markPageAsRead]", () => navHandler.markPageAsRead());
  669. mouseHandler.onClick("a[data-confirm]", (event) => {
  670. (new ConfirmHandler()).handle(event);
  671. });
  672. if (document.documentElement.clientWidth < 600) {
  673. let menuHandler = new MenuHandler();
  674. mouseHandler.onClick(".logo", () => menuHandler.toggleMainMenu());
  675. mouseHandler.onClick(".header nav li", (event) => menuHandler.clickMenuListItem(event));
  676. }
  677. });
  678. })();