-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwidget.html
More file actions
429 lines (386 loc) · 22.2 KB
/
widget.html
File metadata and controls
429 lines (386 loc) · 22.2 KB
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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reroute NJ — Widget</title>
<link rel="icon" type="image/svg+xml" href="img/favicon.svg">
<link rel="stylesheet" href="css/styles.css">
<style>
/* Override site chrome for widget context */
body { padding-top: 0 !important; background: #f8f9fb; }
.header, .tool-nav, .seo-summary, .footer { display: none !important; }
.container { max-width: 100%; padding: 0.5rem; }
.widget-attribution { text-align: center; padding: 8px 0; font-size: 0.75rem; color: #73849a; border-top: 1px solid #e8ecf1; margin-top: 1rem; }
.widget-attribution a { color: #73849a; text-decoration: none; }
.widget-attribution a:hover { color: #e87722; }
.widget-error { text-align: center; padding: 40px 20px; color: #73849a; }
.widget-error h2 { font-size: 1.1rem; margin-bottom: 8px; color: #1a2332; }
.widget-error a { color: #e87722; }
/* Hide hero sections in widget context */
.compare-hero, .coverage-hero, .embed-hero { display: none; }
#map { height: 400px; }
</style>
</head>
<body>
<main class="container" id="main-content"></main>
<div class="widget-attribution">
Powered by <a href="https://reroutenj.org" target="_blank" rel="noopener">Reroute NJ</a>
</div>
<script>
// Widget loader — reads URL params and builds the appropriate tool
// Security note: setStaticHTML is used only for static HTML fragments with
// no user-controlled content. User input (tool param in error case) uses
// textContent for XSS safety.
(function () {
"use strict";
function getParam(name) {
var match = window.location.search.match(new RegExp("[?&]" + name + "=([^&]*)"));
return match ? decodeURIComponent(match[1]) : null;
}
function loadScript(src, callback) {
var s = document.createElement("script");
s.src = src;
s.onload = callback || function () {};
document.body.appendChild(s);
}
function loadStylesheet(href) {
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
document.head.appendChild(link);
}
// Make all links in the widget open in a new tab
function externalizeLinks() {
document.addEventListener("click", function (e) {
var a = e.target.closest ? e.target.closest("a") : null;
if (a && a.href && a.href.indexOf("reroutenj.org") > -1 && !a.getAttribute("target")) {
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener");
}
});
}
// Build DOM safely from static HTML fragments (no user input)
function setStaticHTML(el, html) {
el.innerHTML = html; // eslint-disable-line -- static markup only, no user input
}
/**
* Fetch translations for the given language and set window._T.
* Falls back to English (empty _T) on error or if lang is "en"/absent.
*/
function fetchTranslations(callback) {
var lang = getParam("lang");
if (!lang || lang === "en") {
// Don't set window._T here — let i18n.js set it to the
// built-in EN object so t() lookups work for English.
callback();
return;
}
// Set html lang attribute
document.documentElement.lang = lang;
var url = "translations/" + encodeURIComponent(lang) + ".json";
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = function () {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
// Populate window._T with all runtime-relevant sections
var sections = ["common", "js", "compare", "coverage", "card", "index", "map"];
window._T = {};
for (var i = 0; i < sections.length; i++) {
var key = sections[i];
if (data[key]) {
window._T[key] = data[key];
}
}
} catch (e) {
// Leave _T unset so i18n.js loads English as fallback
}
}
// If _T wasn't populated (parse error or non-200), i18n.js
// will set it to the built-in EN object as fallback.
callback();
};
xhr.onerror = function () {
// Leave _T unset so i18n.js loads English as fallback
callback();
};
xhr.send();
}
/** Get a translated string, with fallback */
function wt(section, key, fallback) {
if (window._T && window._T[section] && window._T[section][key]) {
return window._T[section][key];
}
return fallback;
}
/**
* After translations load but before tool JS runs, update the
* hardcoded English labels in the scaffold DOM.
*/
function retranslateScaffold(tool) {
if (!window._T || !Object.keys(window._T).length) return;
if (tool === "line-guide") {
// Line badge placeholder
var badge = document.getElementById("line-badge");
if (badge) badge.textContent = wt("js", "select_line", "Select a line");
// Station picker label
var stationLabel = document.querySelector('.station-picker label[for="station-select"]');
if (stationLabel) stationLabel.textContent = wt("index", "your_station", "Your station");
// Station select placeholder
var stationSelect = document.getElementById("station-select");
if (stationSelect && stationSelect.options[0]) {
stationSelect.options[0].textContent = wt("index", "choose_station", "Choose your station\u2026");
}
// Direction label
var dirLabel = document.querySelector('.direction-picker label[for="direction-select"]');
if (dirLabel) dirLabel.textContent = wt("index", "direction_of_travel", "Direction of travel");
// Direction buttons
var dirBtns = document.querySelectorAll(".direction-btn");
if (dirBtns.length >= 2) {
dirBtns[0].innerHTML = wt("index", "nj_to_nyc", "NJ → NYC") + ' <span class="dir-sub">' + wt("index", "morning_commute", "morning commute") + "</span>"; // eslint-disable-line -- static translated strings only
dirBtns[1].innerHTML = wt("index", "nyc_to_nj", "NYC → NJ") + ' <span class="dir-sub">' + wt("index", "evening_commute", "evening commute") + "</span>"; // eslint-disable-line -- static translated strings only
}
// Tab buttons
var tabAffected = document.getElementById("tab-affected");
if (tabAffected) tabAffected.textContent = wt("index", "am_i_affected", "Am I affected?");
var tabRoutes = document.getElementById("tab-routes");
if (tabRoutes) tabRoutes.textContent = wt("index", "route_planner", "Route planner");
var tabTickets = document.getElementById("tab-tickets");
if (tabTickets) tabTickets.textContent = wt("index", "ticket_guide", "Ticket guide");
// Panel headings and descriptions
var panelAffected = document.querySelector("#panel-affected .panel-intro");
if (panelAffected) {
var h2a = panelAffected.querySelector("h2");
var pa = panelAffected.querySelector("p");
if (h2a) h2a.textContent = wt("index", "am_i_affected", "Am I affected?");
if (pa) pa.textContent = wt("index", "affected_intro", "Select your line and station above to see exactly how the cutover changes your commute.");
}
var panelRoutes = document.querySelector("#panel-routes .panel-intro");
if (panelRoutes) {
var h2r = panelRoutes.querySelector("h2");
var pr = panelRoutes.querySelector("p");
if (h2r) h2r.textContent = wt("index", "route_planner", "Route planner");
if (pr) pr.textContent = wt("index", "routes_intro", "Select a line above to see your options during the cutover.");
}
var panelTickets = document.querySelector("#panel-tickets .panel-intro");
if (panelTickets) {
var h2t = panelTickets.querySelector("h2");
var pt = panelTickets.querySelector("p");
if (h2t) h2t.textContent = wt("index", "ticket_guide_title", "What ticket should I buy?");
if (pt) pt.textContent = wt("index", "tickets_intro", "Select a line above to see ticket guidance for the cutover period.");
}
} else if (tool === "compare") {
// Step labels
var steps = document.querySelectorAll(".step-label");
if (steps.length >= 3) {
// Keep the step-circle spans, update the text after them
var s1 = steps[0]; s1.childNodes[s1.childNodes.length - 1].textContent = " " + wt("compare", "your_line", "Your line");
var s2label = document.querySelector('label.step-label[for="station-select"]');
if (s2label) s2label.childNodes[s2label.childNodes.length - 1].textContent = " " + wt("compare", "your_station", "Your station");
var s3 = steps[2]; s3.childNodes[s3.childNodes.length - 1].textContent = " " + wt("compare", "where_manhattan", "Where in Manhattan?");
}
// Station select placeholder
var cmpStation = document.getElementById("station-select");
if (cmpStation && cmpStation.options[0]) {
cmpStation.options[0].textContent = wt("compare", "choose_station", "Choose your station\u2026");
}
} else if (tool === "coverage") {
// Filter labels
var filterLabels = {
"filter-source": wt("coverage", "source", "Source"),
"filter-category": wt("coverage", "category", "Category"),
"filter-line": wt("coverage", "line", "Line"),
"filter-direction": wt("coverage", "direction", "Direction"),
"filter-search": wt("coverage", "search", "Search")
};
for (var fid in filterLabels) {
if (filterLabels.hasOwnProperty(fid)) {
var lbl = document.querySelector('label[for="' + fid + '"]');
if (lbl) lbl.textContent = filterLabels[fid];
}
}
// "All sources" placeholder
var srcSelect = document.getElementById("filter-source");
if (srcSelect && srcSelect.options[0]) {
srcSelect.options[0].textContent = wt("coverage", "all_sources", "All sources");
}
// Search placeholder
var searchInput = document.getElementById("filter-search");
if (searchInput) searchInput.placeholder = wt("coverage", "search_articles", "Search articles\u2026");
} else if (tool === "map") {
// Map filter buttons
var allBtn = document.querySelector('[data-filter="all"]');
if (allBtn) allBtn.textContent = wt("map", "all", "All");
var hubBtn = document.querySelector('[data-filter="transfer-hubs"]');
if (hubBtn) hubBtn.textContent = wt("map", "transfer_hubs", "Transfer hubs");
}
}
var tool = getParam("tool");
var lineId = getParam("line");
var main = document.getElementById("main-content");
if (!tool) {
setStaticHTML(main, '<div class="widget-error"><h2>No tool specified</h2><p>Add ?tool=line-guide, compare, coverage, or map</p></div>');
return;
}
externalizeLinks();
// Fetch translations, then build the tool scaffold + load JS
fetchTranslations(function () {
if (tool === "line-guide") {
// Build DOM structure matching index.html / app.js expectations
setStaticHTML(main,
'<div class="control-panel">' +
'<section class="line-header">' +
'<div class="line-badge" id="line-badge">Select a line</div>' +
'<nav class="line-nav" id="line-nav" aria-label="Select train line"></nav>' +
'</section>' +
'<section class="station-picker">' +
'<label for="station-select">Your station</label>' +
'<select id="station-select"><option value="">Choose your station…</option></select>' +
'</section>' +
'<section class="direction-picker">' +
'<label for="direction-select">Direction of travel</label>' +
'<div class="direction-toggle" id="direction-toggle" role="group" aria-labelledby="direction-label">' +
'<span id="direction-label" class="sr-only">Direction of travel</span>' +
'<button class="direction-btn active" data-dir="nj-to-nyc" aria-pressed="true">NJ → NYC <span class="dir-sub">morning commute</span></button>' +
'<button class="direction-btn" data-dir="nyc-to-nj" aria-pressed="false">NYC → NJ <span class="dir-sub">evening commute</span></button>' +
'</div>' +
'</section>' +
'</div>' +
'<nav class="tool-tabs" role="tablist" aria-label="Tool options">' +
'<button class="tab active" role="tab" aria-selected="true" aria-controls="panel-affected" id="tab-affected" data-tab="affected">Am I affected?</button>' +
'<button class="tab" role="tab" aria-selected="false" aria-controls="panel-routes" id="tab-routes" data-tab="routes" tabindex="-1">Route planner</button>' +
'<button class="tab" role="tab" aria-selected="false" aria-controls="panel-tickets" id="tab-tickets" data-tab="tickets" tabindex="-1">Ticket guide</button>' +
'</nav>' +
'<section id="panel-affected" class="tool-panel active" role="tabpanel" aria-labelledby="tab-affected">' +
'<div class="panel-intro"><h2>Am I affected?</h2><p>Select your line and station above to see exactly how the cutover changes your commute.</p></div>' +
'<div id="impact-empty"></div>' +
'<div id="impact-result" class="hidden" aria-live="polite" tabindex="-1"></div>' +
'</section>' +
'<section id="panel-routes" class="tool-panel" role="tabpanel" aria-labelledby="tab-routes">' +
'<div class="panel-intro" id="routes-intro"><h2>Route planner</h2><p>Select a line above to see your options during the cutover.</p></div>' +
'<div id="routes-content" aria-live="polite" tabindex="-1"></div>' +
'</section>' +
'<section id="panel-tickets" class="tool-panel" role="tabpanel" aria-labelledby="tab-tickets">' +
'<div class="panel-intro" id="tickets-intro"><h2>What ticket should I buy?</h2><p>Select a line above to see ticket guidance for the cutover period.</p></div>' +
'<div id="tickets-content" aria-live="polite" tabindex="-1"></div>' +
'</section>' +
'<div id="countdown"></div>');
// Retranslate the scaffold before tool JS runs
retranslateScaffold("line-guide");
// Load scripts in order: i18n -> shared -> line-data -> app
loadScript("js/i18n.js", function () {
loadScript("js/shared.js", function () {
loadScript("js/line-data.js", function () {
loadScript("js/app.js", function () {
// Pre-select line if specified
if (lineId) {
setTimeout(function () {
var btn = document.querySelector('[data-line="' + lineId + '"]');
if (btn) btn.click();
}, 100);
}
});
});
});
});
} else if (tool === "compare") {
// Build DOM structure matching compare.html / compare.js expectations
setStaticHTML(main,
'<section class="input-step">' +
'<div class="step-label"><span class="step-circle">1</span> Your line</div>' +
'<nav class="line-nav" id="line-nav" aria-label="Select train line"></nav>' +
'</section>' +
'<section class="input-step">' +
'<label class="step-label" for="station-select"><span class="step-circle">2</span> Your station</label>' +
'<select id="station-select"><option value="">Choose your station…</option></select>' +
'</section>' +
'<section class="input-step">' +
'<div class="step-label"><span class="step-circle">3</span> Where in Manhattan?</div>' +
'<div class="dest-grid" id="dest-grid" role="group" aria-label="Manhattan destinations"></div>' +
'</section>' +
'<section id="results" class="compare-results hidden" aria-live="polite" tabindex="-1">' +
'<div id="results-header" class="results-header"></div>' +
'<div id="results-normal" class="results-normal"></div>' +
'<div id="results-list"></div>' +
'<div id="results-share" class="results-share"></div>' +
'</section>');
retranslateScaffold("compare");
// Load scripts in order: i18n -> shared -> compare
// compare.js has its own embedded line/station data, no line-data.js needed
loadScript("js/i18n.js", function () {
loadScript("js/shared.js", function () {
loadScript("js/compare.js", function () {
if (lineId) {
setTimeout(function () {
var btn = document.querySelector('[data-line="' + lineId + '"]');
if (btn) btn.click();
}, 100);
}
});
});
});
} else if (tool === "coverage") {
// Build DOM structure matching coverage.html / coverage.js expectations
setStaticHTML(main,
'<p class="coverage-updated" id="coverage-updated"></p>' +
'<section class="coverage-filters" id="coverage-filters" role="search" aria-label="Filter news articles">' +
'<div class="filter-group"><label for="filter-source">Source</label><select id="filter-source"><option value="">All sources</option></select></div>' +
'<div class="filter-group"><label for="filter-category">Category</label><select id="filter-category"><option value="">All categories</option><option value="news">News</option><option value="opinion">Opinion</option><option value="analysis">Analysis</option><option value="official">Official</option><option value="community">Community</option></select></div>' +
'<div class="filter-group"><label for="filter-line">Line</label><select id="filter-line"><option value="">All lines</option><option value="montclair-boonton">Montclair-Boonton</option><option value="morris-essex">Morris & Essex</option><option value="northeast-corridor">Northeast Corridor</option><option value="north-jersey-coast">North Jersey Coast</option><option value="raritan-valley">Raritan Valley</option></select></div>' +
'<div class="filter-group"><label for="filter-direction">Direction</label><select id="filter-direction"><option value="">All directions</option><option value="nj-to-nyc">NJ to NYC</option><option value="nyc-to-nj">NYC to NJ</option><option value="both">Both directions</option></select></div>' +
'<div class="filter-group filter-search"><label for="filter-search">Search</label><input type="text" id="filter-search" placeholder="Search articles…"></div>' +
'</section>' +
'<section class="coverage-feed" id="coverage-feed" aria-live="polite" aria-label="News articles" tabindex="-1"></section>');
retranslateScaffold("coverage");
loadScript("js/i18n.js", function () {
loadScript("js/shared.js", function () {
loadScript("js/coverage.js");
});
});
} else if (tool === "map") {
// Map needs Leaflet CSS + JS, filter bar, and map container
loadStylesheet("https://unpkg.com/leaflet@1.9.4/dist/leaflet.css");
setStaticHTML(main,
'<div class="map-filter-bar" id="map-filters" role="group" aria-label="Filter map by line">' +
'<button class="map-filter-btn active" data-filter="all" aria-pressed="true">All</button>' +
'<button class="map-filter-btn" data-filter="montclair-boonton" aria-pressed="false">Montclair-Boonton</button>' +
'<button class="map-filter-btn" data-filter="morris-essex" aria-pressed="false">Morris & Essex</button>' +
'<button class="map-filter-btn" data-filter="northeast-corridor" aria-pressed="false">Northeast Corridor</button>' +
'<button class="map-filter-btn" data-filter="north-jersey-coast" aria-pressed="false">North Jersey Coast</button>' +
'<button class="map-filter-btn" data-filter="raritan-valley" aria-pressed="false">Raritan Valley</button>' +
'<button class="map-filter-btn" data-filter="transfer-hubs" aria-pressed="false">Transfer hubs</button>' +
'</div>' +
'<div id="map" role="application" aria-label="Interactive map" style="height:400px;border-radius:8px;"></div>');
retranslateScaffold("map");
loadScript("https://unpkg.com/leaflet@1.9.4/dist/leaflet.js", function () {
loadScript("js/i18n.js", function () {
loadScript("js/shared.js", function () {
loadScript("js/map.js");
});
});
});
} else {
// XSS-safe error for unknown tool — use textContent, not innerHTML for user input
var errDiv = document.createElement("div");
errDiv.className = "widget-error";
var h2 = document.createElement("h2");
h2.textContent = "Unknown tool: " + tool;
var p = document.createElement("p");
p.textContent = "Valid tools: line-guide, compare, coverage, map. ";
var link = document.createElement("a");
link.href = "https://reroutenj.org/embed.html";
link.setAttribute("target", "_blank");
link.setAttribute("rel", "noopener");
link.textContent = "See all embed options";
p.appendChild(link);
errDiv.appendChild(h2);
errDiv.appendChild(p);
main.appendChild(errDiv);
}
}); // end fetchTranslations callback
})();
</script>
</body>
</html>