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.log("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

  // 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.log("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);
});