Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 46 additions & 12 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,61 @@
<body>
<!-- Page Header with logo and title -->
<header class="header">
<a href="./"
><img src="./bunnings-logo.png" alt="Bunnings Logo" /><span
>Bunnings API Problems</span
></a
>
<div class="header-content">
<a href="./" class="header-title"
><img src="./bunnings-logo.png" alt="Bunnings Logo" /><span
>Bunnings API Problems</span
></a
>
<div class="search-container">
<input
type="text"
id="searchInput"
placeholder="Search problems or error codes..."
aria-label="Search problems and error codes"
/>
<button id="clearSearch" class="clear-btn" aria-label="Clear search" style="display: none;">✕</button>
</div>
</div>
</header>

<!-- Navigation bar for domains -->
<nav class="domain-nav" id="domainNav">
<div class="nav-content">
<a href="./" class="nav-link active">All</a>
<a href="./?codes=errorCodesBasket" class="nav-link">Basket</a>
<a href="./?codes=errorCodesCheckout" class="nav-link">Checkout</a>
<a href="./?codes=errorCodesCommercialCustomer" class="nav-link">Commercial</a>
<a href="./?codes=errorCodesCommunications" class="nav-link">Communications</a>
<a href="./?codes=errorCodesConsumer" class="nav-link">Consumer</a>
<a href="./?codes=errorCodesInputSchemaErrors" class="nav-link">Schema</a>
<a href="./?codes=errorCodesOrdering" class="nav-link">Ordering</a>
<a href="./?codes=errorCodesPayment" class="nav-link">Payment</a>
<a href="./?codes=errorCodesPricing" class="nav-link">Pricing</a>
<a href="./?codes=errorCodesTransaction" class="nav-link">Transaction</a>
<a href="./?codes=errorCodesVoucher" class="nav-link">Voucher</a>
</div>
</nav>

<main id="main-content">
<!-- Container for dynamically loaded Markdown content based on URL parameters -->
<div id="docContainer">
<!-- Content is injected here by script.js using <zero-md> -->
</div>

<!-- Container for the main page content (problem type lists and error code lists) -->
<div>
<!-- Container for the list of API problem types, populated by script.js from index.xml and index.xsl -->
<div id="index" class="markdown-body"></div>
<!-- Container for the heading of the error codes section, populated by script.js from errorCodes.md -->
<div id="errorCodesHeading" class="md-html"></div>
<!-- Container for the list of API error codes, populated by script.js from errorCodes.xml and index.xsl -->
<div id="errorCodes" class="markdown-body"></div>
<div id="mainPageContent">
<!-- Container for the Problems section -->
<section class="content-section">
<div id="indexHeading" class="md-html"></div>
<div id="index" class="markdown-body"></div>
</section>

<!-- Container for the Error Codes section -->
<section class="content-section error-codes-section">
<div id="errorCodesHeading" class="md-html"></div>
<div id="errorCodes" class="markdown-body"></div>
</section>
</div>
</main>

Expand Down
262 changes: 260 additions & 2 deletions script.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @file Manages dynamic content loading and XSLT transformation for the API problems page.
* @file Manages dynamic content loading, XSLT transformation, and search functionality for the API problems page.
*/

/**
Expand All @@ -18,6 +18,241 @@ if (urlParams.has('codes')) {
var docContainer = document.getElementById('docContainer');
docContainer.innerHTML = "<zero-md src='" + doc + "'></zero-md>";

/**
* Search state management
*/
let searchData = {
problems: [],
errorCodes: []
};

/**
* Initialize search functionality
*/
function initializeSearch() {
const searchInput = document.getElementById('searchInput');
const clearButton = document.getElementById('clearSearch');

if (!searchInput) return;

// Load search data
loadSearchData();

// Search input handler with debounce
let searchTimeout;
searchInput.addEventListener('input', function(e) {
const query = e.target.value.trim();

// Show/hide clear button
if (query) {
clearButton.style.display = 'block';
} else {
clearButton.style.display = 'none';
}

// Debounce search
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
performSearch(query);
}, 300);
});

// Clear button handler
clearButton.addEventListener('click', function() {
searchInput.value = '';
clearButton.style.display = 'none';
performSearch('');
searchInput.focus();
});

// Clear on Escape key
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
searchInput.value = '';
clearButton.style.display = 'none';
performSearch('');
}
});
}

/**
* Load search data from the index files
*/
async function loadSearchData() {
try {
// Load problem types from index.xml
const indexResponse = await fetch('./index.xml');
const indexText = await indexResponse.text();
const parser = new DOMParser();
const indexDoc = parser.parseFromString(indexText, "application/xml");
const problems = indexDoc.querySelectorAll('problem');

searchData.problems = Array.from(problems).map(problem => ({
title: problem.getAttribute('title'),
href: problem.getAttribute('href'),
type: 'problem'
}));

// Load error code domains from errorCodes.xml
const errorCodesResponse = await fetch('./errorCodes.xml');
const errorCodesText = await errorCodesResponse.text();
const errorCodesDoc = parser.parseFromString(errorCodesText, "application/xml");
const errorCodes = errorCodesDoc.querySelectorAll('problem');

searchData.errorCodes = Array.from(errorCodes).map(code => ({
title: code.getAttribute('title'),
href: code.getAttribute('href'),
type: 'errorCode'
}));
} catch (error) {
console.error('Error loading search data:', error);
}
}

/**
* Perform search and filter visible items
*/
function performSearch(query) {
const normalizedQuery = query.toLowerCase();

// Only perform search on main page
if (!urlParams.has('type') && !urlParams.has('codes')) {
const indexList = document.querySelector('#index ul');
const errorCodesList = document.querySelector('#errorCodes ul');

if (indexList) {
filterList(indexList, normalizedQuery);
}

if (errorCodesList) {
filterList(errorCodesList, normalizedQuery);
}

// Show/hide sections if all items are hidden
updateSectionVisibility();
}
}

/**
* Filter list items based on search query
*/
function filterList(list, query) {
const items = list.querySelectorAll('li');
let visibleCount = 0;

items.forEach(item => {
const text = item.textContent.toLowerCase();
const link = item.querySelector('a');

if (!query || text.includes(query)) {
item.classList.remove('search-hidden');
visibleCount++;

// Highlight matching text
if (query && link) {
highlightText(link, query);
} else if (link) {
// Remove highlights if no query
const originalText = link.getAttribute('data-original-text');
if (originalText) {
link.textContent = originalText;
}
}
} else {
item.classList.add('search-hidden');
}
});

return visibleCount;
}

/**
* Highlight matching text in element
*/
function highlightText(element, query) {
const originalText = element.getAttribute('data-original-text') || element.textContent;
if (!element.hasAttribute('data-original-text')) {
element.setAttribute('data-original-text', originalText);
}

if (!query) {
element.textContent = originalText;
return;
}

const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
const highlightedText = originalText.replace(regex, '<span class="search-highlight">$1</span>');
element.innerHTML = highlightedText;
}

/**
* Escape special regex characters
*/
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
* Update section visibility based on search results
*/
function updateSectionVisibility() {
const indexSection = document.querySelector('.content-section:first-child');
const errorCodesSection = document.querySelector('.error-codes-section');

if (indexSection) {
const indexList = document.querySelector('#index ul');
const visibleItems = indexList ? indexList.querySelectorAll('li:not(.search-hidden)').length : 0;

if (visibleItems === 0 && indexList) {
const existingNoResults = indexSection.querySelector('.search-no-results');
if (!existingNoResults) {
const noResults = document.createElement('div');
noResults.className = 'search-no-results';
noResults.innerHTML = '<p>No problem types match your search.</p>';
indexSection.appendChild(noResults);
}
} else {
const noResults = indexSection.querySelector('.search-no-results');
if (noResults) noResults.remove();
}
}

if (errorCodesSection) {
const errorCodesList = document.querySelector('#errorCodes ul');
const visibleItems = errorCodesList ? errorCodesList.querySelectorAll('li:not(.search-hidden)').length : 0;

if (visibleItems === 0 && errorCodesList) {
const existingNoResults = errorCodesSection.querySelector('.search-no-results');
if (!existingNoResults) {
const noResults = document.createElement('div');
noResults.className = 'search-no-results';
noResults.innerHTML = '<p>No error code domains match your search.</p>';
errorCodesSection.appendChild(noResults);
}
} else {
const noResults = errorCodesSection.querySelector('.search-no-results');
if (noResults) noResults.remove();
}
}
}

/**
* Update active nav link based on current page
*/
function updateActiveNavLink() {
const navLinks = document.querySelectorAll('.nav-link');
const currentPath = window.location.search;

navLinks.forEach(link => {
link.classList.remove('active');
const linkPath = new URL(link.href).search;

if ((!currentPath && linkPath === '') || (currentPath && linkPath === currentPath)) {
link.classList.add('active');
}
});
}

/**
* Asynchronously fetches an XML file and an XSLT file, then transforms the XML using the XSLT.
* The resulting HTML fragment is then injected into a specified target HTML element.
Expand Down Expand Up @@ -84,10 +319,33 @@ async function loadAndTransformXml(xmlPath, xsltPath, targetElementId) {
* This block runs if the URL does not contain 'type' or 'codes' parameters,
* indicating that the main index page with problem types and error codes lists should be displayed.
* It calls `loadAndTransformXml` to populate the 'index' and 'errorCodes' divs,
* and loads 'errorCodes.md' into the 'errorCodesHeading' div.
* and loads 'index.md' and 'errorCodes.md' for the headings.
*/
if (!urlParams.has('type') && !urlParams.has('codes')) {
// Load the heading and introduction from index.md
document.getElementById('indexHeading').innerHTML = "<zero-md src='index.md'></zero-md>";
// Load the problem types list
loadAndTransformXml('./index.xml', './index.xsl', 'index');

// Load the error codes heading from errorCodes.md
document.getElementById('errorCodesHeading').innerHTML = "<zero-md src='errorCodes.md'></zero-md>";
// Load the error codes list
loadAndTransformXml('./errorCodes.xml', './index.xsl', 'errorCodes');

// Show the main page content
document.getElementById('mainPageContent').style.display = 'block';
} else {
// Hide the main page content when viewing specific problem or error code
document.getElementById('mainPageContent').style.display = 'none';
}

// Initialize search functionality when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
initializeSearch();
updateActiveNavLink();

// Re-initialize search after content is loaded
setTimeout(() => {
initializeSearch();
}, 1000);
});
Loading