-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
primerpedia.js
319 lines (257 loc) · 11.2 KB
/
primerpedia.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/**
* @file Primerpedia Script Entry Point
*
* There are 3 ways (that I know about) of getting the first section of an article.
* 1) We can use action=parse coupled with the section=0 parameter,
* but the "page" parameter needs to be supplied,
* which means that we'd have to make two API requests
* (either to get a random page, or to resolve a search term)
* btw, this action seems to have a mobileformat= parameter, but the resuts are poor.
* 2) Another option is to use the prop=revisions query, coupled with rvprop=content,
* &rvsection=0 and rvparse=true (see https://stackoverflow.com/q/13517901/266309).
* This request can be combined with a random article generator,
* using generator=random&grnnamespace=0,
* or with a search generator, using generator=search.
* But the parser returns the full html, so we'd have to clean it up ourselves.
* 3) The third option (and the one we are using at the moment) is the prop=extracts query
* (from Extension:TextExtracts, https://www.mediawiki.org/wiki/Extension:TextExtracts),
* coupled with the "exintro" parameter.
* This can be paired with a random page generator, or a search generator.
* The cleanup is pretty good, but links are removed (maybe not a bad thing)
* and some templates are still displayed, so they need to be hidden via css.
*/
// Declare eslint globals (needed because we're using separated files)
/* global clearNode, toggleVisibility, getQueryVariable */
var wikipediaUrl = "wikipedia.org";
var requestTimeoutInMs = 3000;
var requestCallbackName = "requestCallback";
var notificationTimeoutInMs = 3000;
var notificationTimeout = null;
var apiUrl = wikipediaUrl + "/w/api.php";
var articleIntroQuery = "action=query&prop=extracts&exintro&indexpageids=true&format=json";
var editIntroQuery = "?action=edit&section=0";
var searchQuery = "&generator=search&gsrlimit=1&gsrsearch=";
var randomArticleQuery = "&generator=random&grnnamespace=0";
var notificationElement = null;
var notificationContentElement = null;
var searchTermInputElement = null;
var languageInputElement = null;
var searchButton = null;
var contentDivElement = null;
var viewLinkElem = null;
var editLinkElement = null;
var copyShareLinkElement = null;
var articleTitleElement = null;
var licenseIconElement = null;
var infoIconElement = null;
var copyShareLinkInputElement = null;
var copyInputContainer = null;
/**
* Used to trigger search for a random topic
*/
// Disable eslint's no-unused-vars warning for the next line.
// Needed because this function is currently only called via HTML
// eslint-disable-next-line no-unused-vars
function random() {
searchTermInputElement.value = "";
var language = getLanguageCode();
apiRequest(language, articleIntroQuery + randomArticleQuery);
}
function search() {
updateSearchButtonEnabledState();
var searchTerm = searchTermInputElement.value;
var language = getLanguageCode();
if(typeof searchTerm === "string" && searchTerm.length > 0) {
apiRequest(language, articleIntroQuery + searchQuery + searchTerm.replace(/ /g, "_"));
}
}
function renderLoadingSpinner() {
// Show animated loading spinner -- from https://commons.wikimedia.org/wiki/File:Chromiumthrobber.svg
contentDivElement.innerHTML = "<img src='img/loading.svg' alt='Loading...' id='loading-spinner'/>";
}
/**
* Execute a JSONP Request
* @param {string} queryString
*/
function apiRequest(language, queryString) {
if(typeof queryString !== "string" || queryString.length <= 0) {
throw new Error("apiRequest requires a non-empty string parameter.");
}
renderLoadingSpinner();
var script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = prefixUrl(language) + apiUrl + "?" + queryString + "&callback=" + requestCallbackName;
document.getElementsByTagName("head")[0].appendChild(script);
var onCompleted = function () {
// reduce global namespace pollution
delete (window[requestCallbackName]);
// remove jsonp result tag
script.remove();
}
var requestTimeout = window.setTimeout(function () {
onCompleted();
}, requestTimeoutInMs);
window[requestCallbackName] = function (jsonObject) {
window.clearTimeout(requestTimeout);
handleRequestResult(jsonObject);
onCompleted();
}
}
function getShareableLink(search, language) {
return window.location.pathname + "?search=" + search + "&language=" + language;
}
function getLanguageCode() {
return languageInputElement.value || 'en';
}
function renderSearchResult(jsonObject) {
var pageid = jsonObject.query.pageids[0];
var article = jsonObject.query.pages[pageid];
var encodedArticleTitle = encodeURIComponent(article.title).replace(/%20/g, "_");
article.url = prefixUrl(getLanguageCode()) + wikipediaUrl + "/wiki/" + encodedArticleTitle;
var editlink = article.url + editIntroQuery;
var shareLink = window.location.href;
viewLinkElem.textContent = article.title;
viewLinkElem.setAttribute("href", article.url);
editLinkElement.setAttribute("href", editlink);
toggleVisibility(articleTitleElement, true);
copyShareLinkInputElement.value = getShareableLink(encodedArticleTitle, language.value);
contentDivElement = clearNode(contentDivElement);
contentDivElement.innerHTML = article.extract;
toggleVisibility(licenseIconElement, true);
toggleVisibility(infoIconElement, true);
}
function renderNotFoundNode() {
toggleVisibility(articleTitleElement, false);
toggleVisibility(licenseIconElement, false);
toggleVisibility(infoIconElement, false);
var notFoundNode = document.createElement("div");
notFoundNode.classList.add("error");
notFoundNode.textContent = "The search term wasn't found.";
contentDivElement = clearNode(contentDivElement);
contentDivElement.appendChild(notFoundNode);
}
function isHistoryStateSet() {
if(history.state === undefined) {
return false;
}
if(history.state === null) {
return false;
}
if(!history.state.hasOwnProperty("search")) {
return false;
}
return true;
}
function addToBrowserHistory(jsonObject) {
// pretty WET, probably best to make a DTO from the request
var pageid = jsonObject.query.pageids[0];
var article = jsonObject.query.pages[pageid];
var search = encodeURIComponent(article.title).replace(/%20/g, "_");
var language = getLanguageCode();
var historyState = {
search: search
};
if(isHistoryStateSet() && history.state.search === search) {
//Current page is already in history
return;
}
history.pushState(historyState, window.title, getShareableLink(search, language));
}
function handleRequestResult(jsonObject) {
if(jsonObject.hasOwnProperty("query")) {
var searchData = jsonObject.query.searchinfo;
if(typeof searchData === "undefined" || searchData.totalhits > 0) {
renderSearchResult(jsonObject);
addToBrowserHistory(jsonObject);
return;
} else if(typeof searchData.suggestion !== "undefined") {
apiRequest(articleIntroQuery + searchQuery + searchData.suggestion);
return;
}
}
renderNotFoundNode();
}
function updateSearchButtonEnabledState() {
if(searchTermInputElement instanceof HTMLInputElement && searchButton instanceof HTMLInputElement) {
var searchTermInputElementValue = searchTermInputElement.value;
if(typeof searchTermInputElementValue === "string" && searchTermInputElementValue.length > 0) {
searchButton.removeAttribute("disabled");
} else {
searchButton.setAttribute("disabled", "disabled");
}
}
}
// Upon loading the page, check if an URL parameter was passed, and use it to perform a search
window.onload = function () {
notificationElement = document.getElementById("notification");
notificationContentElement = document.getElementById("notification-content");
searchTermInputElement = document.getElementById("search-term");
languageInputElement = document.getElementById("language");
searchButton = document.getElementById("searchButton");
contentDivElement = document.getElementById("content");
viewLinkElem = document.getElementById("viewlink");
editLinkElement = document.getElementById("editlink");
copyShareLinkElement = document.getElementById("copysharelink");
articleTitleElement = document.getElementById("article-title");
licenseIconElement = document.getElementById("license-icon");
infoIconElement = document.getElementById("info-icon");
copyShareLinkInputElement = document.getElementById("copyShareLinkInput");
copyInputContainer = document.getElementById("copyInputContainer");
var queryParam = getQueryVariable("search");
if(queryParam !== null) {
searchTermInputElement.value = queryParam.replace(/_/g, " ");
search();
}
if(!isHistoryStateSet()) {
// just for convenience
history.replaceState({
search: queryParam
}, window.title, window.location.href);
}
searchTermInputElement.addEventListener("keyup", function () {
updateSearchButtonEnabledState();
});
searchTermInputElement.addEventListener("blur", function () {
updateSearchButtonEnabledState();
});
copyShareLinkElement.addEventListener("click", function () {
// this should allways be true, but doesn't hurt to check
if(copyShareLinkInputElement instanceof HTMLInputElement) {
// some browsers require a visible source for selection & copy to work
// this container virtually stays invisible
// since we hide it again as soon as we are done executing the copy instruction
toggleVisibility(copyInputContainer, true);
// clipboard interaction is a dodgy thing
// this should prevent the worst things where browser support
// or permissions are missing
try {
copyShareLinkInputElement.select(); // select the contents of the input
document.execCommand("copy"); // copy the selection into the clipboard
toggleVisibility(notificationElement, true); // show notification to user
// hide notification after an agreeable time
setTimeout(function () {
toggleVisibility(notificationElement, false); //
}, notificationTimeoutInMs);
} catch(e) {
}
toggleVisibility(copyInputContainer, false);
}
});
notificationContentElement.addEventListener("click", function () {
toggleVisibility(notificationElement, false);
});
};
window.onpopstate = function () {
if(isHistoryStateSet()) {
if(history.state.search === null) {
// we can't search for nothing so we reload the base location
window.location.assign(window.location.href);
return;
}
// needs to be done to comform to the requirements of search()
var queryParam = decodeURIComponent(history.state.search);
searchTermInputElement.value = queryParam;
search();
}
};