From be04a27359e7c0d5781876399448e2ec945dec90 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Mon, 25 Mar 2024 16:38:10 +1100 Subject: [PATCH 01/10] lightbox first commit --- src/lightbox/README.md | 12 +++++++ src/lightbox/index.html | 44 +++++++++++++++++++++++ src/lightbox/index.ts | 24 +++++++++++++ src/lightbox/style.scss | 1 + src/lightbox/tp-lightbox-close.ts | 28 +++++++++++++++ src/lightbox/tp-lightbox-content.ts | 5 +++ src/lightbox/tp-lightbox-next.ts | 28 +++++++++++++++ src/lightbox/tp-lightbox-previous.ts | 28 +++++++++++++++ src/lightbox/tp-lightbox-trigger.ts | 33 +++++++++++++++++ src/lightbox/tp-lightbox.ts | 54 ++++++++++++++++++++++++++++ webpack.config.js | 1 + 11 files changed, 258 insertions(+) create mode 100644 src/lightbox/README.md create mode 100644 src/lightbox/index.html create mode 100644 src/lightbox/index.ts create mode 100644 src/lightbox/style.scss create mode 100644 src/lightbox/tp-lightbox-close.ts create mode 100644 src/lightbox/tp-lightbox-content.ts create mode 100644 src/lightbox/tp-lightbox-next.ts create mode 100644 src/lightbox/tp-lightbox-previous.ts create mode 100644 src/lightbox/tp-lightbox-trigger.ts create mode 100644 src/lightbox/tp-lightbox.ts diff --git a/src/lightbox/README.md b/src/lightbox/README.md new file mode 100644 index 0000000..47d14c6 --- /dev/null +++ b/src/lightbox/README.md @@ -0,0 +1,12 @@ +# Lightbox + + + + + + +
+

Built by the super talented team at Travelopia.

+
+ +
diff --git a/src/lightbox/index.html b/src/lightbox/index.html new file mode 100644 index 0000000..a8b9589 --- /dev/null +++ b/src/lightbox/index.html @@ -0,0 +1,44 @@ + + + + + + + Web Component: Lightbox + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/src/lightbox/index.ts b/src/lightbox/index.ts new file mode 100644 index 0000000..26627d7 --- /dev/null +++ b/src/lightbox/index.ts @@ -0,0 +1,24 @@ +/** + * Styles. + */ +import './style.scss'; + +/** + * Components. + */ +import { TPLightboxElement } from './tp-lightbox'; +import { TPLightboxContentElement } from './tp-lightbox-content'; +import { TPLightboxCloseElement } from './tp-lightbox-close'; +import { TPLightboxPreviousElement } from './tp-lightbox-previous'; +import { TPLightboxNextElement } from './tp-lightbox-next'; +import { TPLightboxTriggerElement } from './tp-lightbox-trigger'; + +/** + * Register Components. + */ +customElements.define( 'tp-lightbox', TPLightboxElement ); +customElements.define( 'tp-lightbox-content', TPLightboxContentElement ); +customElements.define( 'tp-lightbox-close', TPLightboxCloseElement ); +customElements.define( 'tp-lightbox-previous', TPLightboxPreviousElement ); +customElements.define( 'tp-lightbox-next', TPLightboxNextElement ); +customElements.define( 'tp-lightbox-trigger', TPLightboxTriggerElement ); diff --git a/src/lightbox/style.scss b/src/lightbox/style.scss new file mode 100644 index 0000000..95cf34e --- /dev/null +++ b/src/lightbox/style.scss @@ -0,0 +1 @@ +tp-lightbox {} diff --git a/src/lightbox/tp-lightbox-close.ts b/src/lightbox/tp-lightbox-close.ts new file mode 100644 index 0000000..36f3474 --- /dev/null +++ b/src/lightbox/tp-lightbox-close.ts @@ -0,0 +1,28 @@ +/** + * Internal dependencies. + */ +import { TPLightboxElement } from './tp-lightbox'; + +/** + * TP Lightbox Close. + */ +export class TPLightboxCloseElement extends HTMLElement { + /** + * Constructor. + */ + constructor() { + super(); + + // Events. + this.querySelector( 'button' )?.addEventListener( 'click', this.close.bind( this ) ); + } + + close(): void { + const lightbox: TPLightboxElement | null = this.closest( 'tp-lightbox' ); + if ( lightbox ) { + setTimeout( (): void => { + lightbox.close(); + }, 0 ); + } + } +} diff --git a/src/lightbox/tp-lightbox-content.ts b/src/lightbox/tp-lightbox-content.ts new file mode 100644 index 0000000..d57f0d7 --- /dev/null +++ b/src/lightbox/tp-lightbox-content.ts @@ -0,0 +1,5 @@ +/** + * TP Lightbox Content. + */ +export class TPLightboxContentElement extends HTMLElement { +} diff --git a/src/lightbox/tp-lightbox-next.ts b/src/lightbox/tp-lightbox-next.ts new file mode 100644 index 0000000..ff1fb65 --- /dev/null +++ b/src/lightbox/tp-lightbox-next.ts @@ -0,0 +1,28 @@ +/** + * Internal dependencies. + */ +import { TPLightboxElement } from './tp-lightbox'; + +/** + * TP Lightbox Close. + */ +export class TPLightboxNextElement extends HTMLElement { + /** + * Constructor. + */ + constructor() { + super(); + + // Events. + this.querySelector( 'button' )?.addEventListener( 'click', this.close.bind( this ) ); + } + + close(): void { + const lightbox: TPLightboxElement | null = this.closest( 'tp-lightbox' ); + if ( lightbox ) { + setTimeout( (): void => { + lightbox.next(); + }, 0 ); + } + } +} diff --git a/src/lightbox/tp-lightbox-previous.ts b/src/lightbox/tp-lightbox-previous.ts new file mode 100644 index 0000000..9123f40 --- /dev/null +++ b/src/lightbox/tp-lightbox-previous.ts @@ -0,0 +1,28 @@ +/** + * Internal dependencies. + */ +import { TPLightboxElement } from './tp-lightbox'; + +/** + * TP Lightbox Close. + */ +export class TPLightboxPreviousElement extends HTMLElement { + /** + * Constructor. + */ + constructor() { + super(); + + // Events. + this.querySelector( 'button' )?.addEventListener( 'click', this.close.bind( this ) ); + } + + close(): void { + const lightbox: TPLightboxElement | null = this.closest( 'tp-lightbox' ); + if ( lightbox ) { + setTimeout( (): void => { + lightbox.previous(); + }, 0 ); + } + } +} diff --git a/src/lightbox/tp-lightbox-trigger.ts b/src/lightbox/tp-lightbox-trigger.ts new file mode 100644 index 0000000..5afd27a --- /dev/null +++ b/src/lightbox/tp-lightbox-trigger.ts @@ -0,0 +1,33 @@ +/** + * Internal dependencies. + */ +import { TPLightboxElement } from './tp-lightbox'; + +/** + * TP Lightbox Trigger. + */ +export class TPLightboxTriggerElement extends HTMLElement { + /** + * Constructor. + */ + constructor() { + super(); + + // Events. + this.querySelector( 'button' )?.addEventListener( 'click', this.trigger.bind( this ) ); + } + + trigger(): void { + const lightboxId: string | null = this.getAttribute( 'lightbox' ); + const template: HTMLTemplateElement | null = this.querySelector( 'template' ); + + if ( ! lightboxId || ! template ) { + return; + } + + const lightbox: TPLightboxElement | null = document.querySelector( `#${ lightboxId.toString() }` ); + setTimeout( (): void => { + lightbox?.open( template ); + }, 0 ); + } +} diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts new file mode 100644 index 0000000..25a9a50 --- /dev/null +++ b/src/lightbox/tp-lightbox.ts @@ -0,0 +1,54 @@ +/** + * Internal dependencies. + */ +import { TPLightboxContentElement } from './tp-lightbox-content'; + +/** + * TP Lightbox. + */ +export class TPLightboxElement extends HTMLElement { + /** + * Properties. + */ + protected group: string = ''; + + /** + * Open lightbox with contents of a TEMPLATE tag. + * + * @param {Object} template HTML template object. + * @param {string} group The lightbox group. + */ + open( template: HTMLTemplateElement, group: string = '' ): void { + const dialog: HTMLDialogElement | null = this.querySelector( 'dialog' ); + const content: TPLightboxContentElement | null = this.querySelector( 'tp-lightbox-content' ); + + if ( ! dialog || ! content ) { + return; + } + + const templateContent: Node = template.content.cloneNode( true ); + content.replaceChildren( templateContent ); + + this.group = group; + this.prepareNavigation(); + + dialog.showModal(); + } + + close(): void { + const dialog: HTMLDialogElement | null = this.querySelector( 'dialog' ); + dialog?.close(); + } + + previous(): void { + console.log( 'previous' ); + } + + next(): void { + console.log( 'next' ); + } + + prepareNavigation(): void { + console.log( 'prepare nav' ); + } +} diff --git a/webpack.config.js b/webpack.config.js index e75ea95..14f1bb9 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -82,6 +82,7 @@ module.exports = ( env ) => { form: './src/form/index.ts', accordion: './src/accordion/index.ts', 'multi-select': './src/multi-select/index.ts', + lightbox: './src/lightbox/index.ts', }, module: { rules: [ From 6434790dab268b5ad0a87ead4570aad0d2295924 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 2 Apr 2024 14:54:30 +1100 Subject: [PATCH 02/10] lightbox open and navigation --- src/lightbox/tp-lightbox-close.ts | 3 + src/lightbox/tp-lightbox-next.ts | 11 +- src/lightbox/tp-lightbox-previous.ts | 11 +- src/lightbox/tp-lightbox-trigger.ts | 11 +- src/lightbox/tp-lightbox.ts | 232 +++++++++++++++++++++++++-- 5 files changed, 248 insertions(+), 20 deletions(-) diff --git a/src/lightbox/tp-lightbox-close.ts b/src/lightbox/tp-lightbox-close.ts index 36f3474..7c4b9b2 100644 --- a/src/lightbox/tp-lightbox-close.ts +++ b/src/lightbox/tp-lightbox-close.ts @@ -17,6 +17,9 @@ export class TPLightboxCloseElement extends HTMLElement { this.querySelector( 'button' )?.addEventListener( 'click', this.close.bind( this ) ); } + /** + * Close the lightbox. + */ close(): void { const lightbox: TPLightboxElement | null = this.closest( 'tp-lightbox' ); if ( lightbox ) { diff --git a/src/lightbox/tp-lightbox-next.ts b/src/lightbox/tp-lightbox-next.ts index ff1fb65..126f4b0 100644 --- a/src/lightbox/tp-lightbox-next.ts +++ b/src/lightbox/tp-lightbox-next.ts @@ -14,10 +14,17 @@ export class TPLightboxNextElement extends HTMLElement { super(); // Events. - this.querySelector( 'button' )?.addEventListener( 'click', this.close.bind( this ) ); + this.querySelector( 'button' )?.addEventListener( 'click', this.next.bind( this ) ); } - close(): void { + /** + * Navigate next. + */ + next(): void { + if ( 'yes' === this.getAttribute( 'disabled' ) ) { + return; + } + const lightbox: TPLightboxElement | null = this.closest( 'tp-lightbox' ); if ( lightbox ) { setTimeout( (): void => { diff --git a/src/lightbox/tp-lightbox-previous.ts b/src/lightbox/tp-lightbox-previous.ts index 9123f40..d303186 100644 --- a/src/lightbox/tp-lightbox-previous.ts +++ b/src/lightbox/tp-lightbox-previous.ts @@ -14,10 +14,17 @@ export class TPLightboxPreviousElement extends HTMLElement { super(); // Events. - this.querySelector( 'button' )?.addEventListener( 'click', this.close.bind( this ) ); + this.querySelector( 'button' )?.addEventListener( 'click', this.previous.bind( this ) ); } - close(): void { + /** + * Navigate previous. + */ + previous(): void { + if ( 'yes' === this.getAttribute( 'disabled' ) ) { + return; + } + const lightbox: TPLightboxElement | null = this.closest( 'tp-lightbox' ); if ( lightbox ) { setTimeout( (): void => { diff --git a/src/lightbox/tp-lightbox-trigger.ts b/src/lightbox/tp-lightbox-trigger.ts index 5afd27a..d9f9e23 100644 --- a/src/lightbox/tp-lightbox-trigger.ts +++ b/src/lightbox/tp-lightbox-trigger.ts @@ -17,6 +17,9 @@ export class TPLightboxTriggerElement extends HTMLElement { this.querySelector( 'button' )?.addEventListener( 'click', this.trigger.bind( this ) ); } + /** + * Trigger the lightbox. + */ trigger(): void { const lightboxId: string | null = this.getAttribute( 'lightbox' ); const template: HTMLTemplateElement | null = this.querySelector( 'template' ); @@ -26,8 +29,14 @@ export class TPLightboxTriggerElement extends HTMLElement { } const lightbox: TPLightboxElement | null = document.querySelector( `#${ lightboxId.toString() }` ); + if ( ! lightbox ) { + return; + } + setTimeout( (): void => { - lightbox?.open( template ); + lightbox.template = template; + lightbox.group = this.getAttribute( 'group' ) ?? ''; + lightbox.open(); }, 0 ); } } diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index 25a9a50..55ab8cf 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -2,6 +2,9 @@ * Internal dependencies. */ import { TPLightboxContentElement } from './tp-lightbox-content'; +import { TPLightboxPreviousElement } from './tp-lightbox-previous'; +import { TPLightboxNextElement } from './tp-lightbox-next'; +import { TPLightboxTriggerElement } from './tp-lightbox-trigger'; /** * TP Lightbox. @@ -10,45 +13,244 @@ export class TPLightboxElement extends HTMLElement { /** * Properties. */ - protected group: string = ''; + protected currentTemplate: HTMLTemplateElement | null = null; + protected currentGroup: string = ''; + protected allGroups: NodeListOf | null = null; /** - * Open lightbox with contents of a TEMPLATE tag. + * Get observed attributes. * - * @param {Object} template HTML template object. - * @param {string} group The lightbox group. + * @return {Array} List of observed attributes. */ - open( template: HTMLTemplateElement, group: string = '' ): void { - const dialog: HTMLDialogElement | null = this.querySelector( 'dialog' ); - const content: TPLightboxContentElement | null = this.querySelector( 'tp-lightbox-content' ); + static get observedAttributes(): string[] { + return [ 'index' ]; + } - if ( ! dialog || ! content ) { + /** + * Attribute changed callback. + * + * @param {string} _name Attribute name. + * @param {string} oldValue Old value. + * @param {string} newValue New value. + */ + attributeChangedCallback( _name: string = '', oldValue: string = '', newValue: string = '' ): void { + // Prevent redundant updates. + if ( oldValue === newValue ) { return; } - const templateContent: Node = template.content.cloneNode( true ); - content.replaceChildren( templateContent ); + // Get all groups and check if current index exists within group. + const allGroups: NodeListOf | null = this.getAllGroups(); + if ( ! allGroups || ! allGroups[ this.currentIndex - 1 ] ) { + return; + } + + // Trigger element within group. + allGroups[ this.currentIndex - 1 ].trigger(); + } - this.group = group; - this.prepareNavigation(); + /** + * Get template. + */ + get template(): HTMLTemplateElement | null { + return this.currentTemplate; + } + + /** + * Set template. + * + * @param {HTMLTemplateElement} template The template. + */ + set template( template: HTMLTemplateElement | null ) { + // Set the template. + this.currentTemplate = template; + + // Get lightbox content element. + const content: TPLightboxContentElement | null = this.querySelector( 'tp-lightbox-content' ); + if ( ! content ) { + return; + } + + // Check if we have a template. + if ( this.currentTemplate ) { + // We do, update content with template's content. + // We do this rather than a string to avoid script injection. + const templateContent: Node = this.currentTemplate.content.cloneNode( true ); + content.replaceChildren( templateContent ); + + setTimeout( (): void => { + this.prepareNavigation(); + }, 0 ); + } else { + // We don't, set content as empty. + content.innerHTML = ''; + } + } + + /** + * Get current group. + */ + get group(): string { + return this.currentGroup; + } + + /** + * Set current group. + * + * @param {string} group Group name. + */ + set group( group: string ) { + this.currentGroup = group; + } + + /** + * Get current index. + */ + get currentIndex(): number { + return parseInt( this.getAttribute( 'index' ) ?? '1' ); + } + + /** + * Set current index. + * + * @param {number} index Current index. + */ + set currentIndex( index: number ) { + if ( index < 1 ) { + index = 1; + } + + // Setting this attributes triggers a re-trigger. + this.setAttribute( 'index', index.toString() ); + } + + /** + * Open lightbox. + */ + open(): void { + // Get the dialog element. + const dialog: HTMLDialogElement | null = this.querySelector( 'dialog' ); + // Check if dialog exists or is already open. + if ( ! dialog || dialog.open ) { + return; + } + + // First, take this opportunity to update all groups. + this.updateAllGroups(); + + // Now, show the modal. dialog.showModal(); } + /** + * Close lightbox. + */ close(): void { + // Find and close the dialog. const dialog: HTMLDialogElement | null = this.querySelector( 'dialog' ); dialog?.close(); + + // Clear groups from memory. + this.allGroups = null; } + /** + * Navigate previous. + */ previous(): void { - console.log( 'previous' ); + // Check if we even have a group. + if ( '' === this.group ) { + return; + } + + // Check if we have elements within group. + const allGroups: NodeListOf | null = this.getAllGroups(); + if ( ! allGroups ) { + return; + } + + // Decrement the current index. + if ( this.currentIndex > 1 ) { + this.currentIndex--; + } } + /** + * Navigate next. + */ next(): void { - console.log( 'next' ); + // Check if we even have a group. + if ( '' === this.group ) { + return; + } + + // Check if we have elements within group. + const allGroups: NodeListOf | null = this.getAllGroups(); + if ( ! allGroups ) { + return; + } + + // Increment the current index. + if ( this.currentIndex < allGroups.length ) { + this.currentIndex++; + } + } + + /** + * Update all groups and save it to memory. + */ + updateAllGroups(): void { + this.allGroups = document.querySelectorAll( `tp-lightbox-trigger[group="${ this.group }"]` ); + } + + /** + * Get all groups from memory. + */ + getAllGroups(): NodeListOf | null { + return this.allGroups; } + /** + * Prepare navigation. + */ prepareNavigation(): void { - console.log( 'prepare nav' ); + // Get previous and next elements. + const previous: TPLightboxPreviousElement | null = this.querySelector( 'tp-lightbox-previous' ); + const next: TPLightboxNextElement | null = this.querySelector( 'tp-lightbox-next' ); + + // Bail early if we don't have either. + if ( ! previous && ! next ) { + return; + } + + // Check if we have a group. + if ( '' === this.group ) { + previous?.setAttribute( 'disabled', 'yes' ); + next?.setAttribute( 'disabled', 'yes' ); + return; + } + + // Check if we have elements within the group. + const allGroups: NodeListOf | null = this.getAllGroups(); + if ( ! allGroups ) { + previous?.setAttribute( 'disabled', 'yes' ); + next?.setAttribute( 'disabled', 'yes' ); + return; + } + + // Enable / disable previous navigation. + if ( this.currentIndex <= 1 ) { + previous?.setAttribute( 'disabled', 'yes' ); + } else { + previous?.removeAttribute( 'disabled' ); + } + + // Enable / disable next navigation. + if ( this.currentIndex < allGroups.length ) { + next?.removeAttribute( 'disabled' ); + } else { + next?.setAttribute( 'disabled', 'yes' ); + } } } From 854787d9537363070864b622b7a28379485f2996 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 2 Apr 2024 15:17:36 +1100 Subject: [PATCH 03/10] images loading attribute --- src/lightbox/tp-lightbox.ts | 74 ++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index 55ab8cf..4e0ed92 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -29,24 +29,20 @@ export class TPLightboxElement extends HTMLElement { /** * Attribute changed callback. * - * @param {string} _name Attribute name. + * @param {string} name Attribute name. * @param {string} oldValue Old value. * @param {string} newValue New value. */ - attributeChangedCallback( _name: string = '', oldValue: string = '', newValue: string = '' ): void { + attributeChangedCallback( name: string = '', oldValue: string = '', newValue: string = '' ): void { // Prevent redundant updates. if ( oldValue === newValue ) { return; } - // Get all groups and check if current index exists within group. - const allGroups: NodeListOf | null = this.getAllGroups(); - if ( ! allGroups || ! allGroups[ this.currentIndex - 1 ] ) { - return; + // Trigger current index target if index has changed. + if ( 'index' === name ) { + this.triggerCurrentIndexTarget(); } - - // Trigger element within group. - allGroups[ this.currentIndex - 1 ].trigger(); } /** @@ -79,6 +75,7 @@ export class TPLightboxElement extends HTMLElement { content.replaceChildren( templateContent ); setTimeout( (): void => { + this.prepareImageLoading(); this.prepareNavigation(); }, 0 ); } else { @@ -124,6 +121,20 @@ export class TPLightboxElement extends HTMLElement { this.setAttribute( 'index', index.toString() ); } + /** + * Trigger the target that matches the current index within current group. + */ + triggerCurrentIndexTarget(): void { + // Get all groups and check if current index exists within group. + const allGroups: NodeListOf | null = this.getAllGroups(); + if ( ! allGroups || ! allGroups[ this.currentIndex - 1 ] ) { + return; + } + + // Trigger element within group. + allGroups[ this.currentIndex - 1 ].trigger(); + } + /** * Open lightbox. */ @@ -253,4 +264,49 @@ export class TPLightboxElement extends HTMLElement { next?.setAttribute( 'disabled', 'yes' ); } } + + /** + * Prepare image loading. + */ + prepareImageLoading(): void { + // Get lightbox content element. + const content: TPLightboxContentElement | null = this.querySelector( 'tp-lightbox-content' ); + if ( ! content ) { + return; + } + + // Bail if there are no images within current content. + const images: NodeListOf = content.querySelectorAll( 'img' ); + if ( ! images ) { + return; + } + + // Start off by setting the state as loading. + this.setAttribute( 'loading', 'yes' ); + + // Prepare increment variables. + let counter: number = 0; + const totalImages: number = images.length; + + /** + * Increment counter. + */ + const incrementLoadingCounter = (): void => { + counter++; + + // Remove loading attribute once all images have loaded. + if ( counter === totalImages ) { + this.removeAttribute( 'loading' ); + } + }; + + // Check if images have loaded, else add an event listener. + images.forEach( ( image: HTMLImageElement ): void => { + if ( image.complete ) { + incrementLoadingCounter(); + } else { + image.addEventListener( 'load', incrementLoadingCounter, { once: true } ); + } + } ); + } } From 489bb63d614c7d4f27536188398a39e19e6548f7 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 2 Apr 2024 16:35:12 +1100 Subject: [PATCH 04/10] close on overlay click, styles --- src/lightbox/style.scss | 45 ++++++++++++++++++++++++++++++++++++- src/lightbox/tp-lightbox.ts | 24 ++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/lightbox/style.scss b/src/lightbox/style.scss index 95cf34e..0a13562 100644 --- a/src/lightbox/style.scss +++ b/src/lightbox/style.scss @@ -1 +1,44 @@ -tp-lightbox {} +// Prevent scrolling when lightbox is open. +:root:has(tp-lightbox dialog[open]) { + overflow: clip; +} + +@keyframes show-tp-lightbox { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +tp-lightbox { + + dialog { + border: 0; + padding: 0; + + &[open] { + animation-name: show-tp-lightbox; + animation-duration: 0.5s; + animation-timing-function: ease-in-out; + } + + &::backdrop { + background: rgba(0, 0, 0, 0.6); + } + } +} + +tp-lightbox-content { + display: block; + position: relative; +} + +tp-lightbox[loading] tp-lightbox-content::after { + position: absolute; + content: "Loading..."; + z-index: 5; + top: 50px; + left: 50px; +} diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index 4e0ed92..deac189 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -17,6 +17,16 @@ export class TPLightboxElement extends HTMLElement { protected currentGroup: string = ''; protected allGroups: NodeListOf | null = null; + /** + * Constructor. + */ + constructor() { + super(); + + // Event listeners. + this.querySelector( 'dialog' )?.addEventListener( 'click', this.handleDialogClick.bind( this ) ); + } + /** * Get observed attributes. * @@ -309,4 +319,18 @@ export class TPLightboxElement extends HTMLElement { } } ); } + + /** + * Handle when the dialog is clicked. + * + * @param {Event} e Click event. + */ + handleDialogClick( e: MouseEvent ): void { + if ( + 'yes' === this.getAttribute( 'close-on-overlay-click' ) && + this.querySelector( 'dialog' ) === e.target + ) { + this.close(); + } + } } From f21e36d0000a32025d411ba16ff709f45202f9a6 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 2 Apr 2024 16:56:30 +1100 Subject: [PATCH 05/10] fix loading --- src/lightbox/tp-lightbox.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index deac189..63f6bcf 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -223,6 +223,9 @@ export class TPLightboxElement extends HTMLElement { */ updateAllGroups(): void { this.allGroups = document.querySelectorAll( `tp-lightbox-trigger[group="${ this.group }"]` ); + if ( ! this.allGroups.length ) { + this.allGroups = null; + } } /** @@ -287,7 +290,8 @@ export class TPLightboxElement extends HTMLElement { // Bail if there are no images within current content. const images: NodeListOf = content.querySelectorAll( 'img' ); - if ( ! images ) { + if ( ! images.length ) { + this.removeAttribute( 'loading' ); return; } From d629852ff0d534a65ea136a65b03f72638d3a545 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 2 Apr 2024 17:33:13 +1100 Subject: [PATCH 06/10] fix trigger index --- src/lightbox/index.html | 23 +++++++++++++++++++--- src/lightbox/tp-lightbox-trigger.ts | 30 ++++++++++++++++++++++++++++- src/lightbox/tp-lightbox.ts | 15 ++++++++++++--- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/lightbox/index.html b/src/lightbox/index.html index a8b9589..3ca5f89 100644 --- a/src/lightbox/index.html +++ b/src/lightbox/index.html @@ -11,7 +11,7 @@
- + @@ -27,18 +27,35 @@ - + - + + +

+ + + + + + + + + +
diff --git a/src/lightbox/tp-lightbox-trigger.ts b/src/lightbox/tp-lightbox-trigger.ts index d9f9e23..1700e88 100644 --- a/src/lightbox/tp-lightbox-trigger.ts +++ b/src/lightbox/tp-lightbox-trigger.ts @@ -21,21 +21,49 @@ export class TPLightboxTriggerElement extends HTMLElement { * Trigger the lightbox. */ trigger(): void { + // Get lightbox ID and template. const lightboxId: string | null = this.getAttribute( 'lightbox' ); const template: HTMLTemplateElement | null = this.querySelector( 'template' ); + // We can't proceed without them. if ( ! lightboxId || ! template ) { return; } + // Get the lightbox. const lightbox: TPLightboxElement | null = document.querySelector( `#${ lightboxId.toString() }` ); if ( ! lightbox ) { return; } + // Check to see if we have a group. + const group: string = this.getAttribute( 'group' ) ?? ''; + + // Yield to main thread. setTimeout( (): void => { + // Prepare lightbox. lightbox.template = template; - lightbox.group = this.getAttribute( 'group' ) ?? ''; + lightbox.group = group; + + // Set index and group if we have them. + if ( '' !== group ) { + const allGroups: NodeListOf = document.querySelectorAll( `tp-lightbox-trigger[group="${ group }"]` ); + if ( allGroups.length ) { + // Update all groups. + // We do this when we're opening a lightbox, or navigating. + // This allows consumers to inject elements at any point. + lightbox.updateAllGroups( allGroups ); + + // Get current trigger's index within the group. + allGroups.forEach( ( triggerElement: TPLightboxTriggerElement, index: number ): void => { + if ( this === triggerElement ) { + lightbox.currentIndex = index + 1; + } + } ); + } + } + + // All done, lets open the lightbox. lightbox.open(); }, 0 ); } diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index 63f6bcf..0e957b8 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -157,8 +157,10 @@ export class TPLightboxElement extends HTMLElement { return; } - // First, take this opportunity to update all groups. - this.updateAllGroups(); + // First, take this opportunity to update all groups (if it wasn't set from the trigger). + if ( '' !== this.group && ! this.allGroups ) { + this.updateAllGroups(); + } // Now, show the modal. dialog.showModal(); @@ -220,8 +222,15 @@ export class TPLightboxElement extends HTMLElement { /** * Update all groups and save it to memory. + * + * @param {NodeList} allGroups All groups. */ - updateAllGroups(): void { + updateAllGroups( allGroups: NodeListOf | null = null ): void { + if ( allGroups && allGroups.length ) { + this.allGroups = allGroups; + return; + } + this.allGroups = document.querySelectorAll( `tp-lightbox-trigger[group="${ this.group }"]` ); if ( ! this.allGroups.length ) { this.allGroups = null; From 4de0a12326a472d55c79bc35ec492455a713db13 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 2 Apr 2024 18:48:43 +1100 Subject: [PATCH 07/10] lightbox counter --- src/lightbox/index.html | 1 + src/lightbox/index.ts | 2 + src/lightbox/tp-lightbox-count.ts | 64 +++++++++++++++++++++++++++++++ src/lightbox/tp-lightbox.ts | 12 +++++- 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/lightbox/tp-lightbox-count.ts diff --git a/src/lightbox/index.html b/src/lightbox/index.html index 3ca5f89..be520eb 100644 --- a/src/lightbox/index.html +++ b/src/lightbox/index.html @@ -23,6 +23,7 @@ + diff --git a/src/lightbox/index.ts b/src/lightbox/index.ts index 26627d7..e2c359d 100644 --- a/src/lightbox/index.ts +++ b/src/lightbox/index.ts @@ -11,6 +11,7 @@ import { TPLightboxContentElement } from './tp-lightbox-content'; import { TPLightboxCloseElement } from './tp-lightbox-close'; import { TPLightboxPreviousElement } from './tp-lightbox-previous'; import { TPLightboxNextElement } from './tp-lightbox-next'; +import { TPLightboxCountElement } from './tp-lightbox-count'; import { TPLightboxTriggerElement } from './tp-lightbox-trigger'; /** @@ -21,4 +22,5 @@ customElements.define( 'tp-lightbox-content', TPLightboxContentElement ); customElements.define( 'tp-lightbox-close', TPLightboxCloseElement ); customElements.define( 'tp-lightbox-previous', TPLightboxPreviousElement ); customElements.define( 'tp-lightbox-next', TPLightboxNextElement ); +customElements.define( 'tp-lightbox-count', TPLightboxCountElement ); customElements.define( 'tp-lightbox-trigger', TPLightboxTriggerElement ); diff --git a/src/lightbox/tp-lightbox-count.ts b/src/lightbox/tp-lightbox-count.ts new file mode 100644 index 0000000..89957cb --- /dev/null +++ b/src/lightbox/tp-lightbox-count.ts @@ -0,0 +1,64 @@ +/** + * Internal dependencies. + */ +import { TPLightboxElement } from './tp-lightbox'; + +/** + * TP Slider Count. + */ +export class TPLightboxCountElement extends HTMLElement { + /** + * Get observed attributes. + * + * @return {Array} Observed attributes. + */ + static get observedAttributes(): string[] { + return [ 'format' ]; + } + + /** + * Get format. + * + * @return {string} Format. + */ + get format(): string { + return this.getAttribute( 'format' ) ?? '$current / $total'; + } + + /** + * Set format. + * + * @param {string} format Format. + */ + set format( format: string ) { + this.setAttribute( 'format', format ); + } + + /** + * Attribute changed callback. + */ + attributeChangedCallback(): void { + this.update(); + } + + /** + * Update component. + */ + update(): void { + const lightbox: TPLightboxElement | null = this.closest( 'tp-lightbox' ); + if ( ! lightbox ) { + return; + } + + const current: string = lightbox.currentIndex.toString(); + const total: string = lightbox.getAttribute( 'total' ) ?? ''; + + this.innerHTML = + this.format + .replace( '$current', current ) + .replace( '$total', total ); + + this.setAttribute( 'current', current ); + this.setAttribute( 'total', total ); + } +} diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index 0e957b8..15c0757 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -5,6 +5,7 @@ import { TPLightboxContentElement } from './tp-lightbox-content'; import { TPLightboxPreviousElement } from './tp-lightbox-previous'; import { TPLightboxNextElement } from './tp-lightbox-next'; import { TPLightboxTriggerElement } from './tp-lightbox-trigger'; +import { TPLightboxCountElement } from './tp-lightbox-count'; /** * TP Lightbox. @@ -33,7 +34,7 @@ export class TPLightboxElement extends HTMLElement { * @return {Array} List of observed attributes. */ static get observedAttributes(): string[] { - return [ 'index' ]; + return [ 'index', 'total', 'close-on-overlay-click', 'loading' ]; } /** @@ -49,6 +50,8 @@ export class TPLightboxElement extends HTMLElement { return; } + this.dispatchEvent( new CustomEvent( 'change' ) ); + // Trigger current index target if index has changed. if ( 'index' === name ) { this.triggerCurrentIndexTarget(); @@ -228,12 +231,15 @@ export class TPLightboxElement extends HTMLElement { updateAllGroups( allGroups: NodeListOf | null = null ): void { if ( allGroups && allGroups.length ) { this.allGroups = allGroups; + this.setAttribute( 'total', this.allGroups.length.toString() ); return; } this.allGroups = document.querySelectorAll( `tp-lightbox-trigger[group="${ this.group }"]` ); if ( ! this.allGroups.length ) { this.allGroups = null; + } else { + this.setAttribute( 'total', this.allGroups.length.toString() ); } } @@ -251,6 +257,7 @@ export class TPLightboxElement extends HTMLElement { // Get previous and next elements. const previous: TPLightboxPreviousElement | null = this.querySelector( 'tp-lightbox-previous' ); const next: TPLightboxNextElement | null = this.querySelector( 'tp-lightbox-next' ); + const count: TPLightboxCountElement | null = this.querySelector( 'tp-lightbox-count' ); // Bail early if we don't have either. if ( ! previous && ! next ) { @@ -285,6 +292,9 @@ export class TPLightboxElement extends HTMLElement { } else { next?.setAttribute( 'disabled', 'yes' ); } + + // Update counter. + count?.update(); } /** From 777fd441c8dddc4f37f7e3b4d737bf3a891d742f Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Tue, 2 Apr 2024 18:49:37 +1100 Subject: [PATCH 08/10] update counter --- src/lightbox/tp-lightbox.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index 15c0757..f674b71 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -254,10 +254,13 @@ export class TPLightboxElement extends HTMLElement { * Prepare navigation. */ prepareNavigation(): void { + // Update counter. + const count: TPLightboxCountElement | null = this.querySelector( 'tp-lightbox-count' ); + count?.update(); + // Get previous and next elements. const previous: TPLightboxPreviousElement | null = this.querySelector( 'tp-lightbox-previous' ); const next: TPLightboxNextElement | null = this.querySelector( 'tp-lightbox-next' ); - const count: TPLightboxCountElement | null = this.querySelector( 'tp-lightbox-count' ); // Bail early if we don't have either. if ( ! previous && ! next ) { @@ -292,9 +295,6 @@ export class TPLightboxElement extends HTMLElement { } else { next?.setAttribute( 'disabled', 'yes' ); } - - // Update counter. - count?.update(); } /** From c9e92b2384b5b48a5ef0e9d29ab42832e76d56a5 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Wed, 3 Apr 2024 09:56:40 +1100 Subject: [PATCH 09/10] update readme --- src/lightbox/README.md | 37 +++++++++++++++++++++++++++++++++++++ src/lightbox/index.html | 4 ++-- src/lightbox/tp-lightbox.ts | 4 +++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/lightbox/README.md b/src/lightbox/README.md index 47d14c6..63a680a 100644 --- a/src/lightbox/README.md +++ b/src/lightbox/README.md @@ -10,3 +10,40 @@ + +## Sample Usage + +This is a super minimal modal that is designed to be highly extendable. + +Example: + +First, create the lightbox and give it an ID. Style as needed: + +```html + + + + <-- There must be a button inside this component. + + + <-- There must be a button inside this component. + + + <-- There must be a button inside this component. + + + + + +``` + +Next, we need to trigger the lightbox with and give it some content. Any content added inside the `template` will be added to the lightbox, so you have full control over it: + +```html + <-- Group multiple lightboxes together with a unique name. + <-- There must be a button inside this component. + + +``` diff --git a/src/lightbox/index.html b/src/lightbox/index.html index be520eb..a6da0cd 100644 --- a/src/lightbox/index.html +++ b/src/lightbox/index.html @@ -30,14 +30,14 @@ diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index f674b71..0bd7dd9 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -34,7 +34,7 @@ export class TPLightboxElement extends HTMLElement { * @return {Array} List of observed attributes. */ static get observedAttributes(): string[] { - return [ 'index', 'total', 'close-on-overlay-click', 'loading' ]; + return [ 'open', 'index', 'total', 'close-on-overlay-click', 'loading' ]; } /** @@ -167,6 +167,7 @@ export class TPLightboxElement extends HTMLElement { // Now, show the modal. dialog.showModal(); + this.setAttribute( 'open', 'yes' ); } /** @@ -176,6 +177,7 @@ export class TPLightboxElement extends HTMLElement { // Find and close the dialog. const dialog: HTMLDialogElement | null = this.querySelector( 'dialog' ); dialog?.close(); + this.removeAttribute( 'open' ); // Clear groups from memory. this.allGroups = null; From 4257b3ae77987d68d6db1f0cfed443e6b8c16332 Mon Sep 17 00:00:00 2001 From: Junaid Bhura Date: Wed, 3 Apr 2024 10:02:18 +1100 Subject: [PATCH 10/10] add more events --- src/lightbox/README.md | 24 ++++++++++++++++++++++++ src/lightbox/tp-lightbox.ts | 2 ++ 2 files changed, 26 insertions(+) diff --git a/src/lightbox/README.md b/src/lightbox/README.md index 63a680a..33d25d2 100644 --- a/src/lightbox/README.md +++ b/src/lightbox/README.md @@ -47,3 +47,27 @@ Next, we need to trigger the lightbox with and give it some content. Any content ``` + +## Attributes + +| Attribute | Required | Values | Notes | +|------------------------|-----------|----------|----------------------------------------------| +| close-on-overlay-click | No | `yes` | Closes the modal when the overlay is clicked | + +## Events + +| Event | Notes | +|----------------|-------------------------------------------------------------| +| change | When any attribute has changed | +| template-set | When a template is set, before content has actually updated | +| content-update | When the content has updated inside the lightbox | + +## Methods + +### `open` + +Open the lightbox. + +### `close` + +Close the lightbox. diff --git a/src/lightbox/tp-lightbox.ts b/src/lightbox/tp-lightbox.ts index 0bd7dd9..f56b0cc 100644 --- a/src/lightbox/tp-lightbox.ts +++ b/src/lightbox/tp-lightbox.ts @@ -73,6 +73,7 @@ export class TPLightboxElement extends HTMLElement { set template( template: HTMLTemplateElement | null ) { // Set the template. this.currentTemplate = template; + this.dispatchEvent( new CustomEvent( 'template-set' ) ); // Get lightbox content element. const content: TPLightboxContentElement | null = this.querySelector( 'tp-lightbox-content' ); @@ -86,6 +87,7 @@ export class TPLightboxElement extends HTMLElement { // We do this rather than a string to avoid script injection. const templateContent: Node = this.currentTemplate.content.cloneNode( true ); content.replaceChildren( templateContent ); + this.dispatchEvent( new CustomEvent( 'content-change' ) ); setTimeout( (): void => { this.prepareImageLoading();