{
  "version": 3,
  "sources": ["row_focus.js"],
  "sourcesContent": ["/*\n * static/js/row_focus.js \u2014 framing the place a UGC panel row names.\n *\n * Hugo: \"For routes, resorts, and observations, clicking on the name of an\n * item should zoom in to it.\" The row's name is a real `<button>` on those\n * three map panels (includes/_ugc_panel_row.html's `focus_target`), and\n * this is what the button does. The sibling of static/js/row_rename_commit.js,\n * in the same shape and for the same reason: three panels, one behaviour,\n * one owner.\n *\n * TWO ENTRY POINTS SINCE SNOW-886, and they are the two moments a user\n * has a particular saved thing in mind:\n *\n *   `handleClick` \u2014 a row press. The full sequence below: overlay, close\n *     the panel, camera.\n *   `reveal` \u2014 an item that has just been CREATED, called by\n *     static/js/favourites.js, report.js and routes.js from their submit\n *     handlers. Same overlay-then-camera order, no close: the panel is\n *     showing the confirmation card the caller has just rendered, and\n *     that card is the answer to \"did that work\".\n *\n * The second exists because all three creates had the first step's defect\n * and none had its fix \u2014 a pin saved with the favourites layer switched\n * off landed on a map drawing nothing, which reads as a create that\n * failed rather than as a hidden layer. That is the same misreading this\n * module's step 1 was written to prevent for a row press, so it is fixed\n * in the same place rather than three times over in the three panels.\n *\n * Three steps, and the ORDER of them is the whole module:\n *\n *   1. TURN THE OVERLAY ON. Flying to a route with the routes layer\n *      switched off lands on an empty map, which reads as a broken button\n *      rather than as a hidden layer. Somebody who presses \"take me to\n *      this\" means to see it, so the panel's own overlay bridge is asked\n *      to show \u2014 the same call the layers menu's own row makes, so the\n *      preference persists and the lazy load runs. The\n *      ``/map/?favourite=<uuid>`` deep link set this precedent and its own\n *      note in static/js/map.js explains why flipping `visibility` by hand\n *      instead would be undone by the next thing to re-read the\n *      preference.\n *   2. CLOSE THE PANEL. Below `sm` the sheet is a full-width bottom dock\n *      covering the map (includes/_overlay_sheet.html), so a zoom behind\n *      it would be invisible on a phone \u2014 the device most likely to be\n *      holding this panel open. Closing on every width rather than only\n *      the narrow one keeps one behaviour to describe, and the roundel\n *      that opened the panel is one tap away.\n *   3. MOVE THE CAMERA, through window.pwaMapFocus. See that bridge in\n *      static/js/map.js for why it takes a coordinate rather than a uuid.\n *\n * SNOW-835 inserts a fourth step BETWEEN the close and the camera, for\n * the one panel whose rows name a basemap as well as a place: switch to\n * that basemap first. The downloads panel's squares are filtered to the\n * active basemap's tile template (`refreshDownloadedOverlay` in map.js),\n * so framing a Swisstopo area while the map is on OpenFreeMap lands on\n * ground with nothing drawn on it \u2014 the row took the camera with it and\n * left the basemap behind. It goes after the close so the sheet is\n * already out of the way while the new style loads, and the camera then\n * WAITS for `snowdesk:basemap-changed`: `setStyle` tears every layer off\n * the map and a `fitBounds` issued before the reinstall lands is silently\n * discarded, which would switch the basemap and go nowhere. Silent, like\n * everything else here \u2014 the group heading the row sits under already\n * names the basemap (SNOW-832).\n *\n * NO USER-FACING STRINGS. Nothing here is announced: the button's own\n * \"Zoom to <name>\" is server-rendered in the row, where makemessages can\n * see it (docs/i18n.md), and a press that cannot be served moves nothing\n * rather than explaining itself. That silence is deliberate \u2014 every way\n * this returns early is a state the user cannot act on (the map bundle\n * has not booted, the row carries no coordinate), and a toast for it\n * would be noise on a surface that is already showing them their list.\n */\n\n(function rowFocusInit() {\n  'use strict';\n\n  // Two attributes, one control. `data-row-focus` carries degrees \u2014 the\n  // three server-rendered panels' only form. `data-row-focus-region`\n  // (SNOW-811) carries an EAWS micro-region id instead, for the downloads\n  // panel's region rows: a region download is recorded against its id and\n  // deliberately carries no bbox, because the polygon is already on the map\n  // and a stored box would be a second, coarser copy of a boundary\n  // MapLibre is drawing anyway. Resolving it is window.pwaMapFocus's job,\n  // not this module's.\n  var FOCUS_SELECTOR = '[data-row-focus], [data-row-focus-region]';\n\n  // A third attribute, deliberately NOT in the selector above:\n  // `data-row-focus-basemap` (SNOW-835) says which basemap to be on when\n  // the camera lands, never on its own where to land, so it can no more\n  // make a label pressable than an empty `data-row-focus` can. Only the\n  // downloads panel's rows carry it, stamped on the clone by\n  // map_downloads_manager.js's `applyRowFocus` \u2014 the three server-rendered\n  // panels pass nothing, so every branch reading it is skipped for them\n  // and their behaviour is the one this module had before.\n  var BASEMAP_ATTRIBUTE = 'data-row-focus-basemap';\n\n  // How long the camera waits for the new style before moving anyway. A\n  // style that fails to load still leaves a map worth moving \u2014 offline on\n  // a basemap this device has not downloaded resolves to SNOW-483's inline\n  // fallback, which is a real style with real coordinates \u2014 so the wait\n  // is bounded rather than a promise that can hang (see\n  // docs/decisions/bounded-offline-read-paths.md for the same rule on the\n  // read paths). Four seconds is long enough for a cold style + sprite\n  // fetch on a slow connection and short enough that a user who pressed a\n  // row is not left looking at an unmoved map wondering whether it\n  // registered.\n  var BASEMAP_SETTLE_MS = 4000;\n\n  /**\n   * Parse a row's `data-row-focus` into numbers.\n   *\n   * The attribute is plain comma-separated degrees, written by\n   * includes/_ugc_panel_row.html through `stringformat:\"f\"` \u2014 see that\n   * partial's `focus_target` note for why a locale-formatted float or a\n   * JSON attribute would not survive the trip.\n   *\n   * @param {?string} value The attribute's value.\n   * @returns {?number[]} Two numbers ([lon, lat]) or four ([west, south,\n   *   east, north]); null for anything else, including a partially\n   *   numeric string \u2014 a bbox missing an ordinate frames nothing, and\n   *   guessing at it would fly the map somewhere arbitrary.\n   */\n  function parse(value) {\n    if (!value) return null;\n    var parts = String(value).split(',');\n    if (parts.length !== 2 && parts.length !== 4) return null;\n    var numbers = [];\n    for (var i = 0; i < parts.length; i += 1) {\n      var n = Number(parts[i].trim());\n      // Number('') is 0, so an empty ordinate would otherwise pass as the\n      // equator rather than as the malformed attribute it is.\n      if (parts[i].trim() === '' || !isFinite(n)) return null;\n      numbers.push(n);\n    }\n    return numbers;\n  }\n\n  /**\n   * The basemap picker row to press to get onto `key`, if pressing one is\n   * both possible and needed (SNOW-835).\n   *\n   * The swap is DELEGATED to that row rather than reimplemented here.\n   * map_basemap_picker.js's own click handler dispatches\n   * `snowdesk:basemap-changing` for the consumers that must surrender\n   * control (the timelapse), persists the preference, moves `aria-checked`\n   * across the radio group, closes the popover and only then calls\n   * `resolveBasemapStyle(\u2026).then(setStyle)`. A second implementation of\n   * that sequence would be a preference left unpersisted and a popover\n   * showing the wrong row checked \u2014 so this module's whole contribution is\n   * finding the row and clicking it.\n   *\n   * The checked row is also how \"which basemap am I on\" is answered here,\n   * the same source map_basemap_downloads.js's `activeBasemapKey` reads:\n   * the picker DOM leads the render (it is updated synchronously on click,\n   * before `setStyle` has resolved), which for this caller is the right\n   * end of that lag \u2014 a second press while a swap is in flight sees the\n   * incoming basemap and does not queue a redundant one.\n   *\n   * Iterates and compares `dataset.basemapKey` per element rather than\n   * interpolating a stored key into a selector, the rule\n   * map_basemap_downloads.js's `basemapLabel` already follows: the key\n   * comes off a record on disk, and selector syntax in one would otherwise\n   * be an injection.\n   *\n   * @param {?string} key The row's stored basemap key.\n   * @returns {?HTMLElement} The picker row, or null when there is nothing\n   *   to press: no key, no picker on the page, no row offering that key\n   *   (a `swisstopo_light` record whose basemap has since been retired),\n   *   the key is already active, or the row is inert. The caller frames\n   *   without switching in every one of those cases.\n   */\n  function basemapSwitchTarget(key) {\n    if (!key) return null;\n    var menu = document.getElementById('basemap-menu');\n    if (!menu) return null;\n    var items = menu.querySelectorAll('.basemap-menu-item[data-basemap-key]');\n    var match = null;\n    for (var i = 0; i < items.length; i += 1) {\n      if (items[i].dataset.basemapKey === key) {\n        match = items[i];\n        break;\n      }\n    }\n    if (!match) return null;\n    // Already on it: no setStyle, no flicker, and no wait \u2014 this is the\n    // common case for every row in the group the map is already showing.\n    if (match.getAttribute('aria-checked') === 'true') return null;\n    // A row map_layer_sync_status.js has disabled (offline, and that\n    // basemap's style is not cached) is inert \u2014 the picker's own handler\n    // returns on the same attribute. Clicking it would do nothing and then\n    // hold the camera for the full settle timeout, so the press frames\n    // straight away instead.\n    if (match.getAttribute('aria-disabled') === 'true') return null;\n    return match;\n  }\n\n  /**\n   * Run `then` once the new basemap's style has settled, or give up\n   * waiting (SNOW-835).\n   *\n   * `setStyle` is asynchronous and wipes every app layer; map.js\n   * re-installs them on `styledata` and ends by dispatching\n   * `snowdesk:basemap-changed`. A camera move issued before that lands is\n   * discarded by MapLibre without an error, so the wait is the feature \u2014\n   * and the timeout is what keeps a style that never loads from swallowing\n   * the press entirely.\n   *\n   * @param {function(): void} then What to do when the style has settled.\n   * @returns {void}\n   */\n  function afterBasemapSettles(then) {\n    var settled = false;\n    var timer = null;\n    function done() {\n      // Whichever of the two arrives first wins, and the other is torn\n      // down: a listener left on `document` would fire again on the next\n      // swap the user makes by hand, framing a row they pressed minutes\n      // ago.\n      if (settled) return;\n      settled = true;\n      document.removeEventListener('snowdesk:basemap-changed', done);\n      if (timer !== null) clearTimeout(timer);\n      then();\n    }\n    document.addEventListener('snowdesk:basemap-changed', done);\n    timer = setTimeout(done, BASEMAP_SETTLE_MS);\n  }\n\n  /**\n   * Move the camera to whichever address the row carried.\n   *\n   * Its own function since SNOW-835 because there are now two moments it\n   * can run at \u2014 immediately, or after a basemap swap has settled \u2014 and\n   * the answer must not differ between them.\n   *\n   * @param {Object} focus window.pwaMapFocus, read by the caller.\n   * @param {?number[]} coordinates Two ordinates or four, as `parse`\n   *   returns them.\n   * @param {?string} regionId An EAWS micro-region id.\n   * @returns {void}\n   */\n  function frame(focus, coordinates, regionId) {\n    // Degrees win when a row somehow carries both: they are the exact\n    // extent, where the region id is a lookup that can miss (see\n    // pwaMapFocus.region on regions loading per country).\n    if (coordinates && coordinates.length === 2) {\n      focus.point(coordinates[0], coordinates[1]);\n    } else if (coordinates) {\n      focus.bounds(coordinates);\n    } else if (focus.region) {\n      focus.region(regionId);\n    }\n  }\n\n  /**\n   * Switch a panel's overlay on, if the user has it off.\n   *\n   * Its own function since SNOW-886 because there are now two callers \u2014\n   * a row press and a just-created item \u2014 and the answer must not differ\n   * between them. It is the same call the layers menu's own row makes\n   * (SNOW-904), so the preference persists and the lazy load runs.\n   *\n   * isEnabled(), the persisted preference \u2014 NOT isVisible(), which is\n   * what MapLibre is drawing. An overlay enabled offline with nothing\n   * cached reads enabled and paints nothing, and calling show() again for\n   * it would neither draw anything nor be what the user asked for.\n   *\n   * @param {?object} overlay A panel's own window.pwa*Overlay bridge, or\n   *   nothing \u2014 a caller with no overlay to offer passes none, and every\n   *   bridge is read defensively because map.js may not have booted.\n   * @returns {void}\n   */\n  function enable(overlay) {\n    if (overlay && overlay.isEnabled && !overlay.isEnabled() && overlay.show) {\n      overlay.show();\n    }\n  }\n\n  /**\n   * Handle a click that may be a row asking to be framed on the map.\n   *\n   * Shaped like window.pwaRowRenameCommit.handleClick, and answering the\n   * same question its callers ask next: \"was this mine, or should I keep\n   * testing?\" A click that is not on a focus button returns false and is\n   * left entirely alone.\n   *\n   * A focus button that cannot be served returns TRUE without moving\n   * anything. The click WAS a focus \u2014 reporting it as \"not mine\" would\n   * send it on to the caller's add-CTA test, which is a different action\n   * on the same surface.\n   *\n   * Returns as soon as it has claimed the press either way: a row that\n   * needs its basemap switching first (SNOW-835) frames LATER, from the\n   * `snowdesk:basemap-changed` listener, so a true answer means \"this was\n   * mine\", not \"the camera has already moved\".\n   *\n   * @param {MouseEvent} event The click.\n   * @param {{\n   *   overlay: ?object,\n   *   close: ?function(): void,\n   * }} options\n   *   `overlay` is this panel's own window.pwa*Overlay bridge, switched on\n   *   before the flight when the user has it off; `close` dismisses the\n   *   panel so the map is visible when the camera arrives.\n   * @returns {boolean} Whether this click was a focus.\n   */\n  function handleClick(event, options) {\n    var target = event && event.target;\n    var button = target && target.closest ? target.closest(FOCUS_SELECTOR) : null;\n    if (!button) return false;\n\n    var regionId = button.getAttribute('data-row-focus-region');\n    var coordinates = parse(button.getAttribute('data-row-focus'));\n    // Neither form resolved \u2014 a region row and a coordinate row are the\n    // only two there are, so this is a button with nothing behind it. The\n    // press WAS a focus (see the note above), so it is claimed and\n    // silently dropped rather than passed on.\n    if (!coordinates && !regionId) return true;\n\n    enable(options && options.overlay);\n\n    if (options && options.close) options.close();\n\n    var focus = window.pwaMapFocus;\n    // No map bundle, so nothing to frame \u2014 and therefore nothing to switch\n    // the basemap FOR either. Persisting a new preference here would be a\n    // side effect with no camera move to justify it, which is the same\n    // reason an account-only row never carries the attribute at all.\n    if (!focus) return true;\n\n    // SNOW-835: the basemap the row's area was downloaded against. Absent\n    // on every row of the other three panels, and on the downloads rows\n    // that have nothing to switch to \u2014 so this resolves to null and the\n    // synchronous path below is the one this module has always taken.\n    var picker = basemapSwitchTarget(button.getAttribute(BASEMAP_ATTRIBUTE));\n    if (!picker) {\n      frame(focus, coordinates, regionId);\n      return true;\n    }\n\n    // Listener first, click second. `setStyle` cannot resolve\n    // synchronously, so the order is not load-bearing today \u2014 but binding\n    // after the press is the shape that would miss the event if it ever\n    // became so.\n    afterBasemapSettles(function () {\n      frame(focus, coordinates, regionId);\n    });\n    picker.click();\n    return true;\n  }\n\n  /**\n   * Show an item that has JUST BEEN CREATED (SNOW-886).\n   *\n   * The second entry point, and the same two steps in the same order as a\n   * row press: the overlay first, the camera second. What it leaves out is\n   * the close \u2014 the panel is showing the confirmation card that its caller\n   * has just rendered, which is the whole answer to \"did that work\", so\n   * dismissing it would take the answer away with it. The caller's own\n   * card owns the close.\n   *\n   * The defect this fixes is the overlay, not the camera. Saving a pin\n   * with the favourites layer switched off dropped it onto a map that\n   * draws nothing, so the create appeared to fail \u2014 the same reading a\n   * row press produced before this module existed. Somebody who saves a\n   * place means to see it.\n   *\n   * window.pwaMapFocus is read HERE rather than at module scope: map.js\n   * runs later in the document than these modules, so a reference taken\n   * at parse time would be undefined for the life of the page. Every\n   * existing consumer reads it the same way, and a call with no map\n   * bundle switches the overlay on (which is persisted, and will paint\n   * when a map does exist) and moves nothing.\n   *\n   * @param {{\n   *   overlay: ?object,\n   *   coordinates: ?number[],\n   * }} options\n   *   `overlay` is the creating panel's own window.pwa*Overlay bridge.\n   *   `coordinates` is two ordinates ([lon, lat]) or four ([west, south,\n   *   east, north]) \u2014 the same two forms `parse` returns, so a caller\n   *   holding a row's `data-row-focus` can hand it straight over. Absent\n   *   is a legitimate case and not a failure: a MANUAL field observation\n   *   carries no coordinate at all, and that create still wants its layer\n   *   switched on.\n   * @returns {void}\n   */\n  function reveal(options) {\n    enable(options && options.overlay);\n\n    var coordinates = options && options.coordinates;\n    if (!coordinates) return;\n\n    var focus = window.pwaMapFocus;\n    if (!focus) return;\n\n    frame(focus, coordinates, null);\n  }\n\n  window.pwaRowFocus = Object.freeze({\n    handleClick: handleClick,\n    parse: parse,\n    reveal: reveal,\n  });\n})();\n"],
  "mappings": "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",
  "names": ["FOCUS_SELECTOR", "BASEMAP_ATTRIBUTE", "BASEMAP_SETTLE_MS", "parse", "value", "parts", "numbers", "i", "basemapSwitchTarget", "key", "menu", "items", "match", "afterBasemapSettles", "then", "settled", "timer", "done", "frame", "focus", "coordinates", "regionId", "enable", "overlay", "handleClick", "event", "options", "target", "button", "picker", "reveal"]
}
