Multi-Use Modal Popup
Accessible and usable for just about any type of content you want to pop up gracefully on your posts in Ghost...
Launch it from a Link...
Wine bar location (interactive map)From a Button...
From an Image Link...
Launch From Any HTML Element...
This is a div :-)
For Opening an Uploaded PDF...
List of Nursing Homes for Mom (PDF)For Popping Up a Web Page...
Denver Geeks Website (Web Page)Popping Up a Youtube Video or Other Embeddable Media...
Pop-up YouTube video...TO USE:
- Add attribute
data-modal
to any link, or button, or tag, or div, etc. to indicate "Launch in modal". - Give
data-modal
the url of theiframe
src
if it's different than the linkhref
. - The modal
title
may be taken from the optional data attributedata-title=""
, or the link text, or the imagealt
text. - The modal description may also be overriden by the data attribute
data-desc
.
Here is an example of a simple link to a website:
<a class="lnk_modal-open" href="https://denverit.com/" data-modal>Denver Geeks Website (Web Page)</a>
The snippet above creates this...
Denver Geeks Website (Web Page)To enjoy, put this CSS code into your Header Injection:
<style>
/* Modal opening object (link, button, or pretend button) */
button,
[aria-role="button"] {
cursor: pointer;
}
button[data-modal][aria-controls],
[aria-role="button"][data-modal][aria-controls] {
transition: all .3s ease-out;
}
/* Hover & focus indication. */
/* Reads as: if button, or aria-role=button, has data-model attribute and JavaScript has added aria-controls then on hover or focus */
button[data-modal][aria-controls]:hover,
button[data-modal][aria-controls]:focus,
[aria-role="button"][data-modal][aria-controls]:hover,
[aria-role="button"][data-modal][aria-controls]:focus {
-webkit-filter: contrast(120%);
filter: contrast(120%);
box-shadow: 0 0 0 4px rgba(255, 0, 0, .6); /* Red so you can tell */
outline: 0 solid;
}
/* Modal opening link cosmetics */
.lnk_modal-open {
font-size: larger;
background-color: #fff;
padding:.25rem .5rem;
display: inline-block;
text-decoration: none;
border: 0 solid;
margin: 0 auto;
}
.lnk_modal-img {
padding: 0;
}
.lnk_modal-open:active {
-webkit-filter: brightness(85%);
filter: brightness(85%);
}
.img_modal-open {
display: block;
border: 0 solid;
}
/* The modal section is added via JS */
.modal {
max-width: 80vw;
max-height: 90vh;
background-color: #F7F0E8;
margin: 0 auto;
position: absolute;
left: 10%;
right: 10%;
top: 5%;
bottom: 5%;
z-index: 99999;
border: 1px solid #000;
box-shadow: 0 .25em .5em #000;
transition: opacity .5s ease-out, visibility 0s ease-out 1s, transform .5s ease-out .5s;
backface-visibility: hidden;
opacity: 0;
visibility: hidden;
transform: scale(.8) translate3d(0,0,0);
pointer-events: none;
}
.modal[aria-hidden="false"] {
position: fixed;
transition-delay: 0s,0s, 0s;
opacity: 1;
visibility: visible;
transform: scale(1) translate3d(0,0,0);
pointer-events: auto;
}
/* Light box properties */
.modal_lightbox {
text-indent: -200em;
background-color: rgba(0, 0, 0, 0.8);
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
/* places the modal overlay between the main page (0) and the modal dialog (10) */
z-index: 5;
cursor: pointer;
transition: opacity .5s ease-out, visibility 0s ease-out .5s;
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.modal_lightbox-on {
transition-delay: 0s, 0s;
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.modal_lightbox-on:hover {
/* Stolen from trickle.js. Under consideration. SVGs will not work here */
cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAANpJREFUOBGNkz0KAjEQhYPYWXgCKws7LcXWyt7WG4i9jegBvIJ4AvEyXsRCsBDiN0sGkiE/O/A2O9n3viS7rPPeb9ETrVzPwrtHDzR1XF5I6oPWLQaeq5hDXQRw0I6xCuF5HH7Tz7oFuTkhrSyEhza8THaLoQhphpWUg/QOFyA/AFpy5nTbGrIjxvg4AiiGBzYc+rGZH9KPzFy+ZbX4bX9l+VDZr5NQMMbhbtvMxccpQ3JhpTchtXATQviItIpvW0CY7HHm8c9UDRd2chbABt3RQk2tEe8O3dDkD4JQ4iOR7BMpAAAAAElFTkSuQmCC"), pointer;
}
/* Modal title and description */
.modal_title,
.modal_desc {
position: absolute;
top: 5px;
left: -200em;
background-color: #fff;
color: #000;
text-shadow: 0 0 0 #fff;
font-size: 20px;
padding: 0.125em .25em;
/* Tesco requirement
font-family: Tesco_W_Rg, sans-serif; */
margin: 0;
}
.modal_title:focus,
.modal_desc:focus {
left: 5px;
}
[aria-hidden="false"] .modal_title {
transition: opacity .5s ease-out 3s;
opacity: 0;
}
.modal_title,
.modal_title:focus {
opacity: 1;
transition: opacity .5s ease-out;
}
/* The iframe */
.modal_iframe {
transition: opacity .5s ease-out, visibility 0s ease-out 1s;
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.modal_iframe-on {
width: 100%;
transition: opacity .8s ease-out, visibility 0s ease-out 0s;
opacity: 1;
visibility: visible;
pointer-events: auto;
}
[aria-hidden="true"] .modal_iframe {
display: none;
}
[aria-hidden="false"] .modal_iframe {
display: block;
}
/* The modal pop-ups close button, appears last in the modal, but is moved visually to the top right of the pop-up */
.modal_lnk-close {
cursor: pointer;
position: absolute;
top: -20px;
right: -20px;
border: 0 solid;
border-radius: 50%;
width: 40px;
height: 40px;
background-color: #f00;
box-shadow: 0 .25em .5em rgba(0, 0, 0, .25);
overflow: hidden;
transition: background-color .3s ease-out;
}
.modal_lnk-close:hover,
.modal_lnk-close:active,
.modal_lnk-close:focus {
background-color: #c00;
outline: 0 solid;
}
.svg-close {
pointer-events: none;
width: 40px;
height: 40px;
stroke: #fff;
stroke-width: 2;
}
/* Modal SVG (Tesco) loading animation version 2 (overlaid on itself and out of phase) */
[class*="svg-loading"] {
position: absolute;
width: 80px;
height: 80px;
top: calc(50% - 40px);
left: calc(50% - 40px);
z-index: -1;
transition: opacity .3s ease-out;
backface-visibility: hidden;
}
.svg-loading {
fill: #00539f;
-webkit-animation: rotate 4s linear 0s infinite;
animation: rotate 4s linear 0s infinite;
}
.svg-loading2 {
/* Match to .modal background colour for full effect */
fill: #F7F0E8;
-webkit-animation: rotate 5s linear 1s infinite;
animation: rotate 5s linear 1s infinite;
}
@-webkit-keyframes rotate {
to {
-webkit-transform: rotate(360deg) translate3d(0,0,0);
transform: rotate(360deg) translate3d(0,0,0);
}
}
@keyframes rotate {
to {
-webkit-transform: rotate(360deg) translate3d(0,0,0);
transform: rotate(360deg) translate3d(0,0,0);
}
}
/* While modal is open */
/* Class added to body tag to prevent scroll
Note the body does not require class "-modal" */
body.-modal-open {
overflow: hidden;
}
/* Any tags classed with "-modal", when open, get "-modal-open" added */
/* Both of these are equivalent, best practice to use the attribute version which enforces accessibility */
.-modal[aria-hidden="true"] {
-webkit-filter: blur(4px);
filter: blur(4px);
}
.-modal.-modal-open {
-webkit-filter: blur(4px);
filter: blur(4px);
}
/* Generic helper style */
.u-margin2 {
margin: 2rem 0;
}
</style>
. . . and put this Javascript into your Footer Injection:
<script>
// https://john-dugan.com/javascript-debounce/
var debounce=function(e,t,n){var a;return function(){var r=this,i=arguments,o=function(){a=null,n||e.apply(r,i)},s=n&&!a;clearTimeout(a),a=setTimeout(o,t||200),s&&e.apply(r,i)}};
(function (window, d, debounce) {
"use strict";
// Modal pop-up window iframe version 4.1 22-08-2016
// v4.1 - Launch from an anchor link, button or any other not recommended object
// modal title text set by data-modalTitle, image alt or link text.
// Object with .-modal (modalName) with have .-modal-open appended === "-" + modalName + contentClass
// To do - under consideration:
// make lightbox the modal section and code around it?
// perhaps <section class=modal = lightbox
// <div =modal_inner = modal?
// lightbox doesnt need to be in keychain just add onclick to close
// To do:
// Option to set an ideal pop-up size eg an image
// - maintain the aspect ratio
// - centre it
// Requires:
// SVG definitions for: #icon-cross, #icon-loading
// External functions: debounce()
// Assumptions:
// First object in modal is the modal title
// Last object is the modal close link
// defaults
var modalName = "modal";
var lightboxClass = "lightbox";
var openClass = "-" + modalName + "-open";
//var modalDesc = "<kbd>tab</kbd> or <kbd>shift + tab</kbd> to move focus.";
var modalDesc = "Tab or Shift + Tab to move focus.";
var _setContentObjs = function (isModalOpen) {
var objs = d.getElementsByClassName("-" +modalName);
var i = objs.length;
while (i--) {
if (!!isModalOpen) {
objs[i].classList.add(openClass);
if (objs[i].tagName.toLowerCase !== "body") {
objs[i].setAttribute("aria-hidden", "true");
}
} else {
objs[i].classList.remove(openClass);
objs[i].removeAttribute("aria-hidden");
}
}
return !!isModalOpen;
};
var _closeModal = function (e) {
var count = e.target.count; // = lightbox, modal (ESC key), close btn
var modalSection = d.getElementById(modalName + "_" + count);
var lightbox = d.getElementById(modalName + "_" + count + "_" + lightboxClass);
var modalLink;
if (modalSection) {
modalSection.setAttribute("aria-hidden", "true");
lightbox.className = lightbox.className.replace(lightboxClass + "-on", "");
_setContentObjs(!modalSection.getAttribute("aria-hidden"));
modalLink = d.getElementById(modalSection.returnId);
d.body.classList.remove(openClass);
modalLink.focus();
}
};
var _getModalSize = function (modalSection) {
var clone = modalSection.cloneNode(true);
var size = {};
clone.className = modalName;
clone.setAttribute("style", "position:fixed;visibility:hidden;transform: none");
modalSection.parentElement.appendChild(clone);
size.width = clone.clientWidth; // more performant than getBoundingClientRect
size.height = clone.clientHeight; // more performant than getBoundingClientRect
modalSection.parentElement.removeChild(clone);
return size;
};
var _resizeIframes = function () {
var size;
var iframes;
var ii;
var modals = d.getElementsByClassName(modalName);
var i = modals.length;
while (i--) {
size = _getModalSize(modals[i]);
iframes = modals[i].getElementsByClassName(modalName + "_iframe");
ii = iframes.length;
while (ii--) {
iframes[ii].width = size.width;
iframes[ii].height = size.height;
}
}
};
var _addIframe = function (modalSection) {
var size;
var close_lnk;
var frames = modalSection.getElementsByClassName(modalName + "_iframe");
var iframe;
if (!frames[0]) {
iframe = d.createElement("iframe");
// Don't display iframe until it's content is ready
iframe.addEventListener("load", function () {
iframe.classList.add(modalName + "_iframe-on");
}, false);
iframe.src = modalSection.modalSrc;
iframe.className = modalName + "_iframe";
size = _getModalSize(modalSection);
iframe.width = size.width;
iframe.height = size.height;
iframe.setAttribute("frameborder", 0);
iframe.setAttribute("allowfullscreen", true);
// Add iframe before the close button
close_lnk = d.getElementById(modalName + "_" + modalSection.count + "_lnk_close");
modalSection.insertBefore(iframe, close_lnk);
}
};
var _getTarget = function (obj) {
var target = obj;
var isBodyTag = obj.tagName.toLowerCase() === "body";
if (isBodyTag) {
return false;
}
if (!obj.modalSrc) {
target = _getTarget(obj.parentElement);
}
return target;
}
var _openModal = function (e) {
e.preventDefault();
var target = _getTarget(e.target);
if (target) {
var count = target.count;
var tempId = modalName + "_" + count;
var tempLightboxClass = modalName + "_" + lightboxClass;
var modalSection = d.getElementById(tempId);
var lightbox = d.getElementById(tempId + "_" + lightboxClass);
if (modalSection && lightbox) {
if (!lightbox.className.match(tempLightboxClass + "-on")) {
lightbox.className += " " + tempLightboxClass + "-on";
}
modalSection.setAttribute("aria-hidden", "false");
_addIframe(modalSection);
_setContentObjs(!!modalSection.getAttribute("aria-hidden"));
d.body.classList.add(openClass);
d.getElementById(modalName + "_" + count + "_title").focus();
}
}
};
var _keydown_openerObj = function (e) {
// enter or space from the opener object
if (e.which === 13 || e.which === 32) {
e.preventDefault();
_openModal(e);
}
};
var _addOpenModalLinkAttr = function (modalLink) {
modalLink.id = modalLink.id || "modal_" + modalLink.count + "_lnk";
modalLink.setAttribute("aria-controls", modalName + "_" + modalLink.count);
// test if it's not a button
var tag = modalLink.tagName.toLowerCase();
if (tag !== "button") {
modalLink.setAttribute("aria-role", "button");
modalLink.addEventListener("keydown", _keydown_openerObj, false);
}
// click only requires space and enter activtion too
if (tag !== "a" || "button") {
modalLink.tabIndex = 0;
}
modalLink.addEventListener("click", _openModal, false);
};
var _keydown_modal = function (e) {
var target = e.target;
// ESC key on anything actionable
if (e.which === 27) {
_closeModal(e);
}
// tab key and shift on the h1
if (e.which === 9 && e.shiftKey) {
if (target.classList.contains(modalName + "_title")) {
e.preventDefault();
//focus on last object in modal (close btn)
d.getElementById(modalName + "_" + e.target.count + "_lnk_close").focus();
}
}
// tab key and not shift on the close link.
if (e.which === 9 && !e.shiftKey) {
if (target.classList.contains(modalName + "_lnk-close")) {
e.preventDefault();
//focus on first object in modal - or should it be the modal? Requires testing
d.getElementById(modalName + "_" + e.target.count + "_title").focus();
}
}
// enter or space on the close link - why again??
if (e.which === 13 || e.which === 32) {
if (target.classList.contains(modalName + "_lnk-close")) {
e.preventDefault();
_closeModal(e);
}
}
};
var _getTitleText = function (modalLink) {
var alt = "";
var imgs = modalLink.getElementsByTagName("img");
if (imgs && imgs[0]) {
alt = imgs[0].alt;
}
return modalLink.getAttribute("data-modalTitle") || alt || modalLink.textContent;
};
var _getModalTitle = function (modalLink) {
var title = d.createElement("h1");
title.id = modalName + "_" + modalLink.count + "_title";
title.className = modalName + "_title";
title.tabIndex = 0;
title.textContent = _getTitleText(modalLink);
title.count = modalLink.count;
title.addEventListener("keydown", _keydown_modal, false);
return title;
};
var _getModalSVG = function (icon, clss, title) {
var svg = d.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.classList.add(clss);
if (title) {
var t = d.createElementNS("http://www.w3.org/2000/svg", "title");
t.textContent = title;
svg.appendChild(t);
}
var use = d.createElementNS("http://www.w3.org/2000/svg", "use");
use.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#" + icon);
svg.appendChild(use);
return svg;
};
var _getModalDesc = function (modalLink) {
var desc = d.createElement("p");
desc.id = modalName + "_" + modalLink.count + "_desc";
desc.className = modalName + "_desc";
desc.tabIndex = 0;
desc.innerHTML = modalLink.getAttribute("data-modalDesc") || modalDesc;
desc.count = modalLink.count;
desc.addEventListener("keydown", _keydown_modal, false);
return desc;
};
var _getModalCloseLink = function (modalLink) {
var link = d.createElement("a");
link.id = modalName + "_" + modalLink.count + "_lnk_close";
link.className = modalName + "_lnk-close";
link.tabIndex = 0;
link.appendChild(_getModalSVG("icon-cross", "svg-close", "Close modal"));
link.count = modalLink.count;
link.addEventListener("click", _closeModal, false);
link.addEventListener("keydown", _keydown_modal, false);
return link;
};
var _addModalSection = function(modalLink) {
var section = d.createElement("section");
section.id = modalName + "_" + modalLink.count;
section.count = modalLink.count;
section.returnId = modalLink.id;
section.className = modalName;
section.setAttribute("aria-hidden", "true");
// should be on the activating link?
section.setAttribute("aria-labelledby", modalName +"_" + modalLink.count + "_title");
section.setAttribute("aria-describedby", modalName +"_" + modalLink.count + "_desc");
section.setAttribute("role", "dialog");
section.modalSrc = modalLink.modalSrc;
section.appendChild(_getModalTitle(modalLink));
section.appendChild(_getModalSVG("icon-loading", "svg-loading", "Loading"));
section.appendChild(_getModalSVG("icon-loading", "svg-loading2", ""));
section.appendChild(_getModalDesc(modalLink));
section.appendChild(_getModalCloseLink(modalLink));
d.body.appendChild(section);
};
var _addLightbox = function (modalLink) {
var count = modalLink.count;
var lightboxDiv = d.createElement("div");
lightboxDiv.id = modalName + "_" + count + "_" + lightboxClass;
lightboxDiv.className = modalName + "_" + lightboxClass;
lightboxDiv.count = count;
lightboxDiv.returnId = modalLink.id;
// mouse / touch only
lightboxDiv.addEventListener("click", _closeModal, false);
d.body.appendChild(lightboxDiv);
};
var configuration = function (cfg) {
modalName = cfg.modalName || modalName;
lightboxClass = cfg.lightboxClass || lightboxClass;
// any object with a class -modal will have the class -modal-open added when the modal is open.
//openClass = "-" + modalName + (cfg.openClass || "-open");
openClass = cfg.openClass ? "-" + modalName + cfg.openClass : openClass;
};
var initialise = function (cfg) {
configuration(cfg);
var modalSrc;
var dataModals = d.querySelectorAll("[data-" + modalName + "]");
if (dataModals) {
var i = dataModals.length;
while (i--) {
// Link href and iframe src are not always the same!
modalSrc = false;
// use the href
if (dataModals[i].hasAttribute("href")) {
modalSrc = dataModals[i].href;
}
// overwrite src with data-modal content when available
if (dataModals[i].getAttribute("data-modal").length) {
modalSrc = dataModals[i].getAttribute("data-modal");
}
if (modalSrc) {
dataModals[i].modalSrc = modalSrc;
dataModals[i].count = i;
_addOpenModalLinkAttr(dataModals[i]);
_addModalSection(dataModals[i]);
_addLightbox(dataModals[i]);
}
}
window.addEventListener("resize", debounce(_resizeIframes, 250, false));
}
};
initialise({
modalName : "modal", // class name of modal, also used as the base for all classes used except on SVGs.
openClass : "-open", // is default ("-" + modaName automatically prepended)
lightboxClass : "lightbox" // is default (modaName + "_" automatically prepended)
});
}(window, document, debounce));
</script>
From Accessible modal dialog pop-up iframe (v4)
Uses an anchor to launch a modal pop-up which is then populated with an iframe
. This version removed the requirement for an image, also allows modal title and description to be user defined.
Production ready (encapuslated with external configuration and instantiation) version available: Modal Dialog demo. For minified JavaScript see source.
Alternatives...
Tingle - Tingle is a simple modal plugin written in pure JavaScript (Source code on GitHub).