1/** 2 * MMX / IMAGE ACROSS 3 */ 4class MMX_ImageAcross extends MMX_Element { 5 #auto_image_height; 6 #scroll_position; 7 #resize_observer; 8 #intersection_observer; 9 10 static get props() { 11 return { 12 align: { 13 options: [ 14 'left', 15 'center', 16 'right' 17 ], 18 default: 'center' 19 }, 20 size: { 21 options: [ 22 'auto', 23 's', 24 'm', 25 'l' 26 ], 27 allowAny: true, 28 default: 'auto' 29 }, 30 images: { 31 allowAny: true, 32 default: null 33 }, 34 columns: { 35 options: [ 36 'auto', 37 '1', 38 '2', 39 '3', 40 '4', 41 '5', 42 '6', 43 '7', 44 '8' 45 ], 46 allowAny: true, 47 default: 'auto', 48 max: 8 49 }, 50 overflow: { 51 options: [ 52 'scroll', 53 'wrap' 54 ], 55 default: 'scroll' 56 } 57 }; 58 } 59 60 styleResourceCodes = ['mmx-base', 'mmx-button', 'mmx-hero', 'mmx-image-across']; 61 62 constructor() { 63 super(); 64 this.makeShadow(); 65 this.#scroll_position = 0; 66 this.#resize_observer = new ResizeObserver(entries => this.setScrollPosition()); 67 this.#intersection_observer = new IntersectionObserver(entries => {this.calculateScrollPosition();}, { rootMargin: '100% 0%', threshold: 0 }); 68 } 69 70 render() { 71 return /*html*/` 72 <div part="wrapper" class="mmx-image-across"> 73 <div part="title" class="mmx-image-across__title"> 74 <slot name="title"></slot> 75 </div> 76 <div part="images" class="mmx-image-across__images mmx-image-across__images-columns--${this.maxColumns()} mmx-image-across__images-overflow--${this.getPropValue('overflow')}"> 77 ${this.renderImages()} 78 <slot name="image"></slot> 79 </div> 80 </div> 81 `; 82 } 83 84 afterRender() { 85 this.setImageHeight(); 86 this.#resize_observer.observe(this.shadowImagesContainer());
87 this.slottedImages().forEach(image => {this.#intersection_observer.observe(image);}); 88 } 89 90 styles() { 91 return /*css*/` 92 :host { 93 --mmx-image-across__text-align: ${this.getPropValue('align')}; 94 --mmx-image-across__columns: ${this.maxColumns()}; 95 --mmx-image-across__image-count: ${this.getImageCount()}; 96 } 97 `; 98 } 99 100 images() { 101 return this.loadPropertyData('images') || []; 102 } 103 104 slottedImages() { 105 return this.querySelectorAll(':scope > [slot="image"]'); 106 } 107 108 shadowImages() { 109 return this.shadowRoot.querySelectorAll('[part="image"]'); 110 } 111 112 shadowImagesContainer() { 113 return this.shadowRoot.querySelector('[part="images"]'); 114 } 115 116 imageElements() { 117 return [...this.shadowImages(), ...this.slottedImages()]; 118 } 119 120 renderImages() { 121 return this.images().map((image => { 122 return this.createElement({ 123 type: 'mmx-hero', 124 attributes: { 125 part: 'image', 126 ...image 127 } 128 }).outerHTML; 129 })).join(''); 130 } 131 132 getImageCount() { 133 const slottedImageCount = this.slottedImages().length; 134 return this.images().length + slottedImageCount; 135 } 136 137 maxColumns() { 138 const columns = this.getPropValue('columns'); 139 140 if (columns !== 'auto') { 141 return isNaN(columns) ? columns : Number(columns); 142 } 143 144 const imageCount = this.getImageCount(); 145 if (imageCount < this.constructor.props.columns.max) { 146 return imageCount; 147 } 148 149 return this.constructor.props.columns.max; 150 } 151 152 getSize() { 153 const size = this.getPropValue('size'); 154 if (size === 'auto') { 155 return this.#auto_image_height + 'px'; 156 } else { 157 return size; 158 } 159 } 160 161 setImageHeight() { 162 if (this.getPropValue('size') !== 'auto') { 163 return this.updateImageHeight(this.getPropValue('size')); 164 } 165 166 this.#auto_image_height = 0; 167 168 this.imageElements().forEach(image => { 169 const img = image?.slottedImage?.() ?? image?.shadowImage?.(); 170 171 if (img?.complete) { 172 return this.checkLoadedImage(img); 173 } 174 175 image.addEventListener('img:load', this.#event_image_load); 176 }); 177 } 178 179 #event_image_load = (e) => { 180 this.checkLoadedImage(e.detail.element); 181 }; 182 183 checkLoadedImage(image) { 184 const last_auto_image_height = this.#auto_image_height; 185 186 this.calculateMinImageHeight(this.getResponsiveImageHeight(image)); 187 188 if (last_auto_image_height !== this.#auto_image_height) { 189 this.updateImageHeight(this.#auto_image_height + 'px'); 190 } 191 192 this.#updateRenderStatus(); 193 } 194 195 #updateRenderStatus() { 196 if (this.imageLoadedCount === this.getImageCount()) { 197 this.classList.add('mmx-image-across--images-loaded'); 198 } else { 199 this.classList.remove('mmx-image-across--images-loaded'); 200 } 201 } 202 203 get imageLoadedCount() { 204 const loadedImages = this.imageElements().filter(image => { 205 const img = image?.slottedImage?.() ?? image?.shadowImage?.(); 206 return img?.complete; 207 }); 208 209 return loadedImages.length; 210 } 211 212 updateImageHeight(size) { 213 this.imageElements().forEach(image => { 214 image.setAttribute('data-size', size); 215 }); 216 } 217 218 calculateMinImageHeight(height) 219 { 220 if (this.#auto_image_height === 0) { 221 this.#auto_image_height = height; 222 } 223 else if (height < this.#auto_image_height) { 224 this.#auto_image_height = height; 225 } 226 } 227 228 calculateScrollPosition() 229 { 230 const image_container = this.shadowImagesContainer().scrollWidth; 231 const image_position = this.shadowImagesContainer().scrollLeft; 232 const position_percent = image_position / image_container; 233 234 this.#scroll_position = position_percent; 235 } 236 237 setScrollPosition() 238 { 239 const image_container = this.shadowImagesContainer().scrollWidth; 240 const new_position = this.#scroll_position * image_container; 241 242 this.shadowImagesContainer().scrollLeft = new_position; 243 } 244} 245 246if (!customElements.get('mmx-image-across')){ 247 customElements.define('mmx-image-across', MMX_ImageAcross); 248}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.