kempo-ui 0.4.21 → 0.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import e from"./ShadowComponent.js";import{html as t,css as n}from"../lit-all.min.js";import{boolExists as o}from"../utils/propConverters.js";import"./Icon.js";export default class i extends e{static properties={src:{type:String,reflect:!0},alt:{type:String,reflect:!0},fullscreen:{type:Boolean,reflect:!0,converter:o},keyboardControls:{type:Boolean,reflect:!0,attribute:"keyboard-controls",converter:o},global:{type:Boolean,reflect:!0,converter:o}};constructor(){super(),this.src="",this.alt="",this.fullscreen=!1,this.keyboardControls=!0,this.global=!1}handleImageClick=()=>{this.open()};handleImageKeydown=e=>{"Enter"!==e.key||this.fullscreen||this.open()};handleCloseClick=()=>{this.close()};handleOverlayClick=e=>{e.target===e.currentTarget&&this.close()};handlePrevClick=e=>{e.stopPropagation();const t=this.getPrevSibling();t&&(this.close(),t.open())};handleNextClick=e=>{e.stopPropagation();const t=this.getNextSibling();t&&(this.close(),t.open())};handleKeydown=e=>{if("Escape"===e.key)this.close();else if("ArrowLeft"===e.key){const e=this.getPrevSibling();e&&(this.close(),e.open())}else if("ArrowRight"===e.key){const e=this.getNextSibling();e&&(this.close(),e.open())}};connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this.keyboardControls&&document.removeEventListener("keydown",this.handleKeydown)}updated(e){super.updated(e),e.has("fullscreen")&&(this.dispatchEvent(new CustomEvent("fullscreenchange",{detail:{fullscreen:this.fullscreen}})),this.dispatchEvent(new CustomEvent(this.fullscreen?"fullscreen":"fullscreenclose")),this.updateNavigationState(),this.keyboardControls&&(this.fullscreen?document.addEventListener("keydown",this.handleKeydown):document.removeEventListener("keydown",this.handleKeydown))),(e.has("src")||e.has("alt"))&&this.handleFullscreenCaption()}firstUpdated(){this.handleFullscreenCaption()}open(){this.fullscreen=!0}close(){this.fullscreen=!1}toggle(){this.fullscreen=!this.fullscreen}getPrevSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[0===t?e.length-1:t-1]}{let e=this.previousElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.lastElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;return e}}getNextSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[t===e.length-1?0:t+1]}{let e=this.nextElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.firstElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;return e}}hasPhotoSiblings(){return this.global?document.getElementsByTagName("k-photo-viewer").length>1:Array.from(this.parentElement.children).filter(e=>e!==this&&"K-PHOTO-VIEWER"===e.tagName).length>0}updateNavigationState(){const e=this.fullscreen&&this.hasPhotoSiblings();this.shadowRoot.querySelectorAll(".nav-btn").forEach(t=>{t.style.display=e?"":"none"})}async handleFullscreenCaption(){await this.updateComplete;const e=this.shadowRoot.querySelector('slot[name="fullscreen-caption"]');if(e&&!e.assignedNodes().length){const e=this.shadowRoot.querySelector("slot:not([name])");(e?e.assignedNodes():[]).forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e.cloneNode(!0);t.slot="fullscreen-caption",this.appendChild(t)}})}}static styles=n`
1
+ import e from"./ShadowComponent.js";import{html as t,css as n,render as o}from"../lit-all.min.js";import{boolExists as i}from"../utils/propConverters.js";import"./Icon.js";export default class l extends e{static properties={src:{type:String,reflect:!0},alt:{type:String,reflect:!0},fullscreen:{type:Boolean,reflect:!0,converter:i},keyboardControls:{type:Boolean,reflect:!0,attribute:"keyboard-controls",converter:i},global:{type:Boolean,reflect:!0,converter:i}};constructor(){super(),this.src="",this.alt="",this.fullscreen=!1,this.keyboardControls=!0,this.global=!1}handleImageClick=()=>{this.open()};handleImageKeydown=e=>{"Enter"!==e.key||this.fullscreen||this.open()};handleCloseClick=()=>{this.close()};handleOverlayClick=e=>{e.target===e.currentTarget&&this.close()};handlePrevClick=e=>{e.stopPropagation();const t=this.getPrevSibling();t&&(this.close(),t.open())};handleNextClick=e=>{e.stopPropagation();const t=this.getNextSibling();t&&(this.close(),t.open())};handleKeydown=e=>{if("Escape"===e.key)this.close();else if("ArrowLeft"===e.key){const e=this.getPrevSibling();e&&(this.close(),e.open())}else if("ArrowRight"===e.key){const e=this.getNextSibling();e&&(this.close(),e.open())}};connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this.keyboardControls&&document.removeEventListener("keydown",this.handleKeydown)}updated(e){super.updated(e),e.has("fullscreen")&&(this.dispatchEvent(new CustomEvent("fullscreenchange",{detail:{fullscreen:this.fullscreen}})),this.dispatchEvent(new CustomEvent(this.fullscreen?"fullscreen":"fullscreenclose")),this.updateNavigationState(),this.keyboardControls&&(this.fullscreen?document.addEventListener("keydown",this.handleKeydown):document.removeEventListener("keydown",this.handleKeydown))),(e.has("src")||e.has("alt"))&&this.handleFullscreenCaption()}firstUpdated(){this.handleFullscreenCaption()}open(){this.fullscreen=!0}close(){this.fullscreen=!1}toggle(){this.fullscreen=!this.fullscreen}static open(e,t=0,n={}){const{keyboardControls:i=!0,onClose:l}=n,s=document.createElement("div");s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="0",s.style.height="0",s.style.overflow="hidden";const r=e.map(e=>{const t=document.createElement("k-photo-viewer");return t.src=e.src,t.alt=e.alt||"",t.keyboardControls=i,void 0!==e.caption&&("string"==typeof e.caption?t.innerHTML=e.caption:o(e.caption,t)),s.appendChild(t),t});document.body.appendChild(s);const a=()=>{r.some(e=>e.fullscreen)||(r.forEach(e=>e.removeEventListener("fullscreenclose",a)),s.remove(),l?.())};r.forEach(e=>e.addEventListener("fullscreenclose",a));const c=Math.min(Math.max(t,0),r.length-1);return r[c].open(),r[c]}getPrevSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[0===t?e.length-1:t-1]}{let e=this.previousElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.lastElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;return e}}getNextSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[t===e.length-1?0:t+1]}{let e=this.nextElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.firstElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;return e}}hasPhotoSiblings(){return this.global?document.getElementsByTagName("k-photo-viewer").length>1:Array.from(this.parentElement.children).filter(e=>e!==this&&"K-PHOTO-VIEWER"===e.tagName).length>0}updateNavigationState(){const e=this.fullscreen&&this.hasPhotoSiblings();this.shadowRoot.querySelectorAll(".nav-btn").forEach(t=>{t.style.display=e?"":"none"})}async handleFullscreenCaption(){await this.updateComplete;const e=this.shadowRoot.querySelector('slot[name="fullscreen-caption"]');if(e&&!e.assignedNodes().length){const e=this.shadowRoot.querySelector("slot:not([name])");(e?e.assignedNodes():[]).forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e.cloneNode(!0);t.slot="fullscreen-caption",this.appendChild(t)}})}}static styles=n`
2
2
  :host {
3
3
  display: block;
4
4
  }
@@ -132,4 +132,4 @@ import e from"./ShadowComponent.js";import{html as t,css as n}from"../lit-all.mi
132
132
  </div>
133
133
  </div>
134
134
  </div>
135
- `}}customElements.define("k-photo-viewer",i);
135
+ `}}customElements.define("k-photo-viewer",l);
@@ -506,9 +506,11 @@
506
506
  <a href="#globalNavigation">Global Navigation</a><br />
507
507
  <a href="#customControls">Custom Controls</a><br />
508
508
  <a href="#captions">Captions</a><br />
509
-
509
+ <a href="#programmaticGallery">Programmatic Gallery</a><br />
510
+
510
511
  <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
511
512
  <a href="#constructor">Constructor</a><br />
513
+ <a href="#staticMethods">Static Methods</a><br />
512
514
  <a href="#requirements">Requirements</a><br />
513
515
  <a href="#properties">Properties</a><br />
514
516
  <a href="#methods">Methods</a><br />
@@ -665,6 +667,34 @@
665
667
  </div>
666
668
  </div>
667
669
 
670
+ <h3 id="programmaticGallery"><a href="#programmaticGallery" class="no-link">Programmatic Gallery</a></h3>
671
+ <p>Use the static <code>PhotoViewer.open(content, openIndex, options)</code> method to launch a fullscreen gallery popup directly from JavaScript, without needing any <code>k-photo-viewer</code> thumbnails in the page. <code>content</code> is an array of <code>{ src, caption }</code> objects, where <code>caption</code> can be an HTML string or a lit <code>html</code> template. <code>openIndex</code> (optional, default <code>0</code>) is the index of the photo to open first, and navigation wraps within the array.</p>
672
+
673
+ <div class="row -mx mb">
674
+ <div class="col d-span-6 m-span-12 px">
675
+ <k-card label="HTML">
676
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"galleryBtn"</span>&gt;</span>Open Gallery<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span>&gt;</span><span class="javascript"><br /> <span class="hljs-keyword">import</span> <span class="hljs-title class_">PhotoViewer</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'/src/components/PhotoViewer.js'</span>;<br /> <span class="hljs-keyword">import</span> { html } <span class="hljs-keyword">from</span> <span class="hljs-string">'/src/lit-all.min.js'</span>;<br /><br /> <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'galleryBtn'</span>).<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">'click'</span>, () =&gt; {<br /> <span class="hljs-title class_">PhotoViewer</span>.<span class="hljs-title function_">open</span>([<br /> { <span class="hljs-attr">src</span>: <span class="hljs-string">'../media/civic.jpg'</span>, <span class="hljs-attr">caption</span>: <span class="hljs-string">'&lt;h6&gt;Honda Civic Type R&lt;/h6&gt;'</span> },<br /> { <span class="hljs-attr">src</span>: <span class="hljs-string">'../media/corvette.jpg'</span>, <span class="hljs-attr">caption</span>: html`<span class="hljs-tag">&lt;<span class="hljs-name">h6</span>&gt;</span>Chevrolet Corvette C8<span class="hljs-tag">&lt;/<span class="hljs-name">h6</span>&gt;</span>` },<br /> { <span class="hljs-attr">src</span>: <span class="hljs-string">'../media/evo.jpg'</span>, <span class="hljs-attr">caption</span>: <span class="hljs-string">'&lt;h6&gt;Mitsubishi Lancer Evolution&lt;/h6&gt;'</span> }<br /> ], <span class="hljs-number">1</span>);<br /> });<br /></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></code></pre>
677
+ </k-card>
678
+ </div>
679
+ <div class="col d-span-6 m-span-12 px">
680
+ <k-card label="Results">
681
+ <button id="galleryBtn" class="mb">Open Gallery</button>
682
+ <script type="module">
683
+ import PhotoViewer from '../src/components/PhotoViewer.js';
684
+ import { html } from '../src/lit-all.min.js';
685
+
686
+ document.getElementById('galleryBtn').addEventListener('click', () => {
687
+ PhotoViewer.open([
688
+ { src: '../media/civic.jpg', caption: '<h6>Honda Civic Type R</h6>' },
689
+ { src: '../media/corvette.jpg', caption: html`<h6>Chevrolet Corvette C8</h6>` },
690
+ { src: '../media/evo.jpg', caption: '<h6>Mitsubishi Lancer Evolution</h6>' }
691
+ ], 1);
692
+ });
693
+ </script>
694
+ </k-card>
695
+ </div>
696
+ </div>
697
+
668
698
  <h2 id="jsRef"><a href="#jsRef" class="no-link">JavaScript Reference</a></h2>
669
699
 
670
700
  <h3 id="constructor"><a href="#constructor" class="no-link">Constructor</a></h3>
@@ -674,6 +704,17 @@
674
704
  </h5>
675
705
  <p>Creates a new PhotoViewer instance. Configuration is done through attributes or properties.</p>
676
706
 
707
+ <h3 id="staticMethods"><a href="#staticMethods" class="no-link">Static Methods</a></h3>
708
+ <h5><code>PhotoViewer.open(content<i>: Array&lt;{ src, caption }&gt;</i>, openIndex<i>?: number</i>, options<i>?: object</i>)<i>: PhotoViewer</i></code></h5>
709
+ <p>Opens a fullscreen gallery popup without needing any <code>k-photo-viewer</code> elements on the page. Creates a hidden, isolated set of PhotoViewers, opens the one at <code>openIndex</code>, and removes them all from the DOM once the gallery is fully closed.</p>
710
+ <ul>
711
+ <li><strong>content</strong>: Array of <code>{ src, caption, alt }</code> objects. <code>src</code> is the image URL. <code>caption</code> is optional and may be an HTML string or a lit <code>html</code> template result; it's used as both the default caption and the fullscreen caption. <code>alt</code> is optional alt text for the image.</li>
712
+ <li><strong>openIndex</strong>: Index within <code>content</code> to open first. Defaults to <code>0</code>.</li>
713
+ <li><strong>options.keyboardControls</strong>: Whether keyboard controls (Escape, Arrow Left/Right) are enabled for the popup. Defaults to <code>true</code>.</li>
714
+ <li><strong>options.onClose</strong>: Callback invoked after the gallery is fully closed and removed from the DOM.</li>
715
+ </ul>
716
+ <p>Returns the <code>PhotoViewer</code> instance that was opened.</p>
717
+
677
718
  <h3 id="requirements"><a href="#requirements" class="no-link">Requirements</a></h3>
678
719
  <ul>
679
720
  <li><a href="./ShadowComponent.html">ShadowComponent</a></li>
@@ -1,4 +1,4 @@
1
- import e from"./ShadowComponent.js";import{html as t,css as n}from"../lit-all.min.js";import{boolExists as o}from"../utils/propConverters.js";import"./Icon.js";export default class i extends e{static properties={src:{type:String,reflect:!0},alt:{type:String,reflect:!0},fullscreen:{type:Boolean,reflect:!0,converter:o},keyboardControls:{type:Boolean,reflect:!0,attribute:"keyboard-controls",converter:o},global:{type:Boolean,reflect:!0,converter:o}};constructor(){super(),this.src="",this.alt="",this.fullscreen=!1,this.keyboardControls=!0,this.global=!1}handleImageClick=()=>{this.open()};handleImageKeydown=e=>{"Enter"!==e.key||this.fullscreen||this.open()};handleCloseClick=()=>{this.close()};handleOverlayClick=e=>{e.target===e.currentTarget&&this.close()};handlePrevClick=e=>{e.stopPropagation();const t=this.getPrevSibling();t&&(this.close(),t.open())};handleNextClick=e=>{e.stopPropagation();const t=this.getNextSibling();t&&(this.close(),t.open())};handleKeydown=e=>{if("Escape"===e.key)this.close();else if("ArrowLeft"===e.key){const e=this.getPrevSibling();e&&(this.close(),e.open())}else if("ArrowRight"===e.key){const e=this.getNextSibling();e&&(this.close(),e.open())}};connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this.keyboardControls&&document.removeEventListener("keydown",this.handleKeydown)}updated(e){super.updated(e),e.has("fullscreen")&&(this.dispatchEvent(new CustomEvent("fullscreenchange",{detail:{fullscreen:this.fullscreen}})),this.dispatchEvent(new CustomEvent(this.fullscreen?"fullscreen":"fullscreenclose")),this.updateNavigationState(),this.keyboardControls&&(this.fullscreen?document.addEventListener("keydown",this.handleKeydown):document.removeEventListener("keydown",this.handleKeydown))),(e.has("src")||e.has("alt"))&&this.handleFullscreenCaption()}firstUpdated(){this.handleFullscreenCaption()}open(){this.fullscreen=!0}close(){this.fullscreen=!1}toggle(){this.fullscreen=!this.fullscreen}getPrevSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[0===t?e.length-1:t-1]}{let e=this.previousElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.lastElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;return e}}getNextSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[t===e.length-1?0:t+1]}{let e=this.nextElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.firstElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;return e}}hasPhotoSiblings(){return this.global?document.getElementsByTagName("k-photo-viewer").length>1:Array.from(this.parentElement.children).filter(e=>e!==this&&"K-PHOTO-VIEWER"===e.tagName).length>0}updateNavigationState(){const e=this.fullscreen&&this.hasPhotoSiblings();this.shadowRoot.querySelectorAll(".nav-btn").forEach(t=>{t.style.display=e?"":"none"})}async handleFullscreenCaption(){await this.updateComplete;const e=this.shadowRoot.querySelector('slot[name="fullscreen-caption"]');if(e&&!e.assignedNodes().length){const e=this.shadowRoot.querySelector("slot:not([name])");(e?e.assignedNodes():[]).forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e.cloneNode(!0);t.slot="fullscreen-caption",this.appendChild(t)}})}}static styles=n`
1
+ import e from"./ShadowComponent.js";import{html as t,css as n,render as o}from"../lit-all.min.js";import{boolExists as i}from"../utils/propConverters.js";import"./Icon.js";export default class l extends e{static properties={src:{type:String,reflect:!0},alt:{type:String,reflect:!0},fullscreen:{type:Boolean,reflect:!0,converter:i},keyboardControls:{type:Boolean,reflect:!0,attribute:"keyboard-controls",converter:i},global:{type:Boolean,reflect:!0,converter:i}};constructor(){super(),this.src="",this.alt="",this.fullscreen=!1,this.keyboardControls=!0,this.global=!1}handleImageClick=()=>{this.open()};handleImageKeydown=e=>{"Enter"!==e.key||this.fullscreen||this.open()};handleCloseClick=()=>{this.close()};handleOverlayClick=e=>{e.target===e.currentTarget&&this.close()};handlePrevClick=e=>{e.stopPropagation();const t=this.getPrevSibling();t&&(this.close(),t.open())};handleNextClick=e=>{e.stopPropagation();const t=this.getNextSibling();t&&(this.close(),t.open())};handleKeydown=e=>{if("Escape"===e.key)this.close();else if("ArrowLeft"===e.key){const e=this.getPrevSibling();e&&(this.close(),e.open())}else if("ArrowRight"===e.key){const e=this.getNextSibling();e&&(this.close(),e.open())}};connectedCallback(){super.connectedCallback()}disconnectedCallback(){super.disconnectedCallback(),this.keyboardControls&&document.removeEventListener("keydown",this.handleKeydown)}updated(e){super.updated(e),e.has("fullscreen")&&(this.dispatchEvent(new CustomEvent("fullscreenchange",{detail:{fullscreen:this.fullscreen}})),this.dispatchEvent(new CustomEvent(this.fullscreen?"fullscreen":"fullscreenclose")),this.updateNavigationState(),this.keyboardControls&&(this.fullscreen?document.addEventListener("keydown",this.handleKeydown):document.removeEventListener("keydown",this.handleKeydown))),(e.has("src")||e.has("alt"))&&this.handleFullscreenCaption()}firstUpdated(){this.handleFullscreenCaption()}open(){this.fullscreen=!0}close(){this.fullscreen=!1}toggle(){this.fullscreen=!this.fullscreen}static open(e,t=0,n={}){const{keyboardControls:i=!0,onClose:l}=n,s=document.createElement("div");s.style.position="fixed",s.style.top="0",s.style.left="0",s.style.width="0",s.style.height="0",s.style.overflow="hidden";const r=e.map(e=>{const t=document.createElement("k-photo-viewer");return t.src=e.src,t.alt=e.alt||"",t.keyboardControls=i,void 0!==e.caption&&("string"==typeof e.caption?t.innerHTML=e.caption:o(e.caption,t)),s.appendChild(t),t});document.body.appendChild(s);const a=()=>{r.some(e=>e.fullscreen)||(r.forEach(e=>e.removeEventListener("fullscreenclose",a)),s.remove(),l?.())};r.forEach(e=>e.addEventListener("fullscreenclose",a));const c=Math.min(Math.max(t,0),r.length-1);return r[c].open(),r[c]}getPrevSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[0===t?e.length-1:t-1]}{let e=this.previousElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.lastElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.previousElementSibling;return e}}getNextSibling(){if(this.global){const e=Array.from(document.getElementsByTagName("k-photo-viewer")),t=e.indexOf(this);return-1===t?null:e[t===e.length-1?0:t+1]}{let e=this.nextElementSibling;for(;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;if(!e&&this.hasPhotoSiblings())for(e=this.parentElement.firstElementChild;e&&"K-PHOTO-VIEWER"!==e.tagName;)e=e.nextElementSibling;return e}}hasPhotoSiblings(){return this.global?document.getElementsByTagName("k-photo-viewer").length>1:Array.from(this.parentElement.children).filter(e=>e!==this&&"K-PHOTO-VIEWER"===e.tagName).length>0}updateNavigationState(){const e=this.fullscreen&&this.hasPhotoSiblings();this.shadowRoot.querySelectorAll(".nav-btn").forEach(t=>{t.style.display=e?"":"none"})}async handleFullscreenCaption(){await this.updateComplete;const e=this.shadowRoot.querySelector('slot[name="fullscreen-caption"]');if(e&&!e.assignedNodes().length){const e=this.shadowRoot.querySelector("slot:not([name])");(e?e.assignedNodes():[]).forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e.cloneNode(!0);t.slot="fullscreen-caption",this.appendChild(t)}})}}static styles=n`
2
2
  :host {
3
3
  display: block;
4
4
  }
@@ -132,4 +132,4 @@ import e from"./ShadowComponent.js";import{html as t,css as n}from"../lit-all.mi
132
132
  </div>
133
133
  </div>
134
134
  </div>
135
- `}}customElements.define("k-photo-viewer",i);
135
+ `}}customElements.define("k-photo-viewer",l);
@@ -13,9 +13,11 @@
13
13
  <a href="#globalNavigation">Global Navigation</a><br />
14
14
  <a href="#customControls">Custom Controls</a><br />
15
15
  <a href="#captions">Captions</a><br />
16
-
16
+ <a href="#programmaticGallery">Programmatic Gallery</a><br />
17
+
17
18
  <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
18
19
  <a href="#constructor">Constructor</a><br />
20
+ <a href="#staticMethods">Static Methods</a><br />
19
21
  <a href="#requirements">Requirements</a><br />
20
22
  <a href="#properties">Properties</a><br />
21
23
  <a href="#methods">Methods</a><br />
@@ -172,6 +174,34 @@
172
174
  </div>
173
175
  </div>
174
176
 
177
+ <h3 id="programmaticGallery"><a href="#programmaticGallery" class="no-link">Programmatic Gallery</a></h3>
178
+ <p>Use the static <code>PhotoViewer.open(content, openIndex, options)</code> method to launch a fullscreen gallery popup directly from JavaScript, without needing any <code>k-photo-viewer</code> thumbnails in the page. <code>content</code> is an array of <code>{ src, caption }</code> objects, where <code>caption</code> can be an HTML string or a lit <code>html</code> template. <code>openIndex</code> (optional, default <code>0</code>) is the index of the photo to open first, and navigation wraps within the array.</p>
179
+
180
+ <div class="row -mx mb">
181
+ <div class="col d-span-6 m-span-12 px">
182
+ <k-card label="HTML">
183
+ <pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"galleryBtn"</span>&gt;</span>Open Gallery<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><br /><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span>&gt;</span><span class="javascript"><br /> <span class="hljs-keyword">import</span> <span class="hljs-title class_">PhotoViewer</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'/src/components/PhotoViewer.js'</span>;<br /> <span class="hljs-keyword">import</span> { html } <span class="hljs-keyword">from</span> <span class="hljs-string">'/src/lit-all.min.js'</span>;<br /><br /> <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'galleryBtn'</span>).<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">'click'</span>, () =&gt; {<br /> <span class="hljs-title class_">PhotoViewer</span>.<span class="hljs-title function_">open</span>([<br /> { <span class="hljs-attr">src</span>: <span class="hljs-string">'../media/civic.jpg'</span>, <span class="hljs-attr">caption</span>: <span class="hljs-string">'&lt;h6&gt;Honda Civic Type R&lt;/h6&gt;'</span> },<br /> { <span class="hljs-attr">src</span>: <span class="hljs-string">'../media/corvette.jpg'</span>, <span class="hljs-attr">caption</span>: html`<span class="hljs-tag">&lt;<span class="hljs-name">h6</span>&gt;</span>Chevrolet Corvette C8<span class="hljs-tag">&lt;/<span class="hljs-name">h6</span>&gt;</span>` },<br /> { <span class="hljs-attr">src</span>: <span class="hljs-string">'../media/evo.jpg'</span>, <span class="hljs-attr">caption</span>: <span class="hljs-string">'&lt;h6&gt;Mitsubishi Lancer Evolution&lt;/h6&gt;'</span> }<br /> ], <span class="hljs-number">1</span>);<br /> });<br /></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></code></pre>
184
+ </k-card>
185
+ </div>
186
+ <div class="col d-span-6 m-span-12 px">
187
+ <k-card label="Results">
188
+ <button id="galleryBtn" class="mb">Open Gallery</button>
189
+ <script type="module">
190
+ import PhotoViewer from '{{pathToRoot}}src/components/PhotoViewer.js';
191
+ import { html } from '{{pathToRoot}}src/lit-all.min.js';
192
+
193
+ document.getElementById('galleryBtn').addEventListener('click', () => {
194
+ PhotoViewer.open([
195
+ { src: '{{pathToRoot}}media/civic.jpg', caption: '<h6>Honda Civic Type R</h6>' },
196
+ { src: '{{pathToRoot}}media/corvette.jpg', caption: html`<h6>Chevrolet Corvette C8</h6>` },
197
+ { src: '{{pathToRoot}}media/evo.jpg', caption: '<h6>Mitsubishi Lancer Evolution</h6>' }
198
+ ], 1);
199
+ });
200
+ </script>
201
+ </k-card>
202
+ </div>
203
+ </div>
204
+
175
205
  <h2 id="jsRef"><a href="#jsRef" class="no-link">JavaScript Reference</a></h2>
176
206
 
177
207
  <h3 id="constructor"><a href="#constructor" class="no-link">Constructor</a></h3>
@@ -181,6 +211,17 @@
181
211
  </h5>
182
212
  <p>Creates a new PhotoViewer instance. Configuration is done through attributes or properties.</p>
183
213
 
214
+ <h3 id="staticMethods"><a href="#staticMethods" class="no-link">Static Methods</a></h3>
215
+ <h5><code>PhotoViewer.open(content<i>: Array&lt;{ src, caption }&gt;</i>, openIndex<i>?: number</i>, options<i>?: object</i>)<i>: PhotoViewer</i></code></h5>
216
+ <p>Opens a fullscreen gallery popup without needing any <code>k-photo-viewer</code> elements on the page. Creates a hidden, isolated set of PhotoViewers, opens the one at <code>openIndex</code>, and removes them all from the DOM once the gallery is fully closed.</p>
217
+ <ul>
218
+ <li><strong>content</strong>: Array of <code>{ src, caption, alt }</code> objects. <code>src</code> is the image URL. <code>caption</code> is optional and may be an HTML string or a lit <code>html</code> template result; it's used as both the default caption and the fullscreen caption. <code>alt</code> is optional alt text for the image.</li>
219
+ <li><strong>openIndex</strong>: Index within <code>content</code> to open first. Defaults to <code>0</code>.</li>
220
+ <li><strong>options.keyboardControls</strong>: Whether keyboard controls (Escape, Arrow Left/Right) are enabled for the popup. Defaults to <code>true</code>.</li>
221
+ <li><strong>options.onClose</strong>: Callback invoked after the gallery is fully closed and removed from the DOM.</li>
222
+ </ul>
223
+ <p>Returns the <code>PhotoViewer</code> instance that was opened.</p>
224
+
184
225
  <h3 id="requirements"><a href="#requirements" class="no-link">Requirements</a></h3>
185
226
  <ul>
186
227
  <li><a href="./ShadowComponent.html">ShadowComponent</a></li>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-ui",
3
- "version": "0.4.21",
3
+ "version": "0.4.22",
4
4
  "type": "module",
5
5
  "description": "A Lit based web-component library",
6
6
  "main": "index.js",
@@ -1,5 +1,5 @@
1
1
  import ShadowComponent from './ShadowComponent.js';
2
- import { html, css } from '../lit-all.min.js';
2
+ import { html, css, render } from '../lit-all.min.js';
3
3
  import { boolExists } from '../utils/propConverters.js';
4
4
  import './Icon.js';
5
5
 
@@ -138,6 +138,53 @@ export default class PhotoViewer extends ShadowComponent {
138
138
  this.fullscreen = !this.fullscreen;
139
139
  }
140
140
 
141
+ /*
142
+ Static Methods
143
+ */
144
+ static open(content, openIndex = 0, options = {}) {
145
+ const { keyboardControls = true, onClose } = options;
146
+
147
+ const container = document.createElement('div');
148
+ container.style.position = 'fixed';
149
+ container.style.top = '0';
150
+ container.style.left = '0';
151
+ container.style.width = '0';
152
+ container.style.height = '0';
153
+ container.style.overflow = 'hidden';
154
+
155
+ const viewers = content.map(item => {
156
+ const viewer = document.createElement('k-photo-viewer');
157
+ viewer.src = item.src;
158
+ viewer.alt = item.alt || '';
159
+ viewer.keyboardControls = keyboardControls;
160
+ if(item.caption !== undefined) {
161
+ if(typeof item.caption === 'string') {
162
+ viewer.innerHTML = item.caption;
163
+ } else {
164
+ render(item.caption, viewer);
165
+ }
166
+ }
167
+ container.appendChild(viewer);
168
+ return viewer;
169
+ });
170
+
171
+ document.body.appendChild(container);
172
+
173
+ const handleClose = () => {
174
+ if(!viewers.some(viewer => viewer.fullscreen)) {
175
+ viewers.forEach(viewer => viewer.removeEventListener('fullscreenclose', handleClose));
176
+ container.remove();
177
+ onClose?.();
178
+ }
179
+ };
180
+ viewers.forEach(viewer => viewer.addEventListener('fullscreenclose', handleClose));
181
+
182
+ const startIndex = Math.min(Math.max(openIndex, 0), viewers.length - 1);
183
+ viewers[startIndex].open();
184
+
185
+ return viewers[startIndex];
186
+ }
187
+
141
188
  /*
142
189
  Private Methods
143
190
  */
@@ -182,29 +182,6 @@ export default {
182
182
  pass('Send button has padding defined via theme custom property');
183
183
  },
184
184
 
185
- 'send button should be circular': async ({pass, fail}) => {
186
- const { container, chat } = await createChat();
187
-
188
- const sendBtn = chat.shadowRoot?.querySelector('.send-btn');
189
- if(!sendBtn){
190
- cleanup(container);
191
- fail('Send button not found in shadow DOM');
192
- return;
193
- }
194
-
195
- const styles = getComputedStyle(sendBtn);
196
- const borderRadius = styles.borderRadius;
197
-
198
- if(borderRadius !== '50%'){
199
- cleanup(container);
200
- fail(`Expected border-radius 50%, got ${borderRadius}`);
201
- return;
202
- }
203
-
204
- cleanup(container);
205
- pass('Send button is circular');
206
- },
207
-
208
185
  /*
209
186
  Chat Component - Message Management Tests
210
187
  */
@@ -1,4 +1,5 @@
1
1
  import PhotoViewer from '../../src/components/PhotoViewer.js';
2
+ import { html } from '../../src/lit-all.min.js';
2
3
 
3
4
  const createPhotoViewer = async (options = {}) => {
4
5
  const container = document.createElement('div');
@@ -636,5 +637,180 @@ export default {
636
637
 
637
638
  cleanup(container);
638
639
  pass('Caption content slotted correctly');
640
+ },
641
+
642
+ /*
643
+ Static open() Tests
644
+ */
645
+ 'should create and open a viewer with PhotoViewer.open': async ({pass, fail}) => {
646
+ const viewer = PhotoViewer.open([
647
+ { src: 'https://picsum.photos/200/300', caption: '<span>Photo 1</span>' },
648
+ { src: 'https://picsum.photos/200/301', caption: '<span>Photo 2</span>' }
649
+ ]);
650
+ await viewer.updateComplete;
651
+
652
+ if(!(viewer instanceof PhotoViewer)){
653
+ viewer.close();
654
+ fail('open() should return a PhotoViewer instance');
655
+ return;
656
+ }
657
+
658
+ if(viewer.fullscreen !== true){
659
+ viewer.close();
660
+ fail('open() should open the viewer in fullscreen');
661
+ return;
662
+ }
663
+
664
+ viewer.close();
665
+ pass('PhotoViewer.open() creates and opens a viewer');
666
+ },
667
+
668
+ 'should open at the given openIndex': async ({pass, fail}) => {
669
+ const viewer = PhotoViewer.open([
670
+ { src: 'https://picsum.photos/200/300', caption: 'Photo 1' },
671
+ { src: 'https://picsum.photos/200/301', caption: 'Photo 2' },
672
+ { src: 'https://picsum.photos/200/302', caption: 'Photo 3' }
673
+ ], 1);
674
+ await viewer.updateComplete;
675
+
676
+ if(viewer.src !== 'https://picsum.photos/200/301'){
677
+ viewer.close();
678
+ fail(`Expected src for index 1, got "${viewer.src}"`);
679
+ return;
680
+ }
681
+
682
+ viewer.close();
683
+ pass('PhotoViewer.open() opens at the given openIndex');
684
+ },
685
+
686
+ 'should default to openIndex 0': async ({pass, fail}) => {
687
+ const viewer = PhotoViewer.open([
688
+ { src: 'https://picsum.photos/200/300' },
689
+ { src: 'https://picsum.photos/200/301' }
690
+ ]);
691
+ await viewer.updateComplete;
692
+
693
+ if(viewer.src !== 'https://picsum.photos/200/300'){
694
+ viewer.close();
695
+ fail(`Expected src for index 0, got "${viewer.src}"`);
696
+ return;
697
+ }
698
+
699
+ viewer.close();
700
+ pass('PhotoViewer.open() defaults to openIndex 0');
701
+ },
702
+
703
+ 'should remove the gallery from the DOM after closing': async ({pass, fail}) => {
704
+ const viewer = PhotoViewer.open([
705
+ { src: 'https://picsum.photos/200/300' }
706
+ ]);
707
+ await viewer.updateComplete;
708
+
709
+ viewer.close();
710
+ await viewer.updateComplete;
711
+
712
+ if(document.body.contains(viewer)){
713
+ fail('Gallery should be removed from the DOM after closing');
714
+ return;
715
+ }
716
+
717
+ pass('Gallery removed from the DOM after closing');
718
+ },
719
+
720
+ 'should not remove the gallery from the DOM while navigating': async ({pass, fail}) => {
721
+ const viewer = PhotoViewer.open([
722
+ { src: 'https://picsum.photos/200/300' },
723
+ { src: 'https://picsum.photos/200/301' }
724
+ ], 0);
725
+ await viewer.updateComplete;
726
+
727
+ const next = viewer.getNextSibling();
728
+ viewer.handleNextClick({ stopPropagation: () => {} });
729
+ await next.updateComplete;
730
+
731
+ if(!document.body.contains(next)){
732
+ next.close();
733
+ fail('Gallery should remain in the DOM while navigating between photos');
734
+ return;
735
+ }
736
+
737
+ if(next.fullscreen !== true){
738
+ next.close();
739
+ fail('Next photo should be opened after navigating');
740
+ return;
741
+ }
742
+
743
+ next.close();
744
+ pass('Gallery remains in the DOM while navigating');
745
+ },
746
+
747
+ 'should call onClose when the gallery is fully closed': async ({pass, fail}) => {
748
+ let closed = false;
749
+ const viewer = PhotoViewer.open([
750
+ { src: 'https://picsum.photos/200/300' }
751
+ ], 0, { onClose: () => { closed = true; } });
752
+ await viewer.updateComplete;
753
+
754
+ viewer.close();
755
+ await viewer.updateComplete;
756
+
757
+ if(!closed){
758
+ fail('onClose callback should be called after the gallery closes');
759
+ return;
760
+ }
761
+
762
+ pass('onClose callback called after gallery closes');
763
+ },
764
+
765
+ 'should render string captions in the opened viewer': async ({pass, fail}) => {
766
+ const viewer = PhotoViewer.open([
767
+ { src: 'https://picsum.photos/200/300', caption: '<span class="open-test-caption">Hello</span>' }
768
+ ]);
769
+ await viewer.updateComplete;
770
+
771
+ const caption = viewer.querySelector('.open-test-caption');
772
+ if(!caption || caption.textContent !== 'Hello'){
773
+ viewer.close();
774
+ fail('String caption should be rendered as HTML content');
775
+ return;
776
+ }
777
+
778
+ viewer.close();
779
+ pass('String caption rendered correctly');
780
+ },
781
+
782
+ 'should render lit html template captions in the opened viewer': async ({pass, fail}) => {
783
+ const viewer = PhotoViewer.open([
784
+ { src: 'https://picsum.photos/200/300', caption: html`<span class="open-test-lit-caption">Lit Hello</span>` }
785
+ ]);
786
+ await viewer.updateComplete;
787
+
788
+ const caption = viewer.querySelector('.open-test-lit-caption');
789
+ if(!caption || caption.textContent !== 'Lit Hello'){
790
+ viewer.close();
791
+ fail('Lit html caption should be rendered as content');
792
+ return;
793
+ }
794
+
795
+ viewer.close();
796
+ pass('Lit html template caption rendered correctly');
797
+ },
798
+
799
+ 'should keep multiple opened photos isolated as siblings': async ({pass, fail}) => {
800
+ const viewer = PhotoViewer.open([
801
+ { src: 'https://picsum.photos/200/300' },
802
+ { src: 'https://picsum.photos/200/301' }
803
+ ]);
804
+ await viewer.updateComplete;
805
+
806
+ const hasSiblings = viewer.hasPhotoSiblings();
807
+ if(!hasSiblings){
808
+ viewer.close();
809
+ fail('Opened gallery photos should detect each other as siblings');
810
+ return;
811
+ }
812
+
813
+ viewer.close();
814
+ pass('Opened gallery photos are isolated siblings');
639
815
  }
640
816
  };
@@ -114,19 +114,19 @@ export default {
114
114
  },
115
115
 
116
116
  /*
117
- invokeHost Tests
117
+ Host Method Invocation Tests
118
118
  */
119
- 'should invoke host method via invokeHost': async ({pass, fail}) => {
119
+ 'should invoke host method directly via host getter': async ({pass, fail}) => {
120
120
  const { container, table, control } = await createTableWithControl();
121
121
  const newRecords = [{ id: 99, name: 'Zara', age: 22 }];
122
- control.invokeHost('setRecords', newRecords);
122
+ control.host?.setRecords(newRecords);
123
123
  await table.updateComplete;
124
124
  if(table.records.length !== 1 || table.records[0].name !== 'Zara'){
125
125
  cleanup(container);
126
- return fail('invokeHost should call host method');
126
+ return fail('Host method should be callable via control.host');
127
127
  }
128
128
  cleanup(container);
129
- pass('invokeHost calls host method correctly');
129
+ pass('Host method invoked correctly via control.host');
130
130
  },
131
131
 
132
132
  /*