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();
    if (!surface.hasAttribute('tabindex')) surface.setAttribute('tabindex', '-1');
    surface.focus();
    return;
  }
  const first = focusable[0];
  const last = focusable[focusable.length - 1];
  if (e.shiftKey) {
    if (document.activeElement === first || focusable.indexOf(document.activeElement) < 0) {
      e.preventDefault();
      last.focus();
    }
  } else {
    if (document.activeElement === last || focusable.indexOf(document.activeElement) < 0) {
      e.preventDefault();
      first.focus();
    }
  }
}

function syncSurfaceStack() {
  const previousTop = ARCANE.stack[ARCANE.stack.length - 1];
  ARCANE.stack = ARCANE.stack.filter(function(entry) {
    if (entry.el.isConnected && surfaceIsOpen(entry.el)) return true;
    if (entry.el._arcaneAnchorReposition) {
      window.removeEventListener('resize', entry.el._arcaneAnchorReposition);
      window.removeEventListener('scroll', entry.el._arcaneAnchorReposition, true);
      entry.el._arcaneAnchorReposition = null;
    }
    return false;
  });

  const opens = document.querySelectorAll('[data-arcane-surface][data-arcane-state="open"]');
  for (let i = 0; i < opens.length; i++) {
    const el = opens[i];
    if (surfaceIsOpen(el) && !ARCANE.stack.some(function(entry) { return entry.el === el; })) {
      openSurface(surfaceType(el), surfaceId(el));
    }
  }

  const hasOverlay = ARCANE.stack.some(function(entry) {
    return entry.type === 'dialog' || entry.type === 'sheet' || entry.type === 'drawer';
  });
  document.body.classList.toggle('arcane-overlay-open', hasOverlay);
  if (hasOverlay) document.body.setAttribute('data-arcane-overlay-open', 'true');
  else document.body.removeAttribute('data-arcane-overlay-open');

  if (previousTop && !ARCANE.stack.some(function(entry) { return entry.el === previousTop.el; })) {
    const el = previousTop.el;
    const target = el._arcanePrevFocus;
    const top = ARCANE.stack[ARCANE.stack.length - 1];
    if (el.getAttribute('data-arcane-restore-focus') !== 'false' &&
        target && target.isConnected && (!top || top.el.contains(target))) {
      target.focus({ preventScroll: true });
    }
    el._arcanePrevFocus = null;
  }
}

function observeSurfaces() {
  if (ARCANE._surfaceObserver) return;
  ARCANE._surfaceObserver = new MutationObserver(function(records) {
    const changed = records.some(function(record) {
      if (record.type === 'attributes') return record.target.hasAttribute('data-arcane-surface');
      const nodes = Array.from(record.addedNodes).concat(Array.from(record.removedNodes));
      return nodes.some(function(node) {
        return node.nodeType === 1 && (node.hasAttribute('data-arcane-surface') ||
          node.querySelector('[data-arcane-surface]'));
      });
    });
    if (changed) syncSurfaceStack();
  });
  ARCANE._surfaceObserver.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['data-arcane-state']
  });
}

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;

  if (!surfaceEl._arcaneAnchorInlineStyle) {
    surfaceEl._arcaneAnchorInlineStyle = {
      position: surfaceEl.style.position,
      top: surfaceEl.style.top,
      left: surfaceEl.style.left,
      width: surfaceEl.style.width,
      minWidth: surfaceEl.style.minWidth
    };
  }
  surfaceEl.style.position = 'fixed';
  surfaceEl.style.width = sw + 'px';
  surfaceEl.style.minWidth = sw + 'px';
  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) && ARCANE.stack.some(function(entry) { return entry.el === 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');

  const anchorId = el.getAttribute('data-arcane-anchor');
  if (anchorId) {
    const anchorEl = document.querySelector(
      '[data-arcane-anchor-id="' + cssEscape(anchorId) + '"]'
    );
    if (anchorEl && anchorEl.hasAttribute('aria-expanded')) {
      anchorEl.setAttribute('aria-expanded', 'true');
    }
  }

  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() {
    if (!el.isConnected || !surfaceIsOpen(el) ||
        ARCANE.stack[ARCANE.stack.length - 1]?.el !== el) return;
    el.classList.add('arcane-surface-open');
    el.classList.remove('arcane-surface-closing');

    const focusTarget = el.querySelector('[data-arcane-autofocus]') ||
      getFocusable(el)[0];
    if (focusTarget) {
      if (!focusTarget.hasAttribute('tabindex') && focusTarget.tabIndex < 0) {
        focusTarget.setAttribute('tabindex', '-1');
      }
      try { focusTarget.focus({ preventScroll: false }); } catch (e) { focusTarget.focus(); }
    } else {
      if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1');
      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');

  const anchorId = el.getAttribute('data-arcane-anchor');
  if (anchorId) {
    const anchorEl = document.querySelector(
      '[data-arcane-anchor-id="' + cssEscape(anchorId) + '"]'
    );
    if (anchorEl && anchorEl.hasAttribute('aria-expanded')) {
      anchorEl.setAttribute('aria-expanded', 'false');
    }
  }

  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() {
    if (surfaceIsOpen(el)) return;
    el.setAttribute('hidden', '');
    el.classList.remove('arcane-surface-closing');
    const inlineStyle = el._arcaneAnchorInlineStyle;
    el.style.position = inlineStyle ? inlineStyle.position : '';
    el.style.top = inlineStyle ? inlineStyle.top : '';
    el.style.left = inlineStyle ? inlineStyle.left : '';
    el.style.width = inlineStyle ? inlineStyle.width : '';
    el.style.minWidth = inlineStyle ? inlineStyle.minWidth : '';
    el._arcaneAnchorInlineStyle = null;
    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;
''';