comparison 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
comparison
equal deleted inserted replaced
9:fcd65cf3f634 10:ff5e5e3eba32
1 /*
2 * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name>
3 *
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 */
8
9 'use strict';
10
11 function normalizeURL(text) {
12 return new URL(text).toString();
13 }
14
15 class OptionsPage {
16 constructor() {
17 this.selectedFeedReader = -1;
18
19 document.querySelector('#feed-readers-title').textContent =
20 browser.i18n.getMessage('feedReadersTitle');
21
22 let feedReadersForm = document.forms['feed-readers'];
23 feedReadersForm.elements['move-up'].textContent =
24 browser.i18n.getMessage('feedReaderMoveUpButton');
25 feedReadersForm.elements['move-down'].textContent =
26 browser.i18n.getMessage('feedReaderMoveDownButton');
27 feedReadersForm.elements['remove'].textContent =
28 browser.i18n.getMessage('feedReaderRemoveButton');
29 feedReadersForm.addEventListener('change', this);
30
31 let addFeedReaderForm = document.forms['add-feed-reader'];
32 addFeedReaderForm.elements['add'].textContent =
33 browser.i18n.getMessage('feedReaderAddButton');
34 let titleElement = addFeedReaderForm.elements['title'];
35 titleElement.labels[0].textContent =
36 browser.i18n.getMessage('feedReaderTitleLabel');
37 titleElement.placeholder =
38 browser.i18n.getMessage('feedReaderTitlePlaceholder');
39 let urlTemplateElement =
40 addFeedReaderForm.elements['url-template'];
41 urlTemplateElement.labels[0].textContent =
42 browser.i18n.getMessage('feedReaderUrlTemplateLabel');
43 urlTemplateElement.placeholder =
44 browser.i18n.getMessage('feedReaderUrlTemplatePlaceholder');
45 document.querySelector('#feed-reader-url-caption').textContent =
46 browser.i18n.getMessage('feedReaderUrlTemplateCaption');
47 addFeedReaderForm.addEventListener('focusout', this);
48
49 document.addEventListener('submit', this);
50
51 this.initOptions();
52 }
53
54 async initOptions() {
55 let {feedReaders} = await browser.storage.sync.get('feedReaders');
56 if (Array.isArray(feedReaders)) {
57 console.log('initialized feedReaders from storage', feedReaders);
58 this.updateFeedReaders(feedReaders);
59 }
60
61 browser.storage.onChanged.addListener(this.onStorageChanged.bind(this));
62 }
63
64 validateURLTemplate(text) {
65 let url;
66 try {
67 url = new URL(text);
68 } catch(e) {
69 if (e instanceof TypeError) {
70 return browser.i18n.getMessage('invalidURLError');
71 }
72 throw e;
73 }
74
75 if (url.protocol !== 'http:' && url.protocol !== 'https:') {
76 return browser.i18n.getMessage('invalidProtocolError');
77 }
78
79 if (!(url.pathname.includes('%s') || url.search.includes('%s'))) {
80 return browser.i18n.getMessage('missingPlaceholderError');
81 }
82
83 return '';
84 }
85
86 updateFeedReaders(feedReaders) {
87 let feedReadersForm = document.forms['feed-readers'];
88 let feedReaderItemElements =
89 feedReadersForm.querySelectorAll('.feed-reader-item');
90 for (let feedReaderItemElement of feedReaderItemElements) {
91 feedReaderItemElement.remove();
92 }
93
94 let feedReaderItemTemplateElement =
95 document.querySelector('#feed-reader-item-template');
96 let feedReaderSelectionElement =
97 feedReadersForm.querySelector('#feed-reader-selection')
98 for (let feedReader of feedReaders) {
99 let feedReaderItemNode =
100 document.importNode(feedReaderItemTemplateElement.content,
101 true);
102 let feedReaderInputElement =
103 feedReaderItemNode.querySelector('input[name=feed-reader]');
104 feedReaderInputElement.dataset.title = feedReader.title;
105 feedReaderInputElement.value = feedReader.urlTemplate;
106 feedReaderItemNode.querySelector('.feed-reader-title')
107 .textContent = feedReader.title;
108 feedReaderItemNode.querySelector('.feed-reader-url-template')
109 .textContent = feedReader.urlTemplate;
110 feedReaderSelectionElement.append(feedReaderItemNode);
111 }
112
113 feedReadersForm.elements['buttons'].disabled = true;
114 }
115
116 getFeedReaders() {
117 let feedReaderInput =
118 document.forms['feed-readers'].elements['feed-reader'];
119 if (feedReaderInput instanceof RadioNodeList) {
120 return Array.from(feedReaderInput);
121 } else if (typeof feedReaderInput === 'undefined') {
122 return [];
123 }
124 return Array.from([feedReaderInput]);
125 }
126
127 selectFeedReader() {
128 console.debug('selected:', this.selectedFeedReader);
129 if (this.selectedFeedReader < 0) {
130 return;
131 }
132
133 let feedReadersForm = document.forms['feed-readers'];
134 let feedReaderElements = this.getFeedReaders();
135 feedReaderElements[this.selectedFeedReader].checked = true;
136 // ensure that the checked element will also be the focused one the
137 // next time the radio input group receives focus
138 let activeElement = document.activeElement;
139 feedReaderElements[this.selectedFeedReader].focus();
140 activeElement.focus();
141
142 feedReadersForm.elements['buttons'].disabled = false;
143 }
144
145 serializeFeedReaders() {
146 return this.getFeedReaders().map(element => ({
147 title: element.dataset.title,
148 urlTemplate: element.value
149 }));
150 }
151
152 onStorageChanged(changes, areaName) {
153 if (areaName !== 'sync' || typeof changes.feedReaders === 'undefined') {
154 return;
155 }
156
157 let feedReaders;
158 if (typeof changes.feedReaders.newValue !== 'undefined' &&
159 Array.isArray(changes.feedReaders.newValue)) {
160 feedReaders = changes.feedReaders.newValue;
161 console.log('feedReaders changed to', feedReaders);
162 } else {
163 // list of feed readers was removed or set to nonsensical value
164 feedReaders = [];
165 console.log('feedReaders was removed');
166 }
167 if (this.selectedFeedReader >= feedReaders.length) {
168 // save selected feed reader is no longer valid
169 this.selectedFeedReader = -1;
170 }
171 this.updateFeedReaders(feedReaders);
172 this.selectFeedReader();
173 }
174
175 handleEvent(ev) {
176 console.log('previously selected:', this.selectedFeedReader);
177 if (ev.type === 'change' && ev.target.name === 'feed-reader') {
178 // feed reader was selected by user interaction
179 console.debug(ev);
180 this.selectedFeedReader = this.getFeedReaders().indexOf(ev.target);
181 console.log('now selected:', this.selectedFeedReader);
182
183 document.forms['feed-readers'].elements['buttons'].disabled = false;
184 } else if (ev.type === 'submit' && ev.target.id === 'feed-readers') {
185 // remove feed reader or move feed reader up or down
186 ev.preventDefault();
187
188 let feedReaders = this.serializeFeedReaders();
189 if (ev.explicitOriginalTarget.name === 'move-up') {
190 if (this.selectedFeedReader - 1 < 0) {
191 // the first feed reader is selected
192 return;
193 }
194 [feedReaders[this.selectedFeedReader - 1],
195 feedReaders[this.selectedFeedReader]] =
196 [feedReaders[this.selectedFeedReader],
197 feedReaders[this.selectedFeedReader - 1]];
198 this.selectedFeedReader--;
199 } else if (ev.explicitOriginalTarget.name === 'move-down') {
200 if (this.selectedFeedReader + 1 === feedReaders.length) {
201 // the last feed reader is selected
202 return;
203 }
204 [feedReaders[this.selectedFeedReader + 1],
205 feedReaders[this.selectedFeedReader]] =
206 [feedReaders[this.selectedFeedReader],
207 feedReaders[this.selectedFeedReader + 1]];
208 this.selectedFeedReader++;
209 } else if (ev.explicitOriginalTarget.name === 'remove') {
210 feedReaders.splice(this.selectedFeedReader, 1);
211 this.selectedFeedReader--;
212 }
213 browser.storage.sync.set({feedReaders});
214 console.log('set feedReaders to ', feedReaders);
215 } else if (ev.type === 'focusout' &&
216 ev.target.name === 'url-template') {
217 // url template was changed
218 let validity = this.validateURLTemplate(ev.target.value);
219 ev.target.setCustomValidity(validity);
220 } else if (ev.type === 'submit' &&
221 ev.target.id === 'add-feed-reader') {
222 // feed reader added
223 ev.preventDefault();
224
225 let urlTemplate = ev.target.elements['url-template'].value;
226 let isValid = this.validateURLTemplate(urlTemplate);
227 ev.target.elements['url-template'].setCustomValidity(isValid);
228 if (!ev.target.reportValidity()) {
229 return;
230 }
231
232 let feedReaders = this.serializeFeedReaders();
233 feedReaders.push({
234 title: ev.target.elements['title'].value,
235 urlTemplate: normalizeURL(urlTemplate)
236 });
237 browser.storage.sync.set({feedReaders});
238 console.log('set feedReaders to', feedReaders);
239
240 document.forms['add-feed-reader'].reset();
241 }
242 }
243 }
244
245 var page = new OptionsPage();