Partial SPA sidebar implementation + fix for drawer component
All checks were successful
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Successful in 1m22s
All checks were successful
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Successful in 1m22s
This commit is contained in:
parent
b885a67353
commit
e4f00fcdf6
4 changed files with 270 additions and 1 deletions
|
|
@ -132,7 +132,7 @@ export class Drawer extends ComposableElement {
|
||||||
|
|
||||||
// Create the div that will contain the drawer content
|
// Create the div that will contain the drawer content
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.id = `${drawerId}-drawer-contents`;
|
contentDiv.id = `${config.drawerId}-drawer-contents`;
|
||||||
contentDiv.classList.add('drawer-contents', 'hidden');
|
contentDiv.classList.add('drawer-contents', 'hidden');
|
||||||
|
|
||||||
const contentSlotElem = document.createElement('slot');
|
const contentSlotElem = document.createElement('slot');
|
||||||
|
|
|
||||||
2
src/components/spa-sidebar/README.md
Normal file
2
src/components/spa-sidebar/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Single Page Appliction (SPA) Sidebar
|
||||||
|
Technically slightly misusing the term SPA here but the idea is that there are no loads between section.
|
||||||
79
src/components/spa-sidebar/spa-sidebar.ejs
Normal file
79
src/components/spa-sidebar/spa-sidebar.ejs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<ba-spa-sidebar>
|
||||||
|
<%# Because of complexities with web components and specifically more legacy support and the `createTemplateInJS` method, we need to pass the SPA sidebar data available in EJS to the JavaScript for the web component (easiest way was via a script tag) %>
|
||||||
|
<!-- JSON config data so that `createTemplateInJS` can recreate the SPA sidebar DOM structure accurately if needed -->
|
||||||
|
<script type="application/json" data-spa-sidebar-config>
|
||||||
|
<%- JSON.stringify({
|
||||||
|
sections: locals.sections,
|
||||||
|
spaSidebarExtraStyles: typeof locals.spaSidebarExtraStyles !== 'undefined' ? (Array.isArray(locals.spaSidebarExtraStyles) ? locals.spaSidebarExtraStyles : [locals.spaSidebarExtraStyles]) : [],
|
||||||
|
spaSidebarExtraScripts: typeof locals.spaSidebarExtraScripts !== 'undefined' ? (Array.isArray(locals.spaSidebarExtraScripts) ? locals.spaSidebarExtraScripts : [locals.spaSidebarExtraScripts]) : [],
|
||||||
|
componentsStyleHref: locals.componentsStyleHref
|
||||||
|
}) %>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template shadowrootmode="open">
|
||||||
|
<!-- Styles -->
|
||||||
|
<!-- Component Specific Styling -->
|
||||||
|
<!-- Component Styling (Part of Component Library CSS) -->
|
||||||
|
<link rel="stylesheet" type="text/css" href="<%= locals.componentsStyleHref %>">
|
||||||
|
<!-- END: Component Specific Styling -->
|
||||||
|
|
||||||
|
<!-- Additional Styles -->
|
||||||
|
<% if (typeof locals.spaSidebarExtraStyles !== 'undefined') { %>
|
||||||
|
<% const spaSidebarStyles = Array.isArray(locals.spaSidebarExtraStyles) ? locals.spaSidebarExtraStyles : [locals.spaSidebarExtraStyles]; %>
|
||||||
|
<% for (let style of spaSidebarStyles) { %>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/<%= style %>.css">
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
<!-- END: Additional Styles -->
|
||||||
|
<!-- END: Styles -->
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<% if (typeof locals.spaSidebarExtraScripts !== 'undefined') { %>
|
||||||
|
<% const spaSidebarScripts = Array.isArray(locals.spaSidebarExtraScripts) ? locals.spaSidebarExtraScripts : [locals.spaSidebarExtraScripts]; %>
|
||||||
|
<% for (let script of spaSidebarScripts) { %>
|
||||||
|
<% if(typeof script === 'object') { %>
|
||||||
|
<script type="<%= typeof script.module === 'boolean' && script.module ? 'module' : 'application/javascript' %>" src="<%= script.script %>"></script>
|
||||||
|
<% } else if(typeof script === 'string' && (script.startsWith('http') || script.startsWith('https'))) { %>
|
||||||
|
<script type="<%= script.endsWith('.mjs') ? 'module' : 'application/javascript' %>" src="<%= script %>"></script>
|
||||||
|
<% } else if(typeof script === 'string') { %>
|
||||||
|
<script type="<%= script.endsWith('.mjs') ? 'module' : 'application/javascript' %>" src="/js/<%= script.endsWith('.mjs') ? script : script + '.js' %>"></script>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
|
<!-- END: Scripts -->
|
||||||
|
|
||||||
|
<div id="spa-sidebar-container">
|
||||||
|
<aside class="spa-sidebar">
|
||||||
|
<ul class="spa-sidebar-nav">
|
||||||
|
<% locals.sections.forEach(section => { %>
|
||||||
|
<li>
|
||||||
|
<a href="#<%= section.id %>" class="spa-sidebar-link active">
|
||||||
|
<slot name="link-<%= section.id %>"<% if(typeof slots !== 'undefined' && typeof slots['link-' + section.id] !== 'undefined') { %> slot="<%= 'link-' + section.id %>"<% } %>></slot>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<div class="spa-sidebar-main" aria-live="polite">
|
||||||
|
<% locals.sections.forEach(section => { %>
|
||||||
|
<section id="<%= section.id %>" class="spa-sidebar-sect<%= section.active ? ' active-sect' : '' %>">
|
||||||
|
<slot name="content-<%= section.id %>"<% if(typeof slots !== 'undefined' && typeof slots['content-' + section.id] !== 'undefined') { %> slot="<%= 'content-' + section.id %>"<% } %>></slot>
|
||||||
|
</section>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<% if (typeof slots !== 'undefined' && Object.keys(slots).length > 0) { %>
|
||||||
|
<%- slots %>
|
||||||
|
<% } else { %>
|
||||||
|
<% locals.sections.forEach(section => { %>
|
||||||
|
<div slot="link-<%= section.id %>">
|
||||||
|
<%- typeof section.linkContent !== 'undefined' ? section.linkContent : '' %>
|
||||||
|
</div>
|
||||||
|
<div slot="content-<%= section.id %>">
|
||||||
|
<%- typeof section.content !== 'undefined' ? section.content : '' %>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
<% } %>
|
||||||
|
</ba-spa-sidebar>
|
||||||
188
src/components/spa-sidebar/spa-sidebar.mjs
Normal file
188
src/components/spa-sidebar/spa-sidebar.mjs
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { ComposableElement } from '../ComposableElement.mjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spinner Web Component
|
||||||
|
*
|
||||||
|
* A simple spinner component that indicates loading or processing state.
|
||||||
|
* This component uses the `aria-busy` attribute in combination with CSS tricks to create a spinner that is accessible, modern, and easy to manage.
|
||||||
|
*/
|
||||||
|
export class SPASidebar extends ComposableElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Web Component Lifecycle Methods
|
||||||
|
// -------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does initial setup and adds event listeners for interactivity
|
||||||
|
*
|
||||||
|
* `connectedCallback` is a lifecycle method in web components that runs when the custom element is inserted into the document's Document Object Model (DOM).
|
||||||
|
* It can be invoked multiple times if the element is removed and then re-inserted into the DOM.
|
||||||
|
*
|
||||||
|
* Timing: It is called after the element's constructor() but before the element's children are necessarily connected or fully rendered.
|
||||||
|
* Purpose: It is the ideal place to set up tasks that should only occur when the element is actually present in the live document. Common uses include:
|
||||||
|
*/
|
||||||
|
connectedCallback() {
|
||||||
|
const internals = this.attachInternals();
|
||||||
|
|
||||||
|
this.shadow = this.shadowRoot;
|
||||||
|
if (!this.shadow) {
|
||||||
|
this.shadow = this.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
// Defer execution until the browser finishes parsing the children
|
||||||
|
setTimeout(() => {
|
||||||
|
// Recreate the template using the shadow DOM that is only available through JavaScript
|
||||||
|
this.createTemplateInJS(this.shadow);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.shadow.querySelectorAll('.spa-sidebar-link').forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
// Prevent default link behavior
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Get the current section from the URL hash to determine if we are switching sections or re-clicking the same section
|
||||||
|
const currSect = window.location.hash.substring(1);
|
||||||
|
|
||||||
|
const newSect = link.getAttribute('href').substring('#'.length);
|
||||||
|
|
||||||
|
// Because the URL hash is what determines the active section, if the new section is the same as the current section we can just return early and do nothing
|
||||||
|
if(newSect === currSect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 'active-sect' class from all currently active sections and `active` class from links
|
||||||
|
document.querySelectorAll('.spa-sidebar-page-sect.active-sect').forEach(section => section.classList.remove('active-sect'));
|
||||||
|
document.querySelectorAll('.spa-sidebar-page-sidebar-link.active').forEach(activeLink => activeLink.classList.remove('active'));
|
||||||
|
|
||||||
|
// Add 'active-sect' class to the new section and active class to link
|
||||||
|
link.classList.add('active');
|
||||||
|
document.querySelector(`.spa-sidebar-page-sect#${newSect}`).classList.add('active-sect');
|
||||||
|
|
||||||
|
// Update the URL hash to reflect the new section
|
||||||
|
window.location.hash = newSect;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up event listeners when the component is removed from the DOM
|
||||||
|
*
|
||||||
|
* `disconnectedCallback` is a lifecycle method in web components that runs when the custom element is removed from the document's DOM.
|
||||||
|
* It can be invoked multiple times if the element is removed and then re-inserted into the DOM.
|
||||||
|
*
|
||||||
|
* Timing: It is called after the element is removed from the DOM but before it is garbage collected.
|
||||||
|
* Purpose: It is the ideal place to clean up any resources or event listeners that were set up in `connectedCallback`.
|
||||||
|
*
|
||||||
|
* Common uses include:
|
||||||
|
* - Removing event listeners to prevent memory leaks
|
||||||
|
* - Clearing timers or intervals
|
||||||
|
* - Disconnecting from external data sources or APIs
|
||||||
|
*/
|
||||||
|
disconnectedCallback() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recreate the template in the shadow DOM through JavaScript instead of relying on the `shadowrootmode` attribute
|
||||||
|
*
|
||||||
|
* @param {ShadowRoot} shadow The shadow DOM to attach the template to
|
||||||
|
*/
|
||||||
|
createTemplateInJS(shadow) {
|
||||||
|
const config = this.initializeComponent('spa-sidebar', shadow);
|
||||||
|
|
||||||
|
if(config == null) {
|
||||||
|
throw new Error('Spinner configuration not found. Please ensure you have a script tag with the appropriate data-spinner-config attribute containing the necessary JSON configuration for the spinner component.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the container div for the spinner component
|
||||||
|
const spaSidebarContainerDiv = document.createElement('div');
|
||||||
|
spaSidebarContainerDiv.id = 'spa-sidebar-container';
|
||||||
|
|
||||||
|
const spaSidebarAside = document.createElement('aside');
|
||||||
|
spaSidebarAside.classList.add('spa-sidebar');
|
||||||
|
|
||||||
|
const spaSidebarList = document.createElement('ul');
|
||||||
|
spaSidebarList.classList.add('spa-sidebar-nav');
|
||||||
|
|
||||||
|
config.sections.forEach(section => {
|
||||||
|
const spaSidebarListItem = document.createElement('li');
|
||||||
|
|
||||||
|
const spaSidebarListItemLink = document.createElement('a');
|
||||||
|
spaSidebarListItemLink.href = `#${section.id}`
|
||||||
|
spaSidebarListItemLink.classList.add('spa-sidebar-link', 'active');
|
||||||
|
|
||||||
|
const spaSidebarListItemSlot = document.createElement('slot');
|
||||||
|
spaSidebarListItemSlot.name = `link-${section.id}`;
|
||||||
|
//spaSidebarListItemSlot.slot = `link-${section.id}`;
|
||||||
|
|
||||||
|
spaSidebarListItemLink.appendChild(spaSidebarListItemSlot);
|
||||||
|
|
||||||
|
spaSidebarListItem.appendChild(spaSidebarListItemLink);
|
||||||
|
|
||||||
|
spaSidebarList.appendChild(spaSidebarListItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
spaSidebarAside.appendChild(spaSidebarList);
|
||||||
|
|
||||||
|
const spaSidebarMainDiv = document.createElement('div');
|
||||||
|
spaSidebarMainDiv.classList.add('spa-sidebar-main');
|
||||||
|
spaSidebarMainDiv.setAttribute('aria-live', 'polite');
|
||||||
|
|
||||||
|
config.sections.forEach(section => {
|
||||||
|
const spaSidebarMainSect = document.createElement('section');
|
||||||
|
spaSidebarMainSect.id = section.id
|
||||||
|
spaSidebarMainSect.classList.add('spa-sidebar-sect')
|
||||||
|
if(section.active) {
|
||||||
|
spaSidebarMainSect.classList.add('active-sect');
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaSidebarContentSlot = document.createElement('slot');
|
||||||
|
spaSidebarContentSlot.name = `content-${section.id}`;
|
||||||
|
//spaSidebarContentSlot.slot = `content-${section.id}`;
|
||||||
|
|
||||||
|
spaSidebarMainSect.appendChild(spaSidebarContentSlot);
|
||||||
|
|
||||||
|
spaSidebarMainDiv.appendChild(spaSidebarMainSect);
|
||||||
|
});
|
||||||
|
|
||||||
|
spaSidebarContainerDiv.appendChild(spaSidebarAside);
|
||||||
|
|
||||||
|
spaSidebarContainerDiv.appendChild(spaSidebarMainDiv);
|
||||||
|
|
||||||
|
// Finally, append the entire spinner span to the shadow DOM of the component
|
||||||
|
shadow.appendChild(spaSidebarContainerDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// If a hash is present in the URL on initial load, use it to set the active section
|
||||||
|
//
|
||||||
|
// This is primarily so that when people use the global event switcher drop down they are put back in the same section.
|
||||||
|
// Which should help with the ideas around relatively quickly rapidly doing the same action across events.
|
||||||
|
// For example, if an organizer hs let's say 3 upcoming events and they book ASL they might do it for all 3 at once.
|
||||||
|
// When they get the confirmation they'd likely want to set it for each of them quickly so this should help support that.
|
||||||
|
if(typeof window.location.hash === 'string' && window.location.hash.substring(1).trim() !== '') {
|
||||||
|
const initialSect = window.location.hash.substring(1);
|
||||||
|
|
||||||
|
console.log('Initial Section from URL Hash:', initialSect);
|
||||||
|
|
||||||
|
// Remove default 'active-sect' class from all currently active sections and `active` class from links
|
||||||
|
document.querySelectorAll('.event-page-sect.active-sect').forEach(section => section.classList.remove('active-sect'));
|
||||||
|
document.querySelectorAll('.event-page-sidebar-link.active').forEach(activeLink => activeLink.classList.remove('active'));
|
||||||
|
|
||||||
|
// Add 'active-sect' class to the new section and `active` class to link
|
||||||
|
document.querySelector(`.event-page-sidebar-link[href="#${initialSect}"]`).classList.add('active');
|
||||||
|
document.querySelector(`.event-page-sect#${initialSect}`).classList.add('active-sect');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If no hash is present, set it based on the default active section (if any)
|
||||||
|
window.location.hash = document.querySelector('.event-page-sidebar-link.active').getAttribute('href');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customElements.get('ba-spa-sidebar')) {
|
||||||
|
customElements.define('ba-spa-sidebar', SPASidebar);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue