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