aboutsummaryrefslogtreecommitdiff
path: root/server/static/js
diff options
context:
space:
mode:
Diffstat (limited to 'server/static/js')
-rw-r--r--server/static/js/app.js351
1 files changed, 351 insertions, 0 deletions
diff --git a/server/static/js/app.js b/server/static/js/app.js
new file mode 100644
index 00000000..46a8f720
--- /dev/null
+++ b/server/static/js/app.js
@@ -0,0 +1,351 @@
+/*jshint esversion: 6 */
+(function() {
+'use strict';
+
+class KeyboardHandler {
+ constructor() {
+ this.queue = [];
+ this.shortcuts = {};
+ }
+
+ on(combination, callback) {
+ this.shortcuts[combination] = callback;
+ }
+
+ listen() {
+ document.onkeydown = (event) => {
+ if (this.isEventIgnored(event)) {
+ return;
+ }
+
+ let key = this.getKey(event);
+ this.queue.push(key);
+
+ for (let combination in this.shortcuts) {
+ let keys = combination.split(" ");
+
+ if (keys.every((value, index) => value === this.queue[index])) {
+ this.queue = [];
+ this.shortcuts[combination]();
+ return;
+ }
+
+ if (keys.length === 1 && key === keys[0]) {
+ this.queue = [];
+ this.shortcuts[combination]();
+ return;
+ }
+ }
+
+ if (this.queue.length >= 2) {
+ this.queue = [];
+ }
+ };
+ }
+
+ isEventIgnored(event) {
+ return event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA";
+ }
+
+ getKey(event) {
+ const mapping = {
+ 'Esc': 'Escape',
+ 'Up': 'ArrowUp',
+ 'Down': 'ArrowDown',
+ 'Left': 'ArrowLeft',
+ 'Right': 'ArrowRight'
+ };
+
+ for (let key in mapping) {
+ if (mapping.hasOwnProperty(key) && key === event.key) {
+ return mapping[key];
+ }
+ }
+
+ return event.key;
+ }
+}
+
+class FormHandler {
+ static handleSubmitButtons() {
+ let elements = document.querySelectorAll("form");
+ elements.forEach(function (element) {
+ element.onsubmit = function () {
+ let button = document.querySelector("button");
+
+ if (button) {
+ button.innerHTML = button.dataset.labelLoading;
+ button.disabled = true;
+ }
+ };
+ });
+ }
+}
+
+class MouseHandler {
+ onClick(selector, callback) {
+ let elements = document.querySelectorAll(selector);
+ elements.forEach((element) => {
+ element.onclick = (event) => {
+ event.preventDefault();
+ callback(event);
+ };
+ });
+ }
+}
+
+class App {
+ run() {
+ FormHandler.handleSubmitButtons();
+
+ let keyboardHandler = new KeyboardHandler();
+ keyboardHandler.on("g u", () => this.goToPage("unread"));
+ keyboardHandler.on("g h", () => this.goToPage("history"));
+ keyboardHandler.on("g f", () => this.goToPage("feeds"));
+ keyboardHandler.on("g c", () => this.goToPage("categories"));
+ keyboardHandler.on("g s", () => this.goToPage("settings"));
+ keyboardHandler.on("ArrowLeft", () => this.goToPrevious());
+ keyboardHandler.on("ArrowRight", () => this.goToNext());
+ keyboardHandler.on("j", () => this.goToPrevious());
+ keyboardHandler.on("p", () => this.goToPrevious());
+ keyboardHandler.on("k", () => this.goToNext());
+ keyboardHandler.on("n", () => this.goToNext());
+ keyboardHandler.on("h", () => this.goToPage("previous"));
+ keyboardHandler.on("l", () => this.goToPage("next"));
+ keyboardHandler.on("o", () => this.openSelectedItem());
+ keyboardHandler.on("v", () => this.openOriginalLink());
+ keyboardHandler.on("m", () => this.toggleEntryStatus());
+ keyboardHandler.on("A", () => this.markPageAsRead());
+ keyboardHandler.listen();
+
+ let mouseHandler = new MouseHandler();
+ mouseHandler.onClick("a[data-on-click=markPageAsRead]", () => this.markPageAsRead());
+
+ if (document.documentElement.clientWidth < 600) {
+ mouseHandler.onClick(".logo", () => this.toggleMainMenu());
+ mouseHandler.onClick(".header nav li", (event) => this.clickMenuListItem(event));
+ }
+ }
+
+ clickMenuListItem(event) {
+ let element = event.target;console.log(element);
+
+ if (element.tagName === "A") {
+ window.location.href = element.getAttribute("href");
+ } else {
+ window.location.href = element.querySelector("a").getAttribute("href");
+ }
+ }
+
+ toggleMainMenu() {
+ let menu = document.querySelector(".header nav ul");
+ if (this.isVisible(menu)) {
+ menu.style.display = "none";
+ } else {
+ menu.style.display = "block";
+ }
+ }
+
+ updateEntriesStatus(entryIDs, status) {
+ let url = document.body.dataset.entriesStatusUrl;
+ let request = new Request(url, {
+ method: "POST",
+ cache: "no-cache",
+ credentials: "include",
+ body: JSON.stringify({entry_ids: entryIDs, status: status}),
+ headers: new Headers({
+ "Content-Type": "application/json",
+ "X-Csrf-Token": this.getCsrfToken()
+ })
+ });
+
+ fetch(request);
+ }
+
+ markPageAsRead() {
+ let items = this.getVisibleElements(".items .item");
+ let entryIDs = [];
+
+ items.forEach((element) => {
+ element.classList.add("item-status-read");
+ entryIDs.push(parseInt(element.dataset.id, 10));
+ });
+
+ if (entryIDs.length > 0) {
+ this.updateEntriesStatus(entryIDs, "read");
+ }
+
+ this.goToPage("next");
+ }
+
+ toggleEntryStatus() {
+ let currentItem = document.querySelector(".current-item");
+ if (currentItem !== null) {
+ let entryID = parseInt(currentItem.dataset.id, 10);
+ let statuses = {read: "unread", unread: "read"};
+
+ for (let currentStatus in statuses) {
+ let newStatus = statuses[currentStatus];
+
+ if (currentItem.classList.contains("item-status-" + currentStatus)) {
+ this.goToNextListItem();
+
+ currentItem.classList.remove("item-status-" + currentStatus);
+ currentItem.classList.add("item-status-" + newStatus);
+
+ this.updateEntriesStatus([entryID], newStatus);
+ break;
+ }
+ }
+ }
+ }
+
+ openOriginalLink() {
+ let entryLink = document.querySelector(".entry h1 a");
+ if (entryLink !== null) {
+ this.openNewTab(entryLink.getAttribute("href"));
+ return;
+ }
+
+ let currentItemOriginalLink = document.querySelector(".current-item a[data-original-link]");
+ if (currentItemOriginalLink !== null) {
+ this.openNewTab(currentItemOriginalLink.getAttribute("href"));
+ }
+ }
+
+ openSelectedItem() {
+ let currentItemLink = document.querySelector(".current-item .item-title a");
+ if (currentItemLink !== null) {
+ window.location.href = currentItemLink.getAttribute("href");
+ }
+ }
+
+ goToPage(page) {
+ let element = document.querySelector("a[data-page=" + page + "]");
+
+ if (element) {
+ document.location.href = element.href;
+ }
+ }
+
+ goToPrevious() {
+ if (this.isListView()) {
+ this.goToPreviousListItem();
+ } else {
+ this.goToPage("previous");
+ }
+ }
+
+ goToNext() {
+ if (this.isListView()) {
+ this.goToNextListItem();
+ } else {
+ this.goToPage("next");
+ }
+ }
+
+ goToPreviousListItem() {
+ let items = this.getVisibleElements(".items .item");
+
+ if (items.length === 0) {
+ return;
+ }
+
+ if (document.querySelector(".current-item") === null) {
+ items[0].classList.add("current-item");
+ return;
+ }
+
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].classList.contains("current-item")) {
+ items[i].classList.remove("current-item");
+
+ if (i - 1 >= 0) {
+ items[i - 1].classList.add("current-item");
+ this.scrollPageTo(items[i - 1]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ goToNextListItem() {
+ let items = this.getVisibleElements(".items .item");
+
+ if (items.length === 0) {
+ return;
+ }
+
+ if (document.querySelector(".current-item") === null) {
+ items[0].classList.add("current-item");
+ return;
+ }
+
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].classList.contains("current-item")) {
+ items[i].classList.remove("current-item");
+
+ if (i + 1 < items.length) {
+ items[i + 1].classList.add("current-item");
+ this.scrollPageTo(items[i + 1]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ getVisibleElements(selector) {
+ let elements = document.querySelectorAll(selector);
+ let result = [];
+
+ for (let i = 0; i < elements.length; i++) {
+ if (this.isVisible(elements[i])) {
+ result.push(elements[i]);
+ }
+ }
+
+ return result;
+ }
+
+ isListView() {
+ return document.querySelector(".items") !== null;
+ }
+
+ scrollPageTo(item) {
+ let windowScrollPosition = window.pageYOffset;
+ let windowHeight = document.documentElement.clientHeight;
+ let viewportPosition = windowScrollPosition + windowHeight;
+ let itemBottomPosition = item.offsetTop + item.offsetHeight;
+
+ if (viewportPosition - itemBottomPosition < 0 || viewportPosition - item.offsetTop > windowHeight) {
+ window.scrollTo(0, item.offsetTop - 10);
+ }
+ }
+
+ openNewTab(url) {
+ let win = window.open(url, "_blank");
+ win.focus();
+ }
+
+ isVisible(element) {
+ return element.offsetParent !== null;
+ }
+
+ getCsrfToken() {
+ let element = document.querySelector("meta[name=X-CSRF-Token]");
+
+ if (element !== null) {
+ return element.getAttribute("value");
+ }
+
+ return "";
+ }
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+ (new App()).run();
+});
+
+})();