diff options
Diffstat (limited to 'ui/static/js')
-rw-r--r-- | ui/static/js/.jshintrc | 3 | ||||
-rw-r--r-- | ui/static/js/app.js | 692 | ||||
-rw-r--r-- | ui/static/js/bootstrap.js | 126 | ||||
-rw-r--r-- | ui/static/js/dom_helper.js | 65 | ||||
-rw-r--r-- | ui/static/js/keyboard_handler.js | 72 | ||||
-rw-r--r-- | ui/static/js/modal_handler.js | 101 | ||||
-rw-r--r-- | ui/static/js/request_builder.js | 48 | ||||
-rw-r--r-- | ui/static/js/service_worker.js | 44 | ||||
-rw-r--r-- | ui/static/js/touch_handler.js | 187 |
9 files changed, 0 insertions, 1338 deletions
diff --git a/ui/static/js/.jshintrc b/ui/static/js/.jshintrc deleted file mode 100644 index 80fc4c09..00000000 --- a/ui/static/js/.jshintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "esversion": 8 -}
\ No newline at end of file diff --git a/ui/static/js/app.js b/ui/static/js/app.js deleted file mode 100644 index a89da8bd..00000000 --- a/ui/static/js/app.js +++ /dev/null @@ -1,692 +0,0 @@ -// OnClick attaches a listener to the elements that match the selector. -function onClick(selector, callback, noPreventDefault) { - let elements = document.querySelectorAll(selector); - elements.forEach((element) => { - element.onclick = (event) => { - if (!noPreventDefault) { - event.preventDefault(); - } - - callback(event); - }; - }); -} - -function onAuxClick(selector, callback, noPreventDefault) { - let elements = document.querySelectorAll(selector); - elements.forEach((element) => { - element.onauxclick = (event) => { - if (!noPreventDefault) { - event.preventDefault(); - } - - callback(event); - }; - }); -} - -// Show and hide the main menu on mobile devices. -function toggleMainMenu() { - let menu = document.querySelector(".header nav ul"); - if (DomHelper.isVisible(menu)) { - menu.style.display = "none"; - } else { - menu.style.display = "block"; - } - - let searchElement = document.querySelector(".header .search"); - if (DomHelper.isVisible(searchElement)) { - searchElement.style.display = "none"; - } else { - searchElement.style.display = "block"; - } -} - -// Handle click events for the main menu (<li> and <a>). -function onClickMainMenuListItem(event) { - let element = event.target; - - if (element.tagName === "A") { - window.location.href = element.getAttribute("href"); - } else { - window.location.href = element.querySelector("a").getAttribute("href"); - } -} - -// Change the button label when the page is loading. -function handleSubmitButtons() { - let elements = document.querySelectorAll("form"); - elements.forEach((element) => { - element.onsubmit = () => { - let button = element.querySelector("button"); - - if (button) { - button.innerHTML = button.dataset.labelLoading; - button.disabled = true; - } - }; - }); -} - -// Set cursor focus to the search input. -function setFocusToSearchInput(event) { - event.preventDefault(); - event.stopPropagation(); - - let toggleSwitchElement = document.querySelector(".search-toggle-switch"); - if (toggleSwitchElement) { - toggleSwitchElement.style.display = "none"; - } - - let searchFormElement = document.querySelector(".search-form"); - if (searchFormElement) { - searchFormElement.style.display = "block"; - } - - let searchInputElement = document.getElementById("search-input"); - if (searchInputElement) { - searchInputElement.focus(); - searchInputElement.value = ""; - } -} - -// Show modal dialog with the list of keyboard shortcuts. -function showKeyboardShortcuts() { - let template = document.getElementById("keyboard-shortcuts"); - if (template !== null) { - ModalHandler.open(template.content, "dialog-title"); - } -} - -// Mark as read visible items of the current page. -function markPageAsRead() { - let items = DomHelper.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) { - updateEntriesStatus(entryIDs, "read", () => { - // Make sure the Ajax request reach the server before we reload the page. - - let element = document.querySelector("a[data-action=markPageAsRead]"); - let showOnlyUnread = false; - if (element) { - showOnlyUnread = element.dataset.showOnlyUnread || false; - } - - if (showOnlyUnread) { - window.location.href = window.location.href; - } else { - goToPage("next", true); - } - }); - } -} - -/** - * Handle entry status changes from the list view and entry view. - * Focus the next or the previous entry if it exists. - * @param {string} item Item to focus: "previous" or "next". - * @param {Element} element - * @param {boolean} setToRead - */ -function handleEntryStatus(item, element, setToRead) { - let toasting = !element; - let currentEntry = findEntry(element); - if (currentEntry) { - if (!setToRead || currentEntry.querySelector("a[data-toggle-status]").dataset.value == "unread") { - toggleEntryStatus(currentEntry, toasting); - } - if (isListView() && currentEntry.classList.contains('current-item')) { - switch (item) { - case "previous": - goToListItem(-1); - break; - case "next": - goToListItem(1); - break; - } - } - } -} - -// Change the entry status to the opposite value. -function toggleEntryStatus(element, toasting) { - let entryID = parseInt(element.dataset.id, 10); - let link = element.querySelector("a[data-toggle-status]"); - - let currentStatus = link.dataset.value; - let newStatus = currentStatus === "read" ? "unread" : "read"; - - link.querySelector("span").innerHTML = link.dataset.labelLoading; - updateEntriesStatus([entryID], newStatus, () => { - let iconElement, label; - - if (currentStatus === "read") { - iconElement = document.querySelector("template#icon-read"); - label = link.dataset.labelRead; - if (toasting) { - showToast(link.dataset.toastUnread, iconElement); - } - } else { - iconElement = document.querySelector("template#icon-unread"); - label = link.dataset.labelUnread; - if (toasting) { - showToast(link.dataset.toastRead, iconElement); - } - } - - link.innerHTML = iconElement.innerHTML + '<span class="icon-label">' + label + '</span>'; - link.dataset.value = newStatus; - - if (element.classList.contains("item-status-" + currentStatus)) { - element.classList.remove("item-status-" + currentStatus); - element.classList.add("item-status-" + newStatus); - } - }); -} - -// Mark a single entry as read. -function markEntryAsRead(element) { - if (element.classList.contains("item-status-unread")) { - element.classList.remove("item-status-unread"); - element.classList.add("item-status-read"); - - let entryID = parseInt(element.dataset.id, 10); - updateEntriesStatus([entryID], "read"); - } -} - -// Send the Ajax request to refresh all feeds in the background -function handleRefreshAllFeeds() { - let url = document.body.dataset.refreshAllFeedsUrl; - let request = new RequestBuilder(url); - - request.withCallback(() => { - window.location.reload(); - }); - - request.withHttpMethod("GET"); - request.execute(); -} - -// Send the Ajax request to change entries statuses. -function updateEntriesStatus(entryIDs, status, callback) { - let url = document.body.dataset.entriesStatusUrl; - let request = new RequestBuilder(url); - request.withBody({entry_ids: entryIDs, status: status}); - request.withCallback((resp) => { - resp.json().then(count => { - if (callback) { - callback(resp); - } - - if (status === "read") { - decrementUnreadCounter(count); - } else { - incrementUnreadCounter(count); - } - }); - }); - request.execute(); -} - -// Handle save entry from list view and entry view. -function handleSaveEntry(element) { - let toasting = !element; - let currentEntry = findEntry(element); - if (currentEntry) { - saveEntry(currentEntry.querySelector("a[data-save-entry]"), toasting); - } -} - -// Send the Ajax request to save an entry. -function saveEntry(element, toasting) { - if (!element) { - return; - } - - if (element.dataset.completed) { - return; - } - - let previousInnerHTML = element.innerHTML; - element.innerHTML = '<span class="icon-label">' + element.dataset.labelLoading + '</span>'; - - let request = new RequestBuilder(element.dataset.saveUrl); - request.withCallback(() => { - element.innerHTML = previousInnerHTML; - element.dataset.completed = true; - if (toasting) { - let iconElement = document.querySelector("template#icon-save"); - showToast(element.dataset.toastDone, iconElement); - } - }); - request.execute(); -} - -// Handle bookmark from the list view and entry view. -function handleBookmark(element) { - let toasting = !element; - let currentEntry = findEntry(element); - if (currentEntry) { - toggleBookmark(currentEntry, toasting); - } -} - -// Send the Ajax request and change the icon when bookmarking an entry. -function toggleBookmark(parentElement, toasting) { - let element = parentElement.querySelector("a[data-toggle-bookmark]"); - if (!element) { - return; - } - - element.innerHTML = '<span class="icon-label">' + element.dataset.labelLoading + '</span>'; - - let request = new RequestBuilder(element.dataset.bookmarkUrl); - request.withCallback(() => { - - let currentStarStatus = element.dataset.value; - let newStarStatus = currentStarStatus === "star" ? "unstar" : "star"; - - let iconElement, label; - - if (currentStarStatus === "star") { - iconElement = document.querySelector("template#icon-star"); - label = element.dataset.labelStar; - if (toasting) { - showToast(element.dataset.toastUnstar, iconElement); - } - } else { - iconElement = document.querySelector("template#icon-unstar"); - label = element.dataset.labelUnstar; - if (toasting) { - showToast(element.dataset.toastStar, iconElement); - } - } - - element.innerHTML = iconElement.innerHTML + '<span class="icon-label">' + label + '</span>'; - element.dataset.value = newStarStatus; - }); - request.execute(); -} - -// Send the Ajax request to download the original web page. -function handleFetchOriginalContent() { - if (isListView()) { - return; - } - - let element = document.querySelector("a[data-fetch-content-entry]"); - if (!element) { - return; - } - - let previousInnerHTML = element.innerHTML; - element.innerHTML = '<span class="icon-label">' + element.dataset.labelLoading + '</span>'; - - let request = new RequestBuilder(element.dataset.fetchContentUrl); - request.withCallback((response) => { - element.innerHTML = previousInnerHTML; - - response.json().then((data) => { - if (data.hasOwnProperty("content") && data.hasOwnProperty("reading_time")) { - document.querySelector(".entry-content").innerHTML = data.content; - document.querySelector(".entry-reading-time").innerHTML = data.reading_time; - } - }); - }); - request.execute(); -} - -function openOriginalLink(openLinkInCurrentTab) { - let entryLink = document.querySelector(".entry h1 a"); - if (entryLink !== null) { - if (openLinkInCurrentTab) { - window.location.href = entryLink.getAttribute("href"); - } else { - DomHelper.openNewTab(entryLink.getAttribute("href")); - } - return; - } - - let currentItemOriginalLink = document.querySelector(".current-item a[data-original-link]"); - if (currentItemOriginalLink !== null) { - DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href")); - - let currentItem = document.querySelector(".current-item"); - // If we are not on the list of starred items, move to the next item - if (document.location.href != document.querySelector('a[data-page=starred]').href) { - goToListItem(1); - } - markEntryAsRead(currentItem); - } -} - -function openCommentLink(openLinkInCurrentTab) { - if (!isListView()) { - let entryLink = document.querySelector("a[data-comments-link]"); - if (entryLink !== null) { - if (openLinkInCurrentTab) { - window.location.href = entryLink.getAttribute("href"); - } else { - DomHelper.openNewTab(entryLink.getAttribute("href")); - } - return; - } - } else { - let currentItemCommentsLink = document.querySelector(".current-item a[data-comments-link]"); - if (currentItemCommentsLink !== null) { - DomHelper.openNewTab(currentItemCommentsLink.getAttribute("href")); - } - } -} - -function openSelectedItem() { - let currentItemLink = document.querySelector(".current-item .item-title a"); - if (currentItemLink !== null) { - window.location.href = currentItemLink.getAttribute("href"); - } -} - -function unsubscribeFromFeed() { - let unsubscribeLinks = document.querySelectorAll("[data-action=remove-feed]"); - if (unsubscribeLinks.length === 1) { - let unsubscribeLink = unsubscribeLinks[0]; - - let request = new RequestBuilder(unsubscribeLink.dataset.url); - request.withCallback(() => { - if (unsubscribeLink.dataset.redirectUrl) { - window.location.href = unsubscribeLink.dataset.redirectUrl; - } else { - window.location.reload(); - } - }); - request.execute(); - } -} - -/** - * @param {string} page Page to redirect to. - * @param {boolean} fallbackSelf Refresh actual page if the page is not found. - */ -function goToPage(page, fallbackSelf) { - let element = document.querySelector("a[data-page=" + page + "]"); - - if (element) { - document.location.href = element.href; - } else if (fallbackSelf) { - window.location.reload(); - } -} - -function goToPrevious() { - if (isListView()) { - goToListItem(-1); - } else { - goToPage("previous"); - } -} - -function goToNext() { - if (isListView()) { - goToListItem(1); - } else { - goToPage("next"); - } -} - -function goToFeedOrFeeds() { - if (isEntry()) { - goToFeed(); - } else { - goToPage('feeds'); - } -} - -function goToFeed() { - if (isEntry()) { - let feedAnchor = document.querySelector("span.entry-website a"); - if (feedAnchor !== null) { - window.location.href = feedAnchor.href; - } - } else { - let currentItemFeed = document.querySelector(".current-item a[data-feed-link]"); - if (currentItemFeed !== null) { - window.location.href = currentItemFeed.getAttribute("href"); - } - } -} - -/** - * @param {number} offset How many items to jump for focus. - */ -function goToListItem(offset) { - let items = DomHelper.getVisibleElements(".items .item"); - if (items.length === 0) { - return; - } - - if (document.querySelector(".current-item") === null) { - items[0].classList.add("current-item"); - items[0].querySelector('.item-header a').focus(); - return; - } - - for (let i = 0; i < items.length; i++) { - if (items[i].classList.contains("current-item")) { - items[i].classList.remove("current-item"); - - let item = items[(i + offset + items.length) % items.length]; - - item.classList.add("current-item"); - DomHelper.scrollPageTo(item); - item.querySelector('.item-header a').focus(); - - break; - } - } -} - -function scrollToCurrentItem() { - let currentItem = document.querySelector(".current-item"); - if (currentItem !== null) { - DomHelper.scrollPageTo(currentItem, true); - } -} - -function decrementUnreadCounter(n) { - updateUnreadCounterValue((current) => { - return current - n; - }); -} - -function incrementUnreadCounter(n) { - updateUnreadCounterValue((current) => { - return current + n; - }); -} - -function updateUnreadCounterValue(callback) { - let counterElements = document.querySelectorAll("span.unread-counter"); - counterElements.forEach((element) => { - let oldValue = parseInt(element.textContent, 10); - element.innerHTML = callback(oldValue); - }); - - if (window.location.href.endsWith('/unread')) { - let oldValue = parseInt(document.title.split('(')[1], 10); - let newValue = callback(oldValue); - - document.title = document.title.replace( - /(.*?)\(\d+\)(.*?)/, - function (match, prefix, suffix, offset, string) { - return prefix + '(' + newValue + ')' + suffix; - } - ); - } -} - -function isEntry() { - return document.querySelector("section.entry") !== null; -} - -function isListView() { - return document.querySelector(".items") !== null; -} - -function findEntry(element) { - if (isListView()) { - if (element) { - return DomHelper.findParent(element, "item"); - } else { - return document.querySelector(".current-item"); - } - } else { - return document.querySelector(".entry"); - } -} - -function handleConfirmationMessage(linkElement, callback) { - if (linkElement.tagName != 'A') { - linkElement = linkElement.parentNode; - } - - linkElement.style.display = "none"; - - let containerElement = linkElement.parentNode; - let questionElement = document.createElement("span"); - - function createLoadingElement() { - let loadingElement = document.createElement("span"); - loadingElement.className = "loading"; - loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading)); - - questionElement.remove(); - containerElement.appendChild(loadingElement); - } - - let yesElement = document.createElement("a"); - yesElement.href = "#"; - yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes)); - yesElement.onclick = (event) => { - event.preventDefault(); - - createLoadingElement(); - - callback(linkElement.dataset.url, linkElement.dataset.redirectUrl); - }; - - let noElement = document.createElement("a"); - noElement.href = "#"; - noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo)); - noElement.onclick = (event) => { - event.preventDefault(); - - const noActionUrl = linkElement.dataset.noActionUrl; - if (noActionUrl) { - createLoadingElement(); - - callback(noActionUrl, linkElement.dataset.redirectUrl); - } else { - linkElement.style.display = "inline"; - questionElement.remove(); - } - }; - - questionElement.className = "confirm"; - questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion + " ")); - questionElement.appendChild(yesElement); - questionElement.appendChild(document.createTextNode(", ")); - questionElement.appendChild(noElement); - - containerElement.appendChild(questionElement); -} - -function showToast(label, iconElement) { - if (!label || !iconElement) { - return; - } - - const toastMsgElement = document.getElementById("toast-msg"); - if (toastMsgElement) { - toastMsgElement.innerHTML = iconElement.innerHTML + '<span class="icon-label">' + label + '</span>'; - - const toastElementWrapper = document.getElementById("toast-wrapper"); - if (toastElementWrapper) { - toastElementWrapper.classList.remove('toast-animate'); - setTimeout(function () { - toastElementWrapper.classList.add('toast-animate'); - }, 100); - } - } -} - -/** Navigate to the new subscription page. */ -function goToAddSubscription() { - window.location.href = document.body.dataset.addSubscriptionUrl; -} - -/** - * save player position to allow to resume playback later - * @param {Element} playerElement - */ -function handlePlayerProgressionSave(playerElement) { - const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value - const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10); - const recordInterval = 10; - - // we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds - if (currentPositionInSeconds >= (lastKnownPositionInSeconds + recordInterval) || - currentPositionInSeconds <= (lastKnownPositionInSeconds - recordInterval) - ) { - playerElement.dataset.lastPosition = currentPositionInSeconds.toString(); - let request = new RequestBuilder(playerElement.dataset.saveUrl); - request.withBody({progression: currentPositionInSeconds}); - request.execute(); - } -} - -/** - * handle new share entires and already shared entries - */ -function handleShare() { - let link = document.querySelector('a[data-share-status]'); - let title = document.querySelector("body > main > section > header > h1 > a"); - if (link.dataset.shareStatus === "shared") { - checkShareAPI(title, link.href); - } - if (link.dataset.shareStatus === "share") { - let request = new RequestBuilder(link.href); - request.withCallback((r) => { - checkShareAPI(title, r.url); - }); - request.withHttpMethod("GET"); - request.execute(); - } -} - -/** -* wrapper for Web Share API -*/ -function checkShareAPI(title, url) { - if (!navigator.canShare) { - console.error("Your browser doesn't support the Web Share API."); - window.location = url; - return; - } - try { - navigator.share({ - title: title, - url: url - }); - window.location.reload(); - } catch (err) { - console.error(err); - window.location.reload(); - } -}
\ No newline at end of file diff --git a/ui/static/js/bootstrap.js b/ui/static/js/bootstrap.js deleted file mode 100644 index 0cd878ef..00000000 --- a/ui/static/js/bootstrap.js +++ /dev/null @@ -1,126 +0,0 @@ -document.addEventListener("DOMContentLoaded", function () { - handleSubmitButtons(); - - if (!document.querySelector("body[data-disable-keyboard-shortcuts=true]")) { - let keyboardHandler = new KeyboardHandler(); - keyboardHandler.on("g u", () => goToPage("unread")); - keyboardHandler.on("g b", () => goToPage("starred")); - keyboardHandler.on("g h", () => goToPage("history")); - keyboardHandler.on("g f", () => goToFeedOrFeeds()); - keyboardHandler.on("g c", () => goToPage("categories")); - keyboardHandler.on("g s", () => goToPage("settings")); - keyboardHandler.on("ArrowLeft", () => goToPrevious()); - keyboardHandler.on("ArrowRight", () => goToNext()); - keyboardHandler.on("k", () => goToPrevious()); - keyboardHandler.on("p", () => goToPrevious()); - keyboardHandler.on("j", () => goToNext()); - keyboardHandler.on("n", () => goToNext()); - keyboardHandler.on("h", () => goToPage("previous")); - keyboardHandler.on("l", () => goToPage("next")); - keyboardHandler.on("z t", () => scrollToCurrentItem()); - keyboardHandler.on("o", () => openSelectedItem()); - keyboardHandler.on("v", () => openOriginalLink()); - keyboardHandler.on("V", () => openOriginalLink(true)); - keyboardHandler.on("c", () => openCommentLink()); - keyboardHandler.on("C", () => openCommentLink(true)); - keyboardHandler.on("m", () => handleEntryStatus("next")); - keyboardHandler.on("M", () => handleEntryStatus("previous")); - keyboardHandler.on("A", () => markPageAsRead()); - keyboardHandler.on("s", () => handleSaveEntry()); - keyboardHandler.on("d", () => handleFetchOriginalContent()); - keyboardHandler.on("f", () => handleBookmark()); - keyboardHandler.on("F", () => goToFeed()); - keyboardHandler.on("R", () => handleRefreshAllFeeds()); - keyboardHandler.on("?", () => showKeyboardShortcuts()); - keyboardHandler.on("+", () => goToAddSubscription()); - keyboardHandler.on("#", () => unsubscribeFromFeed()); - keyboardHandler.on("/", (e) => setFocusToSearchInput(e)); - keyboardHandler.on("a", () => { - let enclosureElement = document.querySelector('.entry-enclosures'); - if (enclosureElement) { - enclosureElement.toggleAttribute('open'); - } - }); - keyboardHandler.on("Escape", () => ModalHandler.close()); - keyboardHandler.listen(); - } - - let touchHandler = new TouchHandler(); - touchHandler.listen(); - - onClick("a[data-save-entry]", (event) => handleSaveEntry(event.target)); - onClick("a[data-toggle-bookmark]", (event) => handleBookmark(event.target)); - onClick("a[data-fetch-content-entry]", () => handleFetchOriginalContent()); - onClick("a[data-action=search]", (event) => setFocusToSearchInput(event)); - onClick("a[data-share-status]", () => handleShare()); - onClick("a[data-action=markPageAsRead]", (event) => handleConfirmationMessage(event.target, () => markPageAsRead())); - onClick("a[data-toggle-status]", (event) => handleEntryStatus("next", event.target)); - - onClick("a[data-confirm]", (event) => handleConfirmationMessage(event.target, (url, redirectURL) => { - let request = new RequestBuilder(url); - - request.withCallback((response) => { - if (redirectURL) { - window.location.href = redirectURL; - } else if (response && response.redirected && response.url) { - window.location.href = response.url; - } else { - window.location.reload(); - } - }); - - request.execute(); - })); - - onClick("a[data-original-link='true']", (event) => { - handleEntryStatus("next", event.target, true); - }, true); - onAuxClick("a[data-original-link='true']", (event) => { - if (event.button == 1) { - handleEntryStatus("next", event.target, true); - } - }, true); - - if (document.documentElement.clientWidth < 600) { - onClick(".logo", () => toggleMainMenu()); - onClick(".header nav li", (event) => onClickMainMenuListItem(event)); - } - - if ("serviceWorker" in navigator) { - let scriptElement = document.getElementById("service-worker-script"); - if (scriptElement) { - navigator.serviceWorker.register(scriptElement.src); - } - } - - window.addEventListener('beforeinstallprompt', (e) => { - // Prevent Chrome 67 and earlier from automatically showing the prompt. - e.preventDefault(); - - let deferredPrompt = e; - const promptHomeScreen = document.getElementById('prompt-home-screen'); - if (promptHomeScreen) { - promptHomeScreen.style.display = "block"; - - const btnAddToHomeScreen = document.getElementById('btn-add-to-home-screen'); - if (btnAddToHomeScreen) { - btnAddToHomeScreen.addEventListener('click', (e) => { - e.preventDefault(); - deferredPrompt.prompt(); - deferredPrompt.userChoice.then(() => { - deferredPrompt = null; - promptHomeScreen.style.display = "none"; - }); - }); - } - } - }); - - // enclosure media player position save & resume - const elements = document.querySelectorAll("audio[data-last-position],video[data-last-position]"); - elements.forEach((element) => { - // we set the current time of media players - if (element.dataset.lastPosition){ element.currentTime = element.dataset.lastPosition; } - element.ontimeupdate = () => handlePlayerProgressionSave(element); - }); -}); diff --git a/ui/static/js/dom_helper.js b/ui/static/js/dom_helper.js deleted file mode 100644 index fffa6965..00000000 --- a/ui/static/js/dom_helper.js +++ /dev/null @@ -1,65 +0,0 @@ -class DomHelper { - static isVisible(element) { - return element.offsetParent !== null; - } - - static openNewTab(url) { - let win = window.open(""); - win.opener = null; - win.location = url; - win.focus(); - } - - static scrollPageTo(element, evenIfOnScreen) { - let windowScrollPosition = window.pageYOffset; - let windowHeight = document.documentElement.clientHeight; - let viewportPosition = windowScrollPosition + windowHeight; - let itemBottomPosition = element.offsetTop + element.offsetHeight; - - if (evenIfOnScreen || viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) { - window.scrollTo(0, element.offsetTop - 10); - } - } - - static 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; - } - - static findParent(element, selector) { - for (; element && element !== document; element = element.parentNode) { - if (element.classList.contains(selector)) { - return element; - } - } - - return null; - } - - static hasPassiveEventListenerOption() { - var passiveSupported = false; - - try { - var options = Object.defineProperty({}, "passive", { - get: function() { - passiveSupported = true; - } - }); - - window.addEventListener("test", options, options); - window.removeEventListener("test", options, options); - } catch(err) { - passiveSupported = false; - } - - return passiveSupported; - } -} diff --git a/ui/static/js/keyboard_handler.js b/ui/static/js/keyboard_handler.js deleted file mode 100644 index 037f9949..00000000 --- a/ui/static/js/keyboard_handler.js +++ /dev/null @@ -1,72 +0,0 @@ -class KeyboardHandler { - constructor() { - this.queue = []; - this.shortcuts = {}; - this.triggers = []; - } - - on(combination, callback) { - this.shortcuts[combination] = callback; - this.triggers.push(combination.split(" ")[0]); - } - - listen() { - document.onkeydown = (event) => { - let key = this.getKey(event); - if (this.isEventIgnored(event, key) || this.isModifierKeyDown(event)) { - return; - } - - event.preventDefault(); - 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](event); - return; - } - - if (keys.length === 1 && key === keys[0]) { - this.queue = []; - this.shortcuts[combination](event); - return; - } - } - - if (this.queue.length >= 2) { - this.queue = []; - } - }; - } - - isEventIgnored(event, key) { - return event.target.tagName === "INPUT" || - event.target.tagName === "TEXTAREA" || - (this.queue.length < 1 && !this.triggers.includes(key)); - } - - isModifierKeyDown(event) { - return event.getModifierState("Control") || event.getModifierState("Alt") || event.getModifierState("Meta"); - } - - 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; - } -} diff --git a/ui/static/js/modal_handler.js b/ui/static/js/modal_handler.js deleted file mode 100644 index 0fa55bfa..00000000 --- a/ui/static/js/modal_handler.js +++ /dev/null @@ -1,101 +0,0 @@ -class ModalHandler { - static exists() { - return document.getElementById("modal-container") !== null; - } - - static getModalContainer() { - return document.getElementById("modal-container"); - } - - static getFocusableElements() { - let container = this.getModalContainer(); - - if (container === null) { - return null; - } - - return container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); - } - - static setupFocusTrap() { - let focusableElements = this.getFocusableElements(); - - if (focusableElements === null) { - return; - } - - let firstFocusableElement = focusableElements[0]; - let lastFocusableElement = focusableElements[focusableElements.length - 1]; - - this.getModalContainer().onkeydown = (e) => { - if (e.key !== 'Tab') { - return; - } - - // If there is only one focusable element in the dialog we always want to focus that one with the tab key. - // This handles the special case of having just one focusable element in a dialog where keyboard focus is placed on an element that is not in the tab order. - if (focusableElements.length === 1) { - firstFocusableElement.focus(); - e.preventDefault(); - return; - } - - if (e.shiftKey && document.activeElement === firstFocusableElement) { - lastFocusableElement.focus(); - e.preventDefault(); - } else if (!e.shiftKey && document.activeElement === lastFocusableElement) { - firstFocusableElement.focus(); - e.preventDefault(); - } - }; - } - - static open(fragment, initialFocusElementId) { - if (ModalHandler.exists()) { - return; - } - - this.activeElement = document.activeElement; - - let container = document.createElement("div"); - container.id = "modal-container"; - container.setAttribute("role", "dialog"); - container.appendChild(document.importNode(fragment, true)); - document.body.appendChild(container); - - let closeButton = document.querySelector("button.btn-close-modal"); - if (closeButton !== null) { - closeButton.onclick = (event) => { - event.preventDefault(); - ModalHandler.close(); - }; - } - - let initialFocusElement; - if (initialFocusElementId !== undefined) { - initialFocusElement = document.getElementById(initialFocusElementId); - } else { - let focusableElements = this.getFocusableElements(); - if (focusableElements !== null) { - initialFocusElement = focusableElements[0]; - } - } - - if (initialFocusElement !== undefined) { - initialFocusElement.focus(); - } - - this.setupFocusTrap(); - } - - static close() { - let container = this.getModalContainer(); - if (container !== null) { - container.parentNode.removeChild(container); - } - - if (this.activeElement !== undefined && this.activeElement !== null) { - this.activeElement.focus(); - } - } -} diff --git a/ui/static/js/request_builder.js b/ui/static/js/request_builder.js deleted file mode 100644 index e19168fc..00000000 --- a/ui/static/js/request_builder.js +++ /dev/null @@ -1,48 +0,0 @@ -class RequestBuilder { - constructor(url) { - this.callback = null; - this.url = url; - this.options = { - method: "POST", - cache: "no-cache", - credentials: "include", - body: null, - headers: new Headers({ - "Content-Type": "application/json", - "X-Csrf-Token": this.getCsrfToken() - }) - }; - } - - withHttpMethod(method) { - this.options.method = method; - return this; - } - - withBody(body) { - this.options.body = JSON.stringify(body); - return this; - } - - withCallback(callback) { - this.callback = callback; - return this; - } - - getCsrfToken() { - let element = document.querySelector("body[data-csrf-token]"); - if (element !== null) { - return element.dataset.csrfToken; - } - - return ""; - } - - execute() { - fetch(new Request(this.url, this.options)).then((response) => { - if (this.callback) { - this.callback(response); - } - }); - } -} diff --git a/ui/static/js/service_worker.js b/ui/static/js/service_worker.js deleted file mode 100644 index 37cce257..00000000 --- a/ui/static/js/service_worker.js +++ /dev/null @@ -1,44 +0,0 @@ - -// Incrementing OFFLINE_VERSION will kick off the install event and force -// previously cached resources to be updated from the network. -const OFFLINE_VERSION = 1; -const CACHE_NAME = "offline"; - -self.addEventListener("install", (event) => { - event.waitUntil( - (async () => { - const cache = await caches.open(CACHE_NAME); - - // Setting {cache: 'reload'} in the new request will ensure that the - // response isn't fulfilled from the HTTP cache; i.e., it will be from - // the network. - await cache.add(new Request(OFFLINE_URL, { cache: "reload" })); - })() - ); - - // Force the waiting service worker to become the active service worker. - self.skipWaiting(); -}); - -self.addEventListener("fetch", (event) => { - // We proxify requests through fetch() only if we are offline because it's slower. - if (navigator.onLine === false && event.request.mode === "navigate") { - event.respondWith( - (async () => { - try { - // Always try the network first. - const networkResponse = await fetch(event.request); - return networkResponse; - } catch (error) { - // catch is only triggered if an exception is thrown, which is likely - // due to a network error. - // If fetch() returns a valid HTTP response with a response code in - // the 4xx or 5xx range, the catch() will NOT be called. - const cache = await caches.open(CACHE_NAME); - const cachedResponse = await cache.match(OFFLINE_URL); - return cachedResponse; - } - })() - ); - } -}); diff --git a/ui/static/js/touch_handler.js b/ui/static/js/touch_handler.js deleted file mode 100644 index 99c1d5b2..00000000 --- a/ui/static/js/touch_handler.js +++ /dev/null @@ -1,187 +0,0 @@ -class TouchHandler { - constructor() { - this.reset(); - } - - reset() { - this.touch = { - start: { x: -1, y: -1 }, - move: { x: -1, y: -1 }, - moved: false, - time: 0, - element: null - }; - } - - calculateDistance() { - if (this.touch.start.x >= -1 && this.touch.move.x >= -1) { - let horizontalDistance = Math.abs(this.touch.move.x - this.touch.start.x); - let verticalDistance = Math.abs(this.touch.move.y - this.touch.start.y); - - if (horizontalDistance > 30 && verticalDistance < 70 || this.touch.moved) { - return this.touch.move.x - this.touch.start.x; - } - } - - return 0; - } - - findElement(element) { - if (element.classList.contains("entry-swipe")) { - return element; - } - - return DomHelper.findParent(element, "entry-swipe"); - } - - onItemTouchStart(event) { - if (event.touches === undefined || event.touches.length !== 1) { - return; - } - - this.reset(); - this.touch.start.x = event.touches[0].clientX; - this.touch.start.y = event.touches[0].clientY; - this.touch.element = this.findElement(event.touches[0].target); - this.touch.element.style.transitionDuration = "0s"; - } - - onItemTouchMove(event) { - if (event.touches === undefined || event.touches.length !== 1 || this.element === null) { - return; - } - - this.touch.move.x = event.touches[0].clientX; - this.touch.move.y = event.touches[0].clientY; - - let distance = this.calculateDistance(); - let absDistance = Math.abs(distance); - - if (absDistance > 0) { - this.touch.moved = true; - - let tx = absDistance > 75 ? Math.pow(absDistance - 75, 0.5) + 75 : absDistance; - - if (distance < 0) { - tx = -tx; - } - - this.touch.element.style.transform = "translateX(" + tx + "px)"; - - event.preventDefault(); - } - } - - onItemTouchEnd(event) { - if (event.touches === undefined) { - return; - } - - if (this.touch.element !== null) { - let absDistance = Math.abs(this.calculateDistance()); - - if (absDistance > 75) { - toggleEntryStatus(this.touch.element); - } - - if (this.touch.moved) { - this.touch.element.style.transitionDuration = "0.15s"; - this.touch.element.style.transform = "none"; - } - } - - this.reset(); - } - - onContentTouchStart(event) { - if (event.touches === undefined || event.touches.length !== 1) { - return; - } - - this.reset(); - this.touch.start.x = event.touches[0].clientX; - this.touch.start.y = event.touches[0].clientY; - this.touch.time = Date.now(); - } - - onContentTouchMove(event) { - if (event.touches === undefined || event.touches.length !== 1 || this.element === null) { - return; - } - - this.touch.move.x = event.touches[0].clientX; - this.touch.move.y = event.touches[0].clientY; - } - - onContentTouchEnd(event) { - if (event.touches === undefined) { - return; - } - - let distance = this.calculateDistance(); - let absDistance = Math.abs(distance); - let now = Date.now(); - - if (now - this.touch.time <= 1000 && absDistance > 75) { - if (distance > 0) { - goToPage("previous"); - } else { - goToPage("next"); - } - } - - this.reset(); - } - - onTapEnd(event) { - if (event.touches === undefined) { - return; - } - - let now = Date.now(); - - if (this.touch.start.x !== -1 && now - this.touch.time <= 200) { - let innerWidthHalf = window.innerWidth / 2; - - if (this.touch.start.x >= innerWidthHalf && event.changedTouches[0].clientX >= innerWidthHalf) { - goToPage("next"); - } else if (this.touch.start.x < innerWidthHalf && event.changedTouches[0].clientX < innerWidthHalf) { - goToPage("previous"); - } - - this.reset(); - } else { - this.reset(); - this.touch.start.x = event.changedTouches[0].clientX; - this.touch.time = now; - } - } - - listen() { - let hasPassiveOption = DomHelper.hasPassiveEventListenerOption(); - - let elements = document.querySelectorAll(".entry-swipe"); - - elements.forEach((element) => { - element.addEventListener("touchstart", (e) => this.onItemTouchStart(e), hasPassiveOption ? { passive: true } : false); - element.addEventListener("touchmove", (e) => this.onItemTouchMove(e), hasPassiveOption ? { passive: false } : false); - element.addEventListener("touchend", (e) => this.onItemTouchEnd(e), hasPassiveOption ? { passive: true } : false); - element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false); - }); - - let element = document.querySelector(".entry-content"); - - if (element) { - if (element.classList.contains("gesture-nav-tap")) { - element.addEventListener("touchend", (e) => this.onTapEnd(e), hasPassiveOption ? { passive: true } : false); - element.addEventListener("touchmove", () => this.reset(), hasPassiveOption ? { passive: true } : false); - element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false); - } else if (element.classList.contains("gesture-nav-swipe")) { - element.addEventListener("touchstart", (e) => this.onContentTouchStart(e), hasPassiveOption ? { passive: true } : false); - element.addEventListener("touchmove", (e) => this.onContentTouchMove(e), hasPassiveOption ? { passive: true } : false); - element.addEventListener("touchend", (e) => this.onContentTouchEnd(e), hasPassiveOption ? { passive: true } : false); - element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false); - } - } - } -} |