RWEP/_extensions/martinomagnifico/simplemenu/simplemenu.js

645 lines
22 KiB
JavaScript

/*****************************************************************
* @author: Martijn De Jongh (Martino), martijn.de.jongh@gmail.com
* https://github.com/Martinomagnifico
*
* Simplemenu.js for Reveal.js
* Version 2.0.1
*
* @license
* MIT licensed
*
* Thanks to:
* - Hakim El Hattab, Reveal.js
******************************************************************/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Simplemenu = factory());
})(this, (function () { 'use strict';
const Plugin = () => {
let options = {};
const vars = {};
const sections = {};
const mainArray = [];
let autoListItems = [];
let manualListItems = [];
const debugLog = text => {
if (options.debug) console.log(text);
};
const isObject = item => {
return item && typeof item === 'object' && !Array.isArray(item);
};
const mergeDeep = function (target) {
for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
sources[_key - 1] = arguments[_key];
}
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, {
[key]: {}
});
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, {
[key]: source[key]
});
}
}
}
return mergeDeep(target, ...sources);
};
const selectionArray = (container, selectors) => {
let selections = container.querySelectorAll(selectors);
let selectionarray = Array.prototype.slice.call(selections);
return selectionarray;
};
const pluginPath = filename => {
let path;
let pluginScript = document.querySelector(`script[src$="${filename}"]`);
if (pluginScript) {
path = pluginScript.getAttribute("src").slice(0, -1 * filename.length);
} else {
path = (typeof document === 'undefined' && typeof location === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('simplemenu.js', document.baseURI).href)).slice(0, (typeof document === 'undefined' && typeof location === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('simplemenu.js', document.baseURI).href)).lastIndexOf('/') + 1);
}
return path;
};
const isBefore = (a, b) => {
var all = document.getElementsByTagName('*');
for (var i = 0; i < all.length; ++i) {
if (all[i] === a) return true;else if (all[i] === b) return false;
}
};
const isStack = section => {
let isStack = false;
for (let i = 0; i < section.childNodes.length; i++) {
if (section.childNodes[i].tagName == "SECTION") {
isStack = true;
break;
}
}
return isStack;
};
const createNode = thehtml => {
const fragment = document.createRange().createContextualFragment(thehtml);
return fragment.firstElementChild;
};
const loadStyle = (url, type, callback) => {
let head = document.querySelector('head');
let style = document.createElement('link');
style.rel = 'stylesheet';
style.href = url;
let finish = () => {
if (typeof callback === 'function') {
callback.call();
callback = null;
}
};
style.onload = finish;
style.onreadystatechange = function () {
if (this.readyState === 'loaded') {
finish();
}
};
head.appendChild(style);
};
const checkOccurrence = (array, element) => {
let counter = 0;
for (let i = 0; i <= array.length; i++) {
if (array[i] == element) {
counter++;
}
}
return counter;
};
const menuArray = () => {
const matchString = vars.matchString;
let menulist = selectionArray(vars.viewport, `.${options.menuclass}`) ? selectionArray(vars.viewport, `.${options.menuclass}`) : [];
let automenus = [];
let manualmenus = [];
if (menulist.length) {
menulist.forEach(menu => {
if (menu.getElementsByTagName('li').length < 1) {
menu.setAttribute('data-simplemenu-auto', '');
automenus.push(menu);
} else {
if (options.selectby == "data-name" || options.selectby == "name") {
let existingListItems = selectionArray(menu, `.${options.menuclass} ${options.activeelement}`);
existingListItems.forEach(listItem => {
if (!listItem.dataset[matchString]) {
let content = listItem.textContent || listItem.querySelector('a').textContent;
listItem.setAttribute(`data-${matchString}`, content);
}
});
}
manualmenus.push(menu);
}
});
return {
automenus: automenus,
manualmenus: manualmenus
};
} else {
return false;
}
};
const setScale = revealScale => {
let totalScale = revealScale * vars.userScale;
vars.viewport.style.setProperty('--simplemenu-scale', totalScale.toFixed(3));
};
const moveRevealUI = (curUiEl, newUiEl) => {
let newUiElClassList = newUiEl.classList;
newUiEl.parentNode.replaceChild(curUiEl, newUiEl);
curUiEl.classList = newUiElClassList;
};
const getRevealUI = () => {
let revealUIs = ['controls', 'slide-number'];
revealUIs.forEach(uielement => {
let curUiEl = vars.deck.getRevealElement().querySelector(`.reveal > .${uielement}`);
let newUiEl = vars.deck.getRevealElement().querySelector(`.reveal > * .${uielement}`);
if (curUiEl && newUiEl) {
moveRevealUI(curUiEl, newUiEl);
}
});
};
function copyDataAttributes(source, target) {
[...source.attributes].filter(attr => attr.nodeName.indexOf('data') > -1).forEach(attr => {
target.setAttribute(attr.nodeName, attr.nodeValue);
});
}
const prepareSlides = () => {
debugLog("Preparing slides");
sections.all = selectionArray(vars.viewport, "section");
sections.all.forEach(section => {
// In Markdown environments, setting a data-name of a stack is not directly possible.
// Satting a data-stack-name on the first child solves this.
if (!section.parentNode.dataset.name && section.dataset && section.dataset.stackName) {
section.parentNode.dataset.name = section.dataset.stackName;
} // If a section has a data-sm='none', it will also remove the data-name.
if (section.dataset && section.dataset[vars.matchString] && section.dataset[vars.matchString] == "false" && section.dataset.name) {
delete section.dataset.name;
}
}); // Get all of the kinds of sections
sections.top = sections.all.filter(section => section.parentNode.classList.contains('slides') && !(section.dataset[vars.matchString] && section.dataset[vars.matchString] == "false"));
sections.named = sections.top.filter(section => section.dataset.name || section.getAttribute('name'));
sections.namedvisible = sections.named.filter(section => section.dataset.visibility != "hidden"); // Go through all the named sections
let namedsectionMatches = [];
sections.named.forEach(namedsection => {
// The 'name' attribute is also allowed.
let matchName = namedsection.dataset.name || namedsection.getAttribute('name'); // Named sections can have the same name, but should then be differentiated.
namedsectionMatches.push(matchName);
let dupsBefore = matchName && checkOccurrence(namedsectionMatches, matchName) > 1 ? `-${checkOccurrence(namedsectionMatches, matchName)}` : null;
let match = dupsBefore ? matchName + dupsBefore : matchName; // We set the name of the match as a data-attribute
namedsection.setAttribute(`data-${vars.matchString}`, match); // If the (named) section is not a stack and does not have an ID, we need to give it one.
if (!isStack(namedsection) && !namedsection.id) {
// Note: Quarto will already have assigned an ID, but it may also have been done manually.
namedsection.id = match.toLowerCase().replace(/\W/g, '');
} else if (isStack(namedsection)) {
// Find the first (visible) section inside a stack.
let allsects = selectionArray(namedsection, `section`);
let allVisibleSects = allsects.filter(section => section.dataset.visibility != "hidden");
let firstChildSection = allVisibleSects[0];
if (firstChildSection && !firstChildSection.id) {
firstChildSection.id = match.toLowerCase().replace(/\W/g, '');
if (namedsection.id == firstChildSection.id) {
namedsection.removeAttribute('id');
}
}
}
});
let currentMatch = null;
let currentid = null; // Get all the sections that are actually slides
sections.regular = sections.all.filter(section => !isStack(section) && section.dataset.visibility != "hidden"); // Go through all the sections
sections.regular.forEach((section, i) => {
// Filling an array with the needed comparison information
let isChildSection = isStack(section.parentNode) && section.parentNode.tagName == "SECTION";
let theSection = isChildSection ? section.parentNode : section;
let dataname = theSection.dataset.name;
let name = theSection.getAttribute(`name`);
let dataintl = theSection.getAttribute(vars.langattribute) ? theSection.getAttribute(vars.langattribute) : null;
let parentid = section.parentNode.id ? section.parentNode.id : null;
let id = section.id ? section.id : isChildSection ? parentid : null;
let match = theSection.dataset[vars.matchString];
if (match) {
currentMatch = match;
}
if (id || match == "false") {
currentid = i;
}
if (options.flat) {
if (match != "false") {
match = currentMatch;
}
}
if (match == "false") {
match = null;
}
if (dataname == "false") {
dataname = null;
}
let sectionObject = {
index: i,
...(section && {
section
}),
...(dataname && {
dataname
}),
...(name && {
name
}),
...(id && {
id
}),
...(match && {
match
}),
...(currentid && {
currentid
}),
...(dataintl && {
dataintl
})
};
mainArray.push(sectionObject);
});
};
const prepareMenubars = () => {
debugLog("Preparing menubars");
let menubars = selectionArray(vars.viewport, `.${options.menubarclass}`) ? selectionArray(vars.viewport, `.${options.menubarclass}`) : [];
if (options.barhtml.header) {
// Generate header menubar
let bar = createNode(options.barhtml.header);
menubars.push(bar);
vars.slides.before(bar);
}
if (options.barhtml.footer) {
// Generate footer menubar
let bar = createNode(options.barhtml.footer);
menubars.push(bar);
vars.slides.after(bar);
}
if (menubars.length) {
// If menubar (pre-existing or just added):
setScale(vars.deck.getScale());
menubars.forEach((menubar, i) => {
let barLocation = isBefore(menubar, vars.slides) ? "top" : "bottom";
menubar.classList.add(barLocation);
if (!menubar.id) {
menubar.id = `${options.menubarclass}${barLocation}`;
}
menubar.classList.add("ready");
});
vars.menubars = menubars;
} else {
console.log("There are no menubars. You can still use Simplemenu to populate empty menus like in an Agenda or Table Of Contents.");
}
};
const prepareMenus = () => {
debugLog("Preparing menus");
let menus = menuArray();
if (!menus || menus && !menus.automenus) {
console.log("There are no menus. Please add one or more menus manually or through the 'barhtml' option.");
return;
}
if (menus.automenus.length >= 1 && sections.namedvisible.length >= 1) {
// There are empty menus. Autofill them.
let idArray = [];
const autoMenuLinks = sections.namedvisible.map(section => {
let match = section.dataset[vars.matchString];
let name = section.dataset.name || section.getAttribute(`name`) || section.id;
let id = section.id || name.toLowerCase().replace(/\W/g, '');
idArray.push(id);
if (vars.quarto) {
id = mainArray.find(item => item.match === match).id;
}
let duplicatesBefore = checkOccurrence(idArray, id) > 1 ? `-${checkOccurrence(idArray, id)}` : '';
let href = vars.quarto ? id : id + duplicatesBefore;
let smmatchString = ` data-${vars.matchString}="${match}"`;
let nameString = section.getAttribute(`name`) ? ` name="${section.getAttribute(`name`)}"` : '';
let intlString = section.getAttribute(vars.langattribute) ? ` ${vars.langattribute}="${section.getAttribute(vars.langattribute)}"` : '';
return `<li><a href="#/${href}"${intlString}${nameString}${smmatchString}>${name}</a></li>`;
}).reduce((combinedHTML, itemHTML) => {
let orderedHTML = vars.rtl ? itemHTML + combinedHTML : combinedHTML + itemHTML;
return orderedHTML;
});
menus.automenus.forEach(automenu => {
automenu.innerHTML = autoMenuLinks;
});
autoListItems = menus.automenus.map(menu => Array.from(menu.querySelectorAll(options.activeelement))).flat();
}
if (menus.manualmenus.length >= 1) {
// There are pre-existing menus. Fix link to ID if needed.
// Only get the listitems
manualListItems = menus.manualmenus.map(menu => Array.from(menu.querySelectorAll(options.activeelement))).flat();
manualListItems.forEach(listItem => {
// Get the anchorlinks
let linker = listItem.tagName == "a" ? listItem : listItem.querySelector('a');
let linkhref = linker.getAttribute('href');
if (linkhref === "#") {
let newLink = listItem.dataset[vars.matchString].toLowerCase().replace(/\W/g, '');
linker.href = `#/${newLink}`;
}
});
}
};
const preparePrint = () => {
const urlParams = new URLSearchParams(window.location.search);
const hasPrintParam = urlParams.has('print-pdf');
if (hasPrintParam) {
mainArray.forEach(item => {
let printSection = item.section;
let datainfo = document.createElement("div");
datainfo.classList.add("datainfo");
copyDataAttributes(printSection, datainfo);
let moreData = ['match', 'name', 'dataname', 'currentid', 'id', 'dataintl'];
moreData.forEach(moreDataItem => {
if (item[moreDataItem]) {
datainfo.dataset[moreDataItem] = item[moreDataItem];
}
});
printSection.appendChild(datainfo);
});
}
};
const prepare = resolve => {
prepareSlides();
prepareMenubars();
prepareMenus();
preparePrint();
return setTimeout(resolve, 0);
};
const compare = (listItem, section) => {
let menukind = listItem.parentNode.hasAttribute('data-simplemenu-auto') ? "auto" : "manual";
let sectionmatch = section.match ? section.match : null;
if (menukind == "manual") {
if (options.selectby == "id") {
sectionmatch = section.id ? section.id : section.currentid ? mainArray[section.currentid].id : null;
} else if (options.selectby == "name") {
sectionmatch = section.name;
} else {
sectionmatch = section.dataname;
}
}
if (sectionmatch) {
let menumatch = listItem.dataset[vars.matchString] || listItem.querySelector('a').dataset[vars.matchString];
if (options.selectby == "id" && menukind == "manual") {
let href = listItem.href || listItem.querySelector('a').href;
let lastHref = href ? href.substring(href.lastIndexOf("/") + 1) : '';
menumatch = lastHref;
}
if (options.selectby == "data-name" && menukind == "manual") {
sectionmatch = section.dataname ? section.dataname : null;
}
if (menumatch && menumatch == sectionmatch) {
listItem.classList.add(options.activeclass);
} else {
listItem.classList.remove(options.activeclass);
}
} else {
listItem.classList.remove(options.activeclass);
}
};
const checkSlidesNormal = event => {
const index = sections.regular.indexOf(event.currentSlide);
let section = mainArray[index];
autoListItems.filter(listItem => {
compare(listItem, section);
});
manualListItems.filter(listItem => {
compare(listItem, section);
});
};
const checkSlidesPDF = event => {
let pdfPages = selectionArray(vars.viewport, '.slides .pdf-page'); // Check if any menubar has a slide number
let anyMenubarHasSlidenumber = false;
if (vars.menubars) {
vars.menubars.forEach(menubar => {
anyMenubarHasSlidenumber = !!menubar.getElementsByClassName("slide-number");
});
}
pdfPages.forEach(pdfPage => {
// The original section has gone, so we rebuild it with the saved data-attributes
let datainfo = pdfPage.getElementsByClassName("datainfo")[0];
let section = {};
section.name = datainfo.dataset.name;
section.dataname = datainfo.dataset.dataname;
section.currentid = datainfo.dataset.currentid;
section.match = datainfo.dataset.match;
section.id = datainfo.dataset.id;
if (datainfo.dataset.state) {
let newClasses = datainfo.dataset.state.split(" ");
newClasses.forEach(newClass => {
pdfPage.classList.add(newClass);
let vp = vars.deck.getRevealElement().closest(".reveal-viewport");
vp.classList.remove(newClass);
});
} // If any menubar has a slide number, turn the original one on this slide off
if (anyMenubarHasSlidenumber && pdfPage.getElementsByClassName("slide-number").length > 0) {
pdfPage.getElementsByClassName("slide-number")[0].style.display = "none";
}
if (vars.menubars) {
vars.menubars.forEach(menubar => {
let bar = menubar.cloneNode(true);
pdfPage.appendChild(bar);
let listItems = selectionArray(bar, `.${options.menuclass} ${options.activeelement}`);
listItems.forEach(listItem => {
compare(listItem, section);
}); // If there is a slidenumber in the menu,
let newSN = pdfPage.querySelector(`.${options.menubarclass} .slide-number`);
let oldSN = pdfPage.querySelector(`:scope > .slide-number`);
if (newSN && oldSN) {
// ...then fill it with the current (total) slidenumber.
newSN.textContent = oldSN.textContent;
}
});
}
});
if (vars.menubars) {
vars.menubars.forEach(menubar => {
menubar.parentNode.removeChild(menubar);
});
}
};
const chapterize = event => {
if (event && event.type == "ready") {
debugLog(mainArray);
getRevealUI();
}
if (event && (event.type == "ready" || event.type == "slidechanged")) {
checkSlidesNormal(event);
}
if (event && event.type == "pdf-ready") {
checkSlidesPDF();
}
};
const simpleMenu = (deck, options, es5Filename) => {
deck.configure({
hash: true
});
vars.deck = deck;
vars.viewport = deck.getRevealElement().tagName == "BODY" ? document : deck.getRevealElement();
vars.slides = deck.getSlidesElement();
vars.langattribute = deck.getConfig().internation ? deck.getConfig().internation.langattribute ? deck.getConfig().internation.langattribute : "data-i18n" : false;
vars.rtl = deck.getConfig().rtl;
vars.quarto = document.querySelector('[name=generator]') && document.querySelector('[name=generator]').content.includes("quarto") ? true : false;
vars.matchString = "sm";
vars.userScale = options.scale;
deck.addEventListener('ready', chapterize, false);
deck.addEventListener('slidechanged', chapterize, false);
deck.addEventListener('pdf-ready', chapterize, false);
deck.addEventListener('resize', _ref => {
let {
scale
} = _ref;
return setScale(scale);
}, false);
const SimplemenuStylePath = options.csspath ? options.csspath : `${pluginPath(es5Filename)}simplemenu.css` || 'plugin/simplemenu/simplemenu.css';
return new Promise(resolve => {
if (options.csspath === false) {
return prepare(resolve);
} else {
loadStyle(SimplemenuStylePath, 'stylesheet', async () => prepare(resolve));
}
});
};
const init = deck => {
let defaultOptions = {
menubarclass: 'menubar',
menuclass: 'menu',
activeclass: 'active',
activeelement: 'li',
selectby: 'id',
barhtml: {
header: '',
footer: ''
},
flat: false,
scale: 0.67,
csspath: ''
};
options = deck.getConfig().simplemenu || {};
options = mergeDeep(defaultOptions, options);
let wronginputs = false;
let warning = '';
if (options.selectby !== "id" && options.selectby !== "data-name" && options.selectby !== "name") {
wronginputs = true;
warning = 'The selectby option can be only "id", "data-name" or "name".';
}
if (wronginputs) {
console.log('Simplemenu did not load:');
console.log(warning);
return false;
}
return simpleMenu(deck, options, "simplemenu.js");
};
return {
id: 'simplemenu',
init: init
};
};
return Plugin;
}));