Source: waiter/waiter.js

import "shiny";
import "jquery";
import { getDimensions } from "../dimensions";
import { hideRecalculate } from "../recalculate";
import { setWaiterHiddenInput, setWaiterShownInput } from "./callbacks";
import { createOverlay } from "./overlay";

import "./css/css-spinners.css";
import "./css/custom.css";
import "./css/devloop.css";
import "./css/loaders.css";
import "./css/spinbolt.css";
import "./css/spinkit.css";
import "./css/spinners.css";
import "./css/waiter.css";

// elements to hide on recomputed
var waiterToHideOnRender = new Map();
var waiterToFadeout = new Map();
var waiterToHideOnError = new Map();
var waiterToHideOnSilentError = new Map();

let defaultWaiter = {
  id: null,
  html: '<div class="container--box"><div class="boxxy"><div class="spinner spinner--1"></div></div></div>',
  color: "#333e48",
  hideOnRender: false,
  hideOnError: false,
  hideOnSilentError: false,
  image: null,
  fadeOut: false,
  ns: null,
  onShown: setWaiterShownInput,
  onHidden: setWaiterHiddenInput,
};

/**
 * Show a waiter screen
 * @function
 * @param  {JSON} params - JSON object of options, see 'defaultWaiter'.
 * @example
 * // defaults
 * show({
 *   id: null,
 *   html: '<div class="container--box"><div class="boxxy"><div class="spinner spinner--1"></div></div></div>',
 *   color: '#333e48',
 *   hideOnRender: false,
 *   hideOnError: false,
 *   hideOnSilentError: false,
 *   image: null,
 *   fadeOut: false,
 *   ns: null,
 *   onShown: setWaiterShownInput,
 *   onHidden: setWaiterHiddenInput
 * });
 */
export const show = (params = defaultWaiter) => {
  // declare
  var dom,
    selector = "body",
    exists = false;

  // get parent
  if (params.id !== null) selector = "#" + params.id;

  dom = document.querySelector(selector);
  if (dom == undefined) {
    console.error("Cannot find", params.id);
    return;
  }

  // allow missing for testing
  params.hideOnRender = params.hideOnRender || false;

  // set in maps
  waiterToHideOnRender.set(params.id, params);
  waiterToFadeout.set(selector, params.fadeOut);
  waiterToHideOnError.set(params.id, params.hideOnError);
  waiterToHideOnSilentError.set(params.id, params.hideOnSilentError);

  let el = getDimensions(dom); // get dimensions

  if (
    dom.classList.contains("leaflet") &&
    document.getElementById("map").children.length > 1
  ) {
    el.top = 0;
    el.left = 0;
  }

  // no id = fll screen
  if (params.id === null) {
    el.height = window.innerHeight;
    el.width = $("body").width();
  }

  // force static if position relative
  // otherwise overlay is completely off
  var pos = window.getComputedStyle(dom, null).position;
  if (pos == "relative") dom.className += " staticParent";

  // check if overlay exists
  dom.childNodes.forEach((el) => {
    if (el.className === "waiter-overlay") exists = true;
  });

  if (exists) {
    console.error("waiter on", params.id, "already exists");
    return;
  }

  hideRecalculate(params.id);

  let overlay = createOverlay(params, el);
  // append overlay to dom
  dom.appendChild(overlay);

  // set input
  if (params.onShown != undefined) params.onShown(params.id);
};
/**
 * @function
 * @param  {string} id - Id of element containing the waiter.
 * if 'null' assumes the waiter is full screen.
 * @param  {Function} onHidden - A callback function to call
 * when the waiter is hidden. Leave on 'null' to not use.
 */
export const hide = (id, onHidden = null) => {
  var selector = "body";
  if (id !== null) selector = "#" + id;

  let overlay = $(selector).find(".waiter-overlay");

  if (overlay.length == 0) return;

  let timeout = 250;
  if (waiterToFadeout.get(selector)) {
    let value = waiterToFadeout.get(selector);

    if (typeof value == "boolean") value = 500;

    $(overlay).fadeOut(value);

    timeout = timeout + value;
  }

  // this is to avoid the waiter screen from flashing
  setTimeout(function () {
    overlay.remove();
  }, timeout);

  if (onHidden != undefined && onHidden != null) onHidden(id);
};

/**
 * Update the content of the waiter.
 * @function
 * @param  {string} id - Id of element to update the waiter.
 * If 'null' assumes the waiter is full screen.
 * @param  {string} html - An html string content to replace
 * the waiter.
 */
export const update = (id, html) => {
  var selector = "body";
  if (id !== null) selector = "#" + id;

  $(selector)
    .find(".waiter-overlay-content")
    .each((index, el) => {
      $(el).html(html);
    });
};

/**
 * Show the recalculate effect from base shiny.
 * Only useful if it was previously hidden.
 * @function
 * @param  {string} id - Id of reactive element.
 */
export const showRecalculate = (id) => {
  $(id + "-waiter-recalculating").remove();
};

// remove when output receives value
$(document).on("shiny:value", function (event) {
  let w = waiterToHideOnRender.get(event.name);

  if (w == undefined) return;

  if (!w.hideOnRender) return;

  hide(event.name, w.onHidden);
});

// remove when output errors
$(document).on("shiny:error", function (event) {
  if (event.error.type == null && waiterToHideOnError.get(event.name)) {
    hide(event.name, setWaiterHiddenInput);
    return;
  }

  if (event.error.type != null && waiterToHideOnSilentError.get(event.name)) {
    hide(event.name, setWaiterHiddenInput);
  }
});

// On resize we need to resize the waiter screens too
window.addEventListener("resize", function () {
  $(".waiter-local").each((index, el) => {
    let dim = getDimensions($(el).parent()[0]);
    $(el).css({
      width: dim.width + "px",
      height: dim.height + "px",
    });
  });

  $(".waiter-fullscreen").css({
    width: window.innerWidth + "px",
    height: window.innerHeight + "px",
  });
});

Shiny.addCustomMessageHandler("waiter-show", function (opts) {
  show(opts);
  Shiny.setInputValue("waiter_shown", true, { priority: "event" });
});

Shiny.addCustomMessageHandler("waiter-update", function (opts) {
  update(opts.id, opts.html);
});

Shiny.addCustomMessageHandler("waiter-hide", function (opts) {
  hide(opts.id, setWaiterHiddenInput);
});