Skip to main content

VIEWER_SCRIPT

Constant VIEWER_SCRIPT 

Source
pub const VIEWER_SCRIPT: &str = "// The diagram viewer: each inlined view sits in a frame at a scale a\n// reader can read, with zoom, pan and Fit, 1:1, Wide and Fullscreen\n// controls. Without this script a view is scaled to the text column (the\n// stylesheet\'s rule) and the controls stay hidden, so a page with no\n// JavaScript shows a diagram and no dead buttons.\n//\n// Scale: a view is shown at the scale that fits the frame\'s width, held\n// between MIN and 1, so a wide view scrolls inside its frame instead of\n// shrinking its text to nothing, and a small one is not blown up. Fit\n// shows the whole view, width and height, at whatever scale that takes;\n// 1:1 shows it at the size LikeC4 laid it out. Zooming (the wheel with\n// Ctrl or Cmd held, which is what a trackpad pinch sends in Chrome and\n// Firefox, and Safari\'s own gesture events for a pinch; the + and -\n// buttons; + - 0 with the frame focused) sets the scale freely, kept\n// about the pointer; a plain wheel scrolls the page as usual. The scale\n// is the view\'s CSS width, and panning is the frame\'s own scroll, by\n// its scrollbars or by dragging, so text stays crisp and the browser\n// keeps the position. Wide lets every figure on the site take the\n// window\'s width, remembered like the color scheme; Legend shows or hides\n// every figure\'s legend, remembered the same way; Fullscreen takes one\n// figure to the screen.\n(function () {\n  var WIDE_KEY = \"asbuilt-docs-wide\";\n  var LEGEND_KEY = \"asbuilt-docs-legend\";\n  var MIN = 0.7;\n  var MIN_ZOOM = 0.1;\n  var MAX_ZOOM = 4;\n  var STEP = 1.25;\n  var WHEEL = 0.002; // scale change per pixel of wheel delta, exponential\n  var DRAG = 4; // pixels of movement before a press becomes a drag\n  var GUTTER = 16;\n  var FRAME_SHARE = 0.75; // of the window\'s height; the stylesheet\'s 75vh\n  var root = document.documentElement;\n  // The zoom modifier\'s name for the hint: Cmd on Apple platforms, Ctrl\n  // elsewhere. The script accepts either key everywhere.\n  var MAC = /Mac|iPhone|iPad|iPod/.test(navigator.platform || \"\");\n  var wide = false;\n  // Each diagram\'s legend shows until a visitor hides it; the choice is\n  // site-wide, like Wide.\n  var legendShown = true;\n  try {\n    wide = window.localStorage.getItem(WIDE_KEY) === \"1\";\n    legendShown = window.localStorage.getItem(LEGEND_KEY) !== \"0\";\n  } catch (e) {}\n  var figures = [];\n\n  function viewBoxSize(svg) {\n    var parts = (svg.getAttribute(\"viewBox\") || \"\").trim().split(/[\\s,]+/);\n    var width = parseFloat(parts[2]);\n    var height = parseFloat(parts[3]);\n    return width > 0 && height > 0 ? { width: width, height: height } : null;\n  }\n\n  function layoutAll() {\n    figures.forEach(function (f) {\n      f.layout();\n    });\n  }\n\n  function setup(figure) {\n    var svg = figure.querySelector(\"svg.c4\");\n    var frame = figure.querySelector(\".viewer-frame\");\n    var bar = figure.querySelector(\".viewer-bar\");\n    if (!svg || !frame || !bar) return;\n    var natural = viewBoxSize(svg);\n    if (!natural) return;\n    var legend = figure.querySelector(\".legend\");\n    var legendButton = bar.querySelector(\"[data-viewer-legend]\");\n    if (legendButton && !legend) {\n      legendButton.hidden = true;\n      legendButton = null;\n    }\n    var mode = \"auto\"; // auto | fit | one | zoom\n    var zoom = 1; // the scale in zoom mode\n    var current = 1; // the scale last applied\n    var pannable = false; // whether the view overflows its frame\n    var readout = bar.querySelector(\".viewer-scale\");\n    var full = bar.querySelector(\"[data-viewer-full]\");\n    var hint = bar.querySelector(\".viewer-hint\");\n    // No wheel to hint at on a touch-only device.\n    if (hint && window.matchMedia && window.matchMedia(\"(hover: none)\").matches) hint = null;\n\n    function fullscreen() {\n      return document.fullscreenElement === figure;\n    }\n    function widthFit() {\n      return frame.clientWidth / natural.width;\n    }\n    // The height the frame may take: the stylesheet\'s share of the\n    // window, or in fullscreen the screen below the controls.\n    function heightBudget() {\n      if (fullscreen()) {\n        return window.innerHeight - frame.getBoundingClientRect().top - GUTTER;\n      }\n      return FRAME_SHARE * window.innerHeight;\n    }\n    function scale() {\n      if (mode === \"zoom\") return zoom;\n      if (mode === \"one\") return 1;\n      if (mode === \"fit\") return Math.min(widthFit(), heightBudget() / natural.height);\n      return Math.min(1, Math.max(MIN, widthFit()));\n    }\n    function layout() {\n      if (wide && !fullscreen()) {\n        // The window\'s width, measured from the document (which leaves\n        // out the scrollbar), from wherever the text column put us; the\n        // root\'s own left accounts for any horizontal scroll.\n        figure.style.marginLeft = \"0\";\n        var left = figure.getBoundingClientRect().left - root.getBoundingClientRect().left;\n        figure.style.marginLeft = GUTTER - left + \"px\";\n        figure.style.width = root.clientWidth - 2 * GUTTER + \"px\";\n      } else {\n        figure.style.marginLeft = \"\";\n        figure.style.width = \"\";\n      }\n      current = scale();\n      svg.style.width = Math.round(natural.width * current) + \"px\";\n      // A view that fits has nowhere to pan to: no grab cursor, no drag.\n      pannable =\n        frame.scrollWidth > frame.clientWidth + 1 || frame.scrollHeight > frame.clientHeight + 1;\n      frame.classList.toggle(\"pannable\", pannable);\n      if (readout) readout.textContent = Math.round(current * 100) + \"%\";\n      if (hint) {\n        hint.textContent =\n          (MAC ? \"\\u2318\" : \"Ctrl\") + \" scroll to zoom\" + (pannable ? \" \\u00b7 drag to pan\" : \"\");\n      }\n      bar.querySelectorAll(\"[data-viewer-mode]\").forEach(function (button) {\n        var pressed = button.getAttribute(\"data-viewer-mode\") === mode;\n        button.setAttribute(\"aria-pressed\", String(pressed));\n      });\n      var wideButton = bar.querySelector(\"[data-viewer-wide]\");\n      if (wideButton) wideButton.setAttribute(\"aria-pressed\", String(wide));\n      if (legend) legend.hidden = !legendShown;\n      if (legendButton) legendButton.setAttribute(\"aria-pressed\", String(legendShown));\n    }\n    // Scale to `next`, keeping the view point under the frame point\n    // (`fx`, `fy`, from the frame\'s top left; its center when absent)\n    // where it is.\n    function zoomTo(next, fx, fy) {\n      next = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, next));\n      if (fx === undefined) {\n        fx = frame.clientWidth / 2;\n        fy = frame.clientHeight / 2;\n      }\n      var before = current;\n      var x = frame.scrollLeft + fx;\n      var y = frame.scrollTop + fy;\n      mode = \"zoom\";\n      zoom = next;\n      layout();\n      var ratio = current / before;\n      frame.scrollLeft = x * ratio - fx;\n      frame.scrollTop = y * ratio - fy;\n    }\n\n    bar.querySelectorAll(\"[data-viewer-mode]\").forEach(function (button) {\n      button.addEventListener(\"click\", function () {\n        var next = button.getAttribute(\"data-viewer-mode\");\n        mode = mode === next ? \"auto\" : next;\n        layout();\n      });\n    });\n    bar.querySelectorAll(\"[data-viewer-zoom]\").forEach(function (button) {\n      button.addEventListener(\"click\", function () {\n        var out = button.getAttribute(\"data-viewer-zoom\") === \"out\";\n        zoomTo(out ? current / STEP : current * STEP);\n      });\n    });\n    if (legendButton) {\n      legendButton.addEventListener(\"click\", function () {\n        legendShown = !legendShown;\n        try {\n          window.localStorage.setItem(LEGEND_KEY, legendShown ? \"1\" : \"0\");\n        } catch (e) {}\n        layoutAll();\n      });\n    }\n    var wideButton = bar.querySelector(\"[data-viewer-wide]\");\n    if (wideButton) {\n      wideButton.addEventListener(\"click\", function () {\n        wide = !wide;\n        try {\n          window.localStorage.setItem(WIDE_KEY, wide ? \"1\" : \"0\");\n        } catch (e) {}\n        layoutAll();\n      });\n    }\n    if (full) {\n      if (figure.requestFullscreen) {\n        full.addEventListener(\"click\", function () {\n          if (fullscreen()) document.exitFullscreen();\n          else figure.requestFullscreen();\n        });\n      } else {\n        full.hidden = true;\n      }\n    }\n\n    frame.addEventListener(\n      \"wheel\",\n      function (event) {\n        if (!(event.ctrlKey || event.metaKey)) return;\n        event.preventDefault();\n        // Lines and pages to pixels: a line is about 16px, a page the frame.\n        var unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? frame.clientHeight : 1;\n        var delta = event.deltaY * unit;\n        var rect = frame.getBoundingClientRect();\n        zoomTo(current * Math.exp(-delta * WHEEL), event.clientX - rect.left, event.clientY - rect.top);\n      },\n      { passive: false }\n    );\n    // Safari reports a trackpad pinch as gesture events (its scale is\n    // relative to the gesture\'s start), not as the wheel with Ctrl.\n    var pinchBase = null;\n    frame.addEventListener(\"gesturestart\", function (event) {\n      event.preventDefault();\n      pinchBase = current;\n    });\n    frame.addEventListener(\"gesturechange\", function (event) {\n      event.preventDefault();\n      if (pinchBase === null) return;\n      var rect = frame.getBoundingClientRect();\n      zoomTo(pinchBase * event.scale, event.clientX - rect.left, event.clientY - rect.top);\n    });\n    frame.addEventListener(\"gestureend\", function () {\n      pinchBase = null;\n    });\n    frame.addEventListener(\"keydown\", function (event) {\n      if (event.target !== frame || event.ctrlKey || event.metaKey || event.altKey) return;\n      if (event.key === \"+\" || event.key === \"=\") zoomTo(current * STEP);\n      else if (event.key === \"-\") zoomTo(current / STEP);\n      else if (event.key === \"0\") {\n        mode = \"auto\";\n        layout();\n      } else return;\n      event.preventDefault();\n    });\n\n    // Drag to pan, with the mouse: touch keeps the browser\'s own\n    // scrolling. A press that moves becomes a drag and is not a click,\n    // so a node\'s link under the pointer is not followed on release.\n    var press = null;\n    var suppressClick = false;\n    // A node is a link, which the browser would start dragging as one.\n    frame.addEventListener(\"dragstart\", function (event) {\n      event.preventDefault();\n    });\n    frame.addEventListener(\n      \"click\",\n      function (event) {\n        if (!suppressClick) return;\n        suppressClick = false;\n        event.stopPropagation();\n        event.preventDefault();\n      },\n      true\n    );\n    frame.addEventListener(\"pointerdown\", function (event) {\n      suppressClick = false;\n      if (!pannable || event.pointerType !== \"mouse\" || event.button !== 0) return;\n      if (event.target.closest(\"button\")) return;\n      press = {\n        id: event.pointerId,\n        x: event.clientX,\n        y: event.clientY,\n        left: frame.scrollLeft,\n        top: frame.scrollTop,\n        dragged: false,\n      };\n    });\n    frame.addEventListener(\"pointermove\", function (event) {\n      if (!press || event.pointerId !== press.id) return;\n      // A release the frame never saw (outside it, or behind another\n      // window) leaves the press armed: the button being up says so.\n      if (!(event.buttons & 1)) {\n        press = null;\n        frame.classList.remove(\"dragging\");\n        return;\n      }\n      var dx = event.clientX - press.x;\n      var dy = event.clientY - press.y;\n      if (!press.dragged && Math.abs(dx) < DRAG && Math.abs(dy) < DRAG) return;\n      if (!press.dragged) {\n        press.dragged = true;\n        frame.classList.add(\"dragging\");\n        frame.setPointerCapture(press.id);\n      }\n      frame.scrollLeft = press.left - dx;\n      frame.scrollTop = press.top - dy;\n    });\n    function release(event) {\n      if (!press || event.pointerId !== press.id) return;\n      // The click that follows a drag\'s release is swallowed above; the\n      // flag is cleared by the next press, so a cancelled drag with no\n      // click leaves nothing armed.\n      suppressClick = press.dragged;\n      press = null;\n      frame.classList.remove(\"dragging\");\n    }\n    frame.addEventListener(\"pointerup\", release);\n    frame.addEventListener(\"pointercancel\", release);\n\n    // An edge stands for the relations between what it joins, more than\n    // its label can say; the page wrote them beside the view. A click on\n    // such an edge opens them in a popover, each endpoint a link.\n    var edges = null;\n    var edgeData = figure.querySelector(\"script.viewer-edges\");\n    if (edgeData) {\n      try {\n        edges = JSON.parse(edgeData.textContent);\n      } catch (e) {}\n    }\n    var popover = null;\n    function closePopover(refocus) {\n      if (!popover || popover.hidden) return;\n      popover.hidden = true;\n      if (refocus) frame.focus();\n    }\n    // A link only for a web or file URL. asbuilt docs writes relative\n    // paths in the edge data, but a host that copies this script may feed\n    // it others; the browser\'s URL parser reads the scheme however it is\n    // spelled, so a `javascript:` or `data:` link shows as text. The link\n    // gets the resolved URL: the value checked is the value written.\n    var LINKABLE = [\"http:\", \"https:\", \"file:\"];\n    function linkable(href) {\n      if (!href) return null;\n      try {\n        var url = new URL(href, document.baseURI);\n        return LINKABLE.indexOf(url.protocol) >= 0 ? url.href : null;\n      } catch (e) {\n        return null;\n      }\n    }\n    function endpoint(text, href) {\n      var url = linkable(href);\n      var el = document.createElement(url ? \"a\" : \"span\");\n      if (url) el.href = url;\n      el.textContent = text;\n      return el;\n    }\n    function openPopover(key, relations, event) {\n      if (!popover) {\n        popover = document.createElement(\"div\");\n        popover.className = \"viewer-popover\";\n        popover.setAttribute(\"role\", \"dialog\");\n        popover.setAttribute(\"aria-label\", \"Relations\");\n        popover.hidden = true;\n        var close = document.createElement(\"button\");\n        close.type = \"button\";\n        close.className = \"viewer-popover-close\";\n        close.setAttribute(\"aria-label\", \"Close\");\n        close.textContent = \"\\u00d7\";\n        close.addEventListener(\"click\", function () {\n          closePopover(true);\n        });\n        popover.appendChild(close);\n        popover.appendChild(document.createElement(\"h3\"));\n        popover.appendChild(document.createElement(\"ul\"));\n        popover.addEventListener(\"keydown\", function (e) {\n          if (e.key === \"Escape\") {\n            e.preventDefault();\n            closePopover(true);\n          }\n        });\n        figure.appendChild(popover);\n      }\n      popover.querySelector(\"h3\").textContent = key.replace(\"->\", \" \\u2192 \");\n      var list = popover.querySelector(\"ul\");\n      list.textContent = \"\";\n      relations.forEach(function (r) {\n        var li = document.createElement(\"li\");\n        li.appendChild(endpoint(r.source, r.source_href));\n        li.appendChild(document.createTextNode(\" \\u2192 \"));\n        li.appendChild(endpoint(r.target, r.target_href));\n        var detail = document.createElement(\"span\");\n        detail.className = \"rel-detail\";\n        detail.textContent =\n          \" \" +\n          r.kind +\n          (r.items.length ? \" \" + r.items.join(\", \") : \"\") +\n          (r.technology ? \" (\" + r.technology + \")\" : \"\");\n        li.appendChild(detail);\n        list.appendChild(li);\n      });\n      popover.hidden = false;\n      // Beside the click, inside the figure (which scrolls in fullscreen).\n      var rect = figure.getBoundingClientRect();\n      var left = event.clientX - rect.left + figure.scrollLeft + 8;\n      var top = event.clientY - rect.top + figure.scrollTop + 8;\n      left = Math.max(0, Math.min(left, figure.scrollLeft + figure.clientWidth - popover.offsetWidth - 8));\n      top = Math.max(0, Math.min(top, figure.scrollTop + figure.clientHeight - popover.offsetHeight - 8));\n      popover.style.left = left + \"px\";\n      popover.style.top = top + \"px\";\n      popover.querySelector(\".viewer-popover-close\").focus();\n    }\n    if (edges) {\n      frame.querySelectorAll(\".edge[data-from]\").forEach(function (edge) {\n        var key = edge.getAttribute(\"data-from\") + \"->\" + edge.getAttribute(\"data-to\");\n        if (edges[key]) edge.classList.add(\"c4-explained\");\n      });\n      frame.addEventListener(\"click\", function (event) {\n        var edge = event.target.closest(\".edge.c4-explained\");\n        if (!edge) return;\n        var key = edge.getAttribute(\"data-from\") + \"->\" + edge.getAttribute(\"data-to\");\n        event.preventDefault();\n        openPopover(key, edges[key], event);\n      });\n      document.addEventListener(\"click\", function (event) {\n        if (!popover || popover.hidden || popover.contains(event.target)) return;\n        // Another edge of this figure is re-opening it; any other click,\n        // including an edge of another figure, closes it.\n        if (frame.contains(event.target) && event.target.closest(\".edge.c4-explained\")) return;\n        closePopover(false);\n      });\n      frame.addEventListener(\"keydown\", function (event) {\n        if (event.key === \"Escape\") closePopover(true);\n      });\n    }\n\n    document.addEventListener(\"fullscreenchange\", layout);\n    // The frame\'s width changes with the column; the window\'s with Wide\n    // (which sizes the figure itself, so the frame alone would not tell).\n    if (window.ResizeObserver) new ResizeObserver(layout).observe(frame);\n\n    figure.setAttribute(\"data-viewer-active\", \"\");\n    frame.setAttribute(\"tabindex\", \"0\");\n    bar.hidden = false;\n    layout();\n    figures.push({ layout: layout });\n  }\n\n  function setupAll(root) {\n    if (root.matches && root.matches(\"figure[data-viewer]:not([data-viewer-active])\")) setup(root);\n    if (root.querySelectorAll) {\n      root.querySelectorAll(\"figure[data-viewer]:not([data-viewer-active])\").forEach(setup);\n    }\n  }\n\n  document.addEventListener(\"DOMContentLoaded\", function () {\n    setupAll(document);\n    window.addEventListener(\"resize\", layoutAll);\n    // A host that builds its page after load (a single-page app embedding\n    // a figure) gets its figures set up as they arrive.\n    if (window.MutationObserver) {\n      new MutationObserver(function (records) {\n        records.forEach(function (record) {\n          record.addedNodes.forEach(function (node) {\n            if (node.nodeType === 1) setupAll(node);\n          });\n        });\n      }).observe(document.body, { childList: true, subtree: true });\n    }\n  });\n})();\n";
Expand description

viewer.js: the diagram viewer, when viewer is on.