Configuring Custom.JS
Below is a snippet of the Custom.JS code for your reference
/* place your custom JS Code here. */
const onValidateFailErrorMessage = 'Please upload a file or draw a shape'; //error message to display when the map validation errors
const onUploadFailErrorMessage = 'Something has gone wrong during submission, please check your connection and try again.';
const onIntersectionMessage = 'Intersections / Exclusions have been found, please check your shapes and try again.';
async function clientValidationFunction(executionContext, controlId) {
//user defined validation function, can use the following for getting the list of shapes, annotations and uploaded files.
//if the user needs to fix the shapes, throw an error.
console.log('Validating shapes with:');
console.log(executionContext);
console.log(controlId);
let maptaskrControl = globalThis && globalThis.top && globalThis.top.maptaskrCORE && globalThis.top.maptaskrCORE[controlId];
if (!maptaskrControl) {
console.error('Maptaskr Control not found');
return;
}
//let shapes = _getShapes();
//let annotation = _getAnnotation();
//let uploads = _getUploadedFiles();
//testing shape intersections and determine what to do with them
const shapeIntersections = await maptaskrControl.getShapeIntersections();
if (shapeIntersections && shapeIntersections.length > 0) {
if (shapeIntersections.some((res) => res.intersectionType == 'Warning')) {
//decide what to do with warnings
//if you want them resolved, throw an error here..
//throw new Error(onIntersectionMessage);
}
if (shapeIntersections.some((res) => res.intersectionType == 'Error')) {
//decide what to do with warnings
//if you want them resolved, throw an error here..
throw new Error(onIntersectionMessage);
}
if (shapeIntersections.some((res) => res.intersectionType == 'Exclusion')) {
//decide what to do with warnings
//if you want them resolved, throw an error here..
throw new Error(onIntersectionMessage);
}
}
//you can also test to make sure your shapes are in the correct position, orintation, contained within eachother, any geometric tests here.
//shapes will come in the format:
// {
// "type": "FeatureCollection",
// "features": [
// {
// "type": "Feature",
// "geometry": {
// "type": "Polygon",
// "coordinates": [
// [
// [
// 12899598.276481498,
// -3758060.96802893
// ],
// ...
// ]
// ]
// },
// "properties": {
// "uploadDocType": "Envelope",
// "markerType": "MARKER_SHAPE"
// }
// }
// ],
// "DocumentType": "Envelope",
// "annotationId": "1ffb72d6-c7c3-ed11-83fd-002248e1bcf1",
// "longlat": [
// 12899440.776481498,
// -3758143.46802893
// ],
// "styleProperty": {
// "geometry_": null,
// "fill_": {
// "color_": "rgba(149,255,0,0.1)"
// },
// ...
// }
// }
//if you require a specific subset of objects, please look into the shapes, annotations, or uploads to ensure specific number of shapes or named shapes are included.
}
if (globalThis && globalThis.top) {
globalThis.top.maptaskrReady = function (pageContext, controlId) {
console.log('Maptaskr Map ID: ' + controlId + ' has Loaded');
let maptaskrControl = globalThis && globalThis.top && globalThis.top.maptaskrCORE && globalThis.top.maptaskrCORE[controlId];
if (maptaskrControl) {
/* Use the following console logs to uniquely identify your map */
// console.log(pageContext);
// console.log(maptaskrControl.registeredLocation);
// console.log(maptaskrControl.webresourceLocation);
/* register the correct client validation function here */
maptaskrControl.clientValidationFunction = clientValidationFunction;
/* put your setup methods here */
/* e.g. maptaskrControl.disableSaving = true; - this will disable the inbuilt save methods, you an use maptaskrControl.saveShapes() to save your own shapes.*/
/* put your event registrations here. */
/* e.g. maptaskrControl.on("FeaturesSelected", ...) */
}
};
}
At the very bottom of the file, let's declare two functions that we will use wait for the infobox modal to render and separately to open the record modal dialog when clicking on the record title on the infobox.
function waitForPopup(selector, timeout = 3000) {
return new Promise((resolve, reject) => {
const interval = 100; // Check every 100ms
let elapsedTime = 0;
const checkExistence = () => {
const popup = $(selector);
if (popup.length && popup.find('h2').text().trim().length > 0) {
resolve(popup);
} else if (elapsedTime >= timeout) {
reject(new Error(`Timeout: ${selector} did not meet conditions after ${timeout}ms`));
} else {
elapsedTime += interval;
setTimeout(checkExistence, interval);
}
};
checkExistence();
});
}
function openModalDialog(entityType, entityId) {
let pageInput = { pageType: 'entityrecord', entityName: entityType, formType: 2, entityId: entityId };
let pageOptions = { target: 2, position: 1, width: { value: 50, unit: '%' } };
Xrm.Navigation.navigateTo(pageInput, pageOptions);
}
Now we need to register the appropriate extension function within the maptaskrReady
functional scope. For this, we will use the FeaturesSelected event to ensure we fire the neccessary logic only when the correct feature has been selected. In this case we specifically want this to target features that happen to be custom POI Data (how we internally differentiate between layers and custom data) AND where the entity name is Account
. You can change this entity name to match whichever entity you wish to target.
if (globalThis && globalThis.top) {
globalThis.top.maptaskrReady = function (pageContext, controlId) {
console.log('Maptaskr Map ID: ' + controlId + ' has Loaded');
let maptaskrControl = globalThis && globalThis.top && globalThis.top.maptaskrCORE && globalThis.top.maptaskrCORE[controlId];
if (maptaskrControl) {
maptaskrControl.clientValidationFunction = clientValidationFunction;
maptaskrControl.on('FeaturesSelected', function (featureArray) {
let featureArrayAsObject = JSON.parse(featureArray);
//check that there are features available
if (featureArrayAsObject.length >= 1) {
//wait for the infobox to populate...
waitForPopup('.pointer-popup').then((popup) => {
for (let i = 0; i < featureArrayAsObject.length; i++) {
let feature = featureArrayAsObject[i];
//check that the feature contains data and is of the correct type.
if (feature && feature.attributes && feature.attributes.CustomPoiData) {
let featureData = feature.attributes.CustomPoiData;
if (featureData.entityName == 'account') {
if ($('.pointer-popup h2').text().indexOf(featureData.name) > -1) {
//replace the title with a clickable one and populate a loading text in the details pane
let newTitle = '<h2>';
newTitle += "<a href='#' onclick='openModalDialog(\"" + featureData.entityName + '","' + featureData.entityId + '")\'>';
newTitle += featureData.name;
newTitle += '</a>';
newTitle += '</h2>';
$('.pointer-popup h2').parent().html(newTitle);
let newInfoboxDetails = "<p class='loadingDetails'>";
newInfoboxDetails += 'loading...';
newInfoboxDetails += '</p>'; //add it to the POI Details tab
if ($('.pointer-popup #poiDetailsTab').length > 0) {
$('.pointer-popup #poiDetailsTab .tools__row').html(newInfoboxDetails);
} else {
$('.pointer-popup').append(newInfoboxDetails);
} //load all additional data you wish.
// Define the fields you want to retrieve from the entity
const fieldsToRetrieve = ['address1_line1', 'address1_city', 'description'];
// Replace with your actual field logical names
// Retrieve the entity record using the Xrm.WebApi.retrieveRecord function
Xrm.WebApi.retrieveRecord(featureData.entityName, featureData.entityId, `?$select=${fieldsToRetrieve.join(',')}`).then(function (result) {
//populate the infobox with your desired column data
newInfoboxDetails = "<p class='details'>";
newInfoboxDetails += 'Address: ' + result.address1_composite + '<br />';
newInfoboxDetails += 'City: ' + result.address1_city + '<br />';
newInfoboxDetails += 'Description: ' + result.description + '<br />';
newInfoboxDetails += '</p>';
$('.pointer-popup .loadingDetails').html(newInfoboxDetails);
});
}
}
}
}
});
}
});
}
};
}
Let's see this code in action!
First we turn on the datasource layer (in this case Accounts
) and wait for it to load. Then we will select one of the pins that has loaded on the map. We should see that while the desired data is being retrieved, the details pane changes to say loading...
and then be replaced with said data.
As you can see below we're now seeing the additional field data within the infobox as expected.
Additionally, we can now click on the title of the infobox and this will open the record in a modal dialog.
Below are gifs demonstrating this functionality in Dashboard and Record-Level respectively:
Success! Using the code snippets above we have successfully listened to the FeaturesSelected
event and extended it to modify the infobox element to be populated with record data and allow us to interact with the title to separately open the record.