diff --git a/configure/package.json b/configure/package.json index 854d0d6d3..928106a84 100644 --- a/configure/package.json +++ b/configure/package.json @@ -1,6 +1,6 @@ { "name": "configure", - "version": "4.2.13-20260701", + "version": "4.2.14-20260702", "homepage": "./configure/build", "private": true, "dependencies": { diff --git a/package.json b/package.json index a3fd07e42..219d438e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmgis", - "version": "4.2.13-20260701", + "version": "4.2.14-20260702", "description": "A web-based mapping and localization solution for science operation on planetary missions.", "homepage": "build", "repository": { diff --git a/src/essence/Ancillary/ContextMenu.js b/src/essence/Ancillary/ContextMenu.js index c4e802ec0..a58ea0476 100644 --- a/src/essence/Ancillary/ContextMenu.js +++ b/src/essence/Ancillary/ContextMenu.js @@ -97,22 +97,28 @@ function showContextMenuMap(e) { $('#contextMenuMapCopyCoords').on('click', function () { F_.copyToClipboard( JSON.stringify(Coordinates.getAllCoordinates(), null, 2) - ) - $('#contextMenuMapCopyCoords').text('Copied!') - setTimeout(function () { - $('#contextMenuMapCopyCoords').text('Copy Coordinates') - }, 2000) + ).then((copied) => { + if (!copied) return + $('#contextMenuMapCopyCoords').text('Copied!') + setTimeout(function () { + $('#contextMenuMapCopyCoords').text('Copy Coordinates') + }, 2000) + }) }) $('#contextMenuCopyable').on('click', function () { const that = this const key = $(that).attr('key') const copyable = L_._toolCopyables[key] - F_.copyToClipboard(JSON.stringify(copyable.copyable, null, 2)) - $(that).text('Copied!') - setTimeout(function () { - $(that).text(copyable.title) - }, 2000) + F_.copyToClipboard(JSON.stringify(copyable.copyable, null, 2)).then( + (copied) => { + if (!copied) return + $(that).text('Copied!') + setTimeout(function () { + $(that).text(copyable.title) + }, 2000) + } + ) }) contextMenuActionsFull.forEach((c) => { diff --git a/src/essence/Ancillary/QueryURL.js b/src/essence/Ancillary/QueryURL.js index 39aea2df0..288478dec 100644 --- a/src/essence/Ancillary/QueryURL.js +++ b/src/essence/Ancillary/QueryURL.js @@ -4,6 +4,7 @@ import F_ from '../Basics/Formulae_/Formulae_' import L_ from '../Basics/Layers_/Layers_' import T_ from '../Basics/ToolController_/ToolController_' import calls from '../../pre/calls' +import { isStaticBuild } from '../../pre/capabilities' import TimeControl from '../Basics/TimeControl_/TimeControl' import TimeUI from '../Basics/TimeControl_/TimeUI' @@ -50,7 +51,10 @@ var QueryURL = { L_.FUTURES.mapView = [ parseFloat(urlMapLat), parseFloat(urlMapLon), - urlMapZoom !== false ? parseInt(urlMapZoom) : null, + // parseFloat, not parseInt: the modern map zooms fractionally and + // truncation visibly changes the restored view (classic Leaflet + // zooms are integers either way). + urlMapZoom !== false ? parseFloat(urlMapZoom) : null, ] } @@ -277,8 +281,10 @@ var QueryURL = { tools "tools=camp$1.3.4," */ + // Builds and returns the full, self-contained view URL synchronously. This + // is the canonical share-link method (exposed publicly via + // mmgisAPI.writeCoordinateURL). It makes no backend call. writeCoordinateURL: function ( - shortenURL = true, mapLon, mapLat, mapZoom, @@ -288,12 +294,6 @@ var QueryURL = { ) { L_.Viewer_.getLocation() - var callback - if (typeof mapLon === 'function') { - callback = mapLon - mapLon = undefined - } - //Defaults if (mapLon == undefined) mapLon = L_.Map_.map.getCenter().lng if (mapLat == undefined) mapLat = L_.Map_.map.getCenter().lat @@ -358,7 +358,13 @@ var QueryURL = { } //panePercents - var pP = L_.UserInterface_.getPanelPercents() + // getPanelPercents only exists on the classic/mobile UI controllers; the + // modern layout has no such method, so fall back to a map-only split. + var pP = + L_.UserInterface_ && + typeof L_.UserInterface_.getPanelPercents === 'function' + ? L_.UserInterface_.getPanelPercents() + : { viewer: 0, map: 100, globe: 0 } var panePercents = pP.viewer + ',' + pP.map + ',' + pP.globe urlAppendage += '&panePercents=' + panePercents @@ -427,29 +433,34 @@ var QueryURL = { var url = encodeURI(urlAppendage) - if (shortenURL) { - calls.api( - 'shortener_shorten', - { - url: url, - }, - function (s) { - //Set and update the short url - L_.url = - window.location.href.split('?')[0] + '?s=' + s.body.url - window.history.replaceState('', '', L_.url) - if (typeof callback === 'function') callback() - }, - function (e) { - //Set and update the full url - L_.url = window.location.href.split('?')[0] + url - window.history.replaceState('', '', L_.url) - if (typeof callback === 'function') callback() - } - ) + return window.location.href.split('?')[0] + url + }, + getShareURL: function (callback) { + var fullUrl = this.writeCoordinateURL() + + if (isStaticBuild()) { + if (typeof callback === 'function') callback(fullUrl) + return } - return window.location.href.split('?')[0] + url + var baseUrl = window.location.href.split('?')[0] + var urlAppendage = fullUrl.startsWith(baseUrl) + ? fullUrl.substring(baseUrl.length) + : fullUrl.substring(fullUrl.indexOf('?')) + + calls.api( + 'shortener_shorten', + { + url: urlAppendage, + }, + function (s) { + var shortUrl = baseUrl + '?s=' + s.body.url + if (typeof callback === 'function') callback(shortUrl) + }, + function () { + if (typeof callback === 'function') callback(fullUrl) + } + ) }, writeSearchURL: function (searchStrs, searchFile) { return //!!!!!!!!!!!!!!!! diff --git a/src/essence/Basics/Formulae_/Formulae_.js b/src/essence/Basics/Formulae_/Formulae_.js index 0bcf8e5d1..2a0337ebd 100644 --- a/src/essence/Basics/Formulae_/Formulae_.js +++ b/src/essence/Basics/Formulae_/Formulae_.js @@ -1803,18 +1803,18 @@ var Formulae_ = { downloadAnchorNode.click() downloadAnchorNode.remove() }, - downloadCanvas(canvasId, name, callback) { - var link = document.createElement('a') - name = name ? name + '.png' : 'mmgis.png' - link.setAttribute('download', name) - document.getElementById(canvasId).toBlob(function (blob) { - var objUrl = URL.createObjectURL(blob) - link.setAttribute('href', objUrl) - document.body.appendChild(link) - link.click() - link.remove() - if (typeof callback === 'function') callback() - }) + // Downloads a Blob as a file via a transient object URL. Don't convert + // via fetch(dataUrl): the shipped CSP's connect-src blocks the data: scheme. + downloadBlob(blob, filename) { + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.setAttribute('download', filename) + link.setAttribute('href', url) + document.body.appendChild(link) // required for firefox + link.click() + link.remove() + // Deferred: an immediate revoke can abort the download. + setTimeout(() => URL.revokeObjectURL(url), 10000) }, getMinMaxOfArray(arrayOfNumbers) { return { @@ -2043,28 +2043,49 @@ var Formulae_ = { return arr1.filter((e) => arr2.indexOf(e) !== -1) }, /** - * Copies input to user's clipboard + * Copies text to the user's clipboard. Resolves true on success, false on + * failure — never rejects. Tries the async Clipboard API first, falling + * back to a hidden textarea + execCommand('copy') when it is absent + * (insecure origins) OR when it rejects (e.g. an embedding iframe without + * allow="clipboard-write", or a call outside the user-gesture window). * @param {string} text - text to copy to clipboard - * @credit https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f + * @returns {Promise} */ - copyToClipboard(text) { - const el = document.createElement('textarea') // Create a