Files
bb-portal/website/news/app.js

280 lines
8.4 KiB
JavaScript

/* app.js - fetch only when Refresh is pressed */
(function () {
function $(id) { return document.getElementById(id); }
function parseQuery() {
var out = {};
var qs = window.location.search || "";
if (qs.indexOf("?") === 0) qs = qs.substring(1);
if (!qs) return out;
var parts = qs.split("&");
for (var i = 0; i < parts.length; i++) {
var kv = parts[i].split("=");
var k = decodeURIComponent(kv[0] || "").trim();
var v = decodeURIComponent(kv.slice(1).join("=") || "").trim();
if (k) out[k] = v;
}
return out;
}
function setQuery(params) {
var keys = [];
for (var k in params) {
if (params.hasOwnProperty(k) && params[k] !== "" && params[k] != null) {
keys.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
}
}
var qs = keys.length ? ("?" + keys.join("&")) : "";
if (window.history && window.history.replaceState) {
window.history.replaceState(null, "", window.location.pathname + qs);
}
}
function findById(arr, id, fallback) {
for (var i = 0; i < arr.length; i++) if (arr[i].id === id) return arr[i];
return fallback || null;
}
function buildFeedUrl(feed, region) {
var base;
if (feed.type === "top") {
base = "https://news.google.com/rss";
} else if (feed.type === "topic" && feed.topic) {
base = "https://news.google.com/rss/headlines/section/topic/" + encodeURIComponent(feed.topic);
} else {
base = "https://news.google.com/rss";
}
var q = [];
q.push("hl=" + encodeURIComponent(region.hl));
q.push("gl=" + encodeURIComponent(region.gl));
q.push("ceid=" + encodeURIComponent(region.ceid));
return base + "?" + q.join("&");
}
function fetchText(url, cb) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.timeout = 15000;
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status >= 200 && xhr.status < 300) return cb(null, xhr.responseText);
cb(new Error("HTTP " + xhr.status + " while fetching feed"));
};
xhr.ontimeout = function () {
cb(new Error("Timed out while fetching feed"));
};
xhr.onerror = function () {
cb(new Error("Network error while fetching feed"));
};
xhr.send(null);
}
function safeText(s) {
if (s == null) return "";
return String(s);
}
function escapeHtml(s) {
s = safeText(s);
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function escapeAttr(s) {
return escapeHtml(s);
}
function stripHtml(s) {
return safeText(s).replace(/<[^>]*>/g, "");
}
function parseRss(xmlText) {
var items = [];
var doc = null;
if (window.DOMParser) {
try {
doc = new DOMParser().parseFromString(xmlText, "text/xml");
} catch (e) {
doc = null;
}
}
if (!doc || !doc.getElementsByTagName) return items;
var nodes = doc.getElementsByTagName("item");
for (var i = 0; i < nodes.length; i++) {
var it = nodes[i];
function get(tag) {
var n = it.getElementsByTagName(tag);
if (!n || !n[0] || !n[0].textContent) return "";
return n[0].textContent;
}
var title = get("title");
var link = get("link");
var pubDate = get("pubDate");
var desc = get("description");
var source = "";
if (title.indexOf(" - ") !== -1) {
var parts = title.split(" - ");
source = parts[parts.length - 1];
}
items.push({
title: title,
link: link,
pubDate: pubDate,
source: source,
description: desc
});
}
return items;
}
function showError(msg, show) {
var el = $("error");
if (!el) return;
if (show) {
el.style.display = "block";
el.textContent = msg;
} else {
el.style.display = "none";
el.textContent = "";
}
}
function renderItems(items) {
var host = $("items");
host.innerHTML = "";
if (!items || !items.length) {
host.innerHTML =
"<div class='item'><span class='title'>No items.</span>" +
"<div class='meta'>Press Refresh to load.</div></div>";
return;
}
var html = [];
for (var i = 0; i < items.length; i++) {
var it = items[i];
html.push("<div class='item'>");
html.push(
"<a class='title' href='" + escapeAttr(it.link) + "' target='_blank' rel='noopener noreferrer'>" +
escapeHtml(it.title) +
"</a>"
);
var meta = [];
if (it.source) meta.push(escapeHtml(it.source));
if (it.pubDate) meta.push(escapeHtml(it.pubDate));
if (meta.length) html.push("<div class='meta'>" + meta.join(" · ") + "</div>");
var snippet = stripHtml(safeText(it.description)).replace(/\s+/g, " ").trim();
if (snippet.length > 220) snippet = snippet.substring(0, 220) + "...";
if (snippet) html.push("<div class='desc'>" + escapeHtml(snippet) + "</div>");
html.push("</div>");
}
host.innerHTML = html.join("");
}
function buildSelectOptions(selectEl, arr, selectedId) {
selectEl.innerHTML = "";
for (var i = 0; i < arr.length; i++) {
var o = document.createElement("option");
o.value = arr[i].id;
o.textContent = arr[i].label;
if (arr[i].id === selectedId) o.selected = true;
selectEl.appendChild(o);
}
}
function getCurrentSelection() {
var feeds = window.NEWS_FEEDS || [];
var regions = window.NEWS_REGIONS || [];
var feedId = $("feedSelect").value;
var regionId = $("regionSelect").value;
var feed = findById(feeds, feedId, feeds[0] || null);
var region = findById(regions, regionId, regions[0] || null);
return { feed: feed, region: region };
}
function syncUrlToSelection() {
setQuery({ feed: $("feedSelect").value, region: $("regionSelect").value });
}
function requestNews() {
var sel = getCurrentSelection();
if (!sel.feed || !sel.region) {
showError("feeds.js is missing NEWS_FEEDS or NEWS_REGIONS.", true);
return;
}
syncUrlToSelection();
showError("", false);
var feedUrl = buildFeedUrl(sel.feed, sel.region);
var proxyBase = window.NEWS_PROXY_BASE || "";
var fetchUrl = proxyBase ? (proxyBase + encodeURIComponent(feedUrl)) : feedUrl;
fetchText(fetchUrl, function (err, txt) {
if (err) {
showError(
"Could not load feed. If this is CORS, set NEWS_PROXY_BASE in feeds.js. Details: " + err.message,
true
);
renderItems([]);
return;
}
renderItems(parseRss(txt));
});
}
window.addEventListener("DOMContentLoaded", function () {
var feeds = window.NEWS_FEEDS || [];
var regions = window.NEWS_REGIONS || [];
if (!feeds.length || !regions.length) {
showError("No feeds/regions configured. Check feeds.js.", true);
renderItems([]);
return;
}
var q = parseQuery();
var feedId = q.feed || window.NEWS_DEFAULT_FEED_ID || feeds[0].id;
var regionId = q.region || window.NEWS_DEFAULT_REGION_ID || regions[0].id;
buildSelectOptions($("feedSelect"), feeds, feedId);
buildSelectOptions($("regionSelect"), regions, regionId);
// Keep URL updated when selecting, but DO NOT request.
$("feedSelect").addEventListener("change", syncUrlToSelection);
$("regionSelect").addEventListener("change", syncUrlToSelection);
$("refreshBtn").addEventListener("click", requestNews);
// No auto-request on load.
renderItems([]);
syncUrlToSelection();
});
})();