@fancyapps/ui
Version:
JavaScript UI Component Library
772 lines (618 loc) • 19.4 kB
JavaScript
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;