diff options/options.js @ 10:ff5e5e3eba32

Implement feed subscription for web-based feed readers Add options page for configuring web-based feed readers which allow for subscribing to feeds via GET requests. Track tabs containing feed previews and inject a content script which retrieves the configured feed readers and keeps them in sync.
author Guido Berhoerster <guido+feed-preview@berhoerster.name>
date Fri, 07 Dec 2018 23:00:41 +0100
parents
children 688d75e554e0
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/options/options.js	Fri Dec 07 23:00:41 2018 +0100
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+'use strict';
+
+function normalizeURL(text) {
+    return new URL(text).toString();
+}
+
+class OptionsPage {
+    constructor() {
+        this.selectedFeedReader = -1;
+
+        document.querySelector('#feed-readers-title').textContent =
+                browser.i18n.getMessage('feedReadersTitle');
+
+        let feedReadersForm = document.forms['feed-readers'];
+        feedReadersForm.elements['move-up'].textContent =
+                browser.i18n.getMessage('feedReaderMoveUpButton');
+        feedReadersForm.elements['move-down'].textContent =
+                browser.i18n.getMessage('feedReaderMoveDownButton');
+        feedReadersForm.elements['remove'].textContent =
+                browser.i18n.getMessage('feedReaderRemoveButton');
+        feedReadersForm.addEventListener('change', this);
+
+        let addFeedReaderForm = document.forms['add-feed-reader'];
+        addFeedReaderForm.elements['add'].textContent =
+                browser.i18n.getMessage('feedReaderAddButton');
+        let titleElement = addFeedReaderForm.elements['title'];
+        titleElement.labels[0].textContent =
+                browser.i18n.getMessage('feedReaderTitleLabel');
+        titleElement.placeholder =
+                browser.i18n.getMessage('feedReaderTitlePlaceholder');
+        let urlTemplateElement =
+                addFeedReaderForm.elements['url-template'];
+        urlTemplateElement.labels[0].textContent =
+                browser.i18n.getMessage('feedReaderUrlTemplateLabel');
+        urlTemplateElement.placeholder =
+                browser.i18n.getMessage('feedReaderUrlTemplatePlaceholder');
+        document.querySelector('#feed-reader-url-caption').textContent =
+                browser.i18n.getMessage('feedReaderUrlTemplateCaption');
+        addFeedReaderForm.addEventListener('focusout', this);
+
+        document.addEventListener('submit', this);
+
+        this.initOptions();
+    }
+
+    async initOptions() {
+        let {feedReaders} = await browser.storage.sync.get('feedReaders');
+        if (Array.isArray(feedReaders)) {
+            console.log('initialized feedReaders from storage', feedReaders);
+            this.updateFeedReaders(feedReaders);
+        }
+
+        browser.storage.onChanged.addListener(this.onStorageChanged.bind(this));
+    }
+
+    validateURLTemplate(text) {
+        let url;
+        try {
+            url = new URL(text);
+        } catch(e) {
+            if (e instanceof TypeError) {
+                return browser.i18n.getMessage('invalidURLError');
+            }
+            throw e;
+        }
+
+        if (url.protocol !== 'http:' && url.protocol !== 'https:') {
+            return browser.i18n.getMessage('invalidProtocolError');
+        }
+
+        if (!(url.pathname.includes('%s') || url.search.includes('%s'))) {
+            return browser.i18n.getMessage('missingPlaceholderError');
+        }
+
+        return '';
+    }
+
+    updateFeedReaders(feedReaders) {
+        let feedReadersForm = document.forms['feed-readers'];
+        let feedReaderItemElements =
+                feedReadersForm.querySelectorAll('.feed-reader-item');
+        for (let feedReaderItemElement of feedReaderItemElements) {
+            feedReaderItemElement.remove();
+        }
+
+        let feedReaderItemTemplateElement =
+                document.querySelector('#feed-reader-item-template');
+        let feedReaderSelectionElement =
+                feedReadersForm.querySelector('#feed-reader-selection')
+        for (let feedReader of feedReaders) {
+            let feedReaderItemNode =
+                    document.importNode(feedReaderItemTemplateElement.content,
+                    true);
+            let feedReaderInputElement =
+                    feedReaderItemNode.querySelector('input[name=feed-reader]');
+            feedReaderInputElement.dataset.title = feedReader.title;
+            feedReaderInputElement.value = feedReader.urlTemplate;
+            feedReaderItemNode.querySelector('.feed-reader-title')
+                    .textContent = feedReader.title;
+            feedReaderItemNode.querySelector('.feed-reader-url-template')
+                    .textContent = feedReader.urlTemplate;
+            feedReaderSelectionElement.append(feedReaderItemNode);
+        }
+
+        feedReadersForm.elements['buttons'].disabled = true;
+    }
+
+    getFeedReaders() {
+        let feedReaderInput =
+                document.forms['feed-readers'].elements['feed-reader'];
+        if (feedReaderInput instanceof RadioNodeList) {
+            return Array.from(feedReaderInput);
+        } else if (typeof feedReaderInput === 'undefined') {
+            return [];
+        }
+        return Array.from([feedReaderInput]);
+    }
+
+    selectFeedReader() {
+        console.debug('selected:', this.selectedFeedReader);
+        if (this.selectedFeedReader < 0) {
+            return;
+        }
+
+        let feedReadersForm = document.forms['feed-readers'];
+        let feedReaderElements = this.getFeedReaders();
+        feedReaderElements[this.selectedFeedReader].checked = true;
+        // ensure that the checked element will also be the focused one the
+        // next time the radio input group receives focus
+        let activeElement = document.activeElement;
+        feedReaderElements[this.selectedFeedReader].focus();
+        activeElement.focus();
+
+        feedReadersForm.elements['buttons'].disabled = false;
+    }
+
+    serializeFeedReaders() {
+        return this.getFeedReaders().map(element => ({
+            title: element.dataset.title,
+            urlTemplate: element.value
+        }));
+    }
+
+    onStorageChanged(changes, areaName) {
+        if (areaName !== 'sync' || typeof changes.feedReaders === 'undefined') {
+            return;
+        }
+
+        let feedReaders;
+        if (typeof changes.feedReaders.newValue !== 'undefined' &&
+                Array.isArray(changes.feedReaders.newValue)) {
+            feedReaders = changes.feedReaders.newValue;
+            console.log('feedReaders changed to', feedReaders);
+        } else {
+            // list of feed readers was removed or set to nonsensical value
+            feedReaders = [];
+            console.log('feedReaders was removed');
+        }
+        if (this.selectedFeedReader >= feedReaders.length) {
+            // save selected feed reader is no longer valid
+            this.selectedFeedReader = -1;
+        }
+        this.updateFeedReaders(feedReaders);
+        this.selectFeedReader();
+    }
+
+    handleEvent(ev) {
+        console.log('previously selected:', this.selectedFeedReader);
+        if (ev.type === 'change' && ev.target.name === 'feed-reader') {
+            // feed reader was selected by user interaction
+            console.debug(ev);
+            this.selectedFeedReader = this.getFeedReaders().indexOf(ev.target);
+            console.log('now selected:', this.selectedFeedReader);
+
+            document.forms['feed-readers'].elements['buttons'].disabled = false;
+        } else if (ev.type === 'submit' && ev.target.id === 'feed-readers') {
+            // remove feed reader or move feed reader up or down
+            ev.preventDefault();
+
+            let feedReaders = this.serializeFeedReaders();
+            if (ev.explicitOriginalTarget.name === 'move-up') {
+                if (this.selectedFeedReader - 1 < 0) {
+                    // the first feed reader is selected
+                    return;
+                }
+                [feedReaders[this.selectedFeedReader - 1],
+                        feedReaders[this.selectedFeedReader]] =
+                        [feedReaders[this.selectedFeedReader],
+                        feedReaders[this.selectedFeedReader - 1]];
+                this.selectedFeedReader--;
+            } else if (ev.explicitOriginalTarget.name === 'move-down') {
+                if (this.selectedFeedReader + 1 === feedReaders.length) {
+                    // the last feed reader is selected
+                    return;
+                }
+                [feedReaders[this.selectedFeedReader + 1],
+                        feedReaders[this.selectedFeedReader]] =
+                        [feedReaders[this.selectedFeedReader],
+                        feedReaders[this.selectedFeedReader + 1]];
+                this.selectedFeedReader++;
+            } else if (ev.explicitOriginalTarget.name === 'remove') {
+                feedReaders.splice(this.selectedFeedReader, 1);
+                this.selectedFeedReader--;
+            }
+            browser.storage.sync.set({feedReaders});
+            console.log('set feedReaders to ', feedReaders);
+        } else if (ev.type === 'focusout' &&
+                ev.target.name === 'url-template') {
+            // url template was changed
+            let validity = this.validateURLTemplate(ev.target.value);
+            ev.target.setCustomValidity(validity);
+        } else if (ev.type === 'submit' &&
+                ev.target.id === 'add-feed-reader') {
+            // feed reader added
+            ev.preventDefault();
+
+            let urlTemplate = ev.target.elements['url-template'].value;
+            let isValid = this.validateURLTemplate(urlTemplate);
+            ev.target.elements['url-template'].setCustomValidity(isValid);
+            if (!ev.target.reportValidity()) {
+                return;
+            }
+
+            let feedReaders = this.serializeFeedReaders();
+            feedReaders.push({
+                title: ev.target.elements['title'].value,
+                urlTemplate: normalizeURL(urlTemplate)
+            });
+            browser.storage.sync.set({feedReaders});
+            console.log('set feedReaders to', feedReaders);
+
+            document.forms['add-feed-reader'].reset();
+        }
+    }
+}
+
+var page = new OptionsPage();