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 };