blob: 2e14463375970f1dbdb7e1eda2b30eadf89ad853 [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001/**
2 * @license
3 * Copyright 2016 Leif Olsen. All Rights Reserved.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 * This code is built with Google Material Design Lite,
18 * which is Licensed under the Apache License, Version 2.0
19 */
20
21
22/**
23 * Responsive Lightbox
24 */
25
26import fullThrottle from '../utils/full-throttle';
27import {
28 VK_ESC,
29 VK_SPACE,
30 VK_END,
31 VK_HOME,
32 VK_ARROW_LEFT,
33 VK_ARROW_UP,
34 VK_ARROW_RIGHT,
35 VK_ARROW_DOWN,
36 IS_UPGRADED
37} from '../utils/constants';
38
39(function() {
40 'use strict';
41
42 const LIGHTBOX = 'mdlext-lightbox';
43 const LIGHTBOX_SLIDER = 'mdlext-lightbox__slider';
44 const LIGHTBOX_SLIDER_SLIDE = 'mdlext-lightbox__slider__slide';
45 const STICKY_FOOTER = 'mdlext-lightbox--sticky-footer';
46 const BUTTON = 'mdl-button';
47
48 /**
49 * https://github.com/google/material-design-lite/issues/4205
50 * @constructor
51 * @param {Element} element The element that will be upgraded.
52 */
53 const MaterialExtLightbox = function MaterialExtLightbox(element) {
54 // Stores the element.
55 this.element_ = element;
56
57 // Initialize instance.
58 this.init();
59 };
60 window['MaterialExtLightbox'] = MaterialExtLightbox;
61
62
63 /**
64 * Handle keypress
65 * @param event
66 * @private
67 */
68 MaterialExtLightbox.prototype.keyDownHandler_ = function(event) {
69
70 if (event) {
71 if ( event.keyCode === VK_ESC || event.keyCode === VK_SPACE
72 || event.keyCode === VK_END || event.keyCode === VK_HOME
73 || event.keyCode === VK_ARROW_UP || event.keyCode === VK_ARROW_LEFT
74 || event.keyCode === VK_ARROW_DOWN || event.keyCode === VK_ARROW_RIGHT) {
75
76 if(event.keyCode !== VK_ESC) {
77 event.preventDefault();
78 event.stopPropagation();
79 }
80
81 let action = 'first';
82 if (event.keyCode === VK_END) {
83 action = 'last';
84 }
85 else if (event.keyCode === VK_ARROW_UP || event.keyCode === VK_ARROW_LEFT) {
86 action = 'prev';
87 }
88 else if (event.keyCode === VK_ARROW_DOWN || event.keyCode === VK_ARROW_RIGHT) {
89 action = 'next';
90 }
91 else if (event.keyCode === VK_SPACE) {
92 action = 'select';
93 }
94 else if (event.keyCode === VK_ESC) {
95 action = 'cancel';
96 }
97
98 dispatchAction_(action, this);
99 }
100 }
101 };
102
103 /**
104 * Handle button clicks
105 * @param event
106 * @private
107 */
108 MaterialExtLightbox.prototype.buttonClickHandler_ = function(event) {
109
110 if (event) {
111 event.preventDefault();
112 event.stopPropagation();
113
114 dispatchAction_(this.getAttribute('data-action') || '', this);
115
116 const n = this.closest(`.${LIGHTBOX}`);
117 if(n) {
118 n.focus();
119 }
120 }
121 };
122
123 /**
124 * Dispatches an action custom event
125 * @param action
126 * @param source
127 * @param target
128 * @private
129 */
130 const dispatchAction_ = (action, source, target = source) => {
131
132 target.dispatchEvent(new CustomEvent('action', {
133 bubbles: true,
134 cancelable: true,
135 detail: {
136 action: action || '',
137 source: source
138 }
139 }));
140 };
141
142 /**
143 * Reposition dialog if component parent element is "DIALOG"
144 * @param lightboxElement
145 * @private
146 */
147 const repositionDialog_ = lightboxElement => {
148 const footerHeight = (footer, isSticky) => isSticky && footer ? footer.offsetHeight : 0;
149
150 const reposition = (dialog, fh) => {
151 if (window.getComputedStyle(dialog).position === 'absolute') {
152 const scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
153 const topValue = scrollTop + (window.innerHeight - dialog.offsetHeight - fh) / 2;
154 dialog.style.top = `${Math.max(scrollTop, topValue)}px`;
155 }
156 };
157
158 const p = lightboxElement.parentNode;
159 const dialog = p && p.nodeName === 'DIALOG' ? p : null;
160
161 if(dialog && dialog.hasAttribute('open')) {
162 lightboxElement.style.width = 'auto';
163 lightboxElement.style.maxWidth = '100%';
164 const img = lightboxElement.querySelector('img');
165 if(img) {
166 lightboxElement.style.maxWidth = img.naturalWidth !== undefined ? `${img.naturalWidth}px` : `${img.width}px` || '100%';
167 }
168
169 const fh = footerHeight(lightboxElement.querySelector('footer'), lightboxElement.classList.contains(STICKY_FOOTER));
170 const vh = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - fh;
171 if (dialog.offsetHeight > vh) {
172 let n = 0;
173 while(dialog.offsetHeight > vh && ++n < 4) {
174 lightboxElement.style.width = `${lightboxElement.offsetWidth * vh / lightboxElement.offsetHeight}px`;
175 }
176 }
177 reposition(dialog, fh);
178 }
179 };
180
181 /**
182 * Handle image load
183 * @param event
184 * @private
185 */
186
187 MaterialExtLightbox.prototype.imgLoadHandler_ = function( /*event*/ ) {
188 repositionDialog_(this);
189 };
190
191
192 /**
193 * Handle image drag
194 * @param event
195 * @private
196 */
197 MaterialExtLightbox.prototype.imgDragHandler_ = function(event ) {
198
199 const setStyles = ( element, properties ) => {
200 //noinspection JSAnnotator
201 for(const [key, value] of Object.entries(properties)) {
202 element.style[key] = value;
203 }
204 // ... or:
205 //for (const key in properties) {
206 // element.style[key] = properties[key];
207 //}
208 };
209
210 event.preventDefault();
211 //event.stopPropagation();
212
213 const x = event.clientX || (event.touches !== undefined ? event.touches[0].clientX : 0);
214
215 const img = this;
216 img.style.opacity = '0.2';
217
218 const slider = document.createElement('div');
219 slider.classList.add(LIGHTBOX_SLIDER);
220 setStyles(slider, {'width': `${img.offsetWidth}px`, 'height': `${img.offsetHeight}px`} );
221
222 let slide = document.createElement('div');
223 slide.classList.add(LIGHTBOX_SLIDER_SLIDE);
224 slide.textContent = '>';
225 setStyles(slide, {
226 'width' : `${img.offsetWidth}px`,
227 'height' : `${img.offsetHeight}px`,
228 'line-height' : `${img.offsetHeight}px`,
229 'font-size' : `${img.offsetHeight/4}px`,
230 'text-align' : 'right',
231 'background-image': `url("${img.getAttribute('data-img-url-prev') || ''}")`
232 });
233 slider.appendChild(slide);
234
235 slide = document.createElement('div');
236 slide.classList.add(LIGHTBOX_SLIDER_SLIDE);
237 setStyles(slide, {
238 'width' : `${img.offsetWidth}px`,
239 'height' : `${img.offsetHeight}px`,
240 'background-image': `url("${img.src}")`
241 });
242 slider.appendChild(slide);
243
244 slide = document.createElement('div');
245 slide.classList.add(LIGHTBOX_SLIDER_SLIDE);
246 slide.textContent = '<';
247 setStyles(slide, {
248 'width' : `${img.offsetWidth}px`,
249 'height' : `${img.offsetHeight}px`,
250 'line-height' : `${img.offsetHeight}px`,
251 'font-size' : `${img.offsetHeight/4}px`,
252 'text-align' : 'left',
253 'background-image': `url("${img.getAttribute('data-img-url-next') || ''}")`
254 });
255 slider.appendChild(slide);
256
257 img.parentNode.appendChild(slider);
258
259
260 // drag handler
261 const drag = e => {
262 e.preventDefault();
263 const dx = (e.clientX || (e.touches !== undefined ? e.touches[0].clientX : 0)) - x; // TODO: maybe rewrite to improve performance
264
265 if(slider.offsetWidth - Math.abs(dx) > 19) {
266 slider.style.left = `${dx}px`;
267 }
268 };
269
270 // end drag handler
271 const endDrag = e => {
272 e.preventDefault();
273 //e.stopPropagation();
274
275 window.removeEventListener('mousemove', drag);
276 window.removeEventListener('touchmove', drag);
277 window.removeEventListener('mouseup', endDrag);
278 window.removeEventListener('touchend', endDrag);
279
280 const dx = slider.offsetLeft;
281 img.parentNode.removeChild(slider);
282 img.style.opacity = '1.0';
283
284 if(Math.abs(dx) > 19) {
285 dispatchAction_( (dx > 0 ? 'prev' : 'next') , img);
286 }
287 };
288
289 window.addEventListener('mousemove', drag);
290 window.addEventListener('touchmove', drag);
291 window.addEventListener('mouseup', endDrag);
292 window.addEventListener('touchend',endDrag);
293 };
294
295
296 /**
297 * Initialize component
298 */
299 MaterialExtLightbox.prototype.init = function() {
300
301 if (this.element_) {
302 // Do the init required for this component to work
303 this.element_.addEventListener('keydown', this.keyDownHandler_.bind(this.element_), true);
304
305 if(!Number.isInteger(this.element_.getAttribute('tabindex'))) {
306 this.element_.setAttribute('tabindex', 1);
307 }
308
309 [...this.element_.querySelectorAll(`.${BUTTON}`)].forEach( button =>
310 button.addEventListener('click', this.buttonClickHandler_.bind(button), false)
311 );
312
313 const figcaption = this.element_.querySelector('figcaption');
314 if(figcaption) {
315 figcaption.addEventListener('click', this.buttonClickHandler_.bind(figcaption), false);
316 }
317
318 const footer = this.element_.querySelector('footer');
319 if(footer) {
320 footer.addEventListener('click', this.buttonClickHandler_.bind(footer), false);
321 }
322
323 const img = this.element_.querySelector('img');
324 if(img) {
325 img.addEventListener('load', this.imgLoadHandler_.bind(this.element_), false);
326 img.addEventListener('click', e => e.preventDefault(), true);
327 img.addEventListener('mousedown', this.imgDragHandler_.bind(img), true);
328 img.addEventListener('touchstart', this.imgDragHandler_.bind(img), true);
329 }
330 window.addEventListener('resize', fullThrottle( () => repositionDialog_(this.element_) ));
331 window.addEventListener('orientationchange', () => repositionDialog_(this.element_));
332
333 // Set upgraded flag
334 this.element_.classList.add(IS_UPGRADED);
335 }
336 };
337
338 /*
339 * Downgrade component
340 * E.g remove listeners and clean up resources
341 *
342 * Nothing to downgrade
343 *
344 MaterialExtLightbox.prototype.mdlDowngrade_ = function() {
345 };
346 */
347
348 /**
349 * The component registers itself. It can assume componentHandler is available in the global scope.
350 */
351 /* jshint undef:false */
352 componentHandler.register({
353 constructor: MaterialExtLightbox,
354 classAsString: 'MaterialExtLightbox',
355 cssClass: 'mdlext-js-lightbox'
356 });
357
358})();
359