human (Nov 10, 2024, 06:14 PM)
The JSON this fetches looks like this - note how the data __compat bit at the start has a whole bunch of additional stuff in it Don't use react - modify the HTML I gave you. Keep the current timeline but underneath it add a chunk for each of the things in data with as much useful information as possible, including nested timelines - and turn the MDN links that start /en-US/docs/Web/API/AnimationEffect into links to https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect add one more feature: when a page is selected add its path to the # fragment hash in the URL using the history API. when the page first loads look for that fragment and navigate there and populate the search box if it is present
paste.txt
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {
box-sizing: border-box;
}
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
margin: 2rem 1rem;
max-width: 800px;
margin: 0 auto;
}
#autocomplete-container {
position: relative;
width: 100%;
}
#search-input {
width: 100%;
padding: 10px;
font-size: 16px;
}
#suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
background: white;
z-index: 10;
margin: 0 15px;
}
.suggestion-item {
padding: 10px;
cursor: pointer;
}
.suggestion-item:hover, .suggestion-item.selected {
background-color: #f0f0f0;
}
#timeline {
position: relative;
border-left: 2px solid #ccc;
margin-left: 20px;
padding-left: 20px;
margin-top: 2em;
}
.event {
margin-bottom: 20px;
position: relative;
}
.event::before {
content: '';
position: absolute;
left: -26px;
top: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #3b82f6;
}
.event-date {
font-size: 0.875rem;
color: #666;
}
.event-browser {
font-weight: 600;
color: #1f2937;
}
.event-version {
color: #059669;
margin-left: 4px;
}
.error {
color: #dc2626;
margin-top: 8px;
display: none;
padding: 8px;
background: #fef2f2;
border-radius: 4px;
}
.not-supported {
margin-top: 20px;
padding: 12px;
background: #f3f4f6;
border-radius: 4px;
}
@media (max-width: 600px) {
body {
margin: 1rem;
font-size: 14px;
}
#timeline {
margin-left: 10px;
padding-left: 10px;
}
.event::before {
left: -16px;
}
}
</style>
</head>
<body>
<body>
<h1>MDN Browser Support Timelines</h1>
<div id="autocomplete-container">
<input type="text" id="search-input" placeholder="Search files...">
<div id="suggestions"></div>
</div>
<div id="timeline-section">
<div id="timeline"></div>
</div>
<script>
let allFiles = [];
let selectedFiles = [];
async function fetchAllFiles(repo, path = 'api', page = 1, allFiles = []) {
const token = ''; // Optional: Add GitHub Personal Access Token for higher rate limits
const headers = token ? { 'Authorization': `token ${token}` } : {};
try {
const url = `https://api.github.com/repos/${repo}/contents/${path}?page=${page}&per_page=100`;
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const files = await response.json();
// Add files to our collection
allFiles.push(...files.map(file => file.path));
// If we got 100 files, there might be more pages
if (files.length === 100) {
return fetchAllFiles(repo, path, page + 1, allFiles);
}
return allFiles;
} catch (error) {
console.error('Error fetching files:', error);
return allFiles;
}
}
function setupAutocomplete() {
const searchInput = document.getElementById('search-input');
const suggestionsContainer = document.getElementById('suggestions');
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.toLowerCase();
const filteredFiles = allFiles.filter(file =>
file.toLowerCase().includes(searchTerm)
).slice(0, 20); // Limit to 20 suggestions
// Clear previous suggestions
suggestionsContainer.innerHTML = '';
// Create suggestion items
filteredFiles.forEach((file, index) => {
const suggestionItem = document.createElement('div');
suggestionItem.textContent = file;
suggestionItem.className = 'suggestion-item';
suggestionItem.setAttribute('data-index', index);
suggestionItem.addEventListener('click', () => {
searchInput.value = file;
suggestionsContainer.innerHTML = '';
fetchBrowserCompatData(file);
});
suggestionsContainer.appendChild(suggestionItem);
});
});
// Keyboard navigation
searchInput.addEventListener('keydown', (e) => {
const suggestions = suggestionsContainer.children;
if (e.key === 'ArrowDown') {
e.preventDefault();
if (suggestions.length > 0) {
const currentSelected = suggestionsContainer.querySelector('.selected');
const nextIndex = currentSelected
? Math.min(parseInt(currentSelected.getAttribute('data-index')) + 1, suggestions.length - 1)
: 0;
if (currentSelected) currentSelected.classList.remove('selected');
suggestions[nextIndex].classList.add('selected');
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (suggestions.length > 0) {
const currentSelected = suggestionsContainer.querySelector('.selected');
const prevIndex = currentSelected
? Math.max(parseInt(currentSelected.getAttribute('data-index')) - 1, 0)
: suggestions.length - 1;
if (currentSelected) currentSelected.classList.remove('selected');
suggestions[prevIndex].classList.add('selected');
}
} else if (e.key === 'Enter') {
const selectedItem = suggestionsContainer.querySelector('.selected') ||
suggestionsContainer.children[0];
if (selectedItem) {
searchInput.value = selectedItem.textContent;
suggestionsContainer.innerHTML = '';
fetchBrowserCompatData(selectedItem.textContent);
}
}
});
}
async function fetchBrowserCompatData(filePath) {
try {
const url = `https://bcd.developer.mozilla.org/bcd/api/v0/current/${filePath.replace('/', '.')}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
renderTimeline(extractSupportData(data));
} catch (error) {
console.error('Error fetching browser compat data:', error);
renderErrorMessage(error.message);
}
}
function extractSupportData(data) {
const browsers = data.browsers;
const support = data.data.__compat.support;
const supportData = [];
const notSupported = [];
for (const [browserName, supportInfo] of Object.entries(support)) {
if (!supportInfo[0]) continue;
if (supportInfo[0].version_added === false) {
notSupported.push(browsers[browserName]?.name || browserName);
continue;
}
if (!supportInfo[0].version_added || !supportInfo[0].release_date) continue;
supportData.push({
browser: browsers[browserName]?.name || browserName,
version: supportInfo[0].version_added,
date: supportInfo[0].release_date
});
}
return {
supported: supportData.sort((a, b) => new Date(a.date) - new Date(b.date)),
notSupported
};
}
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function renderTimeline(data) {
const timeline = document.getElementById('timeline');
timeline.innerHTML = '';
// Add supported browsers timeline
data.supported.forEach(item => {
const event = document.createElement('div');
event.className = 'event';
event.innerHTML = `
<div class="event-date">${formatDate(item.date)}</div>
<div>
<span class="event-browser">${item.browser}</span>
<span class="event-version">v${item.version}</span>
</div>
`;
timeline.appendChild(event);
});
// Add not supported browsers section if any
if (data.notSupported.length > 0) {
const notSupportedDiv = document.createElement('div');
notSupportedDiv.className = 'not-supported';
notSupportedDiv.innerHTML = `
<strong>Not Supported:</strong> ${data.notSupported.join(', ')}
`;
timeline.appendChild(notSupportedDiv);
}
}
function renderErrorMessage(message) {
const timeline = document.getElementById('timeline');
timeline.innerHTML = `
<div class="error">
Error: ${message}
</div>
`;
}
// Initialize the app
async function init() {
allFiles = await fetchAllFiles('mdn/browser-compat-data', 'api');
setupAutocomplete();
}
init();
</script>
</body>
</body>
</html>
paste-2.txt
{
"data": {
"__compat": {
"mdn_url": "/en-US/docs/Web/API/AnimationEffect",
"source_file": "api/AnimationEffect.json",
"spec_url": "https://drafts.csswg.org/web-animations-1/#the-animationeffect-interface",
"status": {
"deprecated": false,
"experimental": false,
"standard_track": true
},
"support": {
"chrome": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"chrome_android": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"edge": [
{
"version_added": "79",
"release_date": "2020-01-15"
}
],
"firefox": [
{
"version_added": "63",
"release_date": "2018-10-23"
},
{
"alternative_name": "AnimationEffectReadOnly",
"version_added": "48",
"version_last": "62",
"version_removed": "63",
"release_date": "2016-08-02"
}
],
"firefox_android": [
{
"version_added": "63",
"release_date": "2018-10-23"
},
{
"alternative_name": "AnimationEffectReadOnly",
"version_added": "48",
"version_last": "62",
"version_removed": "63",
"release_date": "2016-08-02"
}
],
"ie": [
{
"version_added": false
}
],
"oculus": [
{
"version_added": "7.0"
}
],
"opera": [
{
"version_added": "62",
"release_date": "2019-06-27"
}
],
"opera_android": [
{
"version_added": "54",
"release_date": "2019-10-18"
}
],
"safari": [
{
"version_added": "13.1",
"release_date": "2020-03-24"
}
],
"safari_ios": [
{
"version_added": "13.4",
"release_date": "2020-03-24"
}
],
"samsunginternet_android": [
{
"version_added": "11.0",
"release_date": "2019-12-05"
}
],
"webview_android": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"webview_ios": [
{
"version_added": "13.4",
"release_date": "2020-03-24"
}
]
},
"tags": [
"web-features:web-animations"
]
},
"getComputedTiming": {
"__compat": {
"mdn_url": "/en-US/docs/Web/API/AnimationEffect/getComputedTiming",
"source_file": "api/AnimationEffect.json",
"spec_url": "https://drafts.csswg.org/web-animations-1/#dom-animationeffect-getcomputedtiming",
"status": {
"deprecated": false,
"experimental": false,
"standard_track": true
},
"support": {
"chrome": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"chrome_android": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"edge": [
{
"version_added": "79",
"release_date": "2020-01-15"
}
],
"firefox": [
{
"version_added": "63",
"release_date": "2018-10-23"
}
],
"firefox_android": [
{
"version_added": "63",
"release_date": "2018-10-23"
}
],
"ie": [
{
"version_added": false
}
],
"oculus": [
{
"version_added": "7.0"
}
],
"opera": [
{
"version_added": "62",
"release_date": "2019-06-27"
}
],
"opera_android": [
{
"version_added": "54",
"release_date": "2019-10-18"
}
],
"safari": [
{
"version_added": "13.1",
"release_date": "2020-03-24"
}
],
"safari_ios": [
{
"version_added": "13.4",
"release_date": "2020-03-24"
}
],
"samsunginternet_android": [
{
"version_added": "11.0",
"release_date": "2019-12-05"
}
],
"webview_android": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"webview_ios": [
{
"version_added": "13.4",
"release_date": "2020-03-24"
}
]
},
"tags": [
"web-features:web-animations"
]
}
},
"getTiming": {
"__compat": {
"mdn_url": "/en-US/docs/Web/API/AnimationEffect/getTiming",
"source_file": "api/AnimationEffect.json",
"spec_url": "https://drafts.csswg.org/web-animations-1/#dom-animationeffect-gettiming",
"status": {
"deprecated": false,
"experimental": false,
"standard_track": true
},
"support": {
"chrome": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"chrome_android": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"edge": [
{
"version_added": "79",
"release_date": "2020-01-15"
}
],
"firefox": [
{
"version_added": "63",
"release_date": "2018-10-23"
}
],
"firefox_android": [
{
"version_added": "63",
"release_date": "2018-10-23"
}
],
"ie": [
{
"version_added": false
}
],
"oculus": [
{
"version_added": "7.0"
}
],
"opera": [
{
"version_added": "62",
"release_date": "2019-06-27"
}
],
"opera_android": [
{
"version_added": "54",
"release_date": "2019-10-18"
}
],
"safari": [
{
"version_added": "13.1",
"release_date": "2020-03-24"
}
],
"safari_ios": [
{
"version_added": "13.4",
"release_date": "2020-03-24"
}
],
"samsunginternet_android": [
{
"version_added": "11.0",
"release_date": "2019-12-05"
}
],
"webview_android": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"webview_ios": [
{
"version_added": "13.4",
"release_date": "2020-03-24"
}
]
},
"tags": [
"web-features:web-animations"
]
}
},
"updateTiming": {
"__compat": {
"mdn_url": "/en-US/docs/Web/API/AnimationEffect/updateTiming",
"source_file": "api/AnimationEffect.json",
"spec_url": "https://drafts.csswg.org/web-animations-1/#dom-animationeffect-updatetiming",
"status": {
"deprecated": false,
"experimental": false,
"standard_track": true
},
"support": {
"chrome": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"chrome_android": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"edge": [
{
"version_added": "79",
"release_date": "2020-01-15"
}
],
"firefox": [
{
"version_added": "63",
"release_date": "2018-10-23"
}
],
"firefox_android": [
{
"version_added": "63",
"release_date": "2018-10-23"
}
],
"ie": [
{
"version_added": false
}
],
"oculus": [
{
"version_added": "7.0"
}
],
"opera": [
{
"version_added": "62",
"release_date": "2019-06-27"
}
],
"opera_android": [
{
"version_added": "54",
"release_date": "2019-10-18"
}
],
"safari": [
{
"version_added": "13.1",
"release_date": "2020-03-24"
}
],
"safari_ios": [
{
"version_added": "13.4",
"release_date": "2020-03-24"
}
],
"samsunginternet_android": [
{
"version_added": "11.0",
"release_date": "2019-12-05"
}
],
"webview_android": [
{
"version_added": "75",
"release_date": "2019-06-04"
}
],
"webview_ios": [
{
"version_added": "13.4",
"release_date": "2020-03-24"
}
]
},
"tags": [
"web-features:web-animations"
]
}
}
},
"query": "api.AnimationEffect",
"browsers": {
"chrome": {
"accepts_flags": true,
"accepts_webextensions": true,
"name": "Chrome",
"pref_url": "chrome://flags",
"preview_name": "Canary",
"releases": {
"131": {
"engine": "Blink",
"engine_version": "131",
"release_date": "2024-11-12",
"status": "current"
},
"132": {
"engine": "Blink",
"engine_version": "132",
"release_date": "2025-01-14",
"status": "beta"
},
"133": {
"engine": "Blink",
"engine_version": "133",
"release_date": "2025-02-04",
"status": "nightly"
},
"134": {
"engine": "Blink",
"engine_version": "134",
"status": "planned"
}
},
"type": "desktop"
},
"chrome_android": {
"accepts_flags": true,
"accepts_webextensions": false,
"name": "Chrome Android",
"pref_url": "chrome://flags",
"releases": {
"131": {
"engine": "Blink",
"engine_version": "131",
"release_date": "2024-11-12",
"status": "current"
},
"132": {
"engine": "Blink",
"engine_version": "132",
"release_date": "2025-01-14",
"status": "beta"
},
"133": {
"engine": "Blink",
"engine_version": "133",
"release_date": "2025-02-04",
"status": "nightly"
},
"134": {
"engine": "Blink",
"engine_version": "134",
"status": "planned"
}
},
"type": "mobile",
"upstream": "chrome"
},
"deno": {
"accepts_flags": true,
"accepts_webextensions": false,
"name": "Deno",
"releases": {
"1.45": {
"engine": "V8",
"engine_version": "12.7",
"release_date": "2024-07-11",
"release_notes": "https://deno.com/blog/v1.45",
"status": "current"
}
},
"type": "server"
},
"edge": {
"accepts_flags": true,
"accepts_webextensions": true,
"name": "Edge",
"pref_url": "about:flags",
"releases": {
"130": {
"engine": "Blink",
"engine_version": "130",
"release_date": "2024-10-17",
"release_notes": "https://learn.microsoft.com/en-us/deployedge/microsoft-edge-relnote-stable-channel#version-1300284946-october-17-2024",
"status": "current"
},
"131": {
"engine": "Blink",
"engine_version": "131",
"release_date": "2024-11-14",
"status": "beta"
},
"132": {
"engine": "Blink",
"engine_version": "132",
"release_date": "2025-01-09",
"status": "nightly"
},
"133": {
"engine": "Blink",
"engine_version": "133",
"release_date": "2025-02-06",
"status": "planned"
}
},
"type": "desktop",
"upstream": "chrome"
},
"firefox": {
"accepts_flags": true,
"accepts_webextensions": true,
"name": "Firefox",
"pref_url": "about:config",
"preview_name": "Nightly",
"releases": {
"128": {
"engine": "Gecko",
"engine_version": "128",
"release_date": "2024-07-09",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/128",
"status": "esr"
},
"132": {
"engine": "Gecko",
"engine_version": "132",
"release_date": "2024-10-29",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/132",
"status": "current"
},
"133": {
"engine": "Gecko",
"engine_version": "133",
"release_date": "2024-11-26",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/133",
"status": "beta"
},
"134": {
"engine": "Gecko",
"engine_version": "134",
"release_date": "2025-01-07",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/134",
"status": "nightly"
},
"135": {
"engine": "Gecko",
"engine_version": "135",
"release_date": "2025-02-04",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/135",
"status": "planned"
}
},
"type": "desktop"
},
"firefox_android": {
"accepts_flags": false,
"accepts_webextensions": true,
"name": "Firefox for Android",
"pref_url": "about:config",
"releases": {
"128": {
"engine": "Gecko",
"engine_version": "128",
"release_date": "2024-07-09",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/128",
"status": "esr"
},
"132": {
"engine": "Gecko",
"engine_version": "132",
"release_date": "2024-10-29",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/132",
"status": "current"
},
"133": {
"engine": "Gecko",
"engine_version": "133",
"release_date": "2024-11-26",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/133",
"status": "beta"
},
"134": {
"engine": "Gecko",
"engine_version": "134",
"release_date": "2025-01-07",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/134",
"status": "nightly"
},
"135": {
"engine": "Gecko",
"engine_version": "135",
"release_date": "2025-02-04",
"release_notes": "https://developer.mozilla.org/docs/Mozilla/Firefox/Releases/135",
"status": "planned"
}
},
"type": "mobile",
"upstream": "firefox"
},
"ie": {
"accepts_flags": false,
"accepts_webextensions": false,
"name": "Internet Explorer",
"releases": {},
"type": "desktop"
},
"nodejs": {
"accepts_flags": true,
"accepts_webextensions": false,
"name": "Node.js",
"releases": {
"22.0.0": {
"engine": "V8",
"engine_version": "12.4",
"release_date": "2024-04-24",
"release_notes": "https://nodejs.org/en/blog/release/v22.0.0",
"status": "esr"
},
"23.0.0": {
"engine": "V8",
"engine_version": "12.9",
"release_date": "2024-10-16",
"release_notes": "https://nodejs.org/en/blog/release/v23.0.0",
"status": "current"
}
},
"type": "server"
},
"oculus": {
"accepts_flags": true,
"accepts_webextensions": false,
"name": "Quest Browser",
"pref_url": "chrome://flags",
"releases": {
"23.0": {
"engine": "Blink",
"engine_version": "104",
"release_date": "2022-08-15",
"status": "current"
}
},
"type": "xr",
"upstream": "chrome_android"
},
"opera": {
"accepts_flags": true,
"accepts_webextensions": true,
"name": "Opera",
"pref_url": "opera://flags",
"releases": {
"113": {
"engine": "Blink",
"engine_version": "127",
"release_date": "2024-08-22",
"release_notes": "https://blogs.opera.com/desktop/2024/08/opera-113/",
"status": "current"
},
"114": {
"engine": "Blink",
"engine_version": "128",
"status": "beta"
},
"115": {
"engine": "Blink",
"engine_version": "129",
"status": "nightly"
}
},
"type": "desktop",
"upstream": "chrome"
},
"opera_android": {
"accepts_flags": false,
"accepts_webextensions": false,
"name": "Opera Android",
"releases": {
"84": {
"engine": "Blink",
"engine_version": "127",
"release_date": "2024-08-26",
"status": "current"
}
},
"type": "mobile",
"upstream": "chrome_android"
},
"safari": {
"accepts_flags": true,
"accepts_webextensions": true,
"name": "Safari",
"preview_name": "TP",
"releases": {
"18.1": {
"engine": "WebKit",
"engine_version": "619.2.8",
"release_date": "2024-10-28",
"release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-18_1-release-notes",
"status": "current"
},
"18.2": {
"engine": "WebKit",
"engine_version": "620.1.11",
"release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-18_2-release-notes",
"status": "beta"
}
},
"type": "desktop"
},
"safari_ios": {
"accepts_flags": true,
"accepts_webextensions": true,
"name": "Safari on iOS",
"releases": {
"18.1": {
"engine": "WebKit",
"engine_version": "619.2.8",
"release_date": "2024-10-28",
"release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-18_1-release-notes",
"status": "current"
},
"18.2": {
"engine": "WebKit",
"engine_version": "620.1.11",
"release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-18_2-release-notes",
"status": "beta"
}
},
"type": "mobile",
"upstream": "safari"
},
"samsunginternet_android": {
"accepts_flags": false,
"accepts_webextensions": false,
"name": "Samsung Internet",
"releases": {
"26.0": {
"engine": "Blink",
"engine_version": "122",
"release_date": "2024-06-07",
"status": "current"
},
"27.0": {
"engine": "Blink",
"engine_version": "125",
"status": "beta"
}
},
"type": "mobile",
"upstream": "chrome_android"
},
"webview_android": {
"accepts_flags": false,
"accepts_webextensions": false,
"name": "WebView Android",
"releases": {
"131": {
"engine": "Blink",
"engine_version": "131",
"release_date": "2024-11-12",
"status": "current"
},
"132": {
"engine": "Blink",
"engine_version": "132",
"release_date": "2025-01-14",
"status": "beta"
},
"133": {
"engine": "Blink",
"engine_version": "133",
"release_date": "2025-02-04",
"status": "nightly"
},
"134": {
"engine": "Blink",
"engine_version": "134",
"status": "planned"
}
},
"type": "mobile",
"upstream": "chrome_android"
},
"webview_ios": {
"accepts_flags": false,
"accepts_webextensions": false,
"name": "WebView on iOS",
"releases": {
"18.1": {
"engine": "WebKit",
"engine_version": "619.2.8",
"release_date": "2024-10-28",
"release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-18_1-release-notes",
"status": "current"
},
"18.2": {
"engine": "WebKit",
"engine_version": "620.1.11",
"release_notes": "https://developer.apple.com/documentation/safari-release-notes/safari-18_2-release-notes",
"status": "beta"
}
},
"type": "mobile",
"upstream": "safari_ios"
}
}
}
assistant (Nov 10, 2024, 06:14 PM)
I'll help modify the HTML to show detailed information for each API feature and add URL hash handling. Here's how we'll do this:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {
box-sizing: border-box;
}
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
margin: 2rem 1rem;
max-width: 800px;
margin: 0 auto;
}
#autocomplete-container {
position: relative;
width: 100%;
}
#search-input {
width: 100%;
padding: 10px;
font-size: 16px;
}
#suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
background: white;
z-index: 10;
margin: 0 15px;
}
.suggestion-item {
padding: 10px;
cursor: pointer;
}
.suggestion-item:hover, .suggestion-item.selected {
background-color: #f0f0f0;
}
#timeline {
position: relative;
border-left: 2px solid #ccc;
margin-left: 20px;
padding-left: 20px;
margin-top: 2em;
}
.event {
margin-bottom: 20px;
position: relative;
}
.event::before {
content: '';
position: absolute;
left: -26px;
top: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #3b82f6;
}
.event-date {
font-size: 0.875rem;
color: #666;
}
.event-browser {
font-weight: 600;
color: #1f2937;
}
.event-version {
color: #059669;
margin-left: 4px;
}
.error {
color: #dc2626;
margin-top: 8px;
display: none;
padding: 8px;
background: #fef2f2;
border-radius: 4px;
}
.not-supported {
margin-top: 20px;
padding: 12px;
background: #f3f4f6;
border-radius: 4px;
}
.api-section {
margin-top: 2rem;
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.api-section h2 {
margin-top: 0;
color: #1f2937;
font-size: 1.5rem;
}
.api-info {
margin: 1rem 0;
}
.api-info-item {
margin: 0.5rem 0;
}
.api-info-label {
font-weight: 600;
color: #4b5563;
}
.status-indicator {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
margin-right: 0.5rem;
}
.status-experimental {
background-color: #fef3c7;
color: #92400e;
}
.status-deprecated {
background-color: #fee2e2;
color: #991b1b;
}
.status-standard {
background-color: #d1fae5;
color: #065f46;
}
@media (max-width: 600px) {
body {
margin: 1rem;
font-size: 14px;
}
#timeline {
margin-left: 10px;
padding-left: 10px;
}
.event::before {
left: -16px;
}
}
</style>
</head>
<body>
<h1>MDN Browser Support Timelines</h1>
<div id="autocomplete-container">
<input type="text" id="search-input" placeholder="Search files...">
<div id="suggestions"></div>
</div>
<div id="timeline-section">
<div id="timeline"></div>
</div>
<div id="api-details"></div>
<script>
let allFiles = [];
let selectedFiles = [];
async function fetchAllFiles(repo, path = 'api', page = 1, allFiles = []) {
const token = '';
const headers = token ? { 'Authorization': `token ${token}` } : {};
try {
const url = `https://api.github.com/repos/${repo}/contents/${path}?page=${page}&per_page=100`;
const response = await fetch(url, { headers });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const files = await response.json();
allFiles.push(...files.map(file => file.path));
if (files.length === 100) {
return fetchAllFiles(repo, path, page + 1, allFiles);
}
return allFiles;
} catch (error) {
console.error('Error fetching files:', error);
return allFiles;
}
}
function updateUrlHash(filePath) {
const cleanPath = filePath.replace(/^api\//, '');
history.pushState(null, '', `#${cleanPath}`);
}
function getHashPath() {
const hash = window.location.hash.slice(1);
return hash ? `api/${hash}` : null;
}
function setupAutocomplete() {
const searchInput = document.getElementById('search-input');
const suggestionsContainer = document.getElementById('suggestions');
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.toLowerCase();
const filteredFiles = allFiles.filter(file =>
file.toLowerCase().includes(searchTerm)
).slice(0, 20);
suggestionsContainer.innerHTML = '';
filteredFiles.forEach((file, index) => {
const suggestionItem = document.createElement('div');
suggestionItem.textContent = file;
suggestionItem.className = 'suggestion-item';
suggestionItem.setAttribute('data-index', index);
suggestionItem.addEventListener('click', () => {
searchInput.value = file;
suggestionsContainer.innerHTML = '';
fetchBrowserCompatData(file);
});
suggestionsContainer.appendChild(suggestionItem);
});
});
searchInput.addEventListener('keydown', handleKeyboardNavigation);
}
function handleKeyboardNavigation(e) {
const suggestionsContainer = document.getElementById('suggestions');
const suggestions = suggestionsContainer.children;
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (suggestions.length > 0) {
const currentSelected = suggestionsContainer.querySelector('.selected');
const nextIndex = currentSelected
? Math.min(parseInt(currentSelected.getAttribute('data-index')) + (e.key === 'ArrowDown' ? 1 : -1), suggestions.length - 1)
: 0;
if (currentSelected) currentSelected.classList.remove('selected');
suggestions[nextIndex].classList.add('selected');
}
} else if (e.key === 'Enter') {
const selectedItem = suggestionsContainer.querySelector('.selected') ||
suggestionsContainer.children[0];
if (selectedItem) {
e.preventDefault();
const searchInput = document.getElementById('search-input');
searchInput.value = selectedItem.textContent;
suggestionsContainer.innerHTML = '';
fetchBrowserCompatData(selectedItem.textContent);
}
}
}
function renderApiSection(data) {
const apiDetails = document.getElementById('api-details');
apiDetails.innerHTML = '';
// Render main API section
const mainSection = createApiSection(data.data.__compat, data.query);
apiDetails.appendChild(mainSection);
// Render sub-features
Object.entries(data.data).forEach(([key, value]) => {
if (key !== '__compat' && value.__compat) {
const subSection = createApiSection(value.__compat, `${data.query}.${key}`);
apiDetails.appendChild(subSection);
}
});
}
function createApiSection(compat, title) {
const section = document.createElement('section');
section.className = 'api-section';
const heading = document.createElement('h2');
heading.textContent = title;
section.appendChild(heading);
const info = document.createElement('div');
info.className = 'api-info';
// Add MDN link if available
if (compat.mdn_url) {
const mdnLink = document.createElement('div');
mdnLink.className = 'api-info-item';
const mdnUrl = compat.mdn_url.replace('/en-US/docs/Web/API/', 'https://developer.mozilla.org/en-US/docs/Web/API/');
mdnLink.innerHTML = `<span class="api-info-label">MDN Documentation:</span> <a href="${mdnUrl}" target="_blank">${mdnUrl}</a>`;
info.appendChild(mdnLink);
}
// Add specification link if available
if (compat.spec_url) {
const specLink = document.createElement('div');
specLink.className = 'api-info-item';
specLink.innerHTML = `<span class="api-info-label">Specification:</span> <a href="${compat.spec_url}" target="_blank">${compat.spec_url}</a>`;
info.appendChild(specLink);
}
// Add status indicators
if (compat.status) {
const statusDiv = document.createElement('div');
statusDiv.className = 'api-info-item';
Object.entries(compat.status).forEach(([key, value]) => {
if (value) {
const status = document.createElement('span');
status.className = `status-indicator status-${key}`;
status.textContent = key.charAt(0).toUpperCase() + key.slice(1);
statusDiv.appendChild(status);
}
});
info.appendChild(statusDiv);
}
section.appendChild(info);
// Add timeline for this section
const timelineDiv = document.createElement('div');
timelineDiv.className = 'timeline';
const supportData = extractSupportData({
data: { __compat: compat },
browsers: data.browsers
});
renderTimeline(supportData, timelineDiv);
section.appendChild(timelineDiv);
return section;
}
async function fetchBrowserCompatData(filePath) {
try {
updateUrlHash(filePath);
const url = `https://bcd.developer.mozilla.org/bcd/api/v0/current/${filePath.replace('/', '.')}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const timelineData = extractSupportData(data);
renderTimeline(timelineData);
renderApiSection(data);
} catch (error) {
console.error('Error fetching browser compat data:', error);
renderErrorMessage(error.message);
}
}
function extractSupportData(data) {
const browsers = data.browsers;
const support = data.data.__compat.support;
const supportData = [];
const notSupported = [];
for (const [browserName, supportInfo] of Object.entries(support)) {
if (!supportInfo[0]) continue;
if (supportInfo[0].version_added === false) {
notSupported.push(browsers[browserName]?.name || browserName);
continue;
}
if (!supportInfo[0].version_added || !supportInfo[0].release_date) continue;
supportData.push({
browser: browsers[browserName]?.name || browserName,
version: supportInfo[0].version_added,
date: supportInfo[0].release_date
});
}
return {
supported: supportData.sort((a, b) => new Date(a.date) - new Date(b.date)),
notSupported
};
}
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function renderTimeline(data, container = document.getElementById('timeline')) {
container.innerHTML = '';
data.supported.forEach(item => {
const event = document.createElement('div');
event.className = 'event';
event.innerHTML = `
<div class="event-date">${formatDate(item.date)}</div>
<div>
<span class="event-browser">${item.browser}</span>
<span class="event-version">v${item.version}</span>
</div>
`;
container.appendChild(event);
});
if (data.notSupported.length > 0) {
const notSupportedDiv = document.createElement('div');
notSupportedDiv.className = 'not-supported';
notSupportedDiv.innerHTML = `
<strong>Not Supported:</strong> ${data.notSupported.join(', ')}
`;
container.appendChild(notSupportedDiv);
}
}
function renderErrorMessage(message) {
const timeline = document.getElementById('timeline');
timeline.innerHTML = `
<div class="error" style="display: block;">
Error: ${message}
</div>
`;
}
// Initialize the app
async function init() {
allFiles = await fetchAllFiles('mdn/browser-compat-data', 'api');
setupAutocomplete();
// Check for hash in URL and load that file if present
const hashPath = getHashPath();
if (hashPath) {
document.getElementById('search-input').value = hashPath;
fetchBrowserCompatData(hashPath);
}
}
// Handle back/forward navigation
window.addEventListener('popstate', () => {
const hashPath = getHashPath();
if (hashPath) {
document.getElementById('search-input').value = hashPath;
fetchBrowserCompatData(hashPath);
}
});
init();
</script>
</body>
</html>
I've enhanced the original code with these main changes:
- Added URL hash handling:
- Added
updateUrlHash()
function to update the URL when