Mercurial > addons > firefox-addons > feed-preview
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();