bookclub-advr

DSLC Advanced R Book Club
git clone https://git.eamoncaddigan.net/bookclub-advr.git
Log | Files | Refs | README | LICENSE

support.js (14339B)


      1 // catch all plugin for various quarto features
      2 window.QuartoSupport = function () {
      3   function isPrintView() {
      4     return /print-pdf/gi.test(window.location.search) || /view=print/gi.test(window.location.search);
      5   }
      6 
      7   // helper for theme toggling
      8   function toggleBackgroundTheme(el, onDarkBackground, onLightBackground) {
      9     if (onDarkBackground) {
     10       el.classList.add('has-dark-background')
     11     } else {
     12       el.classList.remove('has-dark-background')
     13     }
     14     if (onLightBackground) {
     15       el.classList.add('has-light-background')
     16     } else {
     17       el.classList.remove('has-light-background')
     18     }
     19   }
     20 
     21   // implement controlsAudo
     22   function controlsAuto(deck) {
     23     const config = deck.getConfig();
     24     if (config.controlsAuto === true) {
     25       const iframe = window.location !== window.parent.location;
     26       const localhost =
     27         window.location.hostname === "localhost" ||
     28         window.location.hostname === "127.0.0.1";
     29       deck.configure({
     30         controls:
     31           (iframe && !localhost) ||
     32           (deck.hasVerticalSlides() && config.navigationMode !== "linear"),
     33       });
     34     }
     35   }
     36 
     37   // helper to provide event handlers for all links in a container
     38   function handleLinkClickEvents(deck, container) {
     39     Array.from(container.querySelectorAll("a")).forEach((el) => {
     40       const url = el.getAttribute("href");
     41       if (/^(http|www)/gi.test(url)) {
     42         el.addEventListener(
     43           "click",
     44           (ev) => {
     45             const fullscreen = !!window.document.fullscreen;
     46             const dataPreviewLink = el.getAttribute("data-preview-link");
     47 
     48             // if there is a local specifcation then use that
     49             if (dataPreviewLink) {
     50               if (
     51                 dataPreviewLink === "true" ||
     52                 (dataPreviewLink === "auto" && fullscreen)
     53               ) {
     54                 ev.preventDefault();
     55                 deck.showPreview(url);
     56                 return false;
     57               }
     58             } else {
     59               const previewLinks = !!deck.getConfig().previewLinks;
     60               const previewLinksAuto =
     61                 deck.getConfig().previewLinksAuto === true;
     62               if (previewLinks == true || (previewLinksAuto && fullscreen)) {
     63                 ev.preventDefault();
     64                 deck.showPreview(url);
     65                 return false;
     66               }
     67             }
     68 
     69             // if the deck is in an iframe we want to open it externally
     70             // (don't do this when in vscode though as it has its own
     71             // handler for opening links externally that will be play)
     72             const iframe = window.location !== window.parent.location;
     73             if (
     74               iframe &&
     75               !window.location.search.includes("quartoPreviewReqId=")
     76             ) {
     77               ev.preventDefault();
     78               ev.stopImmediatePropagation();
     79               window.open(url, "_blank");
     80               return false;
     81             }
     82 
     83             // if the user has set data-preview-link to "auto" we need to handle the event
     84             // (because reveal will interpret "auto" as true)
     85             if (dataPreviewLink === "auto") {
     86               ev.preventDefault();
     87               ev.stopImmediatePropagation();
     88               const target =
     89                 el.getAttribute("target") ||
     90                 (ev.ctrlKey || ev.metaKey ? "_blank" : "");
     91               if (target) {
     92                 window.open(url, target);
     93               } else {
     94                 window.location.href = url;
     95               }
     96               return false;
     97             }
     98           },
     99           false
    100         );
    101       }
    102     });
    103   }
    104 
    105   // implement previewLinksAuto
    106   function previewLinksAuto(deck) {
    107     handleLinkClickEvents(deck, deck.getRevealElement());
    108   }
    109 
    110   // apply styles
    111   function applyGlobalStyles(deck) {
    112     if (deck.getConfig()["smaller"] === true) {
    113       const revealParent = deck.getRevealElement();
    114       revealParent.classList.add("smaller");
    115     }
    116   }
    117 
    118   // add logo image
    119   function addLogoImage(deck) {
    120     const revealParent = deck.getRevealElement();
    121     const logoImg = document.querySelector(".slide-logo");
    122     if (logoImg) {
    123       revealParent.appendChild(logoImg);
    124       revealParent.classList.add("has-logo");
    125     }
    126   }
    127 
    128   // tweak slide-number element
    129   function tweakSlideNumber(deck) {
    130     deck.on("slidechanged", function (ev) {
    131       // No slide number in scroll view
    132       if (deck.isScrollView()) { return }
    133       const revealParent = deck.getRevealElement();
    134       const slideNumberEl = revealParent.querySelector(".slide-number");
    135       const slideBackground = Reveal.getSlideBackground(ev.currentSlide);
    136       const onDarkBackground = slideBackground.classList.contains('has-dark-background')
    137       const onLightBackground = slideBackground.classList.contains('has-light-background')
    138       toggleBackgroundTheme(slideNumberEl, onDarkBackground, onLightBackground);
    139     })
    140   }
    141 
    142   // add footer text
    143   function addFooter(deck) {
    144     const revealParent = deck.getRevealElement();
    145     const defaultFooterDiv = document.querySelector(".footer-default");
    146     // Set per slide footer if any defined, 
    147     // or show default unless data-footer="false" for no footer on this slide
    148     const setSlideFooter = (ev, defaultFooterDiv) => {
    149       const currentSlideFooter = ev.currentSlide.querySelector(".footer");
    150       const onDarkBackground = deck.getSlideBackground(ev.currentSlide).classList.contains('has-dark-background')
    151       const onLightBackground = deck.getSlideBackground(ev.currentSlide).classList.contains('has-light-background')
    152       if (currentSlideFooter) {
    153         defaultFooterDiv.style.display = "none";
    154         const slideFooter = currentSlideFooter.cloneNode(true);
    155         handleLinkClickEvents(deck, slideFooter);
    156         deck.getRevealElement().appendChild(slideFooter);
    157         toggleBackgroundTheme(slideFooter, onDarkBackground, onLightBackground)
    158       } else if (ev.currentSlide.getAttribute("data-footer") === "false") {
    159         defaultFooterDiv.style.display = "none";
    160       } else {
    161         defaultFooterDiv.style.display = "block";
    162         toggleBackgroundTheme(defaultFooterDiv, onDarkBackground, onLightBackground)
    163       }
    164     }
    165     if (defaultFooterDiv) {
    166       // move default footnote to the div.reveal element
    167       revealParent.appendChild(defaultFooterDiv);
    168       handleLinkClickEvents(deck, defaultFooterDiv);
    169 
    170       if (!isPrintView()) {
    171         // Ready even is needed so that footer customization applies on first loaded slide
    172         deck.on('ready', (ev) => {
    173           // Set footer (custom, default or none)
    174           setSlideFooter(ev, defaultFooterDiv)
    175         });
    176         // Any new navigated new slide will get the custom footnote check
    177         deck.on("slidechanged", function (ev) {
    178           // Remove presentation footer defined by previous slide
    179           const prevSlideFooter = document.querySelector(
    180             ".reveal > .footer:not(.footer-default)"
    181           );
    182           if (prevSlideFooter) {
    183             prevSlideFooter.remove();
    184           }
    185           // Set new one (custom, default or none)
    186           setSlideFooter(ev, defaultFooterDiv)
    187         });
    188       }
    189     }
    190   }
    191 
    192   // add chalkboard buttons
    193   function addChalkboardButtons(deck) {
    194     const chalkboard = deck.getPlugin("RevealChalkboard");
    195     if (chalkboard && !isPrintView()) {
    196       const revealParent = deck.getRevealElement();
    197       const chalkboardDiv = document.createElement("div");
    198       chalkboardDiv.classList.add("slide-chalkboard-buttons");
    199       if (document.querySelector(".slide-menu-button")) {
    200         chalkboardDiv.classList.add("slide-menu-offset");
    201       }
    202       // add buttons
    203       const buttons = [
    204         {
    205           icon: "easel2",
    206           title: "Toggle Chalkboard (b)",
    207           onclick: chalkboard.toggleChalkboard,
    208         },
    209         {
    210           icon: "brush",
    211           title: "Toggle Notes Canvas (c)",
    212           onclick: chalkboard.toggleNotesCanvas,
    213         },
    214       ];
    215       buttons.forEach(function (button) {
    216         const span = document.createElement("span");
    217         span.title = button.title;
    218         const icon = document.createElement("i");
    219         icon.classList.add("fas");
    220         icon.classList.add("fa-" + button.icon);
    221         span.appendChild(icon);
    222         span.onclick = function (event) {
    223           event.preventDefault();
    224           button.onclick();
    225         };
    226         chalkboardDiv.appendChild(span);
    227       });
    228       revealParent.appendChild(chalkboardDiv);
    229       const config = deck.getConfig();
    230       if (!config.chalkboard.buttons) {
    231         chalkboardDiv.classList.add("hidden");
    232       }
    233 
    234       // show and hide chalkboard buttons on slidechange
    235       deck.on("slidechanged", function (ev) {
    236         const config = deck.getConfig();
    237         let buttons = !!config.chalkboard.buttons;
    238         const slideButtons = ev.currentSlide.getAttribute(
    239           "data-chalkboard-buttons"
    240         );
    241         if (slideButtons) {
    242           if (slideButtons === "true" || slideButtons === "1") {
    243             buttons = true;
    244           } else if (slideButtons === "false" || slideButtons === "0") {
    245             buttons = false;
    246           }
    247         }
    248         if (buttons) {
    249           chalkboardDiv.classList.remove("hidden");
    250         } else {
    251           chalkboardDiv.classList.add("hidden");
    252         }
    253       });
    254     }
    255   }
    256 
    257   function handleTabbyClicks() {
    258     const tabs = document.querySelectorAll(".panel-tabset-tabby > li > a");
    259     for (let i = 0; i < tabs.length; i++) {
    260       const tab = tabs[i];
    261       tab.onclick = function (ev) {
    262         ev.preventDefault();
    263         ev.stopPropagation();
    264         return false;
    265       };
    266     }
    267   }
    268 
    269   function fixupForPrint(deck) {
    270     if (isPrintView()) {
    271       const slides = deck.getSlides();
    272       slides.forEach(function (slide) {
    273         slide.removeAttribute("data-auto-animate");
    274       });
    275       window.document.querySelectorAll(".hljs").forEach(function (el) {
    276         el.classList.remove("hljs");
    277       });
    278       window.document.querySelectorAll(".hljs-ln-code").forEach(function (el) {
    279         el.classList.remove("hljs-ln-code");
    280       });
    281     }
    282   }
    283 
    284   // dispatch for htmlwidgets
    285   // they use slideenter event to trigger resize
    286   const fireSlideEnter = () => {
    287     const event = window.document.createEvent("Event");
    288     event.initEvent("slideenter", true, true);
    289     window.document.dispatchEvent(event);
    290   };
    291 
    292   // dispatch for shiny
    293   // they use BS shown and hidden events to trigger rendering
    294   const distpatchShinyEvents = (previous, current) => {
    295     if (window.jQuery) {
    296       if (previous) {
    297         window.jQuery(previous).trigger("hidden");
    298       }
    299       if (current) {
    300         window.jQuery(current).trigger("shown");
    301       }
    302     }
    303   };
    304 
    305   function handleSlideChanges(deck) {
    306 
    307     const fireSlideChanged = (previousSlide, currentSlide) => {
    308       fireSlideEnter();
    309       distpatchShinyEvents(previousSlide, currentSlide);
    310     };
    311 
    312     deck.on("slidechanged", function (event) {
    313       fireSlideChanged(event.previousSlide, event.currentSlide);
    314     });
    315   }
    316 
    317   function handleTabbyChanges() {
    318     const fireTabChanged = (previousTab, currentTab) => {
    319       fireSlideEnter()
    320       distpatchShinyEvents(previousTab, currentTab);
    321     };
    322     document.addEventListener("tabby", function(event) {
    323       fireTabChanged(event.detail.previousTab, event.detail.tab);
    324     }, false);
    325   }
    326 
    327   function workaroundMermaidDistance(deck) {
    328     if (window.document.querySelector("pre.mermaid-js")) {
    329       const slideCount = deck.getTotalSlides();
    330       deck.configure({
    331         mobileViewDistance: slideCount,
    332         viewDistance: slideCount,
    333       });
    334     }
    335   }
    336 
    337   function handleWhiteSpaceInColumns(deck) {
    338     for (const outerDiv of window.document.querySelectorAll("div.columns")) {
    339       // remove all whitespace text nodes
    340       // whitespace nodes cause the columns to be misaligned
    341       // since they have inline-block layout
    342       // 
    343       // Quarto emits no whitespace nodes, but third-party tooling
    344       // has bugs that can cause whitespace nodes to be emitted.
    345       // See https://github.com/quarto-dev/quarto-cli/issues/8382
    346       for (const node of outerDiv.childNodes) {
    347         if (node.nodeType === 3 && node.nodeValue.trim() === "") {
    348           outerDiv.removeChild(node);
    349         }
    350       }
    351     }
    352   }
    353 
    354   function cleanEmptyAutoGeneratedContent(deck) {
    355     const div = document.querySelector('div.quarto-auto-generated-content')
    356     if (div && div.textContent.trim() === '') {
    357       div.remove()
    358     }
    359   }
    360 
    361   // FIXME: Possibly remove this wrapper class when upstream trigger is fixed
    362   // https://github.com/hakimel/reveal.js/issues/3688
    363   // Currently, scrollActivationWidth needs to be unset for toggle to work
    364   class ScrollViewToggler {
    365     constructor(deck) {
    366       this.deck = deck;
    367       this.oldScrollActivationWidth = deck.getConfig()['scrollActivationWidth'];
    368     }
    369   
    370     toggleScrollViewWrapper() {
    371       if (this.deck.isScrollView() === true) {
    372         this.deck.configure({ scrollActivationWidth: this.oldScrollActivationWidth });
    373         this.deck.toggleScrollView(false);
    374       } else if (this.deck.isScrollView() === false) {
    375         this.deck.configure({ scrollActivationWidth: null });
    376         this.deck.toggleScrollView(true);
    377       }
    378     }
    379   }
    380 
    381   let scrollViewToggler;
    382 
    383   function installScollViewKeyBindings(deck) {
    384 		var config = deck.getConfig();
    385 		var shortcut = config.scrollViewShortcut || 'R';
    386 		Reveal.addKeyBinding({
    387 			keyCode: shortcut.toUpperCase().charCodeAt( 0 ),
    388 			key: shortcut.toUpperCase(),
    389 			description: 'Scroll View Mode'
    390 		}, () => { scrollViewToggler.toggleScrollViewWrapper() } );
    391 	}
    392 
    393   return {
    394     id: "quarto-support",
    395     init: function (deck) {
    396       scrollViewToggler = new ScrollViewToggler(deck);
    397       controlsAuto(deck);
    398       previewLinksAuto(deck);
    399       fixupForPrint(deck);
    400       applyGlobalStyles(deck);
    401       addLogoImage(deck);
    402       tweakSlideNumber(deck);
    403       addFooter(deck);
    404       addChalkboardButtons(deck);
    405       handleTabbyClicks();
    406       handleTabbyChanges();
    407       handleSlideChanges(deck);
    408       workaroundMermaidDistance(deck);
    409       handleWhiteSpaceInColumns(deck);
    410       installScollViewKeyBindings(deck);
    411       // should stay last
    412       cleanEmptyAutoGeneratedContent(deck);
    413     },
    414     // Export for adding in menu
    415     toggleScrollView: function() {
    416       scrollViewToggler.toggleScrollViewWrapper();
    417     }
    418   };
    419 };