<section><a href="#selector" data-type="html" class="lightbox" data-group="inline">
インラインのHTML
</a>
<div style="display: none;">
<div id="selector" data-group="iframe">
hogehoge
<a href="#">foo</a>
</div>
</div>
</section>
<section>
<a href="https://www.youtube.com/embed/Yds5jgqmYpE?autoplay=1" data-type="iframe" class="lightbox" data-group="iframe-youtube" data-width="1120" data-height="630">
Youtube(直接リンクする場合)
</a>
</section>
<section>
<a href="#" data-type="youtube" data-id="RK1K2bCg4J8" class="lightbox" data-group="youtube">
Youtube(idを指定するタイプ)
</a>
</section>
<section>
<a href="https://via.placeholder.com/600.jpg" class="lightbox" data-group="gallery">
画像グループ1
</a> |
<a href="https://via.placeholder.com/500.jpg" class="lightbox" data-group="gallery">
画像グループ2
</a> |
<a href="https://via.placeholder.com/300.jpg" class="lightbox" data-group="gallery">
画像グループ3
</a>
</section>
<section>
</section>
:root {
--base-font-size: 18px;
--transition-duration: 0.3s;
--transition-timing-function: cubic-bezier(0.19, 1, 0.22, 1);
--zoom-icon-background: rgba(25, 41, 56, 0.94);
--zoom-icon-color: #fff;
--lightbox-background: rgba(25, 41, 56, 0.94);
--lightbox-z-index: 1337;
--caption-background: hsla(0, 0%, 100%, 0.94);
--caption-color: #192938;
--counter-background: transparent;
--counter-color: #fff;
--button-background: transparent;
--button-color: #fff;
--loader-color: #fff;
--slide-max-height: 85vh;
--slide-max-width: 85vw;
}
.tobii-zoom {
border: 0;
box-shadow: none;
display: inline-block;
position: relative;
text-decoration: none;
}
.tobii-zoom img {
display: block;
}
.tobii-zoom__icon {
align-items: center;
background-color: var(--zoom-icon-background);
top: 0.44444em;
color: var(--zoom-icon-color);
display: flex;
height: 1.77778em;
justify-content: center;
line-height: 1;
position: absolute;
right: 0.44444em;
width: 1.77778em;
}
.tobii-zoom__icon svg {
fill: none;
height: 1.33333em;
pointer-events: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.5;
stroke: currentColor;
width: 1.33333em;
}
.tobii-is-open {
overflow-y: hidden;
}
.tobii {
background-color: var(--lightbox-background);
bottom: 0;
box-sizing: border-box;
contain: strict;
font-size: var(--base-font-size);
left: 0;
line-height: 1.5;
overflow: hidden;
position: fixed;
right: 0;
top: 0;
z-index: var(--lightbox-z-index);
}
.tobii[aria-hidden="true"] {
display: none;
}
.tobii *,
.tobii :after,
.tobii :before {
box-sizing: inherit;
}
.tobii__slider {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
will-change: transform;
}
.tobii__slider[aria-hidden="true"] {
display: none;
}
@media screen and (prefers-reduced-motion: no-preference) {
.tobii__slider--animate:not(.tobii__slider--is-dragging) {
transition-duration: var(--transition-duration);
transition-property: transform;
transition-timing-function: var(--transition-timing-function);
}
}
.tobii__slider--is-draggable [data-type] {
cursor: grab;
}
.tobii__slider--is-dragging [data-type] {
cursor: grabbing;
}
.tobii__slide {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
width: 100%;
}
.tobii__slide:not(.tobii__slide--is-active) {
visibility: hidden;
}
@media screen and (prefers-reduced-motion: no-preference) {
.tobii__slide:not(.tobii__slide--is-active) {
transition-duration: var(--transition-duration);
transition-property: visibility;
transition-timing-function: var(--transition-timing-function);
}
}
.tobii__slide [data-type] {
max-height: var(--slide-max-height);
max-width: var(--slide-max-width);
overflow: hidden;
overflow-y: auto;
-ms-scroll-chaining: none;
overscroll-behavior: contain;
}
.tobii__slide iframe,
.tobii__slide video {
display: block !important;
}
.tobii__slide figure {
margin: 0;
position: relative;
}
.tobii__slide figure > img {
display: block;
height: auto;
max-height: var(--slide-max-height);
max-width: var(--slide-max-width);
width: auto;
}
.tobii__slide figure > figcaption {
background-color: var(--caption-background);
bottom: 0;
color: var(--caption-color);
padding: 0.22222em 0.44444em;
position: absolute;
white-space: pre-wrap;
width: 100%;
}
.tobii__slide [data-type="html"] video {
cursor: auto;
max-height: var(--slide-max-height);
max-width: var(--slide-max-width);
}
.tobii__slide [data-type="iframe"] {
-webkit-overflow-scrolling: touch;
transform: translateZ(0);
}
.tobii__slide [data-type="iframe"] iframe {
height: var(--slide-max-height);
width: var(--slide-max-width);
}
.tobii__btn {
-webkit-appearance: none;
appearance: none;
background-color: var(--button-background);
border: 0.05556em solid transparent;
color: var(--button-color);
cursor: pointer;
font: inherit;
line-height: 1;
margin: 0;
opacity: 0.5;
padding: 0;
position: absolute;
touch-action: manipulation;
will-change: opacity;
z-index: 1;
}
@media screen and (prefers-reduced-motion: no-preference) {
.tobii__btn {
transition-duration: var(--transition-duration);
transition-property: opacity, transform;
transition-timing-function: var(--transition-timing-function);
will-change: opacity, transform;
}
}
.tobii__btn svg {
fill: none;
height: 3.33333em;
pointer-events: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1;
stroke: currentColor;
width: 3.33333em;
}
.tobii__btn:active,
.tobii__btn:focus,
.tobii__btn:hover {
opacity: 1;
}
.tobii__btn--next,
.tobii__btn--previous {
top: 50%;
transform: translateY(-50%);
}
.tobii__btn--previous {
left: 0.88889em;
}
.tobii__btn--next {
right: 0.88889em;
}
.tobii__btn--close {
right: 0.88889em;
top: 0.88889em;
}
.tobii__btn:disabled,
.tobii__btn[aria-hidden="true"] {
display: none;
}
.tobii__counter {
background-color: var(--counter-background);
color: var(--counter-color);
font-size: 1.11111em;
left: 1em;
line-height: 1;
position: absolute;
top: 2.22222em;
z-index: 1;
}
.tobii__counter[aria-hidden="true"] {
display: none;
}
.tobii__loader {
display: inline-block;
height: 5.55556em;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 5.55556em;
}
.tobii__loader:before {
animation: spin 1s infinite;
border-radius: 100%;
border: 0.22222em solid #949ba3;
border-top: 0.22222em solid var(--loader-color);
bottom: 0;
content: "";
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: 1;
}
@keyframes spin {
to {
transform: rotate(1turn);
}
}
.tobii__slide .tobii-html {
background: #fff;
padding: 10px 20px;
max-width: 800px;
}
section {
margin: 1em;
}
/**
* Tobii
*
* @author midzer
* @version 2.0.7
* @url https://github.com/midzer/tobii
*
* MIT License
*/
export default function Tobii(userOptions) {
/**
* Global variables
*
*/
const FOCUSABLE_ELEMENTS = [
'a[href]:not([tabindex^="-"]):not([inert])',
'area[href]:not([tabindex^="-"]):not([inert])',
"input:not([disabled]):not([inert])",
"select:not([disabled]):not([inert])",
"textarea:not([disabled]):not([inert])",
"button:not([disabled]):not([inert])",
'iframe:not([tabindex^="-"]):not([inert])',
'audio:not([tabindex^="-"]):not([inert])',
'video:not([tabindex^="-"]):not([inert])',
'[contenteditable]:not([tabindex^="-"]):not([inert])',
'[tabindex]:not([tabindex^="-"]):not([inert])'
];
const WAITING_ELS = [];
const GROUP_ATTS = {
gallery: [],
slider: null,
sliderElements: [],
elementsLength: 0,
currentIndex: 0,
x: 0
};
const PLAYER = [];
let config = {};
let figcaptionId = 0;
let lightbox = null;
let prevButton = null;
let nextButton = null;
let closeButton = null;
let counter = null;
let drag = {};
let isDraggingX = false;
let isDraggingY = false;
let pointerDown = false;
let lastFocus = null;
let offset = null;
let offsetTmp = null;
let resizeTicking = false;
let isYouTubeDependencieLoaded = false;
let playerId = 0;
let groups = {};
let newGroup = null;
let activeGroup = null;
/**
* Merge default options with user options
*
* @param {Object} userOptions - Optional user options
* @returns {Object} - Custom options
*/
const mergeOptions = (userOptions) => {
// Default options
const OPTIONS = {
selector: ".lightbox",
captions: true,
captionsSelector: "img",
captionAttribute: "alt",
captionText: null,
nav: "auto",
navText: [
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke="none" d="M0 0h24v24H0z"/><polyline points="15 6 9 12 15 18" /></svg>',
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke="none" d="M0 0h24v24H0z"/><polyline points="9 6 15 12 9 18" /></svg>'
],
navLabel: ["Previous image", "Next image"],
close: true,
closeText:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke="none" d="M0 0h24v24H0z"/><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>',
closeLabel: "Close lightbox",
loadingIndicatorLabel: "Image loading",
counter: true,
download: false, // TODO
downloadText: "", // TODO
downloadLabel: "Download image", // TODO
keyboard: true,
zoom: true,
zoomText:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke="none" d="M0 0h24v24H0z"/><polyline points="16 4 20 4 20 8" /><line x1="14" y1="10" x2="20" y2="4" /><polyline points="8 20 4 20 4 16" /><line x1="4" y1="20" x2="10" y2="14" /><polyline points="16 20 20 20 20 16" /><line x1="14" y1="14" x2="20" y2="20" /><polyline points="8 4 4 4 4 8" /><line x1="4" y1="4" x2="10" y2="10" /></svg>',
docClose: true,
swipeClose: true,
hideScrollbar: true,
draggable: true,
threshold: 100,
rtl: false, // TODO
loop: false, // TODO
autoplayVideo: false,
modal: false,
theme: "tobii--theme-default"
};
return {
...OPTIONS,
...userOptions
};
};
/**
* Types - you can add new type to support something new
*
*/
const SUPPORTED_ELEMENTS = {
image: {
checkSupport(el) {
return (
!el.hasAttribute("data-type") &&
el.href.match(/\.(png|jpe?g|tiff|tif|gif|bmp|webp|avif|svg|ico)(\?.*)?$/i)
);
},
init(el, container) {
const FIGURE = document.createElement("figure");
const FIGCAPTION = document.createElement("figcaption");
const IMAGE = document.createElement("img");
const THUMBNAIL = el.querySelector("img");
const LOADING_INDICATOR = document.createElement("div");
// Hide figure until the image is loaded
FIGURE.style.opacity = "0";
if (THUMBNAIL) {
IMAGE.alt = THUMBNAIL.alt || "";
}
IMAGE.setAttribute("src", "");
IMAGE.setAttribute("data-src", el.href);
if (el.hasAttribute("data-srcset")) {
IMAGE.setAttribute("srcset", el.getAttribute("data-srcset"));
}
// Add image to figure
FIGURE.appendChild(IMAGE);
// Create figcaption
if (config.captions) {
if (typeof config.captionText === "function") {
FIGCAPTION.textContent = config.captionText(el);
} else if (
config.captionsSelector === "self" &&
el.getAttribute(config.captionAttribute)
) {
FIGCAPTION.textContent = el.getAttribute(config.captionAttribute);
} else if (
config.captionsSelector === "img" &&
THUMBNAIL &&
THUMBNAIL.getAttribute(config.captionAttribute)
) {
FIGCAPTION.textContent = THUMBNAIL.getAttribute(config.captionAttribute);
}
if (FIGCAPTION.textContent) {
FIGCAPTION.id = `tobii-figcaption-${figcaptionId}`;
FIGURE.appendChild(FIGCAPTION);
IMAGE.setAttribute("aria-labelledby", FIGCAPTION.id);
++figcaptionId;
}
}
// Add figure to container
container.appendChild(FIGURE);
// Create loading indicator
LOADING_INDICATOR.className = "tobii__loader";
LOADING_INDICATOR.setAttribute("role", "progressbar");
LOADING_INDICATOR.setAttribute("aria-label", config.loadingIndicatorLabel);
// Add loading indicator to container
container.appendChild(LOADING_INDICATOR);
// Register type
container.setAttribute("data-type", "image");
container.classList.add("tobii-image");
},
onPreload(container) {
// Same as preload
SUPPORTED_ELEMENTS.image.onLoad(container);
},
onLoad(container) {
const IMAGE = container.querySelector("img");
if (!IMAGE.hasAttribute("data-src")) {
return;
}
const FIGURE = container.querySelector("figure");
const LOADING_INDICATOR = container.querySelector(".tobii__loader");
IMAGE.onload = () => {
container.removeChild(LOADING_INDICATOR);
FIGURE.style.opacity = "1";
};
IMAGE.setAttribute("src", IMAGE.getAttribute("data-src"));
IMAGE.removeAttribute("data-src");
},
onLeave(container) {
// Nothing
},
onCleanup(container) {
// Nothing
}
},
html: {
checkSupport(el) {
return checkType(el, "html");
},
init(el, container) {
const TARGET_SELECTOR = el.hasAttribute("data-target")
? el.getAttribute("data-target")
: el.getAttribute("href");
const TARGET = document.querySelector(TARGET_SELECTOR);
if (!TARGET) {
throw new Error(`Ups, I can't find the target ${TARGET_SELECTOR}.`);
}
// Add content to container
container.appendChild(TARGET);
// Register type
container.setAttribute("data-type", "html");
container.classList.add("tobii-html");
},
onPreload(container) {
// Nothing
},
onLoad(container) {
const VIDEO = container.querySelector("video");
if (VIDEO) {
if (VIDEO.hasAttribute("data-time") && VIDEO.readyState > 0) {
// Continue where video was stopped
VIDEO.currentTime = VIDEO.getAttribute("data-time");
}
if (config.autoplayVideo) {
// Start playback (and loading if necessary)
VIDEO.play();
}
}
},
onLeave(container) {
const VIDEO = container.querySelector("video");
if (VIDEO) {
if (!VIDEO.paused) {
// Stop if video is playing
VIDEO.pause();
}
// Backup currentTime (needed for revisit)
if (VIDEO.readyState > 0) {
VIDEO.setAttribute("data-time", VIDEO.currentTime);
}
}
},
onCleanup(container) {
const VIDEO = container.querySelector("video");
if (VIDEO) {
if (
VIDEO.readyState > 0 &&
VIDEO.readyState < 3 &&
VIDEO.duration !== VIDEO.currentTime
) {
// Some data has been loaded but not the whole package.
// In order to save bandwidth, stop downloading as soon as possible.
const VIDEO_CLONE = VIDEO.cloneNode(true);
removeSources(VIDEO);
VIDEO.load();
VIDEO.parentNode.removeChild(VIDEO);
container.appendChild(VIDEO_CLONE);
}
}
}
},
iframe: {
checkSupport(el) {
return checkType(el, "iframe");
},
init(el, container) {
const IFRAME = document.createElement("iframe");
const HREF = el.hasAttribute("data-target")
? el.getAttribute("data-target")
: el.getAttribute("href");
IFRAME.setAttribute("frameborder", "0");
IFRAME.setAttribute("src", "");
IFRAME.setAttribute("allowfullscreen", "");
IFRAME.setAttribute("data-src", HREF);
// Hide until loaded
IFRAME.style.opacity = "0";
// set allow parameters
if (HREF.indexOf("youtube.com") > -1) {
IFRAME.setAttribute(
"allow",
"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
);
} else if (HREF.indexOf("vimeo.com") > -1) {
IFRAME.setAttribute("allow", "autoplay; picture-in-picture");
} else if (el.hasAttribute("data-allow")) {
IFRAME.setAttribute("allow", el.getAttribute("data-allow"));
}
if (el.getAttribute("data-width")) {
IFRAME.style.maxWidth = `${el.getAttribute("data-width")}px`;
}
if (el.getAttribute("data-height")) {
IFRAME.style.maxHeight = `${el.getAttribute("data-height")}px`;
}
// Add iframe to container
container.appendChild(IFRAME);
// Register type
container.setAttribute("data-type", "iframe");
container.classList.add("tobii-iframe");
},
onPreload(container) {
// Nothing
},
onLoad(container) {
const IFRAME = container.querySelector("iframe");
IFRAME.setAttribute("src", IFRAME.getAttribute("data-src"));
IFRAME.onload = () => {
IFRAME.style.opacity = "1";
};
},
onLeave(container) {
// Nothing
},
onCleanup(container) {
const IFRAME = container.querySelector("iframe");
IFRAME.setAttribute("src", "");
IFRAME.style.opacity = "0";
}
},
youtube: {
checkSupport(el) {
return checkType(el, "youtube");
},
init(el, container) {
const IFRAME_PLACEHOLDER = document.createElement("div");
// Add iframePlaceholder to container
container.appendChild(IFRAME_PLACEHOLDER);
PLAYER[playerId] = new window.YT.Player(IFRAME_PLACEHOLDER, {
host: "https://www.youtube-nocookie.com",
height: el.getAttribute("data-height") || "360",
width: el.getAttribute("data-width") || "640",
videoId: el.getAttribute("data-id"),
playerVars: {
controls: el.getAttribute("data-controls") || 1,
rel: 0,
playsinline: 1
}
});
// Set player ID
container.setAttribute("data-player", playerId);
// Register type
container.setAttribute("data-type", "youtube");
container.classList.add("tobii-youtube");
playerId++;
},
onPreload(container) {
// Nothing
},
onLoad(container) {
if (config.autoplayVideo) {
PLAYER[container.getAttribute("data-player")].playVideo();
}
},
onLeave(container) {
if (PLAYER[container.getAttribute("data-player")].getPlayerState() === 1) {
PLAYER[container.getAttribute("data-player")].pauseVideo();
}
},
onCleanup(container) {
if (PLAYER[container.getAttribute("data-player")].getPlayerState() === 1) {
PLAYER[container.getAttribute("data-player")].pauseVideo();
}
}
}
};
/**
* Init
*
*/
const init = (userOptions) => {
// Merge user options into defaults
config = mergeOptions(userOptions);
// Check if the lightbox already exists
if (!lightbox) {
createLightbox();
}
// Get a list of all elements within the document
const LIGHTBOX_TRIGGER_ELS = document.querySelectorAll(config.selector);
if (!LIGHTBOX_TRIGGER_ELS) {
throw new Error(
`Ups, I can't find the selector ${config.selector} on this website.`
);
}
// Execute a few things once per element
LIGHTBOX_TRIGGER_ELS.forEach((lightboxTriggerEl) => {
checkDependencies(lightboxTriggerEl);
});
};
/**
* Check dependencies
*
* @param {HTMLElement} el - Element to add
*/
const checkDependencies = (el) => {
// Check if there is a YouTube video and if the YouTube iframe-API is ready
if (
document.querySelector('[data-type="youtube"]') !== null &&
!isYouTubeDependencieLoaded
) {
if (document.getElementById("iframe_api") === null) {
const TAG = document.createElement("script");
const FIRST_SCRIPT_TAG = document.getElementsByTagName("script")[0];
TAG.id = "iframe_api";
TAG.src = "https://www.youtube.com/iframe_api";
FIRST_SCRIPT_TAG.parentNode.insertBefore(TAG, FIRST_SCRIPT_TAG);
}
if (WAITING_ELS.indexOf(el) === -1) {
WAITING_ELS.push(el);
}
window.onYouTubePlayerAPIReady = () => {
WAITING_ELS.forEach((waitingEl) => {
add(waitingEl);
});
isYouTubeDependencieLoaded = true;
};
} else {
add(el);
}
};
/**
* Get group name from element
*
* @param {HTMLElement} el
* @return {string}
*/
const getGroupName = (el) => {
return el.hasAttribute("data-group")
? el.getAttribute("data-group")
: "default";
};
/**
* Copy an object. (The secure way)
*
* @param {object} object
* @return {object}
*/
const copyObject = (object) => {
return JSON.parse(JSON.stringify(object));
};
/**
* Add element
*
* @param {HTMLElement} el - Element to add
*/
const add = (el) => {
newGroup = getGroupName(el);
if (!Object.prototype.hasOwnProperty.call(groups, newGroup)) {
groups[newGroup] = copyObject(GROUP_ATTS);
createSlider();
}
// Check if element already exists
if (groups[newGroup].gallery.indexOf(el) === -1) {
groups[newGroup].gallery.push(el);
groups[newGroup].elementsLength++;
// Set zoom icon if necessary
if (config.zoom && el.querySelector("img")) {
const TOBII_ZOOM = document.createElement("div");
TOBII_ZOOM.className = "tobii-zoom__icon";
TOBII_ZOOM.innerHTML = config.zoomText;
el.classList.add("tobii-zoom");
el.appendChild(TOBII_ZOOM);
}
// Bind click event handler
el.addEventListener("click", triggerTobii);
createSlide(el);
if (isOpen() && newGroup === activeGroup) {
updateConfig();
updateLightbox();
}
} else {
throw new Error("Ups, element already added.");
}
};
/**
* Remove element
*
* @param {HTMLElement} el - Element to remove
*/
const remove = (el) => {
const GROUP_NAME = getGroupName(el);
// Check if element exists
if (groups[GROUP_NAME].gallery.indexOf(el) === -1) {
throw new Error(`Ups, I can't find a slide for the element ${el}.`);
} else {
const SLIDE_INDEX = groups[GROUP_NAME].gallery.indexOf(el);
const SLIDE_EL = groups[GROUP_NAME].sliderElements[SLIDE_INDEX];
// If the element to be removed is the currently visible slide
if (
isOpen() &&
GROUP_NAME === activeGroup &&
SLIDE_INDEX === groups[GROUP_NAME].currentIndex
) {
if (groups[GROUP_NAME].elementsLength === 1) {
close();
throw new Error("Ups, I've closed. There are no slides more to show.");
} else {
// TODO If there is only one slide left, deactivate horizontal dragging/ swiping
// TODO Recalculate counter
// TODO Set new absolute position per slide
// If the first slide is displayed
if (groups[GROUP_NAME].currentIndex === 0) {
next();
} else {
previous();
}
}
}
// TODO Remove element
// groups[GROUP_NAME].gallery.splice(groups[GROUP_NAME].gallery.indexOf(el)) don't work
groups[GROUP_NAME].elementsLength--;
// Remove zoom icon if necessary
if (config.zoom && el.querySelector(".tobii-zoom__icon")) {
const ZOOM_ICON = el.querySelector(".tobii-zoom__icon");
ZOOM_ICON.parentNode.classList.remove("tobii-zoom");
ZOOM_ICON.parentNode.removeChild(ZOOM_ICON);
}
// Unbind click event handler
el.removeEventListener("click", triggerTobii);
// Remove slide
SLIDE_EL.parentNode.removeChild(SLIDE_EL);
}
};
/**
* Create the lightbox
*
*/
const createLightbox = () => {
// Create the lightbox container
lightbox = document.createElement("div");
lightbox.setAttribute("role", "dialog");
lightbox.setAttribute("aria-hidden", "true");
lightbox.classList.add("tobii");
// Adc theme class
lightbox.classList.add(config.theme);
// Create the previous button
prevButton = document.createElement("button");
prevButton.className = "tobii__btn tobii__btn--previous";
prevButton.setAttribute("type", "button");
prevButton.setAttribute("aria-label", config.navLabel[0]);
prevButton.innerHTML = config.navText[0];
lightbox.appendChild(prevButton);
// Create the next button
nextButton = document.createElement("button");
nextButton.className = "tobii__btn tobii__btn--next";
nextButton.setAttribute("type", "button");
nextButton.setAttribute("aria-label", config.navLabel[1]);
nextButton.innerHTML = config.navText[1];
lightbox.appendChild(nextButton);
// Create the close button
closeButton = document.createElement("button");
closeButton.className = "tobii__btn tobii__btn--close";
closeButton.setAttribute("type", "button");
closeButton.setAttribute("aria-label", config.closeLabel);
closeButton.innerHTML = config.closeText;
lightbox.appendChild(closeButton);
// Create the counter
counter = document.createElement("div");
counter.className = "tobii__counter";
lightbox.appendChild(counter);
document.body.appendChild(lightbox);
};
/**
* Create a slider
*/
const createSlider = () => {
groups[newGroup].slider = document.createElement("div");
groups[newGroup].slider.className = "tobii__slider";
// Hide slider
groups[newGroup].slider.setAttribute("aria-hidden", "true");
lightbox.appendChild(groups[newGroup].slider);
};
/**
* Create a slide
*
*/
const createSlide = (el) => {
// Detect type
for (const index in SUPPORTED_ELEMENTS) {
if (Object.prototype.hasOwnProperty.call(SUPPORTED_ELEMENTS, index)) {
if (SUPPORTED_ELEMENTS[index].checkSupport(el)) {
// Create slide elements
const SLIDER_ELEMENT = document.createElement("div");
const SLIDER_ELEMENT_CONTENT = document.createElement("div");
SLIDER_ELEMENT.className = "tobii__slide";
SLIDER_ELEMENT.style.position = "absolute";
SLIDER_ELEMENT.style.left = `${groups[newGroup].x * 100}%`;
// Hide slide
SLIDER_ELEMENT.setAttribute("aria-hidden", "true");
// Create type elements
SUPPORTED_ELEMENTS[index].init(el, SLIDER_ELEMENT_CONTENT);
// Add slide content container to slider element
SLIDER_ELEMENT.appendChild(SLIDER_ELEMENT_CONTENT);
// Add slider element to slider
groups[newGroup].slider.appendChild(SLIDER_ELEMENT);
groups[newGroup].sliderElements.push(SLIDER_ELEMENT);
++groups[newGroup].x;
break;
}
}
}
};
/**
* Open Tobii
*
* @param {number} index - Index to load
*/
const open = (index) => {
activeGroup = activeGroup !== null ? activeGroup : newGroup;
if (isOpen()) {
throw new Error("Ups, I'm aleady open.");
}
if (!isOpen()) {
if (!index) {
index = 0;
}
if (index === -1 || index >= groups[activeGroup].elementsLength) {
throw new Error(`Ups, I can't find slide ${index}.`);
}
}
if (config.hideScrollbar) {
document.documentElement.classList.add("tobii-is-open");
document.body.classList.add("tobii-is-open");
}
updateConfig();
// Hide close if necessary
if (!config.close) {
closeButton.disabled = false;
closeButton.setAttribute("aria-hidden", "true");
}
// Save user’s focus
lastFocus = document.activeElement;
// Use `history.pushState()` to make sure the "Back" button behavior
// that aligns with the user's expectations
const stateObj = {
tobii: "close"
};
const url = window.location.href;
window.history.pushState(stateObj, "Image", url);
// Set current index
groups[activeGroup].currentIndex = index;
clearDrag();
bindEvents();
// Load slide
load(groups[activeGroup].currentIndex);
// Show slider
groups[activeGroup].slider.setAttribute("aria-hidden", "false");
// Show lightbox
lightbox.setAttribute("aria-hidden", "false");
updateLightbox();
// Preload previous and next slide
preload(groups[activeGroup].currentIndex + 1);
preload(groups[activeGroup].currentIndex - 1);
// Hack to prevent animation during opening
setTimeout(() => {
groups[activeGroup].slider.classList.add("tobii__slider--animate");
}, 1000);
// Create and dispatch a new event
const openEvent = new window.CustomEvent("open", {
detail: {
group: activeGroup
}
});
lightbox.dispatchEvent(openEvent);
};
/**
* Close Tobii
*
*/
const close = () => {
if (!isOpen()) {
throw new Error("Ups, I'm already closed.");
}
if (config.hideScrollbar) {
document.documentElement.classList.remove("tobii-is-open");
document.body.classList.remove("tobii-is-open");
}
unbindEvents();
// Remove entry in browser history
if (window.history.state !== null) {
if (window.history.state.tobii === "close") {
window.history.back();
}
}
// Reenable the user’s focus
lastFocus.focus();
// Don't forget to cleanup our current element
leave(groups[activeGroup].currentIndex);
cleanup(groups[activeGroup].currentIndex);
// Hide lightbox
lightbox.setAttribute("aria-hidden", "true");
// Hide slider
groups[activeGroup].slider.setAttribute("aria-hidden", "true");
// Reset current index
groups[activeGroup].currentIndex = 0;
// Remove the hack to prevent animation during opening
groups[activeGroup].slider.classList.remove("tobii__slider--animate");
// Create and dispatch a new event
const closeEvent = new window.CustomEvent("close", {
detail: {
group: activeGroup
}
});
lightbox.dispatchEvent(closeEvent);
};
/**
* Preload slide
*
* @param {number} index - Index to preload
*/
const preload = (index) => {
if (groups[activeGroup].sliderElements[index] === undefined) {
return;
}
const CONTAINER = groups[activeGroup].sliderElements[index].querySelector(
"[data-type]"
);
const TYPE = CONTAINER.getAttribute("data-type");
SUPPORTED_ELEMENTS[TYPE].onPreload(CONTAINER);
};
/**
* Load slide
* Will be called when opening the lightbox or moving index
*
* @param {number} index - Index to load
*/
const load = (index) => {
if (groups[activeGroup].sliderElements[index] === undefined) {
return;
}
const CONTAINER = groups[activeGroup].sliderElements[index].querySelector(
"[data-type]"
);
const TYPE = CONTAINER.getAttribute("data-type");
// Add active slide class
groups[activeGroup].sliderElements[index].classList.add(
"tobii__slide--is-active"
);
groups[activeGroup].sliderElements[index].setAttribute(
"aria-hidden",
"false"
);
SUPPORTED_ELEMENTS[TYPE].onLoad(CONTAINER);
};
/**
* Select a slide
*
* @param {number} index - Index to select
*/
const select = (index) => {
const currIndex = groups[activeGroup].currentIndex;
if (!isOpen()) {
throw new Error("Ups, I'm closed.");
}
if (isOpen()) {
if (!index && index !== 0) {
throw new Error("Ups, no slide specified.");
}
if (index === groups[activeGroup].currentIndex) {
throw new Error(`Ups, slide ${index} is already selected.`);
}
if (index === -1 || index >= groups[activeGroup].elementsLength) {
throw new Error(`Ups, I can't find slide ${index}.`);
}
}
// Set current index
groups[activeGroup].currentIndex = index;
leave(currIndex);
load(index);
if (index < currIndex) {
updateLightbox("left");
cleanup(currIndex);
preload(index - 1);
}
if (index > currIndex) {
updateLightbox("right");
cleanup(currIndex);
preload(index + 1);
}
};
/**
* Select the previous slide
*
*/
const previous = () => {
if (!isOpen()) {
throw new Error("Ups, I'm closed.");
}
if (groups[activeGroup].currentIndex > 0) {
leave(groups[activeGroup].currentIndex);
load(--groups[activeGroup].currentIndex);
updateLightbox("left");
cleanup(groups[activeGroup].currentIndex + 1);
preload(groups[activeGroup].currentIndex - 1);
}
// Create and dispatch a new event
const previousEvent = new window.CustomEvent("previous", {
detail: {
group: activeGroup
}
});
lightbox.dispatchEvent(previousEvent);
};
/**
* Select the next slide
*
*/
const next = () => {
if (!isOpen()) {
throw new Error("Ups, I'm closed.");
}
if (
groups[activeGroup].currentIndex <
groups[activeGroup].elementsLength - 1
) {
leave(groups[activeGroup].currentIndex);
load(++groups[activeGroup].currentIndex);
updateLightbox("right");
cleanup(groups[activeGroup].currentIndex - 1);
preload(groups[activeGroup].currentIndex + 1);
}
// Create and dispatch a new event
const nextEvent = new window.CustomEvent("next", {
detail: {
group: activeGroup
}
});
lightbox.dispatchEvent(nextEvent);
};
/**
* Select a group
*
* @param {string} name - Name of the group to select
*/
const selectGroup = (name) => {
if (isOpen()) {
throw new Error("Ups, I'm open.");
}
if (!name) {
throw new Error("Ups, no group specified.");
}
if (name && !Object.prototype.hasOwnProperty.call(groups, name)) {
throw new Error(`Ups, I don't have a group called "${name}".`);
}
activeGroup = name;
};
/**
* Leave slide
* Will be called before moving index
*
* @param {number} index - Index to leave
*/
const leave = (index) => {
if (groups[activeGroup].sliderElements[index] === undefined) {
return;
}
const CONTAINER = groups[activeGroup].sliderElements[index].querySelector(
"[data-type]"
);
const TYPE = CONTAINER.getAttribute("data-type");
// Remove active slide class
groups[activeGroup].sliderElements[index].classList.remove(
"tobii__slide--is-active"
);
groups[activeGroup].sliderElements[index].setAttribute("aria-hidden", "true");
SUPPORTED_ELEMENTS[TYPE].onLeave(CONTAINER);
};
/**
* Cleanup slide
* Will be called after moving index
*
* @param {number} index - Index to cleanup
*/
const cleanup = (index) => {
if (groups[activeGroup].sliderElements[index] === undefined) {
return;
}
const CONTAINER = groups[activeGroup].sliderElements[index].querySelector(
"[data-type]"
);
const TYPE = CONTAINER.getAttribute("data-type");
SUPPORTED_ELEMENTS[TYPE].onCleanup(CONTAINER);
};
/**
* Update offset
*
*/
const updateOffset = () => {
activeGroup = activeGroup !== null ? activeGroup : newGroup;
offset = -groups[activeGroup].currentIndex * lightbox.offsetWidth;
groups[activeGroup].slider.style.transform = `translate3d(${offset}px, 0, 0)`;
offsetTmp = offset;
};
/**
* Update counter
*
*/
const updateCounter = () => {
counter.textContent = `${groups[activeGroup].currentIndex + 1}/${
groups[activeGroup].elementsLength
}`;
};
/**
* Update focus
*
* @param {string} dir - Current slide direction
*/
const updateFocus = (dir) => {
if (
(config.nav === true || config.nav === "auto") &&
!isTouchDevice() &&
groups[activeGroup].elementsLength > 1
) {
prevButton.setAttribute("aria-hidden", "true");
prevButton.disabled = true;
nextButton.setAttribute("aria-hidden", "true");
nextButton.disabled = true;
// If there is only one slide
if (groups[activeGroup].elementsLength === 1) {
if (config.close) {
closeButton.focus();
}
} else {
// If the first slide is displayed
if (groups[activeGroup].currentIndex === 0) {
nextButton.setAttribute("aria-hidden", "false");
nextButton.disabled = false;
nextButton.focus();
// If the last slide is displayed
} else if (
groups[activeGroup].currentIndex ===
groups[activeGroup].elementsLength - 1
) {
prevButton.setAttribute("aria-hidden", "false");
prevButton.disabled = false;
prevButton.focus();
} else {
prevButton.setAttribute("aria-hidden", "false");
prevButton.disabled = false;
nextButton.setAttribute("aria-hidden", "false");
nextButton.disabled = false;
if (dir === "left") {
prevButton.focus();
} else {
nextButton.focus();
}
}
}
} else if (config.close) {
closeButton.focus();
}
};
/**
* Clear drag after touchend and mousup event
*
*/
const clearDrag = () => {
drag = {
startX: 0,
endX: 0,
startY: 0,
endY: 0
};
};
/**
* Recalculate drag / swipe event
*
*/
const updateAfterDrag = () => {
const MOVEMENT_X = drag.endX - drag.startX;
const MOVEMENT_Y = drag.endY - drag.startY;
const MOVEMENT_X_DISTANCE = Math.abs(MOVEMENT_X);
const MOVEMENT_Y_DISTANCE = Math.abs(MOVEMENT_Y);
if (
MOVEMENT_X > 0 &&
MOVEMENT_X_DISTANCE > config.threshold &&
groups[activeGroup].currentIndex > 0
) {
previous();
} else if (
MOVEMENT_X < 0 &&
MOVEMENT_X_DISTANCE > config.threshold &&
groups[activeGroup].currentIndex !== groups[activeGroup].elementsLength - 1
) {
next();
} else if (
MOVEMENT_Y < 0 &&
MOVEMENT_Y_DISTANCE > config.threshold &&
config.swipeClose
) {
close();
} else {
updateOffset();
}
};
/**
* Resize event using requestAnimationFrame
*
*/
const resizeHandler = () => {
if (!resizeTicking) {
resizeTicking = true;
window.requestAnimationFrame(() => {
updateOffset();
resizeTicking = false;
});
}
};
/**
* Click event handler to trigger Tobii
*
*/
const triggerTobii = (event) => {
event.preventDefault();
activeGroup = getGroupName(event.currentTarget);
open(groups[activeGroup].gallery.indexOf(event.currentTarget));
};
/**
* Click event handler
*
*/
const clickHandler = (event) => {
if (event.target === prevButton) {
previous();
} else if (event.target === nextButton) {
next();
} else if (
event.target === closeButton ||
(isDraggingX === false &&
isDraggingY === false &&
event.target.classList.contains("tobii__slide") &&
config.docClose)
) {
close();
}
event.stopPropagation();
};
/**
* Get the focusable children of the given element
*
* @return {Array<Element>}
*/
const getFocusableChildren = () => {
return Array.prototype.slice
.call(
lightbox.querySelectorAll(
`.tobii__btn:not([disabled]), .tobii__slide--is-active + ${FOCUSABLE_ELEMENTS.join(
", .tobii__slide--is-active "
)}`
)
)
.filter((child) => {
return !!(
child.offsetWidth ||
child.offsetHeight ||
child.getClientRects().length
);
});
};
/**
* Keydown event handler
*
* @TODO: Remove the deprecated event.keyCode when Edge support event.code and we drop f*cking IE
* @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
*/
const keydownHandler = (event) => {
const FOCUSABLE_CHILDREN = getFocusableChildren();
const FOCUSED_ITEM_INDEX = FOCUSABLE_CHILDREN.indexOf(document.activeElement);
if (event.keyCode === 9 || event.code === "Tab") {
// If the SHIFT key is being pressed while tabbing (moving backwards) and
// the currently focused item is the first one, move the focus to the last
// focusable item from the slide
if (event.shiftKey && FOCUSED_ITEM_INDEX === 0) {
FOCUSABLE_CHILDREN[FOCUSABLE_CHILDREN.length - 1].focus();
event.preventDefault();
// If the SHIFT key is not being pressed (moving forwards) and the currently
// focused item is the last one, move the focus to the first focusable item
// from the slide
} else if (
!event.shiftKey &&
FOCUSED_ITEM_INDEX === FOCUSABLE_CHILDREN.length - 1
) {
FOCUSABLE_CHILDREN[0].focus();
event.preventDefault();
}
} else if (event.keyCode === 27 || event.code === "Escape") {
// `ESC` Key: Close Tobii
event.preventDefault();
close();
} else if (event.keyCode === 37 || event.code === "ArrowLeft") {
// `PREV` Key: Show the previous slide
event.preventDefault();
previous();
} else if (event.keyCode === 39 || event.code === "ArrowRight") {
// `NEXT` Key: Show the next slide
event.preventDefault();
next();
}
};
/**
* Touchstart event handler
*
*/
const touchstartHandler = (event) => {
// Prevent dragging / swiping on textareas inputs and selects
if (isIgnoreElement(event.target)) {
return;
}
event.stopPropagation();
isDraggingX = false;
isDraggingY = false;
pointerDown = true;
drag.startX = event.touches[0].pageX;
drag.startY = event.touches[0].pageY;
groups[activeGroup].slider.classList.add("tobii__slider--is-dragging");
};
/**
* Touchmove event handler
*
*/
const touchmoveHandler = (event) => {
event.stopPropagation();
if (pointerDown) {
event.preventDefault();
drag.endX = event.touches[0].pageX;
drag.endY = event.touches[0].pageY;
doSwipe();
}
};
/**
* Touchend event handler
*
*/
const touchendHandler = (event) => {
event.stopPropagation();
pointerDown = false;
groups[activeGroup].slider.classList.remove("tobii__slider--is-dragging");
if (drag.endX) {
updateAfterDrag();
}
clearDrag();
};
/**
* Mousedown event handler
*
*/
const mousedownHandler = (event) => {
// Prevent dragging / swiping on textareas inputs and selects
if (isIgnoreElement(event.target)) {
return;
}
event.preventDefault();
event.stopPropagation();
isDraggingX = false;
isDraggingY = false;
pointerDown = true;
drag.startX = event.pageX;
drag.startY = event.pageY;
groups[activeGroup].slider.classList.add("tobii__slider--is-dragging");
};
/**
* Mousemove event handler
*
*/
const mousemoveHandler = (event) => {
event.preventDefault();
if (pointerDown) {
drag.endX = event.pageX;
drag.endY = event.pageY;
doSwipe();
}
};
/**
* Mouseup event handler
*
*/
const mouseupHandler = (event) => {
event.stopPropagation();
pointerDown = false;
groups[activeGroup].slider.classList.remove("tobii__slider--is-dragging");
if (drag.endX) {
updateAfterDrag();
}
clearDrag();
};
/**
* Contextmenu event handler
* This is a fix for chromium based browser on mac.
* The 'contextmenu' terminates a mouse event sequence.
* https://bugs.chromium.org/p/chromium/issues/detail?id=506801
*
*/
const contextmenuHandler = () => {
pointerDown = false;
};
/**
* Decide whether to do horizontal of vertical swipe
*
*/
const doSwipe = () => {
if (
Math.abs(drag.startX - drag.endX) > 0 &&
!isDraggingY &&
groups[activeGroup].elementsLength > 1
) {
// Horizontal swipe
groups[activeGroup].slider.style.transform = `translate3d(${
offsetTmp - Math.round(drag.startX - drag.endX)
}px, 0, 0)`;
isDraggingX = true;
isDraggingY = false;
} else if (
Math.abs(drag.startY - drag.endY) > 0 &&
!isDraggingX &&
config.swipeClose
) {
// Vertical swipe
groups[
activeGroup
].slider.style.transform = `translate3d(${offsetTmp}px, -${Math.round(
drag.startY - drag.endY
)}px, 0)`;
isDraggingX = false;
isDraggingY = true;
}
};
/**
* Bind events
*
*/
const bindEvents = () => {
if (config.keyboard) {
window.addEventListener("keydown", keydownHandler);
}
// Resize event
window.addEventListener("resize", resizeHandler);
// Popstate event
window.addEventListener("popstate", close);
// Click event
lightbox.addEventListener("click", clickHandler);
if (config.draggable) {
if (isTouchDevice()) {
// Touch events
lightbox.addEventListener("touchstart", touchstartHandler);
lightbox.addEventListener("touchmove", touchmoveHandler);
lightbox.addEventListener("touchend", touchendHandler);
}
// Mouse events
lightbox.addEventListener("mousedown", mousedownHandler);
lightbox.addEventListener("mouseup", mouseupHandler);
lightbox.addEventListener("mousemove", mousemoveHandler);
lightbox.addEventListener("contextmenu", contextmenuHandler);
}
};
/**
* Unbind events
*
*/
const unbindEvents = () => {
if (config.keyboard) {
window.removeEventListener("keydown", keydownHandler);
}
// Resize event
window.removeEventListener("resize", resizeHandler);
// Popstate event
window.removeEventListener("popstate", close);
// Click event
lightbox.removeEventListener("click", clickHandler);
if (config.draggable) {
if (isTouchDevice()) {
// Touch events
lightbox.removeEventListener("touchstart", touchstartHandler);
lightbox.removeEventListener("touchmove", touchmoveHandler);
lightbox.removeEventListener("touchend", touchendHandler);
}
// Mouse events
lightbox.removeEventListener("mousedown", mousedownHandler);
lightbox.removeEventListener("mouseup", mouseupHandler);
lightbox.removeEventListener("mousemove", mousemoveHandler);
lightbox.removeEventListener("contextmenu", contextmenuHandler);
}
};
/**
* Checks whether element has requested data-type value
*
*/
const checkType = (el, type) => {
return el.getAttribute("data-type") === type;
};
/**
* Remove all `src` attributes
*
* @param {HTMLElement} el - Element to remove all `src` attributes
*/
const removeSources = (el) => {
const SOURCES = el.querySelectorAll("src");
if (SOURCES) {
SOURCES.forEach((source) => {
source.setAttribute("src", "");
});
}
};
/**
* Update Config
*
*/
const updateConfig = () => {
if (
(config.draggable &&
config.swipeClose &&
!groups[activeGroup].slider.classList.contains(
"tobii__slider--is-draggable"
)) ||
(config.draggable &&
groups[activeGroup].elementsLength > 1 &&
!groups[activeGroup].slider.classList.contains(
"tobii__slider--is-draggable"
))
) {
groups[activeGroup].slider.classList.add("tobii__slider--is-draggable");
}
// Hide buttons if necessary
if (
!config.nav ||
groups[activeGroup].elementsLength === 1 ||
(config.nav === "auto" && isTouchDevice())
) {
prevButton.setAttribute("aria-hidden", "true");
prevButton.disabled = true;
nextButton.setAttribute("aria-hidden", "true");
nextButton.disabled = true;
} else {
prevButton.setAttribute("aria-hidden", "false");
prevButton.disabled = false;
nextButton.setAttribute("aria-hidden", "false");
nextButton.disabled = false;
}
// Hide counter if necessary
if (!config.counter || groups[activeGroup].elementsLength === 1) {
counter.setAttribute("aria-hidden", "true");
} else {
counter.setAttribute("aria-hidden", "false");
}
};
/**
* Update lightbox
*
* @param {string} dir - Current slide direction
*/
const updateLightbox = (dir) => {
updateOffset();
updateCounter();
updateFocus(dir);
};
/**
* Reset Tobii
*
*/
const reset = () => {
if (isOpen()) {
close();
}
// TODO Cleanup
const GROUPS_ENTRIES = Object.entries(groups);
GROUPS_ENTRIES.forEach((groupsEntrie) => {
const SLIDE_ELS = groupsEntrie[1].gallery;
// Remove slides
SLIDE_ELS.forEach((slideEl) => {
remove(slideEl);
});
});
groups = {};
newGroup = activeGroup = null;
figcaptionId = 0;
// TODO
};
/**
* Destroy Tobii
*
*/
const destroy = () => {
reset();
lightbox.parentNode.removeChild(lightbox);
};
/**
* Check if Tobii is open
*
*/
const isOpen = () => {
return lightbox.getAttribute("aria-hidden") === "false";
};
/**
* Detect whether device is touch capable
*
*/
const isTouchDevice = () => {
return "ontouchstart" in window;
};
/**
* Checks whether element's nodeName is part of array
*
*/
const isIgnoreElement = (el) => {
return (
["TEXTAREA", "OPTION", "INPUT", "SELECT"].indexOf(el.nodeName) !== -1 ||
el === prevButton ||
el === nextButton ||
el === closeButton
);
};
/**
* Return current index
*
*/
const slidesIndex = () => {
return groups[activeGroup].currentIndex;
};
/**
* Return elements length
*
*/
const slidesCount = () => {
return groups[activeGroup].elementsLength;
};
/**
* Return current group
*
*/
const currentGroup = () => {
return activeGroup !== null ? activeGroup : newGroup;
};
/**
* Bind events
* @param {String} eventName
* @param {function} callback - callback to call
*
*/
const on = (eventName, callback) => {
lightbox.addEventListener(eventName, callback);
};
/**
* Unbind events
* @param {String} eventName
* @param {function} callback - callback to call
*
*/
const off = (eventName, callback) => {
lightbox.removeEventListener(eventName, callback);
};
init(userOptions);
Tobii.open = open;
Tobii.previous = previous;
Tobii.next = next;
Tobii.close = close;
Tobii.add = checkDependencies;
Tobii.remove = remove;
Tobii.reset = reset;
Tobii.destroy = destroy;
Tobii.isOpen = isOpen;
Tobii.slidesIndex = slidesIndex;
Tobii.select = select;
Tobii.slidesCount = slidesCount;
Tobii.selectGroup = selectGroup;
Tobii.currentGroup = currentGroup;
Tobii.on = on;
Tobii.off = off;
return Tobii;
}
const tobii = new Tobii();
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.