diff --git a/css/svg/material-icons.svg b/css/svg/material-icons.svg
index 9cc5d6245..001660226 100644
--- a/css/svg/material-icons.svg
+++ b/css/svg/material-icons.svg
@@ -62,4 +62,5 @@
+
diff --git a/scss/_infowindow.scss b/scss/_infowindow.scss
index 3325a9ec3..12a1ce376 100644
--- a/scss/_infowindow.scss
+++ b/scss/_infowindow.scss
@@ -239,6 +239,10 @@
text-align: left;
}
+.export-response-container {
+ margin: 0.5em 0.5em 0.5em 3.6em;
+}
+
.toaster {
border-radius: 0.5em;
box-shadow: 0 0 8px #888;
diff --git a/src/infowindow.js b/src/infowindow.js
index 8b3210841..7d684cd5c 100644
--- a/src/infowindow.js
+++ b/src/infowindow.js
@@ -10,6 +10,7 @@ let exportContainer;
let groupFooterContainer;
let sublists;
let subexports;
+let subexportResponses;
let urvalElements;
let footerContainers;
let expandableContents;
@@ -203,6 +204,8 @@ function showSelectedList(selectionGroup) {
}
const subexportToAppend = subexports.get(selectionGroup);
exportContainer.appendChild(subexportToAppend);
+ const subexportResponseToAppend = subexportResponses.get(selectionGroup);
+ exportContainer.appendChild(subexportResponseToAppend);
selectionManager.clearHighlightedFeatures();
selectionManager.refreshAllLayers();
urvalElements.forEach((value, key) => {
@@ -238,8 +241,18 @@ function createUrvalElement(selectionGroup, selectionGroupTitle) {
const footerContainer = document.createElement('div');
footerContainers.set(selectionGroup, footerContainer);
- const subexportComponent = createSubexportComponent({ selectionGroup, viewer, exportOptions });
+ // Updates the response content for the given selectionGroup
+ const responseHandler = function responseHandler(responseSelectionGroup, text) {
+ const responseContainer = subexportResponses.get(responseSelectionGroup);
+ responseContainer.innerHTML = text;
+ };
+
+ const subexportComponent = createSubexportComponent({ selectionGroup, viewer, exportOptions, responseHandler });
subexports.set(selectionGroup, subexportComponent);
+
+ const subexportResponseComponent = document.createElement('div');
+ subexportResponseComponent.classList.add('export-response-container');
+ subexportResponses.set(selectionGroup, subexportResponseComponent);
}
function createExpandableContent(listElementContentContainer, content, elementId) {
@@ -427,6 +440,7 @@ function init(options) {
exportOptions = infowindowOptions.export || {};
sublists = new Map();
subexports = new Map();
+ subexportResponses = new Map();
urvalElements = new Map();
expandableContents = new Map();
footerContainers = new Map();
diff --git a/src/infowindow_exporthandler.js b/src/infowindow_exporthandler.js
index a10dcd0ba..bfb127684 100644
--- a/src/infowindow_exporthandler.js
+++ b/src/infowindow_exporthandler.js
@@ -52,7 +52,46 @@ export function simpleExportHandler(simpleExportUrl, activeLayer, selectedItems,
});
}
-export function layerSpecificExportHandler(url, activeLayer, selectedItems, attributesToSendToExport, exportedFileName) {
+/**
+ * Makes a HEAD request to find out what the content type of the response will be, so that the
+ * response can be handled accordingly. Non-images will be blocked by CORS restrictions unless
+ * the server/API is set to allow the client's origin.
+ * @param {string} url The url to fetch
+ * @returns {Promise} HTML content containing the response
+ */
+async function fetchByContentTypes(url) {
+ try {
+ // Perform the HEAD request to check response content type
+ const headResponse = await fetch(url, { method: 'HEAD' });
+ if (!headResponse.ok) {
+ throw new Error(`HEAD request failed with status: ${headResponse.status}`);
+ }
+ // Get the content-type header
+ const contentType = headResponse.headers.get('Content-Type');
+
+ // Generate content to display based on Content-Type
+ if (contentType.startsWith('image/')) {
+ return ``;
+ } else if (contentType.startsWith('text/plain') || contentType.startsWith('application/json')) {
+ const getResponse = await fetch(url, { method: 'GET' });
+ if (!getResponse.ok) {
+ throw new Error(`GET request failed with status: ${getResponse.status}`);
+ }
+ const responseText = await getResponse.text();
+ if (contentType.startsWith('text/plain')) {
+ return `${responseText}`;
+ } else if (contentType.startsWith('application/json')) {
+ return `${responseText}
`;
+ }
+ }
+ return 'Unsupported response Content-Type';
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+}
+
+export function layerSpecificExportHandler(url, requestMethod, urlParameters, activeLayer, selectedItems, attributesToSendToExport, exportedFileName) {
if (!url) {
throw new Error('Export URL is not specified.');
}
@@ -80,8 +119,39 @@ export function layerSpecificExportHandler(url, activeLayer, selectedItems, attr
features[layerName].push(obj);
});
+ // Generates the request URL using the urlParameters config option.
+ // Keys and values are translated to url parameters as is, except when a value is an object with
+ // an "attribute" property, in which case the value will be a list of the values from the
+ // corresponding attribute of the selectedItems. Unless specified with a "separator" property,
+ // the list will be separated by semicolons.
+ // Specifying a value as "{{no_value}}" will add a valueless parameter, e g "?Param1&Param2&etc".
+ let requestUrl = url;
+ const requestParams = { ...urlParameters };
+ if (requestParams) {
+ Object.keys(requestParams).forEach((param) => {
+ if (requestParams[param] && typeof requestParams[param] === 'object' && requestParams[param].attribute) {
+ const attributeValues = [];
+ selectedItems.forEach((item) => {
+ const attributeValue = item.getFeature().get(requestParams[param].attribute);
+ if (attributeValue) {
+ attributeValues.push(attributeValue);
+ }
+ });
+ requestParams[param] = attributeValues.join(requestParams[param].separator || ';');
+ }
+ });
+ requestUrl = new URL(url);
+ requestUrl.search = new URLSearchParams(requestParams);
+ requestUrl = requestUrl.toString().replace(/=%7B%7Bno_value%7D%7D/gm, '');
+ }
+
+ if (requestMethod === 'OPEN') {
+ return window.open(requestUrl, '_blank') ? Promise.resolve() : Promise.reject();
+ } else if (requestMethod === 'GET') {
+ return fetchByContentTypes(requestUrl);
+ }
// eslint-disable-next-line consistent-return
- return fetch(url, {
+ return fetch(requestUrl, {
method: 'POST', // or 'PUT'
body: JSON.stringify(features), // data can be `string` or {object}!
headers: {
@@ -239,23 +309,33 @@ function createToaster(status, exportOptions, message) {
function createExportButtons(
obj,
+ buttonPerLayer,
+ requestMethodPerLayer,
+ urlParametersPerLayer,
attributesToSendToExportPerLayer,
+ exportedFileNamePerLayer,
+ displayExportResponsePerLayer,
selectionGroup,
activeLayer,
selectionManager,
- exportOptions
+ exportOptions,
+ responseHandler
) {
- const roundButton = obj.button.roundButton || false;
- const buttonText = obj.button.buttonText || defaultText;
const url = obj.url;
- const layerSpecificExportedFileName = obj.exportedFileName;
- const attributesToSendToExport = obj.attributesToSendToExport
- ? obj.attributesToSendToExport
- : attributesToSendToExportPerLayer;
+ const buttonText = obj.button?.buttonText || buttonPerLayer?.buttonText || defaultText;
+ const roundButton = obj.button?.roundButton ?? buttonPerLayer?.roundButton ?? false;
+ const roundButtonIcon = obj.button?.roundButtonIcon || buttonPerLayer?.roundButtonIcon || defaultIcon;
+ const roundButtonTooltipText = obj.button?.roundButtonTooltipText || buttonPerLayer?.roundButtonTooltipText || defaultText;
+ const requestMethod = obj.requestMethod || requestMethodPerLayer || 'POST_JSON';
+ const urlParameters = obj.urlParameters || urlParametersPerLayer;
+ const attributesToSendToExport = obj.attributesToSendToExport || attributesToSendToExportPerLayer;
+ const exportedFileName = obj.exportedFileName || exportedFileNamePerLayer;
+ const displayExportResponse = obj.displayExportResponse || displayExportResponsePerLayer || false;
+
const exportBtn = roundButton
? createCustomExportButton(
- obj.button.roundButtonIcon || defaultIcon,
- obj.button.roundButtonTooltipText || defaultText
+ roundButtonIcon,
+ roundButtonTooltipText
)
: createExportButton(buttonText);
const btn = exportBtn.querySelector('button');
@@ -268,10 +348,12 @@ function createExportButtons(
const selectedItems = selectionManager.getSelectedItemsForASelectionGroup(selectionGroup);
layerSpecificExportHandler(
url,
+ requestMethod,
+ urlParameters,
activeLayer,
selectedItems,
attributesToSendToExport,
- layerSpecificExportedFileName
+ exportedFileName
)
.then((data) => {
if (data) {
@@ -285,6 +367,9 @@ function createExportButtons(
default:
break;
}
+ if (requestMethod === 'GET' && displayExportResponse) {
+ responseHandler(selectionGroup, data);
+ }
}
btn.loadStop();
})
@@ -297,7 +382,7 @@ function createExportButtons(
return exportBtn;
}
-export function createSubexportComponent({ selectionGroup, viewer, exportOptions }) {
+export function createSubexportComponent({ selectionGroup, viewer, exportOptions, responseHandler }) {
viewerId = viewer.getId();
const selectionManager = viewer.getSelectionManager();
// OBS! selectionGroup corresponds to a layer with the same name in most cases, but in case of a group layer it can contain selected items from all the layers in that GroupLayer.
@@ -314,36 +399,31 @@ export function createSubexportComponent({ selectionGroup, viewer, exportOptions
}
if (layerSpecificExportOptions) {
const exportUrls = layerSpecificExportOptions.exportUrls || [];
+ const buttonPerLayer = layerSpecificExportOptions.button;
+ const requestMethodPerLayer = layerSpecificExportOptions.requestMethod;
+ const urlParametersPerLayer = layerSpecificExportOptions.urlParameters;
const attributesToSendToExportPerLayer = layerSpecificExportOptions.attributesToSendToExport;
- const customButtonExportUrls = exportUrls.filter(
- (e) => e.button.roundButton
- );
- const standardButtonExportUrls = exportUrls.filter(
- (e) => !e.button.roundButton
- );
-
- customButtonExportUrls.forEach((obj) => {
- const button = createExportButtons(
- obj,
- attributesToSendToExportPerLayer,
- selectionGroup,
- activeLayer,
- selectionManager,
- exportOptions
- );
- subexportContainer.appendChild(button);
- });
- standardButtonExportUrls.forEach((obj) => {
- const button = createExportButtons(
- obj,
- attributesToSendToExportPerLayer,
- selectionGroup,
- activeLayer,
- selectionManager,
- exportOptions
- );
- subexportContainer.appendChild(button);
- });
+ const exportedFileNamePerLayer = layerSpecificExportOptions.exportedFileName;
+ const displayExportResponsePerLayer = layerSpecificExportOptions.displayExportResponse;
+
+ exportUrls.sort((exportUrl) => (exportUrl.button.roundButton ? -1 : 1))
+ .forEach((obj) => {
+ const button = createExportButtons(
+ obj,
+ buttonPerLayer,
+ requestMethodPerLayer,
+ urlParametersPerLayer,
+ attributesToSendToExportPerLayer,
+ exportedFileNamePerLayer,
+ displayExportResponsePerLayer,
+ selectionGroup,
+ activeLayer,
+ selectionManager,
+ exportOptions,
+ responseHandler
+ );
+ subexportContainer.appendChild(button);
+ });
}
if (exportOptions.simpleExport && exportOptions.simpleExport.url) {
const simpleExport = exportOptions.simpleExport;