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
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.id = `${drawerId}-drawer-contents`;
|
||||
contentDiv.id = `${config.drawerId}-drawer-contents`;
|
||||
contentDiv.classList.add('drawer-contents', 'hidden');
|
||||
|
||||
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