UNPKG

@fancyapps/ui

Version:

JavaScript UI Component Library

772 lines (618 loc) 19.4 kB
import { extend } from "../../../shared/utils/extend.js"; const buildURLQuery = (src, obj) => { const url = new URL(src); const params = new URLSearchParams(url.search); let rez = new URLSearchParams(); for (const [key, value] of [...params, ...Object.entries(obj)]) { // Youtube if (key === "t") { rez.set("start", parseInt(value)); } else { rez.set(key, value); } } // Convert to 'foo=1&bar=2&baz=3' rez = rez.toString(); // Vimeo // https://vimeo.zendesk.com/hc/en-us/articles/360000121668-Starting-playback-at-a-specific-timecode let matches = src.match(/#t=((.*)?\d+s)/); if (matches) { rez += `#t=${matches[1]}`; } return rez; }; const defaults = { // General options for any video content (Youtube, Vimeo, HTML5 video) video: { autoplay: true, ratio: 16 / 9, }, // Youtube embed parameters youtube: { autohide: 1, fs: 1, rel: 0, hd: 1, wmode: "transparent", enablejsapi: 1, html5: 1, }, // Vimeo embed parameters vimeo: { hd: 1, show_title: 1, show_byline: 1, show_portrait: 0, fullscreen: 1, }, // HTML5 video parameters html5video: { tpl: `<video class="fancybox__html5video" playsinline controls controlsList="nodownload" poster="{{poster}}"> <source src="{{src}}" type="{{format}}" />Sorry, your browser doesn't support embedded videos.</video>`, format: "", }, }; export class Html { constructor(fancybox) { this.fancybox = fancybox; for (const methodName of [ "onInit", "onReady", "onCreateSlide", "onRemoveSlide", "onSelectSlide", "onUnselectSlide", "onRefresh", // For communication with iframed video (youtube/vimeo) "onMessage", ]) { this[methodName] = this[methodName].bind(this); } this.events = { init: this.onInit, ready: this.onReady, "Carousel.createSlide": this.onCreateSlide, "Carousel.removeSlide": this.onRemoveSlide, "Carousel.selectSlide": this.onSelectSlide, "Carousel.unselectSlide": this.onUnselectSlide, "Carousel.refresh": this.onRefresh, }; } /** * Check if each gallery item has type when fancybox starts */ onInit() { for (const slide of this.fancybox.items) { this.processType(slide); } } /** * Set content type for the slide * @param {Object} slide */ processType(slide) { // Add support for `new Fancybox({items : [{html : 'smth'}]});` if (slide.html) { slide.src = slide.html; slide.type = "html"; delete slide.html; return; } const src = slide.src || ""; let type = slide.type || this.fancybox.options.type, rez = null; if (src && typeof src !== "string") { return; } if ( (rez = src.match( /(?:youtube\.com|youtu\.be|youtube\-nocookie\.com)\/(?:watch\?(?:.*&)?v=|v\/|u\/|embed\/?)?(videoseries\?list=(?:.*)|[\w-]{11}|\?listType=(?:.*)&list=(?:.*))(?:.*)/i )) ) { const params = buildURLQuery(src, this.fancybox.option("Html.youtube")); const videoId = encodeURIComponent(rez[1]); slide.videoId = videoId; slide.src = `https://www.youtube-nocookie.com/embed/${videoId}?${params}`; slide.thumb = slide.thumb || `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`; slide.vendor = "youtube"; type = "video"; } else if ((rez = src.match(/^.+vimeo.com\/(?:\/)?([\d]+)(.*)?/))) { const params = buildURLQuery(src, this.fancybox.option("Html.vimeo")); const videoId = encodeURIComponent(rez[1]); slide.videoId = videoId; slide.src = `https://player.vimeo.com/video/${videoId}?${params}`; slide.vendor = "vimeo"; type = "video"; } else if ( (rez = src.match( /(?:maps\.)?google\.([a-z]{2,3}(?:\.[a-z]{2})?)\/(?:(?:(?:maps\/(?:place\/(?:.*)\/)?\@(.*),(\d+.?\d+?)z))|(?:\?ll=))(.*)?/i )) ) { slide.src = `//maps.google.${rez[1]}/?ll=${(rez[2] ? rez[2] + "&z=" + Math.floor(rez[3]) + (rez[4] ? rez[4].replace(/^\//, "&") : "") : rez[4] + "" ).replace(/\?/, "&")}&output=${rez[4] && rez[4].indexOf("layer=c") > 0 ? "svembed" : "embed"}`; type = "map"; } else if ((rez = src.match(/(?:maps\.)?google\.([a-z]{2,3}(?:\.[a-z]{2})?)\/(?:maps\/search\/)(.*)/i))) { slide.src = `//maps.google.${rez[1]}/maps?q=${rez[2].replace("query=", "q=").replace("api=1", "")}&output=embed`; type = "map"; } // Guess content type if (!type) { if (src.charAt(0) === "#") { type = "inline"; } else if ((rez = src.match(/\.(mp4|mov|ogv|webm)((\?|#).*)?$/i))) { type = "html5video"; slide.format = slide.format || "video/" + (rez[1] === "ogv" ? "ogg" : rez[1]); } else if (src.match(/(^data:image\/[a-z0-9+\/=]*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg|ico)((\?|#).*)?$)/i)) { type = "image"; } else if (src.match(/\.(pdf)((\?|#).*)?$/i)) { type = "pdf"; } } slide.type = type || this.fancybox.option("defaultType", "image"); if (type === "html5video" || type === "video") { slide.video = extend({}, this.fancybox.option("Html.video"), slide.video); if (slide._width && slide._height) { slide.ratio = parseFloat(slide._width) / parseFloat(slide._height); } else { slide.ratio = slide.ratio || slide.video.ratio || defaults.video.ratio; } } } /** * Start loading content when Fancybox is ready */ onReady() { this.fancybox.Carousel.slides.forEach((slide) => { if (slide.$el) { this.setContent(slide); if (slide.index === this.fancybox.getSlide().index) { this.playVideo(slide); } } }); } /** * Process `Carousel.createSlide` event to create image content * @param {Object} fancybox * @param {Object} carousel * @param {Object} slide */ onCreateSlide(fancybox, carousel, slide) { if (this.fancybox.state !== "ready") { return; } this.setContent(slide); } /** * Retrieve and set slide content * @param {Object} slide */ loadInlineContent(slide) { let $content; if (slide.src instanceof HTMLElement) { $content = slide.src; } else if (typeof slide.src === "string") { const tmp = slide.src.split("#", 2); const id = tmp.length === 2 && tmp[0] === "" ? tmp[1] : tmp[0]; $content = document.getElementById(id); } if ($content) { if (slide.type === "clone" || $content.$placeHolder) { $content = $content.cloneNode(true); let attrId = $content.getAttribute("id"); attrId = attrId ? `${attrId}--clone` : `clone-${this.fancybox.id}-${slide.index}`; $content.setAttribute("id", attrId); } else { const $placeHolder = document.createElement("div"); $placeHolder.classList.add("fancybox-placeholder"); $content.parentNode.insertBefore($placeHolder, $content); $content.$placeHolder = $placeHolder; } this.fancybox.setContent(slide, $content); } else { this.fancybox.setError(slide, "{{ELEMENT_NOT_FOUND}}"); } } /** * Makes AJAX request and sets response as slide content * @param {Object} slide */ loadAjaxContent(slide) { const fancybox = this.fancybox; const xhr = new XMLHttpRequest(); fancybox.showLoading(slide); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { if (fancybox.state === "ready") { fancybox.hideLoading(slide); if (xhr.status === 200) { fancybox.setContent(slide, xhr.responseText); } else { fancybox.setError(slide, xhr.status === 404 ? "{{AJAX_NOT_FOUND}}" : "{{AJAX_FORBIDDEN}}"); } } } }; const data = slide.ajax || null; xhr.open(data ? "POST" : "GET", slide.src); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.send(data); slide.xhr = xhr; } /** * Creates iframe as slide content, preloads if needed before displaying * @param {Object} slide */ loadIframeContent(slide) { const fancybox = this.fancybox; const $iframe = document.createElement("iframe"); $iframe.className = "fancybox__iframe"; $iframe.setAttribute("id", `fancybox__iframe_${fancybox.id}_${slide.index}`); $iframe.setAttribute("allow", "autoplay; fullscreen"); $iframe.setAttribute("scrolling", "auto"); slide.$iframe = $iframe; if (slide.type !== "iframe" || slide.preload === false) { $iframe.setAttribute("src", slide.src); this.fancybox.setContent(slide, $iframe); this.resizeIframe(slide); return; } fancybox.showLoading(slide); const $content = document.createElement("div"); $content.style.visibility = "hidden"; this.fancybox.setContent(slide, $content); $content.appendChild($iframe); $iframe.onerror = () => { fancybox.setError(slide, "{{IFRAME_ERROR}}"); }; $iframe.onload = () => { fancybox.hideLoading(slide); let isFirstLoad = false; if (!$iframe.isReady) { $iframe.isReady = true; isFirstLoad = true; } if (!$iframe.src.length) { return; } $iframe.parentNode.style.visibility = ""; this.resizeIframe(slide); if (isFirstLoad) { fancybox.revealContent(slide); } }; $iframe.setAttribute("src", slide.src); } /** * Set CSS max/min width/height properties of the content to have the correct aspect ratio * @param {Object} slide */ setAspectRatio(slide) { const $content = slide.$content; const ratio = slide.ratio; if (!$content) { return; } let width = slide._width; let height = slide._height; if (ratio || (width && height)) { Object.assign($content.style, { width: width && height ? "100%" : "", height: width && height ? "100%" : "", maxWidth: "", maxHeight: "", }); let maxWidth = $content.offsetWidth; let maxHeight = $content.offsetHeight; width = width || maxWidth; height = height || maxHeight; // Resize to fit if (width > maxWidth || height > maxHeight) { let maxRatio = Math.min(maxWidth / width, maxHeight / height); width = width * maxRatio; height = height * maxRatio; } // Recheck ratio if (Math.abs(width / height - ratio) > 0.01) { if (ratio < width / height) { width = height * ratio; } else { height = width / ratio; } } Object.assign($content.style, { width: `${width}px`, height: `${height}px`, }); } } /** * Adjust the width and height of the iframe according to the content dimensions, or defined sizes * @param {Object} slide */ resizeIframe(slide) { const $iframe = slide.$iframe; if (!$iframe) { return; } let width_ = slide._width || 0; let height_ = slide._height || 0; if (width_ && height_) { slide.autoSize = false; } const $parent = $iframe.parentNode; const parentStyle = $parent && $parent.style; if (slide.preload !== false && slide.autoSize !== false && parentStyle) { try { const compStyles = window.getComputedStyle($parent), paddingX = parseFloat(compStyles.paddingLeft) + parseFloat(compStyles.paddingRight), paddingY = parseFloat(compStyles.paddingTop) + parseFloat(compStyles.paddingBottom); const document = $iframe.contentWindow.document, $html = document.getElementsByTagName("html")[0], $body = document.body; // Allow content to expand horizontally parentStyle.width = ""; // Get rid of vertical scrollbar $body.style.overflow = "hidden"; width_ = width_ || $html.scrollWidth + paddingX; parentStyle.width = `${width_}px`; $body.style.overflow = ""; parentStyle.flex = "0 0 auto"; parentStyle.height = `${$body.scrollHeight}px`; height_ = $html.scrollHeight + paddingY; } catch (error) { // } } if (width_ || height_) { const newStyle = { flex: "0 1 auto", }; if (width_) { newStyle.width = `${width_}px`; } if (height_) { newStyle.height = `${height_}px`; } Object.assign(parentStyle, newStyle); } } /** * Process `Carousel.onRefresh` event, * trigger iframe autosizing and set content aspect ratio for each slide * @param {Object} fancybox * @param {Object} carousel */ onRefresh(fancybox, carousel) { carousel.slides.forEach((slide) => { if (!slide.$el) { return; } if (slide.$iframe) { this.resizeIframe(slide); } if (slide.ratio) { this.setAspectRatio(slide); } }); } /** * Process `Carousel.onCreateSlide` event to set content * @param {Object} fancybox * @param {Object} carousel * @param {Object} slide */ setContent(slide) { if (!slide || slide.isDom) { return; } switch (slide.type) { case "html": this.fancybox.setContent(slide, slide.src); break; case "html5video": this.fancybox.setContent( slide, this.fancybox .option("Html.html5video.tpl") .replace(/\{\{src\}\}/gi, slide.src) .replace("{{format}}", slide.format || (slide.html5video && slide.html5video.format) || "") .replace("{{poster}}", slide.poster || slide.thumb || "") ); break; case "inline": case "clone": this.loadInlineContent(slide); break; case "ajax": this.loadAjaxContent(slide); break; case "pdf": case "video": case "map": slide.preload = false; case "iframe": this.loadIframeContent(slide); break; } if (slide.ratio) { this.setAspectRatio(slide); } } /** * Process `Carousel.onSelectSlide` event to start video * @param {Object} fancybox * @param {Object} carousel * @param {Object} slide */ onSelectSlide(fancybox, carousel, slide) { if (fancybox.state === "ready") { this.playVideo(slide); } } /** * Attempts to begin playback of the media * @param {Object} slide */ playVideo(slide) { if (slide.type === "html5video" && slide.video.autoplay) { try { const $video = slide.$el.querySelector("video"); if ($video) { const promise = $video.play(); if (promise !== undefined) { promise .then(() => { // Autoplay started }) .catch((error) => { // Autoplay was prevented. $video.muted = true; $video.play(); }); } } } catch (err) {} } if (slide.type !== "video" || !(slide.$iframe && slide.$iframe.contentWindow)) { return; } // This function will be repeatedly called to check // if video iframe has been loaded to send message to start the video const poller = () => { if (slide.state === "done" && slide.$iframe && slide.$iframe.contentWindow) { let command; if (slide.$iframe.isReady) { if (slide.video && slide.video.autoplay) { if (slide.vendor == "youtube") { command = { event: "command", func: "playVideo", }; } else { command = { method: "play", value: "true", }; } } if (command) { slide.$iframe.contentWindow.postMessage(JSON.stringify(command), "*"); } return; } if (slide.vendor === "youtube") { command = { event: "listening", id: slide.$iframe.getAttribute("id"), }; slide.$iframe.contentWindow.postMessage(JSON.stringify(command), "*"); } } slide.poller = setTimeout(poller, 250); }; poller(); } /** * Process `Carousel.onUnselectSlide` event to pause video * @param {Object} fancybox * @param {Object} carousel * @param {Object} slide */ onUnselectSlide(fancybox, carousel, slide) { if (slide.type === "html5video") { try { slide.$el.querySelector("video").pause(); } catch (error) {} return; } let command = false; if (slide.vendor == "vimeo") { command = { method: "pause", value: "true", }; } else if (slide.vendor === "youtube") { command = { event: "command", func: "pauseVideo", }; } if (command && slide.$iframe && slide.$iframe.contentWindow) { slide.$iframe.contentWindow.postMessage(JSON.stringify(command), "*"); } clearTimeout(slide.poller); } /** * Process `Carousel.onRemoveSlide` event to do clean up * @param {Object} fancybox * @param {Object} carousel * @param {Object} slide */ onRemoveSlide(fancybox, carousel, slide) { // Abort ajax request if exists if (slide.xhr) { slide.xhr.abort(); slide.xhr = null; } // Unload iframe content if exists if (slide.$iframe) { slide.$iframe.onload = slide.$iframe.onerror = null; slide.$iframe.src = "//about:blank"; slide.$iframe = null; } // Clear inline content const $content = slide.$content; if (slide.type === "inline" && $content) { $content.classList.remove("fancybox__content"); if ($content.style.display !== "none") { $content.style.display = "none"; } } if (slide.$closeButton) { slide.$closeButton.remove(); slide.$closeButton = null; } const $placeHolder = $content && $content.$placeHolder; if ($placeHolder) { $placeHolder.parentNode.insertBefore($content, $placeHolder); $placeHolder.remove(); $content.$placeHolder = null; } } /** * Process `window.message` event to mark video iframe element as `ready` * @param {Object} e - Event */ onMessage(e) { try { let data = JSON.parse(e.data); if (e.origin === "https://player.vimeo.com") { if (data.event === "ready") { for (let $iframe of document.getElementsByClassName("fancybox__iframe")) { if ($iframe.contentWindow === e.source) { $iframe.isReady = 1; } } } } else if (e.origin === "https://www.youtube-nocookie.com") { if (data.event === "onReady") { document.getElementById(data.id).isReady = 1; } } } catch (ex) {} } attach() { this.fancybox.on(this.events); window.addEventListener("message", this.onMessage, false); } detach() { this.fancybox.off(this.events); window.removeEventListener("message", this.onMessage, false); } } // Expose defaults Html.defaults = defaults;