runtimeSurfacesJs top-level constant

String const runtimeSurfacesJs

Implementation

const String runtimeSurfacesJs = r'''
function surfaceState(el) {
  if (!el) return 'closed';
  return el.getAttribute('data-arcane-state') || 'closed';
}

function surfaceIsOpen(el) {
  return surfaceState(el) === 'open';
}

function surfaceType(el) {
  if (!el) return null;
  return el.getAttribute('data-arcane-surface');
}

function surfaceId(el) {
  if (!el) return null;
  return el.getAttribute('data-arcane-id');
}

function surfaceGroup(el) {
  if (!el) return null;
  return el.getAttribute('data-arcane-surface-group');
}

function trapFocus(surface, e) {
  if (!surface) return;
  if (surface.getAttribute('data-arcane-focus-trap') === 'false') return;
  const focusable = getFocusable(surface);
  if (focusable.length === 0) {
    e.preventDefault();
    surface.focus();
    return;
  }
  const first = focusable[0];
  const last = focusable[focusable.length - 1];
  if (e.shiftKey) {
    if (document.activeElement === first || !surface.contains(document.activeElement)) {
      e.preventDefault();
      last.focus();
    }
  } else {
    if (document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
}

function applyAnchor(surfaceEl) {
  if (!surfaceEl) return;
  const anchorId = surfaceEl.getAttribute('data-arcane-anchor');
  if (!anchorId) return;
  const anchorEl = document.querySelector(
    '[data-arcane-anchor-id="' + cssEscape(anchorId) + '"]'
  );
  if (!anchorEl) return;
  positionAnchored(surfaceEl, anchorEl);
}

function positionAnchored(surfaceEl, anchorEl) {
  const placement = surfaceEl.getAttribute('data-arcane-anchor-placement') || 'bottom';
  const align = surfaceEl.getAttribute('data-arcane-anchor-align') || 'start';
  const offset = parseInt(
    surfaceEl.getAttribute('data-arcane-anchor-offset') || '8', 10
  );
  const rect = anchorEl.getBoundingClientRect();
  const sw = surfaceEl.offsetWidth;
  const sh = surfaceEl.offsetHeight;
  const vw = window.innerWidth;
  const vh = window.innerHeight;
  let top = 0;
  let left = 0;

  let actualPlacement = placement;
  if (placement === 'bottom' && rect.bottom + offset + sh > vh && rect.top - offset - sh > 0) {
    actualPlacement = 'top';
  } else if (placement === 'top' && rect.top - offset - sh < 0 && rect.bottom + offset + sh < vh) {
    actualPlacement = 'bottom';
  } else if (placement === 'right' && rect.right + offset + sw > vw && rect.left - offset - sw > 0) {
    actualPlacement = 'left';
  } else if (placement === 'left' && rect.left - offset - sw < 0 && rect.right + offset + sw < vw) {
    actualPlacement = 'right';
  }

  if (actualPlacement === 'bottom') {
    top = rect.bottom + offset;
    left = align === 'end' ? rect.right - sw : (align === 'center' ? rect.left + (rect.width - sw) / 2 : rect.left);
  } else if (actualPlacement === 'top') {
    top = rect.top - sh - offset;
    left = align === 'end' ? rect.right - sw : (align === 'center' ? rect.left + (rect.width - sw) / 2 : rect.left);
  } else if (actualPlacement === 'right') {
    left = rect.right + offset;
    top = align === 'end' ? rect.bottom - sh : (align === 'center' ? rect.top + (rect.height - sh) / 2 : rect.top);
  } else if (actualPlacement === 'left') {
    left = rect.left - sw - offset;
    top = align === 'end' ? rect.bottom - sh : (align === 'center' ? rect.top + (rect.height - sh) / 2 : rect.top);
  }

  if (left < 4) left = 4;
  if (top < 4) top = 4;
  if (left + sw > vw - 4) left = vw - sw - 4;
  if (top + sh > vh - 4) top = vh - sh - 4;

  surfaceEl.style.position = 'fixed';
  surfaceEl.style.top = top + 'px';
  surfaceEl.style.left = left + 'px';
  surfaceEl.setAttribute('data-arcane-actual-placement', actualPlacement);
}

function openSurface(type, id, opts) {
  opts = opts || {};
  const el = querySurface(type, id);
  if (!el) return false;
  if (surfaceIsOpen(el)) return true;

  const groupName = surfaceGroup(el);
  const exclusive = el.getAttribute('data-arcane-exclusive') === 'true';
  if (groupName) {
    const peers = document.querySelectorAll(
      '[data-arcane-surface-group="' + cssEscape(groupName) + '"][data-arcane-state="open"]'
    );
    for (let i = 0; i < peers.length; i++) {
      if (peers[i] !== el) {
        closeSurface(surfaceType(peers[i]), surfaceId(peers[i]), { silent: true });
      }
    }
  }

  if (exclusive) {
    const all = document.querySelectorAll(
      '[data-arcane-surface="' + type + '"][data-arcane-state="open"]'
    );
    for (let i = 0; i < all.length; i++) {
      if (all[i] !== el) closeSurface(type, surfaceId(all[i]), { silent: true });
    }
  }

  if (opts.trigger) {
    el._arcanePrevFocus = opts.trigger;
  } else {
    el._arcanePrevFocus = document.activeElement;
  }

  el.removeAttribute('hidden');
  el.setAttribute('data-arcane-state', 'open');
  el.setAttribute('aria-hidden', 'false');

  applyAnchor(el);

  ARCANE.stack.push({ type: type, id: id, el: el });

  if (type === 'dialog' || type === 'sheet' || type === 'drawer') {
    document.body.classList.add('arcane-overlay-open');
    document.body.setAttribute('data-arcane-overlay-open', 'true');
  }

  nextFrame(function() {
    el.classList.add('arcane-surface-open');
    el.classList.remove('arcane-surface-closing');

    const focusTarget = el.querySelector('[data-arcane-autofocus]') ||
      getFocusable(el)[0];
    if (focusTarget) {
      try { focusTarget.focus({ preventScroll: false }); } catch (e) { focusTarget.focus(); }
    } else if (el.tabIndex >= 0) {
      el.focus();
    }
  });

  fireEvent(el, 'arcane:open', { type: type, id: id });
  fireEvent(document, 'arcane:surface-open', { type: type, id: id });

  if (!opts.skipAnchorListener && el.getAttribute('data-arcane-anchor')) {
    el._arcaneAnchorReposition = function() { applyAnchor(el); };
    window.addEventListener('resize', el._arcaneAnchorReposition);
    window.addEventListener('scroll', el._arcaneAnchorReposition, true);
  }

  return true;
}

function closeSurface(type, id, opts) {
  opts = opts || {};
  let el;
  if (id) {
    el = querySurface(type, id);
  } else {
    const list = document.querySelectorAll(
      '[data-arcane-surface="' + type + '"][data-arcane-state="open"]'
    );
    if (list.length > 0) el = list[list.length - 1];
  }
  if (!el) return false;
  if (!surfaceIsOpen(el)) return false;

  el.setAttribute('data-arcane-state', 'closed');
  el.setAttribute('aria-hidden', 'true');
  el.classList.remove('arcane-surface-open');
  el.classList.add('arcane-surface-closing');

  ARCANE.stack = ARCANE.stack.filter(function(entry) {
    return entry.el !== el;
  });

  const stillOpen = document.querySelectorAll(
    '[data-arcane-surface="dialog"][data-arcane-state="open"], ' +
    '[data-arcane-surface="sheet"][data-arcane-state="open"], ' +
    '[data-arcane-surface="drawer"][data-arcane-state="open"]'
  ).length;
  if (stillOpen === 0) {
    document.body.classList.remove('arcane-overlay-open');
    document.body.removeAttribute('data-arcane-overlay-open');
  }

  if (el._arcaneAnchorReposition) {
    window.removeEventListener('resize', el._arcaneAnchorReposition);
    window.removeEventListener('scroll', el._arcaneAnchorReposition, true);
    el._arcaneAnchorReposition = null;
  }

  const restoreFocus = el.getAttribute('data-arcane-restore-focus') !== 'false';
  const prevFocus = el._arcanePrevFocus;

  const finalize = function() {
    el.setAttribute('hidden', '');
    el.classList.remove('arcane-surface-closing');
    el.style.position = '';
    el.style.top = '';
    el.style.left = '';
    el.removeAttribute('data-arcane-actual-placement');
    if (restoreFocus && prevFocus && prevFocus.focus) {
      try { prevFocus.focus({ preventScroll: true }); } catch (e) { prevFocus.focus(); }
    }
    el._arcanePrevFocus = null;
  };

  if (ARCANE.config.reducedMotion || opts.immediate) {
    finalize();
  } else {
    const cs = window.getComputedStyle(el);
    const dur = parseFloat(cs.transitionDuration || '0') || 0;
    if (dur > 0) {
      let done = false;
      const handler = function() {
        if (done) return;
        done = true;
        el.removeEventListener('transitionend', handler);
        finalize();
      };
      el.addEventListener('transitionend', handler);
      setTimeout(handler, dur * 1000 + 100);
    } else {
      finalize();
    }
  }

  if (!opts.silent) {
    fireEvent(el, 'arcane:close', { type: type, id: id });
    fireEvent(document, 'arcane:surface-close', { type: type, id: id });
  }
  return true;
}

function toggleSurface(type, id, opts) {
  const el = querySurface(type, id);
  if (!el) return false;
  if (surfaceIsOpen(el)) return closeSurface(type, id, opts);
  return openSurface(type, id, opts);
}

function dismissTopSurface() {
  if (ARCANE.stack.length === 0) return false;
  const top = ARCANE.stack[ARCANE.stack.length - 1];
  return closeSurface(top.type, top.id);
}

ARCANE.surfaces.open = openSurface;
ARCANE.surfaces.close = closeSurface;
ARCANE.surfaces.toggle = toggleSurface;
ARCANE.surfaces.dismissTop = dismissTopSurface;
ARCANE.surfaces.applyAnchor = applyAnchor;
''';