kempo-ui 0.4.21 → 0.4.23
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.
- package/dist/components/PhotoViewer.js +2 -2
- package/docs/components/photo-viewer.html +42 -1
- package/docs/src/components/PhotoViewer.js +2 -2
- package/docs-src/components/photo-viewer.page.html +42 -1
- package/package.json +1 -1
- package/src/components/PhotoViewer.js +49 -1
- package/tests/components/Chat.browser-test.js +0 -23
- package/tests/components/PhotoViewer.browser-test.js +195 -0
- package/tests/components/controls/Control.browser-test.js +5 -5
|
@@ -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
|
|
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",s.style.zIndex="80";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",
|
|
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"><<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"galleryBtn"</span>></span>Open Gallery<span class="hljs-tag"></<span class="hljs-name">button</span>></span><br /><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span>></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>, () => {<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">'<h6>Honda Civic Type R</h6>'</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"><<span class="hljs-name">h6</span>></span>Chevrolet Corvette C8<span class="hljs-tag"></<span class="hljs-name">h6</span>></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">'<h6>Mitsubishi Lancer Evolution</h6>'</span> }<br /> ], <span class="hljs-number">1</span>);<br /> });<br /></span><span class="hljs-tag"></<span class="hljs-name">script</span>></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<{ src, caption }></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
|
|
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",s.style.zIndex="80";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",
|
|
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"><<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"galleryBtn"</span>></span>Open Gallery<span class="hljs-tag"></<span class="hljs-name">button</span>></span><br /><span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span>></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>, () => {<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">'<h6>Honda Civic Type R</h6>'</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"><<span class="hljs-name">h6</span>></span>Chevrolet Corvette C8<span class="hljs-tag"></<span class="hljs-name">h6</span>></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">'<h6>Mitsubishi Lancer Evolution</h6>'</span> }<br /> ], <span class="hljs-number">1</span>);<br /> });<br /></span><span class="hljs-tag"></<span class="hljs-name">script</span>></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<{ src, caption }></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,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,54 @@ 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
|
+
container.style.zIndex = '80';
|
|
155
|
+
|
|
156
|
+
const viewers = content.map(item => {
|
|
157
|
+
const viewer = document.createElement('k-photo-viewer');
|
|
158
|
+
viewer.src = item.src;
|
|
159
|
+
viewer.alt = item.alt || '';
|
|
160
|
+
viewer.keyboardControls = keyboardControls;
|
|
161
|
+
if(item.caption !== undefined) {
|
|
162
|
+
if(typeof item.caption === 'string') {
|
|
163
|
+
viewer.innerHTML = item.caption;
|
|
164
|
+
} else {
|
|
165
|
+
render(item.caption, viewer);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
container.appendChild(viewer);
|
|
169
|
+
return viewer;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
document.body.appendChild(container);
|
|
173
|
+
|
|
174
|
+
const handleClose = () => {
|
|
175
|
+
if(!viewers.some(viewer => viewer.fullscreen)) {
|
|
176
|
+
viewers.forEach(viewer => viewer.removeEventListener('fullscreenclose', handleClose));
|
|
177
|
+
container.remove();
|
|
178
|
+
onClose?.();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
viewers.forEach(viewer => viewer.addEventListener('fullscreenclose', handleClose));
|
|
182
|
+
|
|
183
|
+
const startIndex = Math.min(Math.max(openIndex, 0), viewers.length - 1);
|
|
184
|
+
viewers[startIndex].open();
|
|
185
|
+
|
|
186
|
+
return viewers[startIndex];
|
|
187
|
+
}
|
|
188
|
+
|
|
141
189
|
/*
|
|
142
190
|
Private Methods
|
|
143
191
|
*/
|
|
@@ -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,199 @@ 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');
|
|
815
|
+
},
|
|
816
|
+
|
|
817
|
+
'should have z-index to prevent stacking context issues': async ({pass, fail}) => {
|
|
818
|
+
const viewer = PhotoViewer.open([
|
|
819
|
+
{ src: 'https://picsum.photos/200/300' }
|
|
820
|
+
]);
|
|
821
|
+
await viewer.updateComplete;
|
|
822
|
+
|
|
823
|
+
const container = viewer.parentElement;
|
|
824
|
+
const zIndex = window.getComputedStyle(container).zIndex;
|
|
825
|
+
|
|
826
|
+
if(zIndex !== '80'){
|
|
827
|
+
viewer.close();
|
|
828
|
+
fail(`Expected container z-index 80, got ${zIndex}`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
viewer.close();
|
|
833
|
+
pass('Container has correct z-index elevation');
|
|
639
834
|
}
|
|
640
835
|
};
|
|
@@ -114,19 +114,19 @@ export default {
|
|
|
114
114
|
},
|
|
115
115
|
|
|
116
116
|
/*
|
|
117
|
-
|
|
117
|
+
Host Method Invocation Tests
|
|
118
118
|
*/
|
|
119
|
-
'should invoke host method via
|
|
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.
|
|
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('
|
|
126
|
+
return fail('Host method should be callable via control.host');
|
|
127
127
|
}
|
|
128
128
|
cleanup(container);
|
|
129
|
-
pass('
|
|
129
|
+
pass('Host method invoked correctly via control.host');
|
|
130
130
|
},
|
|
131
131
|
|
|
132
132
|
/*
|